Remove desktop bridge legacy chain
This commit is contained in:
parent
add6d0eb95
commit
895c34dd9d
@ -12,7 +12,7 @@ mobile:
|
||||
description: Mobile assistant destination
|
||||
ui_surface: mobile_shell
|
||||
tasks:
|
||||
enabled: true
|
||||
enabled: false
|
||||
release_tier: stable
|
||||
build_modes: [debug, profile, release]
|
||||
description: Mobile tasks destination
|
||||
@ -37,25 +37,25 @@ mobile:
|
||||
ui_surface: mobile_shell
|
||||
workspace:
|
||||
skills:
|
||||
enabled: true
|
||||
enabled: false
|
||||
release_tier: stable
|
||||
build_modes: [debug, profile, release]
|
||||
description: Mobile workspace skills launcher
|
||||
ui_surface: mobile_workspace_hub
|
||||
nodes:
|
||||
enabled: true
|
||||
enabled: false
|
||||
release_tier: stable
|
||||
build_modes: [debug, profile, release]
|
||||
description: Mobile workspace nodes launcher
|
||||
ui_surface: mobile_workspace_hub
|
||||
agents:
|
||||
enabled: true
|
||||
enabled: false
|
||||
release_tier: stable
|
||||
build_modes: [debug, profile, release]
|
||||
description: Mobile workspace agents launcher
|
||||
ui_surface: mobile_workspace_hub
|
||||
mcp_server:
|
||||
enabled: true
|
||||
enabled: false
|
||||
release_tier: experimental
|
||||
build_modes: [debug, profile, release]
|
||||
description: Mobile workspace MCP launcher
|
||||
@ -264,13 +264,13 @@ desktop:
|
||||
description: Desktop ClawHub destination
|
||||
ui_surface: sidebar_navigation
|
||||
secrets:
|
||||
enabled: true
|
||||
enabled: false
|
||||
release_tier: stable
|
||||
build_modes: [debug, profile, release]
|
||||
description: Desktop secrets destination
|
||||
ui_surface: sidebar_navigation
|
||||
ai_gateway:
|
||||
enabled: true
|
||||
enabled: false
|
||||
release_tier: stable
|
||||
build_modes: [debug, profile, release]
|
||||
description: Desktop AI Gateway destination
|
||||
@ -282,7 +282,7 @@ desktop:
|
||||
description: Desktop settings destination
|
||||
ui_surface: sidebar_navigation
|
||||
account:
|
||||
enabled: true
|
||||
enabled: false
|
||||
release_tier: stable
|
||||
build_modes: [debug, profile, release]
|
||||
description: Desktop account destination
|
||||
|
||||
@ -655,7 +655,12 @@ extension AppControllerDesktopRuntimeHelpers on AppController {
|
||||
}
|
||||
|
||||
Uri? resolveBridgeAcpEndpointInternal() {
|
||||
final uri = Uri.tryParse(kManagedBridgeServerUrl);
|
||||
final syncedBridgeServerUrl =
|
||||
settingsControllerInternal.accountProfile?.bridgeServerUrl.trim() ?? '';
|
||||
final candidate = isSupportedExternalAcpEndpoint(syncedBridgeServerUrl)
|
||||
? syncedBridgeServerUrl
|
||||
: kManagedBridgeServerUrl;
|
||||
final uri = Uri.tryParse(candidate);
|
||||
final scheme = uri?.scheme.trim().toLowerCase() ?? '';
|
||||
if (uri == null || !kSupportedExternalAcpEndpointSchemes.contains(scheme)) {
|
||||
return null;
|
||||
|
||||
@ -26,6 +26,10 @@ class _AppShellState extends State<AppShell> {
|
||||
static const _sidebarViewportPadding = 72.0;
|
||||
static const _mainContentMinWidth = 640.0;
|
||||
static const _sidebarExpandedBaseWidth = 336.0;
|
||||
static const _desktopDestinations = <WorkspaceDestination>[
|
||||
WorkspaceDestination.assistant,
|
||||
WorkspaceDestination.settings,
|
||||
];
|
||||
double? _sidebarExpandedWidth;
|
||||
|
||||
static const _mobileDestinations = [
|
||||
@ -482,14 +486,29 @@ class _AppShellState extends State<AppShell> {
|
||||
}
|
||||
|
||||
Widget _buildCurrentPage(ValueChanged<DetailPanelData> onOpenDetail) {
|
||||
final currentDestination = _resolveDesktopDestination(
|
||||
widget.controller.destination,
|
||||
);
|
||||
return IndexedStack(
|
||||
index: widget.controller.destination.index,
|
||||
children: WorkspaceDestination.values
|
||||
index: _desktopDestinations.indexOf(currentDestination),
|
||||
children: _desktopDestinations
|
||||
.map((destination) => _pageForDestination(destination, onOpenDetail))
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
|
||||
WorkspaceDestination _resolveDesktopDestination(
|
||||
WorkspaceDestination destination,
|
||||
) {
|
||||
if (destination == WorkspaceDestination.account) {
|
||||
return WorkspaceDestination.settings;
|
||||
}
|
||||
if (_desktopDestinations.contains(destination)) {
|
||||
return destination;
|
||||
}
|
||||
return WorkspaceDestination.assistant;
|
||||
}
|
||||
|
||||
Widget _pageForDestination(
|
||||
WorkspaceDestination destination,
|
||||
ValueChanged<DetailPanelData> onOpenDetail,
|
||||
|
||||
@ -399,16 +399,7 @@ class UiFeatureAccess {
|
||||
},
|
||||
UiFeaturePlatform.desktop: <String, WorkspaceDestination>{
|
||||
UiFeatureKeys.navigationAssistant: WorkspaceDestination.assistant,
|
||||
UiFeatureKeys.navigationTasks: WorkspaceDestination.tasks,
|
||||
UiFeatureKeys.navigationSkills: WorkspaceDestination.skills,
|
||||
UiFeatureKeys.navigationNodes: WorkspaceDestination.nodes,
|
||||
UiFeatureKeys.navigationAgents: WorkspaceDestination.agents,
|
||||
UiFeatureKeys.navigationMcpServer: WorkspaceDestination.mcpServer,
|
||||
UiFeatureKeys.navigationClawHub: WorkspaceDestination.clawHub,
|
||||
UiFeatureKeys.navigationSecrets: WorkspaceDestination.secrets,
|
||||
UiFeatureKeys.navigationAiGateway: WorkspaceDestination.aiGateway,
|
||||
UiFeatureKeys.navigationSettings: WorkspaceDestination.settings,
|
||||
UiFeatureKeys.navigationAccount: WorkspaceDestination.account,
|
||||
},
|
||||
UiFeaturePlatform.web: <String, WorkspaceDestination>{
|
||||
UiFeatureKeys.navigationAssistant: WorkspaceDestination.assistant,
|
||||
|
||||
@ -23,7 +23,7 @@ mobile:
|
||||
description: Mobile assistant destination
|
||||
ui_surface: mobile_shell
|
||||
tasks:
|
||||
enabled: true
|
||||
enabled: false
|
||||
release_tier: stable
|
||||
build_modes: [debug, profile, release]
|
||||
description: Mobile tasks destination
|
||||
@ -48,25 +48,25 @@ mobile:
|
||||
ui_surface: mobile_shell
|
||||
workspace:
|
||||
skills:
|
||||
enabled: true
|
||||
enabled: false
|
||||
release_tier: stable
|
||||
build_modes: [debug, profile, release]
|
||||
description: Mobile workspace skills launcher
|
||||
ui_surface: mobile_workspace_hub
|
||||
nodes:
|
||||
enabled: true
|
||||
enabled: false
|
||||
release_tier: stable
|
||||
build_modes: [debug, profile, release]
|
||||
description: Mobile workspace nodes launcher
|
||||
ui_surface: mobile_workspace_hub
|
||||
agents:
|
||||
enabled: true
|
||||
enabled: false
|
||||
release_tier: stable
|
||||
build_modes: [debug, profile, release]
|
||||
description: Mobile workspace agents launcher
|
||||
ui_surface: mobile_workspace_hub
|
||||
mcp_server:
|
||||
enabled: true
|
||||
enabled: false
|
||||
release_tier: stable
|
||||
build_modes: [debug, profile, release]
|
||||
description: Mobile workspace MCP launcher
|
||||
@ -275,13 +275,13 @@ desktop:
|
||||
description: Desktop ClawHub destination
|
||||
ui_surface: sidebar_navigation
|
||||
secrets:
|
||||
enabled: true
|
||||
enabled: false
|
||||
release_tier: stable
|
||||
build_modes: [debug, profile, release]
|
||||
description: Desktop secrets destination
|
||||
ui_surface: sidebar_navigation
|
||||
ai_gateway:
|
||||
enabled: true
|
||||
enabled: false
|
||||
release_tier: stable
|
||||
build_modes: [debug, profile, release]
|
||||
description: Desktop LLM API destination
|
||||
@ -293,7 +293,7 @@ desktop:
|
||||
description: Desktop settings destination
|
||||
ui_surface: sidebar_navigation
|
||||
account:
|
||||
enabled: true
|
||||
enabled: false
|
||||
release_tier: stable
|
||||
build_modes: [debug, profile, release]
|
||||
description: Desktop account destination
|
||||
|
||||
@ -136,10 +136,7 @@ extension SettingsControllerAccountExtension on SettingsController {
|
||||
accountSessionTokenInternal =
|
||||
(await storeInternal.loadAccountSessionToken())?.trim() ?? '';
|
||||
accountSessionInternal = await storeInternal.loadAccountSessionSummary();
|
||||
accountSyncStateInternal = await recoverBridgeAccountSyncStateInternal(
|
||||
this,
|
||||
await storeInternal.loadAccountSyncState(),
|
||||
);
|
||||
accountSyncStateInternal = await storeInternal.loadAccountSyncState();
|
||||
if (!accountBusyInternal) {
|
||||
if (accountSignedIn) {
|
||||
final email = accountSessionInternal?.email.trim() ?? '';
|
||||
|
||||
@ -146,6 +146,7 @@ Future<void> completeAccountSignInSettingsInternal(
|
||||
controller,
|
||||
baseUrl: baseUrl,
|
||||
bridgeTokenOverride: _resolveBridgeAuthorizationToken(payload),
|
||||
bridgeServerUrlOverride: _resolveBridgeServerUrl(payload),
|
||||
quiet: true,
|
||||
);
|
||||
await controller.reloadDerivedStateInternal();
|
||||
@ -223,6 +224,7 @@ Future<AccountSyncResult> syncAccountSettingsInternal(
|
||||
String baseUrl = '',
|
||||
bool quiet = false,
|
||||
String bridgeTokenOverride = '',
|
||||
String bridgeServerUrlOverride = '',
|
||||
}) async {
|
||||
final sessionToken =
|
||||
(await controller.storeInternal.loadAccountSessionToken())?.trim() ?? '';
|
||||
@ -286,7 +288,10 @@ Future<AccountSyncResult> syncAccountSettingsInternal(
|
||||
target: kAccountManagedSecretTargetBridgeAuthToken,
|
||||
value: bridgeToken,
|
||||
);
|
||||
const resolvedBridgeServerUrl = kManagedBridgeServerUrl;
|
||||
final resolvedBridgeServerUrl = _resolveCurrentBridgeServerUrl(
|
||||
controller,
|
||||
bridgeServerUrlOverride: bridgeServerUrlOverride,
|
||||
);
|
||||
await controller.storeInternal.clearAccountManagedSecret(
|
||||
target: kAccountManagedSecretTargetAIGatewayAccessToken,
|
||||
);
|
||||
@ -346,55 +351,6 @@ Future<AccountSyncResult> syncAccountSettingsInternal(
|
||||
);
|
||||
}
|
||||
|
||||
Future<AccountSyncState?> recoverBridgeAccountSyncStateInternal(
|
||||
SettingsController controller,
|
||||
AccountSyncState? currentState,
|
||||
) async {
|
||||
final currentBridgeServerUrl =
|
||||
currentState?.syncedDefaults.bridgeServerUrl.trim() ?? '';
|
||||
if (currentBridgeServerUrl.isNotEmpty) {
|
||||
return currentState;
|
||||
}
|
||||
|
||||
final cloudSynced =
|
||||
controller.snapshotInternal.acpBridgeServerModeConfig.cloudSynced;
|
||||
final legacyBridgeServerUrl = cloudSynced.remoteServerSummary.endpoint.trim();
|
||||
if (!isSupportedExternalAcpEndpoint(legacyBridgeServerUrl)) {
|
||||
return currentState;
|
||||
}
|
||||
|
||||
final defaults = AccountSyncState.defaults();
|
||||
final baseline = currentState ?? defaults;
|
||||
final hasBridgeToken = controller.secureRefsInternal.containsKey(
|
||||
kAccountManagedSecretTargetBridgeAuthToken,
|
||||
);
|
||||
final recoveredState = baseline.copyWith(
|
||||
syncedDefaults: baseline.syncedDefaults.copyWith(
|
||||
bridgeServerUrl: legacyBridgeServerUrl,
|
||||
),
|
||||
syncState: baseline.syncState == defaults.syncState
|
||||
? 'ready'
|
||||
: baseline.syncState,
|
||||
syncMessage: baseline.syncMessage == defaults.syncMessage
|
||||
? 'Bridge access synced'
|
||||
: baseline.syncMessage,
|
||||
lastSyncAtMs: baseline.lastSyncAtMs > 0
|
||||
? baseline.lastSyncAtMs
|
||||
: cloudSynced.lastSyncAt,
|
||||
lastSyncSource: baseline.lastSyncSource.trim().isNotEmpty
|
||||
? baseline.lastSyncSource
|
||||
: legacyBridgeServerUrl,
|
||||
profileScope: baseline.profileScope.trim().isNotEmpty
|
||||
? baseline.profileScope
|
||||
: 'bridge',
|
||||
tokenConfigured: baseline.tokenConfigured.copyWith(
|
||||
bridge: baseline.tokenConfigured.bridge || hasBridgeToken,
|
||||
),
|
||||
);
|
||||
await controller.storeInternal.saveAccountSyncState(recoveredState);
|
||||
return recoveredState;
|
||||
}
|
||||
|
||||
Future<void> logoutAccountSettingsInternal(
|
||||
SettingsController controller, {
|
||||
String statusMessage = 'Signed out',
|
||||
@ -518,6 +474,36 @@ String _resolveBridgeAuthorizationToken(Map<String, dynamic> payload) {
|
||||
return '';
|
||||
}
|
||||
|
||||
String _resolveBridgeServerUrl(Map<String, dynamic> payload) {
|
||||
final explicit = _stringValue(payload['BRIDGE_SERVER_URL']);
|
||||
if (explicit.isNotEmpty) {
|
||||
return explicit;
|
||||
}
|
||||
final camelCase = _stringValue(payload['bridgeServerUrl']);
|
||||
if (camelCase.isNotEmpty) {
|
||||
return camelCase;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
String _resolveCurrentBridgeServerUrl(
|
||||
SettingsController controller, {
|
||||
String bridgeServerUrlOverride = '',
|
||||
}) {
|
||||
final explicit = bridgeServerUrlOverride.trim();
|
||||
if (isSupportedExternalAcpEndpoint(explicit)) {
|
||||
return explicit;
|
||||
}
|
||||
final syncedBridgeServerUrl =
|
||||
controller.accountSyncStateInternal?.syncedDefaults.bridgeServerUrl
|
||||
.trim() ??
|
||||
'';
|
||||
if (isSupportedExternalAcpEndpoint(syncedBridgeServerUrl)) {
|
||||
return syncedBridgeServerUrl;
|
||||
}
|
||||
return kManagedBridgeServerUrl;
|
||||
}
|
||||
|
||||
int _parseExpiresAtMs(Object? value) {
|
||||
if (value is int) {
|
||||
return value;
|
||||
|
||||
@ -1,11 +1,63 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:xworkmate/app/app_controller.dart';
|
||||
import 'package:xworkmate/runtime/mode_switcher.dart';
|
||||
import 'package:xworkmate/runtime/runtime_models.dart';
|
||||
import 'package:xworkmate/runtime/secure_config_store.dart';
|
||||
|
||||
void main() {
|
||||
group('Bridge runtime cleanup', () {
|
||||
test('resolves the managed bridge endpoint without BRIDGE_SERVER_URL', () {
|
||||
test('resolves the current synced bridge endpoint before env leftovers', () async {
|
||||
final storeRoot = await Directory.systemTemp.createTemp(
|
||||
'xworkmate-bridge-runtime-cleanup-',
|
||||
);
|
||||
addTearDown(() async {
|
||||
if (await storeRoot.exists()) {
|
||||
await storeRoot.delete(recursive: true);
|
||||
}
|
||||
});
|
||||
|
||||
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.saveAccountSyncState(
|
||||
AccountSyncState.defaults().copyWith(
|
||||
syncedDefaults: AccountRemoteProfile.defaults().copyWith(
|
||||
bridgeServerUrl: 'https://xworkmate-bridge-alt.svc.plus',
|
||||
),
|
||||
syncState: 'ready',
|
||||
),
|
||||
);
|
||||
|
||||
final controller = AppController(
|
||||
store: store,
|
||||
environmentOverride: const <String, String>{
|
||||
'BRIDGE_SERVER_URL': 'https://stale.example.invalid',
|
||||
},
|
||||
);
|
||||
addTearDown(controller.dispose);
|
||||
await controller.settingsControllerInternal.initialize();
|
||||
|
||||
expect(
|
||||
controller.resolveBridgeAcpEndpointInternal()?.toString(),
|
||||
'https://xworkmate-bridge-alt.svc.plus',
|
||||
);
|
||||
expect(
|
||||
controller
|
||||
.resolveExternalAcpEndpointForTargetInternal(
|
||||
AssistantExecutionTarget.gateway,
|
||||
)
|
||||
?.toString(),
|
||||
'https://xworkmate-bridge-alt.svc.plus',
|
||||
);
|
||||
});
|
||||
|
||||
test('falls back to the managed bridge endpoint without BRIDGE_SERVER_URL', () {
|
||||
final controller = AppController(
|
||||
environmentOverride: const <String, String>{
|
||||
'BRIDGE_SERVER_URL': 'https://stale.example.invalid',
|
||||
@ -17,14 +69,6 @@ void main() {
|
||||
controller.resolveBridgeAcpEndpointInternal()?.toString(),
|
||||
kManagedBridgeServerUrl,
|
||||
);
|
||||
expect(
|
||||
controller
|
||||
.resolveExternalAcpEndpointForTargetInternal(
|
||||
AssistantExecutionTarget.gateway,
|
||||
)
|
||||
?.toString(),
|
||||
kManagedBridgeServerUrl,
|
||||
);
|
||||
});
|
||||
|
||||
test(
|
||||
|
||||
@ -60,7 +60,7 @@ void main() {
|
||||
);
|
||||
|
||||
test(
|
||||
'login sync accepts INTERNAL_SERVICE_TOKEN payload for managed bridge auth',
|
||||
'login sync stores the current bridge contract from login payload',
|
||||
() async {
|
||||
final storeRoot = await Directory.systemTemp.createTemp(
|
||||
'xworkmate-account-sync-uppercase-token-',
|
||||
@ -90,6 +90,7 @@ void main() {
|
||||
loginPayload: <String, dynamic>{
|
||||
'token': 'session-token',
|
||||
'INTERNAL_SERVICE_TOKEN': 'bridge-token-from-login',
|
||||
'BRIDGE_SERVER_URL': 'https://xworkmate-bridge-alt.svc.plus',
|
||||
'user': <String, dynamic>{
|
||||
'id': 'user-1',
|
||||
'email': 'review@svc.plus',
|
||||
@ -108,6 +109,19 @@ void main() {
|
||||
|
||||
expect(controller.accountSyncState, isNotNull);
|
||||
expect(controller.accountSyncState!.syncState, 'ready');
|
||||
expect(
|
||||
controller.accountSyncState!.syncedDefaults.bridgeServerUrl,
|
||||
'https://xworkmate-bridge-alt.svc.plus',
|
||||
);
|
||||
expect(
|
||||
controller
|
||||
.snapshot
|
||||
.acpBridgeServerModeConfig
|
||||
.cloudSynced
|
||||
.remoteServerSummary
|
||||
.endpoint,
|
||||
'https://xworkmate-bridge-alt.svc.plus',
|
||||
);
|
||||
expect(
|
||||
await store.loadAccountManagedSecret(
|
||||
target: kAccountManagedSecretTargetBridgeAuthToken,
|
||||
@ -172,7 +186,7 @@ void main() {
|
||||
});
|
||||
|
||||
test(
|
||||
'recovers bridge sync state from cloud-synced snapshot when support state is missing',
|
||||
'does not recover bridge sync state from stale cloud-synced snapshot state',
|
||||
() async {
|
||||
final storeRoot = await Directory.systemTemp.createTemp(
|
||||
'xworkmate-account-recover-',
|
||||
@ -220,20 +234,9 @@ void main() {
|
||||
addTearDown(controller.dispose);
|
||||
await controller.initialize();
|
||||
|
||||
expect(controller.accountSyncState, isNotNull);
|
||||
expect(
|
||||
controller.accountSyncState!.syncedDefaults.bridgeServerUrl,
|
||||
'https://bridge.svc.plus',
|
||||
);
|
||||
expect(controller.accountSyncState!.syncState, 'ready');
|
||||
expect(controller.accountSyncState!.profileScope, 'bridge');
|
||||
|
||||
expect(controller.accountSyncState, isNull);
|
||||
final persisted = await store.loadAccountSyncState();
|
||||
expect(persisted, isNotNull);
|
||||
expect(
|
||||
persisted!.syncedDefaults.bridgeServerUrl,
|
||||
'https://bridge.svc.plus',
|
||||
);
|
||||
expect(persisted, isNull);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
41
test/runtime/ui_feature_manifest_desktop_surface_test.dart
Normal file
41
test/runtime/ui_feature_manifest_desktop_surface_test.dart
Normal file
@ -0,0 +1,41 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:xworkmate/app/ui_feature_manifest.dart';
|
||||
import 'package:xworkmate/models/app_models.dart';
|
||||
|
||||
void main() {
|
||||
group('Desktop feature manifest cleanup', () {
|
||||
test('repo config only exposes assistant and settings on desktop', () {
|
||||
final raw = File('config/feature_flags.yaml').readAsStringSync();
|
||||
final manifest = UiFeatureManifest.fromYamlString(raw);
|
||||
final desktop = manifest.forPlatform(
|
||||
UiFeaturePlatform.desktop,
|
||||
buildMode: UiFeatureBuildMode.debug,
|
||||
);
|
||||
|
||||
expect(
|
||||
desktop.allowedDestinations,
|
||||
<WorkspaceDestination>{
|
||||
WorkspaceDestination.assistant,
|
||||
WorkspaceDestination.settings,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
test('fallback manifest only exposes assistant and settings on desktop', () {
|
||||
final desktop = UiFeatureManifest.fallback().forPlatform(
|
||||
UiFeaturePlatform.desktop,
|
||||
buildMode: UiFeatureBuildMode.debug,
|
||||
);
|
||||
|
||||
expect(
|
||||
desktop.allowedDestinations,
|
||||
<WorkspaceDestination>{
|
||||
WorkspaceDestination.assistant,
|
||||
WorkspaceDestination.settings,
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user