shipwright-cli 2.4.0 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (169) hide show
  1. package/README.md +16 -11
  2. package/completions/_shipwright +248 -94
  3. package/completions/shipwright.bash +68 -19
  4. package/completions/shipwright.fish +310 -42
  5. package/config/decision-tiers.json +55 -0
  6. package/config/defaults.json +111 -0
  7. package/config/event-schema.json +218 -0
  8. package/config/policy.json +21 -18
  9. package/dashboard/coverage/coverage-summary.json +14 -0
  10. package/dashboard/public/index.html +1 -1
  11. package/dashboard/server.ts +306 -17
  12. package/dashboard/src/components/charts/bar.test.ts +79 -0
  13. package/dashboard/src/components/charts/donut.test.ts +68 -0
  14. package/dashboard/src/components/charts/pipeline-rail.test.ts +117 -0
  15. package/dashboard/src/components/charts/sparkline.test.ts +125 -0
  16. package/dashboard/src/core/api.test.ts +309 -0
  17. package/dashboard/src/core/helpers.test.ts +301 -0
  18. package/dashboard/src/core/router.test.ts +307 -0
  19. package/dashboard/src/core/router.ts +7 -0
  20. package/dashboard/src/core/sse.test.ts +144 -0
  21. package/dashboard/src/views/metrics.test.ts +186 -0
  22. package/dashboard/src/views/overview.test.ts +173 -0
  23. package/dashboard/src/views/pipelines.test.ts +183 -0
  24. package/dashboard/src/views/team.test.ts +253 -0
  25. package/dashboard/vitest.config.ts +14 -5
  26. package/docs/TIPS.md +1 -1
  27. package/docs/patterns/README.md +1 -1
  28. package/package.json +7 -9
  29. package/scripts/adapters/docker-deploy.sh +1 -1
  30. package/scripts/adapters/tmux-adapter.sh +11 -1
  31. package/scripts/adapters/wezterm-adapter.sh +1 -1
  32. package/scripts/check-version-consistency.sh +1 -1
  33. package/scripts/lib/architecture.sh +127 -0
  34. package/scripts/lib/bootstrap.sh +75 -0
  35. package/scripts/lib/compat.sh +89 -6
  36. package/scripts/lib/config.sh +91 -0
  37. package/scripts/lib/daemon-adaptive.sh +3 -3
  38. package/scripts/lib/daemon-dispatch.sh +63 -17
  39. package/scripts/lib/daemon-failure.sh +0 -0
  40. package/scripts/lib/daemon-health.sh +1 -1
  41. package/scripts/lib/daemon-patrol.sh +64 -17
  42. package/scripts/lib/daemon-poll.sh +54 -25
  43. package/scripts/lib/daemon-state.sh +125 -23
  44. package/scripts/lib/daemon-triage.sh +31 -9
  45. package/scripts/lib/decide-autonomy.sh +295 -0
  46. package/scripts/lib/decide-scoring.sh +228 -0
  47. package/scripts/lib/decide-signals.sh +462 -0
  48. package/scripts/lib/fleet-failover.sh +63 -0
  49. package/scripts/lib/helpers.sh +29 -6
  50. package/scripts/lib/pipeline-detection.sh +2 -2
  51. package/scripts/lib/pipeline-github.sh +9 -9
  52. package/scripts/lib/pipeline-intelligence.sh +105 -38
  53. package/scripts/lib/pipeline-quality-checks.sh +17 -16
  54. package/scripts/lib/pipeline-quality.sh +1 -1
  55. package/scripts/lib/pipeline-stages.sh +440 -59
  56. package/scripts/lib/pipeline-state.sh +54 -4
  57. package/scripts/lib/policy.sh +0 -0
  58. package/scripts/lib/test-helpers.sh +247 -0
  59. package/scripts/postinstall.mjs +78 -12
  60. package/scripts/signals/example-collector.sh +36 -0
  61. package/scripts/sw +17 -7
  62. package/scripts/sw-activity.sh +1 -11
  63. package/scripts/sw-adaptive.sh +109 -85
  64. package/scripts/sw-adversarial.sh +4 -14
  65. package/scripts/sw-architecture-enforcer.sh +1 -11
  66. package/scripts/sw-auth.sh +8 -17
  67. package/scripts/sw-autonomous.sh +111 -49
  68. package/scripts/sw-changelog.sh +1 -11
  69. package/scripts/sw-checkpoint.sh +144 -20
  70. package/scripts/sw-ci.sh +2 -12
  71. package/scripts/sw-cleanup.sh +13 -17
  72. package/scripts/sw-code-review.sh +16 -36
  73. package/scripts/sw-connect.sh +5 -12
  74. package/scripts/sw-context.sh +9 -26
  75. package/scripts/sw-cost.sh +17 -18
  76. package/scripts/sw-daemon.sh +76 -71
  77. package/scripts/sw-dashboard.sh +57 -17
  78. package/scripts/sw-db.sh +524 -26
  79. package/scripts/sw-decide.sh +685 -0
  80. package/scripts/sw-decompose.sh +1 -11
  81. package/scripts/sw-deps.sh +15 -25
  82. package/scripts/sw-developer-simulation.sh +1 -11
  83. package/scripts/sw-discovery.sh +138 -30
  84. package/scripts/sw-doc-fleet.sh +7 -17
  85. package/scripts/sw-docs-agent.sh +6 -16
  86. package/scripts/sw-docs.sh +4 -12
  87. package/scripts/sw-doctor.sh +134 -43
  88. package/scripts/sw-dora.sh +11 -19
  89. package/scripts/sw-durable.sh +35 -52
  90. package/scripts/sw-e2e-orchestrator.sh +11 -27
  91. package/scripts/sw-eventbus.sh +115 -115
  92. package/scripts/sw-evidence.sh +114 -30
  93. package/scripts/sw-feedback.sh +3 -13
  94. package/scripts/sw-fix.sh +2 -20
  95. package/scripts/sw-fleet-discover.sh +1 -11
  96. package/scripts/sw-fleet-viz.sh +10 -18
  97. package/scripts/sw-fleet.sh +13 -17
  98. package/scripts/sw-github-app.sh +6 -16
  99. package/scripts/sw-github-checks.sh +1 -11
  100. package/scripts/sw-github-deploy.sh +1 -11
  101. package/scripts/sw-github-graphql.sh +2 -12
  102. package/scripts/sw-guild.sh +1 -11
  103. package/scripts/sw-heartbeat.sh +49 -12
  104. package/scripts/sw-hygiene.sh +45 -43
  105. package/scripts/sw-incident.sh +48 -74
  106. package/scripts/sw-init.sh +35 -37
  107. package/scripts/sw-instrument.sh +1 -11
  108. package/scripts/sw-intelligence.sh +368 -53
  109. package/scripts/sw-jira.sh +5 -14
  110. package/scripts/sw-launchd.sh +2 -12
  111. package/scripts/sw-linear.sh +8 -17
  112. package/scripts/sw-logs.sh +4 -12
  113. package/scripts/sw-loop.sh +905 -104
  114. package/scripts/sw-memory.sh +263 -20
  115. package/scripts/sw-mission-control.sh +2 -12
  116. package/scripts/sw-model-router.sh +73 -34
  117. package/scripts/sw-otel.sh +15 -23
  118. package/scripts/sw-oversight.sh +1 -11
  119. package/scripts/sw-patrol-meta.sh +5 -11
  120. package/scripts/sw-pipeline-composer.sh +7 -17
  121. package/scripts/sw-pipeline-vitals.sh +1 -11
  122. package/scripts/sw-pipeline.sh +550 -122
  123. package/scripts/sw-pm.sh +2 -12
  124. package/scripts/sw-pr-lifecycle.sh +33 -28
  125. package/scripts/sw-predictive.sh +16 -22
  126. package/scripts/sw-prep.sh +6 -16
  127. package/scripts/sw-ps.sh +1 -11
  128. package/scripts/sw-public-dashboard.sh +2 -12
  129. package/scripts/sw-quality.sh +85 -14
  130. package/scripts/sw-reaper.sh +1 -11
  131. package/scripts/sw-recruit.sh +15 -25
  132. package/scripts/sw-regression.sh +11 -21
  133. package/scripts/sw-release-manager.sh +19 -28
  134. package/scripts/sw-release.sh +8 -16
  135. package/scripts/sw-remote.sh +1 -11
  136. package/scripts/sw-replay.sh +48 -44
  137. package/scripts/sw-retro.sh +70 -92
  138. package/scripts/sw-review-rerun.sh +1 -1
  139. package/scripts/sw-scale.sh +174 -41
  140. package/scripts/sw-security-audit.sh +12 -22
  141. package/scripts/sw-self-optimize.sh +239 -23
  142. package/scripts/sw-session.sh +5 -15
  143. package/scripts/sw-setup.sh +8 -18
  144. package/scripts/sw-standup.sh +5 -15
  145. package/scripts/sw-status.sh +32 -23
  146. package/scripts/sw-strategic.sh +129 -13
  147. package/scripts/sw-stream.sh +1 -11
  148. package/scripts/sw-swarm.sh +76 -36
  149. package/scripts/sw-team-stages.sh +10 -20
  150. package/scripts/sw-templates.sh +4 -14
  151. package/scripts/sw-testgen.sh +3 -13
  152. package/scripts/sw-tmux-pipeline.sh +1 -19
  153. package/scripts/sw-tmux-role-color.sh +0 -10
  154. package/scripts/sw-tmux-status.sh +3 -11
  155. package/scripts/sw-tmux.sh +2 -20
  156. package/scripts/sw-trace.sh +1 -19
  157. package/scripts/sw-tracker-github.sh +0 -10
  158. package/scripts/sw-tracker-jira.sh +1 -11
  159. package/scripts/sw-tracker-linear.sh +1 -11
  160. package/scripts/sw-tracker.sh +7 -24
  161. package/scripts/sw-triage.sh +29 -39
  162. package/scripts/sw-upgrade.sh +5 -23
  163. package/scripts/sw-ux.sh +1 -19
  164. package/scripts/sw-webhook.sh +18 -32
  165. package/scripts/sw-widgets.sh +3 -21
  166. package/scripts/sw-worktree.sh +11 -27
  167. package/scripts/update-homebrew-sha.sh +73 -0
  168. package/templates/pipelines/tdd.json +72 -0
  169. package/scripts/sw-pipeline.sh.mock +0 -7
