Files
openclaw/test/scripts/pr-ci-dispatch.test.ts
Peter Steinberger 84e54ab264 chore(pr): landing-UX — aggregated validation, lock classes, ci-dispatch, committer guard (#111287)
* chore(pr): landing-UX — aggregate artifact validation, lock classes, ci-dispatch, committer worktree guard

* fix(pr): CI lane cleanup for landing-ux — lint causes, hermetic tests, private helpers

* chore(deadcode): register pr-lib CLI entries in knip config
2026-07-19 02:09:55 -07:00

142 lines
5.0 KiB
TypeScript

import { spawnSync } from "node:child_process";
import { chmodSync, existsSync, readFileSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import { useAutoCleanupTempDirTracker } from "../helpers/temp-dir.js";
const tempDirs = useAutoCleanupTempDirTracker(afterEach);
const dispatchScript = join(process.cwd(), "scripts/pr-lib/ci-dispatch.mjs");
const sha = "0123456789abcdef0123456789abcdef01234567";
const changedSha = "fedcba9876543210fedcba9876543210fedcba98";
const describePosix = process.platform === "win32" ? describe.skip : describe;
function createFakeGh() {
const tempDir = tempDirs.make("openclaw-pr-ci-dispatch-");
const fakeGh = join(tempDir, "gh");
const calls = join(tempDir, "calls.log");
const dispatched = join(tempDir, "dispatched");
const seenRunList = join(tempDir, "seen-run-list");
writeFileSync(
fakeGh,
`#!/usr/bin/env bash
set -euo pipefail
printf '%s\\n' "$*" >> "$OPENCLAW_TEST_GH_CALLS"
case "$1 $2" in
"run list")
if [ "\${OPENCLAW_TEST_GH_MODE:-}" = "pending-head-change" ]; then
printf '[]\\n'
elif [ -e "$OPENCLAW_TEST_GH_SEEN_RUN_LIST" ]; then
printf '[{"databaseId":99,"url":"https://github.com/openclaw/openclaw/actions/runs/99","headSha":"%s","createdAt":"2026-01-01T00:00:00Z","status":"queued"}]\\n' "$OPENCLAW_TEST_HEAD_SHA"
else
: > "$OPENCLAW_TEST_GH_SEEN_RUN_LIST"
printf '[]\\n'
fi
;;
"pr view")
if [ -e "$OPENCLAW_TEST_GH_DISPATCHED" ] && [ -n "\${OPENCLAW_TEST_GH_MODE:-}" ]; then
printf '%s\\n' "$OPENCLAW_TEST_CHANGED_HEAD_SHA"
else
printf '%s\\n' "$OPENCLAW_TEST_HEAD_SHA"
fi
;;
"workflow run") : > "$OPENCLAW_TEST_GH_DISPATCHED" ;;
*) echo "unexpected gh invocation: $*" >&2; exit 2 ;;
esac
`,
);
chmodSync(fakeGh, 0o755);
return { calls, dispatched, fakeGh, seenRunList };
}
function runDispatch(
fakeGh: ReturnType<typeof createFakeGh>,
options: {
mode?: "observed-head-change" | "pending-head-change";
immediateTimers?: boolean;
} = {},
) {
let nodeOptions = process.env.NODE_OPTIONS ?? "";
if (options.immediateTimers) {
const preload = join(tempDirs.make("openclaw-pr-ci-dispatch-timers-"), "immediate-timers.cjs");
writeFileSync(preload, "global.setTimeout = (callback) => { callback(); return 0; };\n");
nodeOptions = `${nodeOptions} --require ${preload}`.trim();
}
return spawnSync(
process.execPath,
[dispatchScript, "12345", "contributor/fix-hosted-gates", sha, "false"],
{
encoding: "utf8",
env: {
...process.env,
NODE_OPTIONS: nodeOptions,
OPENCLAW_GH_BIN: fakeGh.fakeGh,
OPENCLAW_TEST_CHANGED_HEAD_SHA: changedSha,
OPENCLAW_TEST_GH_CALLS: fakeGh.calls,
OPENCLAW_TEST_GH_DISPATCHED: fakeGh.dispatched,
OPENCLAW_TEST_GH_MODE: options.mode ?? "",
OPENCLAW_TEST_GH_SEEN_RUN_LIST: fakeGh.seenRunList,
OPENCLAW_TEST_HEAD_SHA: sha,
},
},
);
}
describePosix("scripts/pr ci-dispatch", () => {
it("dispatches the exact CI workflow for the remote PR head", () => {
const fakeGh = createFakeGh();
const result = runDispatch(fakeGh);
expect(result.status, `${result.stdout}\n${result.stderr}`).toBe(0);
expect(result.stdout).toContain(
"observed_run_url=https://github.com/openclaw/openclaw/actions/runs/99",
);
expect(readFileSync(fakeGh.calls, "utf8")).toContain(
`workflow run ci.yml --ref contributor/fix-hosted-gates -f target_ref=${sha} -f release_gate=true -f pull_request_number=12345`,
);
});
it("refuses a fork-local branch name before invoking GitHub", () => {
const fakeGh = createFakeGh();
const result = spawnSync(
process.execPath,
[dispatchScript, "12345", "fix-hosted-gates", sha, "true"],
{
encoding: "utf8",
env: {
...process.env,
OPENCLAW_GH_BIN: fakeGh.fakeGh,
OPENCLAW_TEST_GH_CALLS: fakeGh.calls,
OPENCLAW_TEST_GH_DISPATCHED: fakeGh.dispatched,
OPENCLAW_TEST_GH_SEEN_RUN_LIST: fakeGh.seenRunList,
OPENCLAW_TEST_HEAD_SHA: sha,
},
},
);
expect(result.status).not.toBe(0);
expect(`${result.stdout}\n${result.stderr}`).toMatch(/comes from a fork/u);
expect(existsSync(fakeGh.calls)).toBe(false);
});
it("fails closed if the remote head changes while CI run indexing is pending", () => {
const result = runDispatch(createFakeGh(), {
immediateTimers: true,
mode: "pending-head-change",
});
expect(result.status).not.toBe(0);
expect(`${result.stdout}\n${result.stderr}`).toMatch(
/head changed while CI dispatch was being indexed/u,
);
});
it("rechecks the remote head before returning an observed exact-SHA run", () => {
const result = runDispatch(createFakeGh(), { mode: "observed-head-change" });
expect(result.status).not.toBe(0);
expect(`${result.stdout}\n${result.stderr}`).toMatch(
/head changed before an exact-SHA CI run became visible/u,
);
});
});