Isolate gateway secrets per profile slot
This commit is contained in:
parent
1152b4f8fa
commit
024f485a36
@ -94,8 +94,9 @@ class AppController extends ChangeNotifier {
|
||||
(_isFlutterTestEnvironment
|
||||
? const <String>[]
|
||||
: _defaultGatewayOnlySkillScanRoots);
|
||||
_gatewayAcpClient =
|
||||
GatewayAcpClient(endpointResolver: _resolveGatewayAcpEndpoint);
|
||||
_gatewayAcpClient = GatewayAcpClient(
|
||||
endpointResolver: _resolveGatewayAcpEndpoint,
|
||||
);
|
||||
_singleAgentAppServerClient = DirectSingleAgentAppServerClient(
|
||||
endpointResolver: _resolveSingleAgentEndpoint,
|
||||
);
|
||||
@ -283,15 +284,15 @@ class AppController extends ChangeNotifier {
|
||||
AssistantPermissionLevel get assistantPermissionLevel =>
|
||||
settings.assistantPermissionLevel;
|
||||
bool get hasStoredGatewayCredential =>
|
||||
_settingsController.secureRefs.containsKey('gateway_token') ||
|
||||
_settingsController.secureRefs.containsKey('gateway_password') ||
|
||||
hasStoredGatewayTokenForProfile(_activeGatewayProfileIndex) ||
|
||||
hasStoredGatewayPasswordForProfile(_activeGatewayProfileIndex) ||
|
||||
_settingsController.secureRefs.containsKey(
|
||||
'gateway_device_token_operator',
|
||||
);
|
||||
bool get hasStoredGatewayToken =>
|
||||
_settingsController.secureRefs.containsKey('gateway_token');
|
||||
hasStoredGatewayTokenForProfile(_activeGatewayProfileIndex);
|
||||
String? get storedGatewayTokenMask =>
|
||||
_settingsController.secureRefs['gateway_token'];
|
||||
storedGatewayTokenMaskForProfile(_activeGatewayProfileIndex);
|
||||
String get aiGatewayUrl => settings.aiGateway.baseUrl.trim();
|
||||
bool get hasStoredAiGatewayApiKey =>
|
||||
_settingsController.secureRefs.containsKey('ai_gateway_api_key');
|
||||
@ -311,8 +312,6 @@ class AppController extends ChangeNotifier {
|
||||
bool get isMultiAgentRunPending => _multiAgentRunPending;
|
||||
bool _desktopPlatformBusy = false;
|
||||
|
||||
static const String _draftGatewayTokenKey = 'gateway_token';
|
||||
static const String _draftGatewayPasswordKey = 'gateway_password';
|
||||
static const String _draftAiGatewayApiKeyKey = 'ai_gateway_api_key';
|
||||
static const String _draftVaultTokenKey = 'vault_token';
|
||||
static const String _draftOllamaApiKeyKey = 'ollama_cloud_api_key';
|
||||
@ -325,6 +324,26 @@ class AppController extends ChangeNotifier {
|
||||
hasStoredAiGatewayApiKey &&
|
||||
resolvedAiGatewayModel.isNotEmpty;
|
||||
|
||||
int get _activeGatewayProfileIndex {
|
||||
final target = currentAssistantExecutionTarget;
|
||||
if (target == AssistantExecutionTarget.singleAgent) {
|
||||
return kGatewayRemoteProfileIndex;
|
||||
}
|
||||
return _gatewayProfileIndexForExecutionTarget(target);
|
||||
}
|
||||
|
||||
bool hasStoredGatewayTokenForProfile(int profileIndex) =>
|
||||
_settingsController.hasStoredGatewayTokenForProfile(profileIndex);
|
||||
|
||||
bool hasStoredGatewayPasswordForProfile(int profileIndex) =>
|
||||
_settingsController.hasStoredGatewayPasswordForProfile(profileIndex);
|
||||
|
||||
String? storedGatewayTokenMaskForProfile(int profileIndex) =>
|
||||
_settingsController.storedGatewayTokenMaskForProfile(profileIndex);
|
||||
|
||||
String? storedGatewayPasswordMaskForProfile(int profileIndex) =>
|
||||
_settingsController.storedGatewayPasswordMaskForProfile(profileIndex);
|
||||
|
||||
List<SingleAgentProvider> get availableSingleAgentProviders =>
|
||||
(_availableSingleAgentProvidersOverride ??
|
||||
const <SingleAgentProvider>[SingleAgentProvider.codex])
|
||||
@ -1342,7 +1361,15 @@ class AppController extends ChangeNotifier {
|
||||
final resolvedPassword = password.trim().isNotEmpty
|
||||
? password.trim()
|
||||
: (decoded?.password.trim() ?? '');
|
||||
final resolvedProfileIndex = _gatewayProfileIndexForExecutionTarget(
|
||||
_assistantExecutionTargetForMode(
|
||||
_modeFromHost(
|
||||
decoded?.host ?? settings.primaryRemoteGatewayProfile.host,
|
||||
),
|
||||
),
|
||||
);
|
||||
await _settingsController.saveGatewaySecrets(
|
||||
profileIndex: resolvedProfileIndex,
|
||||
token: resolvedToken,
|
||||
password: resolvedPassword,
|
||||
);
|
||||
@ -1378,6 +1405,7 @@ class AppController extends ChangeNotifier {
|
||||
);
|
||||
await _connectProfile(
|
||||
nextProfile,
|
||||
profileIndex: resolvedProfileIndex,
|
||||
authTokenOverride: resolvedToken,
|
||||
authPasswordOverride: resolvedPassword,
|
||||
);
|
||||
@ -1392,7 +1420,10 @@ class AppController extends ChangeNotifier {
|
||||
String token = '',
|
||||
String password = '',
|
||||
}) async {
|
||||
final nextTarget = _assistantExecutionTargetForMode(mode);
|
||||
final nextProfileIndex = _gatewayProfileIndexForExecutionTarget(nextTarget);
|
||||
await _settingsController.saveGatewaySecrets(
|
||||
profileIndex: nextProfileIndex,
|
||||
token: token.trim(),
|
||||
password: password.trim(),
|
||||
);
|
||||
@ -1403,7 +1434,6 @@ class AppController extends ChangeNotifier {
|
||||
final resolvedPort = mode == RuntimeConnectionMode.local && port <= 0
|
||||
? 18789
|
||||
: port;
|
||||
final nextTarget = _assistantExecutionTargetForMode(mode);
|
||||
final nextProfile = _gatewayProfileForAssistantExecutionTarget(nextTarget)
|
||||
.copyWith(
|
||||
mode: mode,
|
||||
@ -1429,6 +1459,7 @@ class AppController extends ChangeNotifier {
|
||||
);
|
||||
await _connectProfile(
|
||||
nextProfile,
|
||||
profileIndex: nextProfileIndex,
|
||||
authTokenOverride: token.trim(),
|
||||
authPasswordOverride: password.trim(),
|
||||
);
|
||||
@ -1456,11 +1487,17 @@ class AppController extends ChangeNotifier {
|
||||
if (target == AssistantExecutionTarget.singleAgent) {
|
||||
return;
|
||||
}
|
||||
await _connectProfile(_gatewayProfileForAssistantExecutionTarget(target));
|
||||
await _connectProfile(
|
||||
_gatewayProfileForAssistantExecutionTarget(target),
|
||||
profileIndex: _gatewayProfileIndexForExecutionTarget(target),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> clearStoredGatewayToken() async {
|
||||
await _settingsController.clearGatewaySecrets(token: true);
|
||||
Future<void> clearStoredGatewayToken({int? profileIndex}) async {
|
||||
await _settingsController.clearGatewaySecrets(
|
||||
profileIndex: profileIndex,
|
||||
token: true,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> refreshGatewayHealth() async {
|
||||
@ -1808,7 +1845,10 @@ class AppController extends ChangeNotifier {
|
||||
resolvedTarget,
|
||||
);
|
||||
try {
|
||||
await _connectProfile(targetProfile);
|
||||
await _connectProfile(
|
||||
targetProfile,
|
||||
profileIndex: _gatewayProfileIndexForExecutionTarget(resolvedTarget),
|
||||
);
|
||||
} catch (_) {
|
||||
// Keep the selected execution target even when the immediate reconnect
|
||||
// fails so the user can retry or adjust gateway settings manually.
|
||||
@ -2147,12 +2187,12 @@ class AppController extends ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void saveGatewayTokenDraft(String value) {
|
||||
_saveSecretDraft(_draftGatewayTokenKey, value);
|
||||
void saveGatewayTokenDraft(String value, {required int profileIndex}) {
|
||||
_saveSecretDraft(_draftGatewayTokenKey(profileIndex), value);
|
||||
}
|
||||
|
||||
void saveGatewayPasswordDraft(String value) {
|
||||
_saveSecretDraft(_draftGatewayPasswordKey, value);
|
||||
void saveGatewayPasswordDraft(String value, {required int profileIndex}) {
|
||||
_saveSecretDraft(_draftGatewayPasswordKey(profileIndex), value);
|
||||
}
|
||||
|
||||
void saveAiGatewayApiKeyDraft(String value) {
|
||||
@ -2682,7 +2722,10 @@ class AppController extends ChangeNotifier {
|
||||
startupProfile.setupCode.trim().isNotEmpty;
|
||||
if (shouldAutoConnect) {
|
||||
try {
|
||||
await _connectProfile(startupProfile);
|
||||
await _connectProfile(
|
||||
startupProfile,
|
||||
profileIndex: _gatewayProfileIndexForExecutionTarget(startupTarget),
|
||||
);
|
||||
} catch (_) {
|
||||
// Keep the shell usable when auto-connect fails.
|
||||
}
|
||||
@ -2711,11 +2754,13 @@ class AppController extends ChangeNotifier {
|
||||
|
||||
Future<void> _connectProfile(
|
||||
GatewayConnectionProfile profile, {
|
||||
int? profileIndex,
|
||||
String authTokenOverride = '',
|
||||
String authPasswordOverride = '',
|
||||
}) async {
|
||||
await _runtime.connectProfile(
|
||||
profile,
|
||||
profileIndex: profileIndex,
|
||||
authTokenOverride: authTokenOverride,
|
||||
authPasswordOverride: authPasswordOverride,
|
||||
);
|
||||
@ -2755,6 +2800,9 @@ class AppController extends ChangeNotifier {
|
||||
SettingsSnapshot previous,
|
||||
SettingsSnapshot next,
|
||||
) {
|
||||
final hasGatewaySecretDraft = _draftSecretValues.keys.any(
|
||||
(key) => _isGatewayDraftKey(key),
|
||||
);
|
||||
final gatewayChanged =
|
||||
jsonEncode(
|
||||
previous.gatewayProfiles.map((item) => item.toJson()).toList(),
|
||||
@ -2763,8 +2811,7 @@ class AppController extends ChangeNotifier {
|
||||
next.gatewayProfiles.map((item) => item.toJson()).toList(),
|
||||
) ||
|
||||
previous.assistantExecutionTarget != next.assistantExecutionTarget ||
|
||||
_draftSecretValues.containsKey(_draftGatewayTokenKey) ||
|
||||
_draftSecretValues.containsKey(_draftGatewayPasswordKey);
|
||||
hasGatewaySecretDraft;
|
||||
final aiGatewayChanged =
|
||||
previous.aiGateway.toJson().toString() !=
|
||||
next.aiGateway.toJson().toString() ||
|
||||
@ -2775,13 +2822,18 @@ class AppController extends ChangeNotifier {
|
||||
}
|
||||
|
||||
Future<void> _persistDraftSecrets() async {
|
||||
final gatewayToken = _draftSecretValues[_draftGatewayTokenKey];
|
||||
final gatewayPassword = _draftSecretValues[_draftGatewayPasswordKey];
|
||||
if ((gatewayToken ?? '').isNotEmpty || (gatewayPassword ?? '').isNotEmpty) {
|
||||
await _settingsController.saveGatewaySecrets(
|
||||
token: gatewayToken ?? '',
|
||||
password: gatewayPassword ?? '',
|
||||
);
|
||||
for (var index = 0; index < kGatewayProfileListLength; index += 1) {
|
||||
final gatewayToken = _draftSecretValues[_draftGatewayTokenKey(index)];
|
||||
final gatewayPassword =
|
||||
_draftSecretValues[_draftGatewayPasswordKey(index)];
|
||||
if ((gatewayToken ?? '').isNotEmpty ||
|
||||
(gatewayPassword ?? '').isNotEmpty) {
|
||||
await _settingsController.saveGatewaySecrets(
|
||||
profileIndex: index,
|
||||
token: gatewayToken ?? '',
|
||||
password: gatewayPassword ?? '',
|
||||
);
|
||||
}
|
||||
}
|
||||
final aiGatewayApiKey = _draftSecretValues[_draftAiGatewayApiKeyKey];
|
||||
if ((aiGatewayApiKey ?? '').isNotEmpty) {
|
||||
@ -2798,6 +2850,15 @@ class AppController extends ChangeNotifier {
|
||||
_draftSecretValues.clear();
|
||||
}
|
||||
|
||||
static String _draftGatewayTokenKey(int profileIndex) =>
|
||||
'gateway_token_$profileIndex';
|
||||
|
||||
static String _draftGatewayPasswordKey(int profileIndex) =>
|
||||
'gateway_password_$profileIndex';
|
||||
|
||||
static bool _isGatewayDraftKey(String key) =>
|
||||
key.startsWith('gateway_token_') || key.startsWith('gateway_password_');
|
||||
|
||||
Future<void> _persistSettingsSnapshot(SettingsSnapshot snapshot) async {
|
||||
final sanitized = _sanitizeFeatureFlagSettings(
|
||||
_sanitizeMultiAgentSettings(
|
||||
@ -4252,10 +4313,7 @@ class AppController extends ChangeNotifier {
|
||||
return appText('无法连接到 LLM API。', 'Unable to reach the LLM API.');
|
||||
}
|
||||
if (error is HandshakeException) {
|
||||
return appText(
|
||||
'LLM API TLS 握手失败。',
|
||||
'LLM API TLS handshake failed.',
|
||||
);
|
||||
return appText('LLM API TLS 握手失败。', 'LLM API TLS handshake failed.');
|
||||
}
|
||||
if (error is TimeoutException) {
|
||||
return appText('LLM API 请求超时。', 'LLM API request timed out.');
|
||||
@ -4746,7 +4804,9 @@ class AppController extends ChangeNotifier {
|
||||
_sessionsController.currentSessionKey,
|
||||
);
|
||||
if (target == AssistantExecutionTarget.singleAgent) {
|
||||
final remote = _gatewayProfileBaseUri(settings.primaryRemoteGatewayProfile);
|
||||
final remote = _gatewayProfileBaseUri(
|
||||
settings.primaryRemoteGatewayProfile,
|
||||
);
|
||||
if (remote != null) {
|
||||
return remote;
|
||||
}
|
||||
|
||||
@ -49,8 +49,8 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
late final TextEditingController _gatewaySetupCodeController;
|
||||
late final TextEditingController _gatewayHostController;
|
||||
late final TextEditingController _gatewayPortController;
|
||||
late final TextEditingController _gatewayTokenController;
|
||||
late final TextEditingController _gatewayPasswordController;
|
||||
late final List<TextEditingController> _gatewayTokenControllers;
|
||||
late final List<TextEditingController> _gatewayPasswordControllers;
|
||||
late final TextEditingController _vaultTokenController;
|
||||
late final TextEditingController _ollamaApiKeyController;
|
||||
late final TextEditingController _runtimeLogFilterController;
|
||||
@ -65,8 +65,8 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
String _gatewaySetupCodeSyncedValue = '';
|
||||
String _gatewayHostSyncedValue = '';
|
||||
String _gatewayPortSyncedValue = '';
|
||||
_SecretFieldUiState _gatewayTokenState = const _SecretFieldUiState();
|
||||
_SecretFieldUiState _gatewayPasswordState = const _SecretFieldUiState();
|
||||
late final List<_SecretFieldUiState> _gatewayTokenStates;
|
||||
late final List<_SecretFieldUiState> _gatewayPasswordStates;
|
||||
bool _aiGatewayTesting = false;
|
||||
String _aiGatewayTestState = 'idle';
|
||||
String _aiGatewayTestMessage = '';
|
||||
@ -94,8 +94,26 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
_gatewaySetupCodeController = TextEditingController();
|
||||
_gatewayHostController = TextEditingController();
|
||||
_gatewayPortController = TextEditingController();
|
||||
_gatewayTokenController = TextEditingController();
|
||||
_gatewayPasswordController = TextEditingController();
|
||||
_gatewayTokenControllers = List<TextEditingController>.generate(
|
||||
kGatewayProfileListLength,
|
||||
(_) => TextEditingController(),
|
||||
growable: false,
|
||||
);
|
||||
_gatewayPasswordControllers = List<TextEditingController>.generate(
|
||||
kGatewayProfileListLength,
|
||||
(_) => TextEditingController(),
|
||||
growable: false,
|
||||
);
|
||||
_gatewayTokenStates = List<_SecretFieldUiState>.filled(
|
||||
kGatewayProfileListLength,
|
||||
const _SecretFieldUiState(),
|
||||
growable: false,
|
||||
);
|
||||
_gatewayPasswordStates = List<_SecretFieldUiState>.filled(
|
||||
kGatewayProfileListLength,
|
||||
const _SecretFieldUiState(),
|
||||
growable: false,
|
||||
);
|
||||
_vaultTokenController = TextEditingController();
|
||||
_ollamaApiKeyController = TextEditingController();
|
||||
_runtimeLogFilterController = TextEditingController();
|
||||
@ -125,8 +143,12 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
_gatewaySetupCodeController.dispose();
|
||||
_gatewayHostController.dispose();
|
||||
_gatewayPortController.dispose();
|
||||
_gatewayTokenController.dispose();
|
||||
_gatewayPasswordController.dispose();
|
||||
for (final controller in _gatewayTokenControllers) {
|
||||
controller.dispose();
|
||||
}
|
||||
for (final controller in _gatewayPasswordControllers) {
|
||||
controller.dispose();
|
||||
}
|
||||
_vaultTokenController.dispose();
|
||||
_ollamaApiKeyController.dispose();
|
||||
_runtimeLogFilterController.dispose();
|
||||
@ -864,10 +886,7 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
appText(
|
||||
'自定义连接源 ${_selectedLlmEndpointIndex + 1}',
|
||||
'Custom source ${_selectedLlmEndpointIndex + 1}',
|
||||
),
|
||||
appText('连接源详情', 'Source details'),
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
@ -889,14 +908,16 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
SettingsSnapshot settings,
|
||||
int index,
|
||||
) {
|
||||
final configured = _isLlmEndpointSlotConfigured(
|
||||
controller,
|
||||
settings,
|
||||
_llmEndpointSlots[index],
|
||||
);
|
||||
final slot = _llmEndpointSlots[index];
|
||||
final configured = _isLlmEndpointSlotConfigured(controller, settings, slot);
|
||||
final label = switch (slot) {
|
||||
_LlmEndpointSlot.aiGateway => appText('主 LLM API', 'Primary LLM API'),
|
||||
_LlmEndpointSlot.ollamaLocal => appText('Ollama 本地', 'Ollama Local'),
|
||||
_LlmEndpointSlot.ollamaCloud => appText('Ollama Cloud', 'Ollama Cloud'),
|
||||
};
|
||||
return appText(
|
||||
'自定义连接源 ${index + 1}(${configured ? '已配置' : '空'})',
|
||||
'Custom source ${index + 1} (${configured ? 'Configured' : 'Empty'})',
|
||||
configured ? label : '$label(空)',
|
||||
configured ? label : '$label (empty)',
|
||||
);
|
||||
}
|
||||
|
||||
@ -1001,6 +1022,12 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
selectedProfileIndex,
|
||||
gatewayProfile,
|
||||
);
|
||||
final gatewayTokenController =
|
||||
_gatewayTokenControllers[selectedProfileIndex];
|
||||
final gatewayPasswordController =
|
||||
_gatewayPasswordControllers[selectedProfileIndex];
|
||||
final gatewayTokenState = _gatewayTokenStates[selectedProfileIndex];
|
||||
final gatewayPasswordState = _gatewayPasswordStates[selectedProfileIndex];
|
||||
final uiFeatures = controller.featuresFor(
|
||||
resolveUiFeaturePlatformFromContext(context),
|
||||
);
|
||||
@ -1011,9 +1038,11 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
final gatewayTls = gatewayMode == RuntimeConnectionMode.local
|
||||
? false
|
||||
: gatewayProfile.tls;
|
||||
final hasStoredGatewayToken = controller.hasStoredGatewayToken;
|
||||
final hasStoredGatewayPassword =
|
||||
controller.settingsController.secureRefs['gateway_password'] != null;
|
||||
final hasStoredGatewayToken = controller.hasStoredGatewayTokenForProfile(
|
||||
selectedProfileIndex,
|
||||
);
|
||||
final hasStoredGatewayPassword = controller
|
||||
.hasStoredGatewayPasswordForProfile(selectedProfileIndex);
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@ -1163,13 +1192,19 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
const SizedBox(height: 16),
|
||||
_buildSecureField(
|
||||
fieldKey: const ValueKey('gateway-shared-token-field'),
|
||||
controller: _gatewayTokenController,
|
||||
controller: gatewayTokenController,
|
||||
label: appText('共享 Token', 'Shared Token'),
|
||||
hasStoredValue: hasStoredGatewayToken,
|
||||
fieldState: _gatewayTokenState,
|
||||
onStateChanged: (value) => setState(() => _gatewayTokenState = value),
|
||||
loadValue: controller.settingsController.loadGatewayToken,
|
||||
onSubmitted: (value) async => controller.saveGatewayTokenDraft(value),
|
||||
fieldState: gatewayTokenState,
|
||||
onStateChanged: (value) =>
|
||||
setState(() => _gatewayTokenStates[selectedProfileIndex] = value),
|
||||
loadValue: () => controller.settingsController.loadGatewayToken(
|
||||
profileIndex: selectedProfileIndex,
|
||||
),
|
||||
onSubmitted: (value) async => controller.saveGatewayTokenDraft(
|
||||
value,
|
||||
profileIndex: selectedProfileIndex,
|
||||
),
|
||||
storedHelperText: appText(
|
||||
'已安全保存,默认以 **** 显示;可直接测试,也可通过本区保存/应用提交。',
|
||||
'Stored securely. Test directly or submit with local Save / Apply actions.',
|
||||
@ -1182,15 +1217,20 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
const SizedBox(height: 12),
|
||||
_buildSecureField(
|
||||
fieldKey: const ValueKey('gateway-password-field'),
|
||||
controller: _gatewayPasswordController,
|
||||
controller: gatewayPasswordController,
|
||||
label: appText('密码', 'Password'),
|
||||
hasStoredValue: hasStoredGatewayPassword,
|
||||
fieldState: _gatewayPasswordState,
|
||||
onStateChanged: (value) =>
|
||||
setState(() => _gatewayPasswordState = value),
|
||||
loadValue: controller.settingsController.loadGatewayPassword,
|
||||
onSubmitted: (value) async =>
|
||||
controller.saveGatewayPasswordDraft(value),
|
||||
fieldState: gatewayPasswordState,
|
||||
onStateChanged: (value) => setState(
|
||||
() => _gatewayPasswordStates[selectedProfileIndex] = value,
|
||||
),
|
||||
loadValue: () => controller.settingsController.loadGatewayPassword(
|
||||
profileIndex: selectedProfileIndex,
|
||||
),
|
||||
onSubmitted: (value) async => controller.saveGatewayPasswordDraft(
|
||||
value,
|
||||
profileIndex: selectedProfileIndex,
|
||||
),
|
||||
storedHelperText: appText(
|
||||
'已安全保存,默认以 **** 显示;可直接测试,也可通过本区保存/应用提交。',
|
||||
'Stored securely. Test directly or submit with local Save / Apply actions.',
|
||||
@ -2813,19 +2853,24 @@ XWorkmate Privacy Policy
|
||||
}
|
||||
|
||||
Future<void> _captureVisibleSecretDrafts(AppController controller) async {
|
||||
final gatewayToken = _secretOverride(
|
||||
_gatewayTokenController,
|
||||
_gatewayTokenState,
|
||||
);
|
||||
if (gatewayToken.isNotEmpty) {
|
||||
controller.saveGatewayTokenDraft(gatewayToken);
|
||||
}
|
||||
final gatewayPassword = _secretOverride(
|
||||
_gatewayPasswordController,
|
||||
_gatewayPasswordState,
|
||||
);
|
||||
if (gatewayPassword.isNotEmpty) {
|
||||
controller.saveGatewayPasswordDraft(gatewayPassword);
|
||||
for (var index = 0; index < kGatewayProfileListLength; index += 1) {
|
||||
final gatewayToken = _secretOverride(
|
||||
_gatewayTokenControllers[index],
|
||||
_gatewayTokenStates[index],
|
||||
);
|
||||
if (gatewayToken.isNotEmpty) {
|
||||
controller.saveGatewayTokenDraft(gatewayToken, profileIndex: index);
|
||||
}
|
||||
final gatewayPassword = _secretOverride(
|
||||
_gatewayPasswordControllers[index],
|
||||
_gatewayPasswordStates[index],
|
||||
);
|
||||
if (gatewayPassword.isNotEmpty) {
|
||||
controller.saveGatewayPasswordDraft(
|
||||
gatewayPassword,
|
||||
profileIndex: index,
|
||||
);
|
||||
}
|
||||
}
|
||||
final aiGatewayApiKey = _secretOverride(
|
||||
_aiGatewayApiKeyController,
|
||||
@ -2848,10 +2893,6 @@ XWorkmate Privacy Policy
|
||||
}
|
||||
|
||||
void _resetSecureFieldUiAfterPersist(AppController controller) {
|
||||
final hasStoredGatewayToken =
|
||||
controller.settingsController.secureRefs['gateway_token'] != null;
|
||||
final hasStoredGatewayPassword =
|
||||
controller.settingsController.secureRefs['gateway_password'] != null;
|
||||
final hasStoredAiGatewayApiKey =
|
||||
controller.settingsController.secureRefs['ai_gateway_api_key'] != null;
|
||||
final hasStoredVaultToken =
|
||||
@ -2859,21 +2900,23 @@ XWorkmate Privacy Policy
|
||||
final hasStoredOllamaApiKey =
|
||||
controller.settingsController.secureRefs['ollama_cloud_api_key'] !=
|
||||
null;
|
||||
_gatewayTokenState = const _SecretFieldUiState();
|
||||
_gatewayPasswordState = const _SecretFieldUiState();
|
||||
for (var index = 0; index < kGatewayProfileListLength; index += 1) {
|
||||
_gatewayTokenStates[index] = const _SecretFieldUiState();
|
||||
_gatewayPasswordStates[index] = const _SecretFieldUiState();
|
||||
_primeSecureFieldController(
|
||||
_gatewayTokenControllers[index],
|
||||
hasStoredValue: controller.hasStoredGatewayTokenForProfile(index),
|
||||
fieldState: _gatewayTokenStates[index],
|
||||
);
|
||||
_primeSecureFieldController(
|
||||
_gatewayPasswordControllers[index],
|
||||
hasStoredValue: controller.hasStoredGatewayPasswordForProfile(index),
|
||||
fieldState: _gatewayPasswordStates[index],
|
||||
);
|
||||
}
|
||||
_aiGatewayApiKeyState = const _SecretFieldUiState();
|
||||
_vaultTokenState = const _SecretFieldUiState();
|
||||
_ollamaApiKeyState = const _SecretFieldUiState();
|
||||
_primeSecureFieldController(
|
||||
_gatewayTokenController,
|
||||
hasStoredValue: hasStoredGatewayToken,
|
||||
fieldState: _gatewayTokenState,
|
||||
);
|
||||
_primeSecureFieldController(
|
||||
_gatewayPasswordController,
|
||||
hasStoredValue: hasStoredGatewayPassword,
|
||||
fieldState: _gatewayPasswordState,
|
||||
);
|
||||
_primeSecureFieldController(
|
||||
_aiGatewayApiKeyController,
|
||||
hasStoredValue: hasStoredAiGatewayApiKey,
|
||||
@ -3176,21 +3219,35 @@ XWorkmate Privacy Policy
|
||||
) async {
|
||||
final messenger = ScaffoldMessenger.of(context);
|
||||
final gatewayDraft = _buildGatewayDraftProfile(settings);
|
||||
final selectedProfileIndex = _selectedGatewayProfileIndex.clamp(
|
||||
0,
|
||||
settings.gatewayProfiles.length - 1,
|
||||
);
|
||||
final gatewayTokenController =
|
||||
_gatewayTokenControllers[selectedProfileIndex];
|
||||
final gatewayPasswordController =
|
||||
_gatewayPasswordControllers[selectedProfileIndex];
|
||||
final gatewayTokenState = _gatewayTokenStates[selectedProfileIndex];
|
||||
final gatewayPasswordState = _gatewayPasswordStates[selectedProfileIndex];
|
||||
final executionTarget = switch (gatewayDraft.mode) {
|
||||
RuntimeConnectionMode.local => AssistantExecutionTarget.local,
|
||||
RuntimeConnectionMode.remote => AssistantExecutionTarget.remote,
|
||||
RuntimeConnectionMode.unconfigured => AssistantExecutionTarget.remote,
|
||||
};
|
||||
var token = _secretOverride(_gatewayTokenController, _gatewayTokenState);
|
||||
var token = _secretOverride(gatewayTokenController, gatewayTokenState);
|
||||
var password = _secretOverride(
|
||||
_gatewayPasswordController,
|
||||
_gatewayPasswordState,
|
||||
gatewayPasswordController,
|
||||
gatewayPasswordState,
|
||||
);
|
||||
if (token.isEmpty) {
|
||||
token = await controller.settingsController.loadGatewayToken();
|
||||
token = await controller.settingsController.loadGatewayToken(
|
||||
profileIndex: selectedProfileIndex,
|
||||
);
|
||||
}
|
||||
if (password.isEmpty) {
|
||||
password = await controller.settingsController.loadGatewayPassword();
|
||||
password = await controller.settingsController.loadGatewayPassword(
|
||||
profileIndex: selectedProfileIndex,
|
||||
);
|
||||
}
|
||||
setState(() => _gatewayTesting = true);
|
||||
try {
|
||||
|
||||
@ -120,6 +120,7 @@ class GatewayRuntime extends ChangeNotifier {
|
||||
|
||||
Future<void> connectProfile(
|
||||
GatewayConnectionProfile profile, {
|
||||
int? profileIndex,
|
||||
String authTokenOverride = '',
|
||||
String authPasswordOverride = '',
|
||||
}) async {
|
||||
@ -130,8 +131,14 @@ class GatewayRuntime extends ChangeNotifier {
|
||||
|
||||
final endpoint = _resolveEndpoint(profile);
|
||||
final setupPayload = decodeGatewaySetupCode(profile.setupCode);
|
||||
final storedToken = (await _store.loadGatewayToken())?.trim() ?? '';
|
||||
final storedPassword = (await _store.loadGatewayPassword())?.trim() ?? '';
|
||||
final storedToken =
|
||||
(await _store.loadGatewayToken(profileIndex: profileIndex))?.trim() ??
|
||||
'';
|
||||
final storedPassword =
|
||||
(await _store.loadGatewayPassword(
|
||||
profileIndex: profileIndex,
|
||||
))?.trim() ??
|
||||
'';
|
||||
final explicitToken = authTokenOverride.trim();
|
||||
final explicitPassword = authPasswordOverride.trim();
|
||||
final sharedTokenSource = explicitToken.isNotEmpty
|
||||
|
||||
@ -67,32 +67,36 @@ class SettingsController extends ChangeNotifier {
|
||||
}
|
||||
|
||||
Future<void> saveGatewaySecrets({
|
||||
int? profileIndex,
|
||||
required String token,
|
||||
required String password,
|
||||
}) async {
|
||||
final trimmedToken = token.trim();
|
||||
final trimmedPassword = password.trim();
|
||||
if (trimmedToken.isNotEmpty) {
|
||||
await _store.saveGatewayToken(trimmedToken);
|
||||
await _store.saveGatewayToken(trimmedToken, profileIndex: profileIndex);
|
||||
await appendAudit(
|
||||
SecretAuditEntry(
|
||||
timeLabel: _timeLabel(),
|
||||
action: 'Updated',
|
||||
provider: 'Gateway',
|
||||
target: 'gateway_token',
|
||||
target: _gatewaySecretTarget('gateway_token', profileIndex),
|
||||
module: 'Assistant',
|
||||
status: 'Success',
|
||||
),
|
||||
);
|
||||
}
|
||||
if (trimmedPassword.isNotEmpty) {
|
||||
await _store.saveGatewayPassword(trimmedPassword);
|
||||
await _store.saveGatewayPassword(
|
||||
trimmedPassword,
|
||||
profileIndex: profileIndex,
|
||||
);
|
||||
await appendAudit(
|
||||
SecretAuditEntry(
|
||||
timeLabel: _timeLabel(),
|
||||
action: 'Updated',
|
||||
provider: 'Gateway',
|
||||
target: 'gateway_password',
|
||||
target: _gatewaySecretTarget('gateway_password', profileIndex),
|
||||
module: 'Assistant',
|
||||
status: 'Success',
|
||||
),
|
||||
@ -103,30 +107,31 @@ class SettingsController extends ChangeNotifier {
|
||||
}
|
||||
|
||||
Future<void> clearGatewaySecrets({
|
||||
int? profileIndex,
|
||||
bool token = false,
|
||||
bool password = false,
|
||||
}) async {
|
||||
if (token) {
|
||||
await _store.clearGatewayToken();
|
||||
await _store.clearGatewayToken(profileIndex: profileIndex);
|
||||
await appendAudit(
|
||||
SecretAuditEntry(
|
||||
timeLabel: _timeLabel(),
|
||||
action: 'Cleared',
|
||||
provider: 'Gateway',
|
||||
target: 'gateway_token',
|
||||
target: _gatewaySecretTarget('gateway_token', profileIndex),
|
||||
module: 'Assistant',
|
||||
status: 'Success',
|
||||
),
|
||||
);
|
||||
}
|
||||
if (password) {
|
||||
await _store.clearGatewayPassword();
|
||||
await _store.clearGatewayPassword(profileIndex: profileIndex);
|
||||
await appendAudit(
|
||||
SecretAuditEntry(
|
||||
timeLabel: _timeLabel(),
|
||||
action: 'Cleared',
|
||||
provider: 'Gateway',
|
||||
target: 'gateway_password',
|
||||
target: _gatewaySecretTarget('gateway_password', profileIndex),
|
||||
module: 'Assistant',
|
||||
status: 'Success',
|
||||
),
|
||||
@ -136,14 +141,38 @@ class SettingsController extends ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<String> loadGatewayToken() async {
|
||||
return (await _store.loadGatewayToken())?.trim() ?? '';
|
||||
Future<String> loadGatewayToken({int? profileIndex}) async {
|
||||
return (await _store.loadGatewayToken(
|
||||
profileIndex: profileIndex,
|
||||
))?.trim() ??
|
||||
'';
|
||||
}
|
||||
|
||||
Future<String> loadGatewayPassword() async {
|
||||
return (await _store.loadGatewayPassword())?.trim() ?? '';
|
||||
Future<String> loadGatewayPassword({int? profileIndex}) async {
|
||||
return (await _store.loadGatewayPassword(
|
||||
profileIndex: profileIndex,
|
||||
))?.trim() ??
|
||||
'';
|
||||
}
|
||||
|
||||
bool hasStoredGatewayTokenForProfile(int profileIndex) =>
|
||||
_secureRefs.containsKey(SecretStore.gatewayTokenRefKey(profileIndex)) ||
|
||||
_secureRefs.containsKey('gateway_token');
|
||||
|
||||
bool hasStoredGatewayPasswordForProfile(int profileIndex) =>
|
||||
_secureRefs.containsKey(
|
||||
SecretStore.gatewayPasswordRefKey(profileIndex),
|
||||
) ||
|
||||
_secureRefs.containsKey('gateway_password');
|
||||
|
||||
String? storedGatewayTokenMaskForProfile(int profileIndex) =>
|
||||
_secureRefs[SecretStore.gatewayTokenRefKey(profileIndex)] ??
|
||||
_secureRefs['gateway_token'];
|
||||
|
||||
String? storedGatewayPasswordMaskForProfile(int profileIndex) =>
|
||||
_secureRefs[SecretStore.gatewayPasswordRefKey(profileIndex)] ??
|
||||
_secureRefs['gateway_password'];
|
||||
|
||||
Future<void> saveOllamaCloudApiKey(String value) async {
|
||||
final trimmed = value.trim();
|
||||
if (trimmed.isEmpty) {
|
||||
@ -754,6 +783,13 @@ class SettingsController extends ChangeNotifier {
|
||||
final now = DateTime.now();
|
||||
return '${now.hour.toString().padLeft(2, '0')}:${now.minute.toString().padLeft(2, '0')}';
|
||||
}
|
||||
|
||||
String _gatewaySecretTarget(String base, int? profileIndex) {
|
||||
if (profileIndex == null) {
|
||||
return base;
|
||||
}
|
||||
return '$base.$profileIndex';
|
||||
}
|
||||
}
|
||||
|
||||
class _AiGatewayResponseException implements Exception {
|
||||
|
||||
@ -140,10 +140,14 @@ class SecretStore {
|
||||
const FlutterSecureStorage(),
|
||||
);
|
||||
} catch (_) {
|
||||
_secureStorage = FileSecureStorageClient(() => _resolveFallbackDirectory());
|
||||
_secureStorage = FileSecureStorageClient(
|
||||
() => _resolveFallbackDirectory(),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
_secureStorage = FileSecureStorageClient(() => _resolveFallbackDirectory());
|
||||
_secureStorage = FileSecureStorageClient(
|
||||
() => _resolveFallbackDirectory(),
|
||||
);
|
||||
}
|
||||
_initialized = true;
|
||||
}
|
||||
@ -171,8 +175,19 @@ class SecretStore {
|
||||
if ((scopedValue ?? '').trim().isNotEmpty) {
|
||||
return scopedValue;
|
||||
}
|
||||
return _readSecure(_legacyGatewayTokenKey);
|
||||
}
|
||||
return _readSecure(_legacyGatewayTokenKey);
|
||||
final legacyValue = await _readSecure(_legacyGatewayTokenKey);
|
||||
if ((legacyValue ?? '').trim().isNotEmpty) {
|
||||
return legacyValue;
|
||||
}
|
||||
for (final index in _gatewayProfileFallbackOrder) {
|
||||
final scopedValue = await _readSecure(_gatewayTokenKeyForProfile(index));
|
||||
if ((scopedValue ?? '').trim().isNotEmpty) {
|
||||
return scopedValue;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<void> saveGatewayToken(String value, {int? profileIndex}) =>
|
||||
@ -183,12 +198,11 @@ class SecretStore {
|
||||
value,
|
||||
);
|
||||
|
||||
Future<void> clearGatewayToken({int? profileIndex}) =>
|
||||
_deleteSecure(
|
||||
profileIndex == null
|
||||
? _legacyGatewayTokenKey
|
||||
: _gatewayTokenKeyForProfile(profileIndex),
|
||||
);
|
||||
Future<void> clearGatewayToken({int? profileIndex}) => _deleteSecure(
|
||||
profileIndex == null
|
||||
? _legacyGatewayTokenKey
|
||||
: _gatewayTokenKeyForProfile(profileIndex),
|
||||
);
|
||||
|
||||
Future<String?> loadGatewayPassword({int? profileIndex}) async {
|
||||
if (profileIndex != null) {
|
||||
@ -198,8 +212,21 @@ class SecretStore {
|
||||
if ((scopedValue ?? '').trim().isNotEmpty) {
|
||||
return scopedValue;
|
||||
}
|
||||
return _readSecure(_legacyGatewayPasswordKey);
|
||||
}
|
||||
return _readSecure(_legacyGatewayPasswordKey);
|
||||
final legacyValue = await _readSecure(_legacyGatewayPasswordKey);
|
||||
if ((legacyValue ?? '').trim().isNotEmpty) {
|
||||
return legacyValue;
|
||||
}
|
||||
for (final index in _gatewayProfileFallbackOrder) {
|
||||
final scopedValue = await _readSecure(
|
||||
_gatewayPasswordKeyForProfile(index),
|
||||
);
|
||||
if ((scopedValue ?? '').trim().isNotEmpty) {
|
||||
return scopedValue;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<void> saveGatewayPassword(String value, {int? profileIndex}) =>
|
||||
@ -210,12 +237,11 @@ class SecretStore {
|
||||
value,
|
||||
);
|
||||
|
||||
Future<void> clearGatewayPassword({int? profileIndex}) =>
|
||||
_deleteSecure(
|
||||
profileIndex == null
|
||||
? _legacyGatewayPasswordKey
|
||||
: _gatewayPasswordKeyForProfile(profileIndex),
|
||||
);
|
||||
Future<void> clearGatewayPassword({int? profileIndex}) => _deleteSecure(
|
||||
profileIndex == null
|
||||
? _legacyGatewayPasswordKey
|
||||
: _gatewayPasswordKeyForProfile(profileIndex),
|
||||
);
|
||||
|
||||
Future<String?> loadOllamaCloudApiKey() => _readSecure(_ollamaCloudApiKeyKey);
|
||||
|
||||
@ -604,6 +630,14 @@ class SecretStore {
|
||||
static String _gatewayPasswordRefKey(int profileIndex) =>
|
||||
'gateway_password_$profileIndex';
|
||||
|
||||
static const List<int> _gatewayProfileFallbackOrder = <int>[
|
||||
kGatewayRemoteProfileIndex,
|
||||
kGatewayLocalProfileIndex,
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
];
|
||||
|
||||
static List<int> _base64UrlDecode(String value) {
|
||||
final normalized = value.replaceAll('-', '+').replaceAll('_', '/');
|
||||
final padded = normalized + '=' * ((4 - normalized.length % 4) % 4);
|
||||
@ -619,7 +653,9 @@ class SecretStore {
|
||||
if (promoted && _secureStorage != null) {
|
||||
return _secureStorage!;
|
||||
}
|
||||
throw StateError('Durable secret storage unavailable: no persistent secure storage client.');
|
||||
throw StateError(
|
||||
'Durable secret storage unavailable: no persistent secure storage client.',
|
||||
);
|
||||
}
|
||||
|
||||
Future<String?> _resolvePath(Future<String?> Function()? resolver) async {
|
||||
|
||||
@ -33,6 +33,7 @@ class _FakeGatewayRuntime extends GatewayRuntime {
|
||||
@override
|
||||
Future<void> connectProfile(
|
||||
GatewayConnectionProfile profile, {
|
||||
int? profileIndex,
|
||||
String authTokenOverride = '',
|
||||
String authPasswordOverride = '',
|
||||
}) async {}
|
||||
|
||||
@ -827,6 +827,7 @@ class _FakeGatewayRuntime extends GatewayRuntime {
|
||||
@override
|
||||
Future<void> connectProfile(
|
||||
GatewayConnectionProfile profile, {
|
||||
int? profileIndex,
|
||||
String authTokenOverride = '',
|
||||
String authPasswordOverride = '',
|
||||
}) async {
|
||||
|
||||
@ -505,6 +505,22 @@ void main() {
|
||||
await tester.tap(find.byKey(const ValueKey('llm-endpoint-add-button')));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(
|
||||
find.descendant(
|
||||
of: find.byKey(const ValueKey('llm-endpoint-chip-0')),
|
||||
matching: find.textContaining('主 LLM API'),
|
||||
),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(
|
||||
find.descendant(
|
||||
of: find.byKey(const ValueKey('llm-endpoint-chip-1')),
|
||||
matching: find.textContaining('Ollama 本地'),
|
||||
),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(find.text('连接源详情'), findsOneWidget);
|
||||
expect(find.textContaining('自定义连接源'), findsNothing);
|
||||
expect(find.byKey(const ValueKey('llm-endpoint-chip-1')), findsOneWidget);
|
||||
expect(
|
||||
find.byKey(const ValueKey('llm-endpoint-panel-ollamaLocal')),
|
||||
|
||||
@ -73,6 +73,7 @@ class MockGatewayRuntime extends GatewayRuntime {
|
||||
@override
|
||||
Future<void> connectProfile(
|
||||
GatewayConnectionProfile profile, {
|
||||
int? profileIndex,
|
||||
String authTokenOverride = '',
|
||||
String authPasswordOverride = '',
|
||||
}) async {}
|
||||
|
||||
@ -175,76 +175,73 @@ void main() {
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'AppController falls back when LLM API ignores stream mode',
|
||||
() async {
|
||||
SharedPreferences.setMockInitialValues(<String, Object>{});
|
||||
final tempDirectory = await Directory.systemTemp.createTemp(
|
||||
'xworkmate-ai-gateway-json-fallback-',
|
||||
);
|
||||
final server = await _FakeAiGatewayServer.start(
|
||||
responseMode: _AiGatewayResponseMode.json,
|
||||
);
|
||||
addTearDown(() async {
|
||||
await server.close();
|
||||
if (await tempDirectory.exists()) {
|
||||
await tempDirectory.delete(recursive: true);
|
||||
}
|
||||
});
|
||||
test('AppController falls back when LLM API ignores stream mode', () async {
|
||||
SharedPreferences.setMockInitialValues(<String, Object>{});
|
||||
final tempDirectory = await Directory.systemTemp.createTemp(
|
||||
'xworkmate-ai-gateway-json-fallback-',
|
||||
);
|
||||
final server = await _FakeAiGatewayServer.start(
|
||||
responseMode: _AiGatewayResponseMode.json,
|
||||
);
|
||||
addTearDown(() async {
|
||||
await server.close();
|
||||
if (await tempDirectory.exists()) {
|
||||
await tempDirectory.delete(recursive: true);
|
||||
}
|
||||
});
|
||||
|
||||
final store = SecureConfigStore(
|
||||
enableSecureStorage: false,
|
||||
databasePathResolver: () async => '${tempDirectory.path}/settings.db',
|
||||
fallbackDirectoryPathResolver: () async => tempDirectory.path,
|
||||
);
|
||||
final controller = AppController(
|
||||
store: store,
|
||||
availableSingleAgentProvidersOverride: const <SingleAgentProvider>[],
|
||||
runtimeCoordinator: RuntimeCoordinator(
|
||||
gateway: _FakeGatewayRuntime(store: store),
|
||||
codex: _FakeCodexRuntime(),
|
||||
final store = SecureConfigStore(
|
||||
enableSecureStorage: false,
|
||||
databasePathResolver: () async => '${tempDirectory.path}/settings.db',
|
||||
fallbackDirectoryPathResolver: () async => tempDirectory.path,
|
||||
);
|
||||
final controller = AppController(
|
||||
store: store,
|
||||
availableSingleAgentProvidersOverride: const <SingleAgentProvider>[],
|
||||
runtimeCoordinator: RuntimeCoordinator(
|
||||
gateway: _FakeGatewayRuntime(store: store),
|
||||
codex: _FakeCodexRuntime(),
|
||||
),
|
||||
singleAgentRunner: _FallbackOnlySingleAgentRunner(),
|
||||
);
|
||||
addTearDown(controller.dispose);
|
||||
|
||||
await _waitFor(() => !controller.initializing);
|
||||
await controller.settingsController.saveAiGatewayApiKey('live-key');
|
||||
await controller.saveSettings(
|
||||
controller.settings.copyWith(
|
||||
aiGateway: controller.settings.aiGateway.copyWith(
|
||||
baseUrl: server.baseUrl,
|
||||
availableModels: const <String>['moonshotai/kimi-k2.5'],
|
||||
selectedModels: const <String>['moonshotai/kimi-k2.5'],
|
||||
),
|
||||
singleAgentRunner: _FallbackOnlySingleAgentRunner(),
|
||||
);
|
||||
addTearDown(controller.dispose);
|
||||
|
||||
await _waitFor(() => !controller.initializing);
|
||||
await controller.settingsController.saveAiGatewayApiKey('live-key');
|
||||
await controller.saveSettings(
|
||||
controller.settings.copyWith(
|
||||
aiGateway: controller.settings.aiGateway.copyWith(
|
||||
baseUrl: server.baseUrl,
|
||||
availableModels: const <String>['moonshotai/kimi-k2.5'],
|
||||
selectedModels: const <String>['moonshotai/kimi-k2.5'],
|
||||
),
|
||||
defaultModel: 'moonshotai/kimi-k2.5',
|
||||
multiAgent: controller.settings.multiAgent.copyWith(
|
||||
autoSync: false,
|
||||
mountTargets: _withAvailableMountTargets(
|
||||
controller.settings.multiAgent.mountTargets,
|
||||
const <String>[],
|
||||
),
|
||||
defaultModel: 'moonshotai/kimi-k2.5',
|
||||
multiAgent: controller.settings.multiAgent.copyWith(
|
||||
autoSync: false,
|
||||
mountTargets: _withAvailableMountTargets(
|
||||
controller.settings.multiAgent.mountTargets,
|
||||
const <String>[],
|
||||
),
|
||||
),
|
||||
refreshAfterSave: false,
|
||||
);
|
||||
await controller.setAssistantExecutionTarget(
|
||||
AssistantExecutionTarget.singleAgent,
|
||||
);
|
||||
),
|
||||
refreshAfterSave: false,
|
||||
);
|
||||
await controller.setAssistantExecutionTarget(
|
||||
AssistantExecutionTarget.singleAgent,
|
||||
);
|
||||
|
||||
await controller.sendChatMessage('你好', thinking: 'low');
|
||||
await controller.sendChatMessage('你好', thinking: 'low');
|
||||
|
||||
await _waitFor(
|
||||
() => controller.chatMessages.any(
|
||||
(message) =>
|
||||
message.role == 'assistant' && message.text == 'FIRST_REPLY',
|
||||
),
|
||||
);
|
||||
await _waitFor(
|
||||
() => controller.chatMessages.any(
|
||||
(message) =>
|
||||
message.role == 'assistant' && message.text == 'FIRST_REPLY',
|
||||
),
|
||||
);
|
||||
|
||||
expect(server.requests.single['stream'], isTrue);
|
||||
expect(controller.chatMessages.last.pending, isFalse);
|
||||
},
|
||||
);
|
||||
expect(server.requests.single['stream'], isTrue);
|
||||
expect(controller.chatMessages.last.pending, isFalse);
|
||||
});
|
||||
|
||||
test(
|
||||
'AppController abortRun stops Single Agent streaming requests',
|
||||
@ -380,7 +377,8 @@ void main() {
|
||||
expect(runner.lastRequest?.provider, SingleAgentProvider.codex);
|
||||
expect(
|
||||
controller.chatMessages.any(
|
||||
(message) => message.role == 'assistant' && message.text == 'CODEX_REPLY',
|
||||
(message) =>
|
||||
message.role == 'assistant' && message.text == 'CODEX_REPLY',
|
||||
),
|
||||
isTrue,
|
||||
);
|
||||
@ -569,6 +567,7 @@ class _FakeGatewayRuntime extends GatewayRuntime {
|
||||
@override
|
||||
Future<void> connectProfile(
|
||||
GatewayConnectionProfile profile, {
|
||||
int? profileIndex,
|
||||
String authTokenOverride = '',
|
||||
String authPasswordOverride = '',
|
||||
}) async {
|
||||
|
||||
@ -56,6 +56,7 @@ class _FakeGatewayRuntime extends GatewayRuntime {
|
||||
@override
|
||||
Future<void> connectProfile(
|
||||
GatewayConnectionProfile profile, {
|
||||
int? profileIndex,
|
||||
String authTokenOverride = '',
|
||||
String authPasswordOverride = '',
|
||||
}) async {
|
||||
@ -130,181 +131,168 @@ class _FakeCodexRuntime extends CodexRuntime {
|
||||
}
|
||||
|
||||
void main() {
|
||||
group(
|
||||
'Manual Codex bridge validation',
|
||||
() {
|
||||
test(
|
||||
'AppController enables external Codex bridge and registers to gateway',
|
||||
() async {
|
||||
SharedPreferences.setMockInitialValues(<String, Object>{});
|
||||
final store = createIsolatedTestStore();
|
||||
final gateway = _FakeGatewayRuntime(connected: true);
|
||||
final codex = _FakeCodexRuntime();
|
||||
final coordinator = RuntimeCoordinator(
|
||||
gateway: gateway,
|
||||
codex: codex,
|
||||
);
|
||||
final controller = AppController(
|
||||
store: store,
|
||||
runtimeCoordinator: coordinator,
|
||||
);
|
||||
addTearDown(controller.dispose);
|
||||
group('Manual Codex bridge validation', () {
|
||||
test(
|
||||
'AppController enables external Codex bridge and registers to gateway',
|
||||
() async {
|
||||
SharedPreferences.setMockInitialValues(<String, Object>{});
|
||||
final store = createIsolatedTestStore();
|
||||
final gateway = _FakeGatewayRuntime(connected: true);
|
||||
final codex = _FakeCodexRuntime();
|
||||
final coordinator = RuntimeCoordinator(gateway: gateway, codex: codex);
|
||||
final controller = AppController(
|
||||
store: store,
|
||||
runtimeCoordinator: coordinator,
|
||||
);
|
||||
addTearDown(controller.dispose);
|
||||
|
||||
await _waitFor(() => !controller.initializing);
|
||||
await _waitFor(() => !controller.initializing);
|
||||
|
||||
final tempDir = await Directory.systemTemp.createTemp('codex-bridge-');
|
||||
addTearDown(() async {
|
||||
if (await tempDir.exists()) {
|
||||
await tempDir.delete(recursive: true);
|
||||
}
|
||||
});
|
||||
final codexBinary = File('${tempDir.path}/codex');
|
||||
await codexBinary.writeAsString('#!/bin/sh\nexit 0\n');
|
||||
final tempDir = await Directory.systemTemp.createTemp('codex-bridge-');
|
||||
addTearDown(() async {
|
||||
if (await tempDir.exists()) {
|
||||
await tempDir.delete(recursive: true);
|
||||
}
|
||||
});
|
||||
final codexBinary = File('${tempDir.path}/codex');
|
||||
await codexBinary.writeAsString('#!/bin/sh\nexit 0\n');
|
||||
|
||||
await controller.settingsController.saveAiGatewayApiKey(
|
||||
'bridge-secret',
|
||||
);
|
||||
await controller.saveSettings(
|
||||
controller.settings.copyWith(
|
||||
workspacePath: tempDir.path,
|
||||
codeAgentRuntimeMode: CodeAgentRuntimeMode.externalCli,
|
||||
codexCliPath: codexBinary.path,
|
||||
aiGateway: controller.settings.aiGateway.copyWith(
|
||||
baseUrl: 'https://gateway.example.com',
|
||||
),
|
||||
await controller.settingsController.saveAiGatewayApiKey(
|
||||
'bridge-secret',
|
||||
);
|
||||
await controller.saveSettings(
|
||||
controller.settings.copyWith(
|
||||
workspacePath: tempDir.path,
|
||||
codeAgentRuntimeMode: CodeAgentRuntimeMode.externalCli,
|
||||
codexCliPath: codexBinary.path,
|
||||
aiGateway: controller.settings.aiGateway.copyWith(
|
||||
baseUrl: 'https://gateway.example.com',
|
||||
),
|
||||
);
|
||||
),
|
||||
);
|
||||
|
||||
await controller.enableCodexBridge();
|
||||
await controller.enableCodexBridge();
|
||||
|
||||
expect(controller.isCodexBridgeEnabled, isTrue);
|
||||
expect(
|
||||
controller.codexCooperationState,
|
||||
CodexCooperationState.registered,
|
||||
);
|
||||
expect(codex.startCalled, isTrue);
|
||||
expect(codex.startedCodexPath, codexBinary.path);
|
||||
expect(codex.startedCwd, tempDir.path);
|
||||
expect(controller.isCodexBridgeEnabled, isTrue);
|
||||
expect(
|
||||
controller.codexCooperationState,
|
||||
CodexCooperationState.registered,
|
||||
);
|
||||
expect(codex.startCalled, isTrue);
|
||||
expect(codex.startedCodexPath, codexBinary.path);
|
||||
expect(codex.startedCwd, tempDir.path);
|
||||
|
||||
final registrationCall = gateway.requests.firstWhere(
|
||||
final registrationCall = gateway.requests.firstWhere(
|
||||
(request) => request['method'] == 'agent/register',
|
||||
);
|
||||
final params = registrationCall['params'] as Map<String, dynamic>;
|
||||
expect(params['transport'], 'stdio-bridge');
|
||||
expect(params['metadata'], containsPair('providerId', 'codex'));
|
||||
expect(params['metadata'], containsPair('runtimeMode', 'externalCli'));
|
||||
expect(
|
||||
(params['metadata']['node'] as Map<String, dynamic>)['kind'],
|
||||
'app-mediated-cooperative-node',
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'AppController keeps bridge running when gateway registration is unavailable',
|
||||
() async {
|
||||
SharedPreferences.setMockInitialValues(<String, Object>{});
|
||||
final store = createIsolatedTestStore();
|
||||
final gateway = _FakeGatewayRuntime(connected: false);
|
||||
final codex = _FakeCodexRuntime();
|
||||
final coordinator = RuntimeCoordinator(gateway: gateway, codex: codex);
|
||||
final controller = AppController(
|
||||
store: store,
|
||||
runtimeCoordinator: coordinator,
|
||||
);
|
||||
addTearDown(controller.dispose);
|
||||
|
||||
await _waitFor(() => !controller.initializing);
|
||||
|
||||
final tempDir = await Directory.systemTemp.createTemp(
|
||||
'codex-bridge-offline-',
|
||||
);
|
||||
addTearDown(() async {
|
||||
if (await tempDir.exists()) {
|
||||
await tempDir.delete(recursive: true);
|
||||
}
|
||||
});
|
||||
final codexBinary = File('${tempDir.path}/codex');
|
||||
await codexBinary.writeAsString('#!/bin/sh\nexit 0\n');
|
||||
|
||||
await controller.saveSettings(
|
||||
controller.settings.copyWith(
|
||||
codeAgentRuntimeMode: CodeAgentRuntimeMode.externalCli,
|
||||
codexCliPath: codexBinary.path,
|
||||
aiGateway: controller.settings.aiGateway.copyWith(
|
||||
baseUrl: 'https://gateway.example.com',
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await controller.enableCodexBridge();
|
||||
|
||||
expect(controller.isCodexBridgeEnabled, isTrue);
|
||||
expect(
|
||||
controller.codexCooperationState,
|
||||
CodexCooperationState.bridgeOnly,
|
||||
);
|
||||
expect(codex.startCalled, isTrue);
|
||||
expect(
|
||||
gateway.requests.where(
|
||||
(request) => request['method'] == 'agent/register',
|
||||
);
|
||||
final params = registrationCall['params'] as Map<String, dynamic>;
|
||||
expect(params['transport'], 'stdio-bridge');
|
||||
expect(params['metadata'], containsPair('providerId', 'codex'));
|
||||
expect(params['metadata'], containsPair('runtimeMode', 'externalCli'));
|
||||
expect(
|
||||
(params['metadata']['node'] as Map<String, dynamic>)['kind'],
|
||||
'app-mediated-cooperative-node',
|
||||
);
|
||||
},
|
||||
);
|
||||
),
|
||||
isEmpty,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'AppController keeps bridge running when gateway registration is unavailable',
|
||||
() async {
|
||||
SharedPreferences.setMockInitialValues(<String, Object>{});
|
||||
final store = createIsolatedTestStore();
|
||||
final gateway = _FakeGatewayRuntime(connected: false);
|
||||
final codex = _FakeCodexRuntime();
|
||||
final coordinator = RuntimeCoordinator(
|
||||
gateway: gateway,
|
||||
codex: codex,
|
||||
);
|
||||
final controller = AppController(
|
||||
store: store,
|
||||
runtimeCoordinator: coordinator,
|
||||
);
|
||||
addTearDown(controller.dispose);
|
||||
test(
|
||||
'AppController preserves built-in mode and does not require external codex binary',
|
||||
() async {
|
||||
SharedPreferences.setMockInitialValues(<String, Object>{});
|
||||
final store = createIsolatedTestStore();
|
||||
final gateway = _FakeGatewayRuntime(connected: false);
|
||||
final codex = _FakeCodexRuntime();
|
||||
final coordinator = RuntimeCoordinator(gateway: gateway, codex: codex);
|
||||
final controller = AppController(
|
||||
store: store,
|
||||
runtimeCoordinator: coordinator,
|
||||
);
|
||||
addTearDown(controller.dispose);
|
||||
|
||||
await _waitFor(() => !controller.initializing);
|
||||
await _waitFor(() => !controller.initializing);
|
||||
|
||||
final tempDir = await Directory.systemTemp.createTemp(
|
||||
'codex-bridge-offline-',
|
||||
);
|
||||
addTearDown(() async {
|
||||
if (await tempDir.exists()) {
|
||||
await tempDir.delete(recursive: true);
|
||||
}
|
||||
});
|
||||
final codexBinary = File('${tempDir.path}/codex');
|
||||
await codexBinary.writeAsString('#!/bin/sh\nexit 0\n');
|
||||
|
||||
await controller.saveSettings(
|
||||
controller.settings.copyWith(
|
||||
codeAgentRuntimeMode: CodeAgentRuntimeMode.externalCli,
|
||||
codexCliPath: codexBinary.path,
|
||||
aiGateway: controller.settings.aiGateway.copyWith(
|
||||
baseUrl: 'https://gateway.example.com',
|
||||
),
|
||||
await controller.saveSettings(
|
||||
controller.settings.copyWith(
|
||||
codeAgentRuntimeMode: CodeAgentRuntimeMode.builtIn,
|
||||
aiGateway: controller.settings.aiGateway.copyWith(
|
||||
baseUrl: 'https://gateway.example.com',
|
||||
),
|
||||
);
|
||||
),
|
||||
);
|
||||
|
||||
await controller.enableCodexBridge();
|
||||
expect(
|
||||
controller.settings.codeAgentRuntimeMode,
|
||||
CodeAgentRuntimeMode.builtIn,
|
||||
);
|
||||
expect(controller.codexRuntimeWarning, isNotNull);
|
||||
|
||||
expect(controller.isCodexBridgeEnabled, isTrue);
|
||||
expect(
|
||||
controller.codexCooperationState,
|
||||
CodexCooperationState.bridgeOnly,
|
||||
);
|
||||
expect(codex.startCalled, isTrue);
|
||||
expect(
|
||||
gateway.requests.where(
|
||||
(request) => request['method'] == 'agent/register',
|
||||
),
|
||||
isEmpty,
|
||||
);
|
||||
},
|
||||
);
|
||||
await controller.enableCodexBridge();
|
||||
|
||||
test(
|
||||
'AppController preserves built-in mode and does not require external codex binary',
|
||||
() async {
|
||||
SharedPreferences.setMockInitialValues(<String, Object>{});
|
||||
final store = createIsolatedTestStore();
|
||||
final gateway = _FakeGatewayRuntime(connected: false);
|
||||
final codex = _FakeCodexRuntime();
|
||||
final coordinator = RuntimeCoordinator(
|
||||
gateway: gateway,
|
||||
codex: codex,
|
||||
);
|
||||
final controller = AppController(
|
||||
store: store,
|
||||
runtimeCoordinator: coordinator,
|
||||
);
|
||||
addTearDown(controller.dispose);
|
||||
|
||||
await _waitFor(() => !controller.initializing);
|
||||
|
||||
await controller.saveSettings(
|
||||
controller.settings.copyWith(
|
||||
codeAgentRuntimeMode: CodeAgentRuntimeMode.builtIn,
|
||||
aiGateway: controller.settings.aiGateway.copyWith(
|
||||
baseUrl: 'https://gateway.example.com',
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(
|
||||
controller.settings.codeAgentRuntimeMode,
|
||||
CodeAgentRuntimeMode.builtIn,
|
||||
);
|
||||
expect(controller.codexRuntimeWarning, isNotNull);
|
||||
|
||||
await controller.enableCodexBridge();
|
||||
|
||||
expect(controller.isCodexBridgeEnabled, isTrue);
|
||||
expect(
|
||||
controller.codexCooperationState,
|
||||
CodexCooperationState.bridgeOnly,
|
||||
);
|
||||
expect(codex.startCalled, isFalse);
|
||||
expect(coordinator.runtimeMode, CodeAgentRuntimeMode.builtIn);
|
||||
},
|
||||
);
|
||||
},
|
||||
skip: _manualCodexBridgeSkipReason,
|
||||
);
|
||||
expect(controller.isCodexBridgeEnabled, isTrue);
|
||||
expect(
|
||||
controller.codexCooperationState,
|
||||
CodexCooperationState.bridgeOnly,
|
||||
);
|
||||
expect(codex.startCalled, isFalse);
|
||||
expect(coordinator.runtimeMode, CodeAgentRuntimeMode.builtIn);
|
||||
},
|
||||
);
|
||||
}, skip: _manualCodexBridgeSkipReason);
|
||||
}
|
||||
|
||||
Future<void> _waitFor(
|
||||
|
||||
@ -38,6 +38,7 @@ class _FakeGatewayRuntime extends GatewayRuntime {
|
||||
@override
|
||||
Future<void> connectProfile(
|
||||
GatewayConnectionProfile profile, {
|
||||
int? profileIndex,
|
||||
String authTokenOverride = '',
|
||||
String authPasswordOverride = '',
|
||||
}) async {
|
||||
|
||||
@ -5,12 +5,14 @@ import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:xworkmate/app/app_controller.dart';
|
||||
|
||||
import '../test_support.dart';
|
||||
|
||||
void main() {
|
||||
test(
|
||||
'AppController tracks stored shared-token mask and clear action',
|
||||
() async {
|
||||
SharedPreferences.setMockInitialValues(<String, Object>{});
|
||||
final controller = AppController();
|
||||
final controller = AppController(store: createIsolatedTestStore());
|
||||
addTearDown(controller.dispose);
|
||||
|
||||
await _waitFor(() => !controller.initializing);
|
||||
@ -32,6 +34,33 @@ void main() {
|
||||
expect(controller.storedGatewayTokenMask, isNull);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'AppController keeps gateway token masks independent per profile slot',
|
||||
() async {
|
||||
SharedPreferences.setMockInitialValues(<String, Object>{});
|
||||
final controller = AppController(store: createIsolatedTestStore());
|
||||
addTearDown(controller.dispose);
|
||||
|
||||
await _waitFor(() => !controller.initializing);
|
||||
|
||||
await controller.settingsController.saveGatewaySecrets(
|
||||
profileIndex: 0,
|
||||
token: 'local-secret',
|
||||
password: '',
|
||||
);
|
||||
await controller.settingsController.saveGatewaySecrets(
|
||||
profileIndex: 1,
|
||||
token: 'remote-secret',
|
||||
password: '',
|
||||
);
|
||||
|
||||
expect(controller.hasStoredGatewayTokenForProfile(0), isTrue);
|
||||
expect(controller.hasStoredGatewayTokenForProfile(1), isTrue);
|
||||
expect(controller.storedGatewayTokenMaskForProfile(0), 'loc••••ret');
|
||||
expect(controller.storedGatewayTokenMaskForProfile(1), 'rem••••ret');
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _waitFor(bool Function() predicate) async {
|
||||
|
||||
@ -23,6 +23,7 @@ class _FakeGatewayRuntime extends GatewayRuntime {
|
||||
@override
|
||||
Future<void> connectProfile(
|
||||
GatewayConnectionProfile profile, {
|
||||
int? profileIndex,
|
||||
String authTokenOverride = '',
|
||||
String authPasswordOverride = '',
|
||||
}) async {}
|
||||
|
||||
@ -59,6 +59,7 @@ class MockGatewayRuntime extends GatewayRuntime {
|
||||
@override
|
||||
Future<void> connectProfile(
|
||||
GatewayConnectionProfile profile, {
|
||||
int? profileIndex,
|
||||
String authTokenOverride = '',
|
||||
String authPasswordOverride = '',
|
||||
}) async {
|
||||
|
||||
@ -81,6 +81,7 @@ class MockGatewayRuntime extends GatewayRuntime {
|
||||
@override
|
||||
Future<void> connectProfile(
|
||||
GatewayConnectionProfile profile, {
|
||||
int? profileIndex,
|
||||
String authTokenOverride = '',
|
||||
String authPasswordOverride = '',
|
||||
}) async {
|
||||
|
||||
@ -41,6 +41,7 @@ class _FakeGatewayRuntime extends GatewayRuntime {
|
||||
@override
|
||||
Future<void> connectProfile(
|
||||
GatewayConnectionProfile profile, {
|
||||
int? profileIndex,
|
||||
String authTokenOverride = '',
|
||||
String authPasswordOverride = '',
|
||||
}) async {
|
||||
|
||||
@ -87,6 +87,69 @@ void main() {
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'SecureConfigStore keeps gateway secrets isolated per profile slot',
|
||||
() async {
|
||||
SharedPreferences.setMockInitialValues(<String, Object>{});
|
||||
final tempDirectory = await Directory.systemTemp.createTemp(
|
||||
'xworkmate-config-store-profiles-',
|
||||
);
|
||||
addTearDown(() async {
|
||||
if (await tempDirectory.exists()) {
|
||||
await tempDirectory.delete(recursive: true);
|
||||
}
|
||||
});
|
||||
final databasePath = '${tempDirectory.path}/settings.sqlite3';
|
||||
final store = SecureConfigStore(
|
||||
databasePathResolver: () async => databasePath,
|
||||
fallbackDirectoryPathResolver: () async => tempDirectory.path,
|
||||
);
|
||||
|
||||
await store.saveGatewayToken(
|
||||
'local-token',
|
||||
profileIndex: kGatewayLocalProfileIndex,
|
||||
);
|
||||
await store.saveGatewayToken(
|
||||
'remote-token',
|
||||
profileIndex: kGatewayRemoteProfileIndex,
|
||||
);
|
||||
await store.saveGatewayPassword(
|
||||
'custom-password',
|
||||
profileIndex: kGatewayCustomProfileStartIndex,
|
||||
);
|
||||
|
||||
final secureRefs = await store.loadSecureRefs();
|
||||
|
||||
expect(
|
||||
await store.loadGatewayToken(profileIndex: kGatewayLocalProfileIndex),
|
||||
'local-token',
|
||||
);
|
||||
expect(
|
||||
await store.loadGatewayToken(profileIndex: kGatewayRemoteProfileIndex),
|
||||
'remote-token',
|
||||
);
|
||||
expect(
|
||||
await store.loadGatewayPassword(
|
||||
profileIndex: kGatewayCustomProfileStartIndex,
|
||||
),
|
||||
'custom-password',
|
||||
);
|
||||
expect(
|
||||
secureRefs['gateway_token_$kGatewayLocalProfileIndex'],
|
||||
'local-token',
|
||||
);
|
||||
expect(
|
||||
secureRefs['gateway_token_$kGatewayRemoteProfileIndex'],
|
||||
'remote-token',
|
||||
);
|
||||
expect(
|
||||
secureRefs['gateway_password_$kGatewayCustomProfileStartIndex'],
|
||||
'custom-password',
|
||||
);
|
||||
expect(await store.loadGatewayToken(), 'remote-token');
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'SecureConfigStore persists sqlite-backed settings across instances',
|
||||
() async {
|
||||
@ -191,20 +254,29 @@ void main() {
|
||||
await tempDirectory.delete(recursive: true);
|
||||
}
|
||||
});
|
||||
final existingSecretsDirectory = Directory('${tempDirectory.path}/secrets');
|
||||
final existingSecretsDirectory = Directory(
|
||||
'${tempDirectory.path}/secrets',
|
||||
);
|
||||
await existingSecretsDirectory.create(recursive: true);
|
||||
final explicitSettingsPath =
|
||||
'${tempDirectory.path}/settings/${SettingsStore.databaseFileName}';
|
||||
|
||||
final store = SecureConfigStore(
|
||||
databasePathResolver: () async => explicitSettingsPath,
|
||||
fallbackDirectoryPathResolver: () async => existingSecretsDirectory.path,
|
||||
fallbackDirectoryPathResolver: () async =>
|
||||
existingSecretsDirectory.path,
|
||||
);
|
||||
|
||||
final snapshot = await store.loadSettingsSnapshot();
|
||||
|
||||
expect(snapshot.accountUsername, SettingsSnapshot.defaults().accountUsername);
|
||||
expect(await Directory('${tempDirectory.path}/settings').exists(), isTrue);
|
||||
expect(
|
||||
snapshot.accountUsername,
|
||||
SettingsSnapshot.defaults().accountUsername,
|
||||
);
|
||||
expect(
|
||||
await Directory('${tempDirectory.path}/settings').exists(),
|
||||
isTrue,
|
||||
);
|
||||
expect(await File(explicitSettingsPath).exists(), isTrue);
|
||||
},
|
||||
);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user