From 6efa70ee8b3eedfedf344de51ebfc7ff3de58027 Mon Sep 17 00:00:00 2001 From: Haitao Pan Date: Mon, 6 Apr 2026 09:57:37 +0800 Subject: [PATCH] Align strict TaskThread workspace semantics --- ...sistant-thread-information-architecture.md | 9 +- .../assistant-thread-target-model-20260328.md | 4 +- .../xworkmate-internal-state-architecture.md | 11 ++- .../xworkmate-layered-architecture.md | 5 +- ...app_controller_desktop_thread_actions.dart | 28 +++--- ...pp_controller_desktop_thread_sessions.dart | 14 +-- lib/app/app_controller_web_gateway_relay.dart | 27 +++--- lib/app/app_controller_web_helpers.dart | 18 ++-- lib/app/app_controller_web_sessions.dart | 22 ++--- lib/app/app_controller_web_workspace.dart | 8 -- lib/web/web_session_repository.dart | 16 +-- lib/web/web_store.dart | 13 ++- ...er_ai_gateway_chat_suite_single_agent.dart | 6 +- ...ntroller_assistant_workspace_ref_test.dart | 97 ++++--------------- ...cure_config_store_suite_compatibility.dart | 2 +- test/test_support_task_thread_fixture.dart | 6 +- 16 files changed, 114 insertions(+), 172 deletions(-) diff --git a/docs/architecture/assistant-thread-information-architecture.md b/docs/architecture/assistant-thread-information-architecture.md index e7237d3c..2f3c2af8 100644 --- a/docs/architecture/assistant-thread-information-architecture.md +++ b/docs/architecture/assistant-thread-information-architecture.md @@ -46,7 +46,9 @@ TaskThread 约束: - `workspaceBinding` 是线程记录的一部分 -- 它只在必要时更新 +- 它只能在当前线程已完整时被显式更新 +- 它不能用于 create first binding +- 它不能跨线程覆盖 - 它不再承担运行前 fallback 猜测语义 ### 2.3 executionBinding @@ -100,7 +102,7 @@ flowchart LR F --> G["执行结果"] G --> H["回写线程上下文\n(主体区域 同步显示)"] - G --> I["必要时更新 workspaceBinding"] + G --> I["仅显式更新当前已完整线程的 workspaceBinding"] H --> J["右栏显示"] I --> J @@ -111,7 +113,8 @@ flowchart LR - `读取 TaskThread` 是 UI 与执行层共享的唯一线程信息入口 - `构造执行请求` 在 agent-core / runtime 协调层完成 - `右栏显示` 明确依赖 `TaskThread` 当前记录 -- `必要时更新 workspaceBinding` 是条件分支,不是固定步骤 +- `workspaceBinding` 更新只允许发生在当前线程已完整的前提下 +- `workspace_root` 不是线程身份,也不是运行时 override ## 4. UI 信息来源矩阵 diff --git a/docs/architecture/assistant-thread-target-model-20260328.md b/docs/architecture/assistant-thread-target-model-20260328.md index e7f55a5a..3658c360 100644 --- a/docs/architecture/assistant-thread-target-model-20260328.md +++ b/docs/architecture/assistant-thread-target-model-20260328.md @@ -178,9 +178,9 @@ flowchart LR - [task-thread-session-key-isolation-20260329.md](task-thread-session-key-isolation-20260329.md) 补充“任务线必须先成为真实 `TaskThread/sessionKey`”的隔离约束,说明为什么 single-agent 的工作目录只能围绕当前线程身份解析。 -- [assistant-thread-information-architecture.md](/Users/shenlan/workspaces/cloud-neutral-toolkit/xworkmate-taskthread-docs-naming-cleanup/docs/architecture/assistant-thread-information-architecture.md) +- [assistant-thread-information-architecture.md](assistant-thread-information-architecture.md) 说明线程信息如何进入 UI、agent-core / runtime 请求构造、结果回写和右栏展示。 -- [xworkmate-internal-state-architecture.md](/Users/shenlan/workspaces/cloud-neutral-toolkit/xworkmate-taskthread-docs-naming-cleanup/docs/architecture/xworkmate-internal-state-architecture.md) +- [xworkmate-internal-state-architecture.md](xworkmate-internal-state-architecture.md) 说明控制器、状态存储和派生 UI 状态如何围绕 `TaskThread` 组织。 归档文档仍可保留作为历史背景,但不再参与当前设计说明。 diff --git a/docs/architecture/xworkmate-internal-state-architecture.md b/docs/architecture/xworkmate-internal-state-architecture.md index 8787b9a7..5a13b6ae 100644 --- a/docs/architecture/xworkmate-internal-state-architecture.md +++ b/docs/architecture/xworkmate-internal-state-architecture.md @@ -229,12 +229,13 @@ Examples: 1. UI 选择 `threadId` 2. 控制器 / runtime 读取 `TaskThread` -3. 若线程字段缺失,才回退到 Settings 中心默认值用于初始化或补全 +3. 若线程缺失或 `workspaceBinding` 不完整,则该线程视为非法或不可执行状态,必须显式失败或在恢复阶段跳过 这意味着: - Settings 是默认值来源,不是当前线程真相源 - 当前线程的执行模式、模型、技能、工作空间都以 `TaskThread` 为准 +- Settings 不能用于补全已存在线程的缺失字段 ### 4.2 执行请求构造优先级 @@ -249,7 +250,7 @@ Examples: 1. 回写 `contextState` 2. 主体区域同步显示 -3. 必要时更新 `workspaceBinding` +3. 仅在当前线程已经完整时,显式更新该线程 `workspaceBinding` 4. 右栏读取最新 `TaskThread` 记录并刷新 ## 5. Lifecycle Baseline @@ -273,7 +274,7 @@ flowchart LR F --> G["执行结果"] G --> H["回写线程上下文\n(主体区域 同步显示)"] - G --> I["必要时更新 workspaceBinding"] + G --> I["仅显式更新当前已完整线程的 workspaceBinding"] H --> J["右栏显示"] I --> J @@ -288,9 +289,9 @@ flowchart LR ## 6. 文档边界 -- [assistant-thread-target-model-20260328.md](/Users/shenlan/workspaces/cloud-neutral-toolkit/xworkmate-taskthread-docs-naming-cleanup/docs/architecture/assistant-thread-target-model-20260328.md) +- [assistant-thread-target-model-20260328.md](assistant-thread-target-model-20260328.md) 负责说明 `TaskThread` 当前模型与生命周期主链。 -- [assistant-thread-information-architecture.md](/Users/shenlan/workspaces/cloud-neutral-toolkit/xworkmate-taskthread-docs-naming-cleanup/docs/architecture/assistant-thread-information-architecture.md) +- [assistant-thread-information-architecture.md](assistant-thread-information-architecture.md) 负责说明线程信息如何进入 UI、请求构造与结果回写。 归档文档只保留为历史背景,不再作为当前内部状态设计依据。 diff --git a/docs/architecture/xworkmate-layered-architecture.md b/docs/architecture/xworkmate-layered-architecture.md index fde0f77d..462094b8 100644 --- a/docs/architecture/xworkmate-layered-architecture.md +++ b/docs/architecture/xworkmate-layered-architecture.md @@ -304,7 +304,7 @@ flowchart LR H --> I I --> J["回写 contextState / lifecycleState"] - I --> K["必要时回写 workspaceBinding"] + I --> K["仅显式回写当前已完整线程的 workspaceBinding"] J --> L["持久化 TaskThread"] K --> L @@ -317,7 +317,8 @@ flowchart LR - 线程先绑定,再执行 - 执行模式由 `executionBinding` 决定 - 结果先回写线程,再刷新 UI -- 远端返回新的 working directory 时,应该回写 `workspaceBinding` +- 远端返回新的 working directory 时,只能显式回写当前已完整线程的 `workspaceBinding` +- 这类回写不能创建 first binding,也不能改变线程身份 ## 当前代码里的真实组件映射 diff --git a/lib/app/app_controller_desktop_thread_actions.dart b/lib/app/app_controller_desktop_thread_actions.dart index 4cad1037..61968b8e 100644 --- a/lib/app/app_controller_desktop_thread_actions.dart +++ b/lib/app/app_controller_desktop_thread_actions.dart @@ -249,6 +249,10 @@ extension AppControllerDesktopThreadActions on AppController { }) async { final currentSessionKey = sessionsControllerInternal.currentSessionKey; final currentTarget = assistantExecutionTargetForSession(currentSessionKey); + await ensureDesktopTaskThreadBindingInternal( + currentSessionKey, + executionTarget: currentTarget, + ); if (currentTarget == AssistantExecutionTarget.singleAgent || currentTarget == AssistantExecutionTarget.auto) { await bootstrapThreadWorkspaceFromExecutionContextInternal( @@ -256,20 +260,9 @@ extension AppControllerDesktopThreadActions on AppController { message, ); } - await ensureDesktopTaskThreadBindingInternal( - currentSessionKey, - executionTarget: currentTarget, - ); var workspacePath = assistantWorkspacePathForSession( currentSessionKey, ).trim(); - if (workspacePath.isEmpty) { - await tryBindWorkspaceForOnlyChatFallbackInternal( - currentSessionKey, - currentTarget, - ); - workspacePath = assistantWorkspacePathForSession(currentSessionKey).trim(); - } if (workspacePath.isEmpty) { final error = StateError( appText( @@ -545,6 +538,13 @@ extension AppControllerDesktopThreadActions on AppController { ); final existing = assistantThreadRecordsInternal[normalizedSessionKey]; if (existing == null || !existing.workspaceBinding.isComplete) { + throw StateError( + 'TaskThread $normalizedSessionKey is missing a complete workspaceBinding.', + ); + } + final target = existing.executionTarget; + if (target != AssistantExecutionTarget.singleAgent && + target != AssistantExecutionTarget.auto) { return; } upsertTaskThreadInternal( @@ -594,5 +594,9 @@ extension AppControllerDesktopThreadActions on AppController { Future tryBindWorkspaceForOnlyChatFallbackInternal( String sessionKey, AssistantExecutionTarget currentTarget, - ) async {} + ) async { + throw StateError( + 'tryBindWorkspaceForOnlyChatFallbackInternal is no longer supported.', + ); + } } diff --git a/lib/app/app_controller_desktop_thread_sessions.dart b/lib/app/app_controller_desktop_thread_sessions.dart index 3259389a..5e1fb6b7 100644 --- a/lib/app/app_controller_desktop_thread_sessions.dart +++ b/lib/app/app_controller_desktop_thread_sessions.dart @@ -158,14 +158,14 @@ extension AppControllerDesktopThreadSessions on AppController { sessionKey, ); final record = assistantThreadRecordsInternal[normalizedSessionKey]; - if (record != null) { - return record.workspaceKind == WorkspaceKind.localFs - ? WorkspaceRefKind.localPath - : WorkspaceRefKind.remotePath; + if (record == null || !record.workspaceBinding.isComplete) { + throw StateError( + 'TaskThread $normalizedSessionKey is missing a complete workspaceBinding.', + ); } - return defaultWorkspaceRefKindForTargetInternal( - assistantExecutionTargetForSession(normalizedSessionKey), - ); + return record.workspaceKind == WorkspaceKind.localFs + ? WorkspaceRefKind.localPath + : WorkspaceRefKind.remotePath; } String assistantWorkspaceDisplayPathForSession(String sessionKey) { diff --git a/lib/app/app_controller_web_gateway_relay.dart b/lib/app/app_controller_web_gateway_relay.dart index bcec3fee..cc54bf70 100644 --- a/lib/app/app_controller_web_gateway_relay.dart +++ b/lib/app/app_controller_web_gateway_relay.dart @@ -115,15 +115,18 @@ extension AppControllerWebGatewayRelay on AppController { subjectId: '', displayName: '', ), - workspaceBinding: - existing?.workspaceBinding ?? - WorkspaceBinding( - workspaceId: sessionKey, - workspaceKind: WorkspaceKind.remoteFs, - workspacePath: '', - displayPath: '', - writable: true, - ), + workspaceBinding: buildWebWorkspaceBindingInternal( + sessionKey, + ownerScope: + existing?.ownerScope ?? + const ThreadOwnerScope( + realm: ThreadRealm.remote, + subjectType: ThreadSubjectType.user, + subjectId: '', + displayName: '', + ), + existingBinding: existing?.workspaceBinding, + ), executionBinding: existing?.executionBinding ?? ExecutionBinding( @@ -158,16 +161,12 @@ extension AppControllerWebGatewayRelay on AppController { existing?.lifecycleState ?? const ThreadLifecycleState( archived: false, - status: 'needs_workspace', + status: 'ready', lastRunAtMs: null, lastResultCode: null, ), ); threadRecordsInternal[sessionKey] = next; - await ensureWebTaskThreadBindingInternal( - sessionKey, - executionTarget: next.executionTarget, - ); } await persistThreadsInternal(); recomputeDerivedWorkspaceStateInternal(); diff --git a/lib/app/app_controller_web_helpers.dart b/lib/app/app_controller_web_helpers.dart index f4ef32bf..4f87723c 100644 --- a/lib/app/app_controller_web_helpers.dart +++ b/lib/app/app_controller_web_helpers.dart @@ -105,7 +105,13 @@ extension AppControllerWebHelpers on AppController { final target = sanitizeTargetInternal(record.executionTarget) ?? AssistantExecutionTarget.singleAgent; - final workspacePath = record.workspacePath.trim(); + final workspaceBinding = record.workspaceBinding; + if (!workspaceBinding.isComplete) { + throw StateError( + 'TaskThread ${record.threadId} is missing a complete workspaceBinding.', + ); + } + final workspacePath = workspaceBinding.workspacePath.trim(); return record.copyWith( executionTarget: target, title: record.title.trim().isEmpty @@ -113,12 +119,12 @@ extension AppControllerWebHelpers on AppController { : record.title.trim(), workspaceBinding: WorkspaceBinding( workspaceId: record.threadId, - workspaceKind: WorkspaceKind.remoteFs, + workspaceKind: workspaceBinding.workspaceKind, workspacePath: workspacePath, displayPath: record.displayPath.trim().isEmpty ? workspacePath : record.displayPath.trim(), - writable: record.workspaceBinding.writable, + writable: workspaceBinding.writable, ), lifecycleState: record.lifecycleState.copyWith(status: 'ready'), ); @@ -303,11 +309,7 @@ extension AppControllerWebHelpers on AppController { lastRunAtMs: null, lastResultCode: null, )) - .copyWith( - status: workspaceBinding.workspacePath.trim().isEmpty - ? 'needs_workspace' - : 'ready', - ), + .copyWith(status: 'ready'), executionTarget: resolvedTarget, updatedAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(), ); diff --git a/lib/app/app_controller_web_sessions.dart b/lib/app/app_controller_web_sessions.dart index bf425a44..0012e626 100644 --- a/lib/app/app_controller_web_sessions.dart +++ b/lib/app/app_controller_web_sessions.dart @@ -68,12 +68,14 @@ extension AppControllerWebSessions on AppController { WorkspaceRefKind assistantWorkspaceKindForSession(String sessionKey) { final normalizedSessionKey = normalizedSessionKeyInternal(sessionKey); final record = threadRecordsInternal[normalizedSessionKey]; - if (record != null) { - return record.workspaceKind == WorkspaceKind.localFs - ? WorkspaceRefKind.localPath - : WorkspaceRefKind.remotePath; + if (record == null || !record.workspaceBinding.isComplete) { + throw StateError( + 'TaskThread $normalizedSessionKey is missing a complete workspaceBinding.', + ); } - return WorkspaceRefKind.remotePath; + return record.workspaceKind == WorkspaceKind.localFs + ? WorkspaceRefKind.localPath + : WorkspaceRefKind.remotePath; } String assistantWorkspaceDisplayPathForSession(String sessionKey) { @@ -560,12 +562,8 @@ extension AppControllerWebSessions on AppController { if (existing != null) { return existing; } - final target = - sanitizeTargetInternal(settingsInternal.assistantExecutionTarget) ?? - AssistantExecutionTarget.singleAgent; - final record = newRecordInternal(target: target); - threadRecordsInternal[record.threadId] = record; - currentSessionKeyInternal = record.threadId; - return record; + throw StateError( + 'Current session $currentSessionKeyInternal has no TaskThread record.', + ); } } diff --git a/lib/app/app_controller_web_workspace.dart b/lib/app/app_controller_web_workspace.dart index 7890033d..c29089a4 100644 --- a/lib/app/app_controller_web_workspace.dart +++ b/lib/app/app_controller_web_workspace.dart @@ -47,10 +47,6 @@ extension AppControllerWebWorkspace on AppController { for (final record in records) { final sanitized = sanitizeRecordInternal(record); threadRecordsInternal[sanitized.sessionKey] = sanitized; - await ensureWebTaskThreadBindingInternal( - sanitized.sessionKey, - executionTarget: sanitized.executionTarget, - ); } if (threadRecordsInternal.isEmpty) { final record = newRecordInternal( @@ -58,10 +54,6 @@ extension AppControllerWebWorkspace on AppController { title: appText('新对话', 'New conversation'), ); threadRecordsInternal[record.sessionKey] = record; - await ensureWebTaskThreadBindingInternal( - record.sessionKey, - executionTarget: record.executionTarget, - ); } final preferredSession = normalizedSessionKeyInternal( settingsInternal.assistantLastSessionKey, diff --git a/lib/web/web_session_repository.dart b/lib/web/web_session_repository.dart index 5666969b..601b323d 100644 --- a/lib/web/web_session_repository.dart +++ b/lib/web/web_session_repository.dart @@ -60,13 +60,15 @@ class RemoteWebSessionRepository implements WebSessionRepository { Map map => map['threads'] as List? ?? const [], _ => const [], }; - return rawThreads - .whereType() - .map( - (item) => - TaskThread.fromJson(item.cast()), - ) - .toList(growable: false); + final records = []; + for (final item in rawThreads.whereType()) { + try { + records.add(TaskThread.fromJson(item.cast())); + } catch (_) { + continue; + } + } + return List.unmodifiable(records); } @override diff --git a/lib/web/web_store.dart b/lib/web/web_store.dart index 907ae46f..268e0a8e 100644 --- a/lib/web/web_store.dart +++ b/lib/web/web_store.dart @@ -61,10 +61,15 @@ class WebStore { await clearTaskThreadState(); return const []; } - return threads - .whereType() - .map((item) => TaskThread.fromJson(item.cast())) - .toList(growable: false); + final records = []; + for (final item in threads.whereType()) { + try { + records.add(TaskThread.fromJson(item.cast())); + } catch (_) { + continue; + } + } + return List.unmodifiable(records); } catch (_) { await clearTaskThreadState(); return const []; diff --git a/test/runtime/app_controller_ai_gateway_chat_suite_single_agent.dart b/test/runtime/app_controller_ai_gateway_chat_suite_single_agent.dart index 346e0194..d3f24894 100644 --- a/test/runtime/app_controller_ai_gateway_chat_suite_single_agent.dart +++ b/test/runtime/app_controller_ai_gateway_chat_suite_single_agent.dart @@ -210,7 +210,7 @@ void registerAppControllerAiGatewayChatSuiteSingleAgentTestsInternal() { ); test( - 'AppController bootstraps the current thread workspace from execution context before single-agent send', + 'AppController updates the current thread workspace from execution context for local single-agent threads', () async { final tempDirectory = await createTempDirectoryInternal( 'xworkmate-single-agent-workspace-bootstrap-', @@ -221,7 +221,7 @@ void registerAppControllerAiGatewayChatSuiteSingleAgentTestsInternal() { final store = createStoreFromTempDirectoryInternal(tempDirectory); await store.initialize(); await store.saveSettingsSnapshot( - SettingsSnapshot.defaults().copyWith(workspacePath: ''), + SettingsSnapshot.defaults().copyWith(workspacePath: tempDirectory.path), ); final client = FakeGoAgentCoreClientInternal( capabilities: GoAgentCoreCapabilities( @@ -527,7 +527,7 @@ void registerAppControllerAiGatewayChatSuiteSingleAgentTestsInternal() { await controller.settingsController.saveAiGatewayApiKey('live-key'); await controller.saveSettings( controller.settings.copyWith( - workspacePath: '', + workspacePath: tempDirectory.path, aiGateway: controller.settings.aiGateway.copyWith( baseUrl: server.baseUrl, availableModels: const ['moonshotai/kimi-k2.5'], diff --git a/test/runtime/app_controller_assistant_workspace_ref_test.dart b/test/runtime/app_controller_assistant_workspace_ref_test.dart index 7c8265e1..f87b73c0 100644 --- a/test/runtime/app_controller_assistant_workspace_ref_test.dart +++ b/test/runtime/app_controller_assistant_workspace_ref_test.dart @@ -472,7 +472,7 @@ void main() { ); test( - 'AppController rebinds the current single-agent thread after configuring a workspace root', + 'AppController keeps the current single-agent thread workspace stable when saving workspace settings', () async { SharedPreferences.setMockInitialValues({}); final tempDirectory = await Directory.systemTemp.createTemp( @@ -494,46 +494,14 @@ void main() { ); await store.initialize(); await store.saveSettingsSnapshot( - seededSettingsSnapshot(workspacePath: ''), + seededSettingsSnapshot(workspacePath: workspaceRoot.path), ); final controller = AppController(store: store); addTearDown(controller.dispose); await waitForControllerInternal(controller); - final existingMain = controller - .assistantThreadRecordsInternal[controller.currentSessionKey]!; - controller.assistantThreadRecordsInternal[controller.currentSessionKey] = - existingMain.copyWith( - workspaceBinding: const WorkspaceBinding( - workspaceId: 'main', - workspaceKind: WorkspaceKind.localFs, - workspacePath: '', - displayPath: '', - writable: true, - ), - lifecycleState: existingMain.lifecycleState.copyWith( - status: 'needs_workspace', - ), - executionTarget: AssistantExecutionTarget.singleAgent, - ); await controller.setAssistantExecutionTarget( AssistantExecutionTarget.singleAgent, ); - controller.assistantThreadRecordsInternal[controller - .currentSessionKey] = controller - .assistantThreadRecordsInternal[controller.currentSessionKey]! - .copyWith( - workspaceBinding: const WorkspaceBinding( - workspaceId: 'main', - workspaceKind: WorkspaceKind.localFs, - workspacePath: '', - displayPath: '', - writable: true, - ), - lifecycleState: controller - .assistantThreadRecordsInternal[controller.currentSessionKey]! - .lifecycleState - .copyWith(status: 'needs_workspace'), - ); final derivedBeforeSave = controller.assistantWorkspacePathForSession( controller.currentSessionKey, ); @@ -544,7 +512,7 @@ void main() { .assistantThreadRecordsInternal[controller.currentSessionKey] ?.lifecycleState .status, - 'needs_workspace', + 'ready', ); await controller.saveSettingsDraft( @@ -564,7 +532,7 @@ void main() { controller.assistantWorkspacePathForSession( controller.currentSessionKey, ), - '${workspaceRoot.path}/.xworkmate/threads/main', + derivedBeforeSave, ); expect(controller.hasPendingSettingsApply, isFalse); expect(controller.hasSettingsDraftChanges, isFalse); @@ -572,7 +540,7 @@ void main() { controller .assistantThreadRecordsInternal[controller.currentSessionKey] ?.displayPath, - '${workspaceRoot.path}/.xworkmate/threads/main', + derivedBeforeSave, ); expect( controller @@ -585,7 +553,7 @@ void main() { ); test( - 'AppController derives a thread workspace path when single-agent binding is empty but workspace root is configured', + 'AppController rejects missing workspace bindings when reading workspace kind', () async { SharedPreferences.setMockInitialValues({}); final tempDirectory = await Directory.systemTemp.createTemp( @@ -616,33 +584,20 @@ void main() { await controller.setAssistantExecutionTarget( AssistantExecutionTarget.singleAgent, ); - final existingMain = - controller.assistantThreadRecordsInternal[controller.currentSessionKey]!; - controller.assistantThreadRecordsInternal[controller.currentSessionKey] = - existingMain.copyWith( - workspaceBinding: const WorkspaceBinding( - workspaceId: 'main', - workspaceKind: WorkspaceKind.localFs, - workspacePath: '', - displayPath: '', - writable: true, - ), - lifecycleState: existingMain.lifecycleState.copyWith( - status: 'needs_workspace', - ), - ); - + controller.assistantThreadRecordsInternal.remove( + controller.currentSessionKey, + ); expect( - controller.assistantWorkspacePathForSession( + () => controller.assistantWorkspaceKindForSession( controller.currentSessionKey, ), - '${workspaceRoot.path}/.xworkmate/threads/main', + throwsA(isA()), ); }, ); test( - 'AppController keeps single-agent threads unbound when the workspace root cannot create thread directories', + 'AppController fails fast when a single-agent thread cannot allocate a writable workspace', () async { SharedPreferences.setMockInitialValues({}); final tempDirectory = await Directory.systemTemp.createTemp( @@ -674,30 +629,12 @@ void main() { AssistantExecutionTarget.singleAgent, ); - controller.initializeAssistantThreadContext( - 'draft:invalid-root', - title: 'Invalid Root', - executionTarget: AssistantExecutionTarget.singleAgent, - ); - - final expectedThreadWorkspace = Directory( - '${invalidRootFile.path}/.xworkmate/threads/draft-invalid-root', - ); - expect(await expectedThreadWorkspace.exists(), isFalse); - expect( - controller.assistantWorkspacePathForSession('draft:invalid-root'), - isEmpty, - ); - expect( - controller - .assistantThreadRecordsInternal['draft:invalid-root'] - ?.lifecycleState - .status, - 'needs_workspace', - ); - await expectLater( - () => controller.sendChatMessage('请输出 SHOULD_NOT_RUN', thinking: 'low'), + () async => controller.initializeAssistantThreadContext( + 'draft:invalid-root', + title: 'Invalid Root', + executionTarget: AssistantExecutionTarget.singleAgent, + ), throwsA(isA()), ); }, diff --git a/test/runtime/secure_config_store_suite_compatibility.dart b/test/runtime/secure_config_store_suite_compatibility.dart index 28f4616c..44d7f237 100644 --- a/test/runtime/secure_config_store_suite_compatibility.dart +++ b/test/runtime/secure_config_store_suite_compatibility.dart @@ -299,7 +299,7 @@ void registerSecureConfigStoreSuiteCompatibilityTestsInternal() { }, 'lifecycleState': const { 'archived': false, - 'status': 'needs_workspace', + 'status': 'ready', 'lastRunAtMs': null, 'lastResultCode': null, }, diff --git a/test/test_support_task_thread_fixture.dart b/test/test_support_task_thread_fixture.dart index eba0748c..bc7dab19 100644 --- a/test/test_support_task_thread_fixture.dart +++ b/test/test_support_task_thread_fixture.dart @@ -9,7 +9,7 @@ TaskThread buildTaskThreadFixture({ AssistantExecutionTarget executionTarget = AssistantExecutionTarget.singleAgent, SingleAgentProvider singleAgentProvider = SingleAgentProvider.auto, - String workspacePath = '', + String workspacePath = '/tmp/task-thread-fixture', WorkspaceKind workspaceKind = WorkspaceKind.localFs, bool writable = true, String? displayPath, @@ -29,9 +29,7 @@ TaskThread buildTaskThreadFixture({ String? lastResultCode, }) { final normalizedDisplayPath = displayPath ?? workspacePath; - final normalizedStatus = - lifecycleStatus ?? - (workspacePath.trim().isEmpty ? 'needs_workspace' : 'ready'); + final normalizedStatus = lifecycleStatus ?? 'ready'; return TaskThread( threadId: threadId, title: title,