Files
openclaw/test/scripts/watch-pr-ci.test.ts
T
Peter Steinberger c70aee247e refactor(scripts): migrate JavaScript tools to TypeScript (#121005)
* refactor(scripts): migrate JavaScript tools to TypeScript

* fix(ci): keep changed-scope preflight zero-install

* fix(ci): preserve zero-install script owners

* fix(ci): complete script migration follow-through

* fix(release): keep stable closeout zero-install

* fix(scripts): preserve standalone execution boundaries

* fix(scripts): repair standalone loader boundaries

* fix(scripts): normalize gateway observation ids

* fix(scripts): keep Docker packager standalone

* test(scripts): preserve rebase cleanup helpers

* test(sessions): use tracked temp directory
2026-08-09 07:21:35 -07:00

684 lines
20 KiB
TypeScript

import { describe, expect, it } from "vitest";
import {
buildFindRunArgs,
classifyAttachedCiRun,
classifyRollup,
classifyRunAttachment,
collectRollupContexts,
parseArgs,
pollUntilDeadline,
sanitizeCheckName,
selectRunAfter,
} from "../../scripts/watch-pr-ci.mts";
const sha = "a".repeat(40);
describe("watch-pr-ci", () => {
it("parses defaults and overrides", () => {
expect(parseArgs(["42", sha])).toEqual({
pr: 42,
headSha: sha,
repo: "openclaw/openclaw",
attachTimeout: 900,
timeout: 3600,
interval: 120,
completion: "rollup",
});
expect(
parseArgs([
"7",
sha,
"--repo",
"fork/project",
"--after",
"1234",
"--attach-timeout",
"30",
"--timeout",
"90",
"--interval",
"5",
"--completion",
"ci-run",
]),
).toMatchObject({
repo: "fork/project",
after: 1234,
attachTimeout: 30,
timeout: 90,
interval: 5,
completion: "ci-run",
});
expect(parseArgs(["1", sha.toUpperCase()]).headSha).toBe(sha);
});
it("rejects malformed arguments", () => {
expect(() => parseArgs(["0", sha])).toThrow("pr-number must be a positive integer");
expect(() => parseArgs(["1", "abc"])).toThrow("full 40-character commit SHA");
expect(() => parseArgs(["1", sha, "--interval", "0"])).toThrow(
"--interval must be a positive integer",
);
expect(() => parseArgs(["1", sha, "--after", "0"])).toThrow(
"--after must be a positive integer",
);
expect(() => parseArgs(["1", sha, "--completion", "required"])).toThrow(
"--completion must be rollup or ci-run",
);
});
it("builds a pull-request-only run attachment query", () => {
expect(buildFindRunArgs("openclaw/openclaw", sha)).toEqual([
"run",
"list",
"--repo",
"openclaw/openclaw",
"--commit",
sha,
"--workflow",
"ci.yml",
"--event",
"pull_request",
"--limit",
"1",
"--json",
"createdAt,databaseId",
]);
});
it("filters run ids at and before --after", () => {
const newer = { databaseId: 102, createdAt: "2026-07-23T02:00:00Z" };
const runs = [newer, { databaseId: 101, createdAt: "2026-07-23T01:00:00Z" }];
expect(selectRunAfter(runs, 101)).toBe(newer);
expect(selectRunAfter(runs, 102)).toBeUndefined();
expect(selectRunAfter(runs)).toBe(newer);
});
it("sanitizes untrusted check names for terminal output", () => {
expect(sanitizeCheckName("plain ASCII / check (1)")).toBe("plain ASCII / check (1)");
expect(sanitizeCheckName("Crème 日本語 123")).toBe("Crème 日本語 123");
expect(sanitizeCheckName("unit\n\r\t\u0000check")).toBe("unit?check");
expect(sanitizeCheckName("safe\u001b[31mred\u001b[0m text")).toBe("safe?red? text");
expect(sanitizeCheckName("link\u001b]8;;https://example.com\u0007text\u001b]8;;\u0007")).toBe(
"link?text?",
);
expect(sanitizeCheckName("left\u202Eright 😀")).toBe("left?right ?");
});
it("sanitizes failing check and status-context names before classification output", () => {
expect(
classifyRollup({
state: "FAILURE",
contexts: {
nodes: [
{
kind: "CheckRun",
name: "unit\u001b[31mowned\u001b[0m",
status: "COMPLETED",
conclusion: "FAILURE",
},
{ kind: "StatusContext", context: "deploy\nprod", state: "ERROR" },
],
},
}).failingNames,
).toEqual(["deploy?prod", "unit?owned?"]);
});
it("polls once more after the deadline-clamped final wait", async () => {
let now = 0;
const waits: number[] = [];
let polls = 0;
const result = await pollUntilDeadline({
deadline: 1_000,
interval: 120,
now: () => now,
wait: async (milliseconds) => {
waits.push(milliseconds);
now += milliseconds;
},
poll: () => (++polls === 2 ? "transitioned" : undefined),
});
expect(result).toBe("transitioned");
expect(waits).toEqual([1_000]);
expect(polls).toBe(2);
});
it("times out only after polling at the deadline", async () => {
let now = 0;
let polls = 0;
const result = await pollUntilDeadline({
deadline: 1_000,
interval: 120,
now: () => now,
wait: async (milliseconds) => {
now += milliseconds;
},
poll: () => {
polls += 1;
return undefined;
},
});
expect(result).toBeUndefined();
expect(now).toBe(1_000);
expect(polls).toBe(2);
});
it("warns for an already-completed late attachment without changing attachment", () => {
expect(classifyRunAttachment(102, { status: "completed", conclusion: "success" })).toEqual({
attach: true,
warning:
"WARN attaching to already-completed run 102 (started before watcher); pass --after 102 to require a fresh run",
});
expect(classifyRunAttachment(102, { status: "completed", conclusion: "success" }, 101)).toEqual(
{ attach: true, warning: undefined },
);
expect(classifyRunAttachment(102, { status: "completed", conclusion: "skipped" })).toEqual({
attach: false,
});
});
it("requires aggregate success for a green rollup", () => {
expect(classifyRollup({ state: "SUCCESS", contexts: { nodes: [] } }).verdict).toBe("GREEN");
expect(
classifyRollup({
state: "PENDING",
contexts: {
nodes: [{ kind: "CheckRun", name: "unit", status: "COMPLETED", conclusion: "SUCCESS" }],
},
}),
).toEqual({ verdict: "PENDING", pendingCount: 0, failingNames: [], supersededCount: 0 });
});
it("counts pending contexts without deriving the verdict from them", () => {
expect(
classifyRollup({
state: "PENDING",
contexts: {
nodes: [{ kind: "CheckRun", name: "unit", status: "IN_PROGRESS", conclusion: null }],
},
}),
).toEqual({ verdict: "PENDING", pendingCount: 1, failingNames: [], supersededCount: 0 });
});
it("lets an attached successful CI run finish while an optional context remains pending", () => {
expect(
classifyRollup({
state: "PENDING",
contexts: {
nodes: [
{ kind: "CheckRun", name: "optional proof", status: "IN_PROGRESS", conclusion: null },
],
},
}).verdict,
).toBe("PENDING");
expect(classifyAttachedCiRun({ status: "completed", conclusion: "success" })).toEqual({
verdict: "GREEN",
});
});
it.each(["FAILURE", "ERROR"])(
"keeps identity-less same-name cancellations failing for aggregate %s",
(state) => {
expect(
classifyRollup({
state,
contexts: {
totalCount: 3,
nodes: [
{
kind: "CheckRun",
name: "Auto response",
status: "COMPLETED",
conclusion: "FAILURE",
},
{ kind: "CheckRun", name: "unit", status: "COMPLETED", conclusion: "CANCELLED" },
{ kind: "CheckRun", name: "unit", status: "COMPLETED", conclusion: "SUCCESS" },
],
},
}),
).toEqual({
verdict: "FAILING",
pendingCount: 0,
failingNames: ["unit"],
supersededCount: 0,
});
},
);
it("keeps a truncated failing rollup failing", () => {
expect(
classifyRollup({
state: "FAILURE",
contexts: {
totalCount: 4,
nodes: [
{ kind: "CheckRun", name: "unit", status: "COMPLETED", conclusion: "CANCELLED" },
{ kind: "CheckRun", name: "unit", status: "COMPLETED", conclusion: "SUCCESS" },
],
},
}),
).toEqual({
verdict: "FAILING",
pendingCount: 0,
failingNames: ["unit", "+2 more contexts not shown"],
supersededCount: 0,
});
});
it("keeps cancelled attempts in failing-name output", () => {
expect(
classifyRollup({
state: "FAILURE",
contexts: {
nodes: [
{ kind: "CheckRun", name: "Auto response", status: "COMPLETED", conclusion: "FAILURE" },
{ kind: "CheckRun", name: "unit", status: "COMPLETED", conclusion: "CANCELLED" },
{ kind: "CheckRun", name: "unit", status: "COMPLETED", conclusion: "SUCCESS" },
{ kind: "CheckRun", name: "lint", status: "COMPLETED", conclusion: "TIMED_OUT" },
],
},
}),
).toEqual({
verdict: "FAILING",
pendingCount: 0,
failingNames: ["lint", "unit"],
supersededCount: 0,
});
});
it("ignores superseded workflow runs while replacements are in progress", () => {
expect(
classifyRollup({
state: "FAILURE",
contexts: {
nodes: [
{
kind: "CheckRun",
name: "Real behavior proof",
status: "COMPLETED",
conclusion: "CANCELLED",
checkSuite: {
workflowRun: { databaseId: 100, workflow: { databaseId: 10 } },
},
},
{
kind: "CheckRun",
name: "Real behavior proof",
status: "IN_PROGRESS",
conclusion: null,
checkSuite: {
workflowRun: { databaseId: 200, workflow: { databaseId: 10 } },
},
},
{
kind: "CheckRun",
name: "CI",
status: "IN_PROGRESS",
conclusion: null,
checkSuite: { workflowRun: { databaseId: 150, workflow: { databaseId: 20 } } },
},
],
},
}),
).toEqual({ verdict: "PENDING", pendingCount: 2, failingNames: [], supersededCount: 1 });
});
it("keeps only the newest same-run check attempt while its replacement is pending", () => {
expect(
classifyRollup({
state: "FAILURE",
contexts: {
nodes: [
{
kind: "CheckRun",
databaseId: 1_000,
name: "unit",
status: "COMPLETED",
conclusion: "CANCELLED",
checkSuite: { workflowRun: { databaseId: 500, workflow: { databaseId: 20 } } },
},
{
kind: "CheckRun",
databaseId: 2_000,
name: "unit",
status: "IN_PROGRESS",
conclusion: null,
checkSuite: { workflowRun: { databaseId: 500, workflow: { databaseId: 20 } } },
},
],
},
}),
).toEqual({ verdict: "PENDING", pendingCount: 1, failingNames: [], supersededCount: 1 });
});
it("accepts a successful newest check attempt when the aggregate remains failed", () => {
expect(
classifyRollup({
state: "FAILURE",
contexts: {
nodes: [
{
kind: "CheckRun",
databaseId: 1_000,
name: "unit",
status: "COMPLETED",
conclusion: "CANCELLED",
checkSuite: { workflowRun: { databaseId: 500, workflow: { databaseId: 20 } } },
},
{
kind: "CheckRun",
databaseId: 2_000,
name: "unit",
status: "COMPLETED",
conclusion: "SUCCESS",
checkSuite: { workflowRun: { databaseId: 500, workflow: { databaseId: 20 } } },
},
],
},
}),
).toEqual({ verdict: "GREEN", pendingCount: 0, failingNames: [], supersededCount: 1 });
});
it("accepts newest successful runs when the aggregate remains failed", () => {
expect(
classifyRollup({
state: "FAILURE",
contexts: {
nodes: [
{
kind: "CheckRun",
name: "old proof",
status: "COMPLETED",
conclusion: "CANCELLED",
checkSuite: {
workflowRun: { databaseId: 100, workflow: { databaseId: 10 } },
},
},
{
kind: "CheckRun",
name: "proof",
status: "COMPLETED",
conclusion: "SUCCESS",
checkSuite: {
workflowRun: { databaseId: 200, workflow: { databaseId: 10 } },
},
},
{
kind: "CheckRun",
name: "old CI",
status: "COMPLETED",
conclusion: "CANCELLED",
checkSuite: { workflowRun: { databaseId: 150, workflow: { databaseId: 20 } } },
},
{
kind: "CheckRun",
name: "CI",
status: "COMPLETED",
conclusion: "SUCCESS",
checkSuite: { workflowRun: { databaseId: 250, workflow: { databaseId: 20 } } },
},
],
},
}),
).toEqual({ verdict: "GREEN", pendingCount: 0, failingNames: [], supersededCount: 2 });
});
it("preserves a genuine failure from the newest workflow run", () => {
expect(
classifyRollup({
state: "FAILURE",
contexts: {
nodes: [
{
kind: "CheckRun",
name: "superseded cancellation",
status: "COMPLETED",
conclusion: "CANCELLED",
checkSuite: { workflowRun: { databaseId: 100, workflow: { databaseId: 20 } } },
},
{
kind: "CheckRun",
name: "unit",
status: "COMPLETED",
conclusion: "FAILURE",
checkSuite: { workflowRun: { databaseId: 200, workflow: { databaseId: 20 } } },
},
],
},
}),
).toEqual({
verdict: "FAILING",
pendingCount: 0,
failingNames: ["unit"],
supersededCount: 1,
});
});
it("preserves failures across interleaved distinct workflow identities", () => {
expect(
classifyRollup({
state: "FAILURE",
contexts: {
nodes: [
{
kind: "CheckRun",
name: "old deploy",
status: "COMPLETED",
conclusion: "CANCELLED",
checkSuite: { workflowRun: { databaseId: 200, workflow: { databaseId: 20 } } },
},
{
kind: "CheckRun",
name: "unit",
status: "COMPLETED",
conclusion: "FAILURE",
checkSuite: { workflowRun: { databaseId: 300, workflow: { databaseId: 10 } } },
},
{
kind: "CheckRun",
name: "deploy",
status: "COMPLETED",
conclusion: "SUCCESS",
checkSuite: { workflowRun: { databaseId: 400, workflow: { databaseId: 20 } } },
},
],
},
}),
).toEqual({
verdict: "FAILING",
pendingCount: 0,
failingNames: ["unit"],
supersededCount: 1,
});
});
it("keeps an older run's unique failing job visible", () => {
expect(
classifyRollup({
state: "FAILURE",
contexts: {
nodes: [
{
kind: "CheckRun",
databaseId: 1_000,
name: "nightly-special",
status: "COMPLETED",
conclusion: "FAILURE",
checkSuite: { workflowRun: { databaseId: 300, workflow: { databaseId: 20 } } },
},
{
kind: "CheckRun",
databaseId: 2_000,
name: "unit",
status: "COMPLETED",
conclusion: "SUCCESS",
checkSuite: { workflowRun: { databaseId: 400, workflow: { databaseId: 20 } } },
},
],
},
}),
).toEqual({
verdict: "FAILING",
pendingCount: 0,
failingNames: ["nightly-special"],
supersededCount: 0,
});
});
it("supersedes same-name checks across runs of the same workflow", () => {
expect(
classifyRollup({
state: "FAILURE",
contexts: {
nodes: [
{
kind: "CheckRun",
databaseId: 1_000,
name: "unit",
status: "COMPLETED",
conclusion: "FAILURE",
checkSuite: { workflowRun: { databaseId: 300, workflow: { databaseId: 20 } } },
},
{
kind: "CheckRun",
databaseId: 2_000,
name: "unit",
status: "COMPLETED",
conclusion: "SUCCESS",
checkSuite: { workflowRun: { databaseId: 400, workflow: { databaseId: 20 } } },
},
],
},
}),
).toEqual({ verdict: "GREEN", pendingCount: 0, failingNames: [], supersededCount: 1 });
});
it("fails conservatively when unseen contexts may explain aggregate failure", () => {
expect(
classifyRollup({
state: "FAILURE",
contexts: {
totalCount: 3,
nodes: [
{
kind: "CheckRun",
name: "old CI",
status: "COMPLETED",
conclusion: "CANCELLED",
checkSuite: { workflowRun: { databaseId: 100, workflow: { databaseId: 20 } } },
},
{
kind: "CheckRun",
name: "CI",
status: "COMPLETED",
conclusion: "SUCCESS",
checkSuite: { workflowRun: { databaseId: 200, workflow: { databaseId: 20 } } },
},
],
},
}),
).toEqual({
verdict: "FAILING",
pendingCount: 0,
failingNames: ["status rollup", "+1 more contexts not shown"],
supersededCount: 1,
});
});
it("collects rollup contexts across pages", () => {
const cursors: Array<string | null> = [];
const result = collectRollupContexts((cursor) => {
cursors.push(cursor);
if (cursor === null) {
return {
statusCheckRollup: {
state: "PENDING",
contexts: {
totalCount: 2,
nodes: [{ kind: "CheckRun", name: "first" }],
pageInfo: { hasNextPage: true, endCursor: "next" },
},
},
};
}
return {
statusCheckRollup: {
state: "PENDING",
contexts: {
totalCount: 2,
nodes: [{ kind: "CheckRun", name: "second" }],
pageInfo: { hasNextPage: false, endCursor: null },
},
},
};
});
expect(cursors).toEqual([null, "next"]);
expect(result?.statusCheckRollup?.contexts?.totalCount).toBe(2);
expect(result?.statusCheckRollup?.contexts?.nodes?.map((node) => node.name)).toEqual([
"first",
"second",
]);
});
it("rejects rollup pages from a changed snapshot", () => {
expect(() =>
collectRollupContexts((cursor) => ({
headRefOid: "a".repeat(40),
statusCheckRollup: {
state: "PENDING",
contexts: {
totalCount: cursor === null ? 2 : 3,
nodes: [{ kind: "CheckRun", name: cursor === null ? "first" : "second" }],
pageInfo:
cursor === null
? { hasNextPage: true, endCursor: "next" }
: { hasNextPage: false, endCursor: null },
},
},
})),
).toThrow("rollup snapshot changed during pagination");
});
it("rejects a pagination read that loses an advertised page", () => {
expect(() =>
collectRollupContexts((cursor) =>
cursor === null
? {
headRefOid: "a".repeat(40),
statusCheckRollup: {
state: "SUCCESS",
contexts: {
totalCount: 2,
nodes: [{ kind: "CheckRun", name: "first" }],
pageInfo: { hasNextPage: true, endCursor: "next" },
},
},
}
: { headRefOid: "b".repeat(40), statusCheckRollup: null },
),
).toThrow("rollup snapshot changed during pagination");
});
it("caps rollup context collection at ten pages", () => {
let calls = 0;
const result = collectRollupContexts(() => {
calls += 1;
return {
statusCheckRollup: {
state: "FAILURE",
contexts: {
totalCount: 11,
nodes: [{ kind: "CheckRun", name: `page-${calls}` }],
pageInfo: { hasNextPage: true, endCursor: `cursor-${calls}` },
},
},
};
});
expect(calls).toBe(10);
expect(result?.statusCheckRollup?.contexts?.nodes).toHaveLength(10);
});
});