Clean bridge provider routing and refresh repo instructions
This commit is contained in:
parent
9e80740378
commit
e4d48d7979
24
AGENTS.md
24
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.
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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>{},
|
||||
);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user