@@ -0,0 +1,117 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { renderPipelineSVG, renderDoraGrades } from "./pipeline-rail";
3
+ import type { PipelineInfo } from "../../types/api";
4
+
5
+ describe("renderPipelineSVG", () => {
6
+ it("renders SVG for an empty pipeline", () => {
7
+ const pipeline = { status: "pending" } as PipelineInfo;
8
+ const svg = renderPipelineSVG(pipeline);
9
+ expect(svg).toContain("<svg");
10
+ expect(svg).toContain("</svg>");
11
+ expect(svg).toContain("pipeline-svg");
12
+ });
13
+
14
+ it("renders completed stages with green fill", () => {
15
+ const pipeline = {
16
+ stagesDone: ["intake", "plan"],
17
+ stage: "build",
18
+ status: "running",
19
+ } as PipelineInfo;
20
+ const svg = renderPipelineSVG(pipeline);
21
+ expect(svg).toContain("#4ade80"); // green for completed
22
+ expect(svg).toContain("#00d4ff"); // cyan for active
23
+ });
24
+
25
+ it("renders failed pipeline with red fill", () => {
26
+ const pipeline = {
27
+ stagesDone: ["intake", "plan"],
28
+ stage: "build",
29
+ status: "failed",
30
+ } as PipelineInfo;
31
+ const svg = renderPipelineSVG(pipeline);
32
+ expect(svg).toContain("#f43f5e"); // red for failed
33
+ });
34
+
35
+ it("renders stage labels", () => {
36
+ const pipeline = {
37
+ stagesDone: [],
38
+ stage: "intake",
39
+ status: "running",
40
+ } as PipelineInfo;
41
+ const svg = renderPipelineSVG(pipeline);
42
+ expect(svg).toContain("intake");
43
+ });
44
+
45
+ it("renders connecting lines between stages", () => {
46
+ const pipeline = { status: "pending" } as PipelineInfo;
47
+ const svg = renderPipelineSVG(pipeline);
48
+ expect(svg).toContain("<line");
49
+ });
50
+
51
+ it("renders animation for active stage", () => {
52
+ const pipeline = {
53
+ stagesDone: [],
54
+ stage: "intake",
55
+ status: "running",
56
+ } as PipelineInfo;
57
+ const svg = renderPipelineSVG(pipeline);
58
+ expect(svg).toContain("animate");
59
+ expect(svg).toContain("stage-node-active");
60
+ });
61
+
62
+ it("uses dashed lines for incomplete connections", () => {
63
+ const pipeline = {
64
+ stagesDone: [],
65
+ stage: "intake",
66
+ status: "running",
67
+ } as PipelineInfo;
68
+ const svg = renderPipelineSVG(pipeline);
69
+ expect(svg).toContain("stroke-dasharray");
70
+ });
71
+ });
72
+
73
+ describe("renderDoraGrades", () => {
74
+ it("returns empty string for null dora", () => {
75
+ expect(renderDoraGrades(null)).toBe("");
76
+ });
77
+
78
+ it("returns empty string for undefined dora", () => {
79
+ expect(renderDoraGrades(undefined)).toBe("");
80
+ });
81
+
82
+ it("renders DORA metrics cards", () => {
83
+ const dora = {
84
+ deploy_freq: { grade: "Elite", value: 7.5, unit: "/week" },
85
+ lead_time: { grade: "High", value: 2.1, unit: "hours" },
86
+ cfr: { grade: "Medium", value: 15.0, unit: "%" },
87
+ mttr: { grade: "Low", value: 24.0, unit: "hours" },
88
+ };
89
+ const html = renderDoraGrades(dora);
90
+ expect(html).toContain("dora-grades-row");
91
+ expect(html).toContain("Deploy Frequency");
92
+ expect(html).toContain("Lead Time");
93
+ expect(html).toContain("Change Failure Rate");
94
+ expect(html).toContain("Mean Time to Recovery");
95
+ expect(html).toContain("Elite");
96
+ expect(html).toContain("dora-elite");
97
+ expect(html).toContain("7.5");
98
+ expect(html).toContain("/week");
99
+ });
100
+
101
+ it("handles missing metrics gracefully", () => {
102
+ const dora = {
103
+ deploy_freq: { grade: "Elite", value: 5.0, unit: "/week" },
104
+ };
105
+ const html = renderDoraGrades(dora);
106
+ expect(html).toContain("Deploy Frequency");
107
+ expect(html).not.toContain("Lead Time");
108
+ });
109
+
110
+ it("handles null value with dash", () => {
111
+ const dora = {
112
+ cfr: { grade: "N/A", value: null as unknown as number, unit: "%" },
113
+ };
114
+ const html = renderDoraGrades(dora);
115
+ expect(html).toContain("\u2014");
116
+ });
117
+ });
@@ -0,0 +1,125 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { renderSparkline, renderSVGLineChart } from "./sparkline";
3
+
4
+ describe("sparkline", () => {
5
+ describe("renderSparkline", () => {
6
+ it("returns empty string for empty data", () => {
7
+ expect(renderSparkline([], "#fff", 100, 40)).toBe("");
8
+ });
9
+
10
+ it("returns empty string for single point", () => {
11
+ expect(renderSparkline([5], "#fff", 100, 40)).toBe("");
12
+ expect(renderSparkline([{ value: 10 }], "#fff", 100, 40)).toBe("");
13
+ });
14
+
15
+ it("returns empty string for null/undefined points", () => {
16
+ expect(
17
+ renderSparkline(null as unknown as number[], "#fff", 100, 40),
18
+ ).toBe("");
19
+ });
20
+
21
+ it("returns SVG with path for multiple points (number array)", () => {
22
+ const svg = renderSparkline([10, 20, 30, 40], "#00d4ff", 100, 40);
23
+ expect(svg).toContain('<svg class="sparkline"');
24
+ expect(svg).toContain('viewBox="0 0 100 40"');
25
+ expect(svg).toContain("<path");
26
+ expect(svg).toContain('stroke="#00d4ff"');
27
+ expect(svg).toContain('stroke-width="1.5"');
28
+ expect(svg).toContain('fill="none"');
29
+ expect(svg).toContain("d=");
30
+ expect(svg).toMatch(/M[\d.]+,[\d.]+ L[\d.]+,[\d.]+/);
31
+ });
32
+
33
+ it("returns SVG with path for object array with value property", () => {
34
+ const svg = renderSparkline(
35
+ [{ value: 5 }, { value: 15 }, { value: 25 }],
36
+ "#7c3aed",
37
+ 80,
38
+ 32,
39
+ );
40
+ expect(svg).toContain('<svg class="sparkline"');
41
+ expect(svg).toContain("<path");
42
+ expect(svg).toContain('stroke="#7c3aed"');
43
+ });
44
+
45
+ it("handles object values with missing value (treats as 0)", () => {
46
+ const svg = renderSparkline([{ value: 0 }, { value: 0 }], "#333", 50, 20);
47
+ expect(svg).toContain('<svg class="sparkline"');
48
+ expect(svg).toContain("<path");
49
+ });
50
+
51
+ it("uses correct dimensions in output", () => {
52
+ const svg = renderSparkline([1, 2, 3], "#000", 200, 60);
53
+ expect(svg).toContain('width="200"');
54
+ expect(svg).toContain('height="60"');
55
+ });
56
+ });
57
+
58
+ describe("renderSVGLineChart", () => {
59
+ it("returns empty-state div for empty points", () => {
60
+ const out = renderSVGLineChart([], "value", "#00d4ff", 100, 40);
61
+ expect(out).toContain('<div class="empty-state">');
62
+ expect(out).toContain("Not enough data");
63
+ });
64
+
65
+ it("returns empty-state div for single point", () => {
66
+ const out = renderSVGLineChart(
67
+ [{ value: 10 }],
68
+ "value",
69
+ "#00d4ff",
70
+ 100,
71
+ 40,
72
+ );
73
+ expect(out).toContain('<div class="empty-state">');
74
+ expect(out).toContain("Not enough data");
75
+ });
76
+
77
+ it("returns empty-state for null/undefined points", () => {
78
+ const out = renderSVGLineChart(
79
+ null as unknown as Record<string, number>[],
80
+ "value",
81
+ "#00d4ff",
82
+ 100,
83
+ 40,
84
+ );
85
+ expect(out).toContain("Not enough data");
86
+ });
87
+
88
+ it("returns SVG with path and grid lines for multiple points", () => {
89
+ const points = [
90
+ { value: 10 },
91
+ { value: 30 },
92
+ { value: 20 },
93
+ { value: 50 },
94
+ ];
95
+ const svg = renderSVGLineChart(points, "value", "#4ade80", 300, 120);
96
+ expect(svg).toContain('<svg class="svg-line-chart"');
97
+ expect(svg).toContain('viewBox="0 0 300 120"');
98
+ expect(svg).toContain("<path");
99
+ expect(svg).toContain('stroke="#4ade80"');
100
+ expect(svg).toContain("<line");
101
+ expect(svg).toContain('stroke="#1a3a6a"');
102
+ });
103
+
104
+ it("uses custom valueKey when provided", () => {
105
+ const points = [{ count: 5 }, { count: 15 }, { count: 25 }];
106
+ const svg = renderSVGLineChart(points, "count", "#ff0000", 200, 80);
107
+ expect(svg).toContain('<svg class="svg-line-chart"');
108
+ expect(svg).toContain("<path");
109
+ expect(svg).toContain('stroke="#ff0000"');
110
+ });
111
+
112
+ it("falls back to value when valueKey missing", () => {
113
+ const points = [{ value: 1 }, { value: 2 }];
114
+ const svg = renderSVGLineChart(points, "missing", "#000", 100, 50);
115
+ expect(svg).toContain("<path");
116
+ });
117
+
118
+ it("handles all-zero max (uses maxVal=1 to avoid div by zero)", () => {
119
+ const points = [{ value: 0 }, { value: 0 }];
120
+ const svg = renderSVGLineChart(points, "value", "#000", 100, 50);
121
+ expect(svg).toContain("<svg");
122
+ expect(svg).not.toContain("Not enough data");
123
+ });
124
+ });
125
+ });
@@ -334,6 +334,315 @@ describe("API Client", () => {
334
334
  });
