Something went wrong. Try again.
An agent harness for spec-driven development.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322import fs from "node:fs/promises";import http from "node:http";import os from "node:os";import path from "node:path";import { afterEach, beforeEach, describe, expect, test } from "vitest";import { createCharterServer, startSpecWatcher } from "../src/server.js";import { createCharterDatabase } from "../src/db/factory.js";
async function makeProject(): Promise<string> { const root = await fs.mkdtemp(path.join(os.tmpdir(), "charter-server-")); await fs.mkdir(path.join(root, "spec")); await fs.writeFile(path.join(root, "spec", "vision.md"), `---id: visiontitle: Visionkind: experiencestatus: active---
# Vision
Original.`, "utf8"); return root;}
async function listen(server: http.Server): Promise<string> { await new Promise<void>((resolve, reject) => { server.once("error", reject); server.listen(0, "127.0.0.1", () => resolve()); }); const address = server.address(); if (!address || typeof address === "string") throw new Error("server did not expose a TCP address"); return `http://127.0.0.1:${address.port}`;}
async function getJson<T>(baseUrl: string, route: string): Promise<T> { const response = await fetch(`${baseUrl}${route}`); expect(response.ok).toBe(true); return await response.json() as T;}
async function postJson<T>(baseUrl: string, route: string, body: unknown, method = "POST"): Promise<T> { const response = await fetch(`${baseUrl}${route}`, { method, headers: { "content-type": "application/json" }, body: JSON.stringify(body), }); expect(response.ok).toBe(true); return await response.json() as T;}
async function waitFor<T>(read: () => Promise<T>, pass: (value: T) => boolean): Promise<T> { const started = Date.now(); let latest: T | undefined; do { latest = await read(); if (pass(latest)) return latest; await new Promise((resolve) => setTimeout(resolve, 25)); } while (Date.now() - started < 2500); throw new Error(`condition did not pass; latest=${JSON.stringify(latest)}`);}
describe("Charter server", () => { const previousAdapter = process.env.CHARTER_DB_ADAPTER;
beforeEach(() => { process.env.CHARTER_DB_ADAPTER = "json"; });
afterEach(() => { if (previousAdapter === undefined) delete process.env.CHARTER_DB_ADAPTER; else process.env.CHARTER_DB_ADAPTER = previousAdapter; });
test("serves a terminal-first status page instead of an editor UI", async () => { const root = await makeProject(); const server = createCharterServer({ projectRoot: root }); const baseUrl = await listen(server);
try { const response = await fetch(baseUrl); expect(response.ok).toBe(true); const html = await response.text(); expect(html).toContain("Edit files. Watch events."); expect(html).toContain("npm run watch"); expect(html).not.toContain("newDocumentButton"); expect(html).not.toContain("Project machinery"); expect(html).not.toContain("<script>"); } finally { await new Promise<void>((resolve) => server.close(() => resolve())); } });
test("standalone watch runtime auto-ingests filesystem edits and emits observable logs", async () => { const root = await makeProject(); const logs: unknown[] = []; const watcher = startSpecWatcher({ projectRoot: root, specDir: "spec", }, { intervalMs: 50, log: (entry) => logs.push(entry), }); const readEvents = async () => { const db = createCharterDatabase({ projectRoot: root, projectId: "charter" }); const snapshot = await db.loadSnapshot(); return Object.values(snapshot.events).sort((left, right) => left.occurredAt.localeCompare(right.occurredAt) || left.id.localeCompare(right.id)); };
try { await waitFor( readEvents, (events) => events.some((event) => event.type === "spec.document.upserted" && event.documentId === "vision"), );
await fs.appendFile(path.join(root, "spec", "vision.md"), "\nWatched edit.\n", "utf8");
const events = await waitFor( readEvents, (state) => state.filter((event) => event.type === "spec.document.upserted" && event.documentId === "vision").length >= 2, );
expect(events.at(-1)).toMatchObject({ type: "spec.document.upserted", documentId: "vision", authority: "human.file" }); expect(logs).toContainEqual(expect.objectContaining({ event: "watch.started" })); expect(logs).toContainEqual(expect.objectContaining({ event: "sync.applied" })); expect(logs).toContainEqual(expect.objectContaining({ event: "project.event", projectEventType: "spec.document.upserted", documentId: "vision" })); expect(logs).toContainEqual(expect.objectContaining({ event: "work.queued", documentId: "vision" })); } finally { watcher.close(); } });
test("creates a new spec document through the document API", async () => { const root = await makeProject(); const server = createCharterServer({ projectRoot: root }); const baseUrl = await listen(server);
try { const created = await postJson<{ document: { id: string; title: string; path: string; raw: string } }>(baseUrl, "/api/documents", { id: "new-thing", title: "New Thing", kind: "note", status: "draft", path: "new-thing.md", });
expect(created.document).toMatchObject({ id: "new-thing", title: "New Thing", path: "new-thing.md" }); expect(created.document.raw).toContain("# New Thing");
const documents = await getJson<{ documents: { id: string }[] }>(baseUrl, "/api/documents"); expect(documents.documents.map((document) => document.id)).toContain("new-thing"); } finally { await new Promise<void>((resolve) => server.close(() => resolve())); } });
test("serves project state, edits a spec file, and syncs resulting work", async () => { const root = await makeProject(); const server = createCharterServer({ projectRoot: root }); const baseUrl = await listen(server);
try { const project = await getJson<{ documents: { current: number }; changes: { added: number } }>(baseUrl, "/api/project"); expect(project.documents.current).toBe(1); expect(project.changes.added).toBe(1);
const document = await getJson<{ document: { id: string; raw: string } }>(baseUrl, "/api/documents/vision"); expect(document.document.id).toBe("vision"); expect(document.document.raw).toContain("Original.");
await postJson(baseUrl, "/api/documents/vision", { raw: document.document.raw.replace("Original.", "Changed through the workspace.") }, "PUT"); const changed = await getJson<{ actionable: { documentId: string; type: string }[] }>(baseUrl, "/api/changes"); expect(changed.actionable).toEqual([{ documentId: "vision", path: "vision.md", title: "Vision", type: "added", currentSha256: expect.any(String), currentPath: "vision.md" }]);
const sync = await postJson<{ eventsCreated: number; workItemsQueued: number }>(baseUrl, "/api/sync", {}); expect(sync.eventsCreated).toBe(1); expect(sync.workItemsQueued).toBe(1);
const work = await getJson<{ workItems: { documentId: string; status: string }[] }>(baseUrl, "/api/work-items"); expect(work.workItems).toHaveLength(1); expect(work.workItems[0]).toMatchObject({ documentId: "vision", status: "queued" }); } finally { await new Promise<void>((resolve) => server.close(() => resolve())); } });
test("registers consumers and creates consumer-scoped request packets", async () => { const root = await makeProject(); const server = createCharterServer({ projectRoot: root }); const baseUrl = await listen(server);
try { const created = await postJson<{ consumer: { id: string; status: string; runnerCommand: string }; eventId: string; matchedWorkItemIds: string[]; matchEventsCreated: number; snapshot?: unknown }>(baseUrl, "/api/consumers", { id: "implementer", name: "Implementer", runnerKind: "script", runnerCommand: "npm test", eventTypes: ["spec.document.upserted"], defaultContextDocumentIds: ["vision"], }); expect(created.consumer).toMatchObject({ id: "implementer", status: "active", runnerCommand: "npm test" }); expect(created.eventId).toContain("consumer.registered"); expect(created.matchedWorkItemIds).toEqual([]); expect(created.matchEventsCreated).toBe(0); expect(created.snapshot).toBeUndefined();
const consumers = await getJson<{ consumers: { id: string }[] }>(baseUrl, "/api/consumers"); expect(consumers.consumers.map((consumer) => consumer.id)).toEqual(["implementer"]);
const sync = await postJson<{ consumerMatches: number }>(baseUrl, "/api/sync", {}); expect(sync.consumerMatches).toBe(1); const work = await getJson<{ workItems: { id: string; documentId: string; matchedConsumerIds: string[] }[] }>(baseUrl, "/api/work-items"); expect(work.workItems).toHaveLength(1); expect(work.workItems[0]?.matchedConsumerIds).toEqual(["implementer"]);
const run = await postJson<{ request: { id: string; consumerId: string; command: string; attachedDocumentIds: string[]; prompt: string }; packet: { id: string; consumerId: string; workItemIds: string[] } }>( baseUrl, `/api/work-items/${encodeURIComponent(work.workItems[0]!.id)}/run`, { consumerId: "implementer" }, ); expect(run.request.consumerId).toBe("implementer"); expect(run.request.command).toBe("npm test"); expect(run.request.attachedDocumentIds).toContain("vision"); expect(run.request.prompt).toContain("Consumer: implementer"); expect(run.packet).toMatchObject({ id: run.request.id, consumerId: "implementer", workItemIds: [work.workItems[0]!.id] });
const packets = await getJson<{ packets: { id: string }[] }>(baseUrl, "/api/packets"); expect(packets.packets.map((packet) => packet.id)).toEqual([run.packet.id]); } finally { await new Promise<void>((resolve) => server.close(() => resolve())); } });
test("rematches existing queued work when registering a consumer", async () => { const root = await makeProject(); const server = createCharterServer({ projectRoot: root }); const baseUrl = await listen(server);
try { await postJson(baseUrl, "/api/sync", {}); const before = await getJson<{ workItems: { id: string; matchedConsumerIds: string[] }[] }>(baseUrl, "/api/work-items"); expect(before.workItems).toHaveLength(1); expect(before.workItems[0]?.matchedConsumerIds).toEqual([]);
const created = await postJson<{ consumer: { id: string }; matchedWorkItemIds: string[]; matchEventsCreated: number; snapshot?: unknown; }>(baseUrl, "/api/consumers", { id: "backfill-consumer", name: "Backfill Consumer", runnerKind: "script", runnerCommand: "npm test", eventTypes: ["spec.document.upserted"], documentSelectors: ["**"], matchExistingQueuedWork: true, });
expect(created.consumer.id).toBe("backfill-consumer"); expect(created.matchedWorkItemIds).toEqual([before.workItems[0]!.id]); expect(created.matchEventsCreated).toBe(1); expect(created.snapshot).toBeUndefined();
const afterRegister = await getJson<{ workItems: { id: string; matchedConsumerIds: string[] }[] }>(baseUrl, "/api/work-items"); expect(afterRegister.workItems[0]?.matchedConsumerIds).toEqual(["backfill-consumer"]);
await postJson(baseUrl, "/api/consumers", { id: "manual-consumer", name: "Manual Consumer", runnerKind: "script", runnerCommand: "npm test", eventTypes: ["spec.document.upserted"], documentSelectors: ["**"], });
const explicit = await postJson<{ matchedWorkItemIds: string[]; matchEventsCreated: number }>(baseUrl, "/api/consumers/manual-consumer/match-queued", {}); expect(explicit.matchedWorkItemIds).toEqual([before.workItems[0]!.id]); expect(explicit.matchEventsCreated).toBe(1);
const afterExplicit = await getJson<{ workItems: { matchedConsumerIds: string[] }[] }>(baseUrl, "/api/work-items"); expect(afterExplicit.workItems[0]?.matchedConsumerIds).toEqual(["backfill-consumer", "manual-consumer"]); } finally { await new Promise<void>((resolve) => server.close(() => resolve())); } });
test("runs the first matched consumer on the server and records packet status", async () => { const root = await makeProject(); const server = createCharterServer({ projectRoot: root }); const baseUrl = await listen(server);
try { await postJson(baseUrl, "/api/consumers", { id: "implementer", name: "Implementer", runnerKind: "script", runnerCommand: `node -e "let d=''; process.stdin.on('data', c => d += c); process.stdin.on('end', () => { console.log('runner ok ' + d.includes('Consumer: implementer')); });"`, eventTypes: ["spec.document.upserted"], }); await postJson(baseUrl, "/api/sync", {});
const result = await postJson<{ request: { id: string; status: string; consumerId: string; stdoutText: string }; packet: { id: string; status: string; consumerId: string; stdoutText: string; exitCode: number }; workItems: { status: string; requestId: string }[]; }>(baseUrl, "/api/agent/next", { exec: true });
expect(result.request).toMatchObject({ status: "executed", consumerId: "implementer" }); expect(result.request.stdoutText).toContain("runner ok true"); expect(result.packet).toMatchObject({ id: result.request.id, status: "executed", consumerId: "implementer", exitCode: 0 }); expect(result.packet.stdoutText).toContain("runner ok true"); expect(result.workItems).toHaveLength(1); expect(result.workItems[0]).toMatchObject({ status: "done", requestId: result.request.id }); } finally { await new Promise<void>((resolve) => server.close(() => resolve())); } });
});