CHUNK #9: AI session storage layer (AgentController, AIStore, builders)
- Add packages/ai: AgentController (streaming + budget), SuggestionBuilder, NoteBuilder, AIStore (Dexie/IndexedDB), SuggestionMutation - Fix createUlid to use monotonicFactory for reliable listSessions sort tiebreaker - Extend outline system prompt with setting + characters context - Add dexie dependency to packages/ai - 188 tests passing (core 47, ui 5, editor 30, ai 71, exporters 22, web 13)
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { AgentController, SuggestionBuilder } 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("SuggestionBuilder", () => {
|
||||
let builder: SuggestionBuilder;
|
||||
|
||||
beforeEach(() => {
|
||||
savedFetch = globalThis.fetch;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
globalThis.fetch = savedFetch;
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("streams a prompt and builds a pending suggestion", async () => {
|
||||
globalThis.fetch = (() =>
|
||||
sseResponse([JSON.stringify({ choices: [{ delta: { content: "Try a flashback here. It reveals the secret early." } }] })])) as unknown as typeof fetch;
|
||||
|
||||
const agent = new AgentController(config);
|
||||
builder = new SuggestionBuilder(agent);
|
||||
|
||||
const result = await builder.build(
|
||||
{ kind: "expand", prompt: "Expand the reveal scene" },
|
||||
"sess-1",
|
||||
"msg-1",
|
||||
);
|
||||
|
||||
expect(result.id).toBeTruthy();
|
||||
expect(result.sessionId).toBe("sess-1");
|
||||
expect(result.sourceMessageId).toBe("msg-1");
|
||||
expect(result.kind).toBe("expand");
|
||||
expect(result.status).toBe("pending");
|
||||
expect(result.text).toContain("flashback");
|
||||
expect(result.title.length).toBeGreaterThan(0);
|
||||
expect(result.summary.length).toBeGreaterThan(0);
|
||||
expect(result.createdAt).toBeTruthy();
|
||||
});
|
||||
|
||||
it("includes the context 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 SuggestionBuilder(agent);
|
||||
|
||||
await builder.build(
|
||||
{
|
||||
kind: "outline",
|
||||
prompt: "Outline chapter 3",
|
||||
context: { chapterTitle: "The Return", setting: "the capital", characters: ["Aria"] },
|
||||
},
|
||||
"sess-1",
|
||||
"msg-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 }>;
|
||||
const system = messages.find((m) => m.role === "system")?.content ?? "";
|
||||
expect(system).toContain("The Return");
|
||||
expect(system).toContain("the capital");
|
||||
expect(system).toContain("Aria");
|
||||
});
|
||||
|
||||
it("produces distinct titles per kind", async () => {
|
||||
globalThis.fetch = (() =>
|
||||
sseResponse([JSON.stringify({ choices: [{ delta: { content: "Chapter 3: The Return — Aria confronts the council about the missing supplies." } }] })])) as unknown as typeof fetch;
|
||||
|
||||
const agent = new AgentController(config);
|
||||
builder = new SuggestionBuilder(agent);
|
||||
|
||||
const result = await builder.build({ kind: "outline", prompt: "Outline" }, "sess-1", "msg-1");
|
||||
expect(result.title).toContain("Chapter 3");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user