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
+71
View File
@@ -0,0 +1,71 @@
import { expect, it } from "bun:test";
import express from "express";
import { mkdtemp, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import type { UIMessage } from "ai";
import { getLocalAgentContext } from "../../src/context/localContext.js";
import { createChatRunAccumulator } from "../../src/routes/chat-run-accumulator.js";
import { registerChatInteractionRoutes } from "../../src/routes/chatInteractionRoutes.js";
import { createInitialStreamingMessages, type ActiveRun } from "../../src/routes/chatUiState.js";
import type { OpencodeRuntimeAdapter } from "../../src/runtime/opencode.js";
import { SessionMetadataStore } from "../../src/sessions/metadataStore.js";
import { SessionUiStateStore } from "../../src/sessions/uiStateStore.js";
import { SessionWorkspaceStore } from "../../src/sessions/workspace-store.js";
it("keeps historical question invalidation across the next live snapshot", async () => {
const root = await mkdtemp(join(tmpdir(), "question-persistence-"));
const workspaces = new SessionWorkspaceStore(join(root, "workspaces"));
const sessions = new SessionMetadataStore(join(root, "metadata"), workspaces);
const states = new SessionUiStateStore(join(root, "unused"), workspaces);
const context = { ...getLocalAgentContext(), sessionId: "history-session" };
await sessions.ensure(context);
const history: UIMessage[] = [{ id: "old-answer", role: "assistant", parts: [
{ type: "data-question_request", data: { request_id: "old-question", session_id: context.sessionId,
questions: [{ question: "继续吗?", options: [] }] } },
{ type: "data-final_answer", data: { content: "此前的结果" } },
] }, { id: "user", role: "user", parts: [{ type: "text", text: "新一轮" }] }];
const run: ActiveRun = {
clientSessionId: context.sessionId, controller: new AbortController(), status: "running",
messages: createInitialStreamingMessages(history, "新一轮"),
pendingPermissions: new Map(), pendingQuestions: new Map(), subscribers: new Set(),
};
const stream = createChatRunAccumulator({ run, messages: history, messageId: "current-answer",
sessionId: context.sessionId, collaboration: false, write() {}, onStatus() {} });
stream.apply("token", { content: "正在核查" });
await states.write(context, { sessionId: context.sessionId, isTitleManuallyEdited: true, messages: stream.uiMessages() });
let nativeReads = 0;
const runtime = { getQuestionStatus: async () => { nativeReads++; return "missing"; } } as unknown as OpencodeRuntimeAdapter;
const app = express();
registerChatInteractionRoutes(app, { activeRuns: new Map([[context.sessionId, run]]), runtime,
sessionMetadataStore: sessions, sessionUiStateStore: states });
const server = app.listen(0, "127.0.0.1");
await new Promise<void>((resolve) => server.once("listening", resolve));
const url = `http://127.0.0.1:${(server.address() as { port: number }).port}/session/${context.sessionId}/questions/old-question`;
const resolved = (messages: unknown[]) => {
const message = messages[0] as UIMessage;
expect(message.id).toBe("old-answer");
const part = message.parts.find((part) => part.type === "data-question_response");
expect(part).toMatchObject({ type: "data-question_response", data: { request_id: "old-question", status: "invalidated" } });
};
try {
expect((await fetch(url, { headers: { "X-Agent-Local-User": "other" } })).status).toBe(404);
expect(nativeReads).toBe(0);
const response = await fetch(url);
expect(response.status).toBe(200);
expect(await response.json()).toMatchObject({ status: "invalidated" });
resolved((await states.read(context))!.messages);
resolved(run.messages);
resolved(stream.uiMessages());
expect(stream.uiMessages().at(-1)?.parts.some((part) => part.type === "data-question_response")).toBe(false);
stream.apply("token", { content: ",继续查询" });
await states.write(context, { ...(await states.read(context))!, messages: stream.uiMessages() });
resolved((await states.read(context))!.messages);
expect((await states.read(context))?.isTitleManuallyEdited).toBe(true);
await fetch(url);
expect(nativeReads).toBe(1);
} finally {
await new Promise<void>((resolve) => server.close(() => resolve()));
await rm(root, { recursive: true, force: true });
}
});