/** * TASK-0123 — MockProvider deterministic writing mode. * * The app layer sends `[SELECTED ]…[/SELECTED ]` markers + * `metadata: { mode: "writing", action, language? }`; the mock parses the * markers and answers with ONE `generateBlocks` tool call of deterministic * per-block transforms. These tests pin that contract (the E2E/integration * layers depend on it being byte-deterministic). */ import { describe, expect, it } from "vitest"; import { MockProvider, parseWritingBlocks, writingTransform } from "./index.js"; import type { ChatRequest } from "../types.js"; function writingReq(opts: { action: string; blocks: Array<{ id: string; text: string }>; language?: string } & Record): ChatRequest { const userText = "Do the writing task.\n" + opts.blocks .map((b) => `[SELECTED ${b.id}]\n${b.text}\n[/SELECTED ${b.id}]`) .join("\n"); const metadata: Record = { mode: "writing", action: opts.action }; if (opts.language) metadata.language = opts.language; return { model: "mock", messages: [ { role: "system", content: "You generate block edits." }, { role: "user", content: userText }, ], tools: [ { name: "generateBlocks", description: "Emit the block edits.", parameters: { type: "object", properties: { blocks: { type: "array", items: { type: "object", properties: { blockId: { type: "string" }, newText: { type: "string" } }, required: ["blockId", "newText"], }, }, }, required: ["blocks"], }, }, ], toolChoice: { name: "generateBlocks" }, metadata: { ...metadata, ...(opts as Record) } as unknown as Record, }; } async function collect(req: ChatRequest): Promise[]> { const p = new MockProvider("openai", 8); const out: Record[] = []; for await (const chunk of p.chat(req as never)) out.push(chunk as Record); return out; } describe("MockProvider writing mode (TASK-0123)", () => { it("summarize: one tool call, per-block 'Summarized:' of the first 80 chars", async () => { const longText = "x".repeat(120); const chunks = await collect( writingReq({ action: "summarize", blocks: [{ id: "b1", text: longText }, { id: "b2", text: "short" }] }), ); const call = chunks.find((c) => c.toolCallDelta)!.toolCallDelta! as { id: string; name: string; arguments: { blocks: Array<{ blockId: string; newText: string }> }; }; expect(call.name).toBe("generateBlocks"); expect(call.arguments.blocks).toEqual([ { blockId: "b1", newText: `Summarized: ${"x".repeat(80)}` }, { blockId: "b2", newText: "Summarized: short" }, ]); const done = chunks.at(-1) as { usage: { in: number; out: number }; finishReason: string; done: boolean }; expect(done.done).toBe(true); expect(done.finishReason).toBe("tool_calls"); expect(done.usage.in).toBeGreaterThan(0); expect(done.usage.out).toBeGreaterThan(0); }); it("rewrite / improve / translate transforms are deterministic", async () => { const rewrite = await collect(writingReq({ action: "rewrite", blocks: [{ id: "b1", text: "hello world" }] })); expect( (rewrite.find((c) => c.toolCallDelta)!.toolCallDelta! as { arguments: { blocks: Array<{ blockId: string; newText: string }> } }).arguments.blocks, ).toEqual([{ blockId: "b1", newText: "Rewritten: hello world" }]); const improve = await collect(writingReq({ action: "improve", blocks: [{ id: "b1", text: "meh sentence" }] })); expect( (improve.find((c) => c.toolCallDelta)!.toolCallDelta! as { arguments: { blocks: Array<{ blockId: string; newText: string }> } }).arguments.blocks, ).toEqual([{ blockId: "b1", newText: "Improved: meh sentence" }]); const translate = await collect(writingReq({ action: "translate", blocks: [{ id: "b1", text: "bonjour" }], language: "fr" })); expect( (translate.find((c) => c.toolCallDelta)!.toolCallDelta! as { arguments: { blocks: Array<{ blockId: string; newText: string }> } }).arguments.blocks, ).toEqual([{ blockId: "b1", newText: "Translated to fr: bonjour" }]); }); it("preserves prompt order across multiple selected blocks (incl. interleaved context markers)", async () => { const req = writingReq({ action: "rewrite", blocks: [{ id: "b3", text: "c" }, { id: "b1", text: "a" }, { id: "b2", text: "b" }] }); // Simulate the real prompt shape: context blocks interleaved. const lastUser = req.messages[req.messages.length - 1]!; lastUser.content = `[CONTEXT x0]\nctx\n[/CONTEXT x0]\n[SELECTED b3]\nc\n[/SELECTED b3]\n[CONTEXT x1]\nctx2\n[/CONTEXT x1]\n[SELECTED b1]\na\n[/SELECTED b1]\n[SELECTED b2]\nb\n[/SELECTED b2]`; const chunks = await collect(req); const call = chunks.find((c) => c.toolCallDelta)!.toolCallDelta! as { arguments: { blocks: Array<{ blockId: string }> } }; expect(call.arguments.blocks.map((b) => b.blockId)).toEqual(["b3", "b1", "b2"]); }); it("metadata.forceUnknownId appends an unknown blockId (validation seam)", async () => { const chunks = await collect( writingReq({ action: "rewrite", blocks: [{ id: "b1", text: "t" }], forceUnknownId: "not_in_prompt" }), ); const call = chunks.find((c) => c.toolCallDelta)!.toolCallDelta! as { arguments: { blocks: Array<{ blockId: string }> } }; expect(call.arguments.blocks.map((b) => b.blockId)).toEqual(["b1", "not_in_prompt"]); }); it("armed failAfterDeltas dies mid-stream (no tool call, no done frame)", async () => { const p = new MockProvider("openai", 8); p.failAfterDeltas = 2; p.failStatus = 503; const chunks: unknown[] = []; let threw = false; try { for await (const c of p.chat(writingReq({ action: "rewrite", blocks: [{ id: "b1", text: "t" }] }) as never)) chunks.push(c); } catch { threw = true; } expect(threw).toBe(true); expect(chunks.some((c) => (c as { toolCallDelta?: unknown }).toolCallDelta)).toBe(false); expect(chunks.some((c) => (c as { done?: boolean }).done === true)).toBe(false); // Single-shot: the next writing call succeeds. const ok = await collect( writingReq({ action: "rewrite", blocks: [{ id: "b1", text: "again" }] }), ); expect(ok.some((c) => (c as { toolCallDelta?: unknown }).toolCallDelta)).toBe(true); }); it("deltaDelayMs paces the call (deterministic latency floor)", async () => { const p = new MockProvider("openai", 8); p.deltaDelayMs = 40; const started = Date.now(); for await (const c of p.chat(writingReq({ action: "rewrite", blocks: [{ id: "b1", text: "t" }] }) as never)) { if ((c as { toolCallDelta?: unknown }).toolCallDelta) break; } expect(Date.now() - started).toBeGreaterThanOrEqual(30); }); }); describe("parseWritingBlocks / writingTransform (pure helpers)", () => { it("parses only SELECTED markers, in order, ignoring CONTEXT", () => { const msgs = [ { role: "user", content: "[CONTEXT a]\nctx\n[/CONTEXT a]\n[SELECTED s1]\ntext one\n[/SELECTED s1]\n[SELECTED s2]\ntwo\n[/SELECTED s2]", }, ]; expect(parseWritingBlocks(msgs)).toEqual([ { id: "s1", text: "text one" }, { id: "s2", text: "two" }, ]); }); it("returns [] with no user message / no markers", () => { expect(parseWritingBlocks([])).toEqual([]); expect(parseWritingBlocks([{ role: "system", content: "[SELECTED x]\nno\n[/SELECTED x]" }])).toEqual([]); expect(parseWritingBlocks([{ role: "user", content: "no markers here" }])).toEqual([]); }); it("transforms are stable per action", () => { expect(writingTransform("summarize", "a".repeat(100))).toBe(`Summarized: ${"a".repeat(80)}`); expect(writingTransform("improve", " x ")).toBe("Improved: x"); expect(writingTransform("translate", "y")).toBe("Translated to the target language: y"); expect(writingTransform("bogus", "z")).toBe("Rewritten: z"); }); });