335
335
  });
336
336
 
337
+ describe("machine health check and join tokens", () => {
338
+ it("machineHealthCheck calls POST", async () => {
339
+ mockFetch.mockReturnValueOnce(jsonResponse({ machine: {} }));
340
+ await api.machineHealthCheck("node-1");
341
+ expect(mockFetch).toHaveBeenCalledWith(
342
+ "/api/machines/node-1/health-check",
343
+ expect.objectContaining({ method: "POST" }),
344
+ );
345
+ });
346
+
347
+ it("fetchJoinTokens calls GET /api/join-token", async () => {
348
+ mockFetch.mockReturnValueOnce(jsonResponse({ tokens: [] }));
349
+ await api.fetchJoinTokens();
350
+ expect(mockFetch).toHaveBeenCalledWith("/api/join-token", undefined);
351
+ });
352
+
353
+ it("generateJoinToken calls POST /api/join-token", async () => {
354
+ mockFetch.mockReturnValueOnce(jsonResponse({ join_cmd: "sw join ..." }));
355
+ await api.generateJoinToken({ label: "test", max_workers: 4 });
356
+ expect(mockFetch).toHaveBeenCalledWith(
357
+ "/api/join-token",
358
+ expect.objectContaining({ method: "POST" }),
359
+ );
360
+ });
361
+ });
362
+
363
+ describe("costs", () => {
364
+ it("fetchCostBreakdown defaults to 7 day period", async () => {
365
+ mockFetch.mockReturnValueOnce(jsonResponse({}));
366
+ await api.fetchCostBreakdown();
367
+ expect(mockFetch).toHaveBeenCalledWith(
368
+ "/api/costs/breakdown?period=7",
369
+ undefined,
370
+ );
371
+ });
372
+
373
+ it("fetchCostTrend defaults to 30 day period", async () => {
374
+ mockFetch.mockReturnValueOnce(jsonResponse({ points: [] }));
375
+ await api.fetchCostTrend();
376
+ expect(mockFetch).toHaveBeenCalledWith(
377
+ "/api/costs/trend?period=30",
378
+ undefined,
379
+ );
380
+ });
381
+ });
382
+
383
+ describe("daemon", () => {
384
+ it("fetchDaemonConfig calls GET /api/daemon/config", async () => {
385
+ mockFetch.mockReturnValueOnce(jsonResponse({}));
386
+ await api.fetchDaemonConfig();
387
+ expect(mockFetch).toHaveBeenCalledWith("/api/daemon/config", undefined);
388
+ });
389
+
390
+ it("daemonControl calls POST /api/daemon/:action", async () => {
391
+ mockFetch.mockReturnValueOnce(jsonResponse({ ok: true }));
392
+ await api.daemonControl("pause");
393
+ expect(mockFetch).toHaveBeenCalledWith(
394
+ "/api/daemon/pause",
395
+ expect.objectContaining({ method: "POST" }),
396
+ );
397
+ });
398
+ });
399
+
400
+ describe("alerts and artifacts", () => {
401
+ it("fetchAlerts calls GET /api/alerts", async () => {
402
+ mockFetch.mockReturnValueOnce(jsonResponse({ alerts: [] }));
403
+ await api.fetchAlerts();
404
+ expect(mockFetch).toHaveBeenCalledWith("/api/alerts", undefined);
405
+ });
406
+
407
+ it("fetchArtifact calls correct endpoint", async () => {
408
+ mockFetch.mockReturnValueOnce(jsonResponse({ content: "..." }));
409
+ await api.fetchArtifact(42, "plan");
410
+ expect(mockFetch).toHaveBeenCalledWith(
411
+ "/api/artifacts/42/plan",
412
+ undefined,
413
+ );
414
+ });
415
+
416
+ it("fetchGitHubStatus calls GET", async () => {
417
+ mockFetch.mockReturnValueOnce(jsonResponse({}));
418
+ await api.fetchGitHubStatus(42);
419
+ expect(mockFetch).toHaveBeenCalledWith("/api/github/42", undefined);
420
+ });
421
+
422
+ it("fetchLogs calls GET", async () => {
423
+ mockFetch.mockReturnValueOnce(jsonResponse({ content: "log" }));
424
+ await api.fetchLogs(42);
425
+ expect(mockFetch).toHaveBeenCalledWith("/api/logs/42", undefined);
426
+ });
427
+ });
428
+
429
+ describe("metrics detail", () => {
430
+ it("fetchStagePerformance defaults to 7 days", async () => {
431
+ mockFetch.mockReturnValueOnce(jsonResponse({ stages: [] }));
432
+ await api.fetchStagePerformance();
433
+ expect(mockFetch).toHaveBeenCalledWith(
434
+ "/api/metrics/stage-performance?period=7",
435
+ undefined,
436
+ );
437
+ });
438
+
439
+ it("fetchBottlenecks calls GET", async () => {
440
+ mockFetch.mockReturnValueOnce(jsonResponse({ bottlenecks: [] }));
441
+ await api.fetchBottlenecks();
442
+ expect(mockFetch).toHaveBeenCalledWith(
443
+ "/api/metrics/bottlenecks",
444
+ undefined,
445
+ );
446
+ });
447
+
448
+ it("fetchThroughputTrend defaults to 30 days", async () => {
449
+ mockFetch.mockReturnValueOnce(jsonResponse({ points: [] }));
450
+ await api.fetchThroughputTrend();
451
+ expect(mockFetch).toHaveBeenCalledWith(
452
+ "/api/metrics/throughput-trend?period=30",
453
+ undefined,
454
+ );
455
+ });
456
+
457
+ it("fetchCapacity calls GET", async () => {
458
+ mockFetch.mockReturnValueOnce(
459
+ jsonResponse({ rate: 2, queue_clear_hours: 1 }),
460
+ );
461
+ await api.fetchCapacity();
462
+ expect(mockFetch).toHaveBeenCalledWith(
463
+ "/api/metrics/capacity",
464
+ undefined,
465
+ );
466
+ });
467
+
468
+ it("fetchDoraTrend defaults to 30 days", async () => {
469
+ mockFetch.mockReturnValueOnce(jsonResponse({}));
470
+ await api.fetchDoraTrend();
471
+ expect(mockFetch).toHaveBeenCalledWith(
472
+ "/api/metrics/dora-trend?period=30",
473
+ undefined,
474
+ );
475
+ });
476
+ });
477
+
478
+ describe("team endpoints", () => {
479
+ it("fetchTeam calls GET /api/team", async () => {
480
+ mockFetch.mockReturnValueOnce(jsonResponse({}));
481
+ await api.fetchTeam();
482
+ expect(mockFetch).toHaveBeenCalledWith("/api/team", undefined);
483
+ });
484
+
485
+ it("fetchTeamActivity returns events array", async () => {
486
+ mockFetch.mockReturnValueOnce(jsonResponse({ events: [{ id: 1 }] }));
487
+ const result = await api.fetchTeamActivity();
488
+ expect(result).toEqual([{ id: 1 }]);
489
+ });
490
+
491
+ it("fetchTeamActivity returns empty array on error", async () => {
492
+ mockFetch.mockReturnValueOnce(errorResponse(500));
493
+ const result = await api.fetchTeamActivity();
494
+ expect(result).toEqual([]);
495
+ });
496
+
497
+ it("createTeamInvite calls POST /api/team/invite", async () => {
498
+ mockFetch.mockReturnValueOnce(
499
+ jsonResponse({ token: "abc", url: "...", expires_at: "..." }),
500
+ );
501
+ await api.createTeamInvite({ expires_hours: 24 });
502
+ expect(mockFetch).toHaveBeenCalledWith(
503
+ "/api/team/invite",
504
+ expect.objectContaining({ method: "POST" }),
505
+ );
506
+ });
507
+ });
508
+
509
+ describe("pipeline test results and learnings", () => {
510
+ it("fetchPipelineTestResults calls GET", async () => {
511
+ mockFetch.mockReturnValueOnce(jsonResponse({}));
512
+ await api.fetchPipelineTestResults(42);
513
+ expect(mockFetch).toHaveBeenCalledWith(
514
+ "/api/pipeline/42/test-results",
515
+ undefined,
516
+ );
517
+ });
518
+
519
+ it("fetchGlobalLearnings calls GET", async () => {
520
+ mockFetch.mockReturnValueOnce(jsonResponse({ learnings: [] }));
521
+ await api.fetchGlobalLearnings();
522
+ expect(mockFetch).toHaveBeenCalledWith("/api/memory/global", undefined);
523
+ });
524
+
525
+ it("fetchPatrol returns findings on success", async () => {
526
+ mockFetch.mockReturnValueOnce(jsonResponse({ findings: [{ id: 1 }] }));
527
+ const result = await api.fetchPatrol();
528
+ expect(result).toEqual({ findings: [{ id: 1 }] });
529
+ });
530
+ });
531
+
532
+ describe("integration and DB endpoints", () => {
533
+ it("fetchLinearStatus calls GET", async () => {
534
+ mockFetch.mockReturnValueOnce(jsonResponse({}));
535
+ await api.fetchLinearStatus();
536
+ expect(mockFetch).toHaveBeenCalledWith("/api/linear/status", undefined);
537
+ });
538
+
539
+ it("fetchDbEvents with defaults", async () => {
540
+ mockFetch.mockReturnValueOnce(jsonResponse({ events: [], source: "db" }));
541
+ await api.fetchDbEvents();
542
+ expect(mockFetch).toHaveBeenCalledWith(
543
+ "/api/db/events?since=0&limit=200",
544
+ undefined,
545
+ );
546
+ });
547
+
548
+ it("fetchDbJobs without status filter", async () => {
549
+ mockFetch.mockReturnValueOnce(jsonResponse({ jobs: [], source: "db" }));
550
+ await api.fetchDbJobs();
551
+ expect(mockFetch).toHaveBeenCalledWith("/api/db/jobs", undefined);
552
+ });
553
+
554
+ it("fetchDbJobs with status filter", async () => {
555
+ mockFetch.mockReturnValueOnce(jsonResponse({ jobs: [], source: "db" }));
556
+ await api.fetchDbJobs("active");
557
+ expect(mockFetch).toHaveBeenCalledWith(
558
+ "/api/db/jobs?status=active",
559
+ undefined,
560
+ );
561
+ });
562
+
563
+ it("fetchDbCostsToday calls GET", async () => {
564
+ mockFetch.mockReturnValueOnce(jsonResponse({}));
565
+ await api.fetchDbCostsToday();
566
+ expect(mockFetch).toHaveBeenCalledWith("/api/db/costs/today", undefined);
567
+ });
568
+
569
+ it("fetchDbHeartbeats calls GET", async () => {
570
+ mockFetch.mockReturnValueOnce(
571
+ jsonResponse({ heartbeats: [], source: "db" }),
572
+ );
573
+ await api.fetchDbHeartbeats();
574
+ expect(mockFetch).toHaveBeenCalledWith("/api/db/heartbeats", undefined);
575
+ });
576
+
577
+ it("fetchDbHealth calls GET", async () => {
578
+ mockFetch.mockReturnValueOnce(jsonResponse({}));
579
+ await api.fetchDbHealth();
580
+ expect(mockFetch).toHaveBeenCalledWith("/api/db/health", undefined);
581
+ });
582
+ });
583
+
584
+ describe("audit, quality gates, approvals, notifications", () => {
585
+ it("fetchAuditLog calls GET", async () => {
586
+ mockFetch.mockReturnValueOnce(jsonResponse({ entries: [] }));
587
+ await api.fetchAuditLog();
588
+ expect(mockFetch).toHaveBeenCalledWith("/api/audit-log", undefined);
589
+ });
590
+
591
+ it("fetchQualityGates calls GET", async () => {
592
+ mockFetch.mockReturnValueOnce(jsonResponse({ enabled: true, rules: [] }));
593
+ await api.fetchQualityGates();
594
+ expect(mockFetch).toHaveBeenCalledWith("/api/quality-gates", undefined);
595
+ });
596
+
597
+ it("fetchPipelineQuality calls GET", async () => {
598
+ mockFetch.mockReturnValueOnce(
599
+ jsonResponse({ quality: {}, gates: {}, results: [] }),
600
+ );
601
+ await api.fetchPipelineQuality(42);
602
+ expect(mockFetch).toHaveBeenCalledWith(
603
+ "/api/pipeline/42/quality",
604
+ undefined,
605
+ );
606
+ });
607
+
608
+ it("fetchApprovalGates calls GET", async () => {
609
+ mockFetch.mockReturnValueOnce(
610
+ jsonResponse({ enabled: true, stages: [], pending: [] }),
611
+ );
612
+ await api.fetchApprovalGates();
613
+ expect(mockFetch).toHaveBeenCalledWith("/api/approval-gates", undefined);
614
+ });
615
+
616
+ it("updateApprovalGates calls POST", async () => {
617
+ mockFetch.mockReturnValueOnce(jsonResponse({ ok: true }));
618
+ await api.updateApprovalGates({ enabled: true, stages: ["review"] });
619
+ expect(mockFetch).toHaveBeenCalledWith(
620
+ "/api/approval-gates",
621
+ expect.objectContaining({ method: "POST" }),
622
+ );
623
+ });
624
+
625
+ it("fetchNotificationConfig calls GET", async () => {
626
+ mockFetch.mockReturnValueOnce(
627
+ jsonResponse({ enabled: true, webhooks: [] }),
628
+ );
629
+ await api.fetchNotificationConfig();
630
+ expect(mockFetch).toHaveBeenCalledWith(
631
+ "/api/notifications/config",
632
+ undefined,
633
+ );
634
+ });
635
+
636
+ it("testNotification calls POST", async () => {
637
+ mockFetch.mockReturnValueOnce(jsonResponse({ ok: true }));
638
+ await api.testNotification();
639
+ expect(mockFetch).toHaveBeenCalledWith(
640
+ "/api/notifications/test",
641
+ expect.objectContaining({ method: "POST" }),
642
+ );
643
+ });
644
+ });
645
+
337
646
  describe("error handling", () => {
338
647
  it("throws on non-ok response", async () => {
339
648
  mockFetch.mockReturnValueOnce(errorResponse(404, { error: "Not found" }));