Files
Samraaj BathandClaude Fable 5 bbe4a5643b Service: job event stream, async in-memory backend, preview routes
Ported from PR #15 (packages-side surface only; the fork's dev UI page is
intentionally dropped - the API surface is the product):
- job_events table + repository, worker emits stage transitions, additive
  GET /v1/clones/:id/events polling route. The fork's hand-written
  migration was never registered in Drizzle's journal (it silently never
  applied); regenerated properly via drizzle-kit from schema.ts with the
  (job_id, seq) index declared, and verified against a live Postgres
- In-memory backend moves to the documented enqueue-202 + poll contract
  with single-flight BUSY guard; POST honors backend httpStatus
- app-preview static serving routes; the preview BUILD half is deferred
  until the compiler exports buildApp/DEFAULT_HARNESS_DIR - routes 404
  cleanly until then
- preview option threaded through core types; cache key intentionally
  unchanged while the flag is inert

All workspaces typecheck; api 18/18, core 13/13, worker 1/1 pass;
migration verified applying (table + index + FK) on postgres:16.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 15:00:55 -07:00

51 lines
2.2 KiB
TypeScript

import { describe, it, before, after } from "node:test";
import assert from "node:assert/strict";
import { runCloneJob } from "@cloner/core";
import { serveDir, FIXTURES_DIR, hasChromium } from "@cloner/test-utils";
import { createApp } from "../src/app.js";
import { InMemoryStore } from "../src/store.js";
import { InMemoryBackend } from "../src/backends/inMemory.js";
// End-to-end: real clone of a served fixture through the HTTP layer. Skipped when
// no Chromium is installed.
describe("POST /v1/clones (real clone, served fixture)", { skip: hasChromium() ? false : "no Chromium installed" }, () => {
let server: { url: string; close: () => Promise<void> };
const store = new InMemoryStore(60_000);
before(async () => {
server = await serveDir(FIXTURES_DIR);
});
after(async () => {
store.clear();
await server.close();
});
it("clones a fixture and returns the real generated app file map", async () => {
const app = createApp({ backend: new InMemoryBackend({ store, runJob: runCloneJob }) });
const res = await app.request("/v1/clones", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ url: server.url + "/components.html", options: { interactions: false, components: true, motion: false } }),
});
assert.equal(res.status, 202);
const queued = await res.json();
let body: Record<string, unknown> | undefined;
for (let i = 0; i < 600; i++) {
const view = await (await app.request(`/v1/clones/${queued.jobId}`)).json();
if (view.status === "succeeded") {
body = await (await app.request(`/v1/clones/${queued.jobId}/result`)).json();
break;
}
if (view.status === "failed") assert.fail(view.error);
await new Promise((r) => setTimeout(r, 50));
}
assert.ok(body);
const files = body!.files as Record<string, { type: string }>;
assert.equal(body!.status, "succeeded");
assert.ok(files["src/app/page.tsx"], "has page.tsx");
assert.equal(files["src/app/page.tsx"].type, "text");
assert.ok(files["package.json"], "has package.json");
assert.ok((body!.capture as { nodeCount: number }).nodeCount > 0);
assert.equal((body!.capture as { blocked: boolean }).blocked, false);
});
});