Skip to content
82 linesCodeBlameRaw
1import assert from "node:assert/strict";
2import { test } from "node:test";
3
4import type { Job, RunAttempt, WorkflowRun } from "@g1t/contracts";
5import { runControls } from "./run-controls.ts";
6
7type Run = Pick<WorkflowRun, "attempt" | "status" | "conclusion" | "error">;
8type Attempt = Pick<RunAttempt, "attempt" | "status">;
9
10const run = (more: Partial<Run> = {}): Run => ({ attempt: 1, status: "completed", conclusion: "success", error: null, ...more });
11const jobs = (...statuses: Job["status"][]) => statuses.map((status) => ({ status }));
12/** Attempts 1 to `latest`, the earlier ones finished, the latest as `status`. */
13function attempts(latest: number, status: Attempt["status"]): Attempt[] {
14 return Array.from({ length: latest }, (_, i) => ({ attempt: i + 1, status: i + 1 === latest ? status : "completed" }));
15}
16
17test("a re-run of failed jobs whose job waits for a self-hosted runner can be cancelled, not re-run", () => {
18 // Attempt 6 of 6: its first job succeeded, the one that needs it is queued.
19 const controls = runControls({
20 run: run({ attempt: 6, status: "in_progress", conclusion: null }),
21 jobs: jobs("completed", "queued"),
22 attempts: attempts(6, "in_progress"),
23 member: true,
24 });
25 assert.deepEqual(controls, { latest: true, going: true, cancelling: false, canCancel: true, canRerun: false });
26});
27
28test("an earlier attempt's page offers Cancel run while the latest attempt runs", () => {
29 // `?attempt=5`: attempt 5 as it ended, every job finished.
30 const controls = runControls({
31 run: run({ attempt: 5, status: "completed", conclusion: "failure" }),
32 jobs: jobs("completed", "completed"),
33 attempts: attempts(6, "in_progress"),
34 member: true,
35 });
36 assert.equal(controls.latest, false);
37 assert.equal(controls.going, true);
38 assert.equal(controls.canCancel, true);
39 assert.equal(controls.canRerun, false);
40 // How attempt 5 ended says nothing of attempt 6 being cancelled.
41 assert.equal(runControls({ run: run({ attempt: 5, conclusion: "cancelled" }), jobs: [], attempts: attempts(6, "queued"), member: true }).cancelling, false);
42});
43
44test("a job not finished keeps the latest attempt going, whatever the run's own status says", () => {
45 for (const status of ["waiting", "pending", "queued", "in_progress", "calling"] as const) {
46 const controls = runControls({ run: run({ attempt: 2, status: "completed" }), jobs: jobs("completed", status), attempts: attempts(2, "completed"), member: true });
47 assert.equal(controls.canCancel, true, status);
48 assert.equal(controls.canRerun, false, status);
49 }
50});
51
52test("every status a run waits or runs in can be cancelled", () => {
53 for (const status of ["pending", "action_required", "queued", "in_progress", "waiting"] as const) {
54 const controls = runControls({ run: run({ status, conclusion: null }), jobs: [], attempts: attempts(1, status), member: true });
55 assert.equal(controls.canCancel, true, status);
56 assert.equal(controls.canRerun, false, status);
57 }
58});
59
60test("a finished run is re-run from its latest attempt only", () => {
61 const latest = runControls({ run: run({ attempt: 2, conclusion: "failure" }), jobs: jobs("completed"), attempts: attempts(2, "completed"), member: true });
62 assert.deepEqual(latest, { latest: true, going: false, cancelling: false, canCancel: false, canRerun: true });
63 const earlier = runControls({ run: run({ attempt: 1, conclusion: "failure" }), jobs: jobs("completed"), attempts: attempts(2, "completed"), member: true });
64 assert.equal(earlier.canRerun, false);
65 assert.equal(earlier.canCancel, false);
66 // A run of one attempt has no list to look through.
67 assert.equal(runControls({ run: run(), jobs: jobs("completed"), attempts: [], member: true }).canRerun, true);
68});
69
70test("a run being cancelled shows Force cancel", () => {
71 const controls = runControls({ run: run({ status: "in_progress", conclusion: "cancelled" }), jobs: jobs("in_progress"), attempts: attempts(1, "in_progress"), member: true });
72 assert.equal(controls.cancelling, true);
73 assert.equal(controls.canCancel, true);
74});
75
76test("neither without Write, nor for a run whose workflow file could not be used", () => {
77 const going = { run: run({ status: "in_progress", conclusion: null }), jobs: jobs("queued"), attempts: attempts(1, "in_progress") };
78 assert.equal(runControls({ ...going, member: false }).canCancel, false);
79 const broken = runControls({ run: run({ conclusion: "failure", error: "Its file is not YAML." }), jobs: [], attempts: [], member: true });
80 assert.equal(broken.canCancel, false);
81 assert.equal(broken.canRerun, false);
82});