Something went wrong. Try again.
This repository has no description
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202import test from "node:test";import assert from "node:assert/strict";import { randomUUID } from "node:crypto";import { createServer } from "node:http";
// Explicitly target an isolated API. This test never contacts a model provider.const base = process.env.ODRE_TEST_API;if (base) { const url = new URL(base); if (url.protocol !== "http:" || !["127.0.0.1", "localhost", "[::1]"].includes(url.hostname)) { throw new Error("ODRE_TEST_API must identify an isolated loopback HTTP API."); }}async function json(path, { method = "GET", body, status = 200 } = {}) { const response = await fetch(base + path, { method, headers: body === undefined ? {} : { "Content-Type": "application/json" }, body: body === undefined ? undefined : JSON.stringify(body), }); const text = await response.text(); assert.equal(response.status, status, `${method} ${path}: ${text.slice(0, 600)}`); return text ? JSON.parse(text) : null;}async function stream(path, body, { signal, onEvent } = {}) { const response = await fetch(base + path, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), signal }); assert.equal(response.status, 200, `${path}: ${response.status === 200 ? "" : await response.text()}`); assert.match(response.headers.get("content-type"), /text\/event-stream/); const events = [], decoder = new TextDecoder(); let buffer = ""; for await (const chunk of response.body) { buffer += decoder.decode(chunk, { stream: true }); let boundary; while ((boundary = buffer.indexOf("\n\n")) >= 0) { const event = buffer.slice(0, boundary); buffer = buffer.slice(boundary + 2); const data = event.split("\n").filter((line) => line.startsWith("data:")).map((line) => line.slice(5).trimStart()).join("\n"); if (data) { const parsed = JSON.parse(data); events.push(parsed); onEvent?.(parsed); } } } return events;}function done(events) { const event = events.findLast((e) => e.type === "done"); assert.ok(event, "Stream ends with persisted completion status"); return event; }async function waitFor(callback, timeout = 5000) { const deadline = Date.now() + timeout; while (Date.now() < deadline) { const result = await callback(); if (result) return result; await new Promise((resolve) => setTimeout(resolve, 30)); } assert.fail("Expected server state did not appear before the deadline");}
// The only network upstream is an ephemeral loopback server with clearly labeled test output.function mockProvider() { const requests = []; const server = createServer(async (req, res) => { if (req.method === "GET" && req.url === "/v1/models") { res.writeHead(200, { "Content-Type": "application/json" }); res.end(JSON.stringify({ data: [{ id: "model-selected" }] })); return; } if (req.method !== "POST" || req.url !== "/v1/chat/completions") { res.writeHead(404); res.end(); return; } let raw = ""; for await (const part of req) raw += part; const input = JSON.parse(raw); requests.push(input); if (input.model === "model-error") { res.writeHead(429, { "Content-Type": "application/json" }); res.end(JSON.stringify({ error: { message: "Local QA rejection qa-local-key-must-not-leak" } })); return; } res.writeHead(200, { "Content-Type": "text/event-stream", "Cache-Control": "no-cache" }); const send = (value) => res.write(`data: ${typeof value === "string" ? value : JSON.stringify(value)}\n\n`); const delta = (text) => send({ choices: [{ delta: { content: text } }] }); if (input.model === "model-slow") { delta("Preserved partial test response. "); let ticks = 0; const timer = setInterval(() => { delta(`continued ${ticks++}. `); if (ticks > 40) { clearInterval(timer); send("[DONE]"); res.end(); } }, 100); res.on("close", () => clearInterval(timer)); return; } if (input.model === "model-incomplete") { delta("Incomplete test candidate."); send({ choices: [{ delta: {}, finish_reason: "length" }] }); send("[DONE]"); res.end(); return; } const text = input.messages?.at(-1)?.content; const replacement = typeof text === "string" && text.includes("Replace only this Markdown passage"); const output = replacement ? "Improved explanation." : "# Test lesson\n\nGenerated explanation. [s1]\n\n## Check understanding\nExplain the state transition.\n"; delta(output.slice(0, 22)); delta(output.slice(22)); send("[DONE]"); res.end(); }); return { server, requests };}
test("one Odre API persists real agent streams and versioned content proposals", { skip: !base, timeout: 60000 }, async (t) => { const { server, requests } = mockProvider(); await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); const upstream = `http://127.0.0.1:${server.address().port}/v1`; const label = `QA Agent ${randomUUID().slice(0, 8)}`, materials = new Set(), conversations = new Set(); let provider, space, generated, nativeLesson; t.after(async () => { for (const id of conversations) await fetch(`${base}/api/conversations/${id}`, { method: "DELETE" }); if (space) await fetch(`${base}/api/learning/${space.id}`, { method: "DELETE" }); for (const id of materials) await fetch(`${base}/api/reader/materials/${id}`, { method: "DELETE" }); if (provider) await fetch(`${base}/api/reader/providers/${provider.id}`, { method: "DELETE" }); server.closeAllConnections(); await new Promise((resolve) => server.close(resolve)); }); provider = await json("/api/reader/providers", { method: "POST", status: 201, body: { name: `${label} local provider`, baseUrl: upstream, apiKey: "qa-local-key-must-not-leak", models: ["model-default", "model-selected", "model-slow", "model-error", "model-incomplete"], options: { protocol: "chat", contextTokens: 6000, maxTokens: 4096, vision: true }, } }); assert.equal(provider.hasKey, true); assert.ok(!JSON.stringify(provider).includes("qa-local-key")); const nativeProvider = (await json("/api/settings/providers")).find((p) => p.provider === provider.id); assert.equal(nativeProvider.configured, true); assert.equal(nativeProvider.name, provider.name); assert.equal((await json(`/api/reader/providers/${provider.id}/test`, { method: "POST", body: {} })).models[0], "model-selected"); space = await json("/api/learning", { method: "POST", body: { kind: "session", title: `${label} Learning` } }); const source = { title: "QA source", url: "https://example.org/qa-agent", excerpt: "This retained source excerpt explains the state transition." }; const generateRequest = { title: `${label} Generated`, instruction: "Explain the state transition clearly.", providerId: provider.id, model: "model-selected", reasoningEffort: "high", spaceId: space.id, sources: [source] };
await t.test("selected model and effort reach upstream; candidate applies to one shared Note and Learning", async () => { const result = done(await stream("/api/reader/generate", generateRequest)); assert.equal(result.status, "complete"); assert.equal(result.proposal.status, "complete"); assert.equal(result.proposal.material_id, null); assert.equal(requests.at(-1).model, "model-selected"); assert.equal(requests.at(-1).reasoning_effort, "high"); assert.ok(JSON.stringify(requests.at(-1)).includes(source.excerpt)); assert.equal((await json("/api/reader/materials")).some((m) => m.title === generateRequest.title), false); assert.ok((await json("/api/reader/proposals")).some((p) => p.id === result.id)); generated = await json(`/api/reader/proposals/${result.id}/apply`, { method: "POST", body: {} }); materials.add(generated.id); assert.deepEqual(generated.sources, [source]); assert.equal(generated.body, result.proposal.replacement); const catalog = (await json("/api/shelf")).items.filter((i) => i.metadata.reader_material_id === generated.id); assert.equal(catalog.length, 0); const native = await json(`/api/notes/${generated.note_id}`); assert.equal(native.reader_material_id, generated.id); assert.equal(native.title, generated.title); assert.equal(native.version, generated.version); const lessons = await json(`/api/learning/${space.id}/lessons`); assert.equal(lessons.filter((l) => l.document.reader_material_id === generated.id).length, 1); const again = await json(`/api/reader/proposals/${result.id}/apply`, { method: "POST", body: {} }); assert.equal(again.id, generated.id); assert.equal(again.version, generated.version); });
await t.test("selected passage edits preserve surrounding text, sources, revisions and stale proposals", async () => { const original = generated.body; const request = { instruction: "Improve this explanation.", selection: { quote: "Generated explanation.", prefix: "", suffix: "" }, expected_version: generated.version, providerId: provider.id, model: "model-selected" }; const proposal = done(await stream(`/api/reader/materials/${generated.id}/rewrite`, request)).proposal; assert.equal((await json(`/api/reader/materials/${generated.id}`)).body, original); generated = await json(`/api/reader/proposals/${proposal.id}/apply`, { method: "POST", body: { expected_version: proposal.base_version } }); assert.equal(generated.body, original.replace("Generated explanation.", "Improved explanation.")); assert.deepEqual(generated.sources, [source]); let revisions = await json(`/api/reader/materials/${generated.id}/revisions`); const initial = revisions.find((r) => r.body === original); assert.ok(initial); generated = await json(`/api/reader/materials/${generated.id}/revisions/${initial.id}/restore`, { method: "POST", body: { expected_version: generated.version } }); assert.equal(generated.body, original); request.expected_version = generated.version; const stale = done(await stream(`/api/reader/materials/${generated.id}/rewrite`, request)).proposal; generated = await json(`/api/reader/materials/${generated.id}`, { method: "PATCH", body: { expected_version: generated.version, content: original + "\nOwner edit." } }); await json(`/api/reader/proposals/${stale.id}/apply`, { method: "POST", body: { expected_version: stale.base_version }, status: 409 }); assert.equal((await json(`/api/reader/materials/${generated.id}`)).body, generated.body); assert.equal((await json(`/api/reader/proposals/${stale.id}`)).status, "complete"); const duplicate = await json("/api/reader/materials", { method: "POST", status: 201, body: { title: `${label} Duplicate`, content: "Repeated text.\n\nRepeated **text**." } }); materials.add(duplicate.id); const count = requests.length; await json(`/api/reader/materials/${duplicate.id}/rewrite`, { method: "POST", status: 400, body: { ...request, expected_version: duplicate.version, selection: { quote: "Repeated text." } } }); assert.equal(requests.length, count, "Ambiguous selections are rejected before a provider call"); });
await t.test("reader conversation uses server sources, comparison context, vision and retry while sharing native conversations", async () => { const other = await json("/api/reader/materials", { method: "POST", status: 201, body: { title: `${label} Other`, content: "A different material describes physical frames.", sources: [{ title: "Comparison source", url: "https://example.org/comparison", excerpt: "Another retained source excerpt." }] } }); materials.add(other.id); const chat = await json("/api/reader/chats", { method: "POST", status: 201, body: { materialId: generated.id } }); conversations.add(chat.id); const reply = done(await stream(`/api/reader/chats/${chat.id}/send`, { content: "Compare the mechanisms.", providerId: provider.id, model: "model-selected", comparison: { materialId: other.id }, image: "data:image/png;base64,YQ==" })); assert.equal(reply.status, "complete"); const upstream = requests.at(-1); assert.ok(upstream.messages[0].content.includes("MATERIAL B")); assert.ok(upstream.messages[0].content.includes(source.excerpt)); assert.equal(upstream.messages.at(-1).content[1].type, "image_url"); const messages = await json(`/api/reader/chats/${chat.id}/messages`); assert.deepEqual(messages.map((m) => m.role), ["user", "assistant"]); assert.ok(messages[1].citations.some((c) => c.comparison && c.materialId === other.id)); assert.equal(messages[1].context.mode, "Two materials + local retrieval"); const shared = await json(`/api/conversations/${chat.id}`); assert.equal(shared.context_type, "reader"); assert.equal(shared.context_id, generated.id); assert.equal(shared.messages[1].id, messages[1].id); const failed = done(await stream(`/api/reader/chats/${chat.id}/send`, { content: "Retry this question.", providerId: provider.id, model: "model-error" })); assert.equal(failed.status, "error"); const prior = await json(`/api/reader/chats/${chat.id}/messages`); assert.equal(prior.at(-1).status, "error"); const user = prior.at(-2); const retried = done(await stream(`/api/reader/chats/${chat.id}/send`, { content: user.content, retryMessageId: user.id, providerId: provider.id, model: "model-selected" })); assert.equal(retried.status, "complete"); const current = await json(`/api/reader/chats/${chat.id}/messages`); assert.equal(current.filter((m) => m.content === user.content && m.role === "user").length, 1); assert.equal(current.at(-1).status, "complete"); });
await t.test("native stream honors selected model, rejects concurrent turns and persists partial cancellation", async () => { const conversation = await json("/api/conversations", { method: "POST", body: { title: `${label} Native chat`, provider: provider.id, model: "model-default", context_type: "lesson" } }); conversations.add(conversation.id); let ready; const seen = new Promise((resolve) => { ready = resolve; }); const controller = new AbortController(); const pending = stream(`/api/conversations/${conversation.id}/stream`, { content: "Explain in detail.", context: "Native current context", provider: provider.id, model: "model-slow", reasoningEffort: "high" }, { signal: controller.signal, onEvent: (event) => { if (event.type === "delta") ready(); } }); await seen; assert.equal(requests.at(-1).model, "model-slow"); assert.equal(requests.at(-1).reasoning_effort, "high"); await json(`/api/conversations/${conversation.id}/stream`, { method: "POST", status: 409, body: { content: "Concurrent question", model: "model-selected" } }); controller.abort(); await assert.rejects(pending, (error) => error.name === "AbortError"); const saved = await waitFor(async () => { const c = await json(`/api/conversations/${conversation.id}`); return c.messages.at(-1)?.metadata.status === "cancelled" && c; }); assert.deepEqual(saved.messages.map((m) => m.role), ["user", "assistant"]); assert.match(saved.messages.at(-1).content, /Preserved partial/); assert.equal(saved.model, "model-slow"); const normal = await json(`/api/conversations/${conversation.id}/messages`, { method: "POST", body: { content: "Use the saved model and finish.", context: "Native context" } }); assert.equal(normal.messages.at(-1).metadata.status, "complete"); assert.equal(requests.at(-1).model, "model-slow"); });
await t.test("failed and truncated generation preserve proposals and cannot modify canonical content", async () => { for (const model of ["model-error", "model-incomplete"]) { const events = await stream("/api/reader/generate", { ...generateRequest, title: `${label} ${model}`, model }); const result = done(events); assert.equal(result.status, "error"); assert.equal(result.proposal.status, "error"); assert.ok(!JSON.stringify(events).includes("qa-local-key-must-not-leak"), "Provider errors redact the saved key"); if (model === "model-incomplete") assert.equal(result.proposal.replacement, "Incomplete test candidate."); await json(`/api/reader/proposals/${result.id}/apply`, { method: "POST", body: {}, status: 409 }); assert.equal((await json("/api/reader/materials")).some((m) => m.title === `${label} ${model}`), false); } });
await t.test("explicit native lesson generation preserves original blocks and versioned restore", async () => { const document = { schema_version: 1, content: { type: "doc", content: [{ type: "paragraph", content: [{ type: "text", text: "Keep this original native explanation." }] }] } }; const shelf = await json("/api/shelf/items", { method: "POST", body: { kind: "link", title: `${label} Curated reference`, url: source.url, metadata: { reader_material_id: generated.id }, tags: [] } }); nativeLesson = await json(`/api/learning/${space.id}/lessons`, { method: "POST", body: { title: `${label} Native lesson`, document, resource_ids: [shelf.id] } }); const candidate = done(await stream("/api/reader/generate", { ...generateRequest, title: nativeLesson.title, lessonId: nativeLesson.id })); assert.ok(JSON.stringify(requests.at(-1)).includes("Keep this original native explanation.")); assert.ok(JSON.stringify(requests.at(-1)).includes(source.excerpt)); const material = await json(`/api/reader/proposals/${candidate.id}/apply`, { method: "POST", body: {} }); materials.add(material.id); assert.equal(material.sources.length, 2); assert.deepEqual(material.sources[1], source); assert.ok(JSON.stringify(requests.at(-1)).includes("[s2]")); const linked = (await json(`/api/learning/${space.id}/lessons`)).find((l) => l.id === nativeLesson.id); assert.equal(linked.version, nativeLesson.version + 1); assert.equal(linked.document.reader_material_id, material.id); assert.deepEqual(linked.document.content, document.content); assert.deepEqual(linked.resource_ids, [shelf.id]); const revisions = await json(`/api/reader/lessons/${nativeLesson.id}/revisions`); assert.deepEqual(revisions[0].document, document); await json(`/api/reader/materials/${material.id}`, { method: "DELETE", status: 409 }); const preserved = (await json(`/api/learning/${space.id}/lessons`)).find((l) => l.id === nativeLesson.id); assert.deepEqual(preserved, linked, "Deleting a linked reader material preserves the native lesson, its original blocks and resource references"); assert.deepEqual(await json(`/api/reader/lessons/${nativeLesson.id}/revisions`), revisions, "Rejected deletion preserves native lesson history"); assert.equal((await json(`/api/reader/materials/${material.id}`)).body, material.body); assert.equal((await json(`/api/notes/${material.note_id}`)).reader_material_id, material.id); const restored = await json(`/api/reader/lessons/${nativeLesson.id}/revisions/${revisions[0].id}/restore`, { method: "POST", body: { expected_version: linked.version } }); assert.deepEqual(restored.document, document); assert.equal(restored.version, linked.version + 1); });});