// Shared fixtures: resolved role instances from the business package's own // test business, scratch directories, a fake tool socket and a scripted // stand-in for the Anthropic Messages API (row S0's probe mock, reduced). // Nothing here reads the real configuration or calls a real model. import { createServer as createHttp } from "node:http"; import { createServer } from "node:net"; import { mkdtempSync, realpathSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { resolveInstance, validateBusinessDocument } from "../../business/src/index.mjs"; import { businessDoc, REPO_ROLES, systemFor } from "../../business/tests/helpers.mjs"; export { REPO } from "../../business/tests/helpers.mjs"; export function scratch(t, prefix = "mosaic-harness-") { const dir = realpathSync(mkdtempSync(join(tmpdir(), prefix))); t.after(() => rmSync(dir, { recursive: true, force: true })); return dir; } // resolved("pm" | "coder" | ..., { mutate }) in a scratch root. export function resolvedFor(t, instance, mutate) { const root = scratch(t, "mosaic-harness-biz-"); const doc = businessDoc(root); mutate?.(doc); const business = validateBusinessDocument(doc, join(root, "businesses", "acme.json"), { rolesDir: REPO_ROLES }); return resolveInstance({ system: systemFor(root), business, instance }); } // A tool socket that answers with `handler(tool, args)`; a throw becomes a refusal. export async function fakeToolSocket(t, dir, handler) { const path = join(dir, "tools.sock"); const calls = []; const server = createServer((s) => { let input = ""; s.on("data", async (b) => { input += b; if (!input.includes("\n")) return; const { tool, args } = JSON.parse(input); calls.push({ tool, args }); try { s.end(JSON.stringify({ ok: true, result: await handler(tool, args) }) + "\n"); } catch (e) { s.end(JSON.stringify({ ok: false, error: e.message }) + "\n"); } }); }); await new Promise((r) => server.listen(path, r)); t.after(() => server.close()); return { path, calls }; } const textOf = (c) => (typeof c === "string" ? c : Array.isArray(c) ? c.map((b) => b.text ?? "").join("") : ""); function toolResults(messages) { const out = []; for (const m of messages ?? []) { if (m.role !== "user" || !Array.isArray(m.content)) continue; for (const b of m.content) if (b.type === "tool_result") out.push({ id: b.tool_use_id, isError: b.is_error ?? false, content: textOf(b.content) }); } return out; } // A scripted Anthropic Messages API. Each request that offers tools gets // the next scripted tool call; once every call has a result it answers // `done(results)` as text. Returns { url, requests, close }. export async function mockAnthropic(t, { script = [], done = (r) => `DONE ${JSON.stringify(r)}` } = {}) { const requests = []; const server = createHttp((req, res) => { let raw = ""; req.on("data", (c) => (raw += c)); req.on("end", () => { if (req.method !== "POST" || !/\/messages(\?|$)/.test(req.url)) { res.writeHead(req.url.includes("count_tokens") ? 200 : 404, { "content-type": "application/json" }); return res.end(JSON.stringify(req.url.includes("count_tokens") ? { input_tokens: 10 } : { type: "error", error: { type: "not_found_error", message: "mock" } })); } const body = JSON.parse(raw || "{}"); const tools = Array.isArray(body.tools) ? body.tools.map((x) => x.name) : []; const results = toolResults(body.messages); const step = tools.length ? script[results.length] : null; const r = step ? { kind: "tool", id: `toolu_${results.length + 1}`, name: step.name, input: step.input } : { kind: "text", text: tools.length ? done(results) : "mock" }; requests.push({ tools, results, system: body.system, answer: r, raw }); const content = r.kind === "tool" ? { type: "tool_use", id: r.id, name: r.name, input: {} } : { type: "text", text: "" }; const delta = r.kind === "tool" ? { type: "input_json_delta", partial_json: JSON.stringify(r.input) } : { type: "text_delta", text: r.text }; const stop = r.kind === "tool" ? "tool_use" : "end_turn"; if (!body.stream) { res.writeHead(200, { "content-type": "application/json" }); const c = r.kind === "tool" ? { type: "tool_use", id: r.id, name: r.name, input: r.input } : { type: "text", text: r.text }; return res.end(JSON.stringify({ id: "msg_mock", type: "message", role: "assistant", model: body.model, content: [c], stop_reason: stop, stop_sequence: null, usage: { input_tokens: 10, output_tokens: 5 } })); } const send = (event, data) => res.write(`event: ${event}\ndata: ${JSON.stringify({ type: event, ...data })}\n\n`); res.writeHead(200, { "content-type": "text/event-stream", "cache-control": "no-cache" }); send("message_start", { message: { id: "msg_mock", type: "message", role: "assistant", model: body.model, content: [], stop_reason: null, stop_sequence: null, usage: { input_tokens: 10, output_tokens: 1, cache_creation_input_tokens: 0, cache_read_input_tokens: 0 } } }); send("content_block_start", { index: 0, content_block: content }); send("content_block_delta", { index: 0, delta }); send("content_block_stop", { index: 0 }); send("message_delta", { delta: { stop_reason: stop, stop_sequence: null }, usage: { output_tokens: 5 } }); send("message_stop", {}); res.end(); }); }); await new Promise((r) => server.listen(0, "127.0.0.1", r)); t.after(() => server.close()); return { url: `http://127.0.0.1:${server.address().port}`, requests }; }