refactor: consolidate chat state and task bridges

Centralize session scope, chat projections and typed task bridges.
Keep historical interaction replies authoritative and publish terminal state to subscribers. Validate Server contracts before replacing generated artifacts.
This commit is contained in:
2026-11-09 17:55:00 +08:00
parent addc81e106
commit 6279a4b555
31 changed files with 849 additions and 522 deletions
+30
View File
@@ -296,6 +296,11 @@ it("marks an explicitly aborted response incomplete and preserves scoped history
const text = await response.text();
expect(text).toContain("event: response.incomplete");
expect(text).not.toContain("event: response.completed");
const context = { actorKey: toActorKey("product:owner"), projectKey: toProjectKey(`product:${project}`), sessionId: "owned" };
const saved = (await states.read(context))!.messages as Array<{ parts: Array<{ type: string; data?: { status?: string } }> }>;
expect(saved.at(-1)?.parts.find((part) => part.type === "data-run_status")?.data?.status).toBe("aborted");
const loaded = await http(`${base}/api/v1/session/owned`, { headers: { Authorization: "Bearer owner", "X-Project-Id": project } });
expect((await loaded.json()).messages).toEqual(saved);
});
it("continues a paged conversation from the complete saved history and preserves client message IDs", async () => {
@@ -316,3 +321,28 @@ it("continues a paged conversation from the complete saved history and preserves
expect((await call({ input: "重复提交", conversation: "owned", tjwater: { client_message_id: "client-turn-a" } })).status).toBe(409);
expect(prompted).toHaveLength(1);
});
it("uses the same canonical live messages for session reads and older pages", async () => {
gate = new Promise<void>((resolve) => { release = resolve; });
const context = { actorKey: toActorKey("product:owner"), projectKey: toProjectKey(`product:${project}`), sessionId: "owned" };
const history = Array.from({ length: 5 }, (_, index) => ({ id: `old-${index}`, role: index % 2 ? "assistant" : "user",
parts: [{ type: "text", text: `历史 ${index}` }] as unknown[] }));
history[1].parts.push({ type: "data-question_request", data: { request_id: "old-question", session_id: "owned", questions: [] } });
await states.write(context, { sessionId: "owned", messages: history });
const response = await call({ input: "新一轮", conversation: "owned", stream: true, tjwater: { client_message_id: "live-user" } });
const headers = { Authorization: "Bearer owner", "X-Project-Id": project };
const live = await http(`${base}/api/v1/session/owned`, { headers });
const snapshot = await live.json();
expect(snapshot.is_streaming).toBe(true);
expect(snapshot.messages).toHaveLength(7);
expect(snapshot.messages.slice(0, 5)).toEqual(history);
expect(snapshot.messages[5].id).toBe("live-user");
const recent = await http(`${base}/api/v1/session/owned?message_limit=2`, { headers });
const page = await recent.json();
expect(page.messages).toEqual(snapshot.messages.slice(-2));
const older = await http(`${base}/api/v1/session/owned/messages?limit=40&before=${encodeURIComponent(page.message_page.before_cursor)}`, { headers });
expect(older.status).toBe(200);
expect((await older.json()).messages).toEqual(history);
release();
await response.text();
});