// OpenClaw source performance isolation executes the workflow's real shared-shell path. import { spawnSync } from "node:child_process"; import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; import { join } from "node:path"; import { expectDefined } from "@openclaw/normalization-core"; import { afterEach, describe, expect, it } from "vitest"; import { parse } from "yaml"; import { useAutoCleanupTempDirTracker } from "../helpers/temp-dir.js"; const WORKFLOW = ".github/workflows/openclaw-performance.yml"; const tempDirs = useAutoCleanupTempDirTracker(afterEach); type WorkflowStep = { name?: string; run?: string; env?: Record; with?: Record; }; type WorkflowJob = { env?: Record; steps?: WorkflowStep[]; }; type Workflow = { env?: Record; jobs?: Record; }; function readWorkflow(): Workflow { return parse(readFileSync(WORKFLOW, "utf8")) as Workflow; } function findStep(name: string, job: string): WorkflowStep { const step = readWorkflow().jobs?.[job]?.steps?.find((candidate) => candidate.name === name); expect(step).toBeDefined(); return step as WorkflowStep; } describe("OpenClaw source performance isolation", () => { it("isolates the source performance gateway from network and scheduled work", () => { const workflow = readWorkflow(); const job = workflow.jobs?.source_performance; const targetStepName = "Run OpenClaw source performance probes"; const step = findStep(targetStepName, "source_performance"); const jobSteps = job?.steps ?? []; const targetStepIndex = jobSteps.findIndex((candidate) => candidate.name === targetStepName); const run = step.run ?? ""; const sliceStartMarker = 'gateway_home="$(mktemp -d)"'; const sliceEndMarker = "cleanup_gateway\ntrap - EXIT"; const sliceStart = run.indexOf(sliceStartMarker); const sliceEndMarkerIndex = run.indexOf(sliceEndMarker, sliceStart); const workflowSlice = run.slice(sliceStart, sliceEndMarkerIndex + sliceEndMarker.length); const cronEnv = "OPENCLAW_SKIP_CRON"; const joinedSliceContinuations = workflowSlice.replace(/\\\s*\n/g, " "); const gatewayConfigHeredocTargets = joinedSliceContinuations .split("\n") .filter((line) => line.includes("<<")) .flatMap((line) => line.match(/\$\{?gateway_config\}?/g) ?? []); const configHeredocStart = workflowSlice.indexOf('cat > "$gateway_config" <}" printf 'OPENCLAW_STATE_DIR=%s\\n' "\${OPENCLAW_STATE_DIR-}" printf 'OPENCLAW_CONFIG_PATH=%s\\n' "\${OPENCLAW_CONFIG_PATH-}" printf 'OPENCLAW_GATEWAY_PORT=%s\\n' "\${OPENCLAW_GATEWAY_PORT-}" printf 'OPENCLAW_SKIP_CHANNELS=%s\\n' "\${OPENCLAW_SKIP_CHANNELS-}" printf 'OPENCLAW_SKIP_CRON=%s\\n' "\${OPENCLAW_SKIP_CRON-}" } > "$CAPTURE_DIR/$1.env" } record_argv() { name="$1" shift printf '%s\\n' "$@" > "$CAPTURE_DIR/$name.argv" } if [ "\${1-}" = "-e" ]; then [ -n "\${REAL_NODE-}" ] || { printf 'REAL_NODE is required\\n' >&2; exit 64; } exec "$REAL_NODE" "$@" fi if [ "\${1-}" = "dist/entry.js" ] && [ "\${2-}" = "gateway" ] && [ "\${3-}" = "run" ]; then record_env gateway record_argv gateway "$@" /bin/cp "$OPENCLAW_CONFIG_PATH" "$CAPTURE_DIR/gateway.config" printf '%s\\n' "$OPENCLAW_GATEWAY_PORT" > "$CAPTURE_DIR/gateway.port" trap 'printf "terminated\\n" > "$CAPTURE_DIR/gateway.terminated"; exit 0' TERM INT printf 'gateway-run\\n' >> "$EVENTS_FILE" : > "$CAPTURE_DIR/gateway.ready" remaining=8 while [ "$remaining" -gt 0 ]; do /bin/sleep 1 remaining=$((remaining - 1)) done exit 64 fi if [ "\${1-}" = "dist/entry.js" ] && [ "\${2-}" = "gateway" ] && [ "\${3-}" = "health" ]; then record_env health record_argv health "$@" /bin/cp "$OPENCLAW_CONFIG_PATH" "$CAPTURE_DIR/health.config" printf 'gateway-health\\n' >> "$EVENTS_FILE" exit 0 fi if [ "\${1-}" = "--import" ] && [ "\${2-}" = "tsx" ]; then case "\${3-}" in *bench-cli-startup.ts) ;; *) exit 64 ;; esac record_env benchmark record_argv benchmark "$@" /bin/cp "$OPENCLAW_CONFIG_PATH" "$CAPTURE_DIR/benchmark.config" printf 'benchmark\\n' >> "$EVENTS_FILE" exit 0 fi printf 'unexpected node argv:' >&2 printf ' %s' "$@" >&2 printf '\\n' >&2 exit 64 `, ); writeFileSync( join(binDir, "cp"), `#!/bin/sh set -eu [ "$#" -eq 2 ] || exit 64 [ ! -e "$CAPTURE_DIR/readiness-copy.config" ] || exit 64 /bin/cp "$1" "$CAPTURE_DIR/readiness-copy.config" /bin/cp "$1" "$2" `, ); writeFileSync( join(binDir, "curl"), `#!/bin/sh set -eu [ -s "$CAPTURE_DIR/gateway.port" ] || exit 1 port="$(cat "$CAPTURE_DIR/gateway.port")" expected="http://127.0.0.1:$port/healthz" found=false for arg in "$@"; do [ "$arg" = "$expected" ] && found=true done [ "$found" = true ] || { printf 'unexpected curl argv\\n' >&2; exit 64; } [ -f "$CAPTURE_DIR/gateway.ready" ] || exit 1 printf 'http-health\\n' >> "$EVENTS_FILE" `, ); writeFileSync( join(binDir, "rg"), `#!/bin/sh set -eu [ "$#" -eq 3 ] && [ "$1" = '-q' ] && [ "$2" = 'catalogRefresh:' ] && [ "$3" = 'src/config/zod-schema.core.ts' ] || { printf 'unexpected rg argv\\n' >&2 exit 64 } printf 'catalog-probe\\n' >> "$EVENTS_FILE" exit "$RG_STATUS" `, ); chmodSync(join(binDir, "rg"), 0o755); chmodSync(join(binDir, "curl"), 0o755); chmodSync(join(binDir, "cp"), 0o755); chmodSync(join(binDir, "node"), 0o755); for (const [branch, rgStatus, expectsCatalogRefresh] of [ ["unsupported", "1", false], ["supported", "0", true], ] as const) { const branchDir = join(fixtureRoot, branch); const captureDir = join(branchDir, "capture"); const sourcePerfDir = join(branchDir, "source"); const eventsFile = join(branchDir, "events.txt"); mkdirSync(captureDir, { recursive: true }); mkdirSync(sourcePerfDir); const isolatedEnv = { ...process.env }; for (const name of [ "OPENCLAW_SKIP_CRON", "OPENCLAW_SKIP_CHANNELS", "BASH_ENV", "ENV", "BASHOPTS", "SHELLOPTS", "CDPATH", "GLOBIGNORE", "PROMPT_COMMAND", "ZDOTDIR", ]) { delete isolatedEnv[name]; } Object.assign(isolatedEnv, { PATH: `${binDir}:/usr/bin:/bin`, HOME: branchDir, TMPDIR: branchDir, CAPTURE_DIR: captureDir, EVENTS_FILE: eventsFile, GITHUB_WORKSPACE: "/fixture/workspace", PERFORMANCE_HELPER_DIR: "/fixture/helpers", REAL_NODE: process.execPath, RG_STATUS: rgStatus, SOURCE_PERF_DIR: sourcePerfDir, source_runs: "2", }); const result = spawnSync( "bash", ["--noprofile", "--norc", "-c", `set -euo pipefail\n${workflowSlice}`], { cwd: fixtureRoot, encoding: "utf8", env: isolatedEnv, timeout: 12_000, }, ); expect(result.error, result.stderr).toBeUndefined(); expect(result.status, result.stderr).toBe(0); expect(readFileSync(eventsFile, "utf8").trim().split("\n")).toEqual([ "catalog-probe", "gateway-run", "http-health", "gateway-health", "benchmark", ]); const gatewayConfig = readFileSync(join(captureDir, "gateway.config"), "utf8"); const gatewayPort = readFileSync(join(captureDir, "gateway.port"), "utf8").trim(); expect(gatewayPort).toMatch(/^[1-9][0-9]*$/); expect(readFileSync(join(captureDir, "readiness-copy.config"), "utf8")).toBe(gatewayConfig); expect(readFileSync(join(captureDir, "health.config"), "utf8")).toBe(gatewayConfig); expect(readFileSync(join(captureDir, "benchmark.config"), "utf8")).toBe(gatewayConfig); parse(gatewayConfig, { uniqueKeys: true }); const parsedConfig = JSON.parse(gatewayConfig) as { models?: { catalogRefresh?: { enabled?: boolean } }; }; expect(parsedConfig).toMatchObject({ agents: { defaults: { heartbeat: { every: "0m" } } }, browser: { enabled: false }, update: { checkOnStart: false }, gateway: { mode: "local", port: Number(gatewayPort), bind: "loopback", auth: { mode: "none" }, controlUi: { enabled: false }, tailscale: { mode: "off" }, }, plugins: { enabled: true, entries: { browser: { enabled: false } } }, }); expect(JSON.stringify(parsedConfig)).not.toContain('"dreaming":'); if (expectsCatalogRefresh) { expect(parsedConfig.models?.catalogRefresh?.enabled).toBe(false); } else { expect(parsedConfig.models?.catalogRefresh).toBeUndefined(); } const readCapturedEnv = (name: string) => Object.fromEntries( readFileSync(join(captureDir, `${name}.env`), "utf8") .trim() .split("\n") .map((line) => { const separator = line.indexOf("="); return [line.slice(0, separator), line.slice(separator + 1)]; }), ) as Record; const gatewayEnv = readCapturedEnv("gateway"); const healthEnv = readCapturedEnv("health"); const benchmarkEnv = readCapturedEnv("benchmark"); const gatewayHome = expectDefined(gatewayEnv.OPENCLAW_HOME, "gateway home capture"); const gatewayState = expectDefined(gatewayEnv.OPENCLAW_STATE_DIR, "gateway state capture"); const gatewayConfigPath = expectDefined( gatewayEnv.OPENCLAW_CONFIG_PATH, "gateway config capture", ); const healthHome = expectDefined(healthEnv.OPENCLAW_HOME, "readiness home capture"); const healthState = expectDefined(healthEnv.OPENCLAW_STATE_DIR, "readiness state capture"); const healthConfigPath = expectDefined( healthEnv.OPENCLAW_CONFIG_PATH, "readiness config capture", ); expect(gatewayEnv.OPENCLAW_SKIP_CRON).toBe("1"); expect(gatewayEnv.OPENCLAW_SKIP_CHANNELS).toBe("1"); expect(benchmarkEnv.OPENCLAW_SKIP_CRON).toBe(""); expect(benchmarkEnv.OPENCLAW_SKIP_CHANNELS).toBe(""); expect(healthEnv.OPENCLAW_SKIP_CRON).toBe(""); expect(healthEnv.OPENCLAW_SKIP_CHANNELS).toBe(""); expect(benchmarkEnv.OPENCLAW_HOME).toBe(gatewayHome); expect(benchmarkEnv.OPENCLAW_STATE_DIR).toBe(gatewayState); expect(benchmarkEnv.OPENCLAW_CONFIG_PATH).toBe(gatewayConfigPath); expect(benchmarkEnv.OPENCLAW_GATEWAY_PORT).toBe(gatewayPort); expect(gatewayEnv.OPENCLAW_GATEWAY_PORT).toBe(gatewayPort); expect(healthHome).not.toBe(gatewayHome); expect(healthState).not.toBe(gatewayState); expect(healthConfigPath).not.toBe(gatewayConfigPath); expect(gatewayState).toBe(join(gatewayHome, ".openclaw")); expect(gatewayConfigPath).toBe(join(gatewayState, "openclaw.json")); expect(healthState).toBe(join(healthHome, ".openclaw")); expect(healthConfigPath).toBe(join(healthState, "openclaw.json")); expect(readFileSync(join(captureDir, "gateway.argv"), "utf8").trim().split("\n")).toEqual([ "dist/entry.js", "gateway", "run", "--bind", "loopback", "--port", gatewayPort, "--auth", "none", "--allow-unconfigured", "--force", ]); expect(readFileSync(join(captureDir, "health.argv"), "utf8")).toContain( `dist/entry.js\ngateway\nhealth\n--port\n${gatewayPort}\n`, ); expect(readFileSync(join(captureDir, "benchmark.argv"), "utf8")).toContain( "--import\ntsx\n/fixture/helpers/scripts/bench-cli-startup.ts\n", ); expect(readFileSync(join(captureDir, "gateway.terminated"), "utf8")).toBe("terminated\n"); expect(existsSync(gatewayHome)).toBe(false); expect(existsSync(healthHome)).toBe(false); } expect(targetStepIndex).toBeGreaterThanOrEqual(0); const precedingStepScopes = jobSteps.slice(0, targetStepIndex).flatMap((candidate, index) => { const stepLabel = `preceding step ${index + 1} (${candidate.name ?? "unnamed"})`; return [ [`${stepLabel} run`, candidate.run ?? ""], [`${stepLabel} env`, JSON.stringify(candidate.env ?? {})], [`${stepLabel} with`, JSON.stringify(candidate.with ?? {})], ] as const; }); const inheritedCronScopes = [ ["workflow env", JSON.stringify(workflow.env ?? {})], ["job env", JSON.stringify(job?.env ?? {})], ["current step env", JSON.stringify(step.env ?? {})], ["current step before harness", run.slice(0, sliceStart)], ...precedingStepScopes, [ "current step GITHUB_ENV writes", run .split("\n") .filter((line) => line.includes("GITHUB_ENV")) .join("\n"), ], ] as const; for (const [scope, text] of inheritedCronScopes) { expect(text, scope).not.toContain(cronEnv); } }); });