server: reconcile ai_sessions persistence to Chunk 9 AISession shape
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.
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { AgentController, isRateLimitError } from "./index.js";
|
||||
import type { ResolvedProvider } from "./index.js";
|
||||
import type { CompletionUsage, ResolvedProvider } from "./index.js";
|
||||
|
||||
/** Build a `text/event-stream` Response from a list of SSE event payloads. */
|
||||
function sseResponse(events: string[], status = 200): Response {
|
||||
@@ -78,7 +78,7 @@ describe("AgentController.stream", () => {
|
||||
])) as unknown as typeof fetch;
|
||||
|
||||
const agent = new AgentController(config);
|
||||
let seen: (typeof config) | null = null;
|
||||
let seen: CompletionUsage | null = null;
|
||||
for await (const chunk of agent.stream([{ role: "user", content: "hi" }])) {
|
||||
if (chunk.type === "done" && chunk.usage) {
|
||||
seen = chunk.usage;
|
||||
@@ -132,7 +132,7 @@ describe("AgentController.collect", () => {
|
||||
describe("thinking-param mapping", () => {
|
||||
it("maps thinking config onto the request body", async () => {
|
||||
const calls: Array<RequestInit> = [];
|
||||
globalThis.fetch = ((url: string, init: RequestInit) => {
|
||||
globalThis.fetch = ((_url: string, init: RequestInit) => {
|
||||
calls.push(init);
|
||||
return Promise.resolve(
|
||||
sseResponse([JSON.stringify({ choices: [{ delta: { content: "ok" } }] })]),
|
||||
|
||||
@@ -143,11 +143,15 @@ export class AgentController {
|
||||
extraBody: { ...opts.extraBody, ...thinking },
|
||||
})) {
|
||||
if (chunk.type === "text") {
|
||||
cb.onText?.(chunk.text);
|
||||
buffered.push(chunk);
|
||||
if (chunk.text !== undefined) {
|
||||
cb.onText?.(chunk.text);
|
||||
buffered.push(chunk);
|
||||
}
|
||||
} else if (chunk.type === "reasoning") {
|
||||
cb.onReasoning?.(chunk.reasoning);
|
||||
buffered.push(chunk);
|
||||
if (chunk.reasoning !== undefined) {
|
||||
cb.onReasoning?.(chunk.reasoning);
|
||||
buffered.push(chunk);
|
||||
}
|
||||
} else if (chunk.type === "done") {
|
||||
if (chunk.usage) {
|
||||
this.usage = chunk.usage;
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
|
||||
import { AIStore } from "./index.js";
|
||||
import type { SessionRow } from "./index.js";
|
||||
import type { AISessionPersistence } from "./index.js";
|
||||
|
||||
/** A simple in-memory persistence backend for tests. */
|
||||
function memoryPersistence(): AISessionPersistence {
|
||||
const rows = new Map<string, unknown>();
|
||||
const rows = new Map<string, SessionRow>();
|
||||
return {
|
||||
list: async (projectId) =>
|
||||
Array.from(rows.values())
|
||||
.filter((r) => r.projectId === projectId)
|
||||
.map((r) => ({ ...r })),
|
||||
get: async (id) => (rows.has(id) ? { ...rows.get(id) } : null),
|
||||
get: async (id) => (rows.has(id) ? ({ ...rows.get(id) } as SessionRow) : null),
|
||||
save: async (session) => {
|
||||
rows.set(session.id, { ...session });
|
||||
},
|
||||
@@ -62,10 +63,8 @@ describe("AIStore", () => {
|
||||
it("adds a message to a session", async () => {
|
||||
const session = await store.createSession();
|
||||
const updated = await store.addMessage(session.id, {
|
||||
id: "m1",
|
||||
role: "user",
|
||||
content: "hello",
|
||||
createdAt: "2026-01-01T00:00:00.000Z",
|
||||
});
|
||||
expect(updated.messages).toHaveLength(1);
|
||||
expect(updated.messages[0]?.role).toBe("user");
|
||||
@@ -74,14 +73,12 @@ describe("AIStore", () => {
|
||||
it("adds a suggestion and lists it", async () => {
|
||||
const session = await store.createSession();
|
||||
const updated = await store.addSuggestion(session.id, {
|
||||
id: "s1",
|
||||
sessionId: session.id,
|
||||
kind: "brainstorm",
|
||||
title: "Ideas",
|
||||
text: "some ideas",
|
||||
summary: "summary",
|
||||
status: "pending",
|
||||
createdAt: "2026-01-01T00:00:00.000Z",
|
||||
});
|
||||
expect(updated.suggestions).toHaveLength(1);
|
||||
const list = await store.listSuggestions(session.id);
|
||||
|
||||
@@ -24,7 +24,7 @@ import type {
|
||||
} from "@pubscript/core";
|
||||
|
||||
/** A persisted session row. */
|
||||
interface SessionRow {
|
||||
export interface SessionRow {
|
||||
id: string;
|
||||
projectId: string;
|
||||
title: string;
|
||||
@@ -76,7 +76,8 @@ export function createIndexedDBPersistence(dbName = "pubscript"): AISessionPersi
|
||||
return table.where("projectId").equals(projectId).sortBy("updatedAt").then((rows) => rows.reverse());
|
||||
},
|
||||
async get(id: string) {
|
||||
return table.get(id) ?? null;
|
||||
const row = await table.get(id);
|
||||
return row ?? null;
|
||||
},
|
||||
async save(row: SessionRow) {
|
||||
await table.put(row);
|
||||
|
||||
@@ -97,7 +97,7 @@ describe("NoteBuilder", () => {
|
||||
|
||||
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 messages = body.messages as Array<{ content: string; role?: string }>;
|
||||
const system = messages.find((m) => m.role === "system")?.content ?? "";
|
||||
expect(system).toContain("The Stolen Relic");
|
||||
});
|
||||
|
||||
@@ -135,7 +135,7 @@ export class NoteBuilder {
|
||||
|
||||
// Extract tags from #word patterns and a leading "tags:" line.
|
||||
const tagMatches = [...cleaned.matchAll(/#([\p{L}[\]_][\p{L}[\]0-9 ]{1,20})/gu)];
|
||||
const tags = [...new Set(tagMatches.map((m) => m[1].trim().toLowerCase()))].slice(0, 8);
|
||||
const tags = [...new Set(tagMatches.map((m) => m[1]!.trim().toLowerCase()))].slice(0, 8);
|
||||
|
||||
// Drop the title line and any tags line from the body.
|
||||
const body = lines
|
||||
|
||||
@@ -78,8 +78,6 @@ describe("SuggestionMutation", () => {
|
||||
const mut = new SuggestionMutation(suggestion({
|
||||
originalText: "The cat sat",
|
||||
newText: "The dog barked",
|
||||
anchorIndex: undefined,
|
||||
spanLength: undefined,
|
||||
}));
|
||||
const result = mut.apply(doc);
|
||||
expect(result.success).toBe(true);
|
||||
@@ -91,8 +89,6 @@ describe("SuggestionMutation", () => {
|
||||
const mut = new SuggestionMutation(suggestion({
|
||||
originalText: "cat",
|
||||
newText: "dog",
|
||||
anchorIndex: undefined,
|
||||
spanLength: 3,
|
||||
}));
|
||||
const result = mut.apply(doc);
|
||||
expect(result.success).toBe(true);
|
||||
|
||||
@@ -87,7 +87,7 @@ describe("SuggestionBuilder", () => {
|
||||
|
||||
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 messages = body.messages as Array<{ content: string; role?: string }>;
|
||||
const system = messages.find((m) => m.role === "system")?.content ?? "";
|
||||
expect(system).toContain("The Return");
|
||||
expect(system).toContain("the capital");
|
||||
|
||||
@@ -164,7 +164,7 @@ export class SuggestionBuilder {
|
||||
const cleaned = text.trim();
|
||||
const firstLine = cleaned.split(/\r?\n/).find((l) => l.trim().length > 0) ?? "";
|
||||
const title = firstLine.replace(/^#+\s*/, "").trim().slice(0, 120) || `${kind} suggestion`;
|
||||
const summary = cleaned.split(/\r?\n\r?\n/)[0].replace(/^#+\s*/, "").trim().slice(0, 300);
|
||||
const summary = cleaned.split(/\r?\n\r?\n/)[0]!.replace(/^#+\s*/, "").trim().slice(0, 300);
|
||||
return { title, summary, text: cleaned };
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user