import { describe, test, expect } from "vitest"; import { DECLARABLE_MARKER, type Declarable } from "@intentius/chant/declarable"; import { applyForgejoDialect, DEFAULT_RUNNER_LABELS } from "./dialect"; // ── Mock entities (github-tagged, as the dialect reuses github entities) ── class MockJob implements Declarable { readonly [DECLARABLE_MARKER] = true as const; readonly lexicon = "github"; readonly entityType = "GitHub::Actions::Job"; readonly kind = "resource" as const; readonly props: Record; constructor(props: Record = {}) { this.props = props; } } class MockWorkflow implements Declarable { readonly [DECLARABLE_MARKER] = true as const; readonly lexicon = "github"; readonly entityType = "GitHub::Actions::Workflow"; readonly kind = "resource" as const; readonly props: Record; constructor(props: Record = {}) { this.props = props; } } function entityMap(...pairs: Array<[string, Declarable]>): Map { return new Map(pairs); } describe("applyForgejoDialect — dropped keys", () => { test("drops workflow-level permissions without warning", () => { const wf = new MockWorkflow({ name: "CI", permissions: { contents: "read" } }); const { entities, warnings } = applyForgejoDialect(entityMap(["workflow", wf])); const props = (entities.get("workflow") as MockWorkflow).props; expect("permissions" in props).toBe(false); expect(props.name).toBe("CI"); expect(warnings.filter((w) => w.includes("permissions"))).toHaveLength(0); }); test("keeps only id-token from job permissions, so a job can request an OIDC token", () => { const job = new MockJob({ "runs-on": "ubuntu-latest", permissions: { "id-token": "write", contents: "read" }, steps: [{ name: "a", run: "echo a" }], }); const { entities, warnings } = applyForgejoDialect(entityMap(["job", job])); const props = (entities.get("job") as MockJob).props; expect(props.permissions).toEqual({ "id-token": "write" }); expect(props["runs-on"]).toBe("docker"); expect(warnings.filter((w) => w.includes("permissions"))).toHaveLength(0); }); test("drops job permissions that carry no id-token, and the read-all shorthand", () => { const job = new MockJob({ "runs-on": "ubuntu-latest", permissions: { contents: "read", "pull-requests": "write" }, steps: [{ name: "a", run: "echo a" }], }); const other = new MockJob({ "runs-on": "ubuntu-latest", permissions: "read-all" }); const { entities, warnings } = applyForgejoDialect(entityMap(["job", job], ["other", other])); expect("permissions" in (entities.get("job") as MockJob).props).toBe(false); expect("permissions" in (entities.get("other") as MockJob).props).toBe(false); expect(warnings).toHaveLength(0); }); test("leaves a step input named permissions alone", () => { const job = new MockJob({ "runs-on": "ubuntu-latest", steps: [{ uses: "some-org/custom-action@v1", with: { permissions: "everyone" } }], }); const { entities } = applyForgejoDialect(entityMap(["job", job])); const steps = (entities.get("job") as MockJob).props.steps as Array<{ with: Record }>; expect(steps[0].with.permissions).toBe("everyone"); }); test("drops job and step continue-on-error, one warning each", () => { const job = new MockJob({ "runs-on": "ubuntu-latest", "continue-on-error": true, steps: [ { name: "a", run: "echo a", "continue-on-error": true }, { name: "b", run: "echo b" }, ], }); const { entities, warnings } = applyForgejoDialect(entityMap(["build", job])); const props = (entities.get("build") as MockJob).props; expect(props["continue-on-error"]).toBeUndefined(); const steps = props.steps as Array>; expect(steps[0]["continue-on-error"]).toBeUndefined(); expect(steps[0].name).toBe("a"); expect(warnings.filter((w) => w.includes("continue-on-error"))).toHaveLength(2); }); test("also catches the camelCase spelling (continueOnError)", () => { const job = new MockJob({ continueOnError: true, "runs-on": "ubuntu-latest" }); const { entities, warnings } = applyForgejoDialect(entityMap(["build", job])); const props = (entities.get("build") as MockJob).props; expect(props.continueOnError).toBeUndefined(); expect(warnings.filter((w) => w.includes("continue-on-error"))).toHaveLength(1); }); test("does not mutate the original entity", () => { const wf = new MockWorkflow({ name: "CI", permissions: { contents: "read" } }); applyForgejoDialect(entityMap(["workflow", wf])); expect(wf.props.permissions).toEqual({ contents: "read" }); }); }); describe("applyForgejoDialect — runner labels", () => { test("maps ubuntu-latest to the default Forgejo label", () => { const job = new MockJob({ "runs-on": "ubuntu-latest" }); const { entities, warnings } = applyForgejoDialect(entityMap(["build", job])); expect((entities.get("build") as MockJob).props["runs-on"]).toBe(DEFAULT_RUNNER_LABELS["ubuntu-latest"]); expect(warnings).toHaveLength(0); }); test("a project override changes the emitted label", () => { const job = new MockJob({ "runs-on": "ubuntu-latest" }); const { entities } = applyForgejoDialect(entityMap(["build", job]), { runnerLabels: { "ubuntu-latest": "big-runner" }, }); expect((entities.get("build") as MockJob).props["runs-on"]).toBe("big-runner"); }); test("an unmapped label passes through unchanged (reported by WFJ011, not here)", () => { const job = new MockJob({ "runs-on": "macos-latest" }); const { entities, warnings } = applyForgejoDialect(entityMap(["build", job])); expect((entities.get("build") as MockJob).props["runs-on"]).toBe("macos-latest"); expect(warnings.filter((w) => w.includes("macos-latest"))).toHaveLength(0); }); test("remaps every label in an array form, leaving unmapped ones untouched", () => { const job = new MockJob({ "runs-on": ["ubuntu-latest", "self-hosted"] }); const { entities } = applyForgejoDialect(entityMap(["build", job])); expect((entities.get("build") as MockJob).props["runs-on"]).toEqual(["docker", "self-hosted"]); }); });