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