openclaw/test/scripts/pr-git-maintenance.test.ts
Peter Steinberger 68663e6c64
fix(pr): unify worktree source checks and suppress automatic repacks (#150399)
Retain the supervised maintenance fix from #150399 and consolidate the
shared eager-import scanner, cold fixture, and duplicate-inventory guard
from #150465. Keep inline type dependency proof tied to real Node execution.

Source contributions: 3d1929b27385d8f86f09ce639395d67758c955b6 and
be6baf02f277466c75e4faef543a42c698b82dc1.
2026-09-16 21:50:35 -07:00

222 lines
8.9 KiB
TypeScript

import { execFileSync, spawn, spawnSync } from "node:child_process";
import {
chmodSync,
existsSync,
mkdirSync,
readFileSync,
realpathSync,
utimesSync,
writeFileSync,
} from "node:fs";
import { join } from "node:path";
import { afterEach, describe, expect, it, vi } from "vitest";
import { useAutoCleanupTempDirTracker } from "../helpers/temp-dir.js";
const tempDirs = useAutoCleanupTempDirTracker(afterEach);
const runner = join(process.cwd(), "scripts/pr-lib/process-group-runner.mjs");
const lockScript = join(process.cwd(), "scripts/pr-lib/operation-lock.sh");
const lockRef = "refs/openclaw/pr-operation-locks/42";
const describePosix = process.platform === "win32" ? describe.skip : describe;
function shellQuote(value: string) {
return `'${value.replaceAll("'", "'\\''")}'`;
}
function createMaintenanceFixture(command: string, exitCode: number) {
const root = realpathSync(tempDirs.make("openclaw-pr-git-maintenance-"));
const home = join(root, "home");
const repo = join(root, "repo");
mkdirSync(home);
mkdirSync(repo);
const env = {
PATH: process.env.PATH,
HOME: home,
XDG_CONFIG_HOME: join(home, ".config"),
GIT_CONFIG_GLOBAL: "/dev/null",
GIT_CONFIG_NOSYSTEM: "1",
GIT_AUTHOR_NAME: "OpenClaw Test",
GIT_AUTHOR_EMAIL: "test@example.invalid",
GIT_COMMITTER_NAME: "OpenClaw Test",
GIT_COMMITTER_EMAIL: "test@example.invalid",
GIT_TERMINAL_PROMPT: "0",
GIT_CONFIG_COUNT: "1",
GIT_CONFIG_KEY_0: "test.fromCount",
GIT_CONFIG_VALUE_0: "preserved count value",
GIT_CONFIG_PARAMETERS:
"'maintenance.auto=true' 'gc.auto=1' 'maintenance.autoDetach=true' 'gc.autoDetach=true' 'test.fromParameters=value with spaces=kept'",
};
const git = (args: string[], input?: string) =>
execFileSync("git", args, { cwd: repo, env, input, encoding: "utf8" }).trim();
git(["init", "-q", "-b", "main"]);
writeFileSync(join(repo, "base.txt"), "synthetic maintenance fixture\n");
git(["add", "base.txt"]);
git(["-c", "commit.gpgSign=false", "commit", "-qm", "base"]);
git(["remote", "add", "origin", repo]);
git(["fetch", "-q", "origin", "main"]);
// Two tiny packs make auto-GC eligible; old unreachable objects exercise
// the documented recentObjectsHook after maintenance's daemonization point.
for (let index = 0; index < 2; index++) {
const oid = git(["hash-object", "-w", "--stdin"], `unreachable fixture ${index}\n`);
const packed = git(["pack-objects", join(repo, ".git/objects/pack/pack")], `${oid}\n`);
for (const extension of ["pack", "idx"]) {
utimesSync(join(repo, `.git/objects/pack/pack-${packed}.${extension}`), 1, 1);
}
utimesSync(join(repo, ".git/objects", oid.slice(0, 2), oid.slice(2)), 1, 1);
}
git(["config", "maintenance.strategy", "gc"]);
git(["config", "gc.auto", "1"]);
git(["config", "gc.autoPackLimit", "1"]);
const ready = join(root, "maintenance-ready.json");
const release = join(root, "release-maintenance");
const completed = join(root, "maintenance-completed");
const hook = join(root, "recent-objects.mjs");
writeFileSync(
hook,
[
'import { execFileSync } from "node:child_process";',
'import { existsSync, fstatSync, renameSync, writeFileSync } from "node:fs";',
`const ready = ${JSON.stringify(ready)};`,
`const release = ${JSON.stringify(release)};`,
"if (!existsSync(ready)) {",
' const pgid = Number(execFileSync("ps", ["-o", "pgid=", "-p", String(process.pid)], { encoding: "utf8" }).trim());',
" const notifierOpen = fstatSync(3).isSocket() || fstatSync(3).isFIFO();",
' writeFileSync(ready + ".pending", JSON.stringify({ pgid, notifierOpen }));',
' renameSync(ready + ".pending", ready);',
" const deadline = Date.now() + 10_000;",
" while (!existsSync(release)) {",
' if (Date.now() >= deadline) throw new Error("maintenance gate was not released");',
" await new Promise(resolve => setTimeout(resolve, 10));",
" }",
` writeFileSync(${JSON.stringify(completed)}, "complete\\n");`,
"}",
].join("\n"),
);
git(["config", "gc.recentObjectsHook", `${shellQuote(process.execPath)} ${shellQuote(hook)}`]);
const script = join(root, "operation.sh");
writeFileSync(
script,
[
"#!/usr/bin/env bash",
"set -euo pipefail",
`source ${shellQuote(lockScript)}`,
`repo_root() { printf '%s\\n' ${shellQuote(repo)}; }`,
"acquire_pr_operation_lock 42",
"begin_pr_operation_validation_phase",
"mark_pr_operation_side_effects_started",
'test "$(git config --get test.fromCount)" = "preserved count value"',
'test "$(git config --get test.fromParameters)" = "value with spaces=kept"',
command,
`exit ${exitCode}`,
].join("\n"),
);
chmodSync(script, 0o755);
return { repo, env, git, script, ready, release, completed };
}
describePosix("scripts/pr Git maintenance ownership", () => {
it.each(["git fetch origin main", "git gc --auto"])(
"suppresses automatic maintenance during %s",
(command) => {
const fixture = createMaintenanceFixture(command, 0);
const config = readFileSync(join(fixture.repo, ".git/config"), "utf8");
writeFileSync(fixture.release, "release\n");
const result = spawnSync(process.execPath, [runner, fixture.repo, fixture.script], {
cwd: fixture.repo,
env: fixture.env,
encoding: "utf8",
timeout: 10_000,
});
expect(result.status, result.stderr).toBe(0);
expect(existsSync(fixture.ready)).toBe(false);
expect(existsSync(fixture.completed)).toBe(false);
expect(
spawnSync("git", ["show-ref", "--verify", "--quiet", lockRef], {
cwd: fixture.repo,
env: fixture.env,
}).status,
).toBe(1);
expect(readFileSync(join(fixture.repo, ".git/config"), "utf8")).toBe(config);
expect(fixture.git(["config", "--bool", "maintenance.auto"])).toBe("true");
expect(fixture.git(["config", "--int", "gc.auto"])).toBe("1");
},
);
it.each([
{ command: "git gc", exitCode: 0 },
{ command: "git gc", exitCode: 7 },
])(
"joins $command before operation completion (exit $exitCode)",
async ({ command, exitCode }) => {
const fixture = createMaintenanceFixture(command, exitCode);
const child = spawn(process.execPath, [runner, fixture.repo, fixture.script], {
cwd: fixture.repo,
env: fixture.env,
stdio: ["ignore", "pipe", "pipe"],
});
let stderr = "";
child.stdout.resume();
child.stderr.setEncoding("utf8");
child.stderr.on("data", (chunk: string) => {
stderr += chunk;
});
const closed = new Promise<number | null>((resolve) => {
child.once("close", resolve);
});
let ownerOid = "";
try {
await vi.waitFor(() => expect(existsSync(fixture.ready), stderr).toBe(true), {
timeout: 5000,
});
ownerOid = fixture.git(["rev-parse", lockRef]);
const owner = fixture.git(["cat-file", "blob", ownerOid]);
const pgid = Number(/^pgid=(\d+)$/m.exec(owner)?.[1]);
const hookState = JSON.parse(readFileSync(fixture.ready, "utf8")) as {
pgid: number;
notifierOpen: boolean;
};
expect(hookState.notifierOpen).toBe(true);
expect(hookState.pgid).toBe(pgid);
expect(child.exitCode).toBeNull();
expect(existsSync(fixture.completed)).toBe(false);
// A second operation cannot acquire the exact PR while maintenance is held.
const probe = spawn(
"bash",
[
"-c",
`source ${shellQuote(lockScript)}; repo_root() { printf '%s\\n' ${shellQuote(fixture.repo)}; }; try_acquire_pr_operation_lock 42`,
],
{ cwd: fixture.repo, env: fixture.env, detached: true, stdio: "ignore" },
);
const probeCode = await new Promise<number | null>((resolve) => {
probe.once("close", resolve);
});
expect(probeCode).toBe(1);
expect(fixture.git(["rev-parse", lockRef])).toBe(ownerOid);
} finally {
// The gate belongs to this fixture. Let Git finish and join it; no PID-based cleanup.
writeFileSync(fixture.release, "release\n");
await closed;
}
expect(child.exitCode, stderr).toBe(exitCode);
expect(existsSync(fixture.completed)).toBe(true);
expect(stderr).not.toContain("drain deadline");
expect(stderr).not.toContain("process group remained active");
if (exitCode === 0) {
expect(
spawnSync("git", ["show-ref", "--verify", "--quiet", lockRef], {
cwd: fixture.repo,
env: fixture.env,
}).status,
).toBe(1);
} else {
expect(fixture.git(["rev-parse", lockRef])).toBe(ownerOid);
expect(stderr).toContain(`reason: child exited with code ${exitCode}`);
}
expect(fixture.git(["config", "--bool", "maintenance.autoDetach"])).toBe("true");
expect(fixture.git(["config", "--bool", "gc.autoDetach"])).toBe("true");
},
);
});