Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .eslintrc.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
env:
node: true
es6: true
es2022: true

globals:
Atomics: readonly
Expand Down
28 changes: 28 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
name: Test

on:
pull_request:
merge_group:
workflow_dispatch:

permissions:
contents: read

jobs:
test:
runs-on: ubuntu-24.04
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version-file: .node-version
cache: npm

- name: Install dependencies
run: npm ci

- name: Run tests
run: npm test
135 changes: 135 additions & 0 deletions src/display.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import * as core from "@actions/core";
import { Display } from "./display";
import { RelevantCheckRuns } from "./relevant-check-runs";
import type { CheckRun } from "./fetch-check-runs";

vi.mock("@actions/core");

const colors = {
reset: "\x1b[0m",
red: "\x1b[31m",
green: "\x1b[32m",
} as const;

describe("Display", () => {
let consoleInfo: ReturnType<typeof vi.spyOn>;

beforeEach(() => {
vi.clearAllMocks();
consoleInfo = vi.spyOn(console, "info").mockImplementation(() => {});
});

afterEach(() => {
consoleInfo.mockRestore();
});

describe("timedOut", () => {
it("prints timeout message", () => {
Display.timedOut();
expect(consoleInfo).toHaveBeenNthCalledWith(1, "");
expect(consoleInfo).toHaveBeenNthCalledWith(
2,
`⏰ ${colors.red}Timed out!${colors.reset}`,
);
});
});

describe("delaying", () => {
it("prints delay message with the given seconds", () => {
Display.delaying(10);
expect(consoleInfo).toHaveBeenCalledWith("🦥 Inspecting again in 10s...");
});
});

describe("overallFailure", () => {
it("prints failure message", () => {
Display.overallFailure();
expect(consoleInfo).toHaveBeenNthCalledWith(1, "");
expect(consoleInfo).toHaveBeenNthCalledWith(
2,
`❗ ${colors.red}Failure!${colors.reset}`,
);
});
});

describe("overallSuccess", () => {
it("prints success message", () => {
Display.overallSuccess();
expect(consoleInfo).toHaveBeenNthCalledWith(1, "");
expect(consoleInfo).toHaveBeenNthCalledWith(
2,
`🚀 ${colors.green}Success!${colors.reset}`,
);
});
});

describe("startingIteration", () => {
it("prints an empty line", () => {
Display.startingIteration();
expect(consoleInfo).toHaveBeenCalledWith("");
});
});

describe("ignoredCheckPatterns", () => {
it("groups and prints patterns when present", () => {
Display.ignoredCheckPatterns(["check1", "check2"]);
expect(core.startGroup).toHaveBeenCalledWith("Ignored check patterns");
expect(consoleInfo).toHaveBeenNthCalledWith(1, "check1");
expect(consoleInfo).toHaveBeenNthCalledWith(2, "check2");
expect(core.endGroup).toHaveBeenCalled();
});

it("prints nothing when the list is empty", () => {
Display.ignoredCheckPatterns([]);
expect(core.startGroup).not.toHaveBeenCalled();
expect(consoleInfo).not.toHaveBeenCalled();
expect(core.endGroup).not.toHaveBeenCalled();
});
});

describe("relevantCheckRuns", () => {
function stubRun(name: string, conclusion: string | null): CheckRun {
return { name, conclusion } as CheckRun;
}

it("groups each non-empty category with counts and icons", () => {
const runs = new RelevantCheckRuns([
stubRun("success-check", "success"),
stubRun("failed-check", "failure"),
stubRun("pending-check", null),
]);

Display.relevantCheckRuns(runs);

expect(core.startGroup).toHaveBeenNthCalledWith(
1,
`✅ ${colors.green}1${colors.reset}`,
);
expect(consoleInfo).toHaveBeenNthCalledWith(1, "success-check");

expect(core.startGroup).toHaveBeenNthCalledWith(
2,
`❌ ${colors.red}1${colors.reset}`,
);
expect(consoleInfo).toHaveBeenNthCalledWith(2, "failed-check");

expect(core.startGroup).toHaveBeenNthCalledWith(
3,
`⏳ ${colors.reset}1${colors.reset}`,
);
expect(consoleInfo).toHaveBeenNthCalledWith(3, "pending-check");
});

it("skips empty categories", () => {
const runs = new RelevantCheckRuns([stubRun("success-check", "success")]);

Display.relevantCheckRuns(runs);

expect(core.startGroup).toHaveBeenCalledTimes(1);
expect(core.startGroup).toHaveBeenCalledWith(
`✅ ${colors.green}1${colors.reset}`,
);
});
});
});
173 changes: 173 additions & 0 deletions src/fetch-check-runs.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
import { describe, it, expect, vi, beforeEach, beforeAll } from "vitest";
import type { Octokit } from "@octokit/core";
import { IgnoreMatcher } from "./ignore-matcher";

const mockPaginateIterator = vi.fn();
const mockOctokit = {
paginate: { iterator: mockPaginateIterator },
rest: { checks: { listForRef: vi.fn() } },
} as unknown as Octokit;

vi.mock("@actions/github", () => ({
getOctokit: vi.fn(() => mockOctokit),
context: { repo: { owner: "test-owner", repo: "test-repo" } },
}));

vi.mock("./inputs", () => ({
inputs: {
token: "fake-token",
name: "sloth",
ignored: new IgnoreMatcher(["ignored-check"]),
ref: "main",
},
}));

let fetchCheckRuns: Awaited<
typeof import("./fetch-check-runs")
>["fetchCheckRuns"];

beforeAll(async () => {
({ fetchCheckRuns } = await import("./fetch-check-runs"));
});

describe("fetchCheckRuns", () => {
beforeEach(() => {
mockPaginateIterator.mockReset();
});

it("filters out the action's own check run and ignored checks", async () => {
mockPaginateIterator.mockImplementation(async function* () {
yield {
data: [
{
name: "test-check-1",
status: "completed",
conclusion: "success",
completed_at: "2024-01-01",
started_at: null,
},
{
name: "sloth",
status: "completed",
conclusion: "success",
completed_at: "2024-01-01",
started_at: null,
},
{
name: "ignored-check",
status: "completed",
conclusion: "success",
completed_at: "2024-01-01",
started_at: null,
},
{
name: "test-check-2",
status: "in_progress",
conclusion: null,
completed_at: null,
started_at: "2024-01-01",
},
],
};
});

const result = await fetchCheckRuns();

expect(result.total()).toBe(2);
expect(result.succeeded).toEqual(["test-check-1"]);
expect(result.pending).toEqual(["test-check-2"]);
expect(result.failed).toEqual([]);
});

it("paginates across multiple pages", async () => {
mockPaginateIterator.mockImplementation(async function* () {
yield {
data: [
{
name: "check-1",
status: "completed",
conclusion: "success",
completed_at: "2024-01-01",
started_at: null,
},
],
};
yield {
data: [
{
name: "check-2",
status: "completed",
conclusion: "success",
completed_at: "2024-01-01",
started_at: null,
},
],
};
});

const result = await fetchCheckRuns();
expect(result.total()).toBe(2);
expect(result.succeeded).toEqual(["check-1", "check-2"]);
});

it("returns empty results when no check runs are found", async () => {
mockPaginateIterator.mockImplementation(async function* () {
yield { data: [] };
});

const result = await fetchCheckRuns();
expect(result.total()).toBe(0);
});

it("identifies overall failure when any run has failed", async () => {
mockPaginateIterator.mockImplementation(async function* () {
yield {
data: [
{
name: "test-1",
status: "completed",
conclusion: "failure",
completed_at: "2024-01-01",
started_at: null,
},
{
name: "test-2",
status: "completed",
conclusion: "success",
completed_at: "2024-01-01",
started_at: null,
},
],
};
});

const result = await fetchCheckRuns();
expect(result.isOverallFailure()).toBe(true);
});

it("identifies overall success when all runs have passed", async () => {
mockPaginateIterator.mockImplementation(async function* () {
yield {
data: [
{
name: "test-1",
status: "completed",
conclusion: "success",
completed_at: "2024-01-01",
started_at: null,
},
{
name: "test-2",
status: "completed",
conclusion: "success",
completed_at: "2024-01-01",
started_at: null,
},
],
};
});

const result = await fetchCheckRuns();
expect(result.isOverallSuccess()).toBe(true);
});
});
Loading