CodeIssuesDiscussionsWikiPull RequestsProjectsCommitsActionsReleasesContributorsPulse● GatesSecuritySettingsDeploymentsPipelineInsightsAgents✨ Explain✨ Ask AI✨ Workspace✨ Spec✨ Tests▓ Debt Map✨ NL Search🏛 Archaeology
Blame · Line-by-line history
codeowners.test.ts
Each line is annotated with the commit that last touched it. Click any SHA to jump to that commit and see the surrounding change.
| 91b054e | 1 | /** |
| 2 | * Tests for src/lib/codeowners.ts's `requiredOwnersApproved()` — the fix for | |
| 3 | * CODEOWNERS approval being advisory-only. Before this, `reviewersForChangedFiles()` | |
| 4 | * was only used to auto-*request* reviewers on PR open; nothing at merge time | |
| 5 | * checked whether a CODEOWNERS-designated reviewer actually approved. | |
| 6 | * | |
| 7 | * `getCodeownersForRepo` and `loadApprovedUsernames` are passed in via the | |
| 8 | * injectable `deps` param (same DI rationale as push-workflow-sync.ts / | |
| 9 | * pr-workflow-sync.ts): the real implementations touch ../git/repository and | |
| 10 | * ../db respectively, both imported by dozens of unrelated test files, so a | |
| 11 | * global mock.module() on either would leak across the whole `bun test` run. | |
| 12 | * `matchOwners` / `expandOwnerTokens` (pure + already covered by | |
| 13 | * green-ecosystem.test.ts) run for real — only usernames (no team tokens) | |
| 14 | * are used here so `expandOwnerTokens` never touches the DB. | |
| 15 | */ | |
| 16 | ||
| 17 | import { describe, expect, it } from "bun:test"; | |
| 18 | import { requiredOwnersApproved } from "../lib/codeowners"; | |
| 19 | import type { OwnerRule } from "../lib/codeowners"; | |
| 20 | ||
| 21 | function deps(rules: OwnerRule[], approved: Set<string>) { | |
| 22 | return { | |
| 23 | getCodeownersForRepo: async () => rules, | |
| 24 | loadApprovedUsernames: async () => approved, | |
| 25 | }; | |
| 26 | } | |
| 27 | ||
| 28 | describe("requiredOwnersApproved", () => { | |
| 29 | it("is satisfied when there is no CODEOWNERS file", async () => { | |
| 30 | const result = await requiredOwnersApproved( | |
| 31 | "acme", | |
| 32 | "widgets", | |
| 33 | "main", | |
| 34 | "pr-1", | |
| 35 | ["src/api/foo.ts"], | |
| 36 | deps([], new Set()) | |
| 37 | ); | |
| 38 | expect(result).toEqual({ satisfied: true, missingOwners: [] }); | |
| 39 | }); | |
| 40 | ||
| 41 | it("is satisfied when CODEOWNERS exists but matches none of the changed files", async () => { | |
| 42 | const rules: OwnerRule[] = [{ pattern: "docs/**", owners: ["alice"] }]; | |
| 43 | const result = await requiredOwnersApproved( | |
| 44 | "acme", | |
| 45 | "widgets", | |
| 46 | "main", | |
| 47 | "pr-1", | |
| 48 | ["src/api/foo.ts"], | |
| 49 | deps(rules, new Set()) | |
| 50 | ); | |
| 51 | expect(result).toEqual({ satisfied: true, missingOwners: [] }); | |
| 52 | }); | |
| 53 | ||
| 54 | it("blocks when the required owner has not approved", async () => { | |
| 55 | const rules: OwnerRule[] = [{ pattern: "src/api/**", owners: ["alice"] }]; | |
| 56 | const result = await requiredOwnersApproved( | |
| 57 | "acme", | |
| 58 | "widgets", | |
| 59 | "main", | |
| 60 | "pr-1", | |
| 61 | ["src/api/foo.ts"], | |
| 62 | deps(rules, new Set()) | |
| 63 | ); | |
| 64 | expect(result.satisfied).toBe(false); | |
| 65 | expect(result.missingOwners).toEqual(["alice"]); | |
| 66 | }); | |
| 67 | ||
| 68 | it("is satisfied when the required owner has approved", async () => { | |
| 69 | const rules: OwnerRule[] = [{ pattern: "src/api/**", owners: ["alice"] }]; | |
| 70 | const result = await requiredOwnersApproved( | |
| 71 | "acme", | |
| 72 | "widgets", | |
| 73 | "main", | |
| 74 | "pr-1", | |
| 75 | ["src/api/foo.ts"], | |
| 76 | deps(rules, new Set(["alice"])) | |
| 77 | ); | |
| 78 | expect(result).toEqual({ satisfied: true, missingOwners: [] }); | |
| 79 | }); | |
| 80 | ||
| 81 | it("reports only the owners who haven't approved out of several", async () => { | |
| 82 | const rules: OwnerRule[] = [ | |
| 83 | { pattern: "src/api/**", owners: ["alice", "bob"] }, | |
| 84 | ]; | |
| 85 | const result = await requiredOwnersApproved( | |
| 86 | "acme", | |
| 87 | "widgets", | |
| 88 | "main", | |
| 89 | "pr-1", | |
| 90 | ["src/api/foo.ts"], | |
| 91 | deps(rules, new Set(["alice"])) | |
| 92 | ); | |
| 93 | expect(result.satisfied).toBe(false); | |
| 94 | expect(result.missingOwners).toEqual(["bob"]); | |
| 95 | }); | |
| 96 | ||
| 97 | it("fails open (satisfied: true) when getCodeownersForRepo throws", async () => { | |
| 98 | const result = await requiredOwnersApproved( | |
| 99 | "acme", | |
| 100 | "widgets", | |
| 101 | "main", | |
| 102 | "pr-1", | |
| 103 | ["src/api/foo.ts"], | |
| 104 | { | |
| 105 | getCodeownersForRepo: async () => { | |
| 106 | throw new Error("git boom"); | |
| 107 | }, | |
| 108 | loadApprovedUsernames: async () => new Set(), | |
| 109 | } | |
| 110 | ); | |
| 111 | expect(result).toEqual({ satisfied: true, missingOwners: [] }); | |
| 112 | }); | |
| 113 | ||
| 114 | it("fails open (satisfied: true) when loadApprovedUsernames throws", async () => { | |
| 115 | const rules: OwnerRule[] = [{ pattern: "src/api/**", owners: ["alice"] }]; | |
| 116 | const result = await requiredOwnersApproved( | |
| 117 | "acme", | |
| 118 | "widgets", | |
| 119 | "main", | |
| 120 | "pr-1", | |
| 121 | ["src/api/foo.ts"], | |
| 122 | { | |
| 123 | getCodeownersForRepo: async () => rules, | |
| 124 | loadApprovedUsernames: async () => { | |
| 125 | throw new Error("db boom"); | |
| 126 | }, | |
| 127 | } | |
| 128 | ); | |
| 129 | expect(result).toEqual({ satisfied: true, missingOwners: [] }); | |
| 130 | }); | |
| 131 | ||
| 132 | it("is satisfied when no files changed match any rule across multiple paths", async () => { | |
| 133 | const rules: OwnerRule[] = [{ pattern: "/docs", owners: ["carol"] }]; | |
| 134 | const result = await requiredOwnersApproved( | |
| 135 | "acme", | |
| 136 | "widgets", | |
| 137 | "main", | |
| 138 | "pr-1", | |
| 139 | ["src/a.ts", "src/b.ts"], | |
| 140 | deps(rules, new Set()) | |
| 141 | ); | |
| 142 | expect(result).toEqual({ satisfied: true, missingOwners: [] }); | |
| 143 | }); | |
| 144 | }); |