192 lines · 6.4 KB · TypeScript
RawDownload
/* oxlint-disable executor/no-try-catch-or-throw, executor/no-error-constructor, executor/no-promise-reject, executor/no-promise-catch -- boundary: standalone build smoke harness tears down a spawned packed binary */import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";import { tmpdir } from "node:os";import { join, resolve } from "node:path";import { spawn, type Subprocess } from "bun"; const binary = process.argv[2];if (!binary) throw new Error("usage: bun apps/cli/scripts/smoke-packed-apps.ts <packed-binary>"); const AUTH_TOKEN = "packed-apps-smoke-token";const READY_TIMEOUT_MS = 45_000; const fail = (message: string): never => {  throw new Error(`[smoke-packed-apps] FAIL: ${message}`);}; const requestJson = async <T>(origin: string, path: string, init: RequestInit = {}): Promise<T> => {  const response = await fetch(new URL(path, origin), {    ...init,    headers: {      authorization: `Bearer ${AUTH_TOKEN}`,      ...(init.body === undefined ? {} : { "content-type": "application/json" }),      ...init.headers,    },  });  const text = await response.text();  if (!response.ok) fail(`${init.method ?? "GET"} ${path} returned ${response.status}: ${text}`);  return (text.length > 0 ? JSON.parse(text) : null) as T;}; const waitForReadyPort = async (proc: Subprocess<"ignore", "pipe", "pipe">): Promise<number> =>  new Promise((resolveReady, rejectReady) => {    const timer = setTimeout(      () => rejectReady(new Error("packed binary did not report ready in time")),      READY_TIMEOUT_MS,    );    let stdout = "";    let stderr = "";    const finish = (result: number | Error): void => {      clearTimeout(timer);      if (result instanceof Error) rejectReady(result);      else resolveReady(result);    };    void proc.exited.then((code) => {      finish(new Error(`packed binary exited before ready with code ${code}: ${stderr}`));    });    void (async () => {      const reader = proc.stdout.getReader();      const decoder = new TextDecoder();      try {        while (true) {          const chunk = await reader.read();          if (chunk.done) return;          stdout += decoder.decode(chunk.value);          const match = stdout.match(/EXECUTOR_READY:(\d+)/);          if (match) {            finish(Number(match[1]));            return;          }        }      } catch (cause) {        finish(cause instanceof Error ? cause : new Error(String(cause)));      }    })();    void (async () => {      const reader = proc.stderr.getReader();      const decoder = new TextDecoder();      while (true) {        const chunk = await reader.read();        if (chunk.done) return;        stderr += decoder.decode(chunk.value);      }    })().catch(() => undefined);  }); const main = async (): Promise<void> => {  const fixture = await mkdtemp(join(tmpdir(), "executor-packed-apps-fixture-"));  const dataDir = await mkdtemp(join(tmpdir(), "executor-packed-apps-data-"));  const scopeDir = await mkdtemp(join(tmpdir(), "executor-packed-apps-scope-"));  let proc: Subprocess<"ignore", "pipe", "pipe"> | null = null;  try {    await mkdir(join(fixture, "tools"), { recursive: true });    await writeFile(      join(fixture, "tools", "greet.ts"),      `        import { z } from "zod";        import { defineTool } from "executor:app";         const Input = z.object({ name: z.string() });         export default defineTool({          description: "Greet",          input: Input,          async handler(input) {            return { greeting: "hello " + input.name };          },        });      `,    );    await writeFile(      join(fixture, "package.json"),      `${JSON.stringify({ name: "packed-apps-smoke", dependencies: { zod: "4.3.6" } })}\n`,    );    await writeFile(join(fixture, "bun.lock"), "");     proc = spawn({      cmd: [        resolve(binary),        "daemon",        "run",        "--foreground",        "--port",        "0",        "--hostname",        "127.0.0.1",        "--auth-token",        AUTH_TOKEN,      ],      env: {        ...process.env,        EXECUTOR_CLIENT: "desktop",        EXECUTOR_DATA_DIR: dataDir,        EXECUTOR_SCOPE_DIR: scopeDir,      },      stdin: "ignore",      stdout: "pipe",      stderr: "pipe",    });     const port = await waitForReadyPort(proc);    const origin = `http://127.0.0.1:${port}`;    console.log(`[smoke-packed-apps] ready on ${origin}`);     await requestJson(origin, "/api/apps/sources", {      method: "POST",      body: JSON.stringify({        kind: "local-directory",        slug: "packed-apps-smoke",        app: "packed-apps-smoke",        path: fixture,      }),    });     const sync = await requestJson<{      readonly status: string;      readonly tools: readonly string[];      readonly errors?: readonly unknown[];    }>(origin, "/api/apps/sources/packed-apps-smoke/sync", { method: "POST" });    if (sync.status !== "published") fail(`sync failed: ${JSON.stringify(sync.errors)}`);    if (JSON.stringify(sync.tools) !== JSON.stringify(["greet"])) {      fail(`expected greet tool, got ${JSON.stringify(sync.tools)}`);    }     const invoked = await requestJson<{      readonly status: string;      readonly structured: unknown;      readonly isError: boolean;      readonly text: string;    }>(origin, "/api/executions", {      method: "POST",      body: JSON.stringify({        code: `export default await tools["packed-apps-smoke.org.published.greet"]({ name: "Ada" })`,        autoApprove: true,      }),    });    if (invoked.status !== "completed" || invoked.isError) {      fail(`execution failed: ${JSON.stringify(invoked)}`);    }    if (      JSON.stringify(invoked.structured) !==      JSON.stringify({        status: "completed",        result: { ok: true, data: { greeting: "hello Ada" } },        logs: [],      })    ) {      fail(`unexpected execution result: ${JSON.stringify(invoked)}`);    }    console.log("[smoke-packed-apps] OK - local-directory app source bundled and invoked");  } finally {    if (proc) {      proc.kill("SIGTERM");      await Promise.race([proc.exited, new Promise((resolve) => setTimeout(resolve, 3000))]);      proc.kill("SIGKILL");    }    await rm(fixture, { recursive: true, force: true }).catch(() => undefined);    await rm(dataDir, { recursive: true, force: true }).catch(() => undefined);    await rm(scopeDir, { recursive: true, force: true }).catch(() => undefined);  }}; await main();