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

issue-dependencies.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.

issue-dependencies.test.tsBlame125 lines · 1 contributor
bed6b57Claude1/**
2 * Block J14 — Issue dependencies. Pure helpers + route-auth smokes.
3 */
4
5import { describe, it, expect } from "bun:test";
6import app from "../app";
7import { __internal } from "../lib/issue-dependencies";
8
9const { wouldCreateCycle, summariseBlockers } = __internal;
10
11describe("issue-dependencies — wouldCreateCycle", () => {
12 it("rejects self-references", () => {
13 expect(wouldCreateCycle([], "a", "a")).toBe(true);
14 });
15
16 it("empty graph — no cycle", () => {
17 expect(wouldCreateCycle([], "a", "b")).toBe(false);
18 });
19
20 it("detects a direct back-edge", () => {
21 // b already blocks a → adding (a blocks b) would close the loop.
22 const edges = [{ blockerIssueId: "b", blockedIssueId: "a" }];
23 expect(wouldCreateCycle(edges, "a", "b")).toBe(true);
24 });
25
26 it("detects a transitive cycle (a → b → c, then c blocks a?)", () => {
27 // Existing: a blocks b, b blocks c. Proposed: c blocks a → cycle.
28 const edges = [
29 { blockerIssueId: "a", blockedIssueId: "b" },
30 { blockerIssueId: "b", blockedIssueId: "c" },
31 ];
32 expect(wouldCreateCycle(edges, "c", "a")).toBe(true);
33 });
34
35 it("allows unrelated edges", () => {
36 const edges = [
37 { blockerIssueId: "a", blockedIssueId: "b" },
38 { blockerIssueId: "c", blockedIssueId: "d" },
39 ];
40 expect(wouldCreateCycle(edges, "e", "f")).toBe(false);
41 expect(wouldCreateCycle(edges, "a", "c")).toBe(false);
42 });
43
44 it("allows adding an edge that does not close any path", () => {
45 // a blocks b. Adding b blocks c is fine — no cycle.
46 const edges = [{ blockerIssueId: "a", blockedIssueId: "b" }];
47 expect(wouldCreateCycle(edges, "b", "c")).toBe(false);
48 });
49
50 it("detects deeply transitive cycle (length 4)", () => {
51 // a → b → c → d, then proposing d blocks a → cycle.
52 const edges = [
53 { blockerIssueId: "a", blockedIssueId: "b" },
54 { blockerIssueId: "b", blockedIssueId: "c" },
55 { blockerIssueId: "c", blockedIssueId: "d" },
56 ];
57 expect(wouldCreateCycle(edges, "d", "a")).toBe(true);
58 });
59
60 it("diamond shapes do not count as cycles", () => {
61 // a blocks b, a blocks c, b blocks d, c blocks d. Adding e blocks a is fine.
62 const edges = [
63 { blockerIssueId: "a", blockedIssueId: "b" },
64 { blockerIssueId: "a", blockedIssueId: "c" },
65 { blockerIssueId: "b", blockedIssueId: "d" },
66 { blockerIssueId: "c", blockedIssueId: "d" },
67 ];
68 expect(wouldCreateCycle(edges, "e", "a")).toBe(false);
69 });
70});
71
72describe("issue-dependencies — summariseBlockers", () => {
73 it("returns zeros for empty input", () => {
74 expect(summariseBlockers([])).toEqual({ open: 0, closed: 0, total: 0 });
75 });
76
77 it("counts open and closed blockers", () => {
78 expect(
79 summariseBlockers([
80 { blockerIssueId: "a", blockerState: "open" },
81 { blockerIssueId: "b", blockerState: "closed" },
82 { blockerIssueId: "c", blockerState: "open" },
83 ])
84 ).toEqual({ open: 2, closed: 1, total: 3 });
85 });
86
87 it("treats any non-open state as closed (defensive)", () => {
88 expect(
89 summariseBlockers([
90 { blockerIssueId: "a", blockerState: "merged" },
91 { blockerIssueId: "b", blockerState: "open" },
92 ])
93 ).toEqual({ open: 1, closed: 1, total: 2 });
94 });
95});
96
97describe("issue-dependencies — routes", () => {
98 it("POST /:o/:r/issues/:n/dependencies requires auth", async () => {
99 const res = await app.request(
100 "/alice/nope/issues/1/dependencies",
101 { method: "POST", body: "blockerNumber=2" }
102 );
103 expect([302, 401].includes(res.status)).toBe(true);
104 });
105
106 it("POST remove route requires auth", async () => {
107 const res = await app.request(
108 "/alice/nope/issues/1/dependencies/blockers/xyz/remove",
109 { method: "POST" }
110 );
111 expect([302, 401].includes(res.status)).toBe(true);
112 });
113
114 it("POST with invalid bearer → 401 JSON", async () => {
115 const res = await app.request(
116 "/alice/nope/issues/1/dependencies",
117 {
118 method: "POST",
119 headers: { authorization: "Bearer glc_garbage" },
120 body: "blockerNumber=2",
121 }
122 );
123 expect(res.status).toBe(401);
124 });
125});