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
|
import { describe, it, expect } from "bun:test";
import app from "../app";
import { __internal } from "../lib/issue-dependencies";
const { wouldCreateCycle, summariseBlockers } = __internal;
describe("issue-dependencies — wouldCreateCycle", () => {
it("rejects self-references", () => {
expect(wouldCreateCycle([], "a", "a")).toBe(true);
});
it("empty graph — no cycle", () => {
expect(wouldCreateCycle([], "a", "b")).toBe(false);
});
it("detects a direct back-edge", () => {
const edges = [{ blockerIssueId: "b", blockedIssueId: "a" }];
expect(wouldCreateCycle(edges, "a", "b")).toBe(true);
});
it("detects a transitive cycle (a → b → c, then c blocks a?)", () => {
const edges = [
{ blockerIssueId: "a", blockedIssueId: "b" },
{ blockerIssueId: "b", blockedIssueId: "c" },
];
expect(wouldCreateCycle(edges, "c", "a")).toBe(true);
});
it("allows unrelated edges", () => {
const edges = [
{ blockerIssueId: "a", blockedIssueId: "b" },
{ blockerIssueId: "c", blockedIssueId: "d" },
];
expect(wouldCreateCycle(edges, "e", "f")).toBe(false);
expect(wouldCreateCycle(edges, "a", "c")).toBe(false);
});
it("allows adding an edge that does not close any path", () => {
const edges = [{ blockerIssueId: "a", blockedIssueId: "b" }];
expect(wouldCreateCycle(edges, "b", "c")).toBe(false);
});
it("detects deeply transitive cycle (length 4)", () => {
const edges = [
{ blockerIssueId: "a", blockedIssueId: "b" },
{ blockerIssueId: "b", blockedIssueId: "c" },
{ blockerIssueId: "c", blockedIssueId: "d" },
];
expect(wouldCreateCycle(edges, "d", "a")).toBe(true);
});
it("diamond shapes do not count as cycles", () => {
const edges = [
{ blockerIssueId: "a", blockedIssueId: "b" },
{ blockerIssueId: "a", blockedIssueId: "c" },
{ blockerIssueId: "b", blockedIssueId: "d" },
{ blockerIssueId: "c", blockedIssueId: "d" },
];
expect(wouldCreateCycle(edges, "e", "a")).toBe(false);
});
});
describe("issue-dependencies — summariseBlockers", () => {
it("returns zeros for empty input", () => {
expect(summariseBlockers([])).toEqual({ open: 0, closed: 0, total: 0 });
});
it("counts open and closed blockers", () => {
expect(
summariseBlockers([
{ blockerIssueId: "a", blockerState: "open" },
{ blockerIssueId: "b", blockerState: "closed" },
{ blockerIssueId: "c", blockerState: "open" },
])
).toEqual({ open: 2, closed: 1, total: 3 });
});
it("treats any non-open state as closed (defensive)", () => {
expect(
summariseBlockers([
{ blockerIssueId: "a", blockerState: "merged" },
{ blockerIssueId: "b", blockerState: "open" },
])
).toEqual({ open: 1, closed: 1, total: 2 });
});
});
describe("issue-dependencies — routes", () => {
it("POST /:o/:r/issues/:n/dependencies requires auth", async () => {
const res = await app.request(
"/alice/nope/issues/1/dependencies",
{ method: "POST", body: "blockerNumber=2" }
);
expect([302, 401].includes(res.status)).toBe(true);
});
it("POST remove route requires auth", async () => {
const res = await app.request(
"/alice/nope/issues/1/dependencies/blockers/xyz/remove",
{ method: "POST" }
);
expect([302, 401].includes(res.status)).toBe(true);
});
it("POST with invalid bearer → 401 JSON", async () => {
const res = await app.request(
"/alice/nope/issues/1/dependencies",
{
method: "POST",
headers: { authorization: "Bearer glc_garbage" },
body: "blockerNumber=2",
}
);
expect(res.status).toBe(401);
});
});
|