fix(agent): synchronize conversation preparation state

This commit is contained in:
yu
2026-08-05 10:40:10 +08:00
parent 46f29f70ca
commit 9bccd0ff1a
13 changed files with 511 additions and 124 deletions
+46 -1
View File
@@ -214,7 +214,7 @@ test("shared thread events are broadcast with the active thread id", (t) => {
});
test("new clients receive the current Codex state and later updates", (t) => {
const session = new CanvasSession();
const session = new CanvasSession("thread-2");
session.setCodexState({ busy: true, threadId: "thread-2", turnId: "turn-1" });
session.trackCodexEvent("codex_approval", { requestId: "approval-1", threadId: "thread-2" });
const client = connect(session, "first", "thread-2");
@@ -223,6 +223,7 @@ test("new clients receive the current Codex state and later updates", (t) => {
const hello = client.event("hello");
assert.equal(field(hello, "protocolVersion"), 5);
assert.deepEqual(field(hello, "workspace"), { activeThreadId: "thread-2" });
assert.deepEqual(field(hello, "conversation"), { revision: 1, conversationId: "thread-2", threadId: "thread-2", status: "ready", mcpStatuses: {} });
assert.deepEqual(field(hello, "codex"), { busy: true, threadId: "thread-2", turnId: "turn-1" });
assert.deepEqual(field(hello, "pendingApprovals"), [{ requestId: "approval-1", threadId: "thread-2" }]);
@@ -236,6 +237,50 @@ test("new clients receive the current Codex state and later updates", (t) => {
assert.deepEqual(client.event("codex_state"), { busy: false, threadId: "thread-2", turnId: "turn-1" });
});
test("对话 revision 单调递增且 MCP 全部进入终态前保持 preparing", () => {
const session = new CanvasSession();
const revisions = [session.conversationStateSnapshot.revision];
revisions.push(session.beginConversation({ sourceClientId: "first" }).revision);
revisions.push(session.updateConversationMcp("late-service", "starting").revision);
revisions.push(session.completeConversationMcpInventory([{ name: "infinite-canvas", authStatus: "unsupported" }]).revision);
const pending = session.completeConversationPreparation("thread-1");
revisions.push(pending.revision);
assert.equal(pending.status, "preparing");
const ready = session.updateConversationMcp("late-service", "ready");
revisions.push(ready.revision);
assert.equal(ready.status, "ready");
assert.equal(ready.threadId, "thread-1");
revisions.slice(1).forEach((revision, index) => assert.ok(revision > revisions[index]));
});
test("可选 MCP 失败进入 warning,画布 MCP 失败进入 failed", () => {
const optionalFailure = new CanvasSession();
optionalFailure.beginConversation();
optionalFailure.completeConversationMcpInventory([
{ name: "infinite-canvas", authStatus: "unsupported" },
{ name: "notion", authStatus: "notLoggedIn" },
]);
const warning = optionalFailure.completeConversationPreparation("thread-1");
assert.equal(warning.status, "warning");
assert.equal(warning.mcpStatuses.notion.status, "failed");
const requiredFailure = new CanvasSession();
requiredFailure.beginConversation();
requiredFailure.completeConversationMcpInventory([{ name: "infinite-canvas", authStatus: "notLoggedIn" }]);
const failed = requiredFailure.completeConversationPreparation("thread-2");
assert.equal(failed.status, "failed");
assert.match(failed.error || "", /Infinite Canvas MCP/);
const requiredMissing = new CanvasSession();
requiredMissing.beginConversation();
requiredMissing.completeConversationMcpInventory([{ name: "notion", authStatus: "unsupported" }]);
const missing = requiredMissing.completeConversationPreparation("thread-3");
assert.equal(missing.status, "failed");
assert.match(missing.error || "", /Infinite Canvas MCP/);
});
test("Codex 写操作在多窗口之间互斥且不能与运行 turn 并发", () => {
const session = new CanvasSession();
assert.equal(session.beginCodexMutation(), true);