fix(gateway): prevent control-plane polling stalls (#124891)

* fix(gateway): avoid repeated control-plane scans

* fix(tooling): allow concurrent worktree validation

* fix(ci): refresh protocol and runner inputs

* perf(ui): defer hidden session refreshes

* fix(ui): resolve session refresh lint failure

* fix(update): preserve pre-cache update channel

* fix(update): normalize cached update channel

* fix(gateway): lifecycle-cache update install identity

* fix(ui): preserve manual history retry after layout scroll

* test(codex): repair side-question tool schema fixture
This commit is contained in:
Peter Steinberger
2026-08-16 20:58:27 -07:00
committed by GitHub
parent d24cb13320
commit bf9b25ab7e
46 changed files with 1082 additions and 1904 deletions
-49
View File
@@ -21,7 +21,6 @@ import {
changedCheckLocalDependenciesReady,
changedCheckRequiresRemote,
cleanupCorepackPnpmShimDir,
createChangedCheckChildEnv,
createChangedCheckPlan,
createPnpmManagedCommand,
createTargetedCoreLintCommand,
@@ -740,9 +739,6 @@ describe("scripts/changed-lanes", () => {
expect(plan.commands.map((command) => command.args[0])).toContain("tsgo:core:test");
expect(plan.commands.find((command) => command.args[0] === "tsgo:core")?.env).toEqual({
PATH: "/usr/bin",
OPENCLAW_OXLINT_SKIP_LOCK: "1",
OPENCLAW_TEST_HEAVY_CHECK_LOCK_HELD: "1",
OPENCLAW_TSGO_HEAVY_CHECK_LOCK_HELD: "1",
OPENCLAW_TSGO_SPARSE_SKIP: "1",
});
expect(plan.commands.find((command) => command.name === "lint core changed file")).toEqual({
@@ -756,9 +752,6 @@ describe("scripts/changed-lanes", () => {
],
env: {
PATH: "/usr/bin",
OPENCLAW_OXLINT_SKIP_LOCK: "1",
OPENCLAW_TEST_HEAVY_CHECK_LOCK_HELD: "1",
OPENCLAW_TSGO_HEAVY_CHECK_LOCK_HELD: "1",
},
});
});
@@ -1074,23 +1067,11 @@ describe("scripts/changed-lanes", () => {
expect(plan.commands.find((command) => command.args[0] === "tsgo:core")?.env).toEqual({
OPENCLAW_LOCAL_CHECK: "1",
OPENCLAW_OXLINT_SKIP_LOCK: "1",
OPENCLAW_TEST_HEAVY_CHECK_LOCK_HELD: "1",
OPENCLAW_TSGO_HEAVY_CHECK_LOCK_HELD: "1",
OPENCLAW_TSGO_SPARSE_SKIP: "1",
PATH: "/usr/bin",
});
});
it("marks changed-check children as covered by the parent heavy-check lock", () => {
expect(createChangedCheckChildEnv({ PATH: "/usr/bin" })).toEqual({
OPENCLAW_OXLINT_SKIP_LOCK: "1",
OPENCLAW_TEST_HEAVY_CHECK_LOCK_HELD: "1",
OPENCLAW_TSGO_HEAVY_CHECK_LOCK_HELD: "1",
PATH: "/usr/bin",
});
});
it("runs CI changed-check children through Corepack pnpm", () => {
const command = createPnpmManagedCommand(
{ name: "conflict markers", args: ["check:no-conflict-markers"] },
@@ -1331,36 +1312,6 @@ describe("scripts/changed-lanes", () => {
).toBe(false);
});
it("runs changed-check lint lanes under the parent heavy-check lock", () => {
const result = detectChangedLanes(["extensions/lmstudio/src/api.ts"]);
const plan = createChangedCheckPlan(result, { env: { PATH: "/usr/bin" } });
const lintCommand = plan.commands.find(
(command) => command.name === "lint extension changed file",
);
expect(lintCommand?.env).toEqual({
OPENCLAW_OXLINT_SKIP_LOCK: "1",
OPENCLAW_TEST_HEAVY_CHECK_LOCK_HELD: "1",
OPENCLAW_TSGO_HEAVY_CHECK_LOCK_HELD: "1",
PATH: "/usr/bin",
});
});
it("runs changed-check app tests under the parent heavy-check lock", () => {
const result = detectChangedLanes([
"apps/shared/OpenClawKit/Sources/OpenClawProtocol/GatewayModels.swift",
]);
const plan = createChangedCheckPlan(result, { env: { PATH: "/usr/bin" } });
const testCommand = plan.commands.find((command) => command.args[0] === "test:macos:ci");
expect(testCommand?.env).toEqual({
OPENCLAW_OXLINT_SKIP_LOCK: "1",
OPENCLAW_TEST_HEAVY_CHECK_LOCK_HELD: "1",
OPENCLAW_TSGO_HEAVY_CHECK_LOCK_HELD: "1",
PATH: "/usr/bin",
});
});
it.each([
{
name: "routes core test-only changes to core test lanes only",
@@ -110,9 +110,6 @@ describe("scripts/ci-run-node-test-shard.mts", () => {
expect(childEnv.IGNORED).toBeUndefined();
expect(childEnv.OPENCLAW_VITEST_SHARD_NAME).toBe("g");
expect(childEnv.OPENCLAW_TEST_PROJECTS_PARALLEL).toBe("1");
expect(childEnv.OPENCLAW_OXLINT_SKIP_LOCK).toBe("1");
expect(childEnv.OPENCLAW_TEST_HEAVY_CHECK_LOCK_HELD).toBe("1");
expect(childEnv.OPENCLAW_TSGO_HEAVY_CHECK_LOCK_HELD).toBe("1");
expect(childEnv.OPENCLAW_VITEST_FS_MODULE_CACHE_PATH).toBe(
path.join(scratchDir, "vitest-cache-3"),
);
@@ -1,20 +1,16 @@
// Local Heavy Check Runtime tests cover local heavy check runtime script behavior.
import { execFileSync, spawnSync } from "node:child_process";
// Local Check Runtime tests cover local check runtime script behavior.
import { spawnSync } from "node:child_process";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { describe, expect, it } from "vitest";
import {
acquireLocalHeavyCheckLockSync,
applyLocalOxlintPolicy,
applyLocalTsgoPolicy,
ensureRepoToolNodeModulesLink,
resolveLocalHeavyCheckEnv,
resolveLocalCheckEnv,
resolveRepoToolBinPath,
shouldAcquireLocalHeavyCheckLockForOxlint,
shouldAcquireLocalHeavyCheckLockForTsgo,
withLocalHeavyCheckLockHeld,
} from "../../scripts/lib/local-heavy-check-runtime.mts";
} from "../../scripts/lib/local-check-runtime.mts";
import { createScriptTestHarness } from "./test-helpers.js";
const { createTempDir } = createScriptTestHarness();
@@ -43,19 +39,7 @@ function makeEnv(overrides: Record<string, string | undefined> = {}) {
return env;
}
describe("local-heavy-check-runtime", () => {
it("marks every nested heavy-check wrapper as covered by the parent lock", () => {
const baseEnv = { BASE: "1" };
expect(withLocalHeavyCheckLockHeld(baseEnv)).toEqual({
BASE: "1",
OPENCLAW_OXLINT_SKIP_LOCK: "1",
OPENCLAW_TEST_HEAVY_CHECK_LOCK_HELD: "1",
OPENCLAW_TSGO_HEAVY_CHECK_LOCK_HELD: "1",
});
expect(baseEnv).toEqual({ BASE: "1" });
});
describe("local-check-runtime", () => {
it("resolves repo tools from the primary checkout for dependency-less worktrees", () => {
const primaryRoot = createTempDir("openclaw-primary-checkout-");
const cwd = path.join(primaryRoot, ".codex", "worktrees", "task", "openclaw");
@@ -124,12 +108,12 @@ describe("local-heavy-check-runtime", () => {
expect(fs.lstatSync(localNodeModules).isSymbolicLink()).toBe(false);
});
it("reenables local heavy-check policy for local wrapper entrypoints", () => {
expect(resolveLocalHeavyCheckEnv({ OPENCLAW_LOCAL_CHECK: "0", PATH: "/usr/bin" })).toEqual({
it("reenables local check policy for local wrapper entrypoints", () => {
expect(resolveLocalCheckEnv({ OPENCLAW_LOCAL_CHECK: "0", PATH: "/usr/bin" })).toEqual({
OPENCLAW_LOCAL_CHECK: "1",
PATH: "/usr/bin",
});
expect(resolveLocalHeavyCheckEnv({ OPENCLAW_LOCAL_CHECK: "false", PATH: "/usr/bin" })).toEqual({
expect(resolveLocalCheckEnv({ OPENCLAW_LOCAL_CHECK: "false", PATH: "/usr/bin" })).toEqual({
OPENCLAW_LOCAL_CHECK: "1",
PATH: "/usr/bin",
});
@@ -137,7 +121,7 @@ describe("local-heavy-check-runtime", () => {
it("preserves local-check disablement in CI", () => {
expect(
resolveLocalHeavyCheckEnv({
resolveLocalCheckEnv({
CI: "true",
OPENCLAW_LOCAL_CHECK: "0",
PATH: "/usr/bin",
@@ -306,29 +290,6 @@ describe("local-heavy-check-runtime", () => {
expect(env.GOMEMLIMIT).toBeUndefined();
});
it("skips the heavy-check lock for tsgo metadata commands", () => {
expect(shouldAcquireLocalHeavyCheckLockForTsgo(["--help"])).toBe(false);
expect(shouldAcquireLocalHeavyCheckLockForTsgo(["-h"])).toBe(false);
expect(shouldAcquireLocalHeavyCheckLockForTsgo(["--version"])).toBe(false);
expect(shouldAcquireLocalHeavyCheckLockForTsgo(["-v"])).toBe(false);
expect(shouldAcquireLocalHeavyCheckLockForTsgo(["--init"])).toBe(false);
expect(shouldAcquireLocalHeavyCheckLockForTsgo(["--showConfig"])).toBe(false);
});
it("keeps the heavy-check lock for real tsgo runs", () => {
expect(shouldAcquireLocalHeavyCheckLockForTsgo([])).toBe(true);
expect(shouldAcquireLocalHeavyCheckLockForTsgo(["--extendedDiagnostics"])).toBe(true);
});
it("allows forcing the tsgo lock back on", () => {
expect(
shouldAcquireLocalHeavyCheckLockForTsgo(
["--help"],
makeEnv({ OPENCLAW_TSGO_FORCE_LOCK: "1" }),
),
).toBe(true);
});
it("serializes local oxlint runs onto one thread on constrained hosts", () => {
const { args, env } = applyLocalOxlintPolicy([], makeEnv(), CONSTRAINED_HOST);
@@ -398,7 +359,6 @@ describe("local-heavy-check-runtime", () => {
CAPTURE_PATH: capturePath,
OPENCLAW_LOCAL_CHECK: "1",
OPENCLAW_LOCAL_CHECK_MODE: "throttled",
OPENCLAW_OXLINT_SKIP_LOCK: "1",
OPENCLAW_OXLINT_SKIP_PREPARE: "1",
};
delete env.GOMAXPROCS;
@@ -463,155 +423,4 @@ describe("local-heavy-check-runtime", () => {
expect(args).not.toContain("stylish");
},
);
it("skips the heavy-check lock for explicit oxlint file targets", () => {
const cwd = createTempDir("openclaw-oxlint-lock-skip-");
const target = path.join(cwd, "sample.ts");
fs.writeFileSync(target, "export const ok = true;\n", "utf8");
expect(
shouldAcquireLocalHeavyCheckLockForOxlint(["--type-aware", "--", "sample.ts"], { cwd }),
).toBe(false);
});
it("skips the heavy-check lock for oxlint metadata commands", () => {
expect(shouldAcquireLocalHeavyCheckLockForOxlint(["--help"])).toBe(false);
expect(shouldAcquireLocalHeavyCheckLockForOxlint(["-h"])).toBe(false);
expect(shouldAcquireLocalHeavyCheckLockForOxlint(["--version"])).toBe(false);
expect(shouldAcquireLocalHeavyCheckLockForOxlint(["-V"])).toBe(false);
expect(shouldAcquireLocalHeavyCheckLockForOxlint(["--rules"])).toBe(false);
expect(shouldAcquireLocalHeavyCheckLockForOxlint(["--print-config"])).toBe(false);
expect(shouldAcquireLocalHeavyCheckLockForOxlint(["--init"])).toBe(false);
});
it("keeps the heavy-check lock for directory targets and broad oxlint runs", () => {
const cwd = createTempDir("openclaw-oxlint-lock-keep-");
fs.mkdirSync(path.join(cwd, "src"), { recursive: true });
fs.writeFileSync(path.join(cwd, "src", "sample.ts"), "export const ok = true;\n", "utf8");
expect(shouldAcquireLocalHeavyCheckLockForOxlint(["--type-aware", "--", "src"], { cwd })).toBe(
true,
);
expect(shouldAcquireLocalHeavyCheckLockForOxlint(["--type-aware"], { cwd })).toBe(true);
});
it("allows forcing the oxlint lock back on", () => {
const cwd = createTempDir("openclaw-oxlint-lock-force-");
fs.writeFileSync(path.join(cwd, "sample.ts"), "export const ok = true;\n", "utf8");
expect(
shouldAcquireLocalHeavyCheckLockForOxlint(["--type-aware", "--", "sample.ts"], {
cwd,
env: makeEnv({ OPENCLAW_OXLINT_FORCE_LOCK: "1" }),
}),
).toBe(true);
});
it("reclaims stale local heavy-check locks from dead pids", () => {
const cwd = createTempDir("openclaw-local-heavy-check-");
const commonDir = path.join(cwd, ".git");
const lockDir = path.join(commonDir, "openclaw-local-checks", "heavy-check.lock");
fs.mkdirSync(lockDir, { recursive: true });
fs.writeFileSync(
path.join(lockDir, "owner.json"),
`${JSON.stringify({
pid: 999_999_999,
tool: "tsgo",
cwd,
})}\n`,
"utf8",
);
const release = acquireLocalHeavyCheckLockSync({
cwd,
env: makeEnv(),
toolName: "oxlint",
});
const owner = JSON.parse(fs.readFileSync(path.join(lockDir, "owner.json"), "utf8"));
expect(owner.pid).toBe(process.pid);
expect(owner.tool).toBe("oxlint");
release();
expect(fs.existsSync(lockDir)).toBe(false);
});
it("uses a worktree-local heavy-check lock when explicitly requested", () => {
const repoRoot = createTempDir("openclaw-local-heavy-check-worktree-");
execFileSync("git", ["init"], { cwd: repoRoot, stdio: "ignore" });
const cwd = path.join(repoRoot, "nested", "tooling");
fs.mkdirSync(cwd, { recursive: true });
const commonLockDir = path.join(repoRoot, ".git", "openclaw-local-checks", "heavy-check.lock");
const worktreeLockDir = path.join(
repoRoot,
".artifacts",
"openclaw-local-checks",
"heavy-check.lock",
);
const nestedLockDir = path.join(cwd, ".artifacts", "openclaw-local-checks", "heavy-check.lock");
const release = acquireLocalHeavyCheckLockSync({
cwd,
env: makeEnv({ OPENCLAW_HEAVY_CHECK_LOCK_SCOPE: "worktree" }),
toolName: "check:changed",
});
const owner = JSON.parse(fs.readFileSync(path.join(worktreeLockDir, "owner.json"), "utf8"));
expect(owner.tool).toBe("check:changed");
expect(fs.existsSync(worktreeLockDir)).toBe(true);
expect(fs.existsSync(commonLockDir)).toBe(false);
expect(fs.existsSync(nestedLockDir)).toBe(false);
release();
expect(fs.existsSync(worktreeLockDir)).toBe(false);
});
it("rejects malformed heavy-check lock timing env values", () => {
const cwd = createTempDir("openclaw-local-heavy-check-malformed-env-");
expect(() =>
acquireLocalHeavyCheckLockSync({
cwd,
env: makeEnv({ OPENCLAW_HEAVY_CHECK_LOCK_TIMEOUT_MS: "10ms" }),
toolName: "oxlint",
}),
).toThrow("OPENCLAW_HEAVY_CHECK_LOCK_TIMEOUT_MS must be a positive integer; got: 10ms");
expect(() =>
acquireLocalHeavyCheckLockSync({
cwd,
env: makeEnv({ OPENCLAW_HEAVY_CHECK_LOCK_POLL_MS: "0" }),
toolName: "oxlint",
}),
).toThrow("OPENCLAW_HEAVY_CHECK_LOCK_POLL_MS must be a positive integer; got: 0");
});
it("cleans up stale legacy test locks when acquiring the shared heavy-check lock", () => {
const cwd = createTempDir("openclaw-local-heavy-check-legacy-");
const commonDir = path.join(cwd, ".git");
const locksDir = path.join(commonDir, "openclaw-local-checks");
const legacyLockDir = path.join(locksDir, "test.lock");
const heavyCheckLockDir = path.join(locksDir, "heavy-check.lock");
fs.mkdirSync(legacyLockDir, { recursive: true });
fs.writeFileSync(
path.join(legacyLockDir, "owner.json"),
`${JSON.stringify({
pid: 999_999_999,
tool: "test",
cwd,
})}\n`,
"utf8",
);
const release = acquireLocalHeavyCheckLockSync({
cwd,
env: makeEnv(),
toolName: "oxlint",
});
expect(fs.existsSync(legacyLockDir)).toBe(false);
expect(fs.existsSync(heavyCheckLockDir)).toBe(true);
release();
expect(fs.existsSync(heavyCheckLockDir)).toBe(false);
});
});
+6 -239
View File
@@ -1,38 +1,18 @@
// Covers the scripts/pr prepare-gates remote testbox mode and the
// cross-worktree gate lock that serializes whole gate blocks.
import { type ChildProcess, spawn, spawnSync } from "node:child_process";
// Covers the scripts/pr prepare-gates remote testbox mode.
import { spawnSync } from "node:child_process";
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import { createTempDirTracker } from "../helpers/temp-dir.js";
const repoRoot = process.cwd();
const gateLockHelperPath = join(repoRoot, "scripts", "pr-gates-lock.mts");
const tempDirs = createTempDirTracker();
const children: ChildProcess[] = [];
function makeLockRepoDir(): string {
const dir = tempDirs.make("openclaw-pr-gates-lock-");
mkdirSync(join(dir, ".git"), { recursive: true });
return dir;
}
function heavyCheckLockDir(repoDir: string): string {
return join(repoDir, ".git", "openclaw-local-checks", "heavy-check.lock");
}
function sanitizedEnv(overrides: NodeJS.ProcessEnv = {}): NodeJS.ProcessEnv {
// check:changed and gate runs export these to children; drop ambient copies
// so lock and mode behavior under test only sees explicit overrides.
const env: NodeJS.ProcessEnv = { ...process.env };
delete env.OPENCLAW_PR_GATES_REMOTE;
delete env.OPENCLAW_TESTBOX;
delete env.OPENCLAW_TEST_HEAVY_CHECK_LOCK_HELD;
delete env.OPENCLAW_TSGO_HEAVY_CHECK_LOCK_HELD;
delete env.OPENCLAW_OXLINT_SKIP_LOCK;
delete env.OPENCLAW_HEAVY_CHECK_LOCK_TIMEOUT_MS;
delete env.OPENCLAW_HEAVY_CHECK_LOCK_POLL_MS;
return { ...env, ...overrides };
}
@@ -70,16 +50,6 @@ function runGatesBash(
);
}
function spawnGateLockHolder(repoDir: string, statusFile: string, env: NodeJS.ProcessEnv = {}) {
const child = spawn(process.execPath, [gateLockHelperPath, "--status-file", statusFile], {
cwd: repoDir,
stdio: ["ignore", "ignore", "pipe"],
env: sanitizedEnv(env),
});
children.push(child);
return child;
}
function makeRetryRepo(): { repoDir: string; stubBin: string; headSha: string } {
const dir = tempDirs.make("openclaw-pr-gates-retry-");
const repoDir = join(dir, "repo");
@@ -242,70 +212,7 @@ function prepareSyncHeadStubs(): string[] {
];
}
async function waitFor(predicate: () => boolean, timeoutMs: number): Promise<boolean> {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
if (predicate()) {
return true;
}
await new Promise((resolve) => {
setTimeout(resolve, 10);
});
}
return predicate();
}
async function waitForExit(child: ChildProcess): Promise<void> {
if (child.exitCode !== null || child.signalCode !== null) {
return;
}
await new Promise((resolve) => {
child.once("exit", resolve);
});
}
async function waitForStderr(
child: ChildProcess,
expected: string,
timeoutMs: number,
): Promise<string> {
const stderr = child.stderr;
if (!stderr) {
throw new Error("child stderr is not piped");
}
stderr.setEncoding("utf8");
let output = "";
return await new Promise<string>((resolve, reject) => {
const cleanup = () => {
clearTimeout(timeout);
stderr.off("data", onData);
child.off("exit", onExit);
};
const onData = (chunk: string) => {
output += chunk;
if (output.includes(expected)) {
cleanup();
resolve(output);
}
};
const onExit = () => {
cleanup();
reject(new Error(`child exited before writing ${JSON.stringify(expected)}: ${output}`));
};
const timeout = setTimeout(() => {
cleanup();
reject(new Error(`timed out waiting for ${JSON.stringify(expected)}: ${output}`));
}, timeoutMs);
stderr.on("data", onData);
child.once("exit", onExit);
});
}
afterEach(async () => {
for (const child of children.splice(0)) {
child.kill("SIGKILL");
await waitForExit(child);
}
afterEach(() => {
tempDirs.cleanup();
});
@@ -936,7 +843,6 @@ describe("prepare gate stamp transitions", () => {
"changelog_required_for_changed_files() { return 1; }",
"prepare_local_gate_workspace() { :; }",
"run_quiet_logged() { :; }",
"release_pr_gates_lock() { :; }",
"prepare_gates 4242",
"cat .local/gates.env",
].join("\n"),
@@ -994,91 +900,12 @@ describe("prepare gate stamp transitions", () => {
});
});
describe("pr-gates-lock helper", () => {
it("acquires the shared heavy-check lock and releases it on SIGTERM", async () => {
const repoDir = makeLockRepoDir();
const statusFile = join(repoDir, "status");
const holder = spawnGateLockHolder(repoDir, statusFile);
expect(await waitFor(() => existsSync(statusFile), 5_000)).toBe(true);
expect(existsSync(heavyCheckLockDir(repoDir))).toBe(true);
holder.kill("SIGTERM");
await waitForExit(holder);
expect(await waitFor(() => !existsSync(heavyCheckLockDir(repoDir)), 5_000)).toBe(true);
});
it("queues behind an existing holder and acquires after it exits", async () => {
const repoDir = makeLockRepoDir();
const firstStatus = join(repoDir, "status-first");
const secondStatus = join(repoDir, "status-second");
const first = spawnGateLockHolder(repoDir, firstStatus);
expect(await waitFor(() => existsSync(firstStatus), 5_000)).toBe(true);
const second = spawnGateLockHolder(repoDir, secondStatus, {
OPENCLAW_HEAVY_CHECK_LOCK_POLL_MS: "50",
});
await waitForStderr(second, "queued behind the local heavy-check lock", 5_000);
expect(existsSync(secondStatus)).toBe(false);
first.kill("SIGTERM");
await waitForExit(first);
expect(await waitFor(() => existsSync(secondStatus), 5_000)).toBe(true);
second.kill("SIGTERM");
await waitForExit(second);
expect(await waitFor(() => !existsSync(heavyCheckLockDir(repoDir)), 5_000)).toBe(true);
});
it("fails instead of holding when the wait times out", async () => {
const repoDir = makeLockRepoDir();
const lockDir = heavyCheckLockDir(repoDir);
mkdirSync(lockDir, { recursive: true });
// Owner pid must be alive or the helper reclaims the stale lock.
writeFileSync(
join(lockDir, "owner.json"),
`${JSON.stringify({ pid: process.pid, tool: "test-holder", cwd: repoDir })}\n`,
);
const statusFile = join(repoDir, "status");
const holder = spawnGateLockHolder(repoDir, statusFile, {
OPENCLAW_HEAVY_CHECK_LOCK_TIMEOUT_MS: "200",
OPENCLAW_HEAVY_CHECK_LOCK_POLL_MS: "50",
});
await waitForExit(holder);
expect(holder.exitCode).not.toBe(0);
expect(existsSync(statusFile)).toBe(false);
});
it("releases the lock when the parent process dies", async () => {
const repoDir = makeLockRepoDir();
const statusFile = join(repoDir, "status");
const parent = spawn(
"bash",
[
"-c",
`node '${gateLockHelperPath}' --status-file '${statusFile}' 2>/dev/null & ` +
`while [ ! -s '${statusFile}' ]; do sleep 0.05; done`,
],
{ cwd: repoDir, stdio: "ignore", env: sanitizedEnv() },
);
children.push(parent);
await waitForExit(parent);
expect(existsSync(statusFile)).toBe(true);
expect(await waitFor(() => !existsSync(heavyCheckLockDir(repoDir)), 8_000)).toBe(true);
});
});
describe("gates.sh gate lock plumbing", () => {
it("acquires the block lock before dependency bootstrap", () => {
describe("gates.sh local gate workspace", () => {
it("pins the worktree before dependency bootstrap", () => {
const result = runGatesBash(
[
"events=$(mktemp)",
'pin_worktree_bundled_plugins_dir() { echo pin >> "$events"; }',
'acquire_pr_gates_lock() { echo lock >> "$events"; }',
'bootstrap_deps_if_needed() { echo bootstrap >> "$events"; }',
"prepare_local_gate_workspace",
'cat "$events"',
@@ -1086,66 +913,6 @@ describe("gates.sh gate lock plumbing", () => {
);
expect(result.status).toBe(0);
expect(result.stdout.trim().split("\n")).toEqual(["pin", "lock", "bootstrap"]);
});
it("exports the held-lock contract while holding and clears it on release", () => {
const repoDir = makeLockRepoDir();
const result = runGatesBash(
[
"acquire_pr_gates_lock",
'echo "held=${OPENCLAW_TEST_HEAVY_CHECK_LOCK_HELD:-unset},${OPENCLAW_TSGO_HEAVY_CHECK_LOCK_HELD:-unset},${OPENCLAW_OXLINT_SKIP_LOCK:-unset}"',
"jq -r .tool .git/openclaw-local-checks/heavy-check.lock/owner.json",
"release_pr_gates_lock",
'echo "released=${OPENCLAW_TEST_HEAVY_CHECK_LOCK_HELD:-unset}"',
'[ -d .git/openclaw-local-checks/heavy-check.lock ] && echo "lock=held" || echo "lock=free"',
].join("\n"),
{ cwd: repoDir },
);
expect(result.status).toBe(0);
expect(result.stdout).toContain("held=1,1,1");
expect(result.stdout).toContain("pr-gates");
expect(result.stdout).toContain("released=unset");
expect(result.stdout).toContain("lock=free");
});
it("skips acquisition when a parent already holds the lock", () => {
const repoDir = makeLockRepoDir();
const result = runGatesBash(
[
"acquire_pr_gates_lock",
'[ -d .git/openclaw-local-checks/heavy-check.lock ] && echo "lock=held" || echo "lock=free"',
'echo "helper_pid=${PR_GATES_LOCK_PID:-none}"',
].join("\n"),
{ cwd: repoDir, env: { OPENCLAW_TEST_HEAVY_CHECK_LOCK_HELD: "1" } },
);
expect(result.status).toBe(0);
expect(result.stdout).toContain("lock=free");
expect(result.stdout).toContain("helper_pid=none");
});
it("fails the gate run when the lock wait times out", () => {
const repoDir = makeLockRepoDir();
const lockDir = heavyCheckLockDir(repoDir);
mkdirSync(lockDir, { recursive: true });
writeFileSync(
join(lockDir, "owner.json"),
`${JSON.stringify({ pid: process.pid, tool: "test-holder", cwd: repoDir })}\n`,
);
const result = runGatesBash("acquire_pr_gates_lock", {
cwd: repoDir,
env: {
OPENCLAW_HEAVY_CHECK_LOCK_TIMEOUT_MS: "200",
OPENCLAW_HEAVY_CHECK_LOCK_POLL_MS: "50",
},
});
expect(result.status).toBe(1);
expect(result.stdout).toContain(
"Failed to acquire the shared local heavy-check lock for prepare gates.",
);
expect(result.stdout.trim().split("\n")).toEqual(["pin", "bootstrap"]);
});
});
-18
View File
@@ -267,20 +267,6 @@ describe("run-oxlint", () => {
);
});
it("holds one parent heavy-check lock for sharded lint runs", () => {
const shardedLintRunner = readFileSync("scripts/run-oxlint-shards.mts", "utf8");
const skipLockIndex = shardedLintRunner.indexOf('env.OPENCLAW_OXLINT_SKIP_LOCK === "1"');
const lockIndex = shardedLintRunner.indexOf("acquireLocalHeavyCheckLockSync({");
const childSkipIndex = shardedLintRunner.indexOf('OPENCLAW_OXLINT_SKIP_LOCK: "1"');
expect(shardedLintRunner).toContain("resolveLocalHeavyCheckEnv");
expect(shardedLintRunner).toContain("shouldAcquireLocalHeavyCheckLockForOxlint");
expect(skipLockIndex).toBeGreaterThan(-1);
expect(lockIndex).toBeGreaterThan(-1);
expect(lockIndex).toBeGreaterThan(skipLockIndex);
expect(childSkipIndex).toBeGreaterThan(lockIndex);
});
it("serializes broad oxlint shards on constrained local hosts", () => {
expect(shouldSerializeShards({})).toBe(true);
});
@@ -624,16 +610,12 @@ describe("run-oxlint", () => {
encoding: "utf8",
env: {
...process.env,
OPENCLAW_HEAVY_CHECK_LOCK_SCOPE: "worktree",
OPENCLAW_LOCAL_CHECK: "1",
},
});
expect(result.status).toBe(1);
expect(result.stderr).not.toContain("[oxlint:");
expect(existsSync(join(tempDir, ".artifacts/openclaw-local-checks/heavy-check.lock"))).toBe(
false,
);
});
it("falls back to the full extension shard when Windows extension dirs are unavailable", () => {
+1 -1
View File
@@ -24,7 +24,7 @@ describe("run-tsgo sparse guard", () => {
{
cwd,
encoding: "utf8",
env: { ...process.env, OPENCLAW_TSGO_HEAVY_CHECK_LOCK_HELD: "1" },
env: process.env,
},
);
-75
View File
@@ -21,7 +21,6 @@ import {
formatNoChangedTestTargetLines,
listFullExtensionVitestProjectConfigs,
orderFullSuiteSpecsForParallelRun,
shouldAcquireLocalHeavyCheckLock,
resolveChangedTestTargetPlanForArgs,
resolveChangedTestTargetPlan,
resolveChangedTargetArgs,
@@ -2891,80 +2890,6 @@ describe("scripts/test-projects changed-target routing", () => {
);
});
describe("scripts/test-projects local heavy-check lock", () => {
const localCheckEnv = () => ({
...process.env,
OPENCLAW_TEST_HEAVY_CHECK_LOCK_HELD: undefined,
OPENCLAW_TEST_PROJECTS_FORCE_LOCK: undefined,
});
it("skips the lock for a single scoped tooling run", () => {
expect(
shouldAcquireLocalHeavyCheckLock(
[
{
config: "test/vitest/vitest.tooling.config.ts",
includePatterns: ["test/scripts/gh-read.test.ts"],
watchMode: false,
},
],
localCheckEnv(),
),
).toBe(false);
});
it("keeps the lock for non-tooling runs", () => {
expect(
shouldAcquireLocalHeavyCheckLock(
[
{
config: "test/vitest/vitest.unit.config.ts",
includePatterns: ["src/infra/vitest-config.test.ts"],
watchMode: false,
},
],
localCheckEnv(),
),
).toBe(true);
});
it("skips the lock when a parent changed gate already holds it", () => {
expect(
shouldAcquireLocalHeavyCheckLock(
[
{
config: "test/vitest/vitest.unit.config.ts",
includePatterns: ["src/infra/vitest-config.test.ts"],
watchMode: false,
},
],
{
...localCheckEnv(),
OPENCLAW_TEST_HEAVY_CHECK_LOCK_HELD: "1",
},
),
).toBe(false);
});
it("allows forcing the lock back on", () => {
expect(
shouldAcquireLocalHeavyCheckLock(
[
{
config: "test/vitest/vitest.tooling.config.ts",
includePatterns: ["test/scripts/gh-read.test.ts"],
watchMode: false,
},
],
{
...localCheckEnv(),
OPENCLAW_TEST_PROJECTS_FORCE_LOCK: "1",
},
),
).toBe(true);
});
});
describe("scripts/test-projects full-suite sharding", () => {
it("interleaves heavy and light configs for cold parallel full-suite runs", () => {
const specs = [