mirror of
https://github.com/nexu-io/open-design.git
synced 2026-05-31 19:04:39 +07:00
* fix: run fork visual reports from trusted code * fix: auto-approve strict web visual capture * fix: address visual report review feedback Generated-By: looper 0.9.1 (runner=fixer, agent=opencode) * fix: propagate visual report storage failures Generated-By: looper 0.9.1 (runner=fixer, agent=opencode) * fix: validate PR screenshots before upload Generated-By: looper 0.9.1 (runner=fixer, agent=opencode) * fix: validate visual PR identity before comment * fix: harden fork visual report validation Generated-By: looper 0.9.1 (runner=fixer, agent=opencode) * fix: address remaining fork visual report review feedback Generated-By: looper 0.9.1 (runner=fixer, agent=opencode) * fix: handle stale fork visual report lookup Generated-By: looper 0.9.1 (runner=fixer, agent=opencode) * fix: allow stale fork visual report fallback Generated-By: looper 0.9.1 (runner=fixer, agent=opencode)
1024 lines
30 KiB
TypeScript
1024 lines
30 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import { readFile } from "node:fs/promises";
|
|
import test from "node:test";
|
|
|
|
import {
|
|
hasPullApprovalStateDrift,
|
|
isAllowedChangedPath,
|
|
isAllowedVisualCaptureChangedPath,
|
|
isDeniedChangedPath,
|
|
isPendingApprovalRun,
|
|
listPendingApprovalRuns,
|
|
runTargetsPullRequest,
|
|
waitForPendingApprovalRuns,
|
|
} from "./approve-fork-pr-workflows.ts";
|
|
|
|
test("visual-pr-comment resolves empty workflow_run.pull_requests from trusted repo/branch metadata and leaves stale SHA handling to trusted-pr", async () => {
|
|
const workflow = await readFile(new URL("../.github/workflows/visual-pr-comment.yml", import.meta.url), "utf8");
|
|
const manifestStep = workflow.match(/- name: Read capture manifest[\s\S]*?- name: Validate live PR state for trusted checkout/u)?.[0];
|
|
const trustedPrStep = workflow.match(/- name: Validate live PR state for trusted checkout[\s\S]*?- name: Checkout trusted base/u)?.[0];
|
|
|
|
assert.ok(manifestStep, "Expected Read capture manifest step block in workflow");
|
|
assert.ok(trustedPrStep, "Expected trusted-pr validation step block in workflow");
|
|
assert.match(manifestStep, /encoded_head=.*\$head\|@uri/u);
|
|
assert.match(manifestStep, /pulls\?state=open&head=\$encoded_head&per_page=100/);
|
|
assert.match(manifestStep, /jq -r --arg repo "\$source_head_repository"/u);
|
|
assert.doesNotMatch(manifestStep, /select\(\.head\.sha == \$sha/u);
|
|
assert.match(manifestStep, /match_count=.*wc -w/u);
|
|
assert.match(manifestStep, /if \[ "\$match_count" -gt 1 \]; then/u);
|
|
assert.match(manifestStep, /found \$match_count matches/);
|
|
assert.match(manifestStep, /pr_number="\$manifest_pr_number"/u);
|
|
assert.match(trustedPrStep, /Skipping stale visual artifact for \$ARTIFACT_HEAD_SHA; current PR head is \$current_head\./u);
|
|
});
|
|
|
|
test("isPendingApprovalRun matches approval-gated fork PR runs from GitHub's captured payload shape", () => {
|
|
const pull = {
|
|
number: 2683,
|
|
state: "open",
|
|
changed_files: 1,
|
|
head: {
|
|
ref: "fix/workflow-linkage",
|
|
sha: "734076155c44e569304856590019cea54506fdab",
|
|
repo: { full_name: "someone/open-design" },
|
|
},
|
|
base: {
|
|
ref: "main",
|
|
sha: "4cd93a5c7a7b0db1961c854e55f8e0e6b1b45542",
|
|
repo: { full_name: "nexu-io/open-design" },
|
|
},
|
|
};
|
|
|
|
const run = {
|
|
id: 26273463769,
|
|
name: "CI",
|
|
event: "pull_request",
|
|
status: "completed",
|
|
conclusion: "action_required",
|
|
head_sha: "734076155c44e569304856590019cea54506fdab",
|
|
path: ".github/workflows/ci.yml@main",
|
|
pull_requests: [],
|
|
};
|
|
|
|
assert.equal(isPendingApprovalRun(run, pull), true);
|
|
});
|
|
|
|
test("isPendingApprovalRun also accepts action_required runs reported only in status", () => {
|
|
const pull = {
|
|
number: 2683,
|
|
state: "open",
|
|
changed_files: 1,
|
|
head: {
|
|
ref: "fix/workflow-linkage",
|
|
sha: "734076155c44e569304856590019cea54506fdab",
|
|
repo: { full_name: "someone/open-design" },
|
|
},
|
|
base: {
|
|
ref: "main",
|
|
sha: "4cd93a5c7a7b0db1961c854e55f8e0e6b1b45542",
|
|
repo: { full_name: "nexu-io/open-design" },
|
|
},
|
|
};
|
|
|
|
const run = {
|
|
id: 26273463770,
|
|
name: "CI",
|
|
event: "pull_request",
|
|
status: "action_required",
|
|
conclusion: null,
|
|
head_sha: "734076155c44e569304856590019cea54506fdab",
|
|
path: ".github/workflows/ci.yml@main",
|
|
pull_requests: [],
|
|
};
|
|
|
|
assert.equal(isPendingApprovalRun(run, pull), true);
|
|
});
|
|
|
|
test("isPendingApprovalRun rejects runs outside the allowlist or without action_required state", () => {
|
|
const pull = {
|
|
number: 2683,
|
|
state: "open",
|
|
changed_files: 1,
|
|
head: {
|
|
ref: "fix/workflow-linkage",
|
|
sha: "734076155c44e569304856590019cea54506fdab",
|
|
repo: { full_name: "someone/open-design" },
|
|
},
|
|
base: {
|
|
ref: "main",
|
|
sha: "4cd93a5c7a7b0db1961c854e55f8e0e6b1b45542",
|
|
repo: { full_name: "nexu-io/open-design" },
|
|
},
|
|
};
|
|
|
|
assert.equal(
|
|
isPendingApprovalRun(
|
|
{
|
|
id: 26273463769,
|
|
name: "CI",
|
|
event: "pull_request",
|
|
status: "completed",
|
|
conclusion: "success",
|
|
head_sha: "734076155c44e569304856590019cea54506fdab",
|
|
path: ".github/workflows/ci.yml@main",
|
|
pull_requests: [],
|
|
},
|
|
pull,
|
|
),
|
|
false,
|
|
);
|
|
|
|
assert.equal(
|
|
isPendingApprovalRun(
|
|
{
|
|
id: 26273463771,
|
|
name: "Visual PR Capture",
|
|
event: "pull_request",
|
|
status: "completed",
|
|
conclusion: "action_required",
|
|
head_sha: "734076155c44e569304856590019cea54506fdab",
|
|
path: ".github/workflows/visual-pr-capture.yml@main",
|
|
pull_requests: [],
|
|
},
|
|
pull,
|
|
),
|
|
false,
|
|
);
|
|
|
|
assert.equal(
|
|
isPendingApprovalRun(
|
|
{
|
|
id: 26273463770,
|
|
name: "Visual PR Comment",
|
|
event: "pull_request",
|
|
status: "completed",
|
|
conclusion: "action_required",
|
|
head_sha: "734076155c44e569304856590019cea54506fdab",
|
|
path: ".github/workflows/visual-pr-comment.yml@main",
|
|
pull_requests: [],
|
|
},
|
|
pull,
|
|
),
|
|
false,
|
|
);
|
|
});
|
|
|
|
test("isPendingApprovalRun approves visual capture only for strict web source changes", () => {
|
|
const pull = {
|
|
number: 2683,
|
|
state: "open",
|
|
changed_files: 1,
|
|
head: {
|
|
ref: "fix/button-copy",
|
|
sha: "734076155c44e569304856590019cea54506fdab",
|
|
repo: { full_name: "someone/open-design" },
|
|
},
|
|
base: {
|
|
ref: "main",
|
|
sha: "4cd93a5c7a7b0db1961c854e55f8e0e6b1b45542",
|
|
repo: { full_name: "nexu-io/open-design" },
|
|
},
|
|
};
|
|
const run = {
|
|
id: 26273463771,
|
|
name: "Visual PR Capture",
|
|
event: "pull_request",
|
|
status: "completed",
|
|
conclusion: "action_required",
|
|
head_sha: "734076155c44e569304856590019cea54506fdab",
|
|
path: ".github/workflows/visual-pr-capture.yml@main",
|
|
pull_requests: [],
|
|
};
|
|
|
|
assert.equal(
|
|
isPendingApprovalRun(run, pull, [
|
|
{ filename: "apps/web/src/components/Button.tsx", status: "modified" },
|
|
{ filename: "apps/web/src/styles/button.css", status: "modified" },
|
|
]),
|
|
true,
|
|
);
|
|
assert.equal(
|
|
isPendingApprovalRun(run, pull, [{ filename: "apps/web/public/logo.png", status: "modified" }]),
|
|
false,
|
|
);
|
|
assert.equal(
|
|
isPendingApprovalRun(run, pull, [
|
|
{
|
|
filename: "apps/web/src/components/Button.tsx",
|
|
previous_filename: "scripts/build.ts",
|
|
status: "renamed",
|
|
},
|
|
]),
|
|
false,
|
|
);
|
|
});
|
|
|
|
test("runTargetsPullRequest accepts empty run.pull_requests only when the head SHA maps to this one open PR", () => {
|
|
const pull = {
|
|
number: 2683,
|
|
state: "open",
|
|
changed_files: 1,
|
|
head: {
|
|
ref: "fix/workflow-linkage",
|
|
sha: "734076155c44e569304856590019cea54506fdab",
|
|
repo: { full_name: "someone/open-design" },
|
|
},
|
|
base: {
|
|
ref: "main",
|
|
sha: "4cd93a5c7a7b0db1961c854e55f8e0e6b1b45542",
|
|
repo: { full_name: "nexu-io/open-design" },
|
|
},
|
|
};
|
|
|
|
const run = {
|
|
id: 26273463769,
|
|
name: "CI",
|
|
event: "pull_request",
|
|
status: "completed",
|
|
conclusion: "action_required",
|
|
head_sha: "734076155c44e569304856590019cea54506fdab",
|
|
path: ".github/workflows/ci.yml@main",
|
|
pull_requests: [],
|
|
};
|
|
|
|
assert.equal(runTargetsPullRequest(run, pull, [pull], []), true);
|
|
});
|
|
|
|
test("runTargetsPullRequest accepts fork PR runs with no GitHub PR association when head identity matches", () => {
|
|
const pull = {
|
|
number: 2683,
|
|
state: "open",
|
|
changed_files: 1,
|
|
head: {
|
|
ref: "fix/workflow-linkage",
|
|
sha: "734076155c44e569304856590019cea54506fdab",
|
|
repo: { full_name: "someone/open-design" },
|
|
},
|
|
base: {
|
|
ref: "main",
|
|
sha: "4cd93a5c7a7b0db1961c854e55f8e0e6b1b45542",
|
|
repo: { full_name: "nexu-io/open-design" },
|
|
},
|
|
};
|
|
|
|
const run = {
|
|
id: 26273463769,
|
|
name: "CI",
|
|
event: "pull_request",
|
|
head_branch: "fix/workflow-linkage",
|
|
head_repository: { full_name: "someone/open-design" },
|
|
status: "completed",
|
|
conclusion: "action_required",
|
|
head_sha: pull.head.sha,
|
|
path: ".github/workflows/ci.yml@main",
|
|
pull_requests: [],
|
|
};
|
|
|
|
assert.equal(runTargetsPullRequest(run, pull, [], [pull]), true);
|
|
});
|
|
|
|
test("runTargetsPullRequest rejects ambiguous empty run.pull_requests associations", () => {
|
|
const pull = {
|
|
number: 2683,
|
|
state: "open",
|
|
changed_files: 1,
|
|
head: {
|
|
ref: "fix/workflow-linkage",
|
|
sha: "734076155c44e569304856590019cea54506fdab",
|
|
repo: { full_name: "someone/open-design" },
|
|
},
|
|
base: {
|
|
ref: "main",
|
|
sha: "4cd93a5c7a7b0db1961c854e55f8e0e6b1b45542",
|
|
repo: { full_name: "nexu-io/open-design" },
|
|
},
|
|
};
|
|
|
|
const otherPull = {
|
|
...pull,
|
|
number: 3001,
|
|
base: {
|
|
ref: "release",
|
|
sha: "8db117d728f967d108f6fdd64cb8d921d057f7f6",
|
|
repo: { full_name: "nexu-io/open-design" },
|
|
},
|
|
};
|
|
|
|
const run = {
|
|
id: 26273463769,
|
|
name: "CI",
|
|
event: "pull_request",
|
|
status: "completed",
|
|
conclusion: "action_required",
|
|
head_sha: "734076155c44e569304856590019cea54506fdab",
|
|
path: ".github/workflows/ci.yml@main",
|
|
pull_requests: [],
|
|
};
|
|
|
|
assert.equal(runTargetsPullRequest(run, pull, [pull, otherPull], []), false);
|
|
});
|
|
|
|
test("runTargetsPullRequest rejects fork PR runs when multiple open PRs share the same head identity", () => {
|
|
const pull = {
|
|
number: 2683,
|
|
state: "open",
|
|
changed_files: 1,
|
|
head: {
|
|
ref: "fix/workflow-linkage",
|
|
sha: "734076155c44e569304856590019cea54506fdab",
|
|
repo: { full_name: "someone/open-design" },
|
|
},
|
|
base: {
|
|
ref: "main",
|
|
sha: "4cd93a5c7a7b0db1961c854e55f8e0e6b1b45542",
|
|
repo: { full_name: "nexu-io/open-design" },
|
|
},
|
|
};
|
|
|
|
const otherPull = {
|
|
...pull,
|
|
number: 3001,
|
|
base: {
|
|
ref: "release",
|
|
sha: "8db117d728f967d108f6fdd64cb8d921d057f7f6",
|
|
repo: { full_name: "nexu-io/open-design" },
|
|
},
|
|
};
|
|
|
|
const run = {
|
|
id: 26273463769,
|
|
name: "CI",
|
|
event: "pull_request",
|
|
head_branch: pull.head.ref,
|
|
head_repository: pull.head.repo,
|
|
status: "completed",
|
|
conclusion: "action_required",
|
|
head_sha: pull.head.sha,
|
|
path: ".github/workflows/ci.yml@main",
|
|
pull_requests: [],
|
|
};
|
|
|
|
assert.equal(runTargetsPullRequest(run, pull, [], [pull, otherPull]), false);
|
|
});
|
|
|
|
test("runTargetsPullRequest rejects empty associations when fork head identity does not match", () => {
|
|
const pull = {
|
|
number: 2683,
|
|
state: "open",
|
|
changed_files: 1,
|
|
head: {
|
|
ref: "fix/workflow-linkage",
|
|
sha: "734076155c44e569304856590019cea54506fdab",
|
|
repo: { full_name: "someone/open-design" },
|
|
},
|
|
base: {
|
|
ref: "main",
|
|
sha: "4cd93a5c7a7b0db1961c854e55f8e0e6b1b45542",
|
|
repo: { full_name: "nexu-io/open-design" },
|
|
},
|
|
};
|
|
|
|
const run = {
|
|
id: 26273463769,
|
|
name: "CI",
|
|
event: "pull_request",
|
|
head_branch: "different-branch",
|
|
head_repository: { full_name: "someone/open-design" },
|
|
status: "completed",
|
|
conclusion: "action_required",
|
|
head_sha: pull.head.sha,
|
|
path: ".github/workflows/ci.yml@main",
|
|
pull_requests: [],
|
|
};
|
|
|
|
assert.equal(runTargetsPullRequest(run, pull, [], [pull]), false);
|
|
});
|
|
|
|
test("runTargetsPullRequest rejects runs that GitHub already associates to a different PR", () => {
|
|
const pull = {
|
|
number: 2683,
|
|
state: "open",
|
|
changed_files: 1,
|
|
head: {
|
|
ref: "fix/workflow-linkage",
|
|
sha: "734076155c44e569304856590019cea54506fdab",
|
|
repo: { full_name: "someone/open-design" },
|
|
},
|
|
base: {
|
|
ref: "main",
|
|
sha: "4cd93a5c7a7b0db1961c854e55f8e0e6b1b45542",
|
|
repo: { full_name: "nexu-io/open-design" },
|
|
},
|
|
};
|
|
|
|
const run = {
|
|
id: 26273463769,
|
|
name: "CI",
|
|
event: "pull_request",
|
|
status: "completed",
|
|
conclusion: "action_required",
|
|
head_sha: "734076155c44e569304856590019cea54506fdab",
|
|
path: ".github/workflows/ci.yml@main",
|
|
pull_requests: [
|
|
{
|
|
number: 3001,
|
|
head: pull.head,
|
|
base: {
|
|
ref: "release",
|
|
sha: "8db117d728f967d108f6fdd64cb8d921d057f7f6",
|
|
repo: { full_name: "nexu-io/open-design" },
|
|
},
|
|
},
|
|
],
|
|
};
|
|
|
|
assert.equal(runTargetsPullRequest(run, pull, [pull], []), false);
|
|
});
|
|
|
|
test("runTargetsPullRequest approves only the run that GitHub associates to the current PR when two PRs share a head SHA", () => {
|
|
const pull = {
|
|
number: 2683,
|
|
state: "open",
|
|
changed_files: 1,
|
|
head: {
|
|
ref: "fix/workflow-linkage",
|
|
sha: "734076155c44e569304856590019cea54506fdab",
|
|
repo: { full_name: "someone/open-design" },
|
|
},
|
|
base: {
|
|
ref: "main",
|
|
sha: "4cd93a5c7a7b0db1961c854e55f8e0e6b1b45542",
|
|
repo: { full_name: "nexu-io/open-design" },
|
|
},
|
|
};
|
|
|
|
const otherPull = {
|
|
...pull,
|
|
number: 3001,
|
|
base: {
|
|
ref: "release",
|
|
sha: "8db117d728f967d108f6fdd64cb8d921d057f7f6",
|
|
repo: { full_name: "nexu-io/open-design" },
|
|
},
|
|
};
|
|
|
|
const currentPrRun = {
|
|
id: 26273463769,
|
|
name: "CI",
|
|
event: "pull_request",
|
|
status: "completed",
|
|
conclusion: "action_required",
|
|
head_sha: pull.head.sha,
|
|
path: ".github/workflows/ci.yml@main",
|
|
pull_requests: [pull],
|
|
};
|
|
|
|
const otherPrRun = {
|
|
...currentPrRun,
|
|
id: 26273463770,
|
|
pull_requests: [otherPull],
|
|
};
|
|
|
|
assert.equal(runTargetsPullRequest(currentPrRun, pull, [pull, otherPull], []), true);
|
|
assert.equal(runTargetsPullRequest(otherPrRun, pull, [pull, otherPull], []), false);
|
|
});
|
|
|
|
test("runTargetsPullRequest ignores base tip churn for the same PR association", () => {
|
|
const pull = {
|
|
number: 2683,
|
|
state: "open",
|
|
changed_files: 1,
|
|
head: {
|
|
ref: "fix/workflow-linkage",
|
|
sha: "734076155c44e569304856590019cea54506fdab",
|
|
repo: { full_name: "someone/open-design" },
|
|
},
|
|
base: {
|
|
ref: "main",
|
|
sha: "4cd93a5c7a7b0db1961c854e55f8e0e6b1b45542",
|
|
repo: { full_name: "nexu-io/open-design" },
|
|
},
|
|
};
|
|
|
|
const run = {
|
|
id: 26273463769,
|
|
name: "CI",
|
|
event: "pull_request",
|
|
status: "completed",
|
|
conclusion: "action_required",
|
|
head_sha: pull.head.sha,
|
|
path: ".github/workflows/ci.yml@main",
|
|
pull_requests: [
|
|
{
|
|
number: pull.number,
|
|
head: pull.head,
|
|
base: {
|
|
...pull.base,
|
|
sha: "08a88a65482123629ebda5a090c71533bd6b8a88",
|
|
},
|
|
},
|
|
],
|
|
};
|
|
|
|
assert.equal(runTargetsPullRequest(run, pull, [pull], []), true);
|
|
});
|
|
|
|
test("listPendingApprovalRuns paginates all pull_request runs for the head SHA and filters action_required client-side", async () => {
|
|
const pull = {
|
|
number: 2683,
|
|
state: "open",
|
|
changed_files: 1,
|
|
head: {
|
|
ref: "fix/workflow-linkage",
|
|
sha: "734076155c44e569304856590019cea54506fdab",
|
|
repo: { full_name: "someone/open-design" },
|
|
},
|
|
base: {
|
|
ref: "main",
|
|
sha: "4cd93a5c7a7b0db1961c854e55f8e0e6b1b45542",
|
|
repo: { full_name: "nexu-io/open-design" },
|
|
},
|
|
};
|
|
|
|
const requestedPaths: string[] = [];
|
|
const pendingRuns = await listPendingApprovalRuns("nexu-io/open-design", pull, {
|
|
loadWorkflowRunsResponsePage: async (path) => {
|
|
requestedPaths.push(path);
|
|
if (path.endsWith("page=1")) {
|
|
return {
|
|
workflow_runs: Array.from({ length: 100 }, (_, index) => ({
|
|
id: 26273463600 + index,
|
|
name: "CI",
|
|
event: "pull_request",
|
|
head_branch: pull.head.ref,
|
|
head_repository: pull.head.repo,
|
|
status: "completed",
|
|
conclusion: "success",
|
|
head_sha: pull.head.sha,
|
|
path: ".github/workflows/ci.yml@main",
|
|
pull_requests: [],
|
|
})),
|
|
};
|
|
}
|
|
|
|
return {
|
|
workflow_runs: [
|
|
{
|
|
id: 26273463769,
|
|
name: "CI",
|
|
event: "pull_request",
|
|
head_branch: pull.head.ref,
|
|
head_repository: pull.head.repo,
|
|
status: "completed",
|
|
conclusion: "action_required",
|
|
head_sha: pull.head.sha,
|
|
path: ".github/workflows/ci.yml@main",
|
|
pull_requests: [],
|
|
},
|
|
{
|
|
id: 26273463770,
|
|
name: "CI",
|
|
event: "pull_request",
|
|
head_branch: pull.head.ref,
|
|
head_repository: pull.head.repo,
|
|
status: "completed",
|
|
conclusion: "success",
|
|
head_sha: pull.head.sha,
|
|
path: ".github/workflows/ci.yml@main",
|
|
pull_requests: [],
|
|
},
|
|
],
|
|
};
|
|
},
|
|
loadPullRequestsForHeadSha: async () => [],
|
|
loadPullRequestsForHeadRef: async () => [pull],
|
|
});
|
|
|
|
assert.deepEqual(requestedPaths, [
|
|
"/repos/nexu-io/open-design/actions/runs?event=pull_request&head_sha=734076155c44e569304856590019cea54506fdab&per_page=100&page=1",
|
|
"/repos/nexu-io/open-design/actions/runs?event=pull_request&head_sha=734076155c44e569304856590019cea54506fdab&per_page=100&page=2",
|
|
]);
|
|
assert.equal(requestedPaths.some((path) => path.includes("status=action_required")), false);
|
|
assert.deepEqual(
|
|
pendingRuns.map((run) => run.id),
|
|
[26273463769],
|
|
);
|
|
});
|
|
|
|
test("listPendingApprovalRuns applies strict changed-path filtering only to visual capture", async () => {
|
|
const pull = {
|
|
number: 2683,
|
|
state: "open",
|
|
changed_files: 1,
|
|
head: {
|
|
ref: "fix/readme-copy",
|
|
sha: "734076155c44e569304856590019cea54506fdab",
|
|
repo: { full_name: "someone/open-design" },
|
|
},
|
|
base: {
|
|
ref: "main",
|
|
sha: "4cd93a5c7a7b0db1961c854e55f8e0e6b1b45542",
|
|
repo: { full_name: "nexu-io/open-design" },
|
|
},
|
|
};
|
|
const workflowRuns = [
|
|
{
|
|
id: 26273463769,
|
|
name: "CI",
|
|
event: "pull_request",
|
|
head_branch: pull.head.ref,
|
|
head_repository: pull.head.repo,
|
|
status: "completed",
|
|
conclusion: "action_required",
|
|
head_sha: pull.head.sha,
|
|
path: ".github/workflows/ci.yml@main",
|
|
pull_requests: [],
|
|
},
|
|
{
|
|
id: 26273463770,
|
|
name: "Visual PR Capture",
|
|
event: "pull_request",
|
|
head_branch: pull.head.ref,
|
|
head_repository: pull.head.repo,
|
|
status: "completed",
|
|
conclusion: "action_required",
|
|
head_sha: pull.head.sha,
|
|
path: ".github/workflows/visual-pr-capture.yml@main",
|
|
pull_requests: [],
|
|
},
|
|
];
|
|
const deps = {
|
|
loadWorkflowRunsResponsePage: async () => ({ workflow_runs: workflowRuns }),
|
|
loadPullRequestsForHeadSha: async () => [pull],
|
|
};
|
|
|
|
assert.deepEqual(
|
|
(await listPendingApprovalRuns("nexu-io/open-design", pull, [{ filename: "README.md", status: "modified" }], deps)).map((run) => run.id),
|
|
[26273463769],
|
|
);
|
|
assert.deepEqual(
|
|
(await listPendingApprovalRuns(
|
|
"nexu-io/open-design",
|
|
pull,
|
|
[{ filename: "apps/web/src/components/Button.tsx", status: "modified" }],
|
|
deps,
|
|
)).map((run) => run.id),
|
|
[26273463769, 26273463770],
|
|
);
|
|
});
|
|
|
|
test("hasPullApprovalStateDrift ignores base tip churn but still rejects base retargeting and head drift", () => {
|
|
const pull = {
|
|
number: 2683,
|
|
state: "open",
|
|
changed_files: 1,
|
|
head: {
|
|
ref: "fix/workflow-linkage",
|
|
sha: "734076155c44e569304856590019cea54506fdab",
|
|
repo: { full_name: "someone/open-design" },
|
|
},
|
|
base: {
|
|
ref: "main",
|
|
sha: "4cd93a5c7a7b0db1961c854e55f8e0e6b1b45542",
|
|
repo: { full_name: "nexu-io/open-design" },
|
|
},
|
|
};
|
|
|
|
assert.equal(hasPullApprovalStateDrift(pull, pull), false);
|
|
assert.equal(
|
|
hasPullApprovalStateDrift(pull, {
|
|
...pull,
|
|
base: { ...pull.base, sha: "08a88a65482123629ebda5a090c71533bd6b8a88" },
|
|
}),
|
|
false,
|
|
);
|
|
assert.equal(hasPullApprovalStateDrift(pull, { ...pull, draft: true }), true);
|
|
assert.equal(hasPullApprovalStateDrift(pull, { ...pull, state: "closed" }), true);
|
|
assert.equal(
|
|
hasPullApprovalStateDrift(pull, {
|
|
...pull,
|
|
head: { ...pull.head, sha: "08a88a65482123629ebda5a090c71533bd6b8a88" },
|
|
}),
|
|
true,
|
|
);
|
|
assert.equal(
|
|
hasPullApprovalStateDrift(pull, {
|
|
...pull,
|
|
base: { ...pull.base, ref: "release" },
|
|
}),
|
|
true,
|
|
);
|
|
});
|
|
|
|
test("isDeniedChangedPath blocks common tool config files under allowlisted source trees", () => {
|
|
assert.equal(isDeniedChangedPath("apps/web/vitest.config.ts"), true);
|
|
assert.equal(isDeniedChangedPath("apps/web/vite.config.ts"), true);
|
|
assert.equal(isDeniedChangedPath("apps/web/playwright.config.ts"), true);
|
|
assert.equal(isDeniedChangedPath("apps/web/tsconfig.sidecar.json"), true);
|
|
assert.equal(isDeniedChangedPath("apps/daemon/tsconfig.tests.json"), true);
|
|
assert.equal(isDeniedChangedPath("packages/contracts/tsconfig.tests.json"), true);
|
|
assert.equal(isDeniedChangedPath("apps/packaged/tsconfig.json"), true);
|
|
assert.equal(isDeniedChangedPath("apps/packaged/vitest.config.ts"), true);
|
|
assert.equal(isDeniedChangedPath("apps/web/src/app/page.tsx"), false);
|
|
});
|
|
|
|
test("isAllowedChangedPath allows ordinary app and package source/test paths while keeping denied surfaces blocked", () => {
|
|
assert.equal(isAllowedChangedPath("apps/packaged/src/main.ts"), true);
|
|
assert.equal(isAllowedChangedPath("apps/packaged/tests/main.test.ts"), true);
|
|
assert.equal(isAllowedChangedPath("apps/desktop/src/main.ts"), true);
|
|
assert.equal(isAllowedChangedPath("packages/sidecar/src/index.ts"), true);
|
|
assert.equal(isAllowedChangedPath("tools/pack/src/index.ts"), false);
|
|
assert.equal(isAllowedChangedPath("apps/packaged/tsconfig.json"), false);
|
|
assert.equal(isAllowedChangedPath("apps/packaged/vitest.config.ts"), false);
|
|
});
|
|
|
|
test("isAllowedVisualCaptureChangedPath is limited to web ts tsx and css source", () => {
|
|
assert.equal(isAllowedVisualCaptureChangedPath("apps/web/src/app/page.tsx"), true);
|
|
assert.equal(isAllowedVisualCaptureChangedPath("apps/web/src/lib/theme.ts"), true);
|
|
assert.equal(isAllowedVisualCaptureChangedPath("apps/web/src/components/Button.css"), true);
|
|
assert.equal(isAllowedVisualCaptureChangedPath("apps/web/src/assets/icon.svg"), false);
|
|
assert.equal(isAllowedVisualCaptureChangedPath("apps/web/public/logo.png"), false);
|
|
assert.equal(isAllowedVisualCaptureChangedPath("apps/web/package.json"), false);
|
|
assert.equal(isAllowedVisualCaptureChangedPath("apps/web/tests/Button.test.tsx"), false);
|
|
assert.equal(isAllowedVisualCaptureChangedPath("packages/contracts/src/api.ts"), false);
|
|
});
|
|
|
|
test("waitForPendingApprovalRuns retries until action_required runs appear and keeps polling through the retry window", async () => {
|
|
const run = {
|
|
id: 26273463769,
|
|
name: "CI",
|
|
event: "pull_request",
|
|
status: "completed",
|
|
conclusion: "action_required",
|
|
head_sha: "734076155c44e569304856590019cea54506fdab",
|
|
path: ".github/workflows/ci.yml@main",
|
|
pull_requests: [],
|
|
};
|
|
|
|
const batches = [[], [], [run]];
|
|
const sleeps: number[] = [];
|
|
let now = 0;
|
|
|
|
const pendingRuns = await waitForPendingApprovalRuns(
|
|
async () => batches.shift() ?? [run],
|
|
async (ms) => {
|
|
sleeps.push(ms);
|
|
now += ms;
|
|
},
|
|
() => now,
|
|
{ settlingWindowMs: 9_000 },
|
|
);
|
|
|
|
assert.deepEqual(pendingRuns, [run]);
|
|
assert.deepEqual(sleeps, [3000, 3000, 3000, 3000, 3000]);
|
|
});
|
|
|
|
test("waitForPendingApprovalRuns keeps polling and returns the latest eligible run snapshot across the retry window", async () => {
|
|
const ciRun = {
|
|
id: 26273463769,
|
|
name: "CI",
|
|
event: "pull_request",
|
|
status: "completed",
|
|
conclusion: "action_required",
|
|
head_sha: "734076155c44e569304856590019cea54506fdab",
|
|
path: ".github/workflows/ci.yml@main",
|
|
pull_requests: [],
|
|
};
|
|
const visualRun = {
|
|
id: 26273463770,
|
|
name: "Visual PR Verify",
|
|
event: "pull_request",
|
|
status: "completed",
|
|
conclusion: "action_required",
|
|
head_sha: "734076155c44e569304856590019cea54506fdab",
|
|
path: ".github/workflows/visual-pr-verify.yml@main",
|
|
pull_requests: [],
|
|
};
|
|
|
|
const batches = [[ciRun], [ciRun], [ciRun, visualRun], [ciRun, visualRun]];
|
|
const sleeps: number[] = [];
|
|
let now = 0;
|
|
|
|
const pendingRuns = await waitForPendingApprovalRuns(
|
|
async () => batches.shift() ?? [ciRun, visualRun],
|
|
async (ms) => {
|
|
sleeps.push(ms);
|
|
now += ms;
|
|
},
|
|
() => now,
|
|
{ settlingWindowMs: 9_000 },
|
|
);
|
|
|
|
assert.deepEqual(pendingRuns, [ciRun, visualRun]);
|
|
assert.deepEqual(sleeps, [3000, 3000, 3000, 3000, 3000]);
|
|
});
|
|
|
|
test("waitForPendingApprovalRuns drops runs that disappear in later polls", async () => {
|
|
const staleRun = {
|
|
id: 26273463769,
|
|
name: "CI",
|
|
event: "pull_request",
|
|
status: "completed",
|
|
conclusion: "action_required",
|
|
head_sha: "734076155c44e569304856590019cea54506fdab",
|
|
path: ".github/workflows/ci.yml@main",
|
|
pull_requests: [],
|
|
};
|
|
const survivingRun = {
|
|
...staleRun,
|
|
id: 26273463770,
|
|
name: "Visual PR Verify",
|
|
path: ".github/workflows/visual-pr-verify.yml@main",
|
|
};
|
|
|
|
const batches = [[staleRun], [staleRun, survivingRun], [survivingRun], [survivingRun]];
|
|
let now = 0;
|
|
|
|
const pendingRuns = await waitForPendingApprovalRuns(
|
|
async () => batches.shift() ?? [survivingRun],
|
|
async (ms) => {
|
|
now += ms;
|
|
},
|
|
() => now,
|
|
{ settlingWindowMs: 6_000 },
|
|
);
|
|
|
|
assert.deepEqual(pendingRuns, [survivingRun]);
|
|
});
|
|
|
|
test("waitForPendingApprovalRuns keeps polling until the run set is stable, even when another eligible run appears after the old three-poll budget", async () => {
|
|
const ciRun = {
|
|
id: 26273463769,
|
|
name: "CI",
|
|
event: "pull_request",
|
|
status: "completed",
|
|
conclusion: "action_required",
|
|
head_sha: "734076155c44e569304856590019cea54506fdab",
|
|
path: ".github/workflows/ci.yml@main",
|
|
pull_requests: [],
|
|
};
|
|
const visualRun = {
|
|
id: 26273463770,
|
|
name: "Visual PR Verify",
|
|
event: "pull_request",
|
|
status: "completed",
|
|
conclusion: "action_required",
|
|
head_sha: "734076155c44e569304856590019cea54506fdab",
|
|
path: ".github/workflows/visual-pr-verify.yml@main",
|
|
pull_requests: [],
|
|
};
|
|
|
|
const batches = [[ciRun], [ciRun], [ciRun], [ciRun], [ciRun], [ciRun, visualRun], [ciRun, visualRun]];
|
|
const sleeps: number[] = [];
|
|
let now = 0;
|
|
|
|
const pendingRuns = await waitForPendingApprovalRuns(
|
|
async () => batches.shift() ?? [ciRun, visualRun],
|
|
async (ms) => {
|
|
sleeps.push(ms);
|
|
now += ms;
|
|
},
|
|
() => now,
|
|
{
|
|
firstAppearanceTimeoutMs: 30_000,
|
|
settlingWindowMs: 15_000,
|
|
},
|
|
);
|
|
|
|
assert.deepEqual(pendingRuns, [ciRun, visualRun]);
|
|
assert.equal(sleeps.length, 10);
|
|
});
|
|
|
|
test("waitForPendingApprovalRuns gives late first appearances their own full settling window", async () => {
|
|
const ciRun = {
|
|
id: 26273463769,
|
|
name: "CI",
|
|
event: "pull_request",
|
|
status: "completed",
|
|
conclusion: "action_required",
|
|
head_sha: "734076155c44e569304856590019cea54506fdab",
|
|
path: ".github/workflows/ci.yml@main",
|
|
pull_requests: [],
|
|
};
|
|
const visualRun = {
|
|
id: 26273463770,
|
|
name: "Visual PR Verify",
|
|
event: "pull_request",
|
|
status: "completed",
|
|
conclusion: "action_required",
|
|
head_sha: "734076155c44e569304856590019cea54506fdab",
|
|
path: ".github/workflows/visual-pr-verify.yml@main",
|
|
pull_requests: [],
|
|
};
|
|
|
|
const batches = [[], [], [], [ciRun], [ciRun], [ciRun, visualRun], [ciRun, visualRun], [ciRun, visualRun]];
|
|
const sleeps: number[] = [];
|
|
let now = 0;
|
|
|
|
const pendingRuns = await waitForPendingApprovalRuns(
|
|
async () => batches.shift() ?? [ciRun, visualRun],
|
|
async (ms) => {
|
|
sleeps.push(ms);
|
|
now += ms;
|
|
},
|
|
() => now,
|
|
{
|
|
firstAppearanceTimeoutMs: 12_000,
|
|
settlingWindowMs: 9_000,
|
|
},
|
|
);
|
|
|
|
assert.deepEqual(pendingRuns, [ciRun, visualRun]);
|
|
assert.deepEqual(sleeps, [3000, 3000, 3000, 3000, 3000, 3000, 3000, 3000]);
|
|
});
|
|
|
|
test("waitForPendingApprovalRuns keeps polling until the first run appears, even after the old short retry budget", async () => {
|
|
const run = {
|
|
id: 26273463769,
|
|
name: "CI",
|
|
event: "pull_request",
|
|
status: "completed",
|
|
conclusion: "action_required",
|
|
head_sha: "734076155c44e569304856590019cea54506fdab",
|
|
path: ".github/workflows/ci.yml@main",
|
|
pull_requests: [],
|
|
};
|
|
|
|
const batches = [[], [], [], [], [run]];
|
|
const sleeps: number[] = [];
|
|
let now = 0;
|
|
|
|
const pendingRuns = await waitForPendingApprovalRuns(
|
|
async () => batches.shift() ?? [run],
|
|
async (ms) => {
|
|
sleeps.push(ms);
|
|
now += ms;
|
|
},
|
|
() => now,
|
|
{
|
|
firstAppearanceTimeoutMs: 15_000,
|
|
settlingWindowMs: 6_000,
|
|
},
|
|
);
|
|
|
|
assert.deepEqual(pendingRuns, [run]);
|
|
assert.deepEqual(sleeps, [3000, 3000, 3000, 3000, 3000, 3000]);
|
|
});
|
|
|
|
test("waitForPendingApprovalRuns accepts a configurable longer polling budget before the first run appears", async () => {
|
|
const run = {
|
|
id: 26273463769,
|
|
name: "CI",
|
|
event: "pull_request",
|
|
status: "completed",
|
|
conclusion: "action_required",
|
|
head_sha: "734076155c44e569304856590019cea54506fdab",
|
|
path: ".github/workflows/ci.yml@main",
|
|
pull_requests: [],
|
|
};
|
|
|
|
const batches = [[], [], [], [], [], [run]];
|
|
const sleeps: number[] = [];
|
|
let now = 0;
|
|
|
|
const pendingRuns = await waitForPendingApprovalRuns(
|
|
async () => batches.shift() ?? [run],
|
|
async (ms) => {
|
|
sleeps.push(ms);
|
|
now += ms;
|
|
},
|
|
() => now,
|
|
{
|
|
firstAppearanceTimeoutMs: 18_000,
|
|
settlingWindowMs: 3_000,
|
|
},
|
|
);
|
|
|
|
assert.deepEqual(pendingRuns, [run]);
|
|
assert.deepEqual(sleeps, [3000, 3000, 3000, 3000, 3000, 3000]);
|
|
});
|
|
|
|
test("waitForPendingApprovalRuns stops after the first-appearance timeout when no runs arrive", async () => {
|
|
let calls = 0;
|
|
const sleeps: number[] = [];
|
|
let now = 0;
|
|
|
|
const pendingRuns = await waitForPendingApprovalRuns(
|
|
async () => {
|
|
calls += 1;
|
|
return [];
|
|
},
|
|
async (ms) => {
|
|
sleeps.push(ms);
|
|
now += ms;
|
|
},
|
|
() => now,
|
|
{
|
|
firstAppearanceTimeoutMs: 9_000,
|
|
},
|
|
);
|
|
|
|
assert.deepEqual(pendingRuns, []);
|
|
assert.equal(calls, 4);
|
|
assert.equal(sleeps.length, 3);
|
|
assert.equal(now, 9_000);
|
|
});
|