86 lines · 3.4 KB · TypeScript
RawDownload
// The selfhost boot recipe — ONE definition shared by the vitest globalsetup// (ephemeral, torn down with the suite) and the dev CLI (persistent, lives// until `down`). The app owns its dev stack; this owns the env contract.import { rmSync } from "node:fs";import { resolve } from "node:path";import { fileURLToPath } from "node:url"; import { bootProcesses, waitForBoot, waitForHttp, type BootedProcesses } from "./boot"; export const selfhostDir = fileURLToPath(new URL("../../apps/host-selfhost/", import.meta.url)); export interface SelfhostBootOptions {  readonly port: number;  /** The URL the app advertises (cookies, redirects). */  readonly webBaseUrl: string;  readonly admin: { readonly email: string; readonly password: string };  /** Defaults to the suite's throwaway dir. */  readonly dataDir?: string;  /** Wipe the data dir before boot (hermetic). Default true. */  readonly fresh?: boolean;  /** vite --host (e.g. "0.0.0.0" to be tailnet-reachable). */  readonly host?: string;  readonly logFile?: string;  /** Shrink the sandbox execution budget (EXECUTOR_SANDBOX_TIMEOUT_MS) so   *  deadline scenarios prove their race in seconds. Omit for production. */  readonly sandboxTimeoutMs?: number;  /** Shrink the MCP session idle window (EXECUTOR_MCP_SESSION_IDLE_TTL_MS) so   *  the eviction scenario proves in seconds what otherwise takes 30 minutes.   *  Omit for production; the app then uses the store's own default. */  readonly mcpSessionIdleTtlMs?: number;} export const bootSelfhost = async (options: SelfhostBootOptions): Promise<BootedProcesses> => {  const dataDir = options.dataDir ?? resolve(selfhostDir, ".e2e-data");  if (options.fresh ?? true) rmSync(dataDir, { recursive: true, force: true });   const procs = bootProcesses(    [      {        cmd: "bunx",        args: [          "--bun",          "vite",          "dev",          "--port",          String(options.port),          "--strictPort",          ...(options.host ? ["--host", options.host] : []),        ],        cwd: selfhostDir,        env: {          EXECUTOR_DATA_DIR: dataDir,          BETTER_AUTH_SECRET: "executor-selfhost-e2e-secret-0123456789",          EXECUTOR_BOOTSTRAP_ADMIN_EMAIL: options.admin.email,          EXECUTOR_BOOTSTRAP_ADMIN_PASSWORD: options.admin.password,          EXECUTOR_WEB_BASE_URL: options.webBaseUrl,          // The harness boots loopback MCP/OAuth test servers and points the          // instance at them; the hosted SSRF guard would otherwise block          // outbound probes/dials to localhost. Hermetic test instance only.          EXECUTOR_ALLOW_LOCAL_NETWORK: "true",          ...(options.sandboxTimeoutMs !== undefined            ? { EXECUTOR_SANDBOX_TIMEOUT_MS: String(options.sandboxTimeoutMs) }            : {}),          ...(options.mcpSessionIdleTtlMs !== undefined            ? { EXECUTOR_MCP_SESSION_IDLE_TTL_MS: String(options.mcpSessionIdleTtlMs) }            : {}),        },        logFile: options.logFile,      },    ],    { label: "selfhost" },  );   try {    // Probe via `localhost`, not 127.0.0.1 — without --host, vite binds the    // resolver's first answer for localhost (::1 on macOS), so the IPv4    // loopback literal never answers.    await waitForBoot(procs, (signal) =>      waitForHttp(`http://localhost:${options.port}`, { signal }),    );  } catch (error) {    await procs.teardown();    throw error;  }  return procs;};