Clean bridge provider routing and refresh repo instructions

This commit is contained in:
Haitao Pan 2026-04-12 14:09:25 +08:00
parent 9e80740378
commit e4d48d7979
4 changed files with 128 additions and 47 deletions

View File

@ -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.

View File

@ -76,6 +76,32 @@ Future<void> 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<void> 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(

View File

@ -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(<String>[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,

View File

@ -113,7 +113,9 @@ void main() {
supportRootPathResolver: () async => root.path,
);
await store.initialize();
final client = _CapturingGoTaskServiceClient();
final client = _CapturingGoTaskServiceClient(
advertisedProviders: const <SingleAgentProvider>[],
);
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 <SingleAgentProvider>[]);
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>[
SingleAgentProvider.codex,
],
});
final List<SingleAgentProvider> advertisedProviders;
final List<GoTaskServiceRequest> requests = <GoTaskServiceRequest>[];
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>[SingleAgentProvider.codex],
providerCatalog: advertisedProviders,
gatewayProviders: <Map<String, dynamic>>[],
raw: <String, dynamic>{},
);