chore(pr): dev-wrapper opt-in — advisory subcommands may dogfood modified wrappers (#111656)

* chore(pr): dev-wrapper opt-in — advisory subcommands may dogfood modified wrappers

* fix(pr): hermetic dev-wrapper test fixture and type narrowing
This commit is contained in:
Peter Steinberger
2026-07-19 22:58:15 -07:00
committed by GitHub
parent 6e19c36d41
commit 104128dd32
3 changed files with 273 additions and 10 deletions
+210 -1
View File
@@ -1,6 +1,15 @@
// PR wrapper tests cover maintainer helper command delegation.
import { spawnSync } from "node:child_process";
import { chmodSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import {
chmodSync,
cpSync,
mkdirSync,
mkdtempSync,
readFileSync,
realpathSync,
rmSync,
writeFileSync,
} from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { describe, expect, it } from "vitest";
@@ -9,6 +18,126 @@ function readScript(path: string): string {
return readFileSync(path, "utf8");
}
const canonicalMismatchMessage = (repo: string) =>
[
"scripts/pr implementation differs between this worktree and the canonical checkout, and does not match origin/main.",
`Refusing to silently substitute canonical wrapper code from: ${repo}`,
"Run scripts/pr from a checkout whose wrapper matches the canonical checkout or a fetched origin/main.",
"",
].join("\n");
function makeMismatchedWrapperRepo() {
const root = realpathSync(mkdtempSync(join(realpathSync(tmpdir()), "openclaw-pr-dev-wrapper-")));
const home = join(root, "home");
const canonicalPath = join(root, "canonical");
const linkedPath = join(root, "linked");
const originPath = join(root, "origin.git");
mkdirSync(home, { recursive: true });
const fixtureEnv = {
...process.env,
GIT_CONFIG_GLOBAL: "/dev/null",
GIT_CONFIG_NOSYSTEM: "1",
HOME: home,
XDG_CONFIG_HOME: join(home, ".config"),
};
const git = (cwd: string, args: string[]) => {
const result = spawnSync("git", args, {
cwd,
encoding: "utf8",
env: fixtureEnv,
stdio: "pipe",
});
expect(result.status, `git ${args.join(" ")}\n${result.stderr}`).toBe(0);
return result;
};
git(root, ["init", "--bare", "-b", "main", originPath]);
git(root, ["init", "-b", "main", canonicalPath]);
const canonical = realpathSync(canonicalPath);
const origin = realpathSync(originPath);
mkdirSync(join(canonical, "scripts", "lib"), { recursive: true });
cpSync("scripts/pr-lib", join(canonical, "scripts", "pr-lib"), { recursive: true });
writeFileSync(join(canonical, "scripts", "pr"), readScript("scripts/pr"));
writeFileSync(
join(canonical, "scripts", "lib", "plain-gh.sh"),
"resolve_plain_gh_bin() { printf '/usr/bin/true\\n'; }\ngh_plain() { :; }\n",
);
chmodSync(join(canonical, "scripts", "pr"), 0o755);
git(canonical, ["config", "user.name", "OpenClaw Test"]);
git(canonical, ["config", "user.email", "test@example.invalid"]);
git(canonical, ["config", "commit.gpgSign", "false"]);
git(canonical, ["config", "core.hooksPath", "/dev/null"]);
git(canonical, ["remote", "add", "origin", origin]);
git(canonical, ["add", "scripts"]);
git(canonical, ["commit", "-m", "test: canonical wrapper"]);
git(canonical, ["push", "-u", "origin", "main"]);
git(canonical, ["worktree", "add", "-b", "feature", linkedPath, "main"]);
const linked = realpathSync(linkedPath);
git(linked, ["config", "user.name", "OpenClaw Test"]);
git(linked, ["config", "user.email", "test@example.invalid"]);
git(linked, ["config", "commit.gpgSign", "false"]);
expect(git(linked, ["rev-parse", "refs/remotes/origin/main"]).stdout.trim()).toBe(
git(canonical, ["rev-parse", "main"]).stdout.trim(),
);
writeFileSync(
join(linked, "scripts", "pr-lib", "gates.sh"),
'ci_dispatch() { echo "local wrapper executed"; }\n',
);
git(linked, ["add", "scripts/pr-lib/gates.sh"]);
git(linked, ["commit", "-m", "test: local wrapper"]);
const localRevision = git(linked, ["rev-parse", "HEAD"]).stdout.trim();
return {
canonical,
cleanup: () => rmSync(root, { recursive: true, force: true }),
env: fixtureEnv,
linked,
localRevision,
};
}
function parseSubcommandClassifications(script: string): Map<string, string> {
const start = script.indexOf("# PR_SUBCOMMAND_CLASSIFICATIONS_BEGIN");
const end = script.indexOf("# PR_SUBCOMMAND_CLASSIFICATIONS_END");
expect(start).toBeGreaterThanOrEqual(0);
expect(end).toBeGreaterThan(start);
const table = script.slice(start, end);
const classifications = new Map<string, string>();
const armPattern = /^\s+([^\n)]+)\)\s*\n\s+printf '(landing|advisory)\\n'/gm;
for (const match of table.matchAll(armPattern)) {
const commandGroup = match[1];
const classification = match[2];
if (commandGroup === undefined || classification === undefined) {
throw new Error("classification regexp returned incomplete captures");
}
for (const command of commandGroup.split("|").map((value) => value.trim())) {
classifications.set(command, classification);
}
}
return classifications;
}
function parseDispatchedSubcommands(script: string): string[] {
const start = script.lastIndexOf(' case "$cmd" in');
expect(start).toBeGreaterThanOrEqual(0);
const end = script.indexOf("\n esac", start);
expect(end).toBeGreaterThan(start);
const commands: string[] = [];
const armPattern = /^\s{4}([^\n)]+)\)/gm;
for (const match of script.slice(start, end).matchAll(armPattern)) {
const commandGroup = match[1];
if (commandGroup === undefined) {
throw new Error("dispatch regexp returned an incomplete capture");
}
commands.push(...commandGroup.split("|").map((value) => value.trim()));
}
return commands.filter((command) => command !== "*");
}
describe("scripts/pr wrappers", () => {
it("keeps the main PR helper usage and command table aligned", () => {
const script = readScript("scripts/pr");
@@ -30,6 +159,86 @@ describe("scripts/pr wrappers", () => {
expect(script).toContain("only support PRs targeting main");
});
it("classifies every dispatched subcommand", () => {
const script = readScript("scripts/pr");
const classifications = parseSubcommandClassifications(script);
const dispatched = parseDispatchedSubcommands(script);
expect([...classifications.keys()].sort()).toEqual([...dispatched, "lock-recover"].sort());
expect(classifications.get("ls")).toBe("advisory");
expect(classifications.get("ci-dispatch")).toBe("advisory");
for (const command of dispatched.filter((value) => !["ls", "ci-dispatch"].includes(value))) {
expect(classifications.get(command), command).toBe("landing");
}
});
it("runs a mismatched advisory wrapper locally with an explicit developer opt-in", () => {
const fixture = makeMismatchedWrapperRepo();
try {
const cliResult = spawnSync(
join(fixture.linked, "scripts", "pr"),
["--dev-wrapper", "ci-dispatch", "123"],
{
cwd: fixture.linked,
encoding: "utf8",
env: fixture.env,
},
);
expect(cliResult.status, cliResult.stderr).toBe(0);
expect(cliResult.stdout).toContain("local wrapper executed");
expect(cliResult.stderr).toContain(
`WARNING: running local scripts/pr revision ${fixture.localRevision} via dev-wrapper opt-in.`,
);
expect(cliResult.stderr).toContain("subcommand 'ci-dispatch' is classified advisory.");
expect(cliResult.stderr).toContain("landing subcommands remain refused");
const envResult = spawnSync(join(fixture.linked, "scripts", "pr"), ["ci-dispatch", "123"], {
cwd: fixture.linked,
encoding: "utf8",
env: { ...fixture.env, OPENCLAW_PR_DEV_WRAPPER: "1" },
});
expect(envResult.status, envResult.stderr).toBe(0);
expect(envResult.stdout).toContain("local wrapper executed");
expect(envResult.stderr).toContain("subcommand 'ci-dispatch' is classified advisory.");
} finally {
fixture.cleanup();
}
});
it("keeps the existing mismatch refusal for advisory commands without opt-in", () => {
const fixture = makeMismatchedWrapperRepo();
try {
const result = spawnSync(join(fixture.linked, "scripts", "pr"), ["ci-dispatch", "123"], {
cwd: fixture.linked,
encoding: "utf8",
env: fixture.env,
});
expect(result.status).toBe(1);
expect(result.stderr).toBe(canonicalMismatchMessage(fixture.canonical));
} finally {
fixture.cleanup();
}
});
it("refuses developer opt-in for a mismatched landing command", () => {
const fixture = makeMismatchedWrapperRepo();
try {
const result = spawnSync(
join(fixture.linked, "scripts", "pr"),
["--dev-wrapper", "prepare-run", "123"],
{ cwd: fixture.linked, encoding: "utf8", env: fixture.env },
);
expect(result.status).toBe(1);
expect(result.stderr).toContain(
"subcommand 'prepare-run' is classified landing; dev-wrapper opt-in is unavailable.",
);
expect(result.stderr).toContain(canonicalMismatchMessage(fixture.canonical).trim());
expect(result.stdout).not.toContain("local wrapper executed");
} finally {
fixture.cleanup();
}
});
it("keeps merge wrapper modes delegated to the main PR helper", () => {
const script = readScript("scripts/pr-merge");