atproto user agency toolkit for individuals and groups
8
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 166 lines 4.6 kB view raw
1/** 2 * Sidecar process integration test. 3 * 4 * Spawns `src/server.ts` as a child process (the same way Tauri does), 5 * validates the full sidecar contract: 6 * 1. Parse P2PDS_READY from stdout to get the port 7 * 2. GET /xrpc/_health returns 200 {"status":"ok"} 8 * 3. GET / returns 200 HTML 9 * 4. SIGTERM → process exits with code 0 10 */ 11 12import { describe, it, expect, afterEach } from "vitest"; 13import { spawn, type ChildProcess } from "node:child_process"; 14import { mkdtempSync, rmSync } from "node:fs"; 15import { tmpdir } from "node:os"; 16import { join } from "node:path"; 17 18const STARTUP_TIMEOUT_MS = 30_000; 19const POLL_INTERVAL_MS = 200; 20 21interface ReadyInfo { 22 port: number; 23 url: string; 24} 25 26function parseReadyLine(line: string): ReadyInfo | null { 27 const prefix = "P2PDS_READY "; 28 if (!line.startsWith(prefix)) return null; 29 try { 30 return JSON.parse(line.slice(prefix.length)) as ReadyInfo; 31 } catch { 32 return null; 33 } 34} 35 36function waitForReady(proc: ChildProcess, timeoutMs: number): Promise<ReadyInfo> { 37 return new Promise((resolve, reject) => { 38 const timer = setTimeout(() => { 39 reject(new Error(`P2PDS_READY not seen within ${timeoutMs}ms`)); 40 }, timeoutMs); 41 42 let buffer = ""; 43 proc.stdout?.on("data", (chunk: Buffer) => { 44 buffer += chunk.toString(); 45 const lines = buffer.split("\n"); 46 buffer = lines.pop()!; // keep incomplete trailing line 47 for (const line of lines) { 48 const info = parseReadyLine(line); 49 if (info) { 50 clearTimeout(timer); 51 resolve(info); 52 return; 53 } 54 } 55 }); 56 57 proc.on("error", (err) => { 58 clearTimeout(timer); 59 reject(err); 60 }); 61 62 proc.on("exit", (code) => { 63 clearTimeout(timer); 64 reject(new Error(`Process exited with code ${code} before P2PDS_READY`)); 65 }); 66 }); 67} 68 69async function pollHealth(url: string, timeoutMs: number): Promise<void> { 70 const deadline = Date.now() + timeoutMs; 71 while (Date.now() < deadline) { 72 try { 73 const res = await fetch(`${url}/xrpc/_health`); 74 if (res.ok) return; 75 } catch { 76 // not ready yet 77 } 78 await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS)); 79 } 80 throw new Error(`Health check did not pass within ${timeoutMs}ms`); 81} 82 83describe("sidecar process", () => { 84 let proc: ChildProcess | undefined; 85 let tmpDir: string | undefined; 86 87 afterEach(async () => { 88 if (proc && !proc.killed) { 89 proc.kill("SIGTERM"); 90 // Wait for exit (up to 5s) 91 await new Promise<void>((resolve) => { 92 const timer = setTimeout(() => { 93 proc?.kill("SIGKILL"); 94 resolve(); 95 }, 5_000); 96 proc!.on("exit", () => { 97 clearTimeout(timer); 98 resolve(); 99 }); 100 }); 101 } 102 proc = undefined; 103 if (tmpDir) { 104 rmSync(tmpDir, { recursive: true, force: true }); 105 tmpDir = undefined; 106 } 107 }); 108 109 it("full sidecar lifecycle: ready → health → app → shutdown", async () => { 110 tmpDir = mkdtempSync(join(tmpdir(), "sidecar-test-")); 111 112 proc = spawn("npx", ["tsx", "src/server.ts"], { 113 env: { 114 ...process.env, 115 PORT: "0", 116 DATA_DIR: tmpDir, 117 IPFS_ENABLED: "true", 118 IPFS_NETWORKING: "false", 119 FIREHOSE_ENABLED: "false", 120 RATE_LIMIT_ENABLED: "false", 121 OAUTH_ENABLED: "false", 122 PDS_HOSTNAME: "localhost", 123 AUTH_TOKEN: "test-token", 124 SIGNING_KEY: "0000000000000000000000000000000000000000000000000000000000000001", 125 SIGNING_KEY_PUBLIC: "zQ3shP2mWsZYWgvZM9GJ3EvMfRXQJwuTh6BdXLvJB9gFhT3Lr", 126 JWT_SECRET: "test-secret", 127 PASSWORD_HASH: "$2a$10$test", 128 DID: "did:plc:sidecartest", 129 }, 130 stdio: ["ignore", "pipe", "pipe"], 131 }); 132 133 // Collect stderr for debugging 134 let stderr = ""; 135 proc.stderr?.on("data", (chunk: Buffer) => { 136 stderr += chunk.toString(); 137 }); 138 139 // 1. Parse P2PDS_READY from stdout 140 const ready = await waitForReady(proc, STARTUP_TIMEOUT_MS); 141 expect(ready.port).toBeGreaterThan(0); 142 expect(ready.url).toMatch(/^http:\/\/localhost:\d+$/); 143 144 // 2. Poll health endpoint 145 await pollHealth(ready.url, 5_000); 146 const healthRes = await fetch(`${ready.url}/xrpc/_health`); 147 expect(healthRes.status).toBe(200); 148 const healthBody = (await healthRes.json()) as { status: string }; 149 expect(healthBody.status).toBe("ok"); 150 151 // 3. App returns HTML 152 const dashRes = await fetch(`${ready.url}/`); 153 expect(dashRes.status).toBe(200); 154 const html = await dashRes.text(); 155 expect(html).toContain("P2PDS"); 156 157 // 4. Graceful shutdown via SIGTERM 158 const exitPromise = new Promise<number | null>((resolve) => { 159 proc!.on("exit", (code) => resolve(code)); 160 }); 161 proc.kill("SIGTERM"); 162 const exitCode = await exitPromise; 163 expect(exitCode).toBe(0); 164 proc = undefined; // prevent double-kill in afterEach 165 }, STARTUP_TIMEOUT_MS + 10_000); 166});