Skip to content
File

Blob: scripts/litmus/harness.ts

typescript100 lines
1import { execFile as execFileCallback, spawn } from "node:child_process";
2import { access } from "node:fs/promises";
3import { constants } from "node:fs";
4import path from "node:path";
5import { promisify } from "node:util";
6 
7import { repoRoot, runIsolatedWorker } from "../dav-harness/server";
8 
9const execFile = promisify(execFileCallback);
10 
11const tsxBinPath = path.join(repoRoot, "node_modules", "tsx", "dist", "cli.mjs");
12const litmusBinaryPath = path.join(repoRoot, "vendor", "litmus", "litmus");
13 
14interface LitmusFixture {
15 url: string;
16 username: string;
17 pat: string;
18 host_label: string;
19}
20 
21async function exists(filePath: string): Promise<boolean> {
22 try {
23 await access(filePath, constants.X_OK);
24 return true;
25 } catch {
26 return false;
27 }
28}
29 
30async function ensureLitmusBinary(): Promise<string> {
31 if (await exists(litmusBinaryPath)) return litmusBinaryPath;
32 await execFile(process.execPath, [tsxBinPath, path.join(repoRoot, "scripts/litmus/setup.ts")], {
33 cwd: repoRoot,
34 env: { ...process.env },
35 timeout: 300_000,
36 maxBuffer: 10 * 1024 * 1024,
37 });
38 if (!(await exists(litmusBinaryPath)))
39 throw new Error("litmus setup completed, but vendor/litmus/litmus was not found");
40 return litmusBinaryPath;
41}
42 
43async function createLitmusFixture(port: number): Promise<LitmusFixture> {
44 const response = await fetch(`http://dab.localhost:${port}/__dab_test/fixtures/litmus`, { method: "POST" });
45 if (response.status !== 200) {
46 throw new Error(`fixture route returned ${response.status}: ${await response.text()}`);
47 }
48 const body = (await response.json()) as Partial<LitmusFixture>;
49 if (!body.url || !body.username || !body.pat || !body.host_label) {
50 throw new Error(`fixture route returned invalid payload: ${JSON.stringify(body)}`);
51 }
52 return body as LitmusFixture;
53}
54 
55async function runLitmusClient(binaryPath: string, fixture: LitmusFixture, workingDirectory: string): Promise<void> {
56 await new Promise<void>((resolve, reject) => {
57 const child = spawn(binaryPath, [fixture.url, fixture.username, fixture.pat], {
58 cwd: workingDirectory,
59 stdio: "inherit",
60 env: { ...process.env, TESTROOT: path.dirname(binaryPath) },
61 });
62 child.on("error", reject);
63 child.on("exit", (code) => {
64 if (code === 0) resolve();
65 else reject(new Error(`litmus exited with ${code}`));
66 });
67 });
68}
69 
70async function main(): Promise<void> {
71 const litmusBinary = await ensureLitmusBinary();
72 await runIsolatedWorker(
73 {
74 name: "dab-litmus",
75 tempPrefix: "dab-litmus-",
76 preserveEnv: "DAB_LITMUS_PRESERVE",
77 oidcIssuer: process.env.DAB_LITMUS_OIDC_ISSUER ?? "http://localhost",
78 d1DatabaseId: "local-litmus",
79 r2BucketName: "dab-file-blobs-litmus",
80 resourceSuffix: "litmus",
81 devVars: {
82 TESSERA_OIDC_CLIENT_ID: "dab-litmus",
83 TESSERA_OIDC_CLIENT_SECRET: "litmus-local-client-secret",
84 DAB_SESSION_SECRET: "litmus-local-session-secret",
85 },
86 },
87 async ({ port, tempDir }) => {
88 console.log(`healthz ok: http://dab.localhost:${port}/healthz`);
89 const fixture = await createLitmusFixture(port);
90 console.log(`litmus target: ${fixture.url}`);
91 await runLitmusClient(litmusBinary, fixture, tempDir);
92 },
93 );
94}
95 
96main().catch((error) => {
97 console.error(error instanceof Error ? error.message : String(error));
98 process.exit(1);
99});