import { describe, expect, it } from "vitest"; import { autoFixRoundForState, clampAutoFixRound, findCIAutomationStateForPR, findPRAutomationOptionsForPR, normalizeAutoFixMaxRounds, } from "./ci-automation"; import type { TaskCIPRAutomationState, TaskPR, TaskPRAutomationOptions } from "@/lib/types/github"; function makePR(repositoryID: string, prNumber = 42, taskID = "task-1"): TaskPR { return { task_id: taskID, repository_id: repositoryID, pr_number: prNumber, } as TaskPR; } function makePRAutomationOptions( repositoryID: string, prNumber: number, overrides: Partial = {}, ): TaskPRAutomationOptions { return { task_id: "task-1", repository_id: repositoryID, pr_number: prNumber, auto_fix_enabled: false, auto_merge_enabled: false, prompt_on_review_requested: false, prompt_on_merged: false, prompt_on_closed: false, created_at: "", updated_at: "", ...overrides, }; } function makeState( repositoryID: string, prNumber: number, roundCount: number, exhaustedAt: string | null = null, ): TaskCIPRAutomationState { return { repository_id: repositoryID, pr_number: prNumber, auto_fix_round_count: roundCount, auto_fix_exhausted_at: exhaustedAt, } as TaskCIPRAutomationState; } describe("CI automation helpers", () => { it("finds PR automation state by repository id and PR number", () => { const states = [makeState("", 42, 1), makeState("repo-1", 42, 2), makeState("repo-1", 7, 3)]; expect(findCIAutomationStateForPR(states, makePR("repo-1", 42))).toBe(states[1]); expect(findCIAutomationStateForPR(states, makePR("", 42))).toBe(states[0]); expect(findCIAutomationStateForPR(states, makePR("repo-missing", 42))).toBeUndefined(); }); it("returns an empty round state when no PR state exists", () => { expect(autoFixRoundForState(undefined, 12)).toEqual({ current: 0, max: 12, exhausted: false, }); }); it("treats only backend exhaustion timestamps as paused", () => { expect(autoFixRoundForState(makeState("repo-1", 42, 10), 10)).toEqual({ current: 10, max: 10, exhausted: false, }); expect(autoFixRoundForState(makeState("repo-1", 42, 3, "2026-06-18T11:00:00Z"), 10)).toEqual({ current: 3, max: 10, exhausted: true, }); }); it("finds PR automation options by repository id and PR number, independent of other PRs", () => { const options = [ makePRAutomationOptions("repo-1", 1, { auto_fix_enabled: true }), makePRAutomationOptions("repo-1", 2, { auto_fix_enabled: false }), makePRAutomationOptions("repo-2", 1, { auto_merge_enabled: true }), ]; expect(findPRAutomationOptionsForPR(options, makePR("repo-1", 1))).toBe(options[0]); expect(findPRAutomationOptionsForPR(options, makePR("repo-1", 2))).toBe(options[1]); // Same PR number, different repository — must not collide with repo-1's entry. expect(findPRAutomationOptionsForPR(options, makePR("repo-2", 1))).toBe(options[2]); }); it("returns all-off defaults for a PR with no stored automation row", () => { const options = [makePRAutomationOptions("repo-1", 1, { auto_fix_enabled: true })]; const result = findPRAutomationOptionsForPR(options, makePR("repo-1", 99, "task-9")); expect(result).toEqual({ task_id: "task-9", repository_id: "repo-1", pr_number: 99, auto_fix_enabled: false, auto_merge_enabled: false, prompt_on_review_requested: false, prompt_on_merged: false, prompt_on_closed: false, created_at: "", updated_at: "", }); }); it("returns all-off defaults when pr_options is undefined", () => { expect(findPRAutomationOptionsForPR(undefined, makePR("repo-1", 1)).auto_fix_enabled).toBe( false, ); }); it("normalizes max rounds and clamps current rounds", () => { expect(normalizeAutoFixMaxRounds(undefined)).toBe(10); expect(normalizeAutoFixMaxRounds(null)).toBe(10); expect(normalizeAutoFixMaxRounds(Number.NaN)).toBe(10); expect(normalizeAutoFixMaxRounds(0)).toBe(1); expect(normalizeAutoFixMaxRounds(-4)).toBe(1); expect(normalizeAutoFixMaxRounds(12.8)).toBe(12); expect(clampAutoFixRound(undefined, 10)).toBe(0); expect(clampAutoFixRound(Number.NaN, 10)).toBe(0); expect(clampAutoFixRound(-2, 10)).toBe(0); expect(clampAutoFixRound(4.9, 10)).toBe(4); expect(clampAutoFixRound(14, 10)).toBe(10); }); });