78 lines
6.9 KiB
TypeScript
78 lines
6.9 KiB
TypeScript
import { afterEach, expect, it } from "bun:test";
|
|
import express from "express";
|
|
import { mkdtemp, rm } from "node:fs/promises";
|
|
import type { Server } from "node:http";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { buildChatRouter } from "../../src/routes/chat.js";
|
|
import { SessionMetadataStore } from "../../src/sessions/metadataStore.js";
|
|
import { SessionUiStateStore } from "../../src/sessions/uiStateStore.js";
|
|
import { SessionWorkspaceStore } from "../../src/sessions/workspace-store.js";
|
|
import { toActorKey, toProjectKey } from "../../src/utils/fileStore.js";
|
|
|
|
const http = globalThis.fetch;
|
|
let root: string;
|
|
let server: Server;
|
|
afterEach(async () => { globalThis.fetch = http; if (server) { server.closeAllConnections(); await new Promise<void>((resolve) => server.close(() => resolve())); } if (root) await rm(root, { recursive: true, force: true }); });
|
|
|
|
it("persists owned topics, searches old text across themes and preserves chats when a theme is deleted", async () => {
|
|
root = await mkdtemp(join(tmpdir(), "tjwater-topics-"));
|
|
const project = "10000000-0000-4000-8000-000000000001";
|
|
const context = { actorKey: toActorKey("product:owner"), projectKey: toProjectKey(`product:${project}`), projectId: project, userId: "owner" };
|
|
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);
|
|
for (const id of ["a", "b", "unclassified"]) {
|
|
await sessions.ensure({ ...context, sessionId: id });
|
|
await states.write({ ...context, sessionId: id }, { sessionId: id, messages: [
|
|
{ id: "old", role: "user", parts: [{ type: "text", text: `历史最早消息的共同关键词:南桥阀门 ${id}` }] },
|
|
...Array.from({ length: 60 }, (_, index) => ({ id: `message-${index}`, role: "assistant", parts: [{ type: "text", text: "正在复核监测记录" }] })),
|
|
{ role: "assistant", parts: [{ type: "reasoning", text: "隐藏推理密码" }, { type: "tool-input", text: "工具秘密参数" }] },
|
|
] });
|
|
}
|
|
await sessions.ensure({ ...context, sessionId: "collaboration", purpose: "task_collaboration" });
|
|
await states.write({ ...context, sessionId: "collaboration" }, { sessionId: "collaboration", messages: [{ role: "user", parts: [{ type: "text", text: "协作讨论:南桥阀门" }] }] });
|
|
await sessions.ensure({ ...context, sessionId: "execution", purpose: "task_execution" });
|
|
const app = express(); app.use(express.json());
|
|
app.use("/api/v1", buildChatRouter({ sessionMetadataStore: sessions, sessionUiStateStore: states, runtime: { createSession: async () => ({ id: "new-topic-chat" }) } as never,
|
|
sessionBridge: {} as never, sessionTranscriptStore: {} as never, memoryStore: {} as never, learningOrchestrator: {} as never, frontendActionCoordinator: {} as never }));
|
|
server = app.listen(0, "127.0.0.1");
|
|
await new Promise<void>((resolve) => server.once("listening", resolve));
|
|
const address = server.address(); if (!address || typeof address === "string") throw new Error("missing address");
|
|
const request = async (path: string, method = "GET", body?: unknown, user = "owner", projectId = project) => {
|
|
const response = await http(`http://127.0.0.1:${address.port}/api/v1${path}`, { method,
|
|
headers: { Authorization: `Bearer ${user}`, "X-Project-Id": projectId, "Content-Type": "application/json" }, body: body === undefined ? undefined : JSON.stringify(body) });
|
|
return { status: response.status, body: JSON.parse(await response.text() || "null") };
|
|
};
|
|
const first = (await request("/topics", "POST", { name: "压力异常事件" })).body;
|
|
const second = (await request("/topics", "POST", { name: "阀门检修问题" })).body;
|
|
expect((await request("/topics", "POST", { name: " " })).status).toBe(400);
|
|
expect((await request("/topics", "GET", undefined, "other")).body.topics).toEqual([]);
|
|
expect((await request(`/topics/${first.id}`, "PATCH", { name: "越权" }, "other")).status).toBe(404);
|
|
expect((await request(`/session/a/topic`, "PATCH", { topic_id: first.id })).status).toBe(200);
|
|
expect((await request(`/session/b/topic`, "PATCH", { topic_id: second.id })).status).toBe(200);
|
|
expect((await request(`/session/a/topic`, "PATCH", { topic_id: first.id }, "other")).status).toBe(404);
|
|
expect((await request(`/session/a/topic`, "PATCH", { topic_id: first.id }, "owner", "10000000-0000-4000-8000-000000000002")).status).toBe(404);
|
|
const hits = (await request(`/sessions?q=${encodeURIComponent("南桥阀门")}&limit=1`)).body;
|
|
const next = (await request(`/sessions?q=${encodeURIComponent("南桥阀门")}&limit=10&cursor=${encodeURIComponent(hits.next_cursor)}`)).body;
|
|
expect(hits.sessions.length + next.sessions.length).toBe(3);
|
|
expect([...hits.sessions, ...next.sessions].every((item) => item.search_snippet.includes("南桥阀门"))).toBe(true);
|
|
expect((await request(`/sessions?topic_id=${first.id}&q=${encodeURIComponent("南桥阀门")}`)).body.sessions.map((item: { id: string }) => item.id)).toEqual(["a"]);
|
|
expect((await request(`/sessions?topic_id=${first.id}&q=${encodeURIComponent("南桥阀门")}&cursor=${encodeURIComponent(hits.next_cursor)}`)).status).toBe(400);
|
|
expect((await request(`/sessions?q=${encodeURIComponent("隐藏推理密码")}`)).body.sessions).toEqual([]);
|
|
expect((await request(`/sessions?q=${encodeURIComponent("工具秘密参数")}`)).body.sessions).toEqual([]);
|
|
await request("/session/collaboration/topic", "PATCH", { topic_id: first.id });
|
|
expect((await request(`/sessions?include_collaboration=true&topic_id=${first.id}&q=${encodeURIComponent("南桥阀门")}`)).body.sessions.map((item: { id: string }) => item.id).sort()).toEqual(["a", "collaboration"]);
|
|
expect((await request("/sessions?include_collaboration=true")).body.sessions.some((item: { id: string }) => item.id === "execution")).toBe(false);
|
|
await request(`/topics/${first.id}`, "PATCH", { name: "压力异常复盘" });
|
|
expect((await new SessionMetadataStore(join(root, "metadata"), workspaces).topics.list(context))[0]?.name).toBe("压力异常复盘");
|
|
const created = await request("/session", "POST", { topic_id: first.id });
|
|
expect(created.body.topic_id).toBe(first.id);
|
|
expect((await request(`/topics/${first.id}`, "DELETE")).status).toBe(204);
|
|
expect((await request("/session/a")).body.topic_id).toBe(null);
|
|
expect((await request("/session/a")).body.messages).toHaveLength(62);
|
|
expect((await request("/sessions?topic_id=unclassified")).body.sessions.map((item: { id: string }) => item.id).sort()).toEqual(["a", "new-topic-chat", "unclassified"]);
|
|
expect((await request(`/session/a/topic`, "PATCH", { topic_id: first.id })).status).toBe(404);
|
|
});
|