Merge branch 'codex/strict-taskthread-docs-align'
This commit is contained in:
commit
a9d3aa35f9
@ -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 信息来源矩阵
|
||||
|
||||
|
||||
@ -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` 组织。
|
||||
|
||||
归档文档仍可保留作为历史背景,但不再参与当前设计说明。
|
||||
|
||||
@ -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、请求构造与结果回写。
|
||||
|
||||
归档文档只保留为历史背景,不再作为当前内部状态设计依据。
|
||||
|
||||
@ -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,也不能改变线程身份
|
||||
|
||||
## 当前代码里的真实组件映射
|
||||
|
||||
|
||||
@ -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<void> tryBindWorkspaceForOnlyChatFallbackInternal(
|
||||
String sessionKey,
|
||||
AssistantExecutionTarget currentTarget,
|
||||
) async {}
|
||||
) async {
|
||||
throw StateError(
|
||||
'tryBindWorkspaceForOnlyChatFallbackInternal is no longer supported.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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(),
|
||||
);
|
||||
|
||||
@ -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.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -60,13 +60,15 @@ class RemoteWebSessionRepository implements WebSessionRepository {
|
||||
Map<String, dynamic> map => map['threads'] as List<dynamic>? ?? const [],
|
||||
_ => const <dynamic>[],
|
||||
};
|
||||
return rawThreads
|
||||
.whereType<Map>()
|
||||
.map(
|
||||
(item) =>
|
||||
TaskThread.fromJson(item.cast<String, dynamic>()),
|
||||
)
|
||||
.toList(growable: false);
|
||||
final records = <TaskThread>[];
|
||||
for (final item in rawThreads.whereType<Map>()) {
|
||||
try {
|
||||
records.add(TaskThread.fromJson(item.cast<String, dynamic>()));
|
||||
} catch (_) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
return List<TaskThread>.unmodifiable(records);
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@ -61,10 +61,15 @@ class WebStore {
|
||||
await clearTaskThreadState();
|
||||
return const <TaskThread>[];
|
||||
}
|
||||
return threads
|
||||
.whereType<Map>()
|
||||
.map((item) => TaskThread.fromJson(item.cast<String, dynamic>()))
|
||||
.toList(growable: false);
|
||||
final records = <TaskThread>[];
|
||||
for (final item in threads.whereType<Map>()) {
|
||||
try {
|
||||
records.add(TaskThread.fromJson(item.cast<String, dynamic>()));
|
||||
} catch (_) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
return List<TaskThread>.unmodifiable(records);
|
||||
} catch (_) {
|
||||
await clearTaskThreadState();
|
||||
return const <TaskThread>[];
|
||||
|
||||
@ -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 <String>['moonshotai/kimi-k2.5'],
|
||||
|
||||
@ -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(<String, Object>{});
|
||||
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(<String, Object>{});
|
||||
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<StateError>()),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
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(<String, Object>{});
|
||||
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<StateError>()),
|
||||
);
|
||||
},
|
||||
|
||||
@ -299,7 +299,7 @@ void registerSecureConfigStoreSuiteCompatibilityTestsInternal() {
|
||||
},
|
||||
'lifecycleState': const <String, Object?>{
|
||||
'archived': false,
|
||||
'status': 'needs_workspace',
|
||||
'status': 'ready',
|
||||
'lastRunAtMs': null,
|
||||
'lastResultCode': null,
|
||||
},
|
||||
|
||||
@ -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,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user