Fix manual bridge save runtime config
This commit is contained in:
parent
22a0376b00
commit
c2128fe5da
@ -1117,6 +1117,10 @@ extension AppControllerDesktopRuntimeHelpers on AppController {
|
||||
return null;
|
||||
}
|
||||
|
||||
final manualBridgeToken = await _resolveManualBridgeAuthTokenInternal();
|
||||
if (manualBridgeToken != null && manualBridgeToken.isNotEmpty) {
|
||||
return _normalizeAuthorizationHeaderInternal(manualBridgeToken);
|
||||
}
|
||||
final bridgeToken = await _resolveManagedBridgeAuthTokenInternal();
|
||||
if (bridgeToken != null && bridgeToken.isNotEmpty) {
|
||||
return _normalizeAuthorizationHeaderInternal(bridgeToken);
|
||||
|
||||
@ -104,38 +104,27 @@ class _MobileSettingsPageState extends State<MobileSettingsPage> {
|
||||
required bool isManualBridge,
|
||||
bool refreshAfterSave = true,
|
||||
}) async {
|
||||
final bridgeConfig = settings.acpBridgeServerModeConfig;
|
||||
final nextBridgeConfig = bridgeConfig.copyWith(
|
||||
selfHosted: bridgeConfig.selfHosted.copyWith(
|
||||
serverUrl: bridgeUrlController.text.trim(),
|
||||
username: isManualBridge ? 'admin' : bridgeConfig.selfHosted.username,
|
||||
),
|
||||
);
|
||||
final nextEffective = widget.controller.settingsController
|
||||
.resolveAcpBridgeServerEffectiveConfig(config: nextBridgeConfig);
|
||||
final nextSettings = settings.copyWith(
|
||||
accountBaseUrl: accountBaseUrlController.text.trim(),
|
||||
accountUsername: accountIdentifierController.text.trim(),
|
||||
acpBridgeServerModeConfig: nextBridgeConfig.copyWith(
|
||||
effective: nextEffective,
|
||||
),
|
||||
);
|
||||
if (isManualBridge && bridgeTokenController.text.isNotEmpty) {
|
||||
await widget.controller.settingsController.saveSecretValueByRef(
|
||||
nextSettings.acpBridgeServerModeConfig.selfHosted.passwordRef,
|
||||
bridgeTokenController.text,
|
||||
provider: 'Bridge',
|
||||
module: 'Manual',
|
||||
);
|
||||
}
|
||||
final nextSettings = await widget.controller.settingsController
|
||||
.buildSavedAccountProfileSettings(
|
||||
settings: settings,
|
||||
accountBaseUrl: accountBaseUrlController.text,
|
||||
accountIdentifier: accountIdentifierController.text,
|
||||
bridgeServerUrl: bridgeUrlController.text,
|
||||
bridgeToken: bridgeTokenController.text,
|
||||
isManualBridge: isManualBridge,
|
||||
);
|
||||
await widget.controller.saveSettings(
|
||||
nextSettings,
|
||||
refreshAfterSave: refreshAfterSave,
|
||||
refreshAfterSave: isManualBridge ? false : refreshAfterSave,
|
||||
);
|
||||
lastSavedAccountBaseUrl = nextSettings.accountBaseUrl;
|
||||
lastSavedAccountIdentifier = nextSettings.accountUsername;
|
||||
lastSavedBridgeUrl =
|
||||
nextSettings.acpBridgeServerModeConfig.selfHosted.serverUrl;
|
||||
if (isManualBridge &&
|
||||
nextSettings.acpBridgeServerModeConfig.selfHosted.isConfigured) {
|
||||
unawaited(refreshBridgeCapabilities());
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> loginAccount(SettingsSnapshot settings) async {
|
||||
@ -245,16 +234,25 @@ class _MobileSettingsPageState extends State<MobileSettingsPage> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: () => controller.navigateTo(WorkspaceDestination.assistant),
|
||||
onTap: () => controller.navigateTo(
|
||||
WorkspaceDestination.assistant,
|
||||
),
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.arrow_back_ios_new_rounded, size: 16, color: palette.textSecondary),
|
||||
Icon(
|
||||
Icons.arrow_back_ios_new_rounded,
|
||||
size: 16,
|
||||
color: palette.textSecondary,
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
appText('返回对话主页', 'Back to Chat'),
|
||||
style: TextStyle(color: palette.textSecondary, fontSize: 16),
|
||||
style: TextStyle(
|
||||
color: palette.textSecondary,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
@ -262,9 +260,8 @@ class _MobileSettingsPageState extends State<MobileSettingsPage> {
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
appText('设置', 'Settings'),
|
||||
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
style: Theme.of(context).textTheme.headlineSmall
|
||||
?.copyWith(fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
if (availableTabs.length > 1) ...[
|
||||
|
||||
@ -101,9 +101,6 @@ class _SettingsAccountPanelState extends State<SettingsAccountPanel>
|
||||
Tab(text: appText('svc.plus 云端同步', 'svc.plus Cloud Sync')),
|
||||
Tab(text: appText('手动 Bridge 配置', 'Manual Bridge Config')),
|
||||
],
|
||||
onTap: (index) {
|
||||
widget.onSaveAccountProfile(isManualBridge: index == 1);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
SizedBox(
|
||||
|
||||
@ -220,34 +220,20 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
SettingsSnapshot settings, {
|
||||
required bool isManualBridge,
|
||||
}) async {
|
||||
final bridgeConfig = settings.acpBridgeServerModeConfig;
|
||||
var nextBridgeConfig = bridgeConfig.copyWith(
|
||||
selfHosted: bridgeConfig.selfHosted.copyWith(
|
||||
serverUrl: _bridgeUrlController.text.trim(),
|
||||
username: isManualBridge ? 'admin' : bridgeConfig.selfHosted.username,
|
||||
),
|
||||
final nextSettings = await widget.controller.settingsController
|
||||
.buildSavedAccountProfileSettings(
|
||||
settings: settings,
|
||||
accountBaseUrl: _accountBaseUrlController.text,
|
||||
accountIdentifier: _accountIdentifierController.text,
|
||||
bridgeServerUrl: _bridgeUrlController.text,
|
||||
bridgeToken: _bridgeTokenController.text,
|
||||
isManualBridge: isManualBridge,
|
||||
);
|
||||
await widget.controller.saveSettings(
|
||||
nextSettings,
|
||||
refreshAfterSave: !isManualBridge,
|
||||
);
|
||||
|
||||
final nextEffective = widget.controller.settingsController
|
||||
.resolveAcpBridgeServerEffectiveConfig(config: nextBridgeConfig);
|
||||
|
||||
final nextSettings = settings.copyWith(
|
||||
accountBaseUrl: _accountBaseUrlController.text.trim(),
|
||||
accountUsername: _accountIdentifierController.text.trim(),
|
||||
acpBridgeServerModeConfig: nextBridgeConfig.copyWith(
|
||||
effective: nextEffective,
|
||||
),
|
||||
);
|
||||
if (isManualBridge && _bridgeTokenController.text.isNotEmpty) {
|
||||
await widget.controller.settingsController.saveSecretValueByRef(
|
||||
nextSettings.acpBridgeServerModeConfig.selfHosted.passwordRef,
|
||||
_bridgeTokenController.text,
|
||||
provider: 'Bridge',
|
||||
module: 'Manual',
|
||||
);
|
||||
}
|
||||
await widget.controller.saveSettings(nextSettings);
|
||||
|
||||
_lastSavedAccountBaseUrl = nextSettings.accountBaseUrl;
|
||||
_lastSavedAccountIdentifier = nextSettings.accountUsername;
|
||||
_lastSavedBridgeUrl =
|
||||
|
||||
@ -120,6 +120,23 @@ extension SettingsControllerAccountExtension on SettingsController {
|
||||
required AcpBridgeServerModeConfig config,
|
||||
}) => resolveAcpBridgeServerEffectiveConfigInternal(this, config: config);
|
||||
|
||||
Future<SettingsSnapshot> buildSavedAccountProfileSettings({
|
||||
required SettingsSnapshot settings,
|
||||
required String accountBaseUrl,
|
||||
required String accountIdentifier,
|
||||
required String bridgeServerUrl,
|
||||
required String bridgeToken,
|
||||
required bool isManualBridge,
|
||||
}) => buildSavedAccountProfileSettingsInternal(
|
||||
this,
|
||||
settings: settings,
|
||||
accountBaseUrl: accountBaseUrl,
|
||||
accountIdentifier: accountIdentifier,
|
||||
bridgeServerUrl: bridgeServerUrl,
|
||||
bridgeToken: bridgeToken,
|
||||
isManualBridge: isManualBridge,
|
||||
);
|
||||
|
||||
List<SecretReferenceEntry> buildSecretReferences() {
|
||||
final entries = <SecretReferenceEntry>[
|
||||
...secureRefsInternal.entries.map(
|
||||
|
||||
@ -612,6 +612,47 @@ AcpBridgeServerEffectiveConfig resolveAcpBridgeServerEffectiveConfigInternal(
|
||||
);
|
||||
}
|
||||
|
||||
Future<SettingsSnapshot> buildSavedAccountProfileSettingsInternal(
|
||||
SettingsController controller, {
|
||||
required SettingsSnapshot settings,
|
||||
required String accountBaseUrl,
|
||||
required String accountIdentifier,
|
||||
required String bridgeServerUrl,
|
||||
required String bridgeToken,
|
||||
required bool isManualBridge,
|
||||
}) async {
|
||||
final bridgeConfig = settings.acpBridgeServerModeConfig;
|
||||
final nextBridgeConfig = bridgeConfig.copyWith(
|
||||
selfHosted: isManualBridge
|
||||
? bridgeConfig.selfHosted.copyWith(
|
||||
serverUrl: bridgeServerUrl.trim(),
|
||||
username: 'admin',
|
||||
)
|
||||
: bridgeConfig.selfHosted,
|
||||
);
|
||||
final nextEffective = resolveAcpBridgeServerEffectiveConfigInternal(
|
||||
controller,
|
||||
config: nextBridgeConfig,
|
||||
);
|
||||
final nextSettings = settings.copyWith(
|
||||
accountBaseUrl: accountBaseUrl.trim(),
|
||||
accountUsername: accountIdentifier.trim(),
|
||||
acpBridgeServerModeConfig: nextBridgeConfig.copyWith(
|
||||
effective: nextEffective,
|
||||
),
|
||||
);
|
||||
final trimmedBridgeToken = bridgeToken.trim();
|
||||
if (isManualBridge && trimmedBridgeToken.isNotEmpty) {
|
||||
await controller.saveSecretValueByRef(
|
||||
nextSettings.acpBridgeServerModeConfig.selfHosted.passwordRef,
|
||||
trimmedBridgeToken,
|
||||
provider: 'Bridge',
|
||||
module: 'Manual',
|
||||
);
|
||||
}
|
||||
return nextSettings;
|
||||
}
|
||||
|
||||
int _parseExpiresAtMs(Object? value) {
|
||||
if (value is int) {
|
||||
return value;
|
||||
|
||||
@ -5,6 +5,8 @@ import 'package:xworkmate/app/app_shell_desktop.dart';
|
||||
import 'package:xworkmate/features/mobile/mobile_settings_page.dart';
|
||||
import 'package:xworkmate/runtime/account_runtime_client.dart';
|
||||
import 'package:xworkmate/runtime/runtime_controllers.dart';
|
||||
import 'package:xworkmate/runtime/runtime_models.dart';
|
||||
import 'package:xworkmate/runtime/secure_config_store.dart';
|
||||
import 'package:xworkmate/theme/app_theme.dart';
|
||||
|
||||
void main() {
|
||||
@ -239,6 +241,78 @@ void main() {
|
||||
);
|
||||
expect(find.text('mobile@svc.plus'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('manual bridge save updates mobile runtime configuration', (
|
||||
tester,
|
||||
) async {
|
||||
final store = _MemorySecureConfigStore();
|
||||
final controller = _NoopRefreshAppController(store: store);
|
||||
addTearDown(controller.dispose);
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
theme: AppTheme.light().copyWith(platform: TargetPlatform.iOS),
|
||||
home: MediaQuery(
|
||||
data: const MediaQueryData(size: Size(390, 844)),
|
||||
child: Scaffold(body: MobileSettingsPage(controller: controller)),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pump(const Duration(milliseconds: 250));
|
||||
|
||||
final urlField = find.byKey(
|
||||
const Key('mobile-settings-manual-bridge-url-field'),
|
||||
);
|
||||
await tester.ensureVisible(urlField);
|
||||
await tester.enterText(
|
||||
find.descendant(of: urlField, matching: find.byType(TextFormField)),
|
||||
'http://127.0.0.1:1',
|
||||
);
|
||||
final tokenField = find.byKey(
|
||||
const Key('mobile-settings-manual-bridge-token-field'),
|
||||
);
|
||||
await tester.enterText(
|
||||
find.descendant(of: tokenField, matching: find.byType(TextFormField)),
|
||||
'mobile-manual-token',
|
||||
);
|
||||
final saveButton = find.byKey(
|
||||
const Key('mobile-settings-manual-bridge-save-button'),
|
||||
);
|
||||
await tester.ensureVisible(saveButton);
|
||||
tester.widget<FilledButton>(saveButton).onPressed!();
|
||||
|
||||
for (
|
||||
var attempt = 0;
|
||||
attempt < 20 &&
|
||||
controller
|
||||
.settings
|
||||
.acpBridgeServerModeConfig
|
||||
.selfHosted
|
||||
.serverUrl !=
|
||||
'http://127.0.0.1:1';
|
||||
attempt += 1
|
||||
) {
|
||||
await tester.pump(const Duration(milliseconds: 50));
|
||||
}
|
||||
|
||||
final bridgeConfig = controller.settings.acpBridgeServerModeConfig;
|
||||
expect(bridgeConfig.selfHosted.serverUrl, 'http://127.0.0.1:1');
|
||||
expect(bridgeConfig.effective.source, 'bridge');
|
||||
expect(
|
||||
await store.loadSecretValueByRef(bridgeConfig.selfHosted.passwordRef),
|
||||
'mobile-manual-token',
|
||||
);
|
||||
expect(
|
||||
controller.resolveGatewayAcpEndpointInternal()?.toString(),
|
||||
'http://127.0.0.1:1',
|
||||
);
|
||||
expect(
|
||||
await controller.resolveGatewayAcpAuthorizationHeaderInternal(
|
||||
Uri.parse('http://127.0.0.1:1/acp/rpc'),
|
||||
),
|
||||
'mobile-manual-token',
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@ -278,3 +352,63 @@ class _MobileFakeAccountRuntimeClient extends AccountRuntimeClient {
|
||||
return syncPayload;
|
||||
}
|
||||
}
|
||||
|
||||
class _NoopRefreshAppController extends AppController {
|
||||
_NoopRefreshAppController({required SecureConfigStore store})
|
||||
: super(environmentOverride: const <String, String>{}, store: store);
|
||||
|
||||
Future<void> refreshAcpCapabilitiesInternal({
|
||||
bool forceRefresh = false,
|
||||
bool persistMountTargets = false,
|
||||
}) async {}
|
||||
|
||||
Future<void> refreshSingleAgentCapabilitiesInternal({
|
||||
bool forceRefresh = false,
|
||||
}) async {}
|
||||
}
|
||||
|
||||
class _MemorySecureConfigStore extends SecureConfigStore {
|
||||
_MemorySecureConfigStore() : super(enableSecureStorage: false);
|
||||
|
||||
SettingsSnapshot _settings = SettingsSnapshot.defaults();
|
||||
final Map<String, String> _secrets = <String, String>{};
|
||||
|
||||
@override
|
||||
Future<void> initialize() async {}
|
||||
|
||||
@override
|
||||
Future<SettingsSnapshot> loadSettingsSnapshot() async => _settings;
|
||||
|
||||
@override
|
||||
Future<void> saveSettingsSnapshot(SettingsSnapshot snapshot) async {
|
||||
_settings = snapshot;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Map<String, String>> loadSecureRefs() async => _secrets;
|
||||
|
||||
@override
|
||||
Future<List<SecretAuditEntry>> loadAuditTrail() async =>
|
||||
const <SecretAuditEntry>[];
|
||||
|
||||
@override
|
||||
Future<void> appendAudit(SecretAuditEntry entry) async {}
|
||||
|
||||
@override
|
||||
Future<String?> loadSecretValueByRef(String refName) async =>
|
||||
_secrets[refName];
|
||||
|
||||
@override
|
||||
Future<void> saveSecretValueByRef(String refName, String value) async {
|
||||
_secrets[refName] = value;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String?> loadAccountSessionToken() async => null;
|
||||
|
||||
@override
|
||||
Future<AccountSessionSummary?> loadAccountSessionSummary() async => null;
|
||||
|
||||
@override
|
||||
Future<AccountSyncState?> loadAccountSyncState() async => null;
|
||||
}
|
||||
|
||||
@ -1,7 +1,10 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:xworkmate/app/app_controller.dart';
|
||||
import 'package:xworkmate/features/settings/settings_account_panel.dart';
|
||||
import 'package:xworkmate/runtime/runtime_controllers.dart';
|
||||
import 'package:xworkmate/runtime/runtime_models.dart';
|
||||
import 'package:xworkmate/runtime/secure_config_store.dart';
|
||||
import 'package:xworkmate/theme/app_theme.dart';
|
||||
import 'package:xworkmate/widgets/surface_card.dart';
|
||||
|
||||
@ -173,6 +176,144 @@ void main() {
|
||||
expect(savedBridgeToken, 'typed-manual-token');
|
||||
});
|
||||
|
||||
testWidgets('switching to manual bridge tab does not save draft values', (
|
||||
tester,
|
||||
) async {
|
||||
final controllers = _TestControllers();
|
||||
addTearDown(controllers.dispose);
|
||||
|
||||
var saveCount = 0;
|
||||
|
||||
await tester.pumpWidget(
|
||||
_buildTestApp(
|
||||
child: SettingsAccountPanel(
|
||||
settings: SettingsSnapshot.defaults(),
|
||||
accountSession: null,
|
||||
accountState: null,
|
||||
accountBusy: false,
|
||||
accountSignedIn: false,
|
||||
accountMfaRequired: false,
|
||||
accountBaseUrlController: controllers.baseUrl,
|
||||
accountIdentifierController: controllers.identifier,
|
||||
accountPasswordController: controllers.password,
|
||||
accountMfaCodeController: controllers.mfaCode,
|
||||
bridgeUrlController: controllers.bridgeUrl,
|
||||
bridgeTokenController: controllers.bridgeToken,
|
||||
onSaveAccountProfile: ({required bool isManualBridge}) async {
|
||||
saveCount += 1;
|
||||
},
|
||||
onLogin: () async {},
|
||||
onVerifyMfa: () async {},
|
||||
onCancelMfa: () async {},
|
||||
onSync: () async {},
|
||||
onLogout: () async {},
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.tap(find.text('手动 Bridge 配置'));
|
||||
await tester.pump();
|
||||
|
||||
expect(saveCount, 0);
|
||||
});
|
||||
|
||||
testWidgets('desktop manual bridge save updates runtime configuration', (
|
||||
tester,
|
||||
) async {
|
||||
final controllers = _TestControllers();
|
||||
addTearDown(controllers.dispose);
|
||||
final store = _MemorySecureConfigStore();
|
||||
final controller = _NoopRefreshAppController(store: store);
|
||||
addTearDown(controller.dispose);
|
||||
|
||||
await tester.pumpWidget(
|
||||
_buildTestApp(
|
||||
child: SettingsAccountPanel(
|
||||
settings: controller.settings,
|
||||
accountSession: null,
|
||||
accountState: null,
|
||||
accountBusy: false,
|
||||
accountSignedIn: false,
|
||||
accountMfaRequired: false,
|
||||
accountBaseUrlController: controllers.baseUrl,
|
||||
accountIdentifierController: controllers.identifier,
|
||||
accountPasswordController: controllers.password,
|
||||
accountMfaCodeController: controllers.mfaCode,
|
||||
bridgeUrlController: controllers.bridgeUrl,
|
||||
bridgeTokenController: controllers.bridgeToken,
|
||||
onSaveAccountProfile: ({required bool isManualBridge}) async {
|
||||
final nextSettings = await controller.settingsController
|
||||
.buildSavedAccountProfileSettings(
|
||||
settings: controller.settings,
|
||||
accountBaseUrl: controllers.baseUrl.text,
|
||||
accountIdentifier: controllers.identifier.text,
|
||||
bridgeServerUrl: controllers.bridgeUrl.text,
|
||||
bridgeToken: controllers.bridgeToken.text,
|
||||
isManualBridge: isManualBridge,
|
||||
);
|
||||
await controller.saveSettings(
|
||||
nextSettings,
|
||||
refreshAfterSave: false,
|
||||
);
|
||||
},
|
||||
onLogin: () async {},
|
||||
onVerifyMfa: () async {},
|
||||
onCancelMfa: () async {},
|
||||
onSync: () async {},
|
||||
onLogout: () async {},
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.tap(find.text('手动 Bridge 配置'));
|
||||
await tester.pump();
|
||||
await tester.enterText(
|
||||
find.byKey(const ValueKey('settings-manual-bridge-url-field')),
|
||||
'http://127.0.0.1:1',
|
||||
);
|
||||
await tester.enterText(
|
||||
find.byKey(const ValueKey('settings-manual-bridge-token-field')),
|
||||
'typed-manual-token',
|
||||
);
|
||||
await tester.tap(
|
||||
find.byKey(const ValueKey('settings-manual-bridge-save-button')),
|
||||
);
|
||||
|
||||
for (
|
||||
var attempt = 0;
|
||||
attempt < 20 &&
|
||||
controller
|
||||
.settings
|
||||
.acpBridgeServerModeConfig
|
||||
.selfHosted
|
||||
.serverUrl !=
|
||||
'http://127.0.0.1:1';
|
||||
attempt += 1
|
||||
) {
|
||||
await tester.pump(const Duration(milliseconds: 50));
|
||||
}
|
||||
|
||||
final bridgeConfig = controller.settings.acpBridgeServerModeConfig;
|
||||
expect(bridgeConfig.selfHosted.serverUrl, 'http://127.0.0.1:1');
|
||||
expect(bridgeConfig.selfHosted.username, 'admin');
|
||||
expect(bridgeConfig.effective.source, 'bridge');
|
||||
expect(bridgeConfig.effective.endpoint, 'http://127.0.0.1:1');
|
||||
expect(
|
||||
await store.loadSecretValueByRef(bridgeConfig.selfHosted.passwordRef),
|
||||
'typed-manual-token',
|
||||
);
|
||||
expect(
|
||||
controller.resolveGatewayAcpEndpointInternal()?.toString(),
|
||||
'http://127.0.0.1:1',
|
||||
);
|
||||
expect(
|
||||
await controller.resolveGatewayAcpAuthorizationHeaderInternal(
|
||||
Uri.parse('http://127.0.0.1:1/acp/rpc'),
|
||||
),
|
||||
'typed-manual-token',
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'shows account sync status, resync, and exit in signed-in mode',
|
||||
(tester) async {
|
||||
@ -562,3 +703,63 @@ class _TestControllers {
|
||||
bridgeToken.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
class _NoopRefreshAppController extends AppController {
|
||||
_NoopRefreshAppController({required SecureConfigStore store})
|
||||
: super(environmentOverride: const <String, String>{}, store: store);
|
||||
|
||||
Future<void> refreshAcpCapabilitiesInternal({
|
||||
bool forceRefresh = false,
|
||||
bool persistMountTargets = false,
|
||||
}) async {}
|
||||
|
||||
Future<void> refreshSingleAgentCapabilitiesInternal({
|
||||
bool forceRefresh = false,
|
||||
}) async {}
|
||||
}
|
||||
|
||||
class _MemorySecureConfigStore extends SecureConfigStore {
|
||||
_MemorySecureConfigStore() : super(enableSecureStorage: false);
|
||||
|
||||
SettingsSnapshot _settings = SettingsSnapshot.defaults();
|
||||
final Map<String, String> _secrets = <String, String>{};
|
||||
|
||||
@override
|
||||
Future<void> initialize() async {}
|
||||
|
||||
@override
|
||||
Future<SettingsSnapshot> loadSettingsSnapshot() async => _settings;
|
||||
|
||||
@override
|
||||
Future<void> saveSettingsSnapshot(SettingsSnapshot snapshot) async {
|
||||
_settings = snapshot;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Map<String, String>> loadSecureRefs() async => _secrets;
|
||||
|
||||
@override
|
||||
Future<List<SecretAuditEntry>> loadAuditTrail() async =>
|
||||
const <SecretAuditEntry>[];
|
||||
|
||||
@override
|
||||
Future<void> appendAudit(SecretAuditEntry entry) async {}
|
||||
|
||||
@override
|
||||
Future<String?> loadSecretValueByRef(String refName) async =>
|
||||
_secrets[refName];
|
||||
|
||||
@override
|
||||
Future<void> saveSecretValueByRef(String refName, String value) async {
|
||||
_secrets[refName] = value;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String?> loadAccountSessionToken() async => null;
|
||||
|
||||
@override
|
||||
Future<AccountSessionSummary?> loadAccountSessionSummary() async => null;
|
||||
|
||||
@override
|
||||
Future<AccountSyncState?> loadAccountSyncState() async => null;
|
||||
}
|
||||
|
||||
@ -168,6 +168,81 @@ void main() {
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'manual bridge token authorizes runtime and artifact requests only for manual endpoint',
|
||||
() async {
|
||||
final storeRoot = await Directory.systemTemp.createTemp(
|
||||
'xworkmate-manual-bridge-artifact-auth-',
|
||||
);
|
||||
addTearDown(() async {
|
||||
if (await storeRoot.exists()) {
|
||||
try {
|
||||
await storeRoot.delete(recursive: true);
|
||||
} on FileSystemException {
|
||||
// Temp cleanup is best effort here. The controller may still be
|
||||
// releasing files when teardown starts.
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
final store = SecureConfigStore(
|
||||
secretRootPathResolver: () async => '${storeRoot.path}/secrets',
|
||||
appDataRootPathResolver: () async => '${storeRoot.path}/app-data',
|
||||
supportRootPathResolver: () async => '${storeRoot.path}/support',
|
||||
enableSecureStorage: false,
|
||||
);
|
||||
await store.initialize();
|
||||
await store.saveSettingsSnapshot(
|
||||
SettingsSnapshot.defaults().copyWith(
|
||||
acpBridgeServerModeConfig: AcpBridgeServerModeConfig.defaults()
|
||||
.copyWith(
|
||||
selfHosted: AcpBridgeServerModeConfig.defaults().selfHosted
|
||||
.copyWith(
|
||||
serverUrl: 'https://private-bridge.svc.plus',
|
||||
username: 'admin',
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await store.saveSecretValueByRef(
|
||||
AcpBridgeServerSelfHostedConfig.defaults().passwordRef,
|
||||
'manual-bridge-token',
|
||||
);
|
||||
|
||||
final controller = AppController(
|
||||
environmentOverride: const <String, String>{},
|
||||
store: store,
|
||||
);
|
||||
addTearDown(controller.dispose);
|
||||
await controller.settingsControllerInternal.initialize();
|
||||
|
||||
expect(
|
||||
await controller.resolveGatewayAcpAuthorizationHeaderInternal(
|
||||
Uri.parse('https://private-bridge.svc.plus/acp/rpc'),
|
||||
),
|
||||
'manual-bridge-token',
|
||||
);
|
||||
expect(
|
||||
await controller.resolveBridgeArtifactAuthorizationHeaderInternal(
|
||||
Uri.parse('https://private-bridge.svc.plus/artifacts/file.pdf'),
|
||||
),
|
||||
'Bearer manual-bridge-token',
|
||||
);
|
||||
expect(
|
||||
await controller.resolveGatewayAcpAuthorizationHeaderInternal(
|
||||
Uri.parse('$kManagedBridgeServerUrl/acp/rpc'),
|
||||
),
|
||||
isNull,
|
||||
);
|
||||
expect(
|
||||
await controller.resolveBridgeArtifactAuthorizationHeaderInternal(
|
||||
Uri.parse('$kManagedBridgeServerUrl/artifacts/file.pdf'),
|
||||
),
|
||||
isNull,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'runtime coordinator only exposes remote and offline gateway modes',
|
||||
() {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user