diff --git a/config/settings.yaml b/config/settings.yaml new file mode 100644 index 00000000..c56ee663 --- /dev/null +++ b/config/settings.yaml @@ -0,0 +1,32 @@ +schemaVersion: 2 +appLanguage: zh +workspacePath: '' +cliPath: openclaw +defaultProvider: gateway +accountBaseUrl: https://accounts.svc.plus +accountUsername: '' +acpBridgeServerModeConfig: + effective: + endpoint: https://xworkmate-bridge.svc.plus + tokenRef: bridge.auth_token + source: cloud + reason: Synced cloud configuration from svc.plus is active + cloudSynced: + accountBaseUrl: https://accounts.svc.plus + accountIdentifier: '' + lastSyncAt: 0 + remoteServerSummary: + endpoint: https://xworkmate-bridge.svc.plus + hasAdvancedOverrides: false + selfHosted: + serverUrl: '' + username: '' + passwordRef: acp_bridge_server_password +gatewayProfiles: +- mode: unconfigured + useSetupCode: false + host: '' + port: 443 + tls: true + tokenRef: gateway_token_0 + passwordRef: gateway_password_0 diff --git a/docs/architecture/bridge-cloud-coexistence-priority.md b/docs/architecture/bridge-cloud-coexistence-priority.md new file mode 100644 index 00000000..deab364d --- /dev/null +++ b/docs/architecture/bridge-cloud-coexistence-priority.md @@ -0,0 +1,62 @@ +# Bridge Server Configuration Coexistence & Priority Resolution + +Date: 2026-04-19 + +## Overview + +The `xworkmate-app` utilizes a "Single Source of Truth" pattern for its Bridge Server configuration. While multiple configuration sources (Cloud Sync from `svc.plus` and Manual Bridge settings) can coexist, the system deterministically resolves them into a single **Effective Configuration** that the runtime consumes. + +### Key Principles +- **Sources as Inputs:** Cloud and Bridge configurations are treated as sources, not mutually exclusive modes. +- **Deterministic Priority:** `Manual Bridge (Self-Hosted)` > `Cloud Sync (Managed)`. +- **Single Source of Truth:** The runtime exclusively uses the `AcpBridgeServerEffectiveConfig` object. +- **Explainability:** Each effective configuration includes a `source` tag and a `reason` explaining why it was selected. + +## Configuration Model + +The system maintains a clear separation between sources and the resolved state in `AcpBridgeServerModeConfig`. + +```dart +class AcpBridgeServerModeConfig { + final AcpBridgeServerEffectiveConfig effective; // Single Source of Truth + final AcpBridgeServerCloudSyncConfig cloudSynced; // Source: svc.plus + final AcpBridgeServerSelfHostedConfig selfHosted; // Source: Manual entries +} +``` + +### Resolution Logic (Mermaid Diagram) + +The following diagram illustrates how the `resolveAcpBridgeServerEffectiveConfig` function implements the priority logic. + +```mermaid +flowchart TD + Start([Resolve Effective Config]) --> CheckManual{Is Manual Source\nconfigured?} + + CheckManual -->|Yes| UseManual[Effective Source: bridge\nEndpoint: selfHosted.serverUrl\nTokenRef: selfHosted.passwordRef] + + CheckManual -->|No| CheckCloud{Is Cloud Source\nsynced & valid?} + + CheckCloud -->|Yes| UseCloud[Effective Source: cloud\nEndpoint: cloudSynced.bridgeServerUrl\nTokenRef: bridge.auth_token] + + CheckCloud -->|No| UseDefault[Effective Source: default\nEndpoint: kManagedBridgeServerUrl\nTokenRef: None] + + UseManual --> End([Persist Effective State]) + UseCloud --> End + UseDefault --> End + + style UseManual fill:#e1f5fe,stroke:#1e88e5,stroke-width:2px + style UseCloud fill:#f3e5f5,stroke:#039be5,stroke-width:2px + style UseDefault fill:#fff3e0,stroke:#8e24aa,stroke-width:2px +``` + +## Traceability & explaining the source + +The `effective` configuration stores metadata that can be used for diagnostics or displayed in the UI: +- **`source`**: One of `bridge`, `cloud`, or `default`. +- **`reason`**: A human-readable string explaining the selection (e.g., "Manual Bridge configuration is present and valid"). + +## State Lifecycle + +1. **On Save:** Whenever the user updates Manual Bridge settings in the UI, the effective config is recalculated. +2. **On Sync:** Whenever a successful Cloud Sync occurs, the effective config is recalculated. +3. **At Runtime:** Components like `resolveBridgeAcpEndpointInternal` simply read from `effective.endpoint` without needing to know about the underlying priority rules. diff --git a/docs/architecture/public-api/_generated/public-symbol-inventory.md b/docs/architecture/public-api/_generated/public-symbol-inventory.md index 09242e2a..1dd9830c 100644 --- a/docs/architecture/public-api/_generated/public-symbol-inventory.md +++ b/docs/architecture/public-api/_generated/public-symbol-inventory.md @@ -1382,7 +1382,6 @@ _No extracted public top-level symbols._ | 68 | `class` | `AccountTokenConfigured` | `class AccountTokenConfigured {` | | 108 | `class` | `AccountSecretLocator` | `class AccountSecretLocator {` | | 166 | `class` | `AccountRemoteProfile` | `class AccountRemoteProfile {` | -| 267 | `enum` | `AcpBridgeServerMode` | `enum AcpBridgeServerMode { cloudSynced }` | | 269 | `class` | `AcpBridgeServerRemoteServerSummary` | `class AcpBridgeServerRemoteServerSummary {` | | 312 | `class` | `AcpBridgeServerCloudSyncConfig` | `class AcpBridgeServerCloudSyncConfig {` | | 370 | `class` | `AcpBridgeServerSelfHostedConfig` | `class AcpBridgeServerSelfHostedConfig {` | diff --git a/docs/xworkmate-app-core-functional-test-plan-v1.md b/docs/xworkmate-app-core-functional-test-plan-v1.md index 25a7637d..8ade1493 100644 --- a/docs/xworkmate-app-core-functional-test-plan-v1.md +++ b/docs/xworkmate-app-core-functional-test-plan-v1.md @@ -17,11 +17,24 @@ ### 1. Provider 发现与 UI 展示 -- “智能体模式” provider 列表由 bridge `acp.capabilities` 动态驱动。 - UI 不依赖固定静态 provider 列表。 +- “智能体模式” provider 列表由 bridge `acp.capabilities` 动态驱动。 +- “Gateway 模式” provider 列表由 bridge `目前支持 openclaw - 当 bridge 广告能力变化时,provider selector、状态文案和可执行状态同步更新。 - `auto` 模式下,UI 应表现为“由 bridge 当前可用项决定”,而不是写死默认 provider。 + | Service Endpoint │ Protocol │ Result │ Functional Check │ + ├───────────────────────┼────────────┼────────┼───────────────────────────────────┤ + │ /acp-server/codex/ │ JSON-RPC │ PASS │ acp.capabilities returned │ + │ │ (SSE/HTTP) │ │ successfully via /acp/rpc │ + │ /acp-server/gemini/ │ JSON-RPC │ PASS │ acp.capabilities returned │ + │ │ (SSE/HTTP) │ │ successfully via /acp/rpc │ + │ /acp-server/opencode/ │ JSON-RPC │ PASS │ acp.capabilities returned │ + │ │ (SSE/HTTP) │ │ successfully via /acp/rpc │ + │ /gateway/openclaw/ │ WSS / RPC │ PASS │ WebSocket handshake successful at │ + │ │ │ │ /acp; received connect.challenge + + ### 2. Assistant 线程体验 - `agent` 线程首次发送时自动绑定完整 `workspaceBinding`。 @@ -76,9 +89,9 @@ | Case | UI 侧目标能力 | 核心验收点 | | --- | --- | --- | -| `pptx` | 展示生成中的文稿任务与追问续写 | 首次生成结果可见;继续追问仍在同线程;workspace 不变;新文件或新版本可见 | | `docx` | 展示文档生成结果 | `.docx` 写回当前线程;assistant 结果与 artifact 面板一致;后续补写仍在同线程 | | `xlsx` | 展示表格与计算结果 | `.xlsx` 结果写回当前线程;后续追问继续修改同一线程内容 | +| `pptx` | 展示生成中的文稿任务与追问续写 | 首次生成结果可见;继续追问仍在同线程;workspace 不变;新文件或新版本可见 | | `pdf` | 展示文件转换结果 | `.pdf` 产物可见;转换后结果属于当前线程;继续操作不漂移 | | `image-resizer` | 展示图片处理结果 | 输出图片写回当前线程;artifact 面板可见;尺寸变化可通过测试或 fixture 校验 | | `browser` | 展示摘要、截图、日志 | assistant 文本有摘要;截图 / 日志进入 artifact 面板;继续浏览仍在当前线程 | diff --git a/lib/app/app_controller_desktop_runtime_helpers.dart b/lib/app/app_controller_desktop_runtime_helpers.dart index 9dc1dc8c..3b325f4f 100644 --- a/lib/app/app_controller_desktop_runtime_helpers.dart +++ b/lib/app/app_controller_desktop_runtime_helpers.dart @@ -644,7 +644,7 @@ extension AppControllerDesktopRuntimeHelpers on AppController { } } final modeConfig = settings.acpBridgeServerModeConfig; - final candidate = modeConfig.mode == AcpBridgeServerMode.manual + final candidate = modeConfig.usesSelfHostedBase ? modeConfig.selfHosted.serverUrl.trim() : kManagedBridgeServerUrl; final uri = Uri.tryParse(candidate.isEmpty ? kManagedBridgeServerUrl : candidate); @@ -696,7 +696,7 @@ extension AppControllerDesktopRuntimeHelpers on AppController { return normalizedToken; } final modeConfig = settings.acpBridgeServerModeConfig; - if (modeConfig.mode == AcpBridgeServerMode.manual) { + if (modeConfig.usesSelfHostedBase) { final manualToken = await settingsControllerInternal .loadSecretValueByRef(modeConfig.selfHosted.passwordRef); if (manualToken.trim().isNotEmpty) { diff --git a/lib/features/settings/settings_account_panel.dart b/lib/features/settings/settings_account_panel.dart index 4e82d18e..70eaf398 100644 --- a/lib/features/settings/settings_account_panel.dart +++ b/lib/features/settings/settings_account_panel.dart @@ -52,8 +52,7 @@ class SettingsAccountPanel extends StatelessWidget { if (!accountSignedIn && !accountMfaRequired) { return DefaultTabController( length: 2, - initialIndex: settings.acpBridgeServerModeConfig.mode == - AcpBridgeServerMode.manual + initialIndex: settings.acpBridgeServerModeConfig.effective.source == 'bridge' ? 1 : 0, child: Column( @@ -64,12 +63,10 @@ class SettingsAccountPanel extends StatelessWidget { Tab(text: appText('手动 Bridge 配置', 'Manual Bridge Config')), ], onTap: (index) { - final mode = index == 1 - ? AcpBridgeServerMode.manual - : AcpBridgeServerMode.cloudSynced; - if (settings.acpBridgeServerModeConfig.mode != mode) { - onSaveAccountProfile(); // This should trigger a save with the new mode - } + // Switching tabs saves the profile, which triggers a resolution of the effective config. + // We don't need a boolean flag anymore; the presence/validity of sources determines the source. + // But we still want to save on tap to persist the user's intent. + onSaveAccountProfile(); }, ), const SizedBox(height: 24), diff --git a/lib/features/settings/settings_page_core.dart b/lib/features/settings/settings_page_core.dart index 90e39a87..d7ef35bd 100644 --- a/lib/features/settings/settings_page_core.dart +++ b/lib/features/settings/settings_page_core.dart @@ -113,14 +113,25 @@ class _SettingsPageState extends State { final bridgeConfig = settings.acpBridgeServerModeConfig; final isManual = DefaultTabController.of(context).index == 1; + var nextBridgeConfig = bridgeConfig.copyWith( + selfHosted: bridgeConfig.selfHosted.copyWith( + serverUrl: _bridgeUrlController.text.trim(), + username: isManual ? 'admin' : bridgeConfig.selfHosted.username, + ), + ); + + // Resolve the effective config based on the new sources + final nextEffective = resolveAcpBridgeServerEffectiveConfig( + widget.controller.settingsController, + config: nextBridgeConfig, + accountSyncState: widget.controller.settingsController.accountSyncState, + ); + final nextSettings = settings.copyWith( accountBaseUrl: _accountBaseUrlController.text.trim(), accountUsername: _accountIdentifierController.text.trim(), - acpBridgeServerModeConfig: bridgeConfig.copyWith( - mode: isManual ? AcpBridgeServerMode.manual : AcpBridgeServerMode.cloudSynced, - selfHosted: bridgeConfig.selfHosted.copyWith( - serverUrl: _bridgeUrlController.text.trim(), - ), + acpBridgeServerModeConfig: nextBridgeConfig.copyWith( + effective: nextEffective, ), ); if (isManual && _bridgeTokenController.text.isNotEmpty) { diff --git a/lib/runtime/runtime_controllers_settings_account.dart b/lib/runtime/runtime_controllers_settings_account.dart index 03132011..32be839f 100644 --- a/lib/runtime/runtime_controllers_settings_account.dart +++ b/lib/runtime/runtime_controllers_settings_account.dart @@ -51,6 +51,19 @@ extension SettingsControllerAccountExtension on SettingsController { Future loadEffectiveGatewayToken({int? profileIndex}) async { final resolvedProfileIndex = (profileIndex ?? kGatewayRemoteProfileIndex) .clamp(0, kGatewayProfileListLength - 1); + + // Use the Single Source of Truth from the effective configuration for the primary profile + if (resolvedProfileIndex == kGatewayRemoteProfileIndex) { + final effective = snapshotInternal.acpBridgeServerModeConfig.effective; + if (effective.tokenRef.isNotEmpty) { + final token = await loadSecretValueByRef(effective.tokenRef); + if (token.isNotEmpty) { + return token; + } + } + } + + // Local Override / Vault / Cloud Sync (Fallback if effective token is missing or for other profiles) return resolveSecretValueInternal( refName: gatewayTokenRefForProfileInternal(resolvedProfileIndex), fallbackRefName: SecretStore.gatewayTokenRefKey(resolvedProfileIndex), @@ -63,6 +76,11 @@ extension SettingsControllerAccountExtension on SettingsController { Future loadEffectiveGatewayPassword({int? profileIndex}) async { final resolvedProfileIndex = (profileIndex ?? kGatewayRemoteProfileIndex) .clamp(0, kGatewayProfileListLength - 1); + + // Manual bridge usually uses a single token/key, but we check if the effective configuration + // points to bridge and if a password override is actually needed. + // For now, we fall back to the standard resolution logic. + return resolveSecretValueInternal( refName: gatewayPasswordRefForProfileInternal(resolvedProfileIndex), fallbackRefName: SecretStore.gatewayPasswordRefKey(resolvedProfileIndex), diff --git a/lib/runtime/runtime_controllers_settings_account_impl.dart b/lib/runtime/runtime_controllers_settings_account_impl.dart index 33872432..c31d86a4 100644 --- a/lib/runtime/runtime_controllers_settings_account_impl.dart +++ b/lib/runtime/runtime_controllers_settings_account_impl.dart @@ -319,17 +319,31 @@ Future syncAccountSettingsInternal( apisix: false, ), ); - await controller.storeInternal.saveAccountSyncState(nextState); final currentSettings = controller.snapshotInternal; final currentModeConfig = currentSettings.acpBridgeServerModeConfig; + + final nextEffective = resolveAcpBridgeServerEffectiveConfig( + controller, + config: currentModeConfig, + accountSyncState: nextState, + ); + + final identifier = (await controller.storeInternal.loadAccountSessionIdentifier()) + ?.trim() ?? + ''; final nextModeConfig = currentModeConfig.copyWith( + effective: nextEffective, cloudSynced: currentModeConfig.cloudSynced.copyWith( - accountBaseUrl: '', - accountIdentifier: '', + accountBaseUrl: currentModeConfig.cloudSynced.accountBaseUrl.trim().isEmpty + ? normalizedBaseUrl + : currentModeConfig.cloudSynced.accountBaseUrl, + accountIdentifier: currentModeConfig.cloudSynced.accountIdentifier.trim().isEmpty + ? identifier + : currentModeConfig.cloudSynced.accountIdentifier, lastSyncAt: nextState.lastSyncAtMs, remoteServerSummary: currentModeConfig.cloudSynced.remoteServerSummary .copyWith( - endpoint: resolvedBridgeServerUrl, + endpoint: nextEffective.endpoint, hasAdvancedOverrides: false, ), ), @@ -390,8 +404,12 @@ Future logoutAccountSettingsInternal( final currentSnapshot = controller.snapshotInternal; final clearedCloudSync = currentSnapshot.acpBridgeServerModeConfig.cloudSynced .copyWith( - accountBaseUrl: '', - accountIdentifier: '', + accountBaseUrl: quiet + ? currentSnapshot.acpBridgeServerModeConfig.cloudSynced.accountBaseUrl + : '', + accountIdentifier: quiet + ? currentSnapshot.acpBridgeServerModeConfig.cloudSynced.accountIdentifier + : '', lastSyncAt: 0, remoteServerSummary: currentSnapshot .acpBridgeServerModeConfig @@ -495,6 +513,11 @@ Future _persistAccountSessionSummaryFromProfilePayloadInternal( if (summary.userId.trim().isNotEmpty) { await controller.storeInternal.saveAccountSessionUserId(summary.userId); } + if (summary.email.trim().isNotEmpty) { + await controller.storeInternal.saveAccountSessionIdentifier( + summary.email.trim(), + ); + } final identifier = summary.email.trim().isNotEmpty ? summary.email.trim() : (await controller.storeInternal.loadAccountSessionIdentifier()) @@ -557,22 +580,49 @@ String _resolveBridgeServerUrl(Map payload) { return ''; } +AcpBridgeServerEffectiveConfig resolveAcpBridgeServerEffectiveConfig( + SettingsController controller, { + required AcpBridgeServerModeConfig config, + AccountSyncState? accountSyncState, +}) { + // Priority 1: Manual Bridge (Self-Hosted) + // Logic: Must have a valid URL and be explicitly intended (we assume if it's configured, it's intended) + if (config.selfHosted.isConfigured) { + return AcpBridgeServerEffectiveConfig( + endpoint: config.selfHosted.serverUrl, + tokenRef: config.selfHosted.passwordRef, + source: 'bridge', + reason: 'Manual Bridge configuration is present and valid', + ); + } + + // Priority 2: Cloud Sync (svc.plus) + // Logic: Check the synced state for a valid endpoint and token + final syncedUrl = accountSyncState?.syncedDefaults.bridgeServerUrl.trim() ?? ''; + final hasSyncedToken = accountSyncState?.tokenConfigured.bridge == true; + if (isSupportedExternalAcpEndpoint(syncedUrl) && hasSyncedToken) { + return AcpBridgeServerEffectiveConfig( + endpoint: syncedUrl, + tokenRef: kAccountManagedSecretTargetBridgeAuthToken, + source: 'cloud', + reason: 'Synced cloud configuration from svc.plus is active', + ); + } + + // Priority 3: Default Managed Fallback + return AcpBridgeServerEffectiveConfig( + endpoint: kManagedBridgeServerUrl, + tokenRef: '', + source: 'default', + reason: 'Falling back to default managed server', + ); +} + 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; + return controller.snapshotInternal.acpBridgeServerModeConfig.effective.endpoint; } int _parseExpiresAtMs(Object? value) { diff --git a/lib/runtime/runtime_models_account.dart b/lib/runtime/runtime_models_account.dart index 4b175dd0..0fd1b4a2 100644 --- a/lib/runtime/runtime_models_account.dart +++ b/lib/runtime/runtime_models_account.dart @@ -265,17 +265,6 @@ class AccountRemoteProfile { } } -enum AcpBridgeServerMode { cloudSynced, manual } - -extension AcpBridgeServerModeCopy on AcpBridgeServerMode { - static AcpBridgeServerMode fromJsonValue(String? value) { - return AcpBridgeServerMode.values.firstWhere( - (item) => item.name == value, - orElse: () => AcpBridgeServerMode.cloudSynced, - ); - } -} - class AcpBridgeServerRemoteServerSummary { const AcpBridgeServerRemoteServerSummary({ required this.endpoint, @@ -516,22 +505,77 @@ class AcpBridgeServerAdvancedOverrides { } } +class AcpBridgeServerEffectiveConfig { + const AcpBridgeServerEffectiveConfig({ + required this.endpoint, + required this.tokenRef, + required this.source, + required this.reason, + }); + + final String endpoint; + final String tokenRef; + final String source; // 'bridge' | 'cloud' | 'default' + final String reason; + + factory AcpBridgeServerEffectiveConfig.defaults() { + return const AcpBridgeServerEffectiveConfig( + endpoint: kManagedBridgeServerUrl, + tokenRef: '', + source: 'default', + reason: 'No active source configured', + ); + } + + AcpBridgeServerEffectiveConfig copyWith({ + String? endpoint, + String? tokenRef, + String? source, + String? reason, + }) { + return AcpBridgeServerEffectiveConfig( + endpoint: endpoint ?? this.endpoint, + tokenRef: tokenRef ?? this.tokenRef, + source: source ?? this.source, + reason: reason ?? this.reason, + ); + } + + Map toJson() { + return { + 'endpoint': endpoint, + 'tokenRef': tokenRef, + 'source': source, + 'reason': reason, + }; + } + + factory AcpBridgeServerEffectiveConfig.fromJson(Map json) { + return AcpBridgeServerEffectiveConfig( + endpoint: json['endpoint'] as String? ?? kManagedBridgeServerUrl, + tokenRef: json['tokenRef'] as String? ?? '', + source: json['source'] as String? ?? 'default', + reason: json['reason'] as String? ?? '', + ); + } +} + class AcpBridgeServerModeConfig { const AcpBridgeServerModeConfig({ - required this.mode, + required this.effective, required this.cloudSynced, required this.selfHosted, required this.advancedOverrides, }); - final AcpBridgeServerMode mode; + final AcpBridgeServerEffectiveConfig effective; final AcpBridgeServerCloudSyncConfig cloudSynced; final AcpBridgeServerSelfHostedConfig selfHosted; final AcpBridgeServerAdvancedOverrides advancedOverrides; factory AcpBridgeServerModeConfig.defaults() { return AcpBridgeServerModeConfig( - mode: AcpBridgeServerMode.cloudSynced, + effective: AcpBridgeServerEffectiveConfig.defaults(), cloudSynced: AcpBridgeServerCloudSyncConfig.defaults(), selfHosted: AcpBridgeServerSelfHostedConfig.defaults(), advancedOverrides: AcpBridgeServerAdvancedOverrides.defaults(), @@ -539,30 +583,30 @@ class AcpBridgeServerModeConfig { } AcpBridgeServerModeConfig copyWith({ - AcpBridgeServerMode? mode, + AcpBridgeServerEffectiveConfig? effective, AcpBridgeServerCloudSyncConfig? cloudSynced, AcpBridgeServerSelfHostedConfig? selfHosted, AcpBridgeServerAdvancedOverrides? advancedOverrides, }) { return AcpBridgeServerModeConfig( - mode: mode ?? this.mode, + effective: effective ?? this.effective, cloudSynced: cloudSynced ?? this.cloudSynced, selfHosted: selfHosted ?? this.selfHosted, advancedOverrides: advancedOverrides ?? this.advancedOverrides, ); } - bool get usesSelfHostedBase => mode == AcpBridgeServerMode.manual; + bool get usesSelfHostedBase => effective.source == 'bridge'; - bool get usesCloudSyncBase => mode == AcpBridgeServerMode.cloudSynced; + bool get usesCloudSyncBase => !usesSelfHostedBase; - String get sourceTag => mode.name; + String get sourceTag => effective.source; String toJsonString() => jsonEncode(toJson()); Map toJson() { return { - 'mode': mode.name, + 'effective': effective.toJson(), 'cloudSynced': cloudSynced.toJson(), 'selfHosted': selfHosted.toJson(), 'advancedOverrides': advancedOverrides.toJson(), @@ -571,7 +615,9 @@ class AcpBridgeServerModeConfig { factory AcpBridgeServerModeConfig.fromJson(Map json) { return AcpBridgeServerModeConfig( - mode: AcpBridgeServerModeCopy.fromJsonValue(json['mode'] as String?), + effective: AcpBridgeServerEffectiveConfig.fromJson( + (json['effective'] as Map?)?.cast() ?? const {}, + ), cloudSynced: AcpBridgeServerCloudSyncConfig.fromJson( (json['cloudSynced'] as Map?)?.cast() ?? const {}, ), diff --git a/test/runtime/gateway_acp_client_auth_test.dart b/test/runtime/gateway_acp_client_auth_test.dart index fea58336..bbbb6f80 100644 --- a/test/runtime/gateway_acp_client_auth_test.dart +++ b/test/runtime/gateway_acp_client_auth_test.dart @@ -274,9 +274,15 @@ void main() { final settings = SettingsSnapshot.defaults().copyWith( acpBridgeServerModeConfig: AcpBridgeServerModeConfig.defaults().copyWith( - mode: AcpBridgeServerMode.manual, + effective: const AcpBridgeServerEffectiveConfig( + endpoint: 'https://manual-bridge.example.com', + tokenRef: 'acp_bridge_server_password', + source: 'bridge', + reason: 'Manual test configuration', + ), selfHosted: AcpBridgeServerSelfHostedConfig.defaults().copyWith( serverUrl: 'https://manual-bridge.example.com', + username: 'admin', ), ), );