Publish curated projectbrain source from 246c0a7f35e32f54fa1bf8a3f720b01bb00dd9af
This commit is contained in:
1 parent
759d263081
commit
98f7618af2
11 files changed
+540
-63
No files matched your search
@@ -1,15 +1,33 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { GiteaClient } from "../src/integrations/gitea/client.js";
|
||||
import { assertSuccessfulActionRuns, exactHeadActionRuns } from "../src/integrations/gitea/policy.js";
|
||||
import type { GiteaActionRun } from "../src/integrations/gitea/types.js";
|
||||
import { assertSuccessfulActionRuns, assertSuccessfulPullRequestFullActionRun, exactHeadActionRuns } from "../src/integrations/gitea/policy.js";
|
||||
import type { GiteaActionJob, GiteaActionRun } from "../src/integrations/gitea/types.js";
|
||||
|
||||
const branch = "codex/native-ci";
|
||||
const sha = "a".repeat(40);
|
||||
const context = "CI / build-test (pull_request)";
|
||||
const fullJob = (runId: number, headSha = sha, overrides: Partial<GiteaActionJob> = {}): GiteaActionJob => ({
|
||||
id: runId * 10,
|
||||
html_url: `https://gitea.example/runs/${runId}/jobs/${runId * 10}`,
|
||||
run_id: runId,
|
||||
name: "full",
|
||||
labels: [],
|
||||
run_attempt: 1,
|
||||
head_sha: headSha,
|
||||
head_branch: branch,
|
||||
status: "completed",
|
||||
conclusion: "success",
|
||||
runner_id: 1,
|
||||
steps: [],
|
||||
created_at: "2026-10-06T21:33:06Z",
|
||||
...overrides,
|
||||
});
|
||||
|
||||
const run = (id: number, overrides: Partial<GiteaActionRun> = {}): GiteaActionRun => ({
|
||||
id, run_number: id, display_title: "Managed validation / full", path: ".gitea/workflows/managed-validation.yml",
|
||||
run_attempt: 1,
|
||||
event: "pull_request", head_sha: sha, head_branch: "", status: "completed",
|
||||
conclusion: "success", html_url: `https://gitea.example/runs/${id}`, ...overrides,
|
||||
conclusion: "success", html_url: `https://gitea.example/runs/${id}`, jobs: [fullJob(id)], ...overrides,
|
||||
});
|
||||
|
||||
describe("native PR validation discovery", () => {
|
||||
@@ -24,6 +42,133 @@ describe("native PR validation discovery", () => {
|
||||
expect(assertSuccessfulActionRuns([matching], branch, sha)).toEqual([matching]);
|
||||
});
|
||||
|
||||
it("accepts Gitea 28 native PR full validation fields and its exact successful full job", () => {
|
||||
const nativeRun = run(22352, {
|
||||
run_number: 17,
|
||||
display_title: "ci: validate QuizMonitor backend tests",
|
||||
path: "managed-validation.yml@refs/pull/6/head",
|
||||
head_branch: undefined,
|
||||
pull_requests: [{ number: 6, head: { ref: branch, sha } }],
|
||||
jobs: [fullJob(22352, sha, { id: 38976, head_branch: undefined })],
|
||||
});
|
||||
|
||||
expect(assertSuccessfulPullRequestFullActionRun([nativeRun], branch, sha)).toBe(nativeRun);
|
||||
expect(assertSuccessfulPullRequestFullActionRun([nativeRun], branch, sha, 6)).toBe(nativeRun);
|
||||
});
|
||||
|
||||
it("binds merge validation to the requested PR number despite a blank native head branch", () => {
|
||||
const prSix = run(29, {
|
||||
path: "managed-validation.yml@refs/pull/6/head",
|
||||
pull_requests: [{ number: 6, head: { ref: branch, sha } }],
|
||||
head_branch: "",
|
||||
});
|
||||
expect(() => assertSuccessfulPullRequestFullActionRun([prSix], branch, sha, 99))
|
||||
.toThrow("FEATURE_DELIVERY_PULL_REQUEST_FULL_VALIDATION_REQUIRED");
|
||||
expect(() => assertSuccessfulPullRequestFullActionRun([run(30, {
|
||||
path: "managed-validation.yml@refs/pull/6/head",
|
||||
pull_requests: [{ number: 99, head: { ref: branch, sha } }],
|
||||
head_branch: "",
|
||||
})], branch, sha, 6)).toThrow("FEATURE_DELIVERY_PULL_REQUEST_FULL_VALIDATION_REQUIRED");
|
||||
expect(() => assertSuccessfulPullRequestFullActionRun([run(34, {
|
||||
path: ".gitea/workflows/managed-validation.yml",
|
||||
head_branch: "",
|
||||
})], branch, sha, 6)).toThrow("FEATURE_DELIVERY_PULL_REQUEST_FULL_VALIDATION_REQUIRED");
|
||||
expect(() => assertSuccessfulPullRequestFullActionRun([run(35, {
|
||||
path: "managed-validation.yml@refs/pull/6/head",
|
||||
pull_requests: [{ number: 6, head: { ref: branch, sha } }, { number: 99, head: { ref: branch, sha } }],
|
||||
head_branch: "",
|
||||
})], branch, sha, 6)).toThrow("FEATURE_DELIVERY_PULL_REQUEST_FULL_VALIDATION_REQUIRED");
|
||||
});
|
||||
|
||||
it("rejects a newer failed attempt for the requested PR when Gitea omits head_branch", () => {
|
||||
const successful = run(31, {
|
||||
run_number: 17,
|
||||
run_attempt: 1,
|
||||
path: "managed-validation.yml@refs/pull/6/head",
|
||||
pull_requests: [{ number: 6, head: { ref: branch, sha } }],
|
||||
head_branch: "",
|
||||
jobs: [fullJob(31)],
|
||||
});
|
||||
const failed = run(32, {
|
||||
run_number: 17,
|
||||
run_attempt: 2,
|
||||
path: "managed-validation.yml@refs/pull/6/head",
|
||||
pull_requests: [{ number: 6, head: { ref: branch, sha } }],
|
||||
head_branch: "",
|
||||
jobs: [fullJob(32, sha, { run_attempt: 2, conclusion: "failure" })],
|
||||
});
|
||||
expect(() => assertSuccessfulPullRequestFullActionRun([successful, failed], branch, sha, 6))
|
||||
.toThrow("FEATURE_DELIVERY_PULL_REQUEST_FULL_VALIDATION_REQUIRED");
|
||||
});
|
||||
|
||||
it("keeps failed noncanonical PR workflows in the exact-head merge gate", () => {
|
||||
const full = run(37, {
|
||||
path: "managed-validation.yml@refs/pull/6/head",
|
||||
pull_requests: [{ number: 6, head: { ref: branch, sha } }],
|
||||
head_branch: "",
|
||||
});
|
||||
const failedQualityWorkflow = run(38, {
|
||||
path: ".gitea/workflows/security-check.yml@refs/pull/6/head",
|
||||
conclusion: "failure",
|
||||
pull_requests: [{ number: 6, head: { ref: branch, sha } }],
|
||||
head_branch: "",
|
||||
jobs: undefined,
|
||||
});
|
||||
expect(() => assertSuccessfulPullRequestFullActionRun([full, failedQualityWorkflow], branch, sha, 6))
|
||||
.toThrow("FEATURE_DELIVERY_CI_NOT_SUCCESSFUL");
|
||||
});
|
||||
|
||||
it("rejects malformed path metadata without throwing a generic runtime error", () => {
|
||||
const valid = run(33, {
|
||||
path: "managed-validation.yml@refs/pull/6/head",
|
||||
pull_requests: [{ number: 6, head: { ref: branch, sha } }],
|
||||
head_branch: "",
|
||||
});
|
||||
const malformed = run(34, { event: "push", path: null as unknown as string, head_branch: branch });
|
||||
expect(() => assertSuccessfulPullRequestFullActionRun([valid, malformed], branch, sha, 6))
|
||||
.toThrow("FEATURE_DELIVERY_CI_NOT_SUCCESSFUL");
|
||||
});
|
||||
|
||||
it.each([
|
||||
["job id", { id: undefined as unknown as number }],
|
||||
["run id", { run_id: undefined as unknown as number }],
|
||||
["attempt", { run_attempt: undefined as unknown as number }],
|
||||
["head SHA", { head_sha: undefined as unknown as string }],
|
||||
])("rejects a full job missing its %s identity", (_label, overrides) => {
|
||||
expect(() => assertSuccessfulPullRequestFullActionRun([run(36, { jobs: [fullJob(36, sha, overrides)] })], branch, sha))
|
||||
.toThrow("FEATURE_DELIVERY_PULL_REQUEST_FULL_VALIDATION_REQUIRED");
|
||||
});
|
||||
|
||||
it.each([
|
||||
["missing", []],
|
||||
["failed", [fullJob(20, sha, { conclusion: "failure" })]],
|
||||
["wrong run", [fullJob(21, sha, { run_id: 999 })]],
|
||||
["wrong attempt", [fullJob(22, sha, { run_attempt: 2 })]],
|
||||
["wrong head", [fullJob(23, "b".repeat(40))]],
|
||||
["duplicate", [fullJob(24), fullJob(24, sha, { id: 241 })]],
|
||||
])("rejects a %s canonical full job", (_label, jobs) => {
|
||||
expect(() => assertSuccessfulPullRequestFullActionRun([run(20, { jobs })], branch, sha))
|
||||
.toThrow("FEATURE_DELIVERY_PULL_REQUEST_FULL_VALIDATION_REQUIRED");
|
||||
});
|
||||
|
||||
it("does not let an older successful attempt authorize a newer failed full attempt", () => {
|
||||
const successful = run(25, { run_number: 17, run_attempt: 1, jobs: [fullJob(25)] });
|
||||
const failed = run(26, {
|
||||
run_number: 17,
|
||||
run_attempt: 2,
|
||||
jobs: [fullJob(26, sha, { run_attempt: 2, conclusion: "failure" })],
|
||||
});
|
||||
expect(() => assertSuccessfulPullRequestFullActionRun([successful, failed], branch, sha))
|
||||
.toThrow("FEATURE_DELIVERY_PULL_REQUEST_FULL_VALIDATION_REQUIRED");
|
||||
});
|
||||
|
||||
it.each(["untrusted/managed-validation.yml", "other.yml"]) ("does not treat %s or its title as canonical", (path) => {
|
||||
expect(() => assertSuccessfulPullRequestFullActionRun([run(27, {
|
||||
path,
|
||||
display_title: "Managed validation / full",
|
||||
})], branch, sha)).toThrow("FEATURE_DELIVERY_PULL_REQUEST_FULL_VALIDATION_REQUIRED");
|
||||
});
|
||||
|
||||
it.each(["pending", "queued", "waiting", "in_progress"])("does not authorize %s", (status) => {
|
||||
expect(() => assertSuccessfulActionRuns([run(1, { status, conclusion: undefined })], branch, sha)).toThrow("FEATURE_DELIVERY_FULL_VALIDATION_REQUIRED");
|
||||
});
|
||||
@@ -47,12 +192,108 @@ describe("native PR validation discovery", () => {
|
||||
], branch, sha)).toThrow("FEATURE_DELIVERY_CI_NOT_SUCCESSFUL");
|
||||
});
|
||||
|
||||
it("queries by exact SHA without the upstream branch filter", async () => {
|
||||
const fetcher = vi.fn(async () => new Response(JSON.stringify({ total_count: 1, workflow_runs: [run(1)] })));
|
||||
await new GiteaClient("https://gitea.example", "test-only", fetcher as typeof fetch).listActionRuns("Jens", "Repo", { branch, headSha: sha });
|
||||
it("queries by exact SHA and fetches jobs for the exact run attempt", async () => {
|
||||
const workflowRun = run(1);
|
||||
const job = fullJob(1);
|
||||
const fetcher = vi.fn(async (input: string | URL | Request) => {
|
||||
const url = new URL(String(input));
|
||||
if (url.pathname.endsWith("/actions/runs")) return new Response(JSON.stringify({ total_count: 1, workflow_runs: [workflowRun] }));
|
||||
if (url.pathname.endsWith("/actions/runs/1/attempts/1/jobs")) return new Response(JSON.stringify({ total_count: 1, jobs: [job] }));
|
||||
return new Response("unexpected request", { status: 404 });
|
||||
});
|
||||
const result = await new GiteaClient("https://gitea.example", "test-only", fetcher as typeof fetch).listActionRuns("Jens", "Repo", { branch, headSha: sha });
|
||||
const url = new URL(String(fetcher.mock.calls[0]![0]));
|
||||
expect(url.searchParams.get("head_sha")).toBe(sha);
|
||||
expect(url.searchParams.has("branch")).toBe(false);
|
||||
expect(result.workflow_runs[0]?.jobs).toEqual([job]);
|
||||
expect(fetcher).toHaveBeenCalledTimes(2);
|
||||
expect(new URL(String(fetcher.mock.calls[1]![0])).searchParams.get("limit")).toBe("100");
|
||||
});
|
||||
|
||||
it("only fetches validation jobs for the requested PR when another PR has the same head SHA", async () => {
|
||||
const unrelated = run(40, {
|
||||
path: "managed-validation.yml@refs/pull/99/head",
|
||||
pull_requests: [{ number: 99, head: { ref: branch, sha } }],
|
||||
head_branch: "",
|
||||
jobs: undefined,
|
||||
});
|
||||
const expected = run(41, {
|
||||
path: "managed-validation.yml@refs/pull/6/head",
|
||||
pull_requests: [{ number: 6, head: { ref: branch, sha } }],
|
||||
head_branch: "",
|
||||
jobs: undefined,
|
||||
});
|
||||
const fetcher = vi.fn(async (input: string | URL | Request) => {
|
||||
const url = new URL(String(input));
|
||||
if (url.pathname.endsWith("/actions/runs")) return new Response(JSON.stringify({ total_count: 2, workflow_runs: [unrelated, expected] }));
|
||||
if (url.pathname.endsWith("/actions/runs/41/attempts/1/jobs")) return new Response(JSON.stringify({ total_count: 1, jobs: [fullJob(41)] }));
|
||||
return new Response("unexpected request", { status: 404 });
|
||||
});
|
||||
const result = await new GiteaClient("https://gitea.example", "test-only", fetcher as typeof fetch)
|
||||
.listActionRuns("Jens", "Repo", { branch, headSha: sha, pullRequestNumber: 6 });
|
||||
expect(result.workflow_runs.find((item) => item.id === 41)?.jobs).toHaveLength(1);
|
||||
expect(result.workflow_runs.find((item) => item.id === 40)?.jobs).toBeUndefined();
|
||||
expect(fetcher).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("rejects duplicate job identities across paginated job results", async () => {
|
||||
const workflowRun = run(42);
|
||||
const firstPage = Array.from({ length: 100 }, (_, index) => fullJob(42, sha, { id: index + 1 }));
|
||||
const secondPage = [fullJob(42, sha, { id: 100 })];
|
||||
const fetcher = vi.fn(async (input: string | URL | Request) => {
|
||||
const url = new URL(String(input));
|
||||
if (url.pathname.endsWith("/actions/runs")) return new Response(JSON.stringify({ total_count: 1, workflow_runs: [workflowRun] }));
|
||||
if (url.pathname.endsWith("/attempts/1/jobs")) {
|
||||
return new Response(JSON.stringify({ total_count: 101, jobs: url.searchParams.get("page") === "1" ? firstPage : secondPage }));
|
||||
}
|
||||
return new Response("unexpected request", { status: 404 });
|
||||
});
|
||||
await expect(new GiteaClient("https://gitea.example", "test-only", fetcher as typeof fetch)
|
||||
.listActionRuns("Jens", "Repo", { branch, headSha: sha })).rejects.toMatchObject({ code: "GITEA_ACTION_JOBS_INVALID" });
|
||||
});
|
||||
|
||||
it("paginates the exact-head run inventory so a later failed PR run blocks merge", async () => {
|
||||
const successful = run(43, {
|
||||
run_number: 17,
|
||||
path: "managed-validation.yml@refs/pull/6/head",
|
||||
pull_requests: [{ number: 6, head: { ref: branch, sha } }],
|
||||
head_branch: "",
|
||||
jobs: undefined,
|
||||
});
|
||||
const failed = run(44, {
|
||||
run_number: 18,
|
||||
path: "managed-validation.yml@refs/pull/6/head",
|
||||
pull_requests: [{ number: 6, head: { ref: branch, sha } }],
|
||||
head_branch: "",
|
||||
conclusion: "failure",
|
||||
jobs: undefined,
|
||||
});
|
||||
const fetcher = vi.fn(async (input: string | URL | Request) => {
|
||||
const url = new URL(String(input));
|
||||
if (url.pathname.endsWith("/actions/runs")) {
|
||||
return new Response(JSON.stringify({ total_count: 2, workflow_runs: url.searchParams.get("page") === "1" ? [successful] : [failed] }));
|
||||
}
|
||||
if (url.pathname.endsWith("/actions/runs/44/attempts/1/jobs")) {
|
||||
return new Response(JSON.stringify({ total_count: 1, jobs: [fullJob(44, sha, { conclusion: "failure" })] }));
|
||||
}
|
||||
return new Response("unexpected request", { status: 404 });
|
||||
});
|
||||
const result = await new GiteaClient("https://gitea.example", "test-only", fetcher as typeof fetch)
|
||||
.listActionRuns("Jens", "Repo", { branch, headSha: sha, pullRequestNumber: 6 });
|
||||
expect(result.workflow_runs).toHaveLength(2);
|
||||
expect(fetcher).toHaveBeenCalledTimes(3);
|
||||
expect(() => assertSuccessfulPullRequestFullActionRun(result.workflow_runs, branch, sha, 6))
|
||||
.toThrow("FEATURE_DELIVERY_PULL_REQUEST_FULL_VALIDATION_REQUIRED");
|
||||
});
|
||||
|
||||
it("rejects repeated exact-head run identities across API pages", async () => {
|
||||
const repeated = run(45);
|
||||
const fetcher = vi.fn(async (input: string | URL | Request) => {
|
||||
const url = new URL(String(input));
|
||||
return new Response(JSON.stringify({ total_count: 2, workflow_runs: [repeated] }));
|
||||
});
|
||||
await expect(new GiteaClient("https://gitea.example", "test-only", fetcher as typeof fetch)
|
||||
.listActionRuns("Jens", "Repo", { branch, headSha: sha })).rejects.toMatchObject({ code: "GITEA_ACTION_RUNS_INVALID" });
|
||||
});
|
||||
|
||||
it.each([
|
||||
|
||||
Reference in new issue
Block a user