Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
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.

codeowners.test.tsBlame144 lines · 1 contributor
91b054eccanty labs1/**
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
17import { describe, expect, it } from "bun:test";
18import { requiredOwnersApproved } from "../lib/codeowners";
19import type { OwnerRule } from "../lib/codeowners";
20
21function deps(rules: OwnerRule[], approved: Set<string>) {
22 return {
23 getCodeownersForRepo: async () => rules,
24 loadApprovedUsernames: async () => approved,
25 };
26}
27
28describe("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});