72 lines
4.3 KiB
TypeScript
72 lines
4.3 KiB
TypeScript
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(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 = { getQuestion: async () => { nativeReads++; return null; } } 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 });
|
|
}
|
|
});
|