Move project and host queries into native MCP with host-owned OAuth/context, while retaining memory, skills, actions and publication plugins. Add nearby-model queries and explicit chat/task/learning model defaults. Adopt Responses HTTP/SSE and native steer/queue inbox delivery, preserving scoped session ownership, pending input edits and investigation communication. Disable local-provider discovery: Server /health allowed native vLLM to probe /v1/models on port 8000. A native before/after regression prevents recurrence. Validation: 170 focused Bun tests; service/plugin typechecks; native discovery, provider-discovery and input-inbox smoke checks; no paid model inference.
183 lines
6.3 KiB
TypeScript
183 lines
6.3 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it } from "bun:test";
|
|
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
|
|
import {
|
|
SessionContextMismatchError,
|
|
SessionMetadataStore,
|
|
} from "../../src/sessions/metadataStore.js";
|
|
|
|
describe("SessionMetadataStore", () => {
|
|
let tempDir: string;
|
|
let store: SessionMetadataStore;
|
|
|
|
beforeEach(async () => {
|
|
tempDir = await mkdtemp(join(tmpdir(), "tjwater-session-"));
|
|
store = new SessionMetadataStore(tempDir);
|
|
await store.initialize();
|
|
});
|
|
|
|
it("does not return or ensure a session owned by another context", async () => {
|
|
await store.ensure({
|
|
actorKey: "actor-1",
|
|
projectId: "project-1",
|
|
projectKey: "project-key-1",
|
|
sessionId: "shared-id",
|
|
userId: "user-1",
|
|
});
|
|
|
|
expect(
|
|
await store.get(
|
|
{
|
|
actorKey: "actor-2",
|
|
projectId: "project-2",
|
|
projectKey: "project-key-2",
|
|
userId: "user-2",
|
|
},
|
|
"shared-id",
|
|
),
|
|
).toBeNull();
|
|
expect(
|
|
store.ensure({
|
|
actorKey: "actor-2",
|
|
projectId: "project-2",
|
|
projectKey: "project-key-2",
|
|
sessionId: "shared-id",
|
|
userId: "user-2",
|
|
}),
|
|
).rejects.toBeInstanceOf(SessionContextMismatchError);
|
|
});
|
|
|
|
afterEach(async () => {
|
|
await rm(tempDir, { force: true, recursive: true });
|
|
});
|
|
|
|
it("persists the provided opencode session id", async () => {
|
|
const { record, created } = await store.ensure({
|
|
actorKey: "actor-1",
|
|
projectId: "project-1",
|
|
projectKey: "project-key-1",
|
|
sessionId: "opencode-session-1",
|
|
userId: "user-1",
|
|
});
|
|
|
|
expect(created).toBe(true);
|
|
expect(record.sessionId).toBe("opencode-session-1");
|
|
expect(record.ownerUserId).toBe("user-1");
|
|
expect(record.status).toBe("active");
|
|
});
|
|
|
|
it("touches metadata and preserves scoped ownership", async () => {
|
|
const { record } = await store.ensure({
|
|
actorKey: "actor-2",
|
|
projectId: "project-2",
|
|
projectKey: "project-key-2",
|
|
sessionId: "existing-session",
|
|
userId: "user-2",
|
|
});
|
|
|
|
const touched = await store.touch(record, {
|
|
title: "新标题",
|
|
});
|
|
|
|
expect(touched.title).toBe("新标题");
|
|
expect(touched.updatedAt >= record.updatedAt).toBe(true);
|
|
|
|
const fetched = await store.get(
|
|
{
|
|
actorKey: "actor-2",
|
|
projectId: "project-2",
|
|
projectKey: "project-key-2",
|
|
userId: "user-2",
|
|
},
|
|
"existing-session",
|
|
);
|
|
expect(fetched?.title).toBe("新标题");
|
|
});
|
|
|
|
it("finds a triggered session only inside the owning context", async () => {
|
|
const context = {
|
|
actorKey: "actor-trigger",
|
|
projectId: "project-trigger",
|
|
projectKey: "project-key-trigger",
|
|
userId: "user-trigger",
|
|
};
|
|
const { record } = await store.ensure({
|
|
...context,
|
|
sessionId: "triggered-session",
|
|
trigger: {
|
|
triggerId: "peripheral.work_order.created:project:order:1",
|
|
source: "peripheral_sample",
|
|
subjectType: "work_order",
|
|
subjectId: "50000000-0000-4000-8000-000000000001",
|
|
workflow: "water_quality_complaint",
|
|
occurredAt: "2026-09-22T10:00:00+08:00",
|
|
},
|
|
});
|
|
|
|
expect(
|
|
await store.findByTrigger(
|
|
context,
|
|
"peripheral.work_order.created:project:order:1",
|
|
),
|
|
).toEqual(record);
|
|
expect(
|
|
await store.findByTrigger(
|
|
{ ...context, actorKey: "other-actor", userId: "other-user" },
|
|
"peripheral.work_order.created:project:order:1",
|
|
),
|
|
).toBeNull();
|
|
});
|
|
|
|
it("keeps ordinary chat, task execution, and task collaboration distinct", async () => {
|
|
const context = {
|
|
actorKey: "actor-purpose",
|
|
projectId: "project-purpose",
|
|
projectKey: "project-key-purpose",
|
|
userId: "user-purpose",
|
|
};
|
|
await store.ensure({ ...context, sessionId: "ordinary" });
|
|
await store.ensure({
|
|
...context,
|
|
sessionId: "execution",
|
|
purpose: "task_execution",
|
|
taskId: "50000000-0000-4000-8000-000000000001",
|
|
requestId: "60000000-0000-4000-8000-000000000001",
|
|
});
|
|
await store.ensure({
|
|
...context,
|
|
sessionId: "collaboration",
|
|
purpose: "task_collaboration",
|
|
taskId: "50000000-0000-4000-8000-000000000001",
|
|
});
|
|
|
|
expect((await store.list(context, { purpose: "ordinary_chat" })).map((item) => item.sessionId)).toEqual(["ordinary"]);
|
|
expect((await store.list(context, { purpose: "task_execution" })).map((item) => item.sessionId)).toEqual(["execution"]);
|
|
expect((await store.findByTask(context, "50000000-0000-4000-8000-000000000001", "task_collaboration"))?.sessionId).toBe("collaboration");
|
|
expect((await store.findByRequest(context, "60000000-0000-4000-8000-000000000001"))?.sessionId).toBe("execution");
|
|
});
|
|
|
|
it("updates its scoped index after writes and deletion without rereading unrelated records", async () => {
|
|
const context = { actorKey: "actor-a", projectKey: "project-a", userId: "user-a" };
|
|
const { record } = await store.ensure({ ...context, sessionId: "first" });
|
|
expect((await store.list(context)).map((item) => item.sessionId)).toEqual(["first"]);
|
|
// A listing already built the index; subsequent calls must not rescan files.
|
|
await writeFile(join(tempDir, "unrelated.json"), "not valid JSON");
|
|
await store.touch(record, { title: "updated title" });
|
|
const listed = await store.list(context);
|
|
expect(listed[0]?.title).toBe("updated title");
|
|
listed[0]!.actorKey = "other-actor";
|
|
expect((await store.list(context))[0]?.actorKey).toBe("actor-a");
|
|
await store.ensure({ ...context, sessionId: "second" });
|
|
expect((await store.list(context)).map((item) => item.sessionId).sort()).toEqual(["first", "second"]);
|
|
await store.remove(record);
|
|
expect((await store.list(context)).map((item) => item.sessionId)).toEqual(["second"]);
|
|
expect(await store.get(context, "first")).toBeNull();
|
|
expect((await store.get(context, "first", { includeDeleted: true }))?.deletedAt).toBeDefined();
|
|
expect(await store.get({ ...context, userId: "user-b" }, "first", { includeDeleted: true })).toBeNull();
|
|
await expect(store.touch(record, { title: "revived" })).rejects.toThrow("session not found");
|
|
await expect(store.ensure({ ...context, sessionId: "first" })).rejects.toThrow("session not found");
|
|
});
|
|
});
|