a1b5587fcb
Reshape the ai_sessions table (migration 003) and mapping/routes to the full AISession contract: title, kind, notes, created_at, last_message_at; drop legacy role/model/settings. toAISession now maps the new columns and routes create/patch/messages handlers use the new shape.
105 lines
3.5 KiB
TypeScript
105 lines
3.5 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
|
import { AgentController, NoteBuilder } from "./index.js";
|
|
import type { ResolvedProvider } from "./index.js";
|
|
|
|
const config: ResolvedProvider = {
|
|
id: "test-openai",
|
|
kind: "openai-compatible",
|
|
baseUrl: "https://api.openai.com/v1",
|
|
apiKey: "sk-test",
|
|
model: "gpt-4o",
|
|
contextWindow: 128_000,
|
|
};
|
|
|
|
let savedFetch: typeof fetch;
|
|
|
|
/** Build an SSE Response streaming a single text delta. */
|
|
function sseResponse(events: string[]): Response {
|
|
const encoder = new TextEncoder();
|
|
const body = new ReadableStream<Uint8Array<ArrayBuffer>>({
|
|
start(controller) {
|
|
for (const event of events) controller.enqueue(encoder.encode(`data: ${event}\n\n`));
|
|
controller.enqueue(encoder.encode("data: [DONE]\n\n"));
|
|
controller.close();
|
|
},
|
|
});
|
|
return new Response(body, { status: 200, headers: { "content-type": "text/event-stream" } });
|
|
}
|
|
|
|
describe("NoteBuilder", () => {
|
|
let builder: NoteBuilder;
|
|
|
|
beforeEach(() => {
|
|
savedFetch = globalThis.fetch;
|
|
});
|
|
|
|
afterEach(() => {
|
|
globalThis.fetch = savedFetch;
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
it("streams a prompt and builds a draft note", async () => {
|
|
globalThis.fetch = (() =>
|
|
sseResponse([JSON.stringify({ choices: [{ delta: { content: "Aria is the protagonist. She is a young mage with a mysterious past. #protagonist #mage" } }] })])) as unknown as typeof fetch;
|
|
|
|
const agent = new AgentController(config);
|
|
builder = new NoteBuilder(agent);
|
|
|
|
const result = await builder.build(
|
|
{ kind: "character", prompt: "Summarize Aria", context: { title: "Aria" } },
|
|
"sess-1",
|
|
);
|
|
|
|
expect(result.id).toBeTruthy();
|
|
expect(result.sessionId).toBe("sess-1");
|
|
expect(result.kind).toBe("character");
|
|
expect(result.status).toBe("draft");
|
|
expect(result.body).toContain("Aria");
|
|
expect(result.tags).toContain("protagonist");
|
|
expect(result.tags).toContain("mage");
|
|
expect(result.createdAt).toBeTruthy();
|
|
expect(result.updatedAt).toBeTruthy();
|
|
});
|
|
|
|
it("extracts tags from a #tag list in the response", async () => {
|
|
globalThis.fetch = (() =>
|
|
sseResponse([JSON.stringify({ choices: [{ delta: { content: "The Sunspire Mountains. #location #magic #north" } }] })])) as unknown as typeof fetch;
|
|
|
|
const agent = new AgentController(config);
|
|
builder = new NoteBuilder(agent);
|
|
|
|
const result = await builder.build(
|
|
{ kind: "setting", prompt: "Describe the setting" },
|
|
"sess-1",
|
|
);
|
|
|
|
expect(result.kind).toBe("setting");
|
|
expect(result.tags.sort()).toEqual(["location", "magic", "north"]);
|
|
});
|
|
|
|
it("includes the note title in the system prompt", async () => {
|
|
const calls: Array<RequestInit> = [];
|
|
globalThis.fetch = ((_url: string, init: RequestInit) => {
|
|
calls.push(init);
|
|
return Promise.resolve(
|
|
sseResponse([JSON.stringify({ choices: [{ delta: { content: "ok" } }] })]),
|
|
);
|
|
}) as unknown as typeof fetch;
|
|
|
|
const agent = new AgentController(config);
|
|
builder = new NoteBuilder(agent);
|
|
|
|
await builder.build(
|
|
{ kind: "plot", prompt: "Summarize the plot thread", context: { title: "The Stolen Relic" } },
|
|
"sess-1",
|
|
);
|
|
|
|
const init = calls[0] as RequestInit;
|
|
const body = JSON.parse(init.body as string) as Record<string, unknown>;
|
|
const messages = body.messages as Array<{ content: string; role?: string }>;
|
|
const system = messages.find((m) => m.role === "system")?.content ?? "";
|
|
expect(system).toContain("The Stolen Relic");
|
|
});
|
|
});
|