mirror of
https://github.com/openclaw/openclaw.git
synced 2026-10-03 09:39:25 +00:00
fix(test): checkout fixture tests fail healthy runs on loaded macOS hosts (#162888)
* fix(test): checkout fixture budgets cut healthy runs on loaded hosts
On a loaded macOS host (load 55-83), ci-platform-checkout.test.ts and the
other checkout fixture consumers failed through three fixture-internal
wall-clock budgets, not through the workflow under test:
- the supervisor's mock-resolution preflight ran bash under its own 2 s
timeout ("mock command resolution failed: spawnSync bash ETIMEDOUT");
- the POSIX process census shadowed its caller's operation deadline with a
fresh 1 s budget for all singleton ps queries ("Fixture process census
failed (ETIMEDOUT)"); the owner-ancestry walk had the same 1 s shape;
- the supervisor cut every run at 45 s ("fixture deadline exceeded"), nested
under the helper's 50 s close race only because the helper could not see
the Vitest test timeout.
Measured breakdown: a multi-attempt scenario serially starts 25-36 Node
actors plus the Python owner; at load ~72 Linux git-failure spent 19.4 s,
of which the fixture's own 82 ps calls were 1.6 s. The work scales with
scheduler latency and is the contract under test, so the fix is ownership
of the time bound, not less work.
The owning test's AbortSignal now bounds a supervised run.
withCiCheckoutFixture takes it, rejects its close join on abort, and asks
the live supervisor to cancel over IPC so its own stop() retires the
detached shell group and actors, keeping the SIGKILL fallback for a wedged
supervisor. The supervisor drops its 45 s watchdog; its operation deadline
becomes the existing 60 s actor lifetime ceiling, which only bounds orphans.
Census, preflight, and ancestry queries borrow that operation deadline, as
the Windows census witness already did.
Every runCiGitStep and withCiCheckoutFixture caller passes its test signal;
.each registrations that need the context move to .for, which is the only
form Vitest 5 hands the context to.
Regressions: the shared slow-witness preload now gives POSIX supervisors a
first native query that spends 1.1 s on their clock (fails main's fixture
7/7 with "census failed (unverified)"), and the outer-runner retention
proof gains a cancel fault proving the aborted supervisor retires its shell.
* fix(test): keep checkout signal bindings lint-clean
This commit is contained in:
parent
5151a06b8d
commit
d457b2c611
14 changed files with 691 additions and 532 deletions
|
|
@ -192,6 +192,7 @@ export function expectCiCheckoutCleanup(report: Report) {
|
||||||
|
|
||||||
export async function withCiCheckoutFixture<T>(
|
export async function withCiCheckoutFixture<T>(
|
||||||
scenario: string,
|
scenario: string,
|
||||||
|
signal: AbortSignal,
|
||||||
prepare: (root: string) => NodeJS.ProcessEnv | void,
|
prepare: (root: string) => NodeJS.ProcessEnv | void,
|
||||||
inspect: (report: Report, result: CloseResult, stderr: string, root: string) => T | Promise<T>,
|
inspect: (report: Report, result: CloseResult, stderr: string, root: string) => T | Promise<T>,
|
||||||
): Promise<T> {
|
): Promise<T> {
|
||||||
|
|
@ -215,39 +216,64 @@ export async function withCiCheckoutFixture<T>(
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
let stderr = "";
|
let stderr = "";
|
||||||
|
let closeResult: CloseResult | undefined;
|
||||||
// An error can precede close, including failed spawn. Never reject this join.
|
// An error can precede close, including failed spawn. Never reject this join.
|
||||||
const closed = new Promise<CloseResult>((resolve) => {
|
const closed = new Promise<CloseResult>((resolve) => {
|
||||||
supervisor.once("close", (code, signal) => {
|
supervisor.once("close", (code, exitSignal) => {
|
||||||
resolve({ code, signal });
|
closeResult = { code, signal: exitSignal };
|
||||||
|
resolve(closeResult);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
supervisor.stderr?.on("data", (data) => (stderr += String(data)));
|
supervisor.stderr?.on("data", (data) => (stderr += String(data)));
|
||||||
supervisor.on("error", (error) => (stderr += `${error}\n`));
|
supervisor.on("error", (error) => (stderr += `${error}\n`));
|
||||||
let timer: NodeJS.Timeout | undefined;
|
const joinClose = async (deadline: number) => {
|
||||||
|
let timer: NodeJS.Timeout | undefined;
|
||||||
|
try {
|
||||||
|
return await Promise.race([
|
||||||
|
closed.then(() => true),
|
||||||
|
new Promise<boolean>((resolve) => {
|
||||||
|
timer = setTimeout(() => resolve(false), Math.max(0, deadline - Date.now()));
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
} finally {
|
||||||
|
clearTimeout(timer);
|
||||||
|
}
|
||||||
|
};
|
||||||
let report: Report | undefined;
|
let report: Report | undefined;
|
||||||
|
let onAbort = () => {};
|
||||||
try {
|
try {
|
||||||
const completed = await Promise.race([
|
// The owning test bounds the run; a slow host never races a fixture deadline.
|
||||||
closed,
|
// Vitest does not unwind a suspended body on timeout, so its abort must reject
|
||||||
new Promise<never>((_, reject) => {
|
// this join to reach cleanup. Same contract as withinTest, kept inline because
|
||||||
timer = setTimeout(
|
// the outer-runner proof loads this module in plain Node.
|
||||||
() => reject(new Error("Checkout supervisor did not close within 50000ms")),
|
const completed = await new Promise<CloseResult>((resolve, reject) => {
|
||||||
50_000,
|
onAbort = () => {
|
||||||
);
|
const reason: unknown = signal.reason;
|
||||||
}),
|
reject(reason instanceof Error ? reason : new Error("test aborted", { cause: reason }));
|
||||||
]);
|
};
|
||||||
clearTimeout(timer);
|
signal.addEventListener("abort", onAbort, { once: true });
|
||||||
|
if (signal.aborted) {
|
||||||
|
onAbort();
|
||||||
|
}
|
||||||
|
void closed.then(resolve);
|
||||||
|
});
|
||||||
report = reportSchema.parse(JSON.parse(readFileSync(path.join(root, "report.json"), "utf8")));
|
report = reportSchema.parse(JSON.parse(readFileSync(path.join(root, "report.json"), "utf8")));
|
||||||
return await inspect(report, completed, stderr, root);
|
return await inspect(report, completed, stderr, root);
|
||||||
} finally {
|
} finally {
|
||||||
clearTimeout(timer);
|
signal.removeEventListener("abort", onAbort);
|
||||||
if (report) {
|
if (report) {
|
||||||
// A consumer assertion failure does not revoke the producer's release receipt.
|
// A consumer assertion failure does not revoke the producer's release receipt.
|
||||||
rmSync(root, { recursive: true, force: true });
|
rmSync(root, { recursive: true, force: true });
|
||||||
} else {
|
} else {
|
||||||
const deadline = Date.now() + 4_000;
|
|
||||||
// Keep IPC attached through termination: explicit disconnect can suppress Node's close.
|
// Keep IPC attached through termination: explicit disconnect can suppress Node's close.
|
||||||
// Let lease-bound Git descendants stop even if the supervisor cannot run cleanup.
|
// Let lease-bound Git descendants stop even if the supervisor cannot run cleanup.
|
||||||
rmSync(path.join(root, "lease"), { force: true });
|
rmSync(path.join(root, "lease"), { force: true });
|
||||||
|
if (!closeResult && supervisor.connected) {
|
||||||
|
// A cancelled run still has a live owner: let it retire its shell group and actors.
|
||||||
|
supervisor.send({ type: "ci-checkout:cancel" }, () => {});
|
||||||
|
await joinClose(Date.now() + 4_000);
|
||||||
|
}
|
||||||
|
const deadline = Date.now() + 4_000;
|
||||||
const termination = terminateManagedChild(supervisor, "SIGKILL", {
|
const termination = terminateManagedChild(supervisor, "SIGKILL", {
|
||||||
taskkillTimeoutMs: 2_000,
|
taskkillTimeoutMs: 2_000,
|
||||||
processGroupFallback: "never",
|
processGroupFallback: "never",
|
||||||
|
|
|
||||||
|
|
@ -11,9 +11,10 @@ import {
|
||||||
|
|
||||||
it.skipIf(process.platform !== "win32")(
|
it.skipIf(process.platform !== "win32")(
|
||||||
"settles original member handles before the Git owner returns",
|
"settles original member handles before the Git owner returns",
|
||||||
async () => {
|
async ({ signal }) => {
|
||||||
await withCiCheckoutFixture(
|
await withCiCheckoutFixture(
|
||||||
"harness-timeout",
|
"harness-timeout",
|
||||||
|
signal,
|
||||||
(root) => {
|
(root) => {
|
||||||
const source = readFileSync(".github/actions/git-owner/owner.py", "utf8");
|
const source = readFileSync(".github/actions/git-owner/owner.py", "utf8");
|
||||||
const probe = path.join(root, "settlement-probe.py");
|
const probe = path.join(root, "settlement-probe.py");
|
||||||
|
|
|
||||||
|
|
@ -114,6 +114,7 @@ function readWorkflowStep({ file, job, step: name }: WorkflowTarget): Step & { r
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function runCiGitStep(options: {
|
export async function runCiGitStep(options: {
|
||||||
|
signal: AbortSignal;
|
||||||
workflow?: "workflow-sanity" | WorkflowTarget;
|
workflow?: "workflow-sanity" | WorkflowTarget;
|
||||||
job?: string;
|
job?: string;
|
||||||
action?:
|
action?:
|
||||||
|
|
@ -241,6 +242,7 @@ export async function runCiGitStep(options: {
|
||||||
let publisherFixture: ReturnType<typeof prepareGeneratedPublisherFixture> | undefined;
|
let publisherFixture: ReturnType<typeof prepareGeneratedPublisherFixture> | undefined;
|
||||||
return withCiCheckoutFixture(
|
return withCiCheckoutFixture(
|
||||||
`linux:${options.scenario ?? "configured"}`,
|
`linux:${options.scenario ?? "configured"}`,
|
||||||
|
options.signal,
|
||||||
(root) => {
|
(root) => {
|
||||||
const actions = path.join(root, "trusted-actions");
|
const actions = path.join(root, "trusted-actions");
|
||||||
if (options.performance) {
|
if (options.performance) {
|
||||||
|
|
|
||||||
|
|
@ -512,13 +512,15 @@ exec "$REAL_GIT" "$@"`,
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
releasePolicyIt.each([
|
releasePolicyIt.for([
|
||||||
{ label: "timeout", failure: "hang" },
|
{ label: "timeout", failure: "hang" },
|
||||||
{ label: "Git failure", failure: 23 },
|
{ label: "Git failure", failure: 23 },
|
||||||
] as const)(
|
] as const)(
|
||||||
"retries a drained release ancestry fetch after $label",
|
"retries a drained release ancestry fetch after $label",
|
||||||
async ({ failure }) => {
|
{ timeout: 55_000 },
|
||||||
|
async ({ failure }, { signal }) => {
|
||||||
const report = await runCiGitStep({
|
const report = await runCiGitStep({
|
||||||
|
signal,
|
||||||
policy: failure === "hang" ? fastReleaseAncestryPolicy : releaseAncestryPolicy,
|
policy: failure === "hang" ? fastReleaseAncestryPolicy : releaseAncestryPolicy,
|
||||||
env: {
|
env: {
|
||||||
RELEASE_ANCESTRY_MODE: "merge-base",
|
RELEASE_ANCESTRY_MODE: "merge-base",
|
||||||
|
|
@ -539,13 +541,13 @@ releasePolicyIt.each([
|
||||||
expect(report.fetches).toHaveLength(2);
|
expect(report.fetches).toHaveLength(2);
|
||||||
expect(report.output).toContain("fetch failed on attempt 1; retrying");
|
expect(report.output).toContain("fetch failed on attempt 1; retrying");
|
||||||
},
|
},
|
||||||
55_000,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
releasePolicyIt(
|
releasePolicyIt(
|
||||||
"preserves the final release ancestry Git failure after bounded retries",
|
"preserves the final release ancestry Git failure after bounded retries",
|
||||||
async () => {
|
async ({ signal }) => {
|
||||||
const report = await runCiGitStep({
|
const report = await runCiGitStep({
|
||||||
|
signal,
|
||||||
policy: releaseAncestryPolicy,
|
policy: releaseAncestryPolicy,
|
||||||
env: {
|
env: {
|
||||||
RELEASE_ANCESTRY_MODE: "merge-base",
|
RELEASE_ANCESTRY_MODE: "merge-base",
|
||||||
|
|
@ -561,18 +563,22 @@ releasePolicyIt(
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
releasePolicyIt("returns 124 when the release ancestry total budget is exhausted", async () => {
|
releasePolicyIt(
|
||||||
const report = await runCiGitStep({
|
"returns 124 when the release ancestry total budget is exhausted",
|
||||||
policy: expiredReleaseAncestryPolicy,
|
async ({ signal }) => {
|
||||||
env: {
|
const report = await runCiGitStep({
|
||||||
RELEASE_ANCESTRY_MODE: "merge-base",
|
signal,
|
||||||
RELEASE_ANCESTRY_TARGET_REF: "refs/heads/main",
|
policy: expiredReleaseAncestryPolicy,
|
||||||
},
|
env: {
|
||||||
fetchResults: [],
|
RELEASE_ANCESTRY_MODE: "merge-base",
|
||||||
});
|
RELEASE_ANCESTRY_TARGET_REF: "refs/heads/main",
|
||||||
expect(report.code, report.output).toBe(124);
|
},
|
||||||
expect(report.commands).toEqual([]);
|
fetchResults: [],
|
||||||
});
|
});
|
||||||
|
expect(report.code, report.output).toBe(124);
|
||||||
|
expect(report.commands).toEqual([]);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
it("materializes an executable preflight manifest from the workflow revision", async ({
|
it("materializes an executable preflight manifest from the workflow revision", async ({
|
||||||
command,
|
command,
|
||||||
|
|
@ -729,15 +735,18 @@ it("binds read-only checkout authentication only to the workflow repository", ()
|
||||||
expect(ownedCheckouts).toBeGreaterThan(0);
|
expect(ownedCheckouts).toBeGreaterThan(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it.each([false, true])("preserves linked Git metadata (reclaim locks=%s)", async (reclaimLocks) => {
|
it.for([false, true])(
|
||||||
const invocation = reclaimLocks
|
"preserves linked Git metadata (reclaim locks=%s)",
|
||||||
? 'run_git(os.getcwd(), "fetch", "origin", "fixture", reclaim_locks=True)'
|
async (reclaimLocks, { signal }) => {
|
||||||
: 'print(git_output(os.getcwd(), "rev-parse", "HEAD"), end="")';
|
const invocation = reclaimLocks
|
||||||
const report = await runCiGitStep({
|
? 'run_git(os.getcwd(), "fetch", "origin", "fixture", reclaim_locks=True)'
|
||||||
fetchResults: [],
|
: 'print(git_output(os.getcwd(), "rev-parse", "HEAD"), end="")';
|
||||||
policy:
|
const report = await runCiGitStep({
|
||||||
policyImport +
|
signal,
|
||||||
`from pathlib import Path
|
fetchResults: [],
|
||||||
|
policy:
|
||||||
|
policyImport +
|
||||||
|
`from pathlib import Path
|
||||||
shared = Path.cwd().parent / "shared-git"
|
shared = Path.cwd().parent / "shared-git"
|
||||||
shared.mkdir()
|
shared.mkdir()
|
||||||
lock = shared / "shallow.lock"
|
lock = shared / "shallow.lock"
|
||||||
|
|
@ -750,18 +759,20 @@ finally:
|
||||||
assert metadata.read_text() == "gitdir: ../shared-git\\n"
|
assert metadata.read_text() == "gitdir: ../shared-git\\n"
|
||||||
assert lock.read_text() == "not invocation-owned\\n"
|
assert lock.read_text() == "not invocation-owned\\n"
|
||||||
`,
|
`,
|
||||||
});
|
});
|
||||||
expect(report.code, report.output).toBe(reclaimLocks ? 125 : 0);
|
expect(report.code, report.output).toBe(reclaimLocks ? 125 : 0);
|
||||||
expect(report.commands.map(({ args }) => args)).toEqual(
|
expect(report.commands.map(({ args }) => args)).toEqual(
|
||||||
reclaimLocks ? [] : [["rev-parse", "HEAD"]],
|
reclaimLocks ? [] : [["rev-parse", "HEAD"]],
|
||||||
);
|
);
|
||||||
if (!reclaimLocks) {
|
if (!reclaimLocks) {
|
||||||
expect(report.output).toBe(`${head}${EOL}`);
|
expect(report.output).toBe(`${head}${EOL}`);
|
||||||
}
|
}
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
it("reclaims failed supplemental-fetch locks before the next attempt", async () => {
|
it("reclaims failed supplemental-fetch locks before the next attempt", async ({ signal }) => {
|
||||||
const report = await runCiGitStep({
|
const report = await runCiGitStep({
|
||||||
|
signal,
|
||||||
job: "checks-fast-core",
|
job: "checks-fast-core",
|
||||||
step: "Prepare release-gate ratchet merge tree",
|
step: "Prepare release-gate ratchet merge tree",
|
||||||
fetchResults: ["hang", 0],
|
fetchResults: ["hang", 0],
|
||||||
|
|
@ -774,8 +785,9 @@ it("reclaims failed supplemental-fetch locks before the next attempt", async ()
|
||||||
|
|
||||||
linuxIt(
|
linuxIt(
|
||||||
"bootstraps only action-owned bytes outside the candidate with isolated Python",
|
"bootstraps only action-owned bytes outside the candidate with isolated Python",
|
||||||
async () => {
|
async ({ signal }) => {
|
||||||
const report = await runCiGitStep({
|
const report = await runCiGitStep({
|
||||||
|
signal,
|
||||||
action: "git-owner",
|
action: "git-owner",
|
||||||
fetchResults: [],
|
fetchResults: [],
|
||||||
poisonPython: true,
|
poisonPython: true,
|
||||||
|
|
@ -958,8 +970,9 @@ runpy.run_path(os.environ["BASE_REAL_POLICY_PATH"], run_name="__main__")
|
||||||
|
|
||||||
linuxIt(
|
linuxIt(
|
||||||
"drains a timed-out exact fetch before deepening for the base",
|
"drains a timed-out exact fetch before deepening for the base",
|
||||||
async () => {
|
async ({ signal }) => {
|
||||||
const report = await runCiGitStep({
|
const report = await runCiGitStep({
|
||||||
|
signal,
|
||||||
action: "ensure-base-commit",
|
action: "ensure-base-commit",
|
||||||
baseAvailableAfter: 2,
|
baseAvailableAfter: 2,
|
||||||
fetchResults: ["hang", 0],
|
fetchResults: ["hang", 0],
|
||||||
|
|
@ -973,7 +986,7 @@ linuxIt(
|
||||||
55_000,
|
55_000,
|
||||||
);
|
);
|
||||||
|
|
||||||
linuxIt.each([
|
linuxIt.for([
|
||||||
{ label: "empty", sha: "", code: 0, commands: 0 },
|
{ label: "empty", sha: "", code: 0, commands: 0 },
|
||||||
{ label: "all-zero", sha: "00000", code: 0, commands: 0 },
|
{ label: "all-zero", sha: "00000", code: 0, commands: 0 },
|
||||||
{ label: "invalid SHA", sha: "--help", code: 2, commands: 0 },
|
{ label: "invalid SHA", sha: "--help", code: 2, commands: 0 },
|
||||||
|
|
@ -984,8 +997,9 @@ linuxIt.each([
|
||||||
{ label: "already available", sha: base, code: 0, commands: 2 },
|
{ label: "already available", sha: base, code: 0, commands: 2 },
|
||||||
])(
|
])(
|
||||||
"base policy preserves $label validation and skip behavior",
|
"base policy preserves $label validation and skip behavior",
|
||||||
async ({ sha, code, commands, invalidRef }) => {
|
async ({ sha, code, commands, invalidRef }, { signal }) => {
|
||||||
const report = await runCiGitStep({
|
const report = await runCiGitStep({
|
||||||
|
signal,
|
||||||
action: "ensure-base-commit",
|
action: "ensure-base-commit",
|
||||||
env: { BASE_SHA: sha },
|
env: { BASE_SHA: sha },
|
||||||
invalidRef,
|
invalidRef,
|
||||||
|
|
@ -998,10 +1012,12 @@ linuxIt.each([
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
linuxIt.each([1, 2, 3, 4, 5, 6, undefined])(
|
linuxIt.for([1, 2, 3, 4, 5, 6, undefined])(
|
||||||
"base policy preserves exact/deepen/plain-ref order (available after %s)",
|
"base policy preserves exact/deepen/plain-ref order (available after %s)",
|
||||||
async (baseAvailableAfter) => {
|
{ timeout: 55_000 },
|
||||||
|
async (baseAvailableAfter, { signal }) => {
|
||||||
const report = await runCiGitStep({
|
const report = await runCiGitStep({
|
||||||
|
signal,
|
||||||
action: "ensure-base-commit",
|
action: "ensure-base-commit",
|
||||||
baseAvailableAfter,
|
baseAvailableAfter,
|
||||||
fetchResults: [0, 23, 0, 23, 0, 0],
|
fetchResults: [0, 23, 0, 23, 0, 0],
|
||||||
|
|
@ -1037,13 +1053,14 @@ linuxIt.each([1, 2, 3, 4, 5, 6, undefined])(
|
||||||
expect(report.output).toContain("::error title=ensure-base-commit missing base::");
|
expect(report.output).toContain("::error title=ensure-base-commit missing base::");
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
55_000,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
linuxIt.each([125, 143, "hang"] as const)(
|
linuxIt.for([125, 143, "hang"] as const)(
|
||||||
"base remains available after safely drained ordinary outcome %s",
|
"base remains available after safely drained ordinary outcome %s",
|
||||||
async (failure) => {
|
{ timeout: 55_000 },
|
||||||
|
async (failure, { signal }) => {
|
||||||
const report = await runCiGitStep({
|
const report = await runCiGitStep({
|
||||||
|
signal,
|
||||||
action: "ensure-base-commit",
|
action: "ensure-base-commit",
|
||||||
baseAvailableAfter: 1,
|
baseAvailableAfter: 1,
|
||||||
fetchResults: [failure],
|
fetchResults: [failure],
|
||||||
|
|
@ -1053,10 +1070,9 @@ linuxIt.each([125, 143, "hang"] as const)(
|
||||||
expect(report.output).toContain("exact fetch failed");
|
expect(report.output).toContain("exact fetch failed");
|
||||||
expect(report.output).toContain("Resolved base commit after exact fetch");
|
expect(report.output).toContain("Resolved base commit after exact fetch");
|
||||||
},
|
},
|
||||||
55_000,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
linuxIt.each([
|
linuxIt.for([
|
||||||
{ label: "inspection failure", result: "cleanup-failure", code: 125 },
|
{ label: "inspection failure", result: "cleanup-failure", code: 125 },
|
||||||
{ label: "cancellation", result: "hang", scenario: "cancel-SIGTERM", code: 143 },
|
{ label: "cancellation", result: "hang", scenario: "cancel-SIGTERM", code: 143 },
|
||||||
{
|
{
|
||||||
|
|
@ -1067,8 +1083,10 @@ linuxIt.each([
|
||||||
},
|
},
|
||||||
] as const)(
|
] as const)(
|
||||||
"base policy stops before availability/retry on $label",
|
"base policy stops before availability/retry on $label",
|
||||||
async ({ result, code, ...entry }) => {
|
{ timeout: 55_000 },
|
||||||
|
async ({ result, code, ...entry }, { signal }) => {
|
||||||
const report = await runCiGitStep({
|
const report = await runCiGitStep({
|
||||||
|
signal,
|
||||||
action: "ensure-base-commit",
|
action: "ensure-base-commit",
|
||||||
baseAvailableAfter: 1,
|
baseAvailableAfter: 1,
|
||||||
fetchResults: [result],
|
fetchResults: [result],
|
||||||
|
|
@ -1082,13 +1100,13 @@ linuxIt.each([
|
||||||
expect(report.output).not.toContain("Resolved base commit");
|
expect(report.output).not.toContain("Resolved base commit");
|
||||||
expect(report.cancelledDuringCleanup).toBe("cancelDuringCleanup" in entry);
|
expect(report.cancelledDuringCleanup).toBe("cancelDuringCleanup" in entry);
|
||||||
},
|
},
|
||||||
55_000,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
linuxIt(
|
linuxIt(
|
||||||
"keeps the base action's 30-second fetch deadline and drains before recovery",
|
"keeps the base action's 30-second fetch deadline and drains before recovery",
|
||||||
async () => {
|
async ({ signal }) => {
|
||||||
const report = await runCiGitStep({
|
const report = await runCiGitStep({
|
||||||
|
signal,
|
||||||
action: "ensure-base-commit",
|
action: "ensure-base-commit",
|
||||||
baseAvailableAfter: 1,
|
baseAvailableAfter: 1,
|
||||||
fetchResults: ["hang"],
|
fetchResults: ["hang"],
|
||||||
|
|
@ -1108,8 +1126,9 @@ linuxIt(
|
||||||
|
|
||||||
linuxIt(
|
linuxIt(
|
||||||
"fences later calls even if a trusted policy accidentally catches an ownership failure",
|
"fences later calls even if a trusted policy accidentally catches an ownership failure",
|
||||||
async () => {
|
async ({ signal }) => {
|
||||||
const report = await runCiGitStep({
|
const report = await runCiGitStep({
|
||||||
|
signal,
|
||||||
fetchResults: ["cleanup-failure"],
|
fetchResults: ["cleanup-failure"],
|
||||||
policy:
|
policy:
|
||||||
policyImport +
|
policyImport +
|
||||||
|
|
@ -1130,15 +1149,17 @@ except Exception:
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
linuxIt.each(
|
linuxIt.for(
|
||||||
[false, true].flatMap((inlinePolicy) =>
|
[false, true].flatMap((inlinePolicy) =>
|
||||||
([125, "cleanup-failure"] as const).map((failure) => ({ inlinePolicy, failure })),
|
([125, "cleanup-failure"] as const).map((failure) => ({ inlinePolicy, failure })),
|
||||||
),
|
),
|
||||||
)(
|
)(
|
||||||
"preserves generic output and typed recovery (stdin=$inlinePolicy, outcome=$failure)",
|
"preserves generic output and typed recovery (stdin=$inlinePolicy, outcome=$failure)",
|
||||||
async ({ inlinePolicy, failure }) => {
|
{ timeout: 55_000 },
|
||||||
|
async ({ inlinePolicy, failure }, { signal }) => {
|
||||||
const output = " \tpath\0another path\r\n\n\n";
|
const output = " \tpath\0another path\r\n\n\n";
|
||||||
const report = await runCiGitStep({
|
const report = await runCiGitStep({
|
||||||
|
signal,
|
||||||
fetchResults: [failure],
|
fetchResults: [failure],
|
||||||
inlinePolicy,
|
inlinePolicy,
|
||||||
revisions: { HEAD: output.slice(0, -1) },
|
revisions: { HEAD: output.slice(0, -1) },
|
||||||
|
|
@ -1171,14 +1192,15 @@ sys.stdout.write(git_output(os.getcwd(), "rev-parse", "HEAD", env={"CI_OWNER_PRO
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
55_000,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
linuxIt.each([0, 23, "cleanup-failure"] as const)(
|
linuxIt.for([0, 23, "cleanup-failure"] as const)(
|
||||||
"generic Git output drains its writers before consumption (%s)",
|
"generic Git output drains its writers before consumption (%s)",
|
||||||
async (code) => {
|
{ timeout: 55_000 },
|
||||||
|
async (code, { signal }) => {
|
||||||
const output = `${head}\trefs/heads/main\n`;
|
const output = `${head}\trefs/heads/main\n`;
|
||||||
const report = await runCiGitStep({
|
const report = await runCiGitStep({
|
||||||
|
signal,
|
||||||
policy:
|
policy:
|
||||||
policyImport +
|
policyImport +
|
||||||
'import sys\nsys.stdout.write(git_output(os.getcwd(), "ls-remote", "origin", "refs/heads/main"))\n',
|
'import sys\nsys.stdout.write(git_output(os.getcwd(), "ls-remote", "origin", "refs/heads/main"))\n',
|
||||||
|
|
@ -1196,7 +1218,6 @@ linuxIt.each([0, 23, "cleanup-failure"] as const)(
|
||||||
expect(report.output).not.toContain(output);
|
expect(report.output).not.toContain(output);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
55_000,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const posixIt = it.skipIf(process.platform === "win32").concurrent;
|
const posixIt = it.skipIf(process.platform === "win32").concurrent;
|
||||||
|
|
@ -1219,8 +1240,12 @@ function requireAuditObject(ref: string, file: string) {
|
||||||
}
|
}
|
||||||
return object;
|
return object;
|
||||||
}
|
}
|
||||||
const sanity = (options: Omit<Parameters<typeof runCiGitStep>[0], "workflow">) =>
|
const sanity = (
|
||||||
|
signal: AbortSignal,
|
||||||
|
options: Omit<Parameters<typeof runCiGitStep>[0], "workflow" | "signal">,
|
||||||
|
) =>
|
||||||
runCiGitStep({
|
runCiGitStep({
|
||||||
|
signal,
|
||||||
...options,
|
...options,
|
||||||
workflow: "workflow-sanity",
|
workflow: "workflow-sanity",
|
||||||
objects: { ...auditObjects, ...options.objects },
|
objects: { ...auditObjects, ...options.objects },
|
||||||
|
|
@ -1283,10 +1308,11 @@ const sanityFetchCases: SanityFetchCase[] = [
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
posixIt.each(sanityFetchCases)(
|
posixIt.for(sanityFetchCases)(
|
||||||
"workflow sanity preserves fetch policy: $label",
|
"workflow sanity preserves fetch policy: $label",
|
||||||
async ({ fetchResults, baseAvailableAfter, refs, warnings, code }) => {
|
{ timeout: 55_000 },
|
||||||
const report = await sanity({ fetchResults, baseAvailableAfter });
|
async ({ fetchResults, baseAvailableAfter, refs, warnings, code }, { signal }) => {
|
||||||
|
const report = await sanity(signal, { fetchResults, baseAvailableAfter });
|
||||||
expect(report.code, report.output).toBe(code);
|
expect(report.code, report.output).toBe(code);
|
||||||
expect(report.fetches.map(({ args }) => args)).toEqual(
|
expect(report.fetches.map(({ args }) => args)).toEqual(
|
||||||
refs.map((ref) => [
|
refs.map((ref) => [
|
||||||
|
|
@ -1324,17 +1350,17 @@ posixIt.each(sanityFetchCases)(
|
||||||
expect(report.trustedZizmor).toBe("");
|
expect(report.trustedZizmor).toBe("");
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
55_000,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
posixIt.each([
|
posixIt.for([
|
||||||
{ label: "30-second fetch deadline", fetchResults: ["hang", 0], warnings: 1 },
|
{ label: "30-second fetch deadline", fetchResults: ["hang", 0], warnings: 1 },
|
||||||
{ label: "five-second backoff", fetchResults: [137, 0], warnings: 1 },
|
{ label: "five-second backoff", fetchResults: [137, 0], warnings: 1 },
|
||||||
] as const)(
|
] as const)(
|
||||||
"workflow sanity retains $label",
|
"workflow sanity retains $label",
|
||||||
async ({ fetchResults, warnings }) => {
|
{ timeout: 55_000 },
|
||||||
|
async ({ fetchResults, warnings }, { signal }) => {
|
||||||
const readyFetchClockAdvanceSeconds = fetchResults[0] === "hang" ? 30 : undefined;
|
const readyFetchClockAdvanceSeconds = fetchResults[0] === "hang" ? 30 : undefined;
|
||||||
const report = await sanity({
|
const report = await sanity(signal, {
|
||||||
fetchResults: [...fetchResults],
|
fetchResults: [...fetchResults],
|
||||||
realClock: true,
|
realClock: true,
|
||||||
virtualBackoff: true,
|
virtualBackoff: true,
|
||||||
|
|
@ -1356,10 +1382,9 @@ posixIt.each([
|
||||||
(report.backoffClockAdvancedSeconds + (report.fetchClockAdvancedSeconds ?? 0)) * 1000;
|
(report.backoffClockAdvancedSeconds + (report.fetchClockAdvancedSeconds ?? 0)) * 1000;
|
||||||
expect(elapsed).toBeGreaterThanOrEqual(fetchResults[0] === "hang" ? 35_000 : 5_000);
|
expect(elapsed).toBeGreaterThanOrEqual(fetchResults[0] === "hang" ? 35_000 : 5_000);
|
||||||
},
|
},
|
||||||
55_000,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
posixIt.each([
|
posixIt.for([
|
||||||
{ label: "owner inspection failure", fetchResults: ["cleanup-failure"], code: 125 },
|
{ label: "owner inspection failure", fetchResults: ["cleanup-failure"], code: 125 },
|
||||||
{ label: "fetch cancellation", fetchResults: ["hang"], scenario: "cancel-SIGTERM", code: 143 },
|
{ label: "fetch cancellation", fetchResults: ["hang"], scenario: "cancel-SIGTERM", code: 143 },
|
||||||
{
|
{
|
||||||
|
|
@ -1390,8 +1415,9 @@ posixIt.each([
|
||||||
fetchResults: FetchResult[];
|
fetchResults: FetchResult[];
|
||||||
})[])(
|
})[])(
|
||||||
"workflow sanity never recovers or publishes after $label",
|
"workflow sanity never recovers or publishes after $label",
|
||||||
async ({ label: _label, code, ...options }) => {
|
{ timeout: 55_000 },
|
||||||
const report = await sanity(options);
|
async ({ label: _label, code, ...options }, { signal }) => {
|
||||||
|
const report = await sanity(signal, options);
|
||||||
if (code === "launcher") {
|
if (code === "launcher") {
|
||||||
// Bash versions differ for a found executable whose interpreter is missing.
|
// Bash versions differ for a found executable whose interpreter is missing.
|
||||||
expect([126, 127], report.output).toContain(report.code);
|
expect([126, 127], report.output).toContain(report.code);
|
||||||
|
|
@ -1408,13 +1434,13 @@ posixIt.each([
|
||||||
Boolean(options.cancelDuringBackoff),
|
Boolean(options.cancelDuringBackoff),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
55_000,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
posixIt.each([[0], [1]].map((missing) => ({ missing })))(
|
posixIt.for([[0], [1]].map((missing) => ({ missing })))(
|
||||||
"workflow sanity selects missing exact configs independently ($missing)",
|
"workflow sanity selects missing exact configs independently ($missing)",
|
||||||
async ({ missing }) => {
|
{ timeout: 55_000 },
|
||||||
const report = await sanity({
|
async ({ missing }, { signal }) => {
|
||||||
|
const report = await sanity(signal, {
|
||||||
fetchResults: [],
|
fetchResults: [],
|
||||||
baseAvailableAfter: 0,
|
baseAvailableAfter: 0,
|
||||||
objects: Object.fromEntries(
|
objects: Object.fromEntries(
|
||||||
|
|
@ -1449,18 +1475,18 @@ posixIt.each([[0], [1]].map((missing) => ({ missing })))(
|
||||||
`PRE_COMMIT_CONFIG_PATH=${report.runnerTemp}/pre-commit-base.yaml\n`,
|
`PRE_COMMIT_CONFIG_PATH=${report.runnerTemp}/pre-commit-base.yaml\n`,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
55_000,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
posixIt.each(
|
posixIt.for(
|
||||||
auditFiles.flatMap((file) => [
|
auditFiles.flatMap((file) => [
|
||||||
{ file, fallback: false },
|
{ file, fallback: false },
|
||||||
{ file, fallback: true },
|
{ file, fallback: true },
|
||||||
]),
|
]),
|
||||||
)(
|
)(
|
||||||
"workflow sanity rejects partial $file show (fallback=$fallback)",
|
"workflow sanity rejects partial $file show (fallback=$fallback)",
|
||||||
async ({ file, fallback }) => {
|
{ timeout: 55_000 },
|
||||||
const report = await sanity({
|
async ({ file, fallback }, { signal }) => {
|
||||||
|
const report = await sanity(signal, {
|
||||||
fetchResults: [],
|
fetchResults: [],
|
||||||
baseAvailableAfter: 0,
|
baseAvailableAfter: 0,
|
||||||
objects: {
|
objects: {
|
||||||
|
|
@ -1481,11 +1507,10 @@ posixIt.each(
|
||||||
expect(report.output).toContain(`Could not read ${file} from ${base} or origin/main.`);
|
expect(report.output).toContain(`Could not read ${file} from ${base} or origin/main.`);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
55_000,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
posixIt("workflow sanity rejects a config without the Zizmor reference", async () => {
|
posixIt("workflow sanity rejects a config without the Zizmor reference", async ({ signal }) => {
|
||||||
const report = await sanity({
|
const report = await sanity(signal, {
|
||||||
fetchResults: [],
|
fetchResults: [],
|
||||||
baseAvailableAfter: 0,
|
baseAvailableAfter: 0,
|
||||||
objects: { [`${base}:${auditFiles[0]}`]: { text: "repos: []\n" } },
|
objects: { [`${base}:${auditFiles[0]}`]: { text: "repos: []\n" } },
|
||||||
|
|
@ -1512,8 +1537,9 @@ const maturityEnvironment = {
|
||||||
|
|
||||||
posixIt(
|
posixIt(
|
||||||
"generated publisher drains real Git descendants before every continuation",
|
"generated publisher drains real Git descendants before every continuation",
|
||||||
async () => {
|
async ({ signal }) => {
|
||||||
const report = await runCiGitStep({
|
const report = await runCiGitStep({
|
||||||
|
signal,
|
||||||
action: "publish-generated-pr",
|
action: "publish-generated-pr",
|
||||||
step: "Publish generated pull request",
|
step: "Publish generated pull request",
|
||||||
fetchResults: [],
|
fetchResults: [],
|
||||||
|
|
@ -1532,8 +1558,12 @@ posixIt(
|
||||||
55_000,
|
55_000,
|
||||||
);
|
);
|
||||||
|
|
||||||
function publisherRun(options: Partial<Parameters<typeof runCiGitStep>[0]> = {}) {
|
function publisherRun(
|
||||||
|
signal: AbortSignal,
|
||||||
|
options: Partial<Parameters<typeof runCiGitStep>[0]> = {},
|
||||||
|
) {
|
||||||
return runCiGitStep({
|
return runCiGitStep({
|
||||||
|
signal,
|
||||||
action: "publish-generated-pr",
|
action: "publish-generated-pr",
|
||||||
step: "Publish generated pull request",
|
step: "Publish generated pull request",
|
||||||
fetchResults: [],
|
fetchResults: [],
|
||||||
|
|
@ -1541,8 +1571,12 @@ function publisherRun(options: Partial<Parameters<typeof runCiGitStep>[0]> = {})
|
||||||
...options,
|
...options,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
function maturityRun(options: Partial<Parameters<typeof runCiGitStep>[0]> = {}) {
|
function maturityRun(
|
||||||
|
signal: AbortSignal,
|
||||||
|
options: Partial<Parameters<typeof runCiGitStep>[0]> = {},
|
||||||
|
) {
|
||||||
return runCiGitStep({
|
return runCiGitStep({
|
||||||
|
signal,
|
||||||
workflow: maturityValidation,
|
workflow: maturityValidation,
|
||||||
env: maturityEnvironment,
|
env: maturityEnvironment,
|
||||||
fetchResults: [],
|
fetchResults: [],
|
||||||
|
|
@ -1553,14 +1587,15 @@ function maturityRun(options: Partial<Parameters<typeof runCiGitStep>[0]> = {})
|
||||||
|
|
||||||
// Actual-body fault injection covers the former conditional-errexit hole and
|
// Actual-body fault injection covers the former conditional-errexit hole and
|
||||||
// lifecycle/status collisions; the existing real-repository cases own tree semantics.
|
// lifecycle/status collisions; the existing real-repository cases own tree semantics.
|
||||||
posixIt.each(
|
posixIt.for(
|
||||||
["fetch", "ls-remote", "push", "ls-tree"].flatMap((operation) =>
|
["fetch", "ls-remote", "push", "ls-tree"].flatMap((operation) =>
|
||||||
(["cleanup-failure", "cancel"] as const).map((code) => ({ operation, code })),
|
(["cleanup-failure", "cancel"] as const).map((code) => ({ operation, code })),
|
||||||
),
|
),
|
||||||
)(
|
)(
|
||||||
"generated publisher $code at $operation is terminal before any continuation",
|
"generated publisher $code at $operation is terminal before any continuation",
|
||||||
async ({ operation, code }) => {
|
{ timeout: 55_000 },
|
||||||
const report = await publisherRun({ gitFault: { match: `^${operation} `, code } });
|
async ({ operation, code }, { signal }) => {
|
||||||
|
const report = await publisherRun(signal, { gitFault: { match: `^${operation} `, code } });
|
||||||
expect(report.code, report.output).toBe(code === "cancel" ? 143 : 125);
|
expect(report.code, report.output).toBe(code === "cancel" ? 143 : 125);
|
||||||
expect(report.commands.at(-1)?.args[0]).toBe(operation);
|
expect(report.commands.at(-1)?.args[0]).toBe(operation);
|
||||||
expect(report.githubSummary).toBe("");
|
expect(report.githubSummary).toBe("");
|
||||||
|
|
@ -1570,13 +1605,13 @@ posixIt.each(
|
||||||
/refusing a doomed retry|moved concurrently|merged|Deferred|Generated pull request:/u,
|
/refusing a doomed retry|moved concurrently|merged|Deferred|Generated pull request:/u,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
55_000,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
posixIt.each([124, 125, 143])(
|
posixIt.for([124, 125, 143])(
|
||||||
"generated publisher ordinary push %s drains before semantic failure reporting",
|
"generated publisher ordinary push %s drains before semantic failure reporting",
|
||||||
async (code) => {
|
{ timeout: 55_000 },
|
||||||
const report = await publisherRun({
|
async (code, { signal }) => {
|
||||||
|
const report = await publisherRun(signal, {
|
||||||
gitFault: { match: "^push ", code, output: "GH013 repository rule violations\n" },
|
gitFault: { match: "^push ", code, output: "GH013 repository rule violations\n" },
|
||||||
});
|
});
|
||||||
expect(report.code, report.output).toBe(code === 124 ? 0 : code);
|
expect(report.code, report.output).toBe(code === 124 ? 0 : code);
|
||||||
|
|
@ -1596,13 +1631,15 @@ posixIt.each([124, 125, 143])(
|
||||||
expect(report.githubSummary).toBe("");
|
expect(report.githubSummary).toBe("");
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
55_000,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
posixIt.each(["fetch", "ls-remote", "push"])(
|
posixIt.for(["fetch", "ls-remote", "push"])(
|
||||||
"generated publisher %s timeout has bounded recovery",
|
"generated publisher %s timeout has bounded recovery",
|
||||||
async (operation) => {
|
{ timeout: 55_000 },
|
||||||
const report = await publisherRun({ gitFault: { match: `^${operation} `, code: "hang" } });
|
async (operation, { signal }) => {
|
||||||
|
const report = await publisherRun(signal, {
|
||||||
|
gitFault: { match: `^${operation} `, code: "hang" },
|
||||||
|
});
|
||||||
expect(report.code, report.output).toBe(operation === "push" ? 0 : 124);
|
expect(report.code, report.output).toBe(operation === "push" ? 0 : 124);
|
||||||
expect(report.fetches).toHaveLength(1);
|
expect(report.fetches).toHaveLength(1);
|
||||||
expect(report.pushes).toHaveLength(operation === "push" ? 2 : 0);
|
expect(report.pushes).toHaveLength(operation === "push" ? 2 : 0);
|
||||||
|
|
@ -1613,10 +1650,9 @@ posixIt.each(["fetch", "ls-remote", "push"])(
|
||||||
);
|
);
|
||||||
expect(report.authHeaderPresent).toBe(false);
|
expect(report.authHeaderPresent).toBe(false);
|
||||||
},
|
},
|
||||||
55_000,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
posixIt.each([
|
posixIt.for([
|
||||||
{ label: "overlap candidate diff", match: "^diff --name-only", occurrence: 2 },
|
{ label: "overlap candidate diff", match: "^diff --name-only", occurrence: 2 },
|
||||||
{ label: "overlap tree read", match: "^ls-tree ", occurrence: 1 },
|
{ label: "overlap tree read", match: "^ls-tree ", occurrence: 1 },
|
||||||
{ label: "invalidation diff", match: "^diff --quiet ", occurrence: 1 },
|
{ label: "invalidation diff", match: "^diff --quiet ", occurrence: 1 },
|
||||||
|
|
@ -1632,8 +1668,9 @@ posixIt.each([
|
||||||
},
|
},
|
||||||
])(
|
])(
|
||||||
"generated publisher ordinary failure inside $label never becomes success",
|
"generated publisher ordinary failure inside $label never becomes success",
|
||||||
async ({ match, occurrence, merged, noChange, overlap }) => {
|
{ timeout: 55_000 },
|
||||||
const report = await publisherRun({
|
async ({ match, occurrence, merged, noChange, overlap }, { signal }) => {
|
||||||
|
const report = await publisherRun(signal, {
|
||||||
publisher: {
|
publisher: {
|
||||||
mergeGeneratedPush: merged,
|
mergeGeneratedPush: merged,
|
||||||
noGeneratedChange: noChange,
|
noGeneratedChange: noChange,
|
||||||
|
|
@ -1648,13 +1685,13 @@ posixIt.each([
|
||||||
/Generated output was merged|Deferred stale|Neutralized stale/u,
|
/Generated output was merged|Deferred stale|Neutralized stale/u,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
55_000,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
posixIt.each([0, 2, 23, 125, 143, "hang", "cleanup-failure", "cancel"] as const)(
|
posixIt.for([0, 2, 23, 125, 143, "hang", "cleanup-failure", "cancel"] as const)(
|
||||||
"maturity branch lookup %s preserves 0/2/ordinary/fatal policy after drain",
|
"maturity branch lookup %s preserves 0/2/ordinary/fatal policy after drain",
|
||||||
async (code) => {
|
{ timeout: 55_000 },
|
||||||
const report = await maturityRun({
|
async (code, { signal }) => {
|
||||||
|
const report = await maturityRun(signal, {
|
||||||
env: { ...maturityEnvironment, INPUT_REF: "release/2026.8.1" },
|
env: { ...maturityEnvironment, INPUT_REF: "release/2026.8.1" },
|
||||||
gitFault: { match: "^ls-remote ", code },
|
gitFault: { match: "^ls-remote ", code },
|
||||||
});
|
});
|
||||||
|
|
@ -1686,13 +1723,12 @@ posixIt.each([0, 2, 23, 125, 143, "hang", "cleanup-failure", "cancel"] as const)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
55_000,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
posixIt(
|
posixIt(
|
||||||
"generated publisher retries one timed-out push under the unchanged lease",
|
"generated publisher retries one timed-out push under the unchanged lease",
|
||||||
async () => {
|
async ({ signal }) => {
|
||||||
const report = await publisherRun({
|
const report = await publisherRun(signal, {
|
||||||
gitFault: { match: "^push ", occurrence: 1, code: "hang" },
|
gitFault: { match: "^push ", occurrence: 1, code: "hang" },
|
||||||
});
|
});
|
||||||
expect(report.code, report.output).toBe(0);
|
expect(report.code, report.output).toBe(0);
|
||||||
|
|
@ -1705,7 +1741,7 @@ posixIt(
|
||||||
55_000,
|
55_000,
|
||||||
);
|
);
|
||||||
|
|
||||||
posixIt.each(
|
posixIt.for(
|
||||||
[
|
[
|
||||||
{ match: "^fetch ", occurrence: 1 },
|
{ match: "^fetch ", occurrence: 1 },
|
||||||
{ match: "^fetch ", occurrence: 2 },
|
{ match: "^fetch ", occurrence: 2 },
|
||||||
|
|
@ -1717,8 +1753,9 @@ posixIt.each(
|
||||||
),
|
),
|
||||||
)(
|
)(
|
||||||
"maturity $code at $match/$occurrence stops before fallback/output",
|
"maturity $code at $match/$occurrence stops before fallback/output",
|
||||||
async ({ match, occurrence, code }) => {
|
{ timeout: 55_000 },
|
||||||
const report = await maturityRun({
|
async ({ match, occurrence, code }, { signal }) => {
|
||||||
|
const report = await maturityRun(signal, {
|
||||||
env: { ...maturityEnvironment, EXPECTED_SHA: "" },
|
env: { ...maturityEnvironment, EXPECTED_SHA: "" },
|
||||||
gitFault: { match, occurrence, code },
|
gitFault: { match, occurrence, code },
|
||||||
});
|
});
|
||||||
|
|
@ -1727,18 +1764,18 @@ posixIt.each(
|
||||||
expect(report.githubOutput).toBe("");
|
expect(report.githubOutput).toBe("");
|
||||||
expect(report.githubSummary).toBe("");
|
expect(report.githubSummary).toBe("");
|
||||||
},
|
},
|
||||||
55_000,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
posixIt.each([
|
posixIt.for([
|
||||||
{ race: "delete", secondFailure: false, code: 0, pushes: 2, fetches: 2 },
|
{ race: "delete", secondFailure: false, code: 0, pushes: 2, fetches: 2 },
|
||||||
{ race: "advance", secondFailure: false, code: 1, pushes: 1, fetches: 1 },
|
{ race: "advance", secondFailure: false, code: 1, pushes: 1, fetches: 1 },
|
||||||
{ race: "recreate", secondFailure: false, code: 1, pushes: 2, fetches: 2 },
|
{ race: "recreate", secondFailure: false, code: 1, pushes: 2, fetches: 2 },
|
||||||
{ race: "delete", secondFailure: true, code: 1, pushes: 2, fetches: 2 },
|
{ race: "delete", secondFailure: true, code: 1, pushes: 2, fetches: 2 },
|
||||||
] as const)(
|
] as const)(
|
||||||
"generated publisher exact deletion-race lease policy ($race, second failure=$secondFailure)",
|
"generated publisher exact deletion-race lease policy ($race, second failure=$secondFailure)",
|
||||||
async ({ race, secondFailure, code, pushes, fetches }) => {
|
{ timeout: 55_000 },
|
||||||
const report = await publisherRun({
|
async ({ race, secondFailure, code, pushes, fetches }, { signal }) => {
|
||||||
|
const report = await publisherRun(signal, {
|
||||||
publisher: { existingPr: true, race, failGeneratedPush: secondFailure },
|
publisher: { existingPr: true, race, failGeneratedPush: secondFailure },
|
||||||
});
|
});
|
||||||
expect(report.code, report.output).toBe(code);
|
expect(report.code, report.output).toBe(code);
|
||||||
|
|
@ -1776,10 +1813,9 @@ posixIt.each([
|
||||||
).toEqual([]);
|
).toEqual([]);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
55_000,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
posixIt.each(
|
posixIt.for(
|
||||||
[
|
[
|
||||||
{ match: "^fetch ", occurrence: 2 },
|
{ match: "^fetch ", occurrence: 2 },
|
||||||
{ match: "^ls-tree ", occurrence: 5 },
|
{ match: "^ls-tree ", occurrence: 5 },
|
||||||
|
|
@ -1788,8 +1824,9 @@ posixIt.each(
|
||||||
),
|
),
|
||||||
)(
|
)(
|
||||||
"generated publisher verify_publication $code at $match is terminal",
|
"generated publisher verify_publication $code at $match is terminal",
|
||||||
async ({ match, occurrence, code }) => {
|
{ timeout: 55_000 },
|
||||||
const report = await publisherRun({
|
async ({ match, occurrence, code }, { signal }) => {
|
||||||
|
const report = await publisherRun(signal, {
|
||||||
publisher: { reconciliation: "missing" },
|
publisher: { reconciliation: "missing" },
|
||||||
gitFault: { match, occurrence, code },
|
gitFault: { match, occurrence, code },
|
||||||
});
|
});
|
||||||
|
|
@ -1801,13 +1838,13 @@ posixIt.each(
|
||||||
expect(report.commands.at(code === 23 ? -2 : -1)?.args.join(" ")).toMatch(new RegExp(match));
|
expect(report.commands.at(code === 23 ? -2 : -1)?.args.join(" ")).toMatch(new RegExp(match));
|
||||||
expect(report.output).not.toContain("Generated output was merged");
|
expect(report.output).not.toContain("Generated output was merged");
|
||||||
},
|
},
|
||||||
55_000,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
posixIt.each([0, 5, 125, "cleanup-failure", "cancel"] as const)(
|
posixIt.for([0, 5, 125, "cleanup-failure", "cancel"] as const)(
|
||||||
"generated publisher auth cleanup keeps ordinary tolerance but fences fatal %s",
|
"generated publisher auth cleanup keeps ordinary tolerance but fences fatal %s",
|
||||||
async (code) => {
|
{ timeout: 55_000 },
|
||||||
const report = await publisherRun({
|
async (code, { signal }) => {
|
||||||
|
const report = await publisherRun(signal, {
|
||||||
gitFault: { match: "^config --local --unset-all ", code },
|
gitFault: { match: "^config --local --unset-all ", code },
|
||||||
});
|
});
|
||||||
expect(report.code, report.output).toBe(
|
expect(report.code, report.output).toBe(
|
||||||
|
|
@ -1825,13 +1862,12 @@ posixIt.each([0, 5, 125, "cleanup-failure", "cancel"] as const)(
|
||||||
expect(text).not.toContain("test-token");
|
expect(text).not.toContain("test-token");
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
55_000,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
posixIt(
|
posixIt(
|
||||||
"generated publisher removes Git auth after an unexpected policy exception",
|
"generated publisher removes Git auth after an unexpected policy exception",
|
||||||
async () => {
|
async ({ signal }) => {
|
||||||
const report = await publisherRun({
|
const report = await publisherRun(signal, {
|
||||||
publisher: { autoMerge: true, malformedAutoMergeRecord: true },
|
publisher: { autoMerge: true, malformedAutoMergeRecord: true },
|
||||||
});
|
});
|
||||||
expect(report.code, report.output).toBe(125);
|
expect(report.code, report.output).toBe(125);
|
||||||
|
|
@ -1847,16 +1883,17 @@ posixIt(
|
||||||
55_000,
|
55_000,
|
||||||
);
|
);
|
||||||
|
|
||||||
posixIt.each(["main-ancestor", "release-tag", "release-branch-head", "floating-main"])(
|
posixIt.for(["main-ancestor", "release-tag", "release-branch-head", "floating-main"])(
|
||||||
"maturity preserves exact trust order, output hash bytes and fetches: %s",
|
"maturity preserves exact trust order, output hash bytes and fetches: %s",
|
||||||
async (reason) => {
|
{ timeout: 55_000 },
|
||||||
|
async (reason, { signal }) => {
|
||||||
const release = "release/2026.8.1";
|
const release = "release/2026.8.1";
|
||||||
const floating = reason === "floating-main";
|
const floating = reason === "floating-main";
|
||||||
const tag = reason === "release-tag";
|
const tag = reason === "release-tag";
|
||||||
const releaseBranch = reason === "release-branch-head";
|
const releaseBranch = reason === "release-branch-head";
|
||||||
const revision = floating ? "d".repeat(40) : head;
|
const revision = floating ? "d".repeat(40) : head;
|
||||||
const publicationBase = releaseBranch ? release : "main";
|
const publicationBase = releaseBranch ? release : "main";
|
||||||
const report = await maturityRun({
|
const report = await maturityRun(signal, {
|
||||||
realClock: true,
|
realClock: true,
|
||||||
realDrain: false,
|
realDrain: false,
|
||||||
env: {
|
env: {
|
||||||
|
|
@ -1922,29 +1959,30 @@ posixIt.each(["main-ancestor", "release-tag", "release-branch-head", "floating-m
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
55_000,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
posixIt.each(
|
posixIt.for(
|
||||||
["publisher", "maturity"].flatMap((surface) =>
|
["publisher", "maturity"].flatMap((surface) =>
|
||||||
(["owner", "python", "git"] as const).map((setupFailure) => ({ surface, setupFailure })),
|
(["owner", "python", "git"] as const).map((setupFailure) => ({ surface, setupFailure })),
|
||||||
),
|
),
|
||||||
)(
|
)(
|
||||||
"$surface setup failure ($setupFailure) never reaches Git, GH, or outputs",
|
"$surface setup failure ($setupFailure) never reaches Git, GH, or outputs",
|
||||||
async ({ surface, setupFailure }) => {
|
{ timeout: 55_000 },
|
||||||
const report = await (surface === "publisher" ? publisherRun : maturityRun)({ setupFailure });
|
async ({ surface, setupFailure }, { signal }) => {
|
||||||
|
const report = await (surface === "publisher" ? publisherRun : maturityRun)(signal, {
|
||||||
|
setupFailure,
|
||||||
|
});
|
||||||
expect(report.code).not.toBe(0);
|
expect(report.code).not.toBe(0);
|
||||||
expect(report.commands).toEqual([]);
|
expect(report.commands).toEqual([]);
|
||||||
expect(report.githubOutput).toBe("");
|
expect(report.githubOutput).toBe("");
|
||||||
expect(report.githubSummary).toBe("");
|
expect(report.githubSummary).toBe("");
|
||||||
},
|
},
|
||||||
55_000,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
posixIt(
|
posixIt(
|
||||||
"generated publisher reconciliation accepts a tree merged after PR mutation",
|
"generated publisher reconciliation accepts a tree merged after PR mutation",
|
||||||
async () => {
|
async ({ signal }) => {
|
||||||
const report = await publisherRun({ publisher: { reconciliation: "merged" } });
|
const report = await publisherRun(signal, { publisher: { reconciliation: "merged" } });
|
||||||
expect(report.code, report.output).toBe(0);
|
expect(report.code, report.output).toBe(0);
|
||||||
expect(report.fetches).toHaveLength(2);
|
expect(report.fetches).toHaveLength(2);
|
||||||
expect(report.pushes).toHaveLength(1);
|
expect(report.pushes).toHaveLength(1);
|
||||||
|
|
@ -1956,10 +1994,11 @@ posixIt(
|
||||||
55_000,
|
55_000,
|
||||||
);
|
);
|
||||||
|
|
||||||
posixIt.each([125, 143])(
|
posixIt.for([125, 143])(
|
||||||
"generated publisher ordinary stale-lease %s permits the exact deletion rebuild",
|
"generated publisher ordinary stale-lease %s permits the exact deletion rebuild",
|
||||||
async (code) => {
|
{ timeout: 55_000 },
|
||||||
const report = await publisherRun({
|
async (code, { signal }) => {
|
||||||
|
const report = await publisherRun(signal, {
|
||||||
publisher: { existingPr: true, race: "delete" },
|
publisher: { existingPr: true, race: "delete" },
|
||||||
gitFault: { match: "^push ", code, output: "stale info\n" },
|
gitFault: { match: "^push ", code, output: "stale info\n" },
|
||||||
});
|
});
|
||||||
|
|
@ -1972,10 +2011,9 @@ posixIt.each([125, 143])(
|
||||||
expect(report.publication?.generatedA).toBe("desired-a");
|
expect(report.publication?.generatedA).toBe("desired-a");
|
||||||
expect(report.authHeaderPresent).toBe(false);
|
expect(report.authHeaderPresent).toBe(false);
|
||||||
},
|
},
|
||||||
55_000,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
posixIt.each([
|
posixIt.for([
|
||||||
{
|
{
|
||||||
label: "invalid expected SHA",
|
label: "invalid expected SHA",
|
||||||
env: { EXPECTED_SHA: "bad" },
|
env: { EXPECTED_SHA: "bad" },
|
||||||
|
|
@ -2015,8 +2053,12 @@ posixIt.each([
|
||||||
},
|
},
|
||||||
])(
|
])(
|
||||||
"maturity rejects $label without outputs",
|
"maturity rejects $label without outputs",
|
||||||
async ({ env, fault, fetches, diagnostic, code }) => {
|
{ timeout: 55_000 },
|
||||||
const report = await maturityRun({ env: { ...maturityEnvironment, ...env }, gitFault: fault });
|
async ({ env, fault, fetches, diagnostic, code }, { signal }) => {
|
||||||
|
const report = await maturityRun(signal, {
|
||||||
|
env: { ...maturityEnvironment, ...env },
|
||||||
|
gitFault: fault,
|
||||||
|
});
|
||||||
expect(report.code, report.output).toBe(code ?? 1);
|
expect(report.code, report.output).toBe(code ?? 1);
|
||||||
expect(report.fetches).toHaveLength(fetches);
|
expect(report.fetches).toHaveLength(fetches);
|
||||||
expect(report.githubOutput).toBe("");
|
expect(report.githubOutput).toBe("");
|
||||||
|
|
@ -2025,5 +2067,4 @@ posixIt.each([
|
||||||
expect(report.output).toContain(diagnostic);
|
expect(report.output).toContain(diagnostic);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
55_000,
|
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -45,8 +45,11 @@ it.each([false, true])(
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
it("fetches selected history with the initial checkout before the test worker runs", async () => {
|
it("fetches selected history with the initial checkout before the test worker runs", async ({
|
||||||
|
signal,
|
||||||
|
}) => {
|
||||||
const report = await runCiGitStep({
|
const report = await runCiGitStep({
|
||||||
|
signal,
|
||||||
job: "checks-node-core-test-nondist-shard",
|
job: "checks-node-core-test-nondist-shard",
|
||||||
env: { CHECKOUT_GIT_COMMITS_JSON: JSON.stringify([reader.commit]) },
|
env: { CHECKOUT_GIT_COMMITS_JSON: JSON.stringify([reader.commit]) },
|
||||||
fetchResults: [0, 0],
|
fetchResults: [0, 0],
|
||||||
|
|
@ -56,10 +59,11 @@ it("fetches selected history with the initial checkout before the test worker ru
|
||||||
expect(report.fetches).toHaveLength(2);
|
expect(report.fetches).toHaveLength(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
it.each(["{}", '"main"', '["--upload-pack=bad"]', '["abc"]', "[null]"])(
|
it.for(["{}", '"main"', '["--upload-pack=bad"]', '["abc"]', "[null]"])(
|
||||||
"rejects malformed immutable history before checkout mutation: %s",
|
"rejects malformed immutable history before checkout mutation: %s",
|
||||||
async (input) => {
|
async (input, { signal }) => {
|
||||||
const report = await runCiGitStep({
|
const report = await runCiGitStep({
|
||||||
|
signal,
|
||||||
job: "checks-node-core-test-nondist-shard",
|
job: "checks-node-core-test-nondist-shard",
|
||||||
env: { CHECKOUT_GIT_COMMITS_JSON: input },
|
env: { CHECKOUT_GIT_COMMITS_JSON: input },
|
||||||
fetchResults: [],
|
fetchResults: [],
|
||||||
|
|
|
||||||
|
|
@ -38,10 +38,14 @@ const resetCases: { label: string; fetchResults: FetchResult[]; code: number; at
|
||||||
{ label: "timeouts exhausted", fetchResults: Array(5).fill("hang"), code: 1, attempts: 5 },
|
{ label: "timeouts exhausted", fetchResults: Array(5).fill("hang"), code: 1, attempts: 5 },
|
||||||
{ label: "unverified cleanup", fetchResults: ["cleanup-failure"], code: 125, attempts: 1 },
|
{ label: "unverified cleanup", fetchResults: ["cleanup-failure"], code: 125, attempts: 1 },
|
||||||
];
|
];
|
||||||
linuxIt.each(resetProfiles.flatMap((profile) => resetCases.map((entry) => ({ profile, entry }))))(
|
linuxIt.for(resetProfiles.flatMap((profile) => resetCases.map((entry) => ({ profile, entry }))))(
|
||||||
"$profile.job drains descendants before reset/reuse ($entry.label)",
|
"$profile.job drains descendants before reset/reuse ($entry.label)",
|
||||||
async ({ profile: { job, step, target, remote }, entry: { fetchResults, code, attempts } }) => {
|
{ timeout: 55_000 },
|
||||||
const report = await runCiGitStep({ job, step, fetchResults });
|
async (
|
||||||
|
{ profile: { job, step, target, remote }, entry: { fetchResults, code, attempts } },
|
||||||
|
{ signal },
|
||||||
|
) => {
|
||||||
|
const report = await runCiGitStep({ signal, job, step, fetchResults });
|
||||||
expect(report.code).toBe(code);
|
expect(report.code).toBe(code);
|
||||||
expect(report.readyAttempts).toHaveLength(attempts);
|
expect(report.readyAttempts).toHaveLength(attempts);
|
||||||
expect(report.fetches).toHaveLength(attempts);
|
expect(report.fetches).toHaveLength(attempts);
|
||||||
|
|
@ -61,17 +65,17 @@ linuxIt.each(resetProfiles.flatMap((profile) => resetCases.map((entry) => ({ pro
|
||||||
.every(({ args }) => args.at(-1) === `https://github.com/${remote}.git`),
|
.every(({ args }) => args.at(-1) === `https://github.com/${remote}.git`),
|
||||||
).toBe(true);
|
).toBe(true);
|
||||||
},
|
},
|
||||||
55_000,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
linuxIt.each([
|
linuxIt.for([
|
||||||
{ label: "timeout recovery", fetchResults: ["hang", 0], code: 0, attempts: 2 },
|
{ label: "timeout recovery", fetchResults: ["hang", 0], code: 0, attempts: 2 },
|
||||||
{ label: "timeouts exhausted", fetchResults: ["hang", "hang", "hang"], code: 124, attempts: 3 },
|
{ label: "timeouts exhausted", fetchResults: ["hang", "hang", "hang"], code: 124, attempts: 3 },
|
||||||
{ label: "ordinary Git failure", fetchResults: [23], code: 23, attempts: 1 },
|
{ label: "ordinary Git failure", fetchResults: [23], code: 23, attempts: 1 },
|
||||||
] satisfies { label: string; fetchResults: FetchResult[]; code: number; attempts: number }[])(
|
] satisfies { label: string; fetchResults: FetchResult[]; code: number; attempts: number }[])(
|
||||||
"skills preserves exact-SHA retries without a fallback ($label)",
|
"skills preserves exact-SHA retries without a fallback ($label)",
|
||||||
async ({ fetchResults, code, attempts }) => {
|
{ timeout: 55_000 },
|
||||||
const report = await runCiGitStep({ job: "skills-python", fetchResults });
|
async ({ fetchResults, code, attempts }, { signal }) => {
|
||||||
|
const report = await runCiGitStep({ signal, job: "skills-python", fetchResults });
|
||||||
expect(report.code).toBe(code);
|
expect(report.code).toBe(code);
|
||||||
expect(report.fetches).toHaveLength(attempts);
|
expect(report.fetches).toHaveLength(attempts);
|
||||||
expect(
|
expect(
|
||||||
|
|
@ -83,16 +87,16 @@ linuxIt.each([
|
||||||
expect(report.checkouts).toHaveLength(code === 0 ? 1 : 0);
|
expect(report.checkouts).toHaveLength(code === 0 ? 1 : 0);
|
||||||
expect(report.boundaries.some(({ name }) => name === "delete")).toBe(false);
|
expect(report.boundaries.some(({ name }) => name === "delete")).toBe(false);
|
||||||
},
|
},
|
||||||
55_000,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
linuxIt.each([
|
linuxIt.for([
|
||||||
{ phase: "fetch", fetchResults: [23, 0], checkoutResults: [], firstCheckout: false },
|
{ phase: "fetch", fetchResults: [23, 0], checkoutResults: [], firstCheckout: false },
|
||||||
{ phase: "checkout", fetchResults: [0, 0], checkoutResults: [23, 0], firstCheckout: true },
|
{ phase: "checkout", fetchResults: [0, 0], checkoutResults: [23, 0], firstCheckout: true },
|
||||||
])(
|
])(
|
||||||
"Android resets only after safely joined $phase failure",
|
"Android resets only after safely joined $phase failure",
|
||||||
async ({ fetchResults, checkoutResults, firstCheckout }) => {
|
{ timeout: 55_000 },
|
||||||
const report = await runCiGitStep({ job: "android", fetchResults, checkoutResults });
|
async ({ fetchResults, checkoutResults, firstCheckout }, { signal }) => {
|
||||||
|
const report = await runCiGitStep({ signal, job: "android", fetchResults, checkoutResults });
|
||||||
expect(report.code).toBe(0);
|
expect(report.code).toBe(0);
|
||||||
expect(report.readyAttempts).toEqual([1, 2]);
|
expect(report.readyAttempts).toEqual([1, 2]);
|
||||||
expect(report.fetches.map(({ args }) => args.at(-1))).toEqual([
|
expect(report.fetches.map(({ args }) => args.at(-1))).toEqual([
|
||||||
|
|
@ -112,14 +116,13 @@ linuxIt.each([
|
||||||
"checkout",
|
"checkout",
|
||||||
]);
|
]);
|
||||||
},
|
},
|
||||||
55_000,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const manualProfiles = [
|
const manualProfiles = [
|
||||||
{ job: "preflight", step: "Checkout", depth: 1 },
|
{ job: "preflight", step: "Checkout", depth: 1 },
|
||||||
{ job: "security-fast", step: "Checkout manual target", depth: 2 },
|
{ job: "security-fast", step: "Checkout manual target", depth: 2 },
|
||||||
];
|
];
|
||||||
linuxIt.each(
|
linuxIt.for(
|
||||||
manualProfiles.flatMap((profile) => [
|
manualProfiles.flatMap((profile) => [
|
||||||
{ ...profile, label: "missing branch", fetchResults: [128, 0] as FetchResult[], code: 0 },
|
{ ...profile, label: "missing branch", fetchResults: [128, 0] as FetchResult[], code: 0 },
|
||||||
{
|
{
|
||||||
|
|
@ -137,8 +140,10 @@ linuxIt.each(
|
||||||
]),
|
]),
|
||||||
)(
|
)(
|
||||||
"$job only falls back after a safely joined unavailable target ($label)",
|
"$job only falls back after a safely joined unavailable target ($label)",
|
||||||
async ({ job, step, depth, fetchResults, code }) => {
|
{ timeout: 55_000 },
|
||||||
|
async ({ job, step, depth, fetchResults, code }, { signal }) => {
|
||||||
const report = await runCiGitStep({
|
const report = await runCiGitStep({
|
||||||
|
signal,
|
||||||
job,
|
job,
|
||||||
step,
|
step,
|
||||||
fetchResults,
|
fetchResults,
|
||||||
|
|
@ -162,13 +167,13 @@ linuxIt.each(
|
||||||
);
|
);
|
||||||
expect(report.checkouts).toHaveLength(code === 0 ? 1 : 0);
|
expect(report.checkouts).toHaveLength(code === 0 ? 1 : 0);
|
||||||
},
|
},
|
||||||
55_000,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
linuxIt(
|
linuxIt(
|
||||||
"preflight pins a moved exact SHA and retries only its parent metadata",
|
"preflight pins a moved exact SHA and retries only its parent metadata",
|
||||||
async () => {
|
async ({ signal }) => {
|
||||||
const report = await runCiGitStep({
|
const report = await runCiGitStep({
|
||||||
|
signal,
|
||||||
job: "preflight",
|
job: "preflight",
|
||||||
fetchResults: [0, 0, 23, 0],
|
fetchResults: [0, 0, 23, 0],
|
||||||
env: { GITHUB_EVENT_NAME: "workflow_dispatch" },
|
env: { GITHUB_EVENT_NAME: "workflow_dispatch" },
|
||||||
|
|
@ -193,8 +198,9 @@ linuxIt(
|
||||||
|
|
||||||
linuxIt(
|
linuxIt(
|
||||||
"manual security never refetches an unavailable equal fallback",
|
"manual security never refetches an unavailable equal fallback",
|
||||||
async () => {
|
async ({ signal }) => {
|
||||||
const report = await runCiGitStep({
|
const report = await runCiGitStep({
|
||||||
|
signal,
|
||||||
job: "security-fast",
|
job: "security-fast",
|
||||||
step: "Checkout manual target",
|
step: "Checkout manual target",
|
||||||
env: { GITHUB_EVENT_NAME: "workflow_dispatch" },
|
env: { GITHUB_EVENT_NAME: "workflow_dispatch" },
|
||||||
|
|
@ -211,8 +217,9 @@ linuxIt(
|
||||||
|
|
||||||
linuxIt(
|
linuxIt(
|
||||||
"preflight rejects a fallback that cannot satisfy the requested exact SHA",
|
"preflight rejects a fallback that cannot satisfy the requested exact SHA",
|
||||||
async () => {
|
async ({ signal }) => {
|
||||||
const report = await runCiGitStep({
|
const report = await runCiGitStep({
|
||||||
|
signal,
|
||||||
job: "preflight",
|
job: "preflight",
|
||||||
env: { GITHUB_EVENT_NAME: "workflow_dispatch", CHECKOUT_REF: moved },
|
env: { GITHUB_EVENT_NAME: "workflow_dispatch", CHECKOUT_REF: moved },
|
||||||
fetchResults: [128, 0],
|
fetchResults: [128, 0],
|
||||||
|
|
@ -252,15 +259,15 @@ const preflightCases: {
|
||||||
code: 1,
|
code: 1,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
linuxIt.each(preflightCases)(
|
linuxIt.for(preflightCases)(
|
||||||
"preflight fails closed: $label",
|
"preflight fails closed: $label",
|
||||||
async ({ env, fetchResults, code }) => {
|
{ timeout: 55_000 },
|
||||||
const report = await runCiGitStep({ job: "preflight", env, fetchResults });
|
async ({ env, fetchResults, code }, { signal }) => {
|
||||||
|
const report = await runCiGitStep({ signal, job: "preflight", env, fetchResults });
|
||||||
expect(report.code).toBe(code);
|
expect(report.code).toBe(code);
|
||||||
expect(report.fetches).toHaveLength(fetchResults.length);
|
expect(report.fetches).toHaveLength(fetchResults.length);
|
||||||
expect(report.checkouts).toEqual([]);
|
expect(report.checkouts).toEqual([]);
|
||||||
},
|
},
|
||||||
55_000,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const historyProfiles: {
|
const historyProfiles: {
|
||||||
|
|
@ -283,7 +290,7 @@ const historyProfiles: {
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
linuxIt.each(
|
linuxIt.for(
|
||||||
historyProfiles.flatMap((profile) => [
|
historyProfiles.flatMap((profile) => [
|
||||||
{ ...profile, label: "successful leader exit", fetchResults: [0] as FetchResult[], code: 0 },
|
{ ...profile, label: "successful leader exit", fetchResults: [0] as FetchResult[], code: 0 },
|
||||||
{
|
{
|
||||||
|
|
@ -295,8 +302,10 @@ linuxIt.each(
|
||||||
]),
|
]),
|
||||||
)(
|
)(
|
||||||
"$job/$step joins supplemental history before consumption ($label, $target)",
|
"$job/$step joins supplemental history before consumption ($label, $target)",
|
||||||
async ({ job, step, env, target, fetchResults, code }) => {
|
{ timeout: 55_000 },
|
||||||
|
async ({ job, step, env, target, fetchResults, code }, { signal }) => {
|
||||||
const report = await runCiGitStep({
|
const report = await runCiGitStep({
|
||||||
|
signal,
|
||||||
job,
|
job,
|
||||||
step,
|
step,
|
||||||
env,
|
env,
|
||||||
|
|
@ -315,13 +324,13 @@ linuxIt.each(
|
||||||
expect(report.checkouts.map(({ args }) => args.at(-1))).toEqual(code === 0 ? [merge] : []);
|
expect(report.checkouts.map(({ args }) => args.at(-1))).toEqual(code === 0 ? [merge] : []);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
55_000,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
linuxIt(
|
linuxIt(
|
||||||
"ratchet retries a stale merge parent before checkout and base publication",
|
"ratchet retries a stale merge parent before checkout and base publication",
|
||||||
async () => {
|
async ({ signal }) => {
|
||||||
const report = await runCiGitStep({
|
const report = await runCiGitStep({
|
||||||
|
signal,
|
||||||
job: "checks-fast-core",
|
job: "checks-fast-core",
|
||||||
step: "Prepare release-gate ratchet merge tree",
|
step: "Prepare release-gate ratchet merge tree",
|
||||||
fetchResults: [0, 0],
|
fetchResults: [0, 0],
|
||||||
|
|
@ -351,8 +360,8 @@ linuxIt(
|
||||||
|
|
||||||
posixIt(
|
posixIt(
|
||||||
"fetches the CI harness without a second full-repository snapshot",
|
"fetches the CI harness without a second full-repository snapshot",
|
||||||
async () => {
|
async ({ signal }) => {
|
||||||
const report = await runCiGitStep({ job: "checks-fast-core", fetchResults: [0, 0] });
|
const report = await runCiGitStep({ signal, job: "checks-fast-core", fetchResults: [0, 0] });
|
||||||
expect(report.code).toBe(0);
|
expect(report.code).toBe(0);
|
||||||
const harnessDirectory = path.join(report.workspace, ".ci-harness");
|
const harnessDirectory = path.join(report.workspace, ".ci-harness");
|
||||||
const harnessCommands = report.commands.filter(
|
const harnessCommands = report.commands.filter(
|
||||||
|
|
@ -474,8 +483,9 @@ const qaGitCases: QaGitCase[] = [
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
function runQaGitCase(profile: QaGitCase, fetchResults: FetchResult[]) {
|
function runQaGitCase(signal: AbortSignal, profile: QaGitCase, fetchResults: FetchResult[]) {
|
||||||
return runCiGitStep({
|
return runCiGitStep({
|
||||||
|
signal,
|
||||||
workflow: {
|
workflow: {
|
||||||
file: ".github/workflows/qa-profile-evidence.yml",
|
file: ".github/workflows/qa-profile-evidence.yml",
|
||||||
job: profile.job,
|
job: profile.job,
|
||||||
|
|
@ -506,10 +516,12 @@ function runQaGitCase(profile: QaGitCase, fetchResults: FetchResult[]) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
posixIt.each(qaGitCases)(
|
posixIt.for(qaGitCases)(
|
||||||
"QA Git owner drains descendants before the next boundary: $label",
|
"QA Git owner drains descendants before the next boundary: $label",
|
||||||
async (profile) => {
|
{ timeout: 55_000 },
|
||||||
|
async (profile, { signal }) => {
|
||||||
const report = await runQaGitCase(
|
const report = await runQaGitCase(
|
||||||
|
signal,
|
||||||
profile,
|
profile,
|
||||||
profile.fetches.map(() => 0),
|
profile.fetches.map(() => 0),
|
||||||
);
|
);
|
||||||
|
|
@ -553,13 +565,13 @@ posixIt.each(qaGitCases)(
|
||||||
expect(report.githubEnv).toBe("");
|
expect(report.githubEnv).toBe("");
|
||||||
expect(report.githubPath).toBe("");
|
expect(report.githubPath).toBe("");
|
||||||
},
|
},
|
||||||
55_000,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
posixIt.each(qaGitCases.filter(({ label, reason }) => !reason || label === "main validation"))(
|
posixIt.for(qaGitCases.filter(({ label, reason }) => !reason || label === "main validation"))(
|
||||||
"QA Git owner stops without downstream work after cleanup failure: $label",
|
"QA Git owner stops without downstream work after cleanup failure: $label",
|
||||||
async (profile) => {
|
{ timeout: 55_000 },
|
||||||
const report = await runQaGitCase(profile, ["cleanup-failure"]);
|
async (profile, { signal }) => {
|
||||||
|
const report = await runQaGitCase(signal, profile, ["cleanup-failure"]);
|
||||||
expect(report.code, report.output).toBe(125);
|
expect(report.code, report.output).toBe(125);
|
||||||
expect(report.readyAttempts).toEqual([1]);
|
expect(report.readyAttempts).toEqual([1]);
|
||||||
expect(report.fetches.map(({ args }) => args)).toEqual([profile.fetches[0]]);
|
expect(report.fetches.map(({ args }) => args)).toEqual([profile.fetches[0]]);
|
||||||
|
|
@ -574,7 +586,6 @@ posixIt.each(qaGitCases.filter(({ label, reason }) => !reason || label === "main
|
||||||
expect(report.githubPath).toBe("");
|
expect(report.githubPath).toBe("");
|
||||||
expect(report.output).toContain("Git ownership/setup failed");
|
expect(report.output).toContain("Git ownership/setup failed");
|
||||||
},
|
},
|
||||||
55_000,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const mantisReleaseRef = "release/2026.8.1";
|
const mantisReleaseRef = "release/2026.8.1";
|
||||||
|
|
@ -603,7 +614,7 @@ const mantisCases = [
|
||||||
mismatch?: boolean;
|
mismatch?: boolean;
|
||||||
}[];
|
}[];
|
||||||
|
|
||||||
posixIt.each([
|
posixIt.for([
|
||||||
...mantisCases.map((entry) => Object.assign({}, entry, { failure: 0 as FetchResult })),
|
...mantisCases.map((entry) => Object.assign({}, entry, { failure: 0 as FetchResult })),
|
||||||
...[true, false].flatMap((shared) =>
|
...[true, false].flatMap((shared) =>
|
||||||
(["cleanup-failure", 23] satisfies FetchResult[]).map((failure) => ({
|
(["cleanup-failure", 23] satisfies FetchResult[]).map((failure) => ({
|
||||||
|
|
@ -614,13 +625,15 @@ posixIt.each([
|
||||||
),
|
),
|
||||||
])(
|
])(
|
||||||
"Mantis ref Git owner drains before trust probes and publication: $label",
|
"Mantis ref Git owner drains before trust probes and publication: $label",
|
||||||
async (profile) => {
|
{ timeout: 55_000 },
|
||||||
|
async (profile, { signal }) => {
|
||||||
const { shared, failure } = profile;
|
const { shared, failure } = profile;
|
||||||
const baseline = "baseline" in profile && profile.baseline;
|
const baseline = "baseline" in profile && profile.baseline;
|
||||||
const release = "release" in profile && profile.release;
|
const release = "release" in profile && profile.release;
|
||||||
const mismatch = "mismatch" in profile && profile.mismatch;
|
const mismatch = "mismatch" in profile && profile.mismatch;
|
||||||
const fetches = release ? [qaMainFetch, mantisReleaseFetch] : [qaMainFetch];
|
const fetches = release ? [qaMainFetch, mantisReleaseFetch] : [qaMainFetch];
|
||||||
const report = await runCiGitStep({
|
const report = await runCiGitStep({
|
||||||
|
signal,
|
||||||
...(shared
|
...(shared
|
||||||
? ({ action: "mantis-validate-trusted-ref", step: "Validate refs are trusted" } as const)
|
? ({ action: "mantis-validate-trusted-ref", step: "Validate refs are trusted" } as const)
|
||||||
: {
|
: {
|
||||||
|
|
@ -704,7 +717,6 @@ posixIt.each([
|
||||||
expect(report.output).toContain("not trusted for this secret-bearing Mantis run");
|
expect(report.output).toContain("not trusted for this secret-bearing Mantis run");
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
55_000,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const mantisWorktrees = [
|
const mantisWorktrees = [
|
||||||
|
|
@ -731,13 +743,15 @@ const mantisWorktrees = [
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
posixIt.each([
|
posixIt.for([
|
||||||
...mantisWorktrees.map((profile) => ({ ...profile, failure: false })),
|
...mantisWorktrees.map((profile) => ({ ...profile, failure: false })),
|
||||||
{ ...mantisWorktrees[0]!, failure: true },
|
{ ...mantisWorktrees[0]!, failure: true },
|
||||||
])(
|
])(
|
||||||
"Mantis worktree Git owner drains before next worktree/install/build: $workflow (cleanup failure=$failure)",
|
"Mantis worktree Git owner drains before next worktree/install/build: $workflow (cleanup failure=$failure)",
|
||||||
async ({ workflow, job, lanes, offline, build, failure }) => {
|
{ timeout: 55_000 },
|
||||||
|
async ({ workflow, job, lanes, offline, build, failure }, { signal }) => {
|
||||||
const report = await runCiGitStep({
|
const report = await runCiGitStep({
|
||||||
|
signal,
|
||||||
workflow: {
|
workflow: {
|
||||||
file: `.github/workflows/mantis-${workflow}.yml`,
|
file: `.github/workflows/mantis-${workflow}.yml`,
|
||||||
job,
|
job,
|
||||||
|
|
@ -817,7 +831,6 @@ posixIt.each([
|
||||||
expect(report.output).toContain("Git ownership/setup failed");
|
expect(report.output).toContain("Git ownership/setup failed");
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
55_000,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const newer = "d".repeat(40);
|
const newer = "d".repeat(40);
|
||||||
|
|
@ -847,8 +860,13 @@ const commit = [
|
||||||
["commit", "-m", `chore(sync): mirror docs from fixture/checkout@${candidate}`],
|
["commit", "-m", `chore(sync): mirror docs from fixture/checkout@${candidate}`],
|
||||||
];
|
];
|
||||||
|
|
||||||
function runDocs(step: string, options: Partial<Parameters<typeof runCiGitStep>[0]> = {}) {
|
function runDocs(
|
||||||
|
signal: AbortSignal,
|
||||||
|
step: string,
|
||||||
|
options: Partial<Parameters<typeof runCiGitStep>[0]> = {},
|
||||||
|
) {
|
||||||
return runCiGitStep({
|
return runCiGitStep({
|
||||||
|
signal,
|
||||||
workflow: { file: ".github/workflows/docs-sync-publish.yml", job: "sync-publish-repo", step },
|
workflow: { file: ".github/workflows/docs-sync-publish.yml", job: "sync-publish-repo", step },
|
||||||
fetchResults: [],
|
fetchResults: [],
|
||||||
objects: { [sourceObject]: { text: JSON.stringify({ sha: candidate }) } },
|
objects: { [sourceObject]: { text: JSON.stringify({ sha: candidate }) } },
|
||||||
|
|
@ -865,10 +883,14 @@ function backoffs(report: Awaited<ReturnType<typeof runDocs>>) {
|
||||||
return [...report.output.matchAll(/fixture backoff: (\d+)/gu)].map((match) => Number(match[1]));
|
return [...report.output.matchAll(/fixture backoff: (\d+)/gu)].map((match) => Number(match[1]));
|
||||||
}
|
}
|
||||||
|
|
||||||
posixIt.each(["directory", "file", "symlink"] as const)(
|
posixIt.for(["directory", "file", "symlink"] as const)(
|
||||||
"docs clone drains an ordinary failure before deleting/retrying (%s)",
|
"docs clone drains an ordinary failure before deleting/retrying (%s)",
|
||||||
async (publishPath) => {
|
{ timeout: 55_000 },
|
||||||
const report = await runDocs("Clone publish repo", { cloneResults: [23, 0], publishPath });
|
async (publishPath, { signal }) => {
|
||||||
|
const report = await runDocs(signal, "Clone publish repo", {
|
||||||
|
cloneResults: [23, 0],
|
||||||
|
publishPath,
|
||||||
|
});
|
||||||
expect(report.code, report.output).toBe(0);
|
expect(report.code, report.output).toBe(0);
|
||||||
expect(report.readyAttempts).toEqual([1, 2]);
|
expect(report.readyAttempts).toEqual([1, 2]);
|
||||||
expect(gitArgs(report)).toEqual(
|
expect(gitArgs(report)).toEqual(
|
||||||
|
|
@ -889,13 +911,14 @@ posixIt.each(["directory", "file", "symlink"] as const)(
|
||||||
expect(report.output).toContain("Clone attempt 1 failed; retrying.");
|
expect(report.output).toContain("Clone attempt 1 failed; retrying.");
|
||||||
expect(report.output).not.toContain("fixture-docs-token");
|
expect(report.output).not.toContain("fixture-docs-token");
|
||||||
},
|
},
|
||||||
55_000,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
posixIt(
|
posixIt(
|
||||||
"docs clone cleanup uncertainty is terminal before another deletion or clone",
|
"docs clone cleanup uncertainty is terminal before another deletion or clone",
|
||||||
async () => {
|
async ({ signal }) => {
|
||||||
const report = await runDocs("Clone publish repo", { cloneResults: ["cleanup-failure"] });
|
const report = await runDocs(signal, "Clone publish repo", {
|
||||||
|
cloneResults: ["cleanup-failure"],
|
||||||
|
});
|
||||||
expect(report.code, report.output).toBe(125);
|
expect(report.code, report.output).toBe(125);
|
||||||
expect(report.clones).toHaveLength(1);
|
expect(report.clones).toHaveLength(1);
|
||||||
expect(report.boundaries.map(({ name }) => name)).toEqual(["delete", "clone:1", "exit"]);
|
expect(report.boundaries.map(({ name }) => name)).toEqual(["delete", "clone:1", "exit"]);
|
||||||
|
|
@ -906,10 +929,13 @@ posixIt(
|
||||||
55_000,
|
55_000,
|
||||||
);
|
);
|
||||||
|
|
||||||
posixIt.each([125, "hang"] satisfies FetchResult[])(
|
posixIt.for([125, "hang"] satisfies FetchResult[])(
|
||||||
"docs advisory fetch drains before config/add/commit and still continues (%s)",
|
"docs advisory fetch drains before config/add/commit and still continues (%s)",
|
||||||
async (failure) => {
|
{ timeout: 55_000 },
|
||||||
const report = await runDocs("Commit publish repo sync", { fetchResults: [failure, 0] });
|
async (failure, { signal }) => {
|
||||||
|
const report = await runDocs(signal, "Commit publish repo sync", {
|
||||||
|
fetchResults: [failure, 0],
|
||||||
|
});
|
||||||
expect(report.code, report.output).toBe(0);
|
expect(report.code, report.output).toBe(0);
|
||||||
expect(gitArgs(report)).toEqual([
|
expect(gitArgs(report)).toEqual([
|
||||||
diff,
|
diff,
|
||||||
|
|
@ -950,17 +976,17 @@ posixIt.each([125, "hang"] satisfies FetchResult[])(
|
||||||
"exit",
|
"exit",
|
||||||
]);
|
]);
|
||||||
},
|
},
|
||||||
55_000,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
posixIt.each([
|
posixIt.for([
|
||||||
{ operation: "push", failure: 23, lockChange: true },
|
{ operation: "push", failure: 23, lockChange: true },
|
||||||
{ operation: "rebase", failure: 125, lockChange: false },
|
{ operation: "rebase", failure: 125, lockChange: false },
|
||||||
{ operation: "push", failure: 143, lockChange: false },
|
{ operation: "push", failure: 143, lockChange: false },
|
||||||
])(
|
])(
|
||||||
"docs publication drains failed $operation ($failure) before abort/next fetch and then succeeds",
|
"docs publication drains failed $operation ($failure) before abort/next fetch and then succeeds",
|
||||||
async ({ operation, failure, lockChange }) => {
|
{ timeout: 55_000 },
|
||||||
const report = await runDocs("Commit publish repo sync", {
|
async ({ operation, failure, lockChange }, { signal }) => {
|
||||||
|
const report = await runDocs(signal, "Commit publish repo sync", {
|
||||||
env: lockChange ? { FIXTURE_DOCS_LOCK_AFTER_REBASE: "1" } : {},
|
env: lockChange ? { FIXTURE_DOCS_LOCK_AFTER_REBASE: "1" } : {},
|
||||||
rebaseResults: operation === "rebase" ? [failure, 0] : [],
|
rebaseResults: operation === "rebase" ? [failure, 0] : [],
|
||||||
pushResults: operation === "push" ? [failure, 0] : [],
|
pushResults: operation === "push" ? [failure, 0] : [],
|
||||||
|
|
@ -990,13 +1016,12 @@ posixIt.each([
|
||||||
expect(report.output).toContain("Reused 1 unchanged successful page check(s).");
|
expect(report.output).toContain("Reused 1 unchanged successful page check(s).");
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
55_000,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
posixIt(
|
posixIt(
|
||||||
"docs publication rejects invalid content introduced by the final rebase",
|
"docs publication rejects invalid content introduced by the final rebase",
|
||||||
async () => {
|
async ({ signal }) => {
|
||||||
const report = await runDocs("Commit publish repo sync", {
|
const report = await runDocs(signal, "Commit publish repo sync", {
|
||||||
env: { FIXTURE_DOCS_MDX_AFTER_REBASE: "# Rebased page\n\n{unfinished\n" },
|
env: { FIXTURE_DOCS_MDX_AFTER_REBASE: "# Rebased page\n\n{unfinished\n" },
|
||||||
});
|
});
|
||||||
expect(report.code, report.output).toBe(125);
|
expect(report.code, report.output).toBe(125);
|
||||||
|
|
@ -1007,10 +1032,11 @@ posixIt(
|
||||||
55_000,
|
55_000,
|
||||||
);
|
);
|
||||||
|
|
||||||
posixIt.each(["advisory fetch", "fetch", "rebase", "manifest", "lock", "push"] as const)(
|
posixIt.for(["advisory fetch", "fetch", "rebase", "manifest", "lock", "push"] as const)(
|
||||||
"docs publication cleanup uncertainty at %s prevents abort/retry/next Git",
|
"docs publication cleanup uncertainty at %s prevents abort/retry/next Git",
|
||||||
async (operation) => {
|
{ timeout: 55_000 },
|
||||||
const report = await runDocs("Commit publish repo sync", {
|
async (operation, { signal }) => {
|
||||||
|
const report = await runDocs(signal, "Commit publish repo sync", {
|
||||||
fetchResults:
|
fetchResults:
|
||||||
operation === "advisory fetch"
|
operation === "advisory fetch"
|
||||||
? ["cleanup-failure"]
|
? ["cleanup-failure"]
|
||||||
|
|
@ -1056,14 +1082,14 @@ posixIt.each(["advisory fetch", "fetch", "rebase", "manifest", "lock", "push"] a
|
||||||
expect(report.output).toContain("Git ownership/setup failed");
|
expect(report.output).toContain("Git ownership/setup failed");
|
||||||
expect(report.output).not.toContain("retrying");
|
expect(report.output).not.toContain("retrying");
|
||||||
},
|
},
|
||||||
55_000,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
posixIt.each(["Clone publish repo", "Commit publish repo sync"])(
|
posixIt.for(["Clone publish repo", "Commit publish repo sync"])(
|
||||||
"docs %s preserves five attempts and every backoff including the terminal one",
|
"docs %s preserves five attempts and every backoff including the terminal one",
|
||||||
async (step) => {
|
{ timeout: 55_000 },
|
||||||
|
async (step, { signal }) => {
|
||||||
const cloning = step === "Clone publish repo";
|
const cloning = step === "Clone publish repo";
|
||||||
const report = await runDocs(step, {
|
const report = await runDocs(signal, step, {
|
||||||
cloneResults: cloning ? Array<FetchResult>(5).fill(23) : [],
|
cloneResults: cloning ? Array<FetchResult>(5).fill(23) : [],
|
||||||
fetchResults: cloning ? [] : [0, ...Array<FetchResult>(5).fill(23)],
|
fetchResults: cloning ? [] : [0, ...Array<FetchResult>(5).fill(23)],
|
||||||
});
|
});
|
||||||
|
|
@ -1085,16 +1111,16 @@ posixIt.each(["Clone publish repo", "Commit publish repo sync"])(
|
||||||
),
|
),
|
||||||
).toBe(true);
|
).toBe(true);
|
||||||
},
|
},
|
||||||
55_000,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
posixIt.each([
|
posixIt.for([
|
||||||
{ label: "no changes", diffResult: 0, stale: false },
|
{ label: "no changes", diffResult: 0, stale: false },
|
||||||
{ label: "stale source", diffResult: 1, stale: true },
|
{ label: "stale source", diffResult: 1, stale: true },
|
||||||
])(
|
])(
|
||||||
"docs publication exits successfully without committing for $label",
|
"docs publication exits successfully without committing for $label",
|
||||||
async ({ diffResult, stale }) => {
|
{ timeout: 55_000 },
|
||||||
const report = await runDocs("Commit publish repo sync", {
|
async ({ diffResult, stale }, { signal }) => {
|
||||||
|
const report = await runDocs(signal, "Commit publish repo sync", {
|
||||||
diffResult,
|
diffResult,
|
||||||
objects: { [sourceObject]: { text: JSON.stringify({ sha: newer }) } },
|
objects: { [sourceObject]: { text: JSON.stringify({ sha: newer }) } },
|
||||||
mergeBase: { ancestor: true, revision: candidate },
|
mergeBase: { ancestor: true, revision: candidate },
|
||||||
|
|
@ -1112,10 +1138,9 @@ posixIt.each([
|
||||||
expect(report.commands.at(-1)?.cwd).toBe(report.workspace);
|
expect(report.commands.at(-1)?.cwd).toBe(report.workspace);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
55_000,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
posixIt.each([
|
posixIt.for([
|
||||||
{ label: "missing metadata", text: "", code: 128, ancestor: false },
|
{ label: "missing metadata", text: "", code: 128, ancestor: false },
|
||||||
{ label: "malformed JSON", text: "{", code: 0, ancestor: false },
|
{ label: "malformed JSON", text: "{", code: 0, ancestor: false },
|
||||||
{ label: "non-JSON constant", text: `{"sha":"${newer}","value":NaN}`, code: 0, ancestor: true },
|
{ label: "non-JSON constant", text: `{"sha":"${newer}","value":NaN}`, code: 0, ancestor: true },
|
||||||
|
|
@ -1124,8 +1149,9 @@ posixIt.each([
|
||||||
{ label: "unrelated source", text: JSON.stringify({ sha: newer }), code: 0, ancestor: false },
|
{ label: "unrelated source", text: JSON.stringify({ sha: newer }), code: 0, ancestor: false },
|
||||||
])(
|
])(
|
||||||
"docs publication retains the changed path for $label",
|
"docs publication retains the changed path for $label",
|
||||||
async ({ text, code, ancestor, label }) => {
|
{ timeout: 55_000 },
|
||||||
const report = await runDocs("Commit publish repo sync", {
|
async ({ text, code, ancestor, label }, { signal }) => {
|
||||||
|
const report = await runDocs(signal, "Commit publish repo sync", {
|
||||||
objects: { [sourceObject]: { text, code } },
|
objects: { [sourceObject]: { text, code } },
|
||||||
mergeBase: { ancestor, revision: candidate },
|
mergeBase: { ancestor, revision: candidate },
|
||||||
});
|
});
|
||||||
|
|
@ -1146,10 +1172,9 @@ posixIt.each([
|
||||||
push,
|
push,
|
||||||
]);
|
]);
|
||||||
},
|
},
|
||||||
55_000,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
posixIt.each([
|
posixIt.for([
|
||||||
{
|
{
|
||||||
label: "malformed remote manifest",
|
label: "malformed remote manifest",
|
||||||
text: "{",
|
text: "{",
|
||||||
|
|
@ -1168,8 +1193,9 @@ posixIt.each([
|
||||||
},
|
},
|
||||||
])(
|
])(
|
||||||
"docs publication rejects $label before push without Git retries",
|
"docs publication rejects $label before push without Git retries",
|
||||||
async ({ text, validates, error }) => {
|
{ timeout: 55_000 },
|
||||||
const report = await runDocs("Commit publish repo sync", {
|
async ({ text, validates, error }, { signal }) => {
|
||||||
|
const report = await runDocs(signal, "Commit publish repo sync", {
|
||||||
objects: {
|
objects: {
|
||||||
[sourceObject]: { text: JSON.stringify({ sha: candidate }) },
|
[sourceObject]: { text: JSON.stringify({ sha: candidate }) },
|
||||||
[dependencyReads[0]![1]!]: { text },
|
[dependencyReads[0]![1]!]: { text },
|
||||||
|
|
@ -1192,13 +1218,13 @@ posixIt.each([
|
||||||
expect(report.rebases.map(({ args }) => args)).toEqual([rebase]);
|
expect(report.rebases.map(({ args }) => args)).toEqual([rebase]);
|
||||||
expect(backoffs(report)).toEqual([]);
|
expect(backoffs(report)).toEqual([]);
|
||||||
},
|
},
|
||||||
55_000,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
posixIt.each([0, 23, "cleanup-failure"] satisfies FetchResult[])(
|
posixIt.for([0, 23, "cleanup-failure"] satisfies FetchResult[])(
|
||||||
"docs ClawHub HEAD is owned before Node consumption (%s)",
|
"docs ClawHub HEAD is owned before Node consumption (%s)",
|
||||||
async (revParseResult) => {
|
{ timeout: 55_000 },
|
||||||
const report = await runDocs("Sync docs into publish repo", { revParseResult });
|
async (revParseResult, { signal }) => {
|
||||||
|
const report = await runDocs(signal, "Sync docs into publish repo", { revParseResult });
|
||||||
expect(report.code, report.output).toBe(
|
expect(report.code, report.output).toBe(
|
||||||
revParseResult === "cleanup-failure" ? 125 : revParseResult,
|
revParseResult === "cleanup-failure" ? 125 : revParseResult,
|
||||||
);
|
);
|
||||||
|
|
@ -1226,13 +1252,13 @@ posixIt.each([0, 23, "cleanup-failure"] satisfies FetchResult[])(
|
||||||
: [],
|
: [],
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
55_000,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
posixIt.each(["Clone publish repo", "Commit publish repo sync"])(
|
posixIt.for(["Clone publish repo", "Commit publish repo sync"])(
|
||||||
"docs %s cancellation never reaches retry, abort, or the next Git call",
|
"docs %s cancellation never reaches retry, abort, or the next Git call",
|
||||||
async (step) => {
|
{ timeout: 55_000 },
|
||||||
const report = await runDocs(step, {
|
async (step, { signal }) => {
|
||||||
|
const report = await runDocs(signal, step, {
|
||||||
scenario: "cancel-SIGTERM",
|
scenario: "cancel-SIGTERM",
|
||||||
cloneResults: ["hang"],
|
cloneResults: ["hang"],
|
||||||
fetchResults: ["hang"],
|
fetchResults: ["hang"],
|
||||||
|
|
@ -1251,7 +1277,6 @@ posixIt.each(["Clone publish repo", "Commit publish repo sync"])(
|
||||||
step === "Clone publish repo" ? 1 : 0,
|
step === "Clone publish repo" ? 1 : 0,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
55_000,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const agentGate = "Gate trusted main activity and hourly cadence";
|
const agentGate = "Gate trusted main activity and hourly cadence";
|
||||||
|
|
@ -1272,8 +1297,13 @@ const agentCommitCommands = [
|
||||||
const agentOutput = (reviewBase = base) =>
|
const agentOutput = (reviewBase = base) =>
|
||||||
`run_agent=true\nbase_sha=${candidate}\nreview_base_sha=${reviewBase}\nreview_head_sha=${candidate}\n`;
|
`run_agent=true\nbase_sha=${candidate}\nreview_base_sha=${reviewBase}\nreview_head_sha=${candidate}\n`;
|
||||||
|
|
||||||
function runDocsAgent(step: string, options: Partial<Parameters<typeof runCiGitStep>[0]> = {}) {
|
function runDocsAgent(
|
||||||
|
signal: AbortSignal,
|
||||||
|
step: string,
|
||||||
|
options: Partial<Parameters<typeof runCiGitStep>[0]> = {},
|
||||||
|
) {
|
||||||
return runCiGitStep({
|
return runCiGitStep({
|
||||||
|
signal,
|
||||||
...options,
|
...options,
|
||||||
workflow: { file: ".github/workflows/docs-agent.yml", job: "update-docs", step },
|
workflow: { file: ".github/workflows/docs-agent.yml", job: "update-docs", step },
|
||||||
fetchResults: options.fetchResults ?? [],
|
fetchResults: options.fetchResults ?? [],
|
||||||
|
|
@ -1290,10 +1320,11 @@ function runDocsAgent(step: string, options: Partial<Parameters<typeof runCiGitS
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
posixIt.each([0, 128, 125])(
|
posixIt.for([0, 128, 125])(
|
||||||
"Docs Agent manual gate owns HEAD and parent before exact outputs (parent=%s)",
|
"Docs Agent manual gate owns HEAD and parent before exact outputs (parent=%s)",
|
||||||
async (code) => {
|
{ timeout: 55_000 },
|
||||||
const report = await runDocsAgent(agentGate, {
|
async (code, { signal }) => {
|
||||||
|
const report = await runDocsAgent(signal, agentGate, {
|
||||||
env: { EVENT_NAME: "workflow_dispatch" },
|
env: { EVENT_NAME: "workflow_dispatch" },
|
||||||
commandResults: { "rev-parse HEAD": { code: 0 }, [`rev-parse ${candidate}^`]: { code } },
|
commandResults: { "rev-parse HEAD": { code: 0 }, [`rev-parse ${candidate}^`]: { code } },
|
||||||
});
|
});
|
||||||
|
|
@ -1305,13 +1336,13 @@ posixIt.each([0, 128, 125])(
|
||||||
expect(report.commands.filter(({ tool }) => tool === "gh")).toEqual([]);
|
expect(report.commands.filter(({ tool }) => tool === "gh")).toEqual([]);
|
||||||
expect(report.githubOutput).toBe(agentOutput(code === 0 ? base : candidate));
|
expect(report.githubOutput).toBe(agentOutput(code === 0 ? base : candidate));
|
||||||
},
|
},
|
||||||
55_000,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
posixIt.each([125, "hang"] satisfies FetchResult[])(
|
posixIt.for([125, "hang"] satisfies FetchResult[])(
|
||||||
"Docs Agent gate drains failed fetch before retry, remote read, gh and output (%s)",
|
"Docs Agent gate drains failed fetch before retry, remote read, gh and output (%s)",
|
||||||
async (failure) => {
|
{ timeout: 55_000 },
|
||||||
const report = await runDocsAgent(agentGate, { fetchResults: [failure, 0] });
|
async (failure, { signal }) => {
|
||||||
|
const report = await runDocsAgent(signal, agentGate, { fetchResults: [failure, 0] });
|
||||||
expect(report.code, report.output).toBe(0);
|
expect(report.code, report.output).toBe(0);
|
||||||
expect(report.fetches.map(({ args }) => args)).toEqual([agentFetch, agentFetch]);
|
expect(report.fetches.map(({ args }) => args)).toEqual([agentFetch, agentFetch]);
|
||||||
expect(backoffs(report)).toEqual([2]);
|
expect(backoffs(report)).toEqual([2]);
|
||||||
|
|
@ -1332,13 +1363,13 @@ posixIt.each([125, "hang"] satisfies FetchResult[])(
|
||||||
],
|
],
|
||||||
]);
|
]);
|
||||||
},
|
},
|
||||||
55_000,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
posixIt.each([false, true])(
|
posixIt.for([false, true])(
|
||||||
"Docs Agent gate stops before gh/output/retry on fatal cleanup (cancel=%s)",
|
"Docs Agent gate stops before gh/output/retry on fatal cleanup (cancel=%s)",
|
||||||
async (cancel) => {
|
{ timeout: 55_000 },
|
||||||
const report = await runDocsAgent(agentGate, {
|
async (cancel, { signal }) => {
|
||||||
|
const report = await runDocsAgent(signal, agentGate, {
|
||||||
fetchResults: cancel ? ["hang"] : ["cleanup-failure"],
|
fetchResults: cancel ? ["hang"] : ["cleanup-failure"],
|
||||||
...(cancel ? { scenario: "cancel-SIGTERM", cooperativeTrees: true, realClock: true } : {}),
|
...(cancel ? { scenario: "cancel-SIGTERM", cooperativeTrees: true, realClock: true } : {}),
|
||||||
});
|
});
|
||||||
|
|
@ -1349,13 +1380,12 @@ posixIt.each([false, true])(
|
||||||
expect(backoffs(report)).toEqual([]);
|
expect(backoffs(report)).toEqual([]);
|
||||||
expect(report.output).not.toContain("retrying");
|
expect(report.output).not.toContain("retrying");
|
||||||
},
|
},
|
||||||
55_000,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
posixIt(
|
posixIt(
|
||||||
"Docs Agent superseded gate drains before false output without gh",
|
"Docs Agent superseded gate drains before false output without gh",
|
||||||
async () => {
|
async ({ signal }) => {
|
||||||
const report = await runDocsAgent(agentGate, { revisions: { "origin/main": moved } });
|
const report = await runDocsAgent(signal, agentGate, { revisions: { "origin/main": moved } });
|
||||||
expect(report.code, report.output).toBe(0);
|
expect(report.code, report.output).toBe(0);
|
||||||
expect(gitArgs(report)).toEqual([agentFetch, ["rev-parse", "origin/main"]]);
|
expect(gitArgs(report)).toEqual([agentFetch, ["rev-parse", "origin/main"]]);
|
||||||
expect(report.githubOutput).toBe("run_agent=false\n");
|
expect(report.githubOutput).toBe("run_agent=false\n");
|
||||||
|
|
@ -1367,7 +1397,7 @@ posixIt(
|
||||||
55_000,
|
55_000,
|
||||||
);
|
);
|
||||||
|
|
||||||
posixIt.each([
|
posixIt.for([
|
||||||
{ probe: 128, parent: 0, code: 0, reviewBase: base },
|
{ probe: 128, parent: 0, code: 0, reviewBase: base },
|
||||||
{ probe: 128, parent: 128, code: 0, reviewBase: candidate },
|
{ probe: 128, parent: 128, code: 0, reviewBase: candidate },
|
||||||
{ probe: 0, parent: 0, code: 0, reviewBase: moved },
|
{ probe: 0, parent: 0, code: 0, reviewBase: moved },
|
||||||
|
|
@ -1375,8 +1405,9 @@ posixIt.each([
|
||||||
{ probe: 128, parent: "cleanup-failure", code: 125, reviewBase: "" },
|
{ probe: 128, parent: "cleanup-failure", code: 125, reviewBase: "" },
|
||||||
] satisfies { probe: FetchResult; parent: FetchResult; code: number; reviewBase: string }[])(
|
] satisfies { probe: FetchResult; parent: FetchResult; code: number; reviewBase: string }[])(
|
||||||
"Docs Agent review base only falls back after ordinary Git failure ($probe/$parent)",
|
"Docs Agent review base only falls back after ordinary Git failure ($probe/$parent)",
|
||||||
async ({ probe, parent, code, reviewBase }) => {
|
{ timeout: 55_000 },
|
||||||
const report = await runDocsAgent(agentGate, {
|
async ({ probe, parent, code, reviewBase }, { signal }) => {
|
||||||
|
const report = await runDocsAgent(signal, agentGate, {
|
||||||
workflowRuns: [
|
workflowRuns: [
|
||||||
{
|
{
|
||||||
id: 122,
|
id: 122,
|
||||||
|
|
@ -1436,13 +1467,12 @@ posixIt.each([
|
||||||
]);
|
]);
|
||||||
expect(backoffs(report)).toEqual([]);
|
expect(backoffs(report)).toEqual([]);
|
||||||
},
|
},
|
||||||
55_000,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
posixIt(
|
posixIt(
|
||||||
"Docs Agent no-change commit owns diff before successful exit",
|
"Docs Agent no-change commit owns diff before successful exit",
|
||||||
async () => {
|
async ({ signal }) => {
|
||||||
const report = await runDocsAgent(agentCommit, {
|
const report = await runDocsAgent(signal, agentCommit, {
|
||||||
commandResults: { "diff HEAD --quiet": { code: 0 } },
|
commandResults: { "diff HEAD --quiet": { code: 0 } },
|
||||||
});
|
});
|
||||||
expect(report.code, report.output).toBe(0);
|
expect(report.code, report.output).toBe(0);
|
||||||
|
|
@ -1452,10 +1482,11 @@ posixIt(
|
||||||
55_000,
|
55_000,
|
||||||
);
|
);
|
||||||
|
|
||||||
posixIt.each([125, "hang"] satisfies FetchResult[])(
|
posixIt.for([125, "hang"] satisfies FetchResult[])(
|
||||||
"Docs Agent commit drains diff before config/commit and failed fetch before retry (%s)",
|
"Docs Agent commit drains diff before config/commit and failed fetch before retry (%s)",
|
||||||
async (failure) => {
|
{ timeout: 55_000 },
|
||||||
const report = await runDocsAgent(agentCommit, {
|
async (failure, { signal }) => {
|
||||||
|
const report = await runDocsAgent(signal, agentCommit, {
|
||||||
commandResults: { "diff HEAD --quiet": { code: failure === 125 ? 125 : 1 } },
|
commandResults: { "diff HEAD --quiet": { code: failure === 125 ? 125 : 1 } },
|
||||||
fetchResults: [failure, 0],
|
fetchResults: [failure, 0],
|
||||||
});
|
});
|
||||||
|
|
@ -1465,13 +1496,13 @@ posixIt.each([125, "hang"] satisfies FetchResult[])(
|
||||||
expect(report.output).toContain("Fetch attempt 1 failed; retrying.");
|
expect(report.output).toContain("Fetch attempt 1 failed; retrying.");
|
||||||
expect(report.output).not.toContain("fixture-docs-agent-token");
|
expect(report.output).not.toContain("fixture-docs-agent-token");
|
||||||
},
|
},
|
||||||
55_000,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
posixIt.each([false, true])(
|
posixIt.for([false, true])(
|
||||||
"Docs Agent push failure drains before owned read and retry/stale success (advanced=%s)",
|
"Docs Agent push failure drains before owned read and retry/stale success (advanced=%s)",
|
||||||
async (advanced) => {
|
{ timeout: 55_000 },
|
||||||
const report = await runDocsAgent(agentCommit, {
|
async (advanced, { signal }) => {
|
||||||
|
const report = await runDocsAgent(signal, agentCommit, {
|
||||||
pushResults: [143, 0],
|
pushResults: [143, 0],
|
||||||
revParseResult: 0,
|
revParseResult: 0,
|
||||||
revisions: { "origin/main": advanced ? moved : candidate },
|
revisions: { "origin/main": advanced ? moved : candidate },
|
||||||
|
|
@ -1491,14 +1522,14 @@ posixIt.each([false, true])(
|
||||||
: "Docs update attempt 1 failed; retrying.",
|
: "Docs update attempt 1 failed; retrying.",
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
55_000,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
posixIt.each(["diff", "config", "commit", "fetch", "push", "read"])(
|
posixIt.for(["diff", "config", "commit", "fetch", "push", "read"])(
|
||||||
"Docs Agent commit cleanup failure at %s is terminal before retry/stale success",
|
"Docs Agent commit cleanup failure at %s is terminal before retry/stale success",
|
||||||
async (operation) => {
|
{ timeout: 55_000 },
|
||||||
|
async (operation, { signal }) => {
|
||||||
const index = operation === "diff" ? 0 : operation === "config" ? 1 : 4;
|
const index = operation === "diff" ? 0 : operation === "config" ? 1 : 4;
|
||||||
const report = await runDocsAgent(agentCommit, {
|
const report = await runDocsAgent(signal, agentCommit, {
|
||||||
commandResults: ["diff", "config", "commit"].includes(operation)
|
commandResults: ["diff", "config", "commit"].includes(operation)
|
||||||
? { [agentCommitCommands[index]!.join(" ")]: { code: "cleanup-failure" } }
|
? { [agentCommitCommands[index]!.join(" ")]: { code: "cleanup-failure" } }
|
||||||
: {},
|
: {},
|
||||||
|
|
@ -1521,14 +1552,14 @@ posixIt.each(["diff", "config", "commit", "fetch", "push", "read"])(
|
||||||
expect(backoffs(report)).toEqual([]);
|
expect(backoffs(report)).toEqual([]);
|
||||||
expect(report.output).not.toMatch(/retrying|skipping stale|No docs changes/u);
|
expect(report.output).not.toMatch(/retrying|skipping stale|No docs changes/u);
|
||||||
},
|
},
|
||||||
55_000,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
posixIt.each(["gate", "commit fetch", "commit push"])(
|
posixIt.for(["gate", "commit fetch", "commit push"])(
|
||||||
"Docs Agent %s preserves five attempts and terminal backoff contract",
|
"Docs Agent %s preserves five attempts and terminal backoff contract",
|
||||||
async (phase) => {
|
{ timeout: 55_000 },
|
||||||
|
async (phase, { signal }) => {
|
||||||
const gate = phase === "gate";
|
const gate = phase === "gate";
|
||||||
const report = await runDocsAgent(gate ? agentGate : agentCommit, {
|
const report = await runDocsAgent(signal, gate ? agentGate : agentCommit, {
|
||||||
fetchResults: phase === "commit push" ? [] : Array<FetchResult>(5).fill(23),
|
fetchResults: phase === "commit push" ? [] : Array<FetchResult>(5).fill(23),
|
||||||
pushResults: phase === "commit push" ? Array<FetchResult>(5).fill(23) : [],
|
pushResults: phase === "commit push" ? Array<FetchResult>(5).fill(23) : [],
|
||||||
});
|
});
|
||||||
|
|
@ -1552,7 +1583,6 @@ posixIt.each(["gate", "commit fetch", "commit push"])(
|
||||||
),
|
),
|
||||||
).toBe(true);
|
).toBe(true);
|
||||||
},
|
},
|
||||||
55_000,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const agentProducers = [
|
const agentProducers = [
|
||||||
|
|
@ -1562,15 +1592,15 @@ const agentProducers = [
|
||||||
["diff", "HEAD", "--name-only"],
|
["diff", "HEAD", "--name-only"],
|
||||||
["diff", "--cached", "HEAD", "--name-only"],
|
["diff", "--cached", "HEAD", "--name-only"],
|
||||||
];
|
];
|
||||||
posixIt.each(agentProducers.map((args, index) => ({ args, index })))(
|
posixIt.for(agentProducers.map((args, index) => ({ args, index })))(
|
||||||
"Docs Agent enforcement stops on failed producer $args before consuming partial output",
|
"Docs Agent enforcement stops on failed producer $args before consuming partial output",
|
||||||
async ({ args, index }) => {
|
{ timeout: 55_000 },
|
||||||
const report = await runDocsAgent("Enforce existing-docs-only patch", {
|
async ({ args, index }, { signal }) => {
|
||||||
|
const report = await runDocsAgent(signal, "Enforce existing-docs-only patch", {
|
||||||
commandResults: { [args.join(" ")]: { code: 23, output: "src/forbidden.ts\n" } },
|
commandResults: { [args.join(" ")]: { code: 23, output: "src/forbidden.ts\n" } },
|
||||||
});
|
});
|
||||||
expect(report.code, report.output).toBe(23);
|
expect(report.code, report.output).toBe(23);
|
||||||
expect(gitArgs(report)).toEqual(agentProducers.slice(0, index + 1));
|
expect(gitArgs(report)).toEqual(agentProducers.slice(0, index + 1));
|
||||||
expect(report.output).not.toContain("forbidden");
|
expect(report.output).not.toContain("forbidden");
|
||||||
},
|
},
|
||||||
55_000,
|
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -99,18 +99,20 @@ const linuxCases =
|
||||||
{ scenario: "non-executable-find", attempts: 0, code: null, checkout: false, deletions: 0 },
|
{ scenario: "non-executable-find", attempts: 0, code: null, checkout: false, deletions: 0 },
|
||||||
];
|
];
|
||||||
|
|
||||||
it.concurrent.each([
|
it.concurrent.for([
|
||||||
...platformCases.map((entry) => Object.assign(entry, { linux: false, deletions: 0 })),
|
...platformCases.map((entry) => Object.assign(entry, { linux: false, deletions: 0 })),
|
||||||
...linuxCases.map((entry) => Object.assign(entry, { linux: true })),
|
...linuxCases.map((entry) => Object.assign(entry, { linux: true })),
|
||||||
])(
|
])(
|
||||||
"preserves checkout ownership and fixture isolation (Linux=$linux, $scenario)",
|
"preserves checkout ownership and fixture isolation (Linux=$linux, $scenario)",
|
||||||
async ({ scenario, attempts, code, checkout, linux, deletions }) => {
|
{ timeout: 55_000 },
|
||||||
|
async ({ scenario, attempts, code, checkout, linux, deletions }, { signal: testSignal }) => {
|
||||||
const setupFailure = scenario.startsWith("non-executable-");
|
const setupFailure = scenario.startsWith("non-executable-");
|
||||||
const run = readCiCheckoutStep(linux ? "checks-fast-core" : "checks-windows").run;
|
const run = readCiCheckoutStep(linux ? "checks-fast-core" : "checks-windows").run;
|
||||||
|
|
||||||
const policyScenario = `${linux ? "linux:" : ""}${scenario}`;
|
const policyScenario = `${linux ? "linux:" : ""}${scenario}`;
|
||||||
await withCiCheckoutFixture(
|
await withCiCheckoutFixture(
|
||||||
policyScenario,
|
policyScenario,
|
||||||
|
testSignal,
|
||||||
(root) => {
|
(root) => {
|
||||||
const workspace = path.join(root, "workspace");
|
const workspace = path.join(root, "workspace");
|
||||||
if (scenario === "cancel-SIGTERM") {
|
if (scenario === "cancel-SIGTERM") {
|
||||||
|
|
@ -138,7 +140,14 @@ it.concurrent.each([
|
||||||
path.join(root, "checkout.sh"),
|
path.join(root, "checkout.sh"),
|
||||||
setupFailure ? "printf 'unexpected workflow invocation\\n' >&2\nexit 99\n" : accelerated,
|
setupFailure ? "printf 'unexpected workflow invocation\\n' >&2\nexit 99\n" : accelerated,
|
||||||
);
|
);
|
||||||
if (process.platform === "win32" || scenario === "git-exit-124") {
|
// A slow census witness must not replace the workflow's real outcome.
|
||||||
|
const slowWitness = [
|
||||||
|
"timeouts-exhausted",
|
||||||
|
"recovery",
|
||||||
|
"early-leader-exit",
|
||||||
|
"harness-timeout",
|
||||||
|
].includes(scenario);
|
||||||
|
if (process.platform === "win32" || slowWitness || scenario === "git-exit-124") {
|
||||||
return censusPreload(
|
return censusPreload(
|
||||||
root,
|
root,
|
||||||
scenario === "git-exit-124"
|
scenario === "git-exit-124"
|
||||||
|
|
@ -176,9 +185,7 @@ if (process.argv[2] === "sentinel") {
|
||||||
syncFixtureBuiltinExports();
|
syncFixtureBuiltinExports();
|
||||||
`
|
`
|
||||||
: "",
|
: "",
|
||||||
["timeouts-exhausted", "recovery", "early-leader-exit", "harness-timeout"].includes(
|
slowWitness,
|
||||||
scenario,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
|
|
@ -312,10 +319,9 @@ syncFixtureBuiltinExports();
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
55_000,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
it.concurrent.each([
|
it.concurrent.for([
|
||||||
...[
|
...[
|
||||||
...(process.platform === "win32" ? [] : [{ kind: "linux-node", retained: false }]),
|
...(process.platform === "win32" ? [] : [{ kind: "linux-node", retained: false }]),
|
||||||
...(process.platform === "win32"
|
...(process.platform === "win32"
|
||||||
|
|
@ -376,7 +382,8 @@ it.concurrent.each([
|
||||||
].map((entry) => Object.assign(entry, { kind: "preflight", retained: false }))),
|
].map((entry) => Object.assign(entry, { kind: "preflight", retained: false }))),
|
||||||
])(
|
])(
|
||||||
"materializes $kind trusted harness ($event, workflow=$workflow, target=$target, retained=$retained) without mutating the candidate",
|
"materializes $kind trusted harness ($event, workflow=$workflow, target=$target, retained=$retained) without mutating the candidate",
|
||||||
async ({ kind, retained, event, workflow, target, code, fetches }) => {
|
{ timeout: 55_000 },
|
||||||
|
async ({ kind, retained, event, workflow, target, code, fetches }, { signal }) => {
|
||||||
const linux = kind !== "platform";
|
const linux = kind !== "platform";
|
||||||
const preflight = kind === "preflight";
|
const preflight = kind === "preflight";
|
||||||
const posix = process.platform !== "win32";
|
const posix = process.platform !== "win32";
|
||||||
|
|
@ -441,6 +448,7 @@ it.concurrent.each([
|
||||||
let readSourceStatus: (() => string[]) | undefined;
|
let readSourceStatus: (() => string[]) | undefined;
|
||||||
await withCiCheckoutFixture(
|
await withCiCheckoutFixture(
|
||||||
`${linux ? "linux:" : ""}configured`,
|
`${linux ? "linux:" : ""}configured`,
|
||||||
|
signal,
|
||||||
(root) => {
|
(root) => {
|
||||||
const source = path.join(root, "source");
|
const source = path.join(root, "source");
|
||||||
mkdirSync(source);
|
mkdirSync(source);
|
||||||
|
|
@ -743,18 +751,19 @@ it.concurrent.each([
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
55_000,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
registerWindowsCensusTests();
|
registerWindowsCensusTests();
|
||||||
|
|
||||||
it.each(["prepare", "inspect"])(
|
it.for(["prepare", "inspect"])(
|
||||||
"removes checkout artifacts after %s assertion failure",
|
"removes checkout artifacts after %s assertion failure",
|
||||||
async (phase) => {
|
{ timeout: 55_000 },
|
||||||
|
async (phase, { signal }) => {
|
||||||
let root: string | undefined;
|
let root: string | undefined;
|
||||||
await expect(
|
await expect(
|
||||||
withCiCheckoutFixture(
|
withCiCheckoutFixture(
|
||||||
"early-leader-exit",
|
"early-leader-exit",
|
||||||
|
signal,
|
||||||
(directory) => {
|
(directory) => {
|
||||||
root = directory;
|
root = directory;
|
||||||
expect(phase, "injected prepare assertion").not.toBe("prepare");
|
expect(phase, "injected prepare assertion").not.toBe("prepare");
|
||||||
|
|
@ -769,10 +778,9 @@ it.each(["prepare", "inspect"])(
|
||||||
).rejects.toThrow(`injected ${phase} assertion`);
|
).rejects.toThrow(`injected ${phase} assertion`);
|
||||||
expect(existsSync(expectDefined(root, "created checkout root"))).toBe(false);
|
expect(existsSync(expectDefined(root, "created checkout root"))).toBe(false);
|
||||||
},
|
},
|
||||||
55_000,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
it.skipIf(process.platform === "win32").each(["census", "corrupt-report", "timeout"])(
|
it.skipIf(process.platform === "win32").each(["census", "corrupt-report", "timeout", "cancel"])(
|
||||||
"retains checkout artifacts across failed outer-runner cleanup (%s)",
|
"retains checkout artifacts across failed outer-runner cleanup (%s)",
|
||||||
async (fault) => {
|
async (fault) => {
|
||||||
const preload = String.raw`
|
const preload = String.raw`
|
||||||
|
|
@ -802,6 +810,10 @@ if (process.argv[2] === "supervise") {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (fault === "cancel" && args[1]?.[1] !== "sentinel") {
|
||||||
|
// The detached workflow shell is running; only the supervisor can retire its group.
|
||||||
|
queueMicrotask(() => process.send({ type: "ci-checkout:shell-started", pids: [process.pid, ...children] }));
|
||||||
|
}
|
||||||
return child;
|
return child;
|
||||||
};
|
};
|
||||||
cp.spawnSync = (...args) => {
|
cp.spawnSync = (...args) => {
|
||||||
|
|
@ -834,15 +846,18 @@ import fs from "node:fs";
|
||||||
import { fixturePreloadEnv, syncFixtureBuiltinExports } from ${JSON.stringify(new URL("./fixtures/ci-fixture-runtime.cjs", import.meta.url).href)};
|
import { fixturePreloadEnv, syncFixtureBuiltinExports } from ${JSON.stringify(new URL("./fixtures/ci-fixture-runtime.cjs", import.meta.url).href)};
|
||||||
import { tmpdir } from "node:os";
|
import { tmpdir } from "node:os";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { mock } from "node:test";
|
|
||||||
const timeoutFault = process.argv[2] === "timeout";
|
const timeoutFault = process.argv[2] === "timeout";
|
||||||
|
const cancelledFault = timeoutFault || process.argv[2] === "cancel";
|
||||||
|
const cancellation = new AbortController();
|
||||||
let root, failure;
|
let root, failure;
|
||||||
let supervisor, ready, onReady;
|
let supervisor, ready, onReady;
|
||||||
const fork = cp.fork;
|
const fork = cp.fork;
|
||||||
if (timeoutFault) {
|
if (cancelledFault) {
|
||||||
ready = new Promise(resolve => {
|
ready = new Promise(resolve => {
|
||||||
onReady = message => {
|
onReady = message => {
|
||||||
if (message?.type === "ci-checkout:sentinel-created") resolve(message.pids);
|
if (message?.type === (timeoutFault ? "ci-checkout:sentinel-created" : "ci-checkout:shell-started")) {
|
||||||
|
resolve(message.pids);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
cp.fork = (...args) => {
|
cp.fork = (...args) => {
|
||||||
|
|
@ -854,10 +869,9 @@ if (timeoutFault) {
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const { withCiCheckoutFixture } = await import(process.argv[1]);
|
const { withCiCheckoutFixture } = await import(process.argv[1]);
|
||||||
if (timeoutFault) mock.timers.enable({ apis: ["setTimeout"] });
|
const completed = withCiCheckoutFixture("early-leader-exit", cancellation.signal, directory => {
|
||||||
const completed = withCiCheckoutFixture("early-leader-exit", directory => {
|
|
||||||
root = directory;
|
root = directory;
|
||||||
fs.writeFileSync(path.join(root, "checkout.sh"), "exit 0\n");
|
fs.writeFileSync(path.join(root, "checkout.sh"), process.argv[2] === "cancel" ? "exec sleep 30\n" : "exit 0\n");
|
||||||
const preload = path.join(root, "fault.mjs");
|
const preload = path.join(root, "fault.mjs");
|
||||||
fs.writeFileSync(preload, "const fault = " + JSON.stringify(process.argv[2]) + ";\n" + process.argv[3]);
|
fs.writeFileSync(preload, "const fault = " + JSON.stringify(process.argv[2]) + ";\n" + process.argv[3]);
|
||||||
return fixturePreloadEnv(preload);
|
return fixturePreloadEnv(preload);
|
||||||
|
|
@ -868,11 +882,11 @@ try {
|
||||||
failure = String(error);
|
failure = String(error);
|
||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
if (timeoutFault) {
|
if (cancelledFault) {
|
||||||
const pids = await Promise.race([ready, completed.then(() => {
|
const pids = await Promise.race([ready, completed.then(() => {
|
||||||
throw new Error("supervisor completed before the timeout probe was ready");
|
throw new Error("supervisor completed before the cancellation probe was ready");
|
||||||
})]);
|
})]);
|
||||||
assert.equal(pids.length, 2);
|
assert.equal(pids.length, timeoutFault ? 2 : 3);
|
||||||
assert.equal(pids[0], supervisor.pid);
|
assert.equal(pids[0], supervisor.pid);
|
||||||
assert.notEqual(pids[1], supervisor.pid);
|
assert.notEqual(pids[1], supervisor.pid);
|
||||||
for (const pid of pids) {
|
for (const pid of pids) {
|
||||||
|
|
@ -881,11 +895,10 @@ try {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
if (timeoutFault) {
|
if (cancelledFault) {
|
||||||
// Creation belongs to the supervisor, not a child's delayed self-registration.
|
// Creation belongs to the supervisor, not a child's delayed self-registration.
|
||||||
// Restore timers before the expired controller deadline starts real cleanup.
|
// Cancel the run the way a Vitest timeout aborts its owning test.
|
||||||
mock.timers.tick(50_000);
|
cancellation.abort(new Error("owning test cancelled the supervised run"));
|
||||||
mock.timers.reset();
|
|
||||||
}
|
}
|
||||||
await completed;
|
await completed;
|
||||||
}
|
}
|
||||||
|
|
@ -893,8 +906,7 @@ try {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
failure = String(error);
|
failure = String(error);
|
||||||
} finally {
|
} finally {
|
||||||
if (timeoutFault) {
|
if (cancelledFault) {
|
||||||
mock.timers.reset();
|
|
||||||
supervisor?.off("message", onReady);
|
supervisor?.off("message", onReady);
|
||||||
cp.fork = fork;
|
cp.fork = fork;
|
||||||
syncFixtureBuiltinExports();
|
syncFixtureBuiltinExports();
|
||||||
|
|
@ -950,8 +962,15 @@ process.exitCode = 1;
|
||||||
expect(existsSync(path.join(evidence.root, "report.json"))).toBe(false);
|
expect(existsSync(path.join(evidence.root, "report.json"))).toBe(false);
|
||||||
} else if (fault === "timeout") {
|
} else if (fault === "timeout") {
|
||||||
expect(evidence.pids).toHaveLength(2);
|
expect(evidence.pids).toHaveLength(2);
|
||||||
expect(evidence.failure).toContain("did not close within 50000ms");
|
expect(evidence.failure).toContain("owning test cancelled the supervised run");
|
||||||
expect(existsSync(path.join(evidence.root, "report.json"))).toBe(false);
|
expect(existsSync(path.join(evidence.root, "report.json"))).toBe(false);
|
||||||
|
} else if (fault === "cancel") {
|
||||||
|
// The detached shell group died, so the live supervisor ran its own cleanup.
|
||||||
|
expect(evidence.pids).toHaveLength(3);
|
||||||
|
expect(evidence.failure).toContain("owning test cancelled the supervised run");
|
||||||
|
expect(
|
||||||
|
JSON.parse(readFileSync(path.join(evidence.root, "report.json"), "utf8")),
|
||||||
|
).toMatchObject({ error: "test cancelled", cleanupRemaining: [] });
|
||||||
} else {
|
} else {
|
||||||
expect(evidence.failure).not.toContain("unexpected completed report");
|
expect(evidence.failure).not.toContain("unexpected completed report");
|
||||||
expect(readFileSync(path.join(evidence.root, "report.json"), "utf8")).toBe("null");
|
expect(readFileSync(path.join(evidence.root, "report.json"), "utf8")).toBe("null");
|
||||||
|
|
@ -967,8 +986,9 @@ process.exitCode = 1;
|
||||||
|
|
||||||
it.skipIf(process.platform === "win32")(
|
it.skipIf(process.platform === "win32")(
|
||||||
"waits for legal slow tree startup before cancellation",
|
"waits for legal slow tree startup before cancellation",
|
||||||
async () => {
|
async ({ signal }) => {
|
||||||
const report = await runCiGitStep({
|
const report = await runCiGitStep({
|
||||||
|
signal,
|
||||||
job: "checks-windows",
|
job: "checks-windows",
|
||||||
env: { CHECKOUT_KIND: "platform" },
|
env: { CHECKOUT_KIND: "platform" },
|
||||||
fetchResults: ["hang"],
|
fetchResults: ["hang"],
|
||||||
|
|
@ -984,8 +1004,9 @@ it.skipIf(process.platform === "win32")(
|
||||||
|
|
||||||
it.skipIf(process.platform === "win32")(
|
it.skipIf(process.platform === "win32")(
|
||||||
"reports owner exit and output instead of a cleanup readiness timeout",
|
"reports owner exit and output instead of a cleanup readiness timeout",
|
||||||
async () => {
|
async ({ signal }) => {
|
||||||
const report = await runCiGitStep({
|
const report = await runCiGitStep({
|
||||||
|
signal,
|
||||||
policy: 'print("owner exited before cleanup readiness", flush=True)\nraise SystemExit(23)\n',
|
policy: 'print("owner exited before cleanup readiness", flush=True)\nraise SystemExit(23)\n',
|
||||||
fetchResults: [],
|
fetchResults: [],
|
||||||
cancelDuringCleanup: true,
|
cancelDuringCleanup: true,
|
||||||
|
|
|
||||||
|
|
@ -23,8 +23,19 @@ const censusArgs = args => delayed && args?.[2]?.endsWith("ci-windows-process-ce
|
||||||
? ["-I", "-S", "-c", "import runpy,sys,time; time.sleep(1.1); sys.argv=sys.argv[1:]; runpy.run_path(sys.argv[0],run_name='__main__')", ...args.slice(2)]
|
? ["-I", "-S", "-c", "import runpy,sys,time; time.sleep(1.1); sys.argv=sys.argv[1:]; runpy.run_path(sys.argv[0],run_name='__main__')", ...args.slice(2)]
|
||||||
: args;
|
: args;
|
||||||
const spawn = cp.spawn, spawnSync = cp.spawnSync;
|
const spawn = cp.spawn, spawnSync = cp.spawnSync;
|
||||||
|
// POSIX has no census interpreter to delay; its first native query instead
|
||||||
|
// spends as long as a loaded host can, on the supervisor's clock.
|
||||||
|
let slowNativeQuery = delayed && process.argv[2] === "supervise";
|
||||||
// Delay the actual interpreter entry point, including the pre-fix synchronous path.
|
// Delay the actual interpreter entry point, including the pre-fix synchronous path.
|
||||||
cp.spawnSync = (command, args, options) => spawnSync(command, censusArgs(args), options);
|
cp.spawnSync = (command, args, options) => {
|
||||||
|
const result = spawnSync(command, censusArgs(args), options);
|
||||||
|
if (slowNativeQuery && command === "/bin/ps") {
|
||||||
|
slowNativeQuery = false;
|
||||||
|
const now = Date.now;
|
||||||
|
Date.now = () => now() + 1_100;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
};
|
||||||
cp.spawn = (command, args, options) => {
|
cp.spawn = (command, args, options) => {
|
||||||
const child = spawn(command, censusArgs(args), options);
|
const child = spawn(command, censusArgs(args), options);
|
||||||
if (command === "python" && process.argv[2] === "supervise") {
|
if (command === "python" && process.argv[2] === "supervise") {
|
||||||
|
|
@ -274,11 +285,13 @@ fs.rmSync(root, { recursive: true });
|
||||||
censusTestTimeoutMs,
|
censusTestTimeoutMs,
|
||||||
);
|
);
|
||||||
|
|
||||||
it.each(["sentinel", ...(process.platform === "win32" ? ["startup", "query", "lease"] : [])])(
|
it.for(["sentinel", ...(process.platform === "win32" ? ["startup", "query", "lease"] : [])])(
|
||||||
"retains startup errors and joins census before registration (%s)",
|
"retains startup errors and joins census before registration (%s)",
|
||||||
async (fault) => {
|
{ timeout: 55_000 },
|
||||||
|
async (fault, { signal }) => {
|
||||||
await withCiCheckoutFixture(
|
await withCiCheckoutFixture(
|
||||||
"early-leader-exit",
|
"early-leader-exit",
|
||||||
|
signal,
|
||||||
(root) => {
|
(root) => {
|
||||||
writeFileSync(path.join(root, "checkout.sh"), "exit 99\n");
|
writeFileSync(path.join(root, "checkout.sh"), "exit 99\n");
|
||||||
const sampler = String.raw`
|
const sampler = String.raw`
|
||||||
|
|
@ -351,12 +364,12 @@ for line in sys.stdin:
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
55_000,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
it("joins an unregistered sentinel before supervisor close on disconnect", async () => {
|
it("joins an unregistered sentinel before supervisor close on disconnect", async ({ signal }) => {
|
||||||
await withCiCheckoutFixture(
|
await withCiCheckoutFixture(
|
||||||
"early-leader-exit",
|
"early-leader-exit",
|
||||||
|
signal,
|
||||||
(root) => {
|
(root) => {
|
||||||
writeFileSync(path.join(root, "checkout.sh"), "exit 99\n");
|
writeFileSync(path.join(root, "checkout.sh"), "exit 99\n");
|
||||||
// Fault only the asynchronous startup boundary; keep the real safe preflight.
|
// Fault only the asynchronous startup boundary; keep the real safe preflight.
|
||||||
|
|
@ -414,9 +427,10 @@ if (mode === "supervise") {
|
||||||
);
|
);
|
||||||
}, 55_000);
|
}, 55_000);
|
||||||
|
|
||||||
it.each(["direct-child-close", "truthful-final-census"])(
|
it.for(["direct-child-close", "truthful-final-census"])(
|
||||||
"rejects expired supervisor cleanup and joins census (%s)",
|
"rejects expired supervisor cleanup and joins census (%s)",
|
||||||
async (fault) => {
|
{ timeout: 55_000 },
|
||||||
|
async (fault, { signal }) => {
|
||||||
type BoundaryEvent = {
|
type BoundaryEvent = {
|
||||||
event: string;
|
event: string;
|
||||||
role?: string;
|
role?: string;
|
||||||
|
|
@ -436,6 +450,7 @@ if (mode === "supervise") {
|
||||||
let failure: unknown;
|
let failure: unknown;
|
||||||
await withCiCheckoutFixture(
|
await withCiCheckoutFixture(
|
||||||
"early-leader-exit",
|
"early-leader-exit",
|
||||||
|
signal,
|
||||||
(directory) => {
|
(directory) => {
|
||||||
root = directory;
|
root = directory;
|
||||||
writeFileSync(path.join(root, "checkout.sh"), "exit 0\n");
|
writeFileSync(path.join(root, "checkout.sh"), "exit 0\n");
|
||||||
|
|
@ -593,6 +608,5 @@ if (mode === "supervise") {
|
||||||
// Every spawned writer above also has a creator-held close; failures retain evidence.
|
// Every spawned writer above also has a creator-held close; failures retain evidence.
|
||||||
rmSync(directory, { recursive: true });
|
rmSync(directory, { recursive: true });
|
||||||
},
|
},
|
||||||
55_000,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4999,7 +4999,7 @@ printf '%s\n' "\${CURL_SUCCESS_IP:-203.0.113.7}"
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it.skipIf(process.platform === "win32").each([
|
it.skipIf(process.platform === "win32").for([
|
||||||
{ task: "bundled-protocol", eventName: "pull_request" },
|
{ task: "bundled-protocol", eventName: "pull_request" },
|
||||||
{ task: "bundled-protocol", eventName: "workflow_dispatch" },
|
{ task: "bundled-protocol", eventName: "workflow_dispatch" },
|
||||||
{ task: "guards", eventName: "pull_request" },
|
{ task: "guards", eventName: "pull_request" },
|
||||||
|
|
@ -5008,7 +5008,8 @@ printf '%s\n' "\${CURL_SUCCESS_IP:-203.0.113.7}"
|
||||||
{ task: "npm-lock", eventName: "workflow_dispatch" },
|
{ task: "npm-lock", eventName: "workflow_dispatch" },
|
||||||
] as const)(
|
] as const)(
|
||||||
"uses prefetched CI base without later network access ($task, $eventName)",
|
"uses prefetched CI base without later network access ($task, $eventName)",
|
||||||
async ({ task, eventName }) => {
|
{ timeout: 55_000 },
|
||||||
|
async ({ task, eventName }, { signal }) => {
|
||||||
const base = "c".repeat(40);
|
const base = "c".repeat(40);
|
||||||
const baseRef = "refs/remotes/origin/ci-ratchet-base";
|
const baseRef = "refs/remotes/origin/ci-ratchet-base";
|
||||||
const jobName = task === "bundled-protocol" ? "checks-fast-core" : "check-shard";
|
const jobName = task === "bundled-protocol" ? "checks-fast-core" : "check-shard";
|
||||||
|
|
@ -5024,6 +5025,7 @@ printf '%s\n' "\${CURL_SUCCESS_IP:-203.0.113.7}"
|
||||||
preflightOutputs: { diff_base_revision: base },
|
preflightOutputs: { diff_base_revision: base },
|
||||||
});
|
});
|
||||||
const report = await runCiGitStep({
|
const report = await runCiGitStep({
|
||||||
|
signal,
|
||||||
job: jobName,
|
job: jobName,
|
||||||
step:
|
step:
|
||||||
task === "bundled-protocol"
|
task === "bundled-protocol"
|
||||||
|
|
@ -5086,7 +5088,6 @@ printf '%s\n' "\${CURL_SUCCESS_IP:-203.0.113.7}"
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
55_000,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
it.each([
|
it.each([
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,9 @@ const instance = randomUUID();
|
||||||
let ownWindowsCreationTime;
|
let ownWindowsCreationTime;
|
||||||
let census;
|
let census;
|
||||||
let actorLease;
|
let actorLease;
|
||||||
|
// Orphan ceiling for every fixture process. The owning test's abort signal ends a
|
||||||
|
// supervised run; this only bounds processes whose owner died or never cancels.
|
||||||
|
const lifetimeCeilingMs = 60_000;
|
||||||
let operationDeadline;
|
let operationDeadline;
|
||||||
const workspace = path.join(root, "workspace");
|
const workspace = path.join(root, "workspace");
|
||||||
const runnerTemp = path.join(root, "temp");
|
const runnerTemp = path.join(root, "temp");
|
||||||
|
|
@ -210,9 +213,9 @@ async function liveRecords(deadline = operationDeadline) {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Linux can census the owned PID set in one ps call. Apple ps scans the
|
// Linux can census the owned PID set in one ps call. Apple ps scans the
|
||||||
// whole host for multiple PIDs; keep its singleton queries under one budget.
|
// whole host for multiple PIDs; its singleton queries share the caller's
|
||||||
|
// operation deadline, like the Windows witness.
|
||||||
const pidLists = process.platform === "linux" ? [[...pids]] : [...pids].map((pid) => [pid]);
|
const pidLists = process.platform === "linux" ? [[...pids]] : [...pids].map((pid) => [pid]);
|
||||||
const deadline = Date.now() + 1_000;
|
|
||||||
for (const selectedPids of pidLists) {
|
for (const selectedPids of pidLists) {
|
||||||
const remaining = deadline - Date.now();
|
const remaining = deadline - Date.now();
|
||||||
if (remaining <= 0) {
|
if (remaining <= 0) {
|
||||||
|
|
@ -262,8 +265,7 @@ async function liveRecords(deadline = operationDeadline) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function isWorkflowDescendant(pid, shellPid) {
|
function isWorkflowDescendant(pid, shellPid, deadline = operationDeadline) {
|
||||||
const deadline = Date.now() + 1_000;
|
|
||||||
const visited = new Set();
|
const visited = new Set();
|
||||||
while (pid > 1 && !visited.has(pid)) {
|
while (pid > 1 && !visited.has(pid)) {
|
||||||
if (pid === shellPid) return true;
|
if (pid === shellPid) return true;
|
||||||
|
|
@ -386,7 +388,7 @@ function holdLease() {
|
||||||
};
|
};
|
||||||
// Orphans stop themselves when the supervisor releases the lease; no PID discovery/kills.
|
// Orphans stop themselves when the supervisor releases the lease; no PID discovery/kills.
|
||||||
// The independent ceiling also covers a supervisor killed before it can unlink the lease.
|
// The independent ceiling also covers a supervisor killed before it can unlink the lease.
|
||||||
const deadline = Date.now() + 60_000;
|
const deadline = Date.now() + lifetimeCeilingMs;
|
||||||
const checkLease = () => {
|
const checkLease = () => {
|
||||||
if (!isLive() || Date.now() >= deadline) {
|
if (!isLive() || Date.now() >= deadline) {
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
|
|
@ -1285,8 +1287,12 @@ async function supervise() {
|
||||||
for (const signal of ["SIGINT", "SIGTERM", "SIGHUP"]) {
|
for (const signal of ["SIGINT", "SIGTERM", "SIGHUP"]) {
|
||||||
process.once(signal, () => void stop(`supervisor received ${signal}`));
|
process.once(signal, () => void stop(`supervisor received ${signal}`));
|
||||||
}
|
}
|
||||||
operationDeadline = Date.now() + 45_000;
|
// The owning test's signal bounds the run: a slow host must not lose a race
|
||||||
setTimeout(() => void stop("fixture deadline exceeded"), 45_000);
|
// against a fixture deadline. Cancellation still runs this owned cleanup.
|
||||||
|
process.on("message", (message) => {
|
||||||
|
if (message?.type === "ci-checkout:cancel") void stop("test cancelled");
|
||||||
|
});
|
||||||
|
operationDeadline = Date.now() + lifetimeCeilingMs;
|
||||||
try {
|
try {
|
||||||
if (process.platform === "win32") {
|
if (process.platform === "win32") {
|
||||||
census = createWindowsProcessCensus({
|
census = createWindowsProcessCensus({
|
||||||
|
|
@ -1297,7 +1303,7 @@ async function supervise() {
|
||||||
void stop(error);
|
void stop(error);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
// Interpreter startup belongs to the existing supervisor watchdog, not a query deadline.
|
// Interpreter startup belongs to the owning test's cancellation, not a query deadline.
|
||||||
await census.ready;
|
await census.ready;
|
||||||
if (stopping) {
|
if (stopping) {
|
||||||
await stopping;
|
await stopping;
|
||||||
|
|
@ -1322,7 +1328,7 @@ async function supervise() {
|
||||||
cwd: workspace,
|
cwd: workspace,
|
||||||
env: { PATH: commandPath },
|
env: { PATH: commandPath },
|
||||||
encoding: "utf8",
|
encoding: "utf8",
|
||||||
timeout: 2_000,
|
timeout: Math.max(1, operationDeadline - Date.now()),
|
||||||
killSignal: "SIGKILL",
|
killSignal: "SIGKILL",
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -24,11 +24,13 @@ const steps = {
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
function performanceRun(
|
function performanceRun(
|
||||||
|
signal: AbortSignal,
|
||||||
mode: PerformanceFixtureOptions["mode"],
|
mode: PerformanceFixtureOptions["mode"],
|
||||||
options: Partial<Parameters<typeof runCiGitStep>[0]> = {},
|
options: Partial<Parameters<typeof runCiGitStep>[0]> = {},
|
||||||
) {
|
) {
|
||||||
const [job, step] = steps[mode];
|
const [job, step] = steps[mode];
|
||||||
return runCiGitStep({
|
return runCiGitStep({
|
||||||
|
signal,
|
||||||
workflow: { file: ".github/workflows/openclaw-performance.yml", job, step },
|
workflow: { file: ".github/workflows/openclaw-performance.yml", job, step },
|
||||||
fetchResults: [],
|
fetchResults: [],
|
||||||
performance: { mode },
|
performance: { mode },
|
||||||
|
|
@ -39,10 +41,11 @@ function performanceRun(
|
||||||
// The previous semantic tests used short-lived stubs or replayed Git by hand.
|
// The previous semantic tests used short-lived stubs or replayed Git by hand.
|
||||||
// These actual workflow bodies must drain real parent/child/grandchild writers
|
// These actual workflow bodies must drain real parent/child/grandchild writers
|
||||||
// before every command, output, consumer and exit, while a sentinel stays alive.
|
// before every command, output, consumer and exit, while a sentinel stays alive.
|
||||||
posixIt.each(Object.keys(steps) as PerformanceFixtureOptions["mode"][])(
|
posixIt.for(Object.keys(steps) as PerformanceFixtureOptions["mode"][])(
|
||||||
"Performance %s drains Git trees before every continuation",
|
"Performance %s drains Git trees before every continuation",
|
||||||
async (mode) => {
|
{ timeout: 55_000 },
|
||||||
const report = await performanceRun(mode);
|
async (mode, { signal }) => {
|
||||||
|
const report = await performanceRun(signal, mode);
|
||||||
expect(report.code, report.output).toBe(0);
|
expect(report.code, report.output).toBe(0);
|
||||||
expect(report.readyAttempts.length).toBeGreaterThan(0);
|
expect(report.readyAttempts.length).toBeGreaterThan(0);
|
||||||
if (mode === "prepare") {
|
if (mode === "prepare") {
|
||||||
|
|
@ -54,13 +57,13 @@ posixIt.each(Object.keys(steps) as PerformanceFixtureOptions["mode"][])(
|
||||||
expect(report.githubSummary).toContain("### Clawgrit report published");
|
expect(report.githubSummary).toContain("### Clawgrit report published");
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
55_000,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
posixIt.each([23, 125])(
|
posixIt.for([23, 125])(
|
||||||
"baseline ordinary fetch %s is advisory after extinction",
|
"baseline ordinary fetch %s is advisory after extinction",
|
||||||
async (code) => {
|
{ timeout: 55_000 },
|
||||||
const report = await performanceRun("baseline", { fetchResults: [code, code, code] });
|
async (code, { signal }) => {
|
||||||
|
const report = await performanceRun(signal, "baseline", { fetchResults: [code, code, code] });
|
||||||
expect(report.code, report.output).toBe(0);
|
expect(report.code, report.output).toBe(0);
|
||||||
expect(report.githubSummary).toBe(
|
expect(report.githubSummary).toBe(
|
||||||
"No previous source performance baseline could be fetched.\n",
|
"No previous source performance baseline could be fetched.\n",
|
||||||
|
|
@ -68,13 +71,13 @@ posixIt.each([23, 125])(
|
||||||
expect(report.githubEnv).toBe("");
|
expect(report.githubEnv).toBe("");
|
||||||
expect(report.commands.at(-1)?.args[0]).toBe("fetch");
|
expect(report.commands.at(-1)?.args[0]).toBe("fetch");
|
||||||
},
|
},
|
||||||
55_000,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
posixIt.each(["absent", "invalid", "trailing-newline"] as const)(
|
posixIt.for(["absent", "invalid", "trailing-newline"] as const)(
|
||||||
"baseline %s pointer preserves advisory result",
|
"baseline %s pointer preserves advisory result",
|
||||||
async (baseline) => {
|
{ timeout: 55_000 },
|
||||||
const report = await performanceRun("baseline", {
|
async (baseline, { signal }) => {
|
||||||
|
const report = await performanceRun(signal, "baseline", {
|
||||||
performance: { mode: "baseline", baseline },
|
performance: { mode: "baseline", baseline },
|
||||||
});
|
});
|
||||||
expect(report.code, report.output).toBe(0);
|
expect(report.code, report.output).toBe(0);
|
||||||
|
|
@ -86,13 +89,13 @@ posixIt.each(["absent", "invalid", "trailing-newline"] as const)(
|
||||||
expect(report.githubEnv).toBe("");
|
expect(report.githubEnv).toBe("");
|
||||||
expect(report.checkouts).toHaveLength(0);
|
expect(report.checkouts).toHaveLength(0);
|
||||||
},
|
},
|
||||||
55_000,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
posixIt.each(["ls-tree", "show"])(
|
posixIt.for(["ls-tree", "show"])(
|
||||||
"baseline %s failure never becomes absence or invalid JSON",
|
"baseline %s failure never becomes absence or invalid JSON",
|
||||||
async (operation) => {
|
{ timeout: 55_000 },
|
||||||
const report = await performanceRun("baseline", {
|
async (operation, { signal }) => {
|
||||||
|
const report = await performanceRun(signal, "baseline", {
|
||||||
gitFault: { match: `^${operation} `, code: 128 },
|
gitFault: { match: `^${operation} `, code: 128 },
|
||||||
});
|
});
|
||||||
expect(report.code, report.output).toBe(128);
|
expect(report.code, report.output).toBe(128);
|
||||||
|
|
@ -100,7 +103,6 @@ posixIt.each(["ls-tree", "show"])(
|
||||||
expect(report.githubEnv).toBe("");
|
expect(report.githubEnv).toBe("");
|
||||||
expect(report.commands.at(-1)?.args[0]).toBe(operation);
|
expect(report.commands.at(-1)?.args[0]).toBe(operation);
|
||||||
},
|
},
|
||||||
55_000,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const terminalCases = [
|
const terminalCases = [
|
||||||
|
|
@ -124,7 +126,7 @@ const terminalCases = [
|
||||||
].map((operation) => ({ mode: "publish" as const, operation })),
|
].map((operation) => ({ mode: "publish" as const, operation })),
|
||||||
];
|
];
|
||||||
// Every injected lifecycle failure must stop at its command, before later policy actions.
|
// Every injected lifecycle failure must stop at its command, before later policy actions.
|
||||||
posixIt.each(
|
posixIt.for(
|
||||||
terminalCases.flatMap((entry) =>
|
terminalCases.flatMap((entry) =>
|
||||||
(["cleanup-failure", "cancel"] as const).map((code) => ({
|
(["cleanup-failure", "cancel"] as const).map((code) => ({
|
||||||
mode: entry.mode,
|
mode: entry.mode,
|
||||||
|
|
@ -134,8 +136,9 @@ posixIt.each(
|
||||||
),
|
),
|
||||||
)(
|
)(
|
||||||
"$mode $operation $code fences every later action",
|
"$mode $operation $code fences every later action",
|
||||||
async ({ mode, operation, code }) => {
|
{ timeout: 55_000 },
|
||||||
const report = await performanceRun(mode, {
|
async ({ mode, operation, code }, { signal }) => {
|
||||||
|
const report = await performanceRun(signal, mode, {
|
||||||
gitFault: { match: `^${operation}(?: |$)`, code },
|
gitFault: { match: `^${operation}(?: |$)`, code },
|
||||||
...(mode === "publish" && operation !== "config" && operation !== "push"
|
...(mode === "publish" && operation !== "config" && operation !== "push"
|
||||||
? { pushResults: [23] }
|
? { pushResults: [23] }
|
||||||
|
|
@ -158,13 +161,15 @@ posixIt.each(
|
||||||
expect(report.output).not.toContain("fixture backoff:");
|
expect(report.output).not.toContain("fixture backoff:");
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
55_000,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
posixIt.each(["hang", 23, 125] as const)(
|
posixIt.for(["hang", 23, 125] as const)(
|
||||||
"prepare initial fetch %s cannot reach checkout, commit or token readiness",
|
"prepare initial fetch %s cannot reach checkout, commit or token readiness",
|
||||||
async (failure) => {
|
{ timeout: 55_000 },
|
||||||
const report = await performanceRun("prepare", { fetchResults: [failure, failure, failure] });
|
async (failure, { signal }) => {
|
||||||
|
const report = await performanceRun(signal, "prepare", {
|
||||||
|
fetchResults: [failure, failure, failure],
|
||||||
|
});
|
||||||
expect(report.code, report.output).toBe(1);
|
expect(report.code, report.output).toBe(1);
|
||||||
expect(report.fetches).toHaveLength(3);
|
expect(report.fetches).toHaveLength(3);
|
||||||
expect(report.fetches.every(({ args }) => args.includes("--depth=1"))).toBe(true);
|
expect(report.fetches.every(({ args }) => args.includes("--depth=1"))).toBe(true);
|
||||||
|
|
@ -173,13 +178,12 @@ posixIt.each(["hang", 23, 125] as const)(
|
||||||
expect(report.githubOutput).toBe("ready=false\n");
|
expect(report.githubOutput).toBe("ready=false\n");
|
||||||
expect(report.githubSummary).toBe("");
|
expect(report.githubSummary).toBe("");
|
||||||
},
|
},
|
||||||
55_000,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
posixIt(
|
posixIt(
|
||||||
"initial duplicate is verified before token or push",
|
"initial duplicate is verified before token or push",
|
||||||
async () => {
|
async ({ signal }) => {
|
||||||
const report = await performanceRun("prepare", {
|
const report = await performanceRun(signal, "prepare", {
|
||||||
performance: { mode: "prepare", duplicate: true },
|
performance: { mode: "prepare", duplicate: true },
|
||||||
});
|
});
|
||||||
expect(report.code, report.output).toBe(0);
|
expect(report.code, report.output).toBe(0);
|
||||||
|
|
@ -199,21 +203,24 @@ posixIt(
|
||||||
55_000,
|
55_000,
|
||||||
);
|
);
|
||||||
|
|
||||||
posixIt.each([128, 125])(
|
posixIt.for([128, 125])(
|
||||||
"prepare duplicate inspection %s is terminal",
|
"prepare duplicate inspection %s is terminal",
|
||||||
async (code) => {
|
{ timeout: 55_000 },
|
||||||
const report = await performanceRun("prepare", { gitFault: { match: "^ls-tree ", code } });
|
async (code, { signal }) => {
|
||||||
|
const report = await performanceRun(signal, "prepare", {
|
||||||
|
gitFault: { match: "^ls-tree ", code },
|
||||||
|
});
|
||||||
expect(report.code, report.output).toBe(code);
|
expect(report.code, report.output).toBe(code);
|
||||||
expect(report.githubOutput).toBe("ready=false\n");
|
expect(report.githubOutput).toBe("ready=false\n");
|
||||||
expect(report.commands.at(-1)?.args[0]).toBe("ls-tree");
|
expect(report.commands.at(-1)?.args[0]).toBe("ls-tree");
|
||||||
},
|
},
|
||||||
55_000,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
posixIt.each([0, 1, 125])(
|
posixIt.for([0, 1, 125])(
|
||||||
"cached diff status %s commits only for ordinary 1",
|
"cached diff status %s commits only for ordinary 1",
|
||||||
async (code) => {
|
{ timeout: 55_000 },
|
||||||
const report = await performanceRun("prepare", {
|
async (code, { signal }) => {
|
||||||
|
const report = await performanceRun(signal, "prepare", {
|
||||||
gitFault: { match: "^diff --cached --quiet$", code, output: "" },
|
gitFault: { match: "^diff --cached --quiet$", code, output: "" },
|
||||||
});
|
});
|
||||||
expect(report.code, report.output).toBe(code > 1 ? code : 0);
|
expect(report.code, report.output).toBe(code > 1 ? code : 0);
|
||||||
|
|
@ -222,13 +229,13 @@ posixIt.each([0, 1, 125])(
|
||||||
);
|
);
|
||||||
expect(report.githubOutput.includes("ready=true\n")).toBe(code <= 1);
|
expect(report.githubOutput.includes("ready=true\n")).toBe(code <= 1);
|
||||||
},
|
},
|
||||||
55_000,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
posixIt.each([125, "hang"] as const)(
|
posixIt.for([125, "hang"] as const)(
|
||||||
"ambiguous push %s reconciles only after extinction",
|
"ambiguous push %s reconciles only after extinction",
|
||||||
async (code) => {
|
{ timeout: 55_000 },
|
||||||
const report = await performanceRun("publish", { pushResults: [code] });
|
async (code, { signal }) => {
|
||||||
|
const report = await performanceRun(signal, "publish", { pushResults: [code] });
|
||||||
expect(report.code, report.output).toBe(0);
|
expect(report.code, report.output).toBe(0);
|
||||||
expect(report.pushes).toHaveLength(2);
|
expect(report.pushes).toHaveLength(2);
|
||||||
expect(report.fetches).toHaveLength(1);
|
expect(report.fetches).toHaveLength(1);
|
||||||
|
|
@ -260,13 +267,13 @@ posixIt.each([125, "hang"] as const)(
|
||||||
Buffer.from("x-access-token:fixture-performance-token").toString("base64"),
|
Buffer.from("x-access-token:fixture-performance-token").toString("base64"),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
55_000,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
posixIt.each([false, true])(
|
posixIt.for([false, true])(
|
||||||
"five failed pushes always get five fetches (fetch fails=%s)",
|
"five failed pushes always get five fetches (fetch fails=%s)",
|
||||||
async (fetchFails) => {
|
{ timeout: 55_000 },
|
||||||
const report = await performanceRun("publish", {
|
async (fetchFails, { signal }) => {
|
||||||
|
const report = await performanceRun(signal, "publish", {
|
||||||
pushResults: [23, 23, 23, 23, 23],
|
pushResults: [23, 23, 23, 23, 23],
|
||||||
fetchResults: fetchFails ? [23, 23, 23, 23, 23] : [],
|
fetchResults: fetchFails ? [23, 23, 23, 23, 23] : [],
|
||||||
});
|
});
|
||||||
|
|
@ -289,13 +296,13 @@ posixIt.each([false, true])(
|
||||||
expect(report.githubSummary).toContain("failed after 5 attempts.");
|
expect(report.githubSummary).toContain("failed after 5 attempts.");
|
||||||
expect(report.githubSummary).not.toContain("Published report:");
|
expect(report.githubSummary).not.toContain("Published report:");
|
||||||
},
|
},
|
||||||
55_000,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
posixIt.each([1, 5])(
|
posixIt.for([1, 5])(
|
||||||
"remote duplicate after ambiguous attempt %s succeeds without replay",
|
"remote duplicate after ambiguous attempt %s succeeds without replay",
|
||||||
async (attempt) => {
|
{ timeout: 55_000 },
|
||||||
const report = await performanceRun("publish", {
|
async (attempt, { signal }) => {
|
||||||
|
const report = await performanceRun(signal, "publish", {
|
||||||
performance: { mode: "publish", remoteDuplicateAttempt: attempt },
|
performance: { mode: "publish", remoteDuplicateAttempt: attempt },
|
||||||
pushResults: Array.from({ length: attempt }, () => 124),
|
pushResults: Array.from({ length: attempt }, () => 124),
|
||||||
});
|
});
|
||||||
|
|
@ -305,13 +312,13 @@ posixIt.each([1, 5])(
|
||||||
expect(report.checkouts).toHaveLength(attempt - 1);
|
expect(report.checkouts).toHaveLength(attempt - 1);
|
||||||
expect(report.githubSummary).toContain("### Clawgrit report published");
|
expect(report.githubSummary).toContain("### Clawgrit report published");
|
||||||
},
|
},
|
||||||
55_000,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
posixIt.each([23, 125])(
|
posixIt.for([23, 125])(
|
||||||
"ordinary cherry-pick and abort %s failures remain visible publish failure",
|
"ordinary cherry-pick and abort %s failures remain visible publish failure",
|
||||||
async (code) => {
|
{ timeout: 55_000 },
|
||||||
const report = await performanceRun("publish", {
|
async (code, { signal }) => {
|
||||||
|
const report = await performanceRun(signal, "publish", {
|
||||||
pushResults: [23],
|
pushResults: [23],
|
||||||
gitFaults: [
|
gitFaults: [
|
||||||
{ match: "^cherry-pick -X ", code: 23 },
|
{ match: "^cherry-pick -X ", code: 23 },
|
||||||
|
|
@ -326,37 +333,40 @@ posixIt.each([23, 125])(
|
||||||
);
|
);
|
||||||
expect(report.githubSummary).toBe("");
|
expect(report.githubSummary).toBe("");
|
||||||
},
|
},
|
||||||
55_000,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
posixIt.each(["owner", "python", "git"] as const)(
|
posixIt.for(["owner", "python", "git"] as const)(
|
||||||
"prepare setup failure %s cannot publish readiness",
|
"prepare setup failure %s cannot publish readiness",
|
||||||
async (setupFailure) => {
|
{ timeout: 55_000 },
|
||||||
const report = await performanceRun("prepare", { setupFailure });
|
async (setupFailure, { signal }) => {
|
||||||
|
const report = await performanceRun(signal, "prepare", { setupFailure });
|
||||||
expect(report.code, report.output).not.toBe(0);
|
expect(report.code, report.output).not.toBe(0);
|
||||||
expect(report.pushes).toHaveLength(0);
|
expect(report.pushes).toHaveLength(0);
|
||||||
expect(report.githubOutput).not.toContain("ready=true");
|
expect(report.githubOutput).not.toContain("ready=true");
|
||||||
},
|
},
|
||||||
55_000,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
posixIt.each([125, "hang"] as const)(
|
posixIt.for([125, "hang"] as const)(
|
||||||
"reconciliation fetch %s warns once and retries without replay",
|
"reconciliation fetch %s warns once and retries without replay",
|
||||||
async (code) => {
|
{ timeout: 55_000 },
|
||||||
const report = await performanceRun("publish", { pushResults: [23], fetchResults: [code] });
|
async (code, { signal }) => {
|
||||||
|
const report = await performanceRun(signal, "publish", {
|
||||||
|
pushResults: [23],
|
||||||
|
fetchResults: [code],
|
||||||
|
});
|
||||||
expect(report.code, report.output).toBe(0);
|
expect(report.code, report.output).toBe(0);
|
||||||
expect(report.pushes).toHaveLength(2);
|
expect(report.pushes).toHaveLength(2);
|
||||||
expect(report.fetches).toHaveLength(1);
|
expect(report.fetches).toHaveLength(1);
|
||||||
expect(report.checkouts).toHaveLength(0);
|
expect(report.checkouts).toHaveLength(0);
|
||||||
expect(report.output.match(/::warning::Unable to refresh/gu)).toHaveLength(1);
|
expect(report.output.match(/::warning::Unable to refresh/gu)).toHaveLength(1);
|
||||||
},
|
},
|
||||||
55_000,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
posixIt.each([125, 128])(
|
posixIt.for([125, 128])(
|
||||||
"remote duplicate read %s is terminal, never absence",
|
"remote duplicate read %s is terminal, never absence",
|
||||||
async (code) => {
|
{ timeout: 55_000 },
|
||||||
const report = await performanceRun("publish", {
|
async (code, { signal }) => {
|
||||||
|
const report = await performanceRun(signal, "publish", {
|
||||||
pushResults: [23],
|
pushResults: [23],
|
||||||
gitFault: { match: "^ls-tree ", code },
|
gitFault: { match: "^ls-tree ", code },
|
||||||
});
|
});
|
||||||
|
|
@ -365,13 +375,13 @@ posixIt.each([125, 128])(
|
||||||
expect(report.githubSummary).toBe("");
|
expect(report.githubSummary).toBe("");
|
||||||
expect(report.checkouts).toHaveLength(0);
|
expect(report.checkouts).toHaveLength(0);
|
||||||
},
|
},
|
||||||
55_000,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
posixIt.each(["prepare", "publish"] as const)(
|
posixIt.for(["prepare", "publish"] as const)(
|
||||||
"%s cancellation during real TERM-resistant cleanup prevents continuation",
|
"%s cancellation during real TERM-resistant cleanup prevents continuation",
|
||||||
async (mode) => {
|
{ timeout: 55_000 },
|
||||||
const report = await performanceRun(mode, {
|
async (mode, { signal }) => {
|
||||||
|
const report = await performanceRun(signal, mode, {
|
||||||
cancelDuringCleanup: true,
|
cancelDuringCleanup: true,
|
||||||
cleanupCancelMatch: mode === "prepare" ? "^fetch " : "^push ",
|
cleanupCancelMatch: mode === "prepare" ? "^fetch " : "^push ",
|
||||||
...(mode === "prepare" ? { fetchResults: ["hang"] } : { pushResults: ["hang"] }),
|
...(mode === "prepare" ? { fetchResults: ["hang"] } : { pushResults: ["hang"] }),
|
||||||
|
|
@ -383,13 +393,12 @@ posixIt.each(["prepare", "publish"] as const)(
|
||||||
expect(report.githubSummary).toBe("");
|
expect(report.githubSummary).toBe("");
|
||||||
expect(report.output).not.toContain("fixture backoff:");
|
expect(report.output).not.toContain("fixture backoff:");
|
||||||
},
|
},
|
||||||
55_000,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
posixIt(
|
posixIt(
|
||||||
"cancellation during owned backoff prevents reconciliation fetch",
|
"cancellation during owned backoff prevents reconciliation fetch",
|
||||||
async () => {
|
async ({ signal }) => {
|
||||||
const report = await performanceRun("publish", {
|
const report = await performanceRun(signal, "publish", {
|
||||||
pushResults: [23],
|
pushResults: [23],
|
||||||
realClock: true,
|
realClock: true,
|
||||||
cooperativeTrees: true,
|
cooperativeTrees: true,
|
||||||
|
|
|
||||||
|
|
@ -1214,7 +1214,7 @@ printf '%s\\n' \
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
posixIt.each([
|
posixIt.for([
|
||||||
{ name: "direct", pushResults: [], fetchResults: [], success: true },
|
{ name: "direct", pushResults: [], fetchResults: [], success: true },
|
||||||
{ name: "remote duplicate", pushResults: [124], fetchResults: [], success: true, duplicate: 1 },
|
{ name: "remote duplicate", pushResults: [124], fetchResults: [], success: true, duplicate: 1 },
|
||||||
{
|
{
|
||||||
|
|
@ -1226,8 +1226,10 @@ printf '%s\\n' \
|
||||||
{ name: "missing token", pushResults: [], fetchResults: [], success: false, token: "" },
|
{ name: "missing token", pushResults: [], fetchResults: [], success: false, token: "" },
|
||||||
])(
|
])(
|
||||||
"advertises a clawgrit URL only after verified success ($name)",
|
"advertises a clawgrit URL only after verified success ($name)",
|
||||||
async ({ name, pushResults, fetchResults, success, duplicate, token }) => {
|
{ timeout: 55_000 },
|
||||||
|
async ({ name, pushResults, fetchResults, success, duplicate, token }, { signal }) => {
|
||||||
const report = await runCiGitStep({
|
const report = await runCiGitStep({
|
||||||
|
signal,
|
||||||
workflow: { file: WORKFLOW, job: "publish", step: "Publish to clawgrit reports" },
|
workflow: { file: WORKFLOW, job: "publish", step: "Publish to clawgrit reports" },
|
||||||
performance: { mode: "publish", remoteDuplicateAttempt: duplicate },
|
performance: { mode: "publish", remoteDuplicateAttempt: duplicate },
|
||||||
fetchResults,
|
fetchResults,
|
||||||
|
|
@ -1245,13 +1247,13 @@ printf '%s\\n' \
|
||||||
expect(report.githubSummary).toContain("ClawSweeper GitHub App installation");
|
expect(report.githubSummary).toContain("ClawSweeper GitHub App installation");
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
55_000,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
posixIt(
|
posixIt(
|
||||||
"preserves both reports when concurrent writers update one latest pointer",
|
"preserves both reports when concurrent writers update one latest pointer",
|
||||||
async () => {
|
async ({ signal }) => {
|
||||||
const report = await runCiGitStep({
|
const report = await runCiGitStep({
|
||||||
|
signal,
|
||||||
workflow: { file: WORKFLOW, job: "publish", step: "Publish to clawgrit reports" },
|
workflow: { file: WORKFLOW, job: "publish", step: "Publish to clawgrit reports" },
|
||||||
performance: { mode: "publish", race: true },
|
performance: { mode: "publish", race: true },
|
||||||
fetchResults: [],
|
fetchResults: [],
|
||||||
|
|
|
||||||
|
|
@ -106,9 +106,10 @@ const modes: Record<
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
function pluginRun(mode: PluginMode, options: RunOptions = {}) {
|
function pluginRun(signal: AbortSignal, mode: PluginMode, options: RunOptions = {}) {
|
||||||
const selected = modes[mode];
|
const selected = modes[mode];
|
||||||
return runCiGitStep({
|
return runCiGitStep({
|
||||||
|
signal,
|
||||||
workflow: selected.workflow,
|
workflow: selected.workflow,
|
||||||
fetchResults: [],
|
fetchResults: [],
|
||||||
...options,
|
...options,
|
||||||
|
|
@ -121,7 +122,7 @@ function gitCommands(report: Awaited<ReturnType<typeof pluginRun>>) {
|
||||||
return report.commands.filter(({ tool }) => tool === "git").map(({ args }) => args);
|
return report.commands.filter(({ tool }) => tool === "git").map(({ args }) => args);
|
||||||
}
|
}
|
||||||
|
|
||||||
posixIt.each([
|
posixIt.for([
|
||||||
{
|
{
|
||||||
mode: "clawhub-resolve" as const,
|
mode: "clawhub-resolve" as const,
|
||||||
commands: [
|
commands: [
|
||||||
|
|
@ -162,8 +163,9 @@ posixIt.each([
|
||||||
},
|
},
|
||||||
])(
|
])(
|
||||||
"$mode drains every Git tree before success or output",
|
"$mode drains every Git tree before success or output",
|
||||||
async ({ commands, mode, output }) => {
|
{ timeout: 55_000 },
|
||||||
const report = await pluginRun(mode);
|
async ({ commands, mode, output }, { signal }) => {
|
||||||
|
const report = await pluginRun(signal, mode);
|
||||||
expect(report.code, report.output).toBe(0);
|
expect(report.code, report.output).toBe(0);
|
||||||
expect(gitCommands(report)).toEqual(commands);
|
expect(gitCommands(report)).toEqual(commands);
|
||||||
expect(report.githubOutput).toBe(output);
|
expect(report.githubOutput).toBe(output);
|
||||||
|
|
@ -172,14 +174,14 @@ posixIt.each([
|
||||||
expect(report.boundaries.some(({ name }) => name === "output")).toBe(true);
|
expect(report.boundaries.some(({ name }) => name === "output")).toBe(true);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
55_000,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
posixIt.each(["npm-preflight-read", "npm-publish-read"] as const)(
|
posixIt.for(["npm-preflight-read", "npm-publish-read"] as const)(
|
||||||
"%s preserves exact source package bytes before the next consumer",
|
"%s preserves exact source package bytes before the next consumer",
|
||||||
async (mode) => {
|
{ timeout: 55_000 },
|
||||||
|
async (mode, { signal }) => {
|
||||||
const sourceRef = mode === "npm-preflight-read" ? "SOURCE_SHA" : "TARGET_SHA";
|
const sourceRef = mode === "npm-preflight-read" ? "SOURCE_SHA" : "TARGET_SHA";
|
||||||
const report = await pluginRun(mode, {
|
const report = await pluginRun(signal, mode, {
|
||||||
commandResults: {
|
commandResults: {
|
||||||
[`show ${sha}:${packageDir}/package.json`]: { code: 0, output: packageJson },
|
[`show ${sha}:${packageDir}/package.json`]: { code: 0, output: packageJson },
|
||||||
},
|
},
|
||||||
|
|
@ -192,13 +194,13 @@ posixIt.each(["npm-preflight-read", "npm-publish-read"] as const)(
|
||||||
expect(report.pluginSourcePackage).toBe(packageJson);
|
expect(report.pluginSourcePackage).toBe(packageJson);
|
||||||
expect(modes[mode].env[sourceRef]).toBe(sha);
|
expect(modes[mode].env[sourceRef]).toBe(sha);
|
||||||
},
|
},
|
||||||
55_000,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
posixIt.each(["npm-preflight-read", "npm-publish-read"] as const)(
|
posixIt.for(["npm-preflight-read", "npm-publish-read"] as const)(
|
||||||
"%s rejects partial source package output after ordinary show failure",
|
"%s rejects partial source package output after ordinary show failure",
|
||||||
async (mode) => {
|
{ timeout: 55_000 },
|
||||||
const report = await pluginRun(mode, {
|
async (mode, { signal }) => {
|
||||||
|
const report = await pluginRun(signal, mode, {
|
||||||
commandResults: {
|
commandResults: {
|
||||||
[`show ${sha}:${packageDir}/package.json`]: { code: 23, output: "{partial" },
|
[`show ${sha}:${packageDir}/package.json`]: { code: 23, output: "{partial" },
|
||||||
},
|
},
|
||||||
|
|
@ -207,28 +209,28 @@ posixIt.each(["npm-preflight-read", "npm-publish-read"] as const)(
|
||||||
expect(gitCommands(report).at(-1)?.[0]).toBe("show");
|
expect(gitCommands(report).at(-1)?.[0]).toBe("show");
|
||||||
expect(report.pluginSourcePackage).toBe("");
|
expect(report.pluginSourcePackage).toBe("");
|
||||||
},
|
},
|
||||||
55_000,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
posixIt.each(
|
posixIt.for(
|
||||||
(["npm-preflight-read", "npm-publish-read"] as const).flatMap((mode) =>
|
(["npm-preflight-read", "npm-publish-read"] as const).flatMap((mode) =>
|
||||||
([23, 125, "hang"] as const).map((failure) => ({ failure, mode })),
|
([23, 125, "hang"] as const).map((failure) => ({ failure, mode })),
|
||||||
),
|
),
|
||||||
)(
|
)(
|
||||||
"$mode fetch failure $failure stops before source package readback",
|
"$mode fetch failure $failure stops before source package readback",
|
||||||
async ({ failure, mode }) => {
|
{ timeout: 55_000 },
|
||||||
const report = await pluginRun(mode, { fetchResults: [failure] });
|
async ({ failure, mode }, { signal }) => {
|
||||||
|
const report = await pluginRun(signal, mode, { fetchResults: [failure] });
|
||||||
expect(report.code, report.output).toBe(failure === "hang" ? 124 : failure);
|
expect(report.code, report.output).toBe(failure === "hang" ? 124 : failure);
|
||||||
expect(gitCommands(report).at(-1)?.[0]).toBe("fetch");
|
expect(gitCommands(report).at(-1)?.[0]).toBe("fetch");
|
||||||
expect(report.pluginSourcePackage).toBe("");
|
expect(report.pluginSourcePackage).toBe("");
|
||||||
},
|
},
|
||||||
55_000,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
posixIt.each([1, 125, 143])(
|
posixIt.for([1, 125, 143])(
|
||||||
"ClawHub resolves origin fallback after safely drained ordinary local probe failure %s",
|
"ClawHub resolves origin fallback after safely drained ordinary local probe failure %s",
|
||||||
async (code) => {
|
{ timeout: 55_000 },
|
||||||
const report = await pluginRun("clawhub-resolve", {
|
async (code, { signal }) => {
|
||||||
|
const report = await pluginRun(signal, "clawhub-resolve", {
|
||||||
env: { TARGET_REF: "release/fixture" },
|
env: { TARGET_REF: "release/fixture" },
|
||||||
commandResults: {
|
commandResults: {
|
||||||
"rev-parse --verify --quiet release/fixture^{commit}": { code, output: "" },
|
"rev-parse --verify --quiet release/fixture^{commit}": { code, output: "" },
|
||||||
|
|
@ -252,13 +254,12 @@ posixIt.each([1, 125, 143])(
|
||||||
]);
|
]);
|
||||||
expect(report.githubOutput).toBe(`sha=${sha}\n`);
|
expect(report.githubOutput).toBe(`sha=${sha}\n`);
|
||||||
},
|
},
|
||||||
55_000,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
posixIt(
|
posixIt(
|
||||||
"ClawHub release tags retain their second bounded fetch",
|
"ClawHub release tags retain their second bounded fetch",
|
||||||
async () => {
|
async ({ signal }) => {
|
||||||
const report = await pluginRun("clawhub-resolve", {
|
const report = await pluginRun(signal, "clawhub-resolve", {
|
||||||
env: { RELEASE_TAG: releaseTag },
|
env: { RELEASE_TAG: releaseTag },
|
||||||
});
|
});
|
||||||
expect(report.code, report.output).toBe(0);
|
expect(report.code, report.output).toBe(0);
|
||||||
|
|
@ -278,8 +279,8 @@ posixIt(
|
||||||
|
|
||||||
posixIt(
|
posixIt(
|
||||||
"ClawHub protected tooling validates the exact peeled release target",
|
"ClawHub protected tooling validates the exact peeled release target",
|
||||||
async () => {
|
async ({ signal }) => {
|
||||||
const report = await pluginRun("clawhub-oidc", {
|
const report = await pluginRun(signal, "clawhub-oidc", {
|
||||||
env: {
|
env: {
|
||||||
RELEASE_PUBLISH_RUN_ATTEMPT: "2",
|
RELEASE_PUBLISH_RUN_ATTEMPT: "2",
|
||||||
RELEASE_PUBLISH_RUN_ID: "123",
|
RELEASE_PUBLISH_RUN_ID: "123",
|
||||||
|
|
@ -299,10 +300,11 @@ posixIt(
|
||||||
55_000,
|
55_000,
|
||||||
);
|
);
|
||||||
|
|
||||||
posixIt.each([1, 125])(
|
posixIt.for([1, 125])(
|
||||||
"ClawHub protected tag ordinary lookup failure %s retains OIDC rejection",
|
"ClawHub protected tag ordinary lookup failure %s retains OIDC rejection",
|
||||||
async (code) => {
|
{ timeout: 55_000 },
|
||||||
const report = await pluginRun("clawhub-oidc", {
|
async (code, { signal }) => {
|
||||||
|
const report = await pluginRun(signal, "clawhub-oidc", {
|
||||||
env: {
|
env: {
|
||||||
RELEASE_PUBLISH_RUN_ATTEMPT: "2",
|
RELEASE_PUBLISH_RUN_ATTEMPT: "2",
|
||||||
RELEASE_PUBLISH_RUN_ID: "123",
|
RELEASE_PUBLISH_RUN_ID: "123",
|
||||||
|
|
@ -318,13 +320,13 @@ posixIt.each([1, 125])(
|
||||||
"Plugin ClawHub OIDC publish target is not bound to protected tooling and the exact release tag.",
|
"Plugin ClawHub OIDC publish target is not bound to protected tooling and the exact release tag.",
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
55_000,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
posixIt.each(["clawhub-trust", "npm-trust"] as const)(
|
posixIt.for(["clawhub-trust", "npm-trust"] as const)(
|
||||||
"%s accepts a release branch only after successful enumeration",
|
"%s accepts a release branch only after successful enumeration",
|
||||||
async (mode) => {
|
{ timeout: 55_000 },
|
||||||
const report = await pluginRun(mode, {
|
async (mode, { signal }) => {
|
||||||
|
const report = await pluginRun(signal, mode, {
|
||||||
commandResults: {
|
commandResults: {
|
||||||
"merge-base --is-ancestor HEAD origin/main": { code: 1 },
|
"merge-base --is-ancestor HEAD origin/main": { code: 1 },
|
||||||
"for-each-ref --format=%(refname) refs/remotes/origin/release": {
|
"for-each-ref --format=%(refname) refs/remotes/origin/release": {
|
||||||
|
|
@ -342,13 +344,12 @@ posixIt.each(["clawhub-trust", "npm-trust"] as const)(
|
||||||
"refs/remotes/origin/release/2026.8.1",
|
"refs/remotes/origin/release/2026.8.1",
|
||||||
]);
|
]);
|
||||||
},
|
},
|
||||||
55_000,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
posixIt(
|
posixIt(
|
||||||
"npm-trust rejects a Tideclaw alpha publish after main and release misses",
|
"npm-trust rejects a Tideclaw alpha publish after main and release misses",
|
||||||
async () => {
|
async ({ signal }) => {
|
||||||
const report = await pluginRun("npm-trust", {
|
const report = await pluginRun(signal, "npm-trust", {
|
||||||
env: { WORKFLOW_REF: `refs/heads/${alphaBranch}` },
|
env: { WORKFLOW_REF: `refs/heads/${alphaBranch}` },
|
||||||
commandResults: {
|
commandResults: {
|
||||||
"merge-base --is-ancestor HEAD origin/main": { code: 1 },
|
"merge-base --is-ancestor HEAD origin/main": { code: 1 },
|
||||||
|
|
@ -366,8 +367,8 @@ posixIt(
|
||||||
|
|
||||||
posixIt(
|
posixIt(
|
||||||
"clawhub-trust rejects a retired Tideclaw alpha branch before ancestry admission",
|
"clawhub-trust rejects a retired Tideclaw alpha branch before ancestry admission",
|
||||||
async () => {
|
async ({ signal }) => {
|
||||||
const report = await pluginRun("clawhub-trust", {
|
const report = await pluginRun(signal, "clawhub-trust", {
|
||||||
env: { TRUSTED_PUBLISH_BRANCH: alphaBranch },
|
env: { TRUSTED_PUBLISH_BRANCH: alphaBranch },
|
||||||
commandResults: {
|
commandResults: {
|
||||||
"merge-base --is-ancestor HEAD origin/main": { code: 1 },
|
"merge-base --is-ancestor HEAD origin/main": { code: 1 },
|
||||||
|
|
@ -382,11 +383,12 @@ posixIt(
|
||||||
55_000,
|
55_000,
|
||||||
);
|
);
|
||||||
|
|
||||||
posixIt.each(["refs/heads/extended-stable/2026.8.33", "refs/heads/main"])(
|
posixIt.for(["refs/heads/extended-stable/2026.8.33", "refs/heads/main"])(
|
||||||
"npm extended-stable preflight retains exact-tip admission from %s",
|
"npm extended-stable preflight retains exact-tip admission from %s",
|
||||||
async (workflowRef) => {
|
{ timeout: 55_000 },
|
||||||
|
async (workflowRef, { signal }) => {
|
||||||
const branch = "extended-stable/2026.8.33";
|
const branch = "extended-stable/2026.8.33";
|
||||||
const report = await pluginRun("npm-trust", {
|
const report = await pluginRun(signal, "npm-trust", {
|
||||||
env: {
|
env: {
|
||||||
PREFLIGHT_ONLY: "true",
|
PREFLIGHT_ONLY: "true",
|
||||||
NPM_DIST_TAG: "extended-stable",
|
NPM_DIST_TAG: "extended-stable",
|
||||||
|
|
@ -407,7 +409,6 @@ posixIt.each(["refs/heads/extended-stable/2026.8.33", "refs/heads/main"])(
|
||||||
// Preflight adds its exact-source check before the extended-stable tip check.
|
// Preflight adds its exact-source check before the extended-stable tip check.
|
||||||
expect(gitCommands(report).filter(([operation]) => operation === "rev-parse")).toHaveLength(6);
|
expect(gitCommands(report).filter(([operation]) => operation === "rev-parse")).toHaveLength(6);
|
||||||
},
|
},
|
||||||
55_000,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const candidateAdmissionCases: Array<{
|
const candidateAdmissionCases: Array<{
|
||||||
|
|
@ -471,10 +472,11 @@ const candidateAdmissionCases: Array<{
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
posixIt.each(candidateAdmissionCases)(
|
posixIt.for(candidateAdmissionCases)(
|
||||||
"npm canonical candidate admission: $name",
|
"npm canonical candidate admission: $name",
|
||||||
async ({ env, commands, code, message }) => {
|
{ timeout: 55_000 },
|
||||||
const report = await pluginRun("npm-trust", {
|
async ({ env, commands, code, message }, { signal }) => {
|
||||||
|
const report = await pluginRun(signal, "npm-trust", {
|
||||||
env: {
|
env: {
|
||||||
NPM_DIST_TAG: "extended-stable",
|
NPM_DIST_TAG: "extended-stable",
|
||||||
PUBLISH_SCOPE: "all-publishable",
|
PUBLISH_SCOPE: "all-publishable",
|
||||||
|
|
@ -497,18 +499,18 @@ posixIt.each(candidateAdmissionCases)(
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
55_000,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
posixIt.each([
|
posixIt.for([
|
||||||
["moved canonical tip", "refs/heads/main", "c".repeat(40)],
|
["moved canonical tip", "refs/heads/main", "c".repeat(40)],
|
||||||
["untrusted workflow branch", "refs/heads/topic", sha],
|
["untrusted workflow branch", "refs/heads/topic", sha],
|
||||||
["same-name main tag", "refs/tags/main", sha],
|
["same-name main tag", "refs/tags/main", sha],
|
||||||
])(
|
] as const)(
|
||||||
"npm extended-stable preflight rejects %s",
|
"npm extended-stable preflight rejects %s",
|
||||||
async (_name, workflowRef, branchSha) => {
|
{ timeout: 55_000 },
|
||||||
|
async ([_name, workflowRef, branchSha], { signal }) => {
|
||||||
const branch = "extended-stable/2026.8.33";
|
const branch = "extended-stable/2026.8.33";
|
||||||
const report = await pluginRun("npm-trust", {
|
const report = await pluginRun(signal, "npm-trust", {
|
||||||
env: {
|
env: {
|
||||||
PREFLIGHT_ONLY: "true",
|
PREFLIGHT_ONLY: "true",
|
||||||
NPM_DIST_TAG: "extended-stable",
|
NPM_DIST_TAG: "extended-stable",
|
||||||
|
|
@ -524,13 +526,12 @@ posixIt.each([
|
||||||
expect(report.code, report.output).toBe(1);
|
expect(report.code, report.output).toBe(1);
|
||||||
expect(report.output).toContain("Extended-stable plugin");
|
expect(report.output).toContain("Extended-stable plugin");
|
||||||
},
|
},
|
||||||
55_000,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
posixIt(
|
posixIt(
|
||||||
"npm preflight rejects before Tideclaw fallback after main and release misses",
|
"npm preflight rejects before Tideclaw fallback after main and release misses",
|
||||||
async () => {
|
async ({ signal }) => {
|
||||||
const report = await pluginRun("npm-trust", {
|
const report = await pluginRun(signal, "npm-trust", {
|
||||||
env: { PREFLIGHT_ONLY: "true", SOURCE_REF: sha, WORKFLOW_REF: `refs/heads/${alphaBranch}` },
|
env: { PREFLIGHT_ONLY: "true", SOURCE_REF: sha, WORKFLOW_REF: `refs/heads/${alphaBranch}` },
|
||||||
revisions: { [`${sha}^{commit}`]: sha },
|
revisions: { [`${sha}^{commit}`]: sha },
|
||||||
commandResults: {
|
commandResults: {
|
||||||
|
|
@ -548,23 +549,24 @@ posixIt(
|
||||||
55_000,
|
55_000,
|
||||||
);
|
);
|
||||||
|
|
||||||
posixIt.each(["clawhub-trust", "npm-trust"] as const)(
|
posixIt.for(["clawhub-trust", "npm-trust"] as const)(
|
||||||
"%s treats merge-base errors other than ordinary 1 as terminal",
|
"%s treats merge-base errors other than ordinary 1 as terminal",
|
||||||
async (mode) => {
|
{ timeout: 55_000 },
|
||||||
const report = await pluginRun(mode, {
|
async (mode, { signal }) => {
|
||||||
|
const report = await pluginRun(signal, mode, {
|
||||||
commandResults: { "merge-base --is-ancestor HEAD origin/main": { code: 23 } },
|
commandResults: { "merge-base --is-ancestor HEAD origin/main": { code: 23 } },
|
||||||
});
|
});
|
||||||
expect(report.code, report.output).toBe(23);
|
expect(report.code, report.output).toBe(23);
|
||||||
expect(gitCommands(report)).toHaveLength(mode === "npm-trust" ? 2 : 1);
|
expect(gitCommands(report)).toHaveLength(mode === "npm-trust" ? 2 : 1);
|
||||||
expect(report.fetches).toHaveLength(mode === "npm-trust" ? 1 : 0);
|
expect(report.fetches).toHaveLength(mode === "npm-trust" ? 1 : 0);
|
||||||
},
|
},
|
||||||
55_000,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
posixIt.each(["clawhub-trust", "npm-trust"] as const)(
|
posixIt.for(["clawhub-trust", "npm-trust"] as const)(
|
||||||
"%s treats release-ref enumeration failure as terminal",
|
"%s treats release-ref enumeration failure as terminal",
|
||||||
async (mode) => {
|
{ timeout: 55_000 },
|
||||||
const report = await pluginRun(mode, {
|
async (mode, { signal }) => {
|
||||||
|
const report = await pluginRun(signal, mode, {
|
||||||
commandResults: {
|
commandResults: {
|
||||||
"merge-base --is-ancestor HEAD origin/main": { code: 1 },
|
"merge-base --is-ancestor HEAD origin/main": { code: 1 },
|
||||||
"for-each-ref --format=%(refname) refs/remotes/origin/release": { code: 23 },
|
"for-each-ref --format=%(refname) refs/remotes/origin/release": { code: 23 },
|
||||||
|
|
@ -574,7 +576,6 @@ posixIt.each(["clawhub-trust", "npm-trust"] as const)(
|
||||||
expect(gitCommands(report).at(-1)?.[0]).toBe("for-each-ref");
|
expect(gitCommands(report).at(-1)?.[0]).toBe("for-each-ref");
|
||||||
expect(report.fetches).toHaveLength(mode === "npm-trust" ? 1 : 0);
|
expect(report.fetches).toHaveLength(mode === "npm-trust" ? 1 : 0);
|
||||||
},
|
},
|
||||||
55_000,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const terminalCases: Array<{
|
const terminalCases: Array<{
|
||||||
|
|
@ -657,7 +658,7 @@ const terminalCases: Array<{
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
posixIt.each(
|
posixIt.for(
|
||||||
terminalCases.flatMap((entry) =>
|
terminalCases.flatMap((entry) =>
|
||||||
(["cleanup-failure", "cancel"] as const).map((failure) =>
|
(["cleanup-failure", "cancel"] as const).map((failure) =>
|
||||||
Object.assign({}, entry, { failure }),
|
Object.assign({}, entry, { failure }),
|
||||||
|
|
@ -665,8 +666,9 @@ posixIt.each(
|
||||||
),
|
),
|
||||||
)(
|
)(
|
||||||
"$mode $operation $failure fences every later Git/output/consumer boundary",
|
"$mode $operation $failure fences every later Git/output/consumer boundary",
|
||||||
async ({ commandResults, env, failure, match, mode, operation, revisions }) => {
|
{ timeout: 55_000 },
|
||||||
const report = await pluginRun(mode, {
|
async ({ commandResults, env, failure, match, mode, operation, revisions }, { signal }) => {
|
||||||
|
const report = await pluginRun(signal, mode, {
|
||||||
commandResults,
|
commandResults,
|
||||||
env,
|
env,
|
||||||
revisions,
|
revisions,
|
||||||
|
|
@ -680,20 +682,19 @@ posixIt.each(
|
||||||
expect(report.githubOutput).toBe("");
|
expect(report.githubOutput).toBe("");
|
||||||
expect(report.commands.some(({ tool }) => ["node", "pnpm"].includes(tool))).toBe(false);
|
expect(report.commands.some(({ tool }) => ["node", "pnpm"].includes(tool))).toBe(false);
|
||||||
},
|
},
|
||||||
55_000,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
posixIt.each(
|
posixIt.for(
|
||||||
(["clawhub-resolve", "npm-resolve", "npm-preflight-read", "npm-publish-read"] as const).flatMap(
|
(["clawhub-resolve", "npm-resolve", "npm-preflight-read", "npm-publish-read"] as const).flatMap(
|
||||||
(mode) => (["owner", "python", "git"] as const).map((setupFailure) => ({ mode, setupFailure })),
|
(mode) => (["owner", "python", "git"] as const).map((setupFailure) => ({ mode, setupFailure })),
|
||||||
),
|
),
|
||||||
)(
|
)(
|
||||||
"$mode setup failure $setupFailure cannot publish or consume Git output",
|
"$mode setup failure $setupFailure cannot publish or consume Git output",
|
||||||
async ({ mode, setupFailure }) => {
|
{ timeout: 55_000 },
|
||||||
const report = await pluginRun(mode, { setupFailure });
|
async ({ mode, setupFailure }, { signal }) => {
|
||||||
|
const report = await pluginRun(signal, mode, { setupFailure });
|
||||||
expect(report.code, report.output).not.toBe(0);
|
expect(report.code, report.output).not.toBe(0);
|
||||||
expect(report.githubOutput).toBe("");
|
expect(report.githubOutput).toBe("");
|
||||||
expect(report.commands.some(({ tool }) => ["node", "pnpm"].includes(tool))).toBe(false);
|
expect(report.commands.some(({ tool }) => ["node", "pnpm"].includes(tool))).toBe(false);
|
||||||
},
|
},
|
||||||
55_000,
|
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -51,9 +51,10 @@ const releases: Record<
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
function releaseRun(mode: ReleaseMode, options: RunOptions = {}) {
|
function releaseRun(signal: AbortSignal, mode: ReleaseMode, options: RunOptions = {}) {
|
||||||
const release = releases[mode];
|
const release = releases[mode];
|
||||||
return runCiGitStep({
|
return runCiGitStep({
|
||||||
|
signal,
|
||||||
workflow: release.workflow,
|
workflow: release.workflow,
|
||||||
fetchResults: [],
|
fetchResults: [],
|
||||||
...options,
|
...options,
|
||||||
|
|
@ -66,10 +67,11 @@ function gitCommands(report: Awaited<ReturnType<typeof releaseRun>>) {
|
||||||
return report.commands.filter(({ tool }) => tool === "git").map(({ args }) => args);
|
return report.commands.filter(({ tool }) => tool === "git").map(({ args }) => args);
|
||||||
}
|
}
|
||||||
|
|
||||||
posixIt.each([releaseTag, `${releaseTag}-2`])(
|
posixIt.for([releaseTag, `${releaseTag}-2`])(
|
||||||
"Linux admits a stable tag from its matching release branch: %s",
|
"Linux admits a stable tag from its matching release branch: %s",
|
||||||
async (tag) => {
|
{ timeout: 55_000 },
|
||||||
const report = await releaseRun("linux", {
|
async (tag, { signal }) => {
|
||||||
|
const report = await releaseRun(signal, "linux", {
|
||||||
env: { RELEASE_TAG: tag },
|
env: { RELEASE_TAG: tag },
|
||||||
revisions: { [`refs/tags/${tag}^{commit}`]: sha },
|
revisions: { [`refs/tags/${tag}^{commit}`]: sha },
|
||||||
commandResults: {
|
commandResults: {
|
||||||
|
|
@ -86,10 +88,9 @@ posixIt.each([releaseTag, `${releaseTag}-2`])(
|
||||||
"+refs/heads/release/2026.8.1:refs/remotes/origin/release/2026.8.1",
|
"+refs/heads/release/2026.8.1:refs/remotes/origin/release/2026.8.1",
|
||||||
]);
|
]);
|
||||||
},
|
},
|
||||||
55_000,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
posixIt.each([
|
posixIt.for([
|
||||||
{
|
{
|
||||||
mode: "linux" as const,
|
mode: "linux" as const,
|
||||||
commands: [
|
commands: [
|
||||||
|
|
@ -120,8 +121,9 @@ posixIt.each([
|
||||||
},
|
},
|
||||||
])(
|
])(
|
||||||
"$mode admission drains every Git tree before output or consumer",
|
"$mode admission drains every Git tree before output or consumer",
|
||||||
async ({ mode, commands, output }) => {
|
{ timeout: 55_000 },
|
||||||
const report = await releaseRun(mode);
|
async ({ mode, commands, output }, { signal }) => {
|
||||||
|
const report = await releaseRun(signal, mode);
|
||||||
expect(report.code, report.output).toBe(0);
|
expect(report.code, report.output).toBe(0);
|
||||||
expect(gitCommands(report)).toEqual(commands);
|
expect(gitCommands(report)).toEqual(commands);
|
||||||
expect(report.githubOutput).toBe(output);
|
expect(report.githubOutput).toBe(output);
|
||||||
|
|
@ -135,33 +137,33 @@ posixIt.each([
|
||||||
expect(report.boundaries.some(({ name }) => name === "output")).toBe(true);
|
expect(report.boundaries.some(({ name }) => name === "output")).toBe(true);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
55_000,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
posixIt.each(
|
posixIt.for(
|
||||||
(["linux", "macos", "placeholder"] as const).flatMap((mode) =>
|
(["linux", "macos", "placeholder"] as const).flatMap((mode) =>
|
||||||
([23, 125, "hang"] as const).map((failure) => ({ failure, mode })),
|
([23, 125, "hang"] as const).map((failure) => ({ failure, mode })),
|
||||||
),
|
),
|
||||||
)(
|
)(
|
||||||
"$mode fetch failure $failure stops before output or consumer",
|
"$mode fetch failure $failure stops before output or consumer",
|
||||||
async ({ failure, mode }) => {
|
{ timeout: 55_000 },
|
||||||
const report = await releaseRun(mode, { fetchResults: [failure] });
|
async ({ failure, mode }, { signal }) => {
|
||||||
|
const report = await releaseRun(signal, mode, { fetchResults: [failure] });
|
||||||
expect(report.code, report.output).toBe(failure === "hang" ? 124 : failure);
|
expect(report.code, report.output).toBe(failure === "hang" ? 124 : failure);
|
||||||
expect(gitCommands(report).at(-1)?.[0]).toBe("fetch");
|
expect(gitCommands(report).at(-1)?.[0]).toBe("fetch");
|
||||||
expect(report.githubOutput).toBe("");
|
expect(report.githubOutput).toBe("");
|
||||||
expect(report.commands.some(({ tool }) => tool === "pnpm")).toBe(false);
|
expect(report.commands.some(({ tool }) => tool === "pnpm")).toBe(false);
|
||||||
},
|
},
|
||||||
55_000,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
posixIt.each(
|
posixIt.for(
|
||||||
(["linux", "macos"] as const).flatMap((mode) =>
|
(["linux", "macos"] as const).flatMap((mode) =>
|
||||||
([23, 125] as const).map((code) => ({ code, mode })),
|
([23, 125] as const).map((code) => ({ code, mode })),
|
||||||
),
|
),
|
||||||
)(
|
)(
|
||||||
"$mode ordinary rev-parse status $code remains terminal",
|
"$mode ordinary rev-parse status $code remains terminal",
|
||||||
async ({ code, mode }) => {
|
{ timeout: 55_000 },
|
||||||
const report = await releaseRun(mode, {
|
async ({ code, mode }, { signal }) => {
|
||||||
|
const report = await releaseRun(signal, mode, {
|
||||||
gitFault: { match: "^rev-parse ", code },
|
gitFault: { match: "^rev-parse ", code },
|
||||||
});
|
});
|
||||||
expect(report.code, report.output).toBe(code);
|
expect(report.code, report.output).toBe(code);
|
||||||
|
|
@ -169,13 +171,12 @@ posixIt.each(
|
||||||
expect(report.githubOutput).toBe("");
|
expect(report.githubOutput).toBe("");
|
||||||
expect(report.commands.some(({ tool }) => tool === "pnpm")).toBe(false);
|
expect(report.commands.some(({ tool }) => tool === "pnpm")).toBe(false);
|
||||||
},
|
},
|
||||||
55_000,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
posixIt(
|
posixIt(
|
||||||
"macOS rejects an invalid public branch before any Git command",
|
"macOS rejects an invalid public branch before any Git command",
|
||||||
async () => {
|
async ({ signal }) => {
|
||||||
const report = await releaseRun("macos", {
|
const report = await releaseRun(signal, "macos", {
|
||||||
env: { PUBLIC_RELEASE_BRANCH: "feature/not-a-release" },
|
env: { PUBLIC_RELEASE_BRANCH: "feature/not-a-release" },
|
||||||
});
|
});
|
||||||
expect(report.code, report.output).toBe(1);
|
expect(report.code, report.output).toBe(1);
|
||||||
|
|
@ -210,7 +211,7 @@ const terminalOperations = [
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
posixIt.each(
|
posixIt.for(
|
||||||
terminalOperations.flatMap((entry) =>
|
terminalOperations.flatMap((entry) =>
|
||||||
(["cleanup-failure", "cancel"] as const).map((failure) =>
|
(["cleanup-failure", "cancel"] as const).map((failure) =>
|
||||||
Object.assign({}, entry, { failure }),
|
Object.assign({}, entry, { failure }),
|
||||||
|
|
@ -218,8 +219,9 @@ posixIt.each(
|
||||||
),
|
),
|
||||||
)(
|
)(
|
||||||
"$mode $operation $failure is terminal before every later boundary",
|
"$mode $operation $failure is terminal before every later boundary",
|
||||||
async ({ failure, match, mode, occurrence, operation }) => {
|
{ timeout: 55_000 },
|
||||||
const report = await releaseRun(mode, {
|
async ({ failure, match, mode, occurrence, operation }, { signal }) => {
|
||||||
|
const report = await releaseRun(signal, mode, {
|
||||||
gitFault: { match, occurrence, code: failure },
|
gitFault: { match, occurrence, code: failure },
|
||||||
});
|
});
|
||||||
expect(report.code, report.output).toBe(failure === "cancel" ? 143 : 125);
|
expect(report.code, report.output).toBe(failure === "cancel" ? 143 : 125);
|
||||||
|
|
@ -228,26 +230,25 @@ posixIt.each(
|
||||||
expect(report.commands.some(({ tool }) => tool === "pnpm")).toBe(false);
|
expect(report.commands.some(({ tool }) => tool === "pnpm")).toBe(false);
|
||||||
expect(report.output).not.toMatch(/not reachable|requires ref to equal/u);
|
expect(report.output).not.toMatch(/not reachable|requires ref to equal/u);
|
||||||
},
|
},
|
||||||
55_000,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
posixIt.each([23, 125])(
|
posixIt.for([23, 125])(
|
||||||
"Linux ordinary merge-base status %s is terminal without trying another branch",
|
"Linux ordinary merge-base status %s is terminal without trying another branch",
|
||||||
async (code) => {
|
{ timeout: 55_000 },
|
||||||
const report = await releaseRun("linux", {
|
async (code, { signal }) => {
|
||||||
|
const report = await releaseRun(signal, "linux", {
|
||||||
gitFault: { match: "^merge-base ", code },
|
gitFault: { match: "^merge-base ", code },
|
||||||
});
|
});
|
||||||
expect(report.code, report.output).toBe(code);
|
expect(report.code, report.output).toBe(code);
|
||||||
expect(gitCommands(report).at(-1)?.[0]).toBe("merge-base");
|
expect(gitCommands(report).at(-1)?.[0]).toBe("merge-base");
|
||||||
expect(report.githubOutput).toBe("");
|
expect(report.githubOutput).toBe("");
|
||||||
},
|
},
|
||||||
55_000,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
posixIt(
|
posixIt(
|
||||||
"Linux rejects a tag outside main and its matching release branch",
|
"Linux rejects a tag outside main and its matching release branch",
|
||||||
async () => {
|
async ({ signal }) => {
|
||||||
const report = await releaseRun("linux", {
|
const report = await releaseRun(signal, "linux", {
|
||||||
commandResults: {
|
commandResults: {
|
||||||
[`merge-base --is-ancestor ${sha} origin/main`]: { code: 1 },
|
[`merge-base --is-ancestor ${sha} origin/main`]: { code: 1 },
|
||||||
[`merge-base --is-ancestor ${sha} refs/remotes/origin/release/2026.8.1`]: { code: 1 },
|
[`merge-base --is-ancestor ${sha} refs/remotes/origin/release/2026.8.1`]: { code: 1 },
|
||||||
|
|
@ -264,8 +265,8 @@ posixIt(
|
||||||
|
|
||||||
posixIt(
|
posixIt(
|
||||||
"Linux rejects tooling outside main before inspecting the candidate",
|
"Linux rejects tooling outside main before inspecting the candidate",
|
||||||
async () => {
|
async ({ signal }) => {
|
||||||
const report = await releaseRun("linux", {
|
const report = await releaseRun(signal, "linux", {
|
||||||
commandResults: { [`merge-base --is-ancestor ${otherSha} origin/main`]: { code: 1 } },
|
commandResults: { [`merge-base --is-ancestor ${otherSha} origin/main`]: { code: 1 } },
|
||||||
});
|
});
|
||||||
expect(report.code, report.output).toBe(1);
|
expect(report.code, report.output).toBe(1);
|
||||||
|
|
@ -276,10 +277,11 @@ posixIt(
|
||||||
55_000,
|
55_000,
|
||||||
);
|
);
|
||||||
|
|
||||||
posixIt.each([128, "cleanup-failure", "cancel"] as const)(
|
posixIt.for([128, "cleanup-failure", "cancel"] as const)(
|
||||||
"Linux matching release branch fetch failure %s cannot admit a stale ref",
|
"Linux matching release branch fetch failure %s cannot admit a stale ref",
|
||||||
async (failure) => {
|
{ timeout: 55_000 },
|
||||||
const report = await releaseRun("linux", {
|
async (failure, { signal }) => {
|
||||||
|
const report = await releaseRun(signal, "linux", {
|
||||||
commandResults: { [`merge-base --is-ancestor ${sha} origin/main`]: { code: 1 } },
|
commandResults: { [`merge-base --is-ancestor ${sha} origin/main`]: { code: 1 } },
|
||||||
gitFault: { match: "^fetch ", occurrence: 2, code: failure },
|
gitFault: { match: "^fetch ", occurrence: 2, code: failure },
|
||||||
});
|
});
|
||||||
|
|
@ -289,16 +291,16 @@ posixIt.each([128, "cleanup-failure", "cancel"] as const)(
|
||||||
expect(gitCommands(report).at(-1)?.[0]).toBe("fetch");
|
expect(gitCommands(report).at(-1)?.[0]).toBe("fetch");
|
||||||
expect(report.githubOutput).toBe("");
|
expect(report.githubOutput).toBe("");
|
||||||
},
|
},
|
||||||
55_000,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
posixIt.each([
|
posixIt.for([
|
||||||
{ occurrence: 1, message: "workflow revision is not reachable" },
|
{ occurrence: 1, message: "workflow revision is not reachable" },
|
||||||
{ occurrence: 2, message: "target must be reachable" },
|
{ occurrence: 2, message: "target must be reachable" },
|
||||||
])(
|
])(
|
||||||
"placeholder ordinary merge-base failure $occurrence keeps its custom rejection",
|
"placeholder ordinary merge-base failure $occurrence keeps its custom rejection",
|
||||||
async ({ message, occurrence }) => {
|
{ timeout: 55_000 },
|
||||||
const report = await releaseRun("placeholder", {
|
async ({ message, occurrence }, { signal }) => {
|
||||||
|
const report = await releaseRun(signal, "placeholder", {
|
||||||
gitFault: { match: "^merge-base ", occurrence, code: 23 },
|
gitFault: { match: "^merge-base ", occurrence, code: 23 },
|
||||||
});
|
});
|
||||||
expect(report.code, report.output).toBe(1);
|
expect(report.code, report.output).toBe(1);
|
||||||
|
|
@ -308,13 +310,13 @@ posixIt.each([
|
||||||
);
|
);
|
||||||
expect(report.githubOutput).toBe("");
|
expect(report.githubOutput).toBe("");
|
||||||
},
|
},
|
||||||
55_000,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
posixIt.each([23, 125])(
|
posixIt.for([23, 125])(
|
||||||
"placeholder rev-parse status %s retains exact-SHA rejection",
|
"placeholder rev-parse status %s retains exact-SHA rejection",
|
||||||
async (code) => {
|
{ timeout: 55_000 },
|
||||||
const report = await releaseRun("placeholder", {
|
async (code, { signal }) => {
|
||||||
|
const report = await releaseRun(signal, "placeholder", {
|
||||||
gitFault: { match: "^rev-parse ", code },
|
gitFault: { match: "^rev-parse ", code },
|
||||||
});
|
});
|
||||||
expect(report.code, report.output).toBe(1);
|
expect(report.code, report.output).toBe(1);
|
||||||
|
|
@ -324,7 +326,6 @@ posixIt.each([23, 125])(
|
||||||
expect(gitCommands(report)).toHaveLength(1);
|
expect(gitCommands(report)).toHaveLength(1);
|
||||||
expect(report.githubOutput).toBe("");
|
expect(report.githubOutput).toBe("");
|
||||||
},
|
},
|
||||||
55_000,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const placeholderIdentityMismatches: Array<{
|
const placeholderIdentityMismatches: Array<{
|
||||||
|
|
@ -341,22 +342,22 @@ const placeholderIdentityMismatches: Array<{
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
posixIt.each(placeholderIdentityMismatches)(
|
posixIt.for(placeholderIdentityMismatches)(
|
||||||
"placeholder rejects non-Git identity mismatch before checkout inspection",
|
"placeholder rejects non-Git identity mismatch before checkout inspection",
|
||||||
async ({ env, message }) => {
|
{ timeout: 55_000 },
|
||||||
const report = await releaseRun("placeholder", { env });
|
async ({ env, message }, { signal }) => {
|
||||||
|
const report = await releaseRun(signal, "placeholder", { env });
|
||||||
expect(report.code, report.output).toBe(1);
|
expect(report.code, report.output).toBe(1);
|
||||||
expect(report.output).toContain(message);
|
expect(report.output).toContain(message);
|
||||||
expect(gitCommands(report)).toEqual([]);
|
expect(gitCommands(report)).toEqual([]);
|
||||||
expect(report.githubOutput).toBe("");
|
expect(report.githubOutput).toBe("");
|
||||||
},
|
},
|
||||||
55_000,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
posixIt(
|
posixIt(
|
||||||
"placeholder rejects a checked-out SHA mismatch before fetch",
|
"placeholder rejects a checked-out SHA mismatch before fetch",
|
||||||
async () => {
|
async ({ signal }) => {
|
||||||
const report = await releaseRun("placeholder", {
|
const report = await releaseRun(signal, "placeholder", {
|
||||||
commandResults: { "rev-parse HEAD": { code: 0, output: `${otherSha}\n` } },
|
commandResults: { "rev-parse HEAD": { code: 0, output: `${otherSha}\n` } },
|
||||||
});
|
});
|
||||||
expect(report.code, report.output).toBe(1);
|
expect(report.code, report.output).toBe(1);
|
||||||
|
|
@ -369,17 +370,17 @@ posixIt(
|
||||||
55_000,
|
55_000,
|
||||||
);
|
);
|
||||||
|
|
||||||
posixIt.each(
|
posixIt.for(
|
||||||
(["linux", "macos", "placeholder"] as const).flatMap((mode) =>
|
(["linux", "macos", "placeholder"] as const).flatMap((mode) =>
|
||||||
(["owner", "python", "git"] as const).map((setupFailure) => ({ mode, setupFailure })),
|
(["owner", "python", "git"] as const).map((setupFailure) => ({ mode, setupFailure })),
|
||||||
),
|
),
|
||||||
)(
|
)(
|
||||||
"$mode setup failure $setupFailure cannot publish or consume admission",
|
"$mode setup failure $setupFailure cannot publish or consume admission",
|
||||||
async ({ mode, setupFailure }) => {
|
{ timeout: 55_000 },
|
||||||
const report = await releaseRun(mode, { setupFailure });
|
async ({ mode, setupFailure }, { signal }) => {
|
||||||
|
const report = await releaseRun(signal, mode, { setupFailure });
|
||||||
expect(report.code, report.output).not.toBe(0);
|
expect(report.code, report.output).not.toBe(0);
|
||||||
expect(report.githubOutput).toBe("");
|
expect(report.githubOutput).toBe("");
|
||||||
expect(report.commands.some(({ tool }) => tool === "pnpm")).toBe(false);
|
expect(report.commands.some(({ tool }) => tool === "pnpm")).toBe(false);
|
||||||
},
|
},
|
||||||
55_000,
|
|
||||||
);
|
);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue