From e4d48d79798d57c0817618f10f95f0616edbb49c Mon Sep 17 00:00:00 2001 From: Haitao Pan Date: Sun, 12 Apr 2026 14:09:25 +0800 Subject: [PATCH] Clean bridge provider routing and refresh repo instructions --- AGENTS.md | 24 ++--- ...ler_desktop_single_agent_go_task_flow.dart | 44 ++++++---- ...pp_controller_desktop_thread_sessions.dart | 19 +--- ...sktop_working_directory_dispatch_test.dart | 88 ++++++++++++++++++- 4 files changed, 128 insertions(+), 47 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index bddc2bc7..3723e540 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,7 +1,7 @@ ## Skills - Use `xworkmate-acceptance` before claiming build, packaging, installation, or release readiness for this repo. -- For any change that touches gateway auth, `.env`, secure storage, tokens, passwords, TLS, file upload, native entitlements, packaging, or release-sensitive settings, follow the security rules in this file and [docs/security/secure-development-rules.md](/Users/shenlan/workspaces/cloud-neutral-toolkit/XWorkmate.svc.plus/docs/security/secure-development-rules.md). +- For any change that touches gateway auth, `.env`, secure storage, tokens, passwords, TLS, file upload, native entitlements, packaging, or release-sensitive settings, follow the security rules in this file and [docs/security/secure-development-rules.md](docs/security/secure-development-rules.md). - For non-trivial implementation work, default to the worktree-first execution flow in this file without asking the user to restate that preference each time. ## Default Task Mode @@ -94,10 +94,10 @@ Soft triggers (recommended execution): Baseline commands: - `flutter analyze` -- `flutter test test/runtime/app_controller_assistant_flow_test.dart` -- `flutter test test/runtime/code_agent_node_orchestrator_test.dart` -- `flutter test test/runtime/app_controller_thread_skills_test.dart` -- `flutter test test/quality/wave1_file_size_guard_test.dart` +- `flutter test test/app_controller_desktop_runtime_cleanup_test.dart` +- `flutter test test/app_controller_desktop_working_directory_dispatch_test.dart` +- `flutter test test/runtime/external_code_agent_acp_desktop_transport_test.dart` +- `flutter test test/app_controller_desktop_thread_target_cleanup_test.dart` Cleanup baseline requirements: - Every "stale code cleanup" task must include an explicit list of removed compatibility layers; wrapper-only/refactor-only changes are insufficient. @@ -149,19 +149,19 @@ A refactor task is complete only when: - Keep network trust boundaries explicit. Loopback/local mode may use non-TLS intentionally; remote mode must not silently downgrade transport security. - File and attachment access must be user-driven. Never read or send workspace files implicitly. - Any new macOS or iOS entitlement must be least-privilege, justified by the feature, and covered by tests or manual verification notes. -- Auth, secret, network, or entitlement changes require `flutter analyze`, relevant unit/widget tests, and serial device-run integration tests when integration coverage is needed. +- Auth, secret, network, or entitlement changes require `flutter analyze` and relevant Flutter unit/widget tests. ## Testing Rules - Modify any Flutter UI page, and you must add or update widget tests and golden tests. -- Modify any core business flow, and you must add or update `integration_test`. -- Modify permission, camera, file picker, notification, WebView, or native page interaction behavior, and you must add or update Patrol coverage. -- Modify any Go handler, service, or repository, and you must add or update matching `*_test.go` unit tests. +- Modify any core business flow, and you must add or update focused Flutter tests under `test/`. +- Modify permission, camera, file picker, notification, WebView, or native page interaction behavior, and you must add or update the nearest existing Flutter regression coverage under `test/`. - All UI tests must use `Key`-based locators first. Avoid fragile text-only or hierarchy-only selectors unless no Key exists yet. -- Release/* branches must run the full chain: `flutter test`, `flutter test test/golden`, `flutter test integration_test`, `patrol test`, and `go test ./...`. +- Release/* branches must run the current repo-native validation chain from `docs/README_TESTING.md`. + At minimum for this repo that means `flutter analyze` and `flutter test`. - New features must follow test first, then implementation, then full regression. - Keep tests split by module. Do not pile every scenario into one file. -- Golden baseline refreshes require UI review confirmation before updating reference images. +- Golden baseline refreshes require UI review confirmation before updating reference images. Run the actual golden test files that exist in `test/features/**`. - CI failures must be fixed in tests or implementation. Do not skip the failing check in merge workflows. -See [docs/security/secure-development-rules.md](/Users/shenlan/workspaces/cloud-neutral-toolkit/XWorkmate.svc.plus/docs/security/secure-development-rules.md) for the full checklist. +See [docs/security/secure-development-rules.md](docs/security/secure-development-rules.md) for the full checklist. diff --git a/lib/app/app_controller_desktop_single_agent_go_task_flow.dart b/lib/app/app_controller_desktop_single_agent_go_task_flow.dart index 62e01f9e..db9dd822 100644 --- a/lib/app/app_controller_desktop_single_agent_go_task_flow.dart +++ b/lib/app/app_controller_desktop_single_agent_go_task_flow.dart @@ -76,6 +76,32 @@ Future sendSingleAgentMessageDesktopGoTaskFlowInternal( ); throw error; } + final preflightUnavailableReason = + controller.singleAgentShouldSuggestAcpSwitchForSession(sessionKey) || + controller.singleAgentNeedsBridgeProviderForSession(sessionKey) + ? singleAgentUnavailableLabelDesktopInternal( + controller, + sessionKey, + null, + ) + : null; + if (preflightUnavailableReason != null) { + controller.upsertTaskThreadInternal( + sessionKey, + lifecycleStatus: 'ready', + lastRunAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(), + lastResultCode: 'error', + updatedAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(), + ); + controller.appendAssistantThreadMessageInternal( + sessionKey, + assistantErrorMessageSingleAgentDesktopInternal( + controller, + preflightUnavailableReason, + ), + ); + return; + } if (controller.resolveExternalAcpEndpointForTargetInternal( AssistantExecutionTarget.singleAgent, ) == @@ -112,28 +138,12 @@ Future sendSingleAgentMessageDesktopGoTaskFlowInternal( ? resolvedProvider : controller.advertisedSingleAgentProviderInternal(selection) ?? selection; - final unavailableReason = - routingResolution.unavailable + final unavailableReason = routingResolution.unavailable ? singleAgentUnavailableLabelDesktopInternal( controller, sessionKey, routingResolution.unavailableMessage, ) - : controller.singleAgentShouldSuggestAcpSwitchForSession(sessionKey) - ? singleAgentUnavailableLabelDesktopInternal( - controller, - sessionKey, - null, - ) - : controller.singleAgentNeedsBridgeProviderForSession(sessionKey) - ? singleAgentUnavailableLabelDesktopInternal( - controller, - sessionKey, - appText( - 'Bridge 当前没有同步到可用 Provider。', - 'The bridge does not currently have any synced providers.', - ), - ) : null; if (unavailableReason != null) { controller.upsertTaskThreadInternal( diff --git a/lib/app/app_controller_desktop_thread_sessions.dart b/lib/app/app_controller_desktop_thread_sessions.dart index bd4d1b7e..99cb4d94 100644 --- a/lib/app/app_controller_desktop_thread_sessions.dart +++ b/lib/app/app_controller_desktop_thread_sessions.dart @@ -37,6 +37,7 @@ import 'app_controller_desktop_navigation.dart'; import 'app_controller_desktop_gateway.dart'; import 'app_controller_desktop_settings.dart'; import 'app_controller_desktop_single_agent.dart'; +import 'app_controller_desktop_single_agent_status_messages.dart'; import 'app_controller_desktop_thread_binding.dart'; import 'app_controller_desktop_thread_actions.dart'; import 'app_controller_desktop_workspace_execution.dart'; @@ -404,7 +405,6 @@ extension AppControllerDesktopThreadSessions on AppController { final target = assistantExecutionTargetForSession(normalizedSessionKey); if (target == AssistantExecutionTarget.singleAgent) { final primaryLabel = appText('Bridge', 'Bridge'); - final provider = singleAgentProviderForSession(normalizedSessionKey); final resolvedProvider = singleAgentResolvedProviderForSession( normalizedSessionKey, ); @@ -412,21 +412,10 @@ extension AppControllerDesktopThreadSessions on AppController { final providerReady = resolvedProvider != null; final detail = providerReady ? joinConnectionPartsInternal([resolvedProvider.label, model]) - : singleAgentShouldSuggestAcpSwitchForSession(normalizedSessionKey) - ? appText( - '${provider.label} 当前不可用,请改成 Bridge 当前可用的 Provider。', - '${provider.label} is unavailable. Switch to a provider currently advertised by the bridge.', - ) - : singleAgentNeedsBridgeProviderForSession( + : singleAgentUnavailableLabelDesktopInternal( + this, normalizedSessionKey, - ) - ? appText( - 'Bridge 当前没有可用 Provider。', - 'The bridge does not currently advertise any available providers.', - ) - : appText( - '当前线程的 Bridge Provider 尚未就绪。', - 'The bridge provider for this thread is not ready yet.', + null, ); return AssistantThreadConnectionState( executionTarget: target, diff --git a/test/app_controller_desktop_working_directory_dispatch_test.dart b/test/app_controller_desktop_working_directory_dispatch_test.dart index 54d694a8..662c22c2 100644 --- a/test/app_controller_desktop_working_directory_dispatch_test.dart +++ b/test/app_controller_desktop_working_directory_dispatch_test.dart @@ -113,7 +113,9 @@ void main() { supportRootPathResolver: () async => root.path, ); await store.initialize(); - final client = _CapturingGoTaskServiceClient(); + final client = _CapturingGoTaskServiceClient( + advertisedProviders: const [], + ); final controller = AppController( store: store, goTaskServiceClient: client, @@ -139,6 +141,79 @@ void main() { await controller.sendChatMessage('first turn'); expect(client.requests, isEmpty); + expect( + client.resolveExternalAcpRoutingCallCount, + 0, + reason: + 'single-agent turns should stop before routing.resolve when the bridge ACP entrypoint is missing', + ); + }, + ); + + test( + 'single-agent turns stop before routing when bridge has no advertised provider', + () async { + final root = await Directory.systemTemp.createTemp( + 'xworkmate-missing-bridge-provider-', + ); + final store = SecureConfigStore( + enableSecureStorage: false, + appDataRootPathResolver: () async => '${root.path}/settings.sqlite3', + secretRootPathResolver: () async => root.path, + supportRootPathResolver: () async => root.path, + ); + await store.initialize(); + await store.saveSettingsSnapshot( + SettingsSnapshot.defaults().copyWith( + acpBridgeServerModeConfig: SettingsSnapshot.defaults() + .acpBridgeServerModeConfig + .copyWith( + cloudSynced: SettingsSnapshot.defaults() + .acpBridgeServerModeConfig + .cloudSynced + .copyWith( + remoteServerSummary: + const AcpBridgeServerRemoteServerSummary( + endpoint: 'https://bridge.customer.example', + hasAdvancedOverrides: false, + ), + ), + ), + ), + ); + final client = _CapturingGoTaskServiceClient(); + final controller = AppController( + store: store, + goTaskServiceClient: client, + ); + addTearDown(() async { + controller.dispose(); + store.dispose(); + if (await root.exists()) { + await root.delete(recursive: true); + } + }); + + const sessionKey = 'draft:single-agent-missing-bridge-provider'; + controller.initializeAssistantThreadContext( + sessionKey, + executionTarget: AssistantExecutionTarget.singleAgent, + ); + await controller.switchSession(sessionKey); + _seedBridgeProviders(controller, const []); + + expect(controller.currentSingleAgentNeedsBridgeProvider, isTrue); + + await controller.sendChatMessage('first turn'); + + expect(client.requests, isEmpty); + expect( + client.resolveExternalAcpRoutingCallCount, + 0, + reason: + 'single-agent turns should not call routing.resolve when bridge provider state is already unavailable in app state', + ); + expect(controller.chatMessages.last.text, 'Bridge 当前没有可用 Provider。'); }, ); @@ -220,6 +295,13 @@ void _seedBridgeProviders( } class _CapturingGoTaskServiceClient implements GoTaskServiceClient { + _CapturingGoTaskServiceClient({ + this.advertisedProviders = const [ + SingleAgentProvider.codex, + ], + }); + + final List advertisedProviders; final List requests = []; int resolveExternalAcpRoutingCallCount = 0; @@ -275,10 +357,10 @@ class _CapturingGoTaskServiceClient implements GoTaskServiceClient { required AssistantExecutionTarget target, bool forceRefresh = false, }) async { - return const ExternalCodeAgentAcpCapabilities( + return ExternalCodeAgentAcpCapabilities( singleAgent: true, multiAgent: true, - providerCatalog: [SingleAgentProvider.codex], + providerCatalog: advertisedProviders, gatewayProviders: >[], raw: {}, );