refactor: streamline query requests and session storage
Generic Container CI/CD / test-build-publish (push) Successful in 2m4s
Agent CI/CD / build-test-publish-and-deploy (push) Successful in 2m4s

This commit is contained in:
2026-11-09 18:49:32 +08:00
parent 6279a4b555
commit 5ee93329a4
25 changed files with 111 additions and 100 deletions
+2 -2
View File
@@ -99,11 +99,11 @@ describe("locally rendered answers", () => {
try {
const owner = { actorKey: "actor-answer", projectKey: "project-answer", sessionId: "answer-session" };
const workspaces = new SessionWorkspaceStore(join(root, "workspaces"));
const store = new SessionUiStateStore(join(root, "legacy"), workspaces);
const store = new SessionUiStateStore(workspaces);
const parts: UIMessage["parts"] = [{ type: "text", text: "演练:" }, { type: "data-answer_ui", id: "pressure", data: parseAnswerUiInput(input) }, { type: "text", text: "仍待核验。" }];
const messages = [{ id: "a", role: "assistant", parts }];
await store.write(owner, { sessionId: owner.sessionId, messages });
const restored = new SessionUiStateStore(join(root, "legacy"), new SessionWorkspaceStore(workspaces.root));
const restored = new SessionUiStateStore(new SessionWorkspaceStore(workspaces.root));
expect((await restored.read(owner))?.messages).toEqual(messages);
expect(await restored.read({ ...owner, actorKey: "actor-other" })).toBeNull();
expect(await restored.read({ ...owner, projectKey: "project-other" })).toBeNull();
+1
View File
@@ -68,6 +68,7 @@ try {
if (response.isError) throw new Error(`Extended tool failed through merged MCP: ${name}`);
}
if (!requests.includes("server/discover") || !requests.includes("tools/call") || requests.includes("initialize") || requests.includes("GET")) throw new Error("Transport used unexpected protocol requests");
if (requests.filter((method) => method === "tools/list").length !== 1) throw new Error("Business queries fetched the catalog again after startup");
if (grants.join(",") !== "client_credentials,urn:ietf:params:oauth:grant-type:token-exchange") throw new Error("OAuth grants were not validated");
console.log(`PASS: local MCP ${QUERY_MCP_TOOL_IDS.length}-tool discovery, real Server schemas, OAuth discovery, resource-bound grants, host binding, extended query and revocation over MCP HTTP`);
} finally {
+18
View File
@@ -26,6 +26,24 @@ const resourceToken = jwt("resource");
const token = () => Response.json({ access_token: resourceToken, token_type: "Bearer", expires_in: 300 });
function credential(id: string) { sessions.push(id); bindSessionCredential(id, { authorization: `Bearer ${jwt(id)}`, projectId: "project" }); }
it("uses cached tool definitions without fetching the catalog on each query", async () => {
const id = "cached-tool-definition"; credential(id);
const client = fixture((async (url, init) => {
if (String(url).includes("/token")) return token();
return init?.method === "POST"
? Response.json({ context_handle: "x".repeat(43) })
: new Response(null, { status: 204 });
}) as McpHttpFetch);
const definition = { ...catalog[0], outputSchema: { type: "object", properties: { ok: { type: "boolean" } } } };
Object.assign(client, { tools: [definition], withClient: async (_token: string, _project: string, _signal: AbortSignal, run: (client: unknown) => Promise<unknown>) => run({
callTool: async (_params: unknown, options: { toolDefinition: unknown }) => {
expect(options.toolDefinition).toBe(definition);
return { content: [], structuredContent: { ok: true } };
},
}) });
expect((await client.query(id, owner(id), definition.name, {}, "call")).structuredContent).toEqual({ ok: true });
});
it("owns simulation HTTP budgets and rejects model overrides before OAuth", async () => {
const id = "mcp-calculation-budget"; credential(id);
let requests = 0, transportTimeout = 0, sdkTimeout = 0;
+1 -1
View File
@@ -31,7 +31,7 @@ it("authenticates state reads and writes against the persisted session owner and
const context = getLocalAgentContext(ownerRequest);
const sessionId = "state-session";
await sessions.ensure({ ...context, sessionId });
await new SessionUiStateStore(join(root, "unused"), workspaces).write({ ...context, sessionId }, { sessionId, messages: [{ id: "assistant", role: "assistant", parts: [{ type: "data-answer_ui", data: { version: "answer-ui@1", block_id: "block", fallback_text: "fallback", spec: { root: "evidence", elements: { evidence: { type: "Evidence", props: { title: "Evidence", summary: "Summary", source: "source", evidence_id: null, observed_at: null, kind: "observed" }, children: [] } } } } }] }] });
await new SessionUiStateStore(workspaces).write({ ...context, sessionId }, { sessionId, messages: [{ id: "assistant", role: "assistant", parts: [{ type: "data-answer_ui", data: { version: "answer-ui@1", block_id: "block", fallback_text: "fallback", spec: { root: "evidence", elements: { evidence: { type: "Evidence", props: { title: "Evidence", summary: "Summary", source: "source", evidence_id: null, observed_at: null, kind: "observed" }, children: [] } } } } }] }] });
const app = express();
app.use(express.json({ limit: "2mb" }));
app.use(async (req, res, next) => {
+1 -1
View File
@@ -22,7 +22,7 @@ it("persists owned topics, searches old text across themes and preserves chats w
globalThis.fetch = (async (_input, init) => Response.json({ user_id: new Headers(init?.headers).get("authorization") === "Bearer owner" ? "owner" : "other", project_id: new Headers(init?.headers).get("x-project-id"), network: "supply" })) as typeof fetch;
const workspaces = new SessionWorkspaceStore(join(root, "workspaces"));
const sessions = new SessionMetadataStore(join(root, "metadata"), workspaces);
const states = new SessionUiStateStore(join(root, "ui"), workspaces);
const states = new SessionUiStateStore(workspaces);
for (const id of ["a", "b", "unclassified"]) {
await sessions.ensure({ ...context, sessionId: id });
await states.write({ ...context, sessionId: id }, { sessionId: id, messages: [
+2 -1
View File
@@ -7,6 +7,7 @@ import { attachTrustedAgentContext } from "../../src/context/localContext.js";
import { registerChatInteractionRoutes } from "../../src/routes/chatInteractionRoutes.js";
import { bindSessionCredential, credentialRefreshCoordinator, releaseSessionCredential } from "../../src/product/credential-refresh.js";
import { SessionMetadataStore } from "../../src/sessions/metadataStore.js";
import { SessionWorkspaceStore } from "../../src/sessions/workspace-store.js";
import { SessionUiStateStore } from "../../src/sessions/uiStateStore.js";
import { toActorKey, toProjectKey } from "../../src/utils/fileStore.js";
import type { ActiveRun } from "../../src/routes/chatUiState.js";
@@ -28,7 +29,7 @@ describe("credential refresh HTTP route", () => {
const router = express.Router();
router.use((req, res, next) => { void attachTrustedAgentContext(req).then(() => next()).catch(() => res.sendStatus(401)); });
const activeRuns = new Map<string, ActiveRun>([["credential-route-session", { clientSessionId: "credential-route-session", controller: new AbortController(), messages: [], pendingPermissions: new Map(), pendingQuestions: new Map(), subscribers: new Set(), status: "running" }]]);
registerChatInteractionRoutes(router, { activeRuns, runtime: {} as OpencodeRuntimeAdapter, sessionMetadataStore: metadata, sessionUiStateStore: new SessionUiStateStore(join(directory, "ui")) });
registerChatInteractionRoutes(router, { activeRuns, runtime: {} as OpencodeRuntimeAdapter, sessionMetadataStore: metadata, sessionUiStateStore: new SessionUiStateStore(new SessionWorkspaceStore(join(directory, "workspaces"))) });
app.use(router);
const server = app.listen(0, "127.0.0.1");
await new Promise<void>((resolve) => server.once("listening", resolve));
+1 -1
View File
@@ -115,7 +115,7 @@ it("uses authenticated /responses for saved answers and rejects bypass and forei
globalThis.fetch = (async (_input, init) => Response.json({ user_id: new Headers(init?.headers).get("Authorization") === "Bearer owner" ? "owner" : "other", project_id: project, network: "fixture" })) as typeof fetch;
const workspaces = new SessionWorkspaceStore(join(root, "workspaces"));
const sessions = new SessionMetadataStore(join(root, "metadata"), workspaces);
const states = new SessionUiStateStore(join(root, "unused"), workspaces);
const states = new SessionUiStateStore(workspaces);
const interactions = new InteractionRequestStore(workspaces);
const context = { actorKey: toActorKey("product:owner"), projectKey: toProjectKey(`product:${project}`), projectId: project, userId: "owner", sessionId: "owned" };
await sessions.ensure(context);
+30 -3
View File
@@ -5,16 +5,43 @@ import { tmpdir } from "node:os";
import { join } from "node:path";
import { getLocalAgentContext } from "../../src/context/localContext.js";
import { registerChatInteractionRoutes } from "../../src/routes/chatInteractionRoutes.js";
import type { OpencodeRuntimeAdapter } from "../../src/runtime/opencode.js";
import { 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("reads a native form once when checking and answering a persisted question", async () => {
const root = await mkdtemp(join(tmpdir(), "question-single-read-"));
const workspaces = new SessionWorkspaceStore(join(root, "workspaces"));
const sessions = new SessionMetadataStore(join(root, "metadata"), workspaces);
const states = new SessionUiStateStore(workspaces);
const context = { ...getLocalAgentContext(), sessionId: "session" };
await sessions.ensure(context);
await states.write(context, { sessionId: context.sessionId, messages: [{ role: "assistant", questions: [{ requestId: "form", status: "pending" }] }] });
const calls: string[] = [];
const runtime = Object.assign(Object.create(OpencodeRuntimeAdapter.prototype), {
ensureClient: async () => ({ session: { form: {
get: async () => { calls.push("get"); return { fields: [{ key: "answer", type: "text" }], state: { status: "pending" } }; },
reply: async (input: { answer: unknown }) => { calls.push("reply"); expect(input.answer).toEqual({ answer: "A" }); },
} } }),
});
const routes = new Map<string, (req: unknown, res: unknown) => Promise<void>>();
const router = { get() {}, post(path: string, handler: (req: unknown, res: unknown) => Promise<void>) { routes.set(path, handler); } };
registerChatInteractionRoutes(router as unknown as express.Router, { activeRuns: new Map(), runtime, sessionMetadataStore: sessions, sessionUiStateStore: states });
const req = { params: { request_id: "form" }, body: { session_id: "session", answers: [["A"]] }, header() { return undefined; } };
const response = { code: 200, setHeader() {}, status(code: number) { this.code = code; return this; }, json() { return this; } };
try {
await routes.get("/question/:request_id/reply")!(req, response);
expect(response.code).toBe(202);
expect(calls).toEqual(["get", "reply"]);
} finally { await rm(root, { recursive: true, force: true }); }
});
it("reconciles persisted legacy forms and replies after the active run was lost", async () => {
const root = await mkdtemp(join(tmpdir(), "legacy-question-routes-"));
const workspaces = new SessionWorkspaceStore(join(root, "workspaces"));
const sessions = new SessionMetadataStore(join(root, "metadata"), workspaces);
const states = new SessionUiStateStore(join(root, "unused"), workspaces);
const states = new SessionUiStateStore(workspaces);
const context = { ...getLocalAgentContext(), sessionId: "session" };
await sessions.ensure(context);
await states.write(context, { sessionId: "session", messages: [{ id: "old", role: "assistant", questions: [{ requestId: "form", status: "pending", tool: { callID: "call" } }] }, { id: "new", role: "assistant", text: "later message", parts: [{ type: "data-question_request", data: { request_id: "form2", session_id: "session", questions: [], tool: { callID: "call2" } } }, { type: "data-question_request", data: { request_id: "form3", session_id: "session", questions: [] } }, { type: "data-question_request", data: { request_id: "form4", session_id: "session", questions: [] } }] }] });
@@ -22,7 +49,7 @@ it("reconciles persisted legacy forms and replies after the active run was lost"
let unavailable = false;
let reads = 0;
let replies = 0;
const runtime = { getQuestionStatus: async () => { reads++; if (unavailable) throw new Error("private-native-path"); return status; }, replyQuestion: async () => { replies++; status = "answered"; }, rejectQuestion: async () => { status = "cancelled"; } } as unknown as OpencodeRuntimeAdapter;
const runtime = { getQuestion: async () => { reads++; if (unavailable) throw new Error("private-native-path"); return status === "missing" ? null : { state: { status } }; }, replyQuestion: async () => { replies++; status = "answered"; }, rejectQuestion: async () => { status = "cancelled"; } } as unknown as OpencodeRuntimeAdapter;
const app = express(); app.use(express.json());
registerChatInteractionRoutes(app, { activeRuns: new Map(), runtime, sessionMetadataStore: sessions, sessionUiStateStore: states });
const server = app.listen(0, "127.0.0.1");
+1 -1
View File
@@ -16,7 +16,7 @@ it.each([false, true])("persists permission replies in the owner's workspace (na
const root = await mkdtemp(join(tmpdir(), "permission-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 states = new SessionUiStateStore(workspaces);
const context = { ...getLocalAgentContext(), sessionId: "permission-session" };
await sessions.ensure(context);
const messages = [{ id: "answer", role: "assistant", permissions: [] }];
+2 -2
View File
@@ -17,7 +17,7 @@ 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 states = new SessionUiStateStore(workspaces);
const context = { ...getLocalAgentContext(), sessionId: "history-session" };
await sessions.ensure(context);
const history: UIMessage[] = [{ id: "old-answer", role: "assistant", parts: [
@@ -35,7 +35,7 @@ it("keeps historical question invalidation across the next live snapshot", async
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 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 });
+1 -1
View File
@@ -44,7 +44,7 @@ beforeEach(async () => {
})) as typeof fetch;
const workspaces = new SessionWorkspaceStore(join(root, "workspaces"));
const sessions = new SessionMetadataStore(join(root, "metadata"), workspaces);
states = new SessionUiStateStore(join(root, "ui"), workspaces);
states = new SessionUiStateStore(workspaces);
interactions = new InteractionRequestStore(workspaces);
await sessions.initialize();
const context = { actorKey: toActorKey("product:owner"), projectKey: toProjectKey(`product:${project}`), projectId: project, userId: "owner", sessionId: "owned" };
+1 -1
View File
@@ -47,7 +47,7 @@ describe("chat session HTTP isolation", () => {
const workspaces = new SessionWorkspaceStore(join(metadataDir, "workspaces"));
const sessionMetadataStore = new SessionMetadataStore(metadataDir, workspaces);
const sessionUiStateStore = new SessionUiStateStore(join(metadataDir, "old-ui"), workspaces);
const sessionUiStateStore = new SessionUiStateStore(workspaces);
await sessionMetadataStore.initialize();
const ownerContext = {
actorKey: toActorKey("product:user-a"),
@@ -33,7 +33,7 @@ async function fixture() {
const context = { actorKey: toActorKey("product:owner"), projectKey: toProjectKey(`product:${projectId}`), projectId, userId: "owner" };
const workspaces = new SessionWorkspaceStore(join(root, "workspaces"));
const sessions = new SessionMetadataStore(join(root, "metadata"), workspaces);
const states = new SessionUiStateStore(join(root, "states"), workspaces);
const states = new SessionUiStateStore(workspaces);
const taskNames = new Map([[firstTask, "南桥管道爆管调查"], [secondTask, "城南供水压力复核"]]);
const taskReads: string[] = [];
let available = true;
+3 -3
View File
@@ -33,11 +33,11 @@ test("native question lookup distinguishes missing form from unavailable service
const adapter = Object.assign(Object.create(OpencodeRuntimeAdapter.prototype), {
ensureClient: async () => ({ session: { form: { get: async () => { if (failure) throw failure; return { state: { status: "answered" } }; } } } }),
}) as OpencodeRuntimeAdapter;
expect(await adapter.getQuestionStatus({ sessionId: "s", requestId: "form" })).toBe("missing");
expect(await adapter.getQuestion({ sessionId: "s", requestId: "form" })).toBeNull();
failure = new Error("network unavailable");
await expect(adapter.getQuestionStatus({ sessionId: "s", requestId: "form" })).rejects.toThrow("network unavailable");
await expect(adapter.getQuestion({ sessionId: "s", requestId: "form" })).rejects.toThrow("network unavailable");
failure = undefined;
expect(await adapter.getQuestionStatus({ sessionId: "s", requestId: "form" })).toBe("answered");
expect(await adapter.getQuestion({ sessionId: "s", requestId: "form" })).toMatchObject({ state: { status: "answered" } });
});
test("message admission lookup fails closed on inbox and message transport errors", async () => {
let inboxFailure = true;
+1 -1
View File
@@ -26,7 +26,7 @@ describe("answer view state", () => {
beforeEach(async () => {
base = await mkdtemp(join(tmpdir(), "answer-state-"));
workspaces = new SessionWorkspaceStore(join(base, "workspaces"));
messages = new SessionUiStateStore(join(base, "unused"), workspaces);
messages = new SessionUiStateStore(workspaces);
states = new AnswerViewStateStore(workspaces);
currentSpec = spec();
await saveMessages();
+1 -1
View File
@@ -44,7 +44,7 @@ describe("learning session lifecycle", () => {
});
const orchestrator = (runtime: object) => new LearningOrchestrator(
runtime as OpencodeRuntimeAdapter, new MemoryStore(join(base, "memory"), join(base, "memory-backup")),
new SessionTranscriptStore(join(base, "legacy-transcripts"), records), state,
new SessionTranscriptStore(records), state,
);
it("releases both native helper sessions after a completed review and persists progress in chat", async () => {
+2 -2
View File
@@ -3,6 +3,7 @@ import { mkdtemp, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { SessionWorkspaceStore } from "../../src/sessions/workspace-store.js";
import { SessionTranscriptStore } from "../../src/sessions/transcriptStore.js";
describe("SessionTranscriptStore", () => {
@@ -11,8 +12,7 @@ describe("SessionTranscriptStore", () => {
beforeEach(async () => {
tempDir = await mkdtemp(join(tmpdir(), "tjwater-transcript-"));
store = new SessionTranscriptStore(tempDir);
await store.initialize();
store = new SessionTranscriptStore(new SessionWorkspaceStore(tempDir));
});
afterEach(async () => {
+3 -3
View File
@@ -2,16 +2,16 @@ import { expect, it } from "bun:test";
import { mkdtemp, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { SessionWorkspaceStore } from "../../src/sessions/workspace-store.js";
import { SessionUiStateStore } from "../../src/sessions/uiStateStore.js";
it("restores plan history independently of chat messages after reconnecting", async () => {
const directory = await mkdtemp(join(tmpdir(), "tjwater-plan-state-"));
try {
const store = new SessionUiStateStore(directory);
await store.initialize();
const store = new SessionUiStateStore(new SessionWorkspaceStore(directory));
const executionPlans = [{ revision: 1, nodes: [{ id: "sampling", status: "waiting" }] }];
await store.write({ sessionId: "session-1", actorKey: "actor-test", projectKey: "project-test" }, { sessionId: "session-1", messages: [], executionPlans });
const reloaded = await new SessionUiStateStore(directory).read({ sessionId: "session-1", actorKey: "actor-test", projectKey: "project-test" });
const reloaded = await new SessionUiStateStore(new SessionWorkspaceStore(directory)).read({ sessionId: "session-1", actorKey: "actor-test", projectKey: "project-test" });
expect(reloaded?.executionPlans).toEqual(executionPlans);
expect(await store.read({ sessionId: "other-session", actorKey: "actor-test", projectKey: "project-test" })).toBeNull();
} finally {
+11 -11
View File
@@ -20,11 +20,11 @@ describe("owned session workspaces", () => {
afterEach(async () => { await rm(base, { recursive: true, force: true }); });
it("keeps messages and searchable transcripts private to their user and scope", async () => {
const ui = new SessionUiStateStore(join(base, "old-ui"), records);
const transcript = new SessionTranscriptStore(join(base, "old-transcripts"), records);
const ui = new SessionUiStateStore(records);
const transcript = new SessionTranscriptStore(records);
await ui.write(owner, { sessionId: owner.sessionId, messages: [{ text: "private answer" }] });
await transcript.appendTurn(owner, { userMessage: "private question", assistantMessage: "private answer", toolCallCount: 0 });
expect((await new SessionUiStateStore(join(base, "old-ui"), records).read(owner))?.messages).toHaveLength(1);
expect((await new SessionUiStateStore(records).read(owner))?.messages).toHaveLength(1);
expect(await ui.read({ ...owner, actorKey: "actor-b" })).toBeNull();
expect(await ui.read({ ...owner, projectKey: "project-b" })).toBeNull();
expect(await transcript.search(owner, "private")).toHaveLength(2);
@@ -36,7 +36,7 @@ describe("owned session workspaces", () => {
});
it("has a separate personal namespace for each user and separates it from projects", async () => {
const ui = new SessionUiStateStore(join(base, "old-ui"), records);
const ui = new SessionUiStateStore(records);
const personal = { ...owner, projectKey: "personal-actor-a" };
await ui.write(personal, { sessionId: owner.sessionId, messages: [{ text: "personal answer" }] });
expect(records.path(personal, "messages.json")).toContain("/actor-a/personal/");
@@ -60,13 +60,13 @@ describe("owned session workspaces", () => {
expect(await records.migrateLegacyRecords(options)).toEqual({ uiStates: 1, transcripts: 1, learningStates: 0, retainedUnownedUiStates: 1 });
expect(await readFile(records.path(owner, "messages.json"), "utf8")).toBe(messages);
expect(await readFile(records.path(owner, "transcript.json"), "utf8")).toBe(turns);
await new SessionUiStateStore(uiStateDir, records).write(owner, { sessionId: owner.sessionId, messages: [{ text: "new answer" }] });
await new SessionUiStateStore(records).write(owner, { sessionId: owner.sessionId, messages: [{ text: "new answer" }] });
expect((await records.migrateLegacyRecords(options)).uiStates).toBe(0);
const archive = join(sessionWorkspaceDirectory(records.backupRoot, owner), "chat");
const backup = (await readdir(archive)).find((name) => name.startsWith("messages-"))!;
expect(await readFile(join(archive, backup), "utf8")).toBe(messages);
expect(await readdir(uiStateDir)).toEqual(["orphan.json"]);
expect((await new SessionUiStateStore(uiStateDir, records).read(owner))?.messages).toEqual([{ text: "new answer" }]);
expect((await new SessionUiStateStore(records).read(owner))?.messages).toEqual([{ text: "new answer" }]);
});
it("rejects traversal, linked records and mismatched ownership", async () => {
@@ -75,7 +75,7 @@ describe("owned session workspaces", () => {
const foreign = join(base, "foreign.json");
await writeFile(foreign, "private");
await symlink(foreign, records.path(owner, "messages.json"));
await expect(new SessionUiStateStore(join(base, "old-ui"), records).read(owner)).rejects.toThrow("symbolic link");
await expect(new SessionUiStateStore(records).read(owner)).rejects.toThrow("symbolic link");
await writeFile(join(sessionWorkspaceDirectory(records.root, owner), "chat", "owner.json"), JSON.stringify({ ...owner, actorKey: "actor-b" }));
await expect(records.ensure(owner)).rejects.toThrow("ownership mismatch");
});
@@ -114,8 +114,8 @@ describe("owned session workspaces", () => {
for (const path of [metadataDir, uiStateDir, transcriptDir, learningStateDir]) await mkdir(path);
const metadata = new SessionMetadataStore(metadataDir, records);
const { record } = await metadata.ensure(owner);
const ui = new SessionUiStateStore(uiStateDir, records);
const transcript = new SessionTranscriptStore(transcriptDir, records);
const ui = new SessionUiStateStore(records);
const transcript = new SessionTranscriptStore(records);
const learning = new SessionLearningStateStore(learningStateDir, records);
const legacyUi = JSON.stringify({ sessionId: owner.sessionId, messages: [{ text: "private content" }] });
await writeFile(join(uiStateDir, "chat.json"), legacyUi);
@@ -153,7 +153,7 @@ describe("owned session workspaces", () => {
const metadataDir = join(base, "metadata");
const metadata = new SessionMetadataStore(metadataDir, records);
const { record } = await metadata.ensure(owner);
const transcript = new SessionTranscriptStore(join(base, "legacy"), records);
const transcript = new SessionTranscriptStore(records);
await transcript.appendTurn(owner, { userMessage: "private question", assistantMessage: "private answer", toolCallCount: 0 });
const markDeleted = records.markDeleted.bind(records);
records.markDeleted = async () => { throw new Error("disk write interrupted"); };
@@ -163,7 +163,7 @@ describe("owned session workspaces", () => {
await expect(records.writeRecord(owner, "messages.json", { sessionId: owner.sessionId, messages: [] })).rejects.toThrow("session not found");
records = new SessionWorkspaceStore(records.root, records.backupRoot);
const restarted = new SessionMetadataStore(metadataDir, records);
expect(await new SessionTranscriptStore(join(base, "legacy"), records).search(owner, "private")).toEqual([]);
expect(await new SessionTranscriptStore(records).search(owner, "private")).toEqual([]);
await expect(restarted.ensure(owner)).rejects.toThrow("session not found");
await markDeleted(owner);
await restarted.remove(record);