shipwright-cli 2.4.0 → 3.0.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 (161) hide show
  1. package/README.md +16 -11
  2. package/completions/_shipwright +1 -1
  3. package/completions/shipwright.bash +3 -8
  4. package/completions/shipwright.fish +1 -1
  5. package/config/defaults.json +111 -0
  6. package/config/event-schema.json +81 -0
  7. package/config/policy.json +13 -18
  8. package/dashboard/coverage/coverage-summary.json +14 -0
  9. package/dashboard/public/index.html +1 -1
  10. package/dashboard/server.ts +306 -17
  11. package/dashboard/src/components/charts/bar.test.ts +79 -0
  12. package/dashboard/src/components/charts/donut.test.ts +68 -0
  13. package/dashboard/src/components/charts/pipeline-rail.test.ts +117 -0
  14. package/dashboard/src/components/charts/sparkline.test.ts +125 -0
  15. package/dashboard/src/core/api.test.ts +309 -0
  16. package/dashboard/src/core/helpers.test.ts +301 -0
  17. package/dashboard/src/core/router.test.ts +307 -0
  18. package/dashboard/src/core/router.ts +7 -0
  19. package/dashboard/src/core/sse.test.ts +144 -0
  20. package/dashboard/src/views/metrics.test.ts +186 -0
  21. package/dashboard/src/views/overview.test.ts +173 -0
  22. package/dashboard/src/views/pipelines.test.ts +183 -0
  23. package/dashboard/src/views/team.test.ts +253 -0
  24. package/dashboard/vitest.config.ts +14 -5
  25. package/docs/TIPS.md +1 -1
  26. package/docs/patterns/README.md +1 -1
  27. package/package.json +5 -7
  28. package/scripts/adapters/docker-deploy.sh +1 -1
  29. package/scripts/adapters/tmux-adapter.sh +11 -1
  30. package/scripts/adapters/wezterm-adapter.sh +1 -1
  31. package/scripts/check-version-consistency.sh +1 -1
  32. package/scripts/lib/architecture.sh +126 -0
  33. package/scripts/lib/bootstrap.sh +75 -0
  34. package/scripts/lib/compat.sh +89 -6
  35. package/scripts/lib/config.sh +91 -0
  36. package/scripts/lib/daemon-adaptive.sh +3 -3
  37. package/scripts/lib/daemon-dispatch.sh +39 -16
  38. package/scripts/lib/daemon-health.sh +1 -1
  39. package/scripts/lib/daemon-patrol.sh +24 -12
  40. package/scripts/lib/daemon-poll.sh +37 -25
  41. package/scripts/lib/daemon-state.sh +115 -23
  42. package/scripts/lib/daemon-triage.sh +30 -8
  43. package/scripts/lib/fleet-failover.sh +63 -0
  44. package/scripts/lib/helpers.sh +30 -6
  45. package/scripts/lib/pipeline-detection.sh +2 -2
  46. package/scripts/lib/pipeline-github.sh +9 -9
  47. package/scripts/lib/pipeline-intelligence.sh +85 -35
  48. package/scripts/lib/pipeline-quality-checks.sh +16 -16
  49. package/scripts/lib/pipeline-quality.sh +1 -1
  50. package/scripts/lib/pipeline-stages.sh +242 -28
  51. package/scripts/lib/pipeline-state.sh +40 -4
  52. package/scripts/lib/test-helpers.sh +247 -0
  53. package/scripts/postinstall.mjs +3 -11
  54. package/scripts/sw +10 -4
  55. package/scripts/sw-activity.sh +1 -11
  56. package/scripts/sw-adaptive.sh +109 -85
  57. package/scripts/sw-adversarial.sh +4 -14
  58. package/scripts/sw-architecture-enforcer.sh +1 -11
  59. package/scripts/sw-auth.sh +8 -17
  60. package/scripts/sw-autonomous.sh +111 -49
  61. package/scripts/sw-changelog.sh +1 -11
  62. package/scripts/sw-checkpoint.sh +144 -20
  63. package/scripts/sw-ci.sh +2 -12
  64. package/scripts/sw-cleanup.sh +13 -17
  65. package/scripts/sw-code-review.sh +16 -36
  66. package/scripts/sw-connect.sh +5 -12
  67. package/scripts/sw-context.sh +9 -26
  68. package/scripts/sw-cost.sh +6 -16
  69. package/scripts/sw-daemon.sh +75 -70
  70. package/scripts/sw-dashboard.sh +57 -17
  71. package/scripts/sw-db.sh +506 -15
  72. package/scripts/sw-decompose.sh +1 -11
  73. package/scripts/sw-deps.sh +15 -25
  74. package/scripts/sw-developer-simulation.sh +1 -11
  75. package/scripts/sw-discovery.sh +112 -30
  76. package/scripts/sw-doc-fleet.sh +7 -17
  77. package/scripts/sw-docs-agent.sh +6 -16
  78. package/scripts/sw-docs.sh +4 -12
  79. package/scripts/sw-doctor.sh +134 -43
  80. package/scripts/sw-dora.sh +11 -19
  81. package/scripts/sw-durable.sh +35 -52
  82. package/scripts/sw-e2e-orchestrator.sh +11 -27
  83. package/scripts/sw-eventbus.sh +115 -115
  84. package/scripts/sw-evidence.sh +114 -30
  85. package/scripts/sw-feedback.sh +3 -13
  86. package/scripts/sw-fix.sh +2 -20
  87. package/scripts/sw-fleet-discover.sh +1 -11
  88. package/scripts/sw-fleet-viz.sh +10 -18
  89. package/scripts/sw-fleet.sh +13 -17
  90. package/scripts/sw-github-app.sh +6 -16
  91. package/scripts/sw-github-checks.sh +1 -11
  92. package/scripts/sw-github-deploy.sh +1 -11
  93. package/scripts/sw-github-graphql.sh +2 -12
  94. package/scripts/sw-guild.sh +1 -11
  95. package/scripts/sw-heartbeat.sh +49 -12
  96. package/scripts/sw-hygiene.sh +45 -43
  97. package/scripts/sw-incident.sh +48 -74
  98. package/scripts/sw-init.sh +35 -37
  99. package/scripts/sw-instrument.sh +1 -11
  100. package/scripts/sw-intelligence.sh +362 -51
  101. package/scripts/sw-jira.sh +5 -14
  102. package/scripts/sw-launchd.sh +2 -12
  103. package/scripts/sw-linear.sh +8 -17
  104. package/scripts/sw-logs.sh +4 -12
  105. package/scripts/sw-loop.sh +641 -90
  106. package/scripts/sw-memory.sh +243 -17
  107. package/scripts/sw-mission-control.sh +2 -12
  108. package/scripts/sw-model-router.sh +73 -34
  109. package/scripts/sw-otel.sh +11 -21
  110. package/scripts/sw-oversight.sh +1 -11
  111. package/scripts/sw-patrol-meta.sh +5 -11
  112. package/scripts/sw-pipeline-composer.sh +7 -17
  113. package/scripts/sw-pipeline-vitals.sh +1 -11
  114. package/scripts/sw-pipeline.sh +478 -122
  115. package/scripts/sw-pm.sh +2 -12
  116. package/scripts/sw-pr-lifecycle.sh +27 -25
  117. package/scripts/sw-predictive.sh +16 -22
  118. package/scripts/sw-prep.sh +6 -16
  119. package/scripts/sw-ps.sh +1 -11
  120. package/scripts/sw-public-dashboard.sh +2 -12
  121. package/scripts/sw-quality.sh +77 -10
  122. package/scripts/sw-reaper.sh +1 -11
  123. package/scripts/sw-recruit.sh +15 -25
  124. package/scripts/sw-regression.sh +11 -21
  125. package/scripts/sw-release-manager.sh +19 -28
  126. package/scripts/sw-release.sh +8 -16
  127. package/scripts/sw-remote.sh +1 -11
  128. package/scripts/sw-replay.sh +48 -44
  129. package/scripts/sw-retro.sh +70 -92
  130. package/scripts/sw-review-rerun.sh +1 -1
  131. package/scripts/sw-scale.sh +109 -32
  132. package/scripts/sw-security-audit.sh +12 -22
  133. package/scripts/sw-self-optimize.sh +239 -23
  134. package/scripts/sw-session.sh +3 -13
  135. package/scripts/sw-setup.sh +8 -18
  136. package/scripts/sw-standup.sh +5 -15
  137. package/scripts/sw-status.sh +32 -23
  138. package/scripts/sw-strategic.sh +129 -13
  139. package/scripts/sw-stream.sh +1 -11
  140. package/scripts/sw-swarm.sh +76 -36
  141. package/scripts/sw-team-stages.sh +10 -20
  142. package/scripts/sw-templates.sh +4 -14
  143. package/scripts/sw-testgen.sh +3 -13
  144. package/scripts/sw-tmux-pipeline.sh +1 -19
  145. package/scripts/sw-tmux-role-color.sh +0 -10
  146. package/scripts/sw-tmux-status.sh +3 -11
  147. package/scripts/sw-tmux.sh +2 -20
  148. package/scripts/sw-trace.sh +1 -19
  149. package/scripts/sw-tracker-github.sh +0 -10
  150. package/scripts/sw-tracker-jira.sh +1 -11
  151. package/scripts/sw-tracker-linear.sh +1 -11
  152. package/scripts/sw-tracker.sh +7 -24
  153. package/scripts/sw-triage.sh +24 -34
  154. package/scripts/sw-upgrade.sh +5 -23
  155. package/scripts/sw-ux.sh +1 -19
  156. package/scripts/sw-webhook.sh +18 -32
  157. package/scripts/sw-widgets.sh +3 -21
  158. package/scripts/sw-worktree.sh +11 -27
  159. package/scripts/update-homebrew-sha.sh +67 -0
  160. package/templates/pipelines/tdd.json +72 -0
  161. package/scripts/sw-pipeline.sh.mock +0 -7
@@ -0,0 +1,144 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
+ import { SSEClient } from "./sse";
3
+
4
+ const EventSourceOpen = 1;
5
+ const EventSourceClosed = 2;
6
+
7
+ describe("SSEClient", () => {
8
+ let mockEventSource: {
9
+ close: ReturnType<typeof vi.fn>;
10
+ readyState: number;
11
+ onmessage: ((e: { data: string }) => void) | null;
12
+ onerror: (() => void) | null;
13
+ };
14
+ let EventSourceConstructor: ReturnType<typeof vi.fn>;
15
+
16
+ beforeEach(() => {
17
+ mockEventSource = {
18
+ close: vi.fn(),
19
+ readyState: EventSourceClosed,
20
+ onmessage: null,
21
+ onerror: null,
22
+ };
23
+ EventSourceConstructor = vi.fn(function (this: unknown) {
24
+ return mockEventSource;
25
+ }) as ReturnType<typeof vi.fn> & { OPEN: number };
26
+ EventSourceConstructor.OPEN = EventSourceOpen;
27
+ EventSourceConstructor.CLOSED = EventSourceClosed;
28
+ EventSourceConstructor.CONNECTING = 0;
29
+ vi.stubGlobal("EventSource", EventSourceConstructor);
30
+ });
31
+
32
+ afterEach(() => {
33
+ vi.unstubAllGlobals();
34
+ });
35
+
36
+ describe("constructor", () => {
37
+ it("stores url and callbacks", () => {
38
+ const onMessage = vi.fn();
39
+ const onError = vi.fn();
40
+ const client = new SSEClient(
41
+ "https://example.com/events",
42
+ onMessage,
43
+ onError,
44
+ );
45
+ expect(client).toBeDefined();
46
+ // Verify connect uses stored values
47
+ client.connect();
48
+ expect(EventSourceConstructor).toHaveBeenCalledWith(
49
+ "https://example.com/events",
50
+ );
51
+ });
52
+ });
53
+
54
+ describe("connect", () => {
55
+ it("creates EventSource with url", () => {
56
+ const client = new SSEClient("/api/logs/42/stream", vi.fn());
57
+ client.connect();
58
+ expect(EventSourceConstructor).toHaveBeenCalledWith(
59
+ "/api/logs/42/stream",
60
+ );
61
+ });
62
+
63
+ it("closes existing connection before reconnecting", () => {
64
+ const client = new SSEClient("/api/stream", vi.fn());
65
+ client.connect();
66
+ const firstES = EventSourceConstructor.mock.results[0].value;
67
+ client.connect();
68
+ expect(firstES.close).toHaveBeenCalled();
69
+ });
70
+
71
+ it("registers onmessage handler that forwards to callback", () => {
72
+ const onMessage = vi.fn();
73
+ const client = new SSEClient("/api/stream", onMessage);
74
+ client.connect();
75
+
76
+ expect(mockEventSource.onmessage).toBeDefined();
77
+ mockEventSource.onmessage!({ data: "hello world" });
78
+ expect(onMessage).toHaveBeenCalledWith("hello world");
79
+ });
80
+
81
+ it("registers onerror handler when onError provided", () => {
82
+ const onError = vi.fn();
83
+ const client = new SSEClient("/api/stream", vi.fn(), onError);
84
+ client.connect();
85
+
86
+ expect(mockEventSource.onerror).toBeDefined();
87
+ mockEventSource.onerror!();
88
+ expect(onError).toHaveBeenCalled();
89
+ });
90
+ });
91
+
92
+ describe("close", () => {
93
+ it("closes EventSource and sets to null", () => {
94
+ const client = new SSEClient("/api/stream", vi.fn());
95
+ client.connect();
96
+ client.close();
97
+ expect(mockEventSource.close).toHaveBeenCalled();
98
+ // eventSource is private, but isConnected should reflect closed state
99
+ mockEventSource.readyState = EventSourceClosed;
100
+ expect(client.isConnected()).toBe(false);
101
+ });
102
+
103
+ it("is safe to call when not connected", () => {
104
+ const client = new SSEClient("/api/stream", vi.fn());
105
+ expect(() => client.close()).not.toThrow();
106
+ });
107
+ });
108
+
109
+ describe("isConnected", () => {
110
+ it("returns false when not connected", () => {
111
+ const client = new SSEClient("/api/stream", vi.fn());
112
+ expect(client.isConnected()).toBe(false);
113
+ });
114
+
115
+ it("returns false when EventSource is closed", () => {
116
+ const client = new SSEClient("/api/stream", vi.fn());
117
+ client.connect();
118
+ mockEventSource.readyState = EventSourceClosed;
119
+ expect(client.isConnected()).toBe(false);
120
+ });
121
+
122
+ it("returns true when EventSource is OPEN", () => {
123
+ const client = new SSEClient("/api/stream", vi.fn());
124
+ client.connect();
125
+ mockEventSource.readyState = EventSourceOpen;
126
+ expect(client.isConnected()).toBe(true);
127
+ });
128
+ });
129
+
130
+ describe("message handling", () => {
131
+ it("receives multiple messages", () => {
132
+ const onMessage = vi.fn();
133
+ const client = new SSEClient("/api/stream", onMessage);
134
+ client.connect();
135
+
136
+ mockEventSource.onmessage!({ data: "msg1" });
137
+ mockEventSource.onmessage!({ data: "msg2" });
138
+
139
+ expect(onMessage).toHaveBeenCalledTimes(2);
140
+ expect(onMessage).toHaveBeenNthCalledWith(1, "msg1");
141
+ expect(onMessage).toHaveBeenNthCalledWith(2, "msg2");
142
+ });
143
+ });
144
+ });
@@ -0,0 +1,186 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
+ import { store } from "../core/state";
3
+ import type { FleetState, MetricsData } from "../types/api";
4
+
5
+ vi.mock("../core/api", () => ({
6
+ fetchMetricsHistory: vi.fn().mockResolvedValue({
7
+ success_rate: 0.95,
8
+ avg_duration_s: 600,
9
+ throughput_per_hour: 2.5,
10
+ total_completed: 100,
11
+ total_failed: 5,
12
+ stage_durations: {},
13
+ daily_counts: [],
14
+ }),
15
+ fetchCostBreakdown: vi.fn().mockResolvedValue({}),
16
+ fetchCostTrend: vi.fn().mockResolvedValue({ points: [] }),
17
+ fetchDoraTrend: vi.fn().mockResolvedValue({}),
18
+ fetchStagePerformance: vi.fn().mockResolvedValue({ stages: [] }),
19
+ fetchBottlenecks: vi.fn().mockResolvedValue({ bottlenecks: [] }),
20
+ fetchThroughputTrend: vi.fn().mockResolvedValue({ points: [] }),
21
+ fetchCapacity: vi.fn().mockResolvedValue({ rate: 1, queue_clear_hours: 2 }),
22
+ }));
23
+
24
+ function createMetricsDOM(): void {
25
+ const ids = [
26
+ "metric-donut-wrap",
27
+ "metric-avg-duration",
28
+ "metric-throughput",
29
+ "metric-total-completed",
30
+ "metric-total-failed",
31
+ "stage-breakdown",
32
+ "daily-chart",
33
+ "dora-grades-container",
34
+ ];
35
+ for (const id of ids) {
36
+ const el = document.createElement("div");
37
+ el.id = id;
38
+ document.body.appendChild(el);
39
+ }
40
+ }
41
+
42
+ function cleanupMetricsDOM(): void {
43
+ const ids = [
44
+ "metric-donut-wrap",
45
+ "metric-avg-duration",
46
+ "metric-throughput",
47
+ "metric-total-completed",
48
+ "metric-total-failed",
49
+ "stage-breakdown",
50
+ "daily-chart",
51
+ "dora-grades-container",
52
+ ];
53
+ ids.forEach((id) => document.getElementById(id)?.remove());
54
+ }
55
+
56
+ function emptyFleetState(): FleetState {
57
+ return {
58
+ timestamp: new Date().toISOString(),
59
+ daemon: {
60
+ running: false,
61
+ pid: null,
62
+ uptime_s: 0,
63
+ maxParallel: 0,
64
+ pollInterval: 5,
65
+ },
66
+ pipelines: [],
67
+ queue: [],
68
+ events: [],
69
+ scale: {},
70
+ metrics: {},
71
+ agents: [],
72
+ machines: [],
73
+ cost: { today_spent: 0, daily_budget: 0, pct_used: 0 },
74
+ dora: {} as any,
75
+ };
76
+ }
77
+
78
+ describe("MetricsView", () => {
79
+ beforeEach(() => {
80
+ store.set("firstRender", false);
81
+ store.set("metricsCache", null);
82
+ createMetricsDOM();
83
+ });
84
+
85
+ afterEach(() => {
86
+ cleanupMetricsDOM();
87
+ vi.clearAllMocks();
88
+ });
89
+
90
+ it("renders metric cards with cached data", async () => {
91
+ const { metricsView } = await import("./metrics");
92
+ const metricsData: MetricsData = {
93
+ success_rate: 0.92,
94
+ avg_duration_s: 420,
95
+ throughput_per_hour: 3.5,
96
+ total_completed: 50,
97
+ total_failed: 4,
98
+ stage_durations: { plan: 60, code: 300, review: 120 },
99
+ daily_counts: [{ date: "2025-02-17", completed: 5, failed: 1 }],
100
+ dora_grades: {} as any,
101
+ };
102
+ store.set("metricsCache", metricsData);
103
+ const data = emptyFleetState();
104
+ metricsView.render(data);
105
+ const avgEl = document.getElementById("metric-avg-duration");
106
+ const tpEl = document.getElementById("metric-throughput");
107
+ const tcEl = document.getElementById("metric-total-completed");
108
+ expect(avgEl?.textContent).toBeTruthy();
109
+ expect(tpEl?.textContent).toBe("3.50");
110
+ expect(tcEl?.textContent).toContain("50");
111
+ });
112
+
113
+ it("handles missing data gracefully", async () => {
114
+ const { metricsView } = await import("./metrics");
115
+ const emptyMetrics: MetricsData = {
116
+ success_rate: 0,
117
+ avg_duration_s: 0,
118
+ throughput_per_hour: 0,
119
+ total_completed: 0,
120
+ total_failed: 0,
121
+ stage_durations: {},
122
+ daily_counts: [],
123
+ dora_grades: {} as any,
124
+ };
125
+ store.set("metricsCache", emptyMetrics);
126
+ const data = emptyFleetState();
127
+ expect(() => metricsView.render(data)).not.toThrow();
128
+ const donutEl = document.getElementById("metric-donut-wrap");
129
+ expect(donutEl?.innerHTML).toBeTruthy();
130
+ });
131
+
132
+ it("formats numbers correctly for totals", async () => {
133
+ const { metricsView } = await import("./metrics");
134
+ const metricsData: MetricsData = {
135
+ success_rate: 1,
136
+ avg_duration_s: 0,
137
+ throughput_per_hour: 0,
138
+ total_completed: 1234,
139
+ total_failed: 10,
140
+ stage_durations: {},
141
+ daily_counts: [],
142
+ dora_grades: {} as any,
143
+ };
144
+ store.set("metricsCache", metricsData);
145
+ const data = emptyFleetState();
146
+ metricsView.render(data);
147
+ const tcEl = document.getElementById("metric-total-completed");
148
+ const failedEl = document.getElementById("metric-total-failed");
149
+ expect(tcEl?.textContent).toContain("1,234");
150
+ expect(failedEl?.textContent).toContain("10");
151
+ });
152
+
153
+ it("renders stage breakdown when stage_durations provided", async () => {
154
+ const { metricsView } = await import("./metrics");
155
+ const metricsData: MetricsData = {
156
+ success_rate: 0,
157
+ avg_duration_s: 0,
158
+ throughput_per_hour: 0,
159
+ total_completed: 0,
160
+ total_failed: 0,
161
+ stage_durations: { plan: 120, code: 400 },
162
+ daily_counts: [],
163
+ dora_grades: {} as any,
164
+ };
165
+ store.set("metricsCache", metricsData);
166
+ const data = emptyFleetState();
167
+ metricsView.render(data);
168
+ const breakdownEl = document.getElementById("stage-breakdown");
169
+ expect(breakdownEl?.innerHTML).toContain("plan");
170
+ expect(breakdownEl?.innerHTML).toContain("code");
171
+ });
172
+
173
+ it("shows empty state when no metrics cache", async () => {
174
+ const { metricsView } = await import("./metrics");
175
+ store.set("metricsCache", null);
176
+ const data = emptyFleetState();
177
+ metricsView.render(data);
178
+ expect(() => metricsView.render(data)).not.toThrow();
179
+ });
180
+
181
+ it("init and destroy do not throw", async () => {
182
+ const { metricsView } = await import("./metrics");
183
+ expect(() => metricsView.init()).not.toThrow();
184
+ expect(() => metricsView.destroy()).not.toThrow();
185
+ });
186
+ });
@@ -0,0 +1,173 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
+ import { store } from "../core/state";
3
+ import type { FleetState } from "../types/api";
4
+
5
+ vi.mock("../core/api", () => ({
6
+ fetchQueueDetailed: vi.fn().mockResolvedValue({ items: [] }),
7
+ }));
8
+
9
+ vi.mock("./pipelines", () => ({
10
+ fetchPipelineDetail: vi.fn(),
11
+ }));
12
+
13
+ vi.mock("../components/header", () => ({
14
+ renderCostTicker: vi.fn(),
15
+ }));
16
+
17
+ function createOverviewDOM(): void {
18
+ const ids = [
19
+ "stat-status",
20
+ "status-dot",
21
+ "stat-active",
22
+ "stat-active-bar",
23
+ "stat-queue",
24
+ "stat-queue-sub",
25
+ "stat-completed",
26
+ "stat-failed-sub",
27
+ "active-pipelines",
28
+ "queue-list",
29
+ "activity-feed",
30
+ "res-cpu-bar",
31
+ "res-cpu-info",
32
+ "res-mem-bar",
33
+ "res-mem-info",
34
+ "res-budget-bar",
35
+ "res-budget-info",
36
+ "resource-constraint",
37
+ "machines-section",
38
+ "machines-grid",
39
+ ];
40
+ for (const id of ids) {
41
+ if (!document.getElementById(id)) {
42
+ const el = document.createElement("div");
43
+ el.id = id;
44
+ document.body.appendChild(el);
45
+ }
46
+ }
47
+ }
48
+
49
+ function cleanupOverviewDOM(): void {
50
+ const ids = [
51
+ "stat-status",
52
+ "status-dot",
53
+ "stat-active",
54
+ "stat-active-bar",
55
+ "stat-queue",
56
+ "stat-queue-sub",
57
+ "stat-completed",
58
+ "stat-failed-sub",
59
+ "active-pipelines",
60
+ "queue-list",
61
+ "activity-feed",
62
+ "res-cpu-bar",
63
+ "res-cpu-info",
64
+ "res-mem-bar",
65
+ "res-mem-info",
66
+ "res-budget-bar",
67
+ "res-budget-info",
68
+ "resource-constraint",
69
+ "machines-section",
70
+ "machines-grid",
71
+ ];
72
+ ids.forEach((id) => document.getElementById(id)?.remove());
73
+ }
74
+
75
+ function emptyFleetState(): FleetState {
76
+ return {
77
+ timestamp: new Date().toISOString(),
78
+ daemon: {
79
+ running: false,
80
+ pid: null,
81
+ uptime_s: 0,
82
+ maxParallel: 0,
83
+ pollInterval: 5,
84
+ },
85
+ pipelines: [],
86
+ queue: [],
87
+ events: [],
88
+ scale: {},
89
+ metrics: {},
90
+ agents: [],
91
+ machines: [],
92
+ cost: { today_spent: 0, daily_budget: 0, pct_used: 0 },
93
+ dora: {} as any,
94
+ };
95
+ }
96
+
97
+ describe("OverviewView", () => {
98
+ beforeEach(() => {
99
+ store.set("firstRender", false);
100
+ createOverviewDOM();
101
+ });
102
+
103
+ afterEach(() => {
104
+ cleanupOverviewDOM();
105
+ vi.clearAllMocks();
106
+ });
107
+
108
+ it("renders without crashing when given empty data", async () => {
109
+ const { overviewView } = await import("./overview");
110
+ const data = emptyFleetState();
111
+ expect(() => overviewView.render(data)).not.toThrow();
112
+ });
113
+
114
+ it("renders pipeline summary section with empty pipelines", async () => {
115
+ const { overviewView } = await import("./overview");
116
+ const data = emptyFleetState();
117
+ overviewView.render(data);
118
+ const container = document.getElementById("active-pipelines");
119
+ expect(container).toBeTruthy();
120
+ expect(container!.innerHTML).toContain("No active pipelines");
121
+ });
122
+
123
+ it("renders pipeline cards when pipelines exist", async () => {
124
+ const { overviewView } = await import("./overview");
125
+ const data = emptyFleetState();
126
+ data.pipelines = [
127
+ {
128
+ issue: 42,
129
+ title: "Fix bug",
130
+ stage: "code",
131
+ stagesDone: ["plan"],
132
+ elapsed_s: 120,
133
+ iteration: 2,
134
+ maxIterations: 20,
135
+ },
136
+ ];
137
+ overviewView.render(data);
138
+ const container = document.getElementById("active-pipelines");
139
+ expect(container).toBeTruthy();
140
+ expect(container!.innerHTML).toContain("#42");
141
+ expect(container!.innerHTML).toContain("Fix bug");
142
+ expect(container!.innerHTML).toContain("pipeline-card");
143
+ });
144
+
145
+ it("handles null/undefined state gracefully", async () => {
146
+ const { overviewView } = await import("./overview");
147
+ const data = emptyFleetState();
148
+ (data as any).pipelines = null;
149
+ (data as any).queue = undefined;
150
+ (data as any).events = undefined;
151
+ expect(() => overviewView.render(data)).not.toThrow();
152
+ const pipelinesEl = document.getElementById("active-pipelines");
153
+ expect(pipelinesEl!.innerHTML).toContain("No active pipelines");
154
+ });
155
+
156
+ it("renders queue empty state", async () => {
157
+ const { overviewView } = await import("./overview");
158
+ const data = emptyFleetState();
159
+ overviewView.render(data);
160
+ const queueEl = document.getElementById("queue-list");
161
+ expect(queueEl).toBeTruthy();
162
+ expect(queueEl!.innerHTML).toContain("Queue clear");
163
+ });
164
+
165
+ it("renders activity empty state", async () => {
166
+ const { overviewView } = await import("./overview");
167
+ const data = emptyFleetState();
168
+ overviewView.render(data);
169
+ const activityEl = document.getElementById("activity-feed");
170
+ expect(activityEl).toBeTruthy();
171
+ expect(activityEl!.innerHTML).toContain("Awaiting events");
172
+ });
173
+ });
@@ -0,0 +1,183 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
+ import { store } from "../core/state";
3
+ import type { FleetState } from "../types/api";
4
+
5
+ vi.mock("../core/api", () => ({
6
+ fetchPipelineDetail: vi.fn().mockResolvedValue({}),
7
+ fetchPipelineReasoning: vi.fn().mockResolvedValue({ reasoning: [] }),
8
+ fetchPipelineFailures: vi.fn().mockResolvedValue({ failures: [] }),
9
+ fetchArtifact: vi.fn().mockResolvedValue({ content: "" }),
10
+ fetchGitHubStatus: vi.fn().mockResolvedValue({ configured: false }),
11
+ fetchPipelineQuality: vi.fn().mockResolvedValue({ results: [] }),
12
+ fetchApprovalGates: vi.fn().mockResolvedValue({ enabled: false }),
13
+ fetchLogs: vi.fn().mockResolvedValue({ content: "" }),
14
+ }));
15
+
16
+ vi.mock("../components/modal", () => ({
17
+ updateBulkToolbar: vi.fn(),
18
+ }));
19
+
20
+ function createPipelinesDOM(): void {
21
+ const tbody = document.createElement("tbody");
22
+ tbody.id = "pipeline-table-body";
23
+ document.body.appendChild(tbody);
24
+
25
+ const filters = document.createElement("div");
26
+ filters.id = "pipeline-filters";
27
+ const chipAll = document.createElement("span");
28
+ chipAll.className = "filter-chip active";
29
+ chipAll.setAttribute("data-filter", "all");
30
+ filters.appendChild(chipAll);
31
+ document.body.appendChild(filters);
32
+
33
+ const closeBtn = document.createElement("button");
34
+ closeBtn.id = "detail-panel-close";
35
+ document.body.appendChild(closeBtn);
36
+
37
+ const selectAll = document.createElement("input");
38
+ selectAll.id = "pipeline-select-all";
39
+ selectAll.type = "checkbox";
40
+ document.body.appendChild(selectAll);
41
+
42
+ const panel = document.createElement("div");
43
+ panel.id = "pipeline-detail-panel";
44
+ const title = document.createElement("div");
45
+ title.id = "detail-panel-title";
46
+ const body = document.createElement("div");
47
+ body.id = "detail-panel-body";
48
+ panel.appendChild(title);
49
+ panel.appendChild(body);
50
+ document.body.appendChild(panel);
51
+ }
52
+
53
+ function cleanupPipelinesDOM(): void {
54
+ [
55
+ "pipeline-table-body",
56
+ "pipeline-filters",
57
+ "detail-panel-close",
58
+ "pipeline-select-all",
59
+ "pipeline-detail-panel",
60
+ ].forEach((id) => document.getElementById(id)?.remove());
61
+ }
62
+
63
+ function emptyFleetState(): FleetState {
64
+ return {
65
+ timestamp: new Date().toISOString(),
66
+ daemon: {
67
+ running: false,
68
+ pid: null,
69
+ uptime_s: 0,
70
+ maxParallel: 0,
71
+ pollInterval: 5,
72
+ },
73
+ pipelines: [],
74
+ queue: [],
75
+ events: [],
76
+ scale: {},
77
+ metrics: {},
78
+ agents: [],
79
+ machines: [],
80
+ cost: { today_spent: 0, daily_budget: 0, pct_used: 0 },
81
+ dora: {} as any,
82
+ };
83
+ }
84
+
85
+ describe("PipelinesView", () => {
86
+ beforeEach(() => {
87
+ store.set("pipelineFilter", "all");
88
+ store.set("selectedPipelineIssue", null);
89
+ store.set("selectedIssues", {});
90
+ store.set("fleetState", emptyFleetState());
91
+ createPipelinesDOM();
92
+ });
93
+
94
+ afterEach(() => {
95
+ cleanupPipelinesDOM();
96
+ vi.clearAllMocks();
97
+ });
98
+
99
+ it("renders pipeline list from mock data", async () => {
100
+ const { pipelinesView } = await import("./pipelines");
101
+ pipelinesView.init();
102
+ const data = emptyFleetState();
103
+ data.pipelines = [
104
+ {
105
+ issue: 100,
106
+ title: "Add feature X",
107
+ stage: "code",
108
+ stagesDone: ["plan", "design"],
109
+ elapsed_s: 300,
110
+ iteration: 3,
111
+ maxIterations: 20,
112
+ },
113
+ ];
114
+ pipelinesView.render(data);
115
+ const tbody = document.getElementById("pipeline-table-body");
116
+ expect(tbody).toBeTruthy();
117
+ expect(tbody!.innerHTML).toContain("#100");
118
+ expect(tbody!.innerHTML).toContain("Add feature X");
119
+ expect(tbody!.innerHTML).toContain("ACTIVE");
120
+ });
121
+
122
+ it("renders empty state when no pipelines", async () => {
123
+ const { pipelinesView } = await import("./pipelines");
124
+ pipelinesView.init();
125
+ const data = emptyFleetState();
126
+ pipelinesView.render(data);
127
+ const tbody = document.getElementById("pipeline-table-body");
128
+ expect(tbody).toBeTruthy();
129
+ expect(tbody!.innerHTML).toContain("No pipelines match filter");
130
+ });
131
+
132
+ it("handles various pipeline statuses from events", async () => {
133
+ const { pipelinesView } = await import("./pipelines");
134
+ pipelinesView.init();
135
+ const data = emptyFleetState();
136
+ data.pipelines = [];
137
+ data.events = [
138
+ {
139
+ type: "pipeline.completed",
140
+ issue: 50,
141
+ issueTitle: "Done",
142
+ duration_s: 600,
143
+ },
144
+ {
145
+ type: "pipeline.failed",
146
+ issue: 51,
147
+ issueTitle: "Failed",
148
+ duration_s: 120,
149
+ },
150
+ ];
151
+ pipelinesView.render(data);
152
+ const tbody = document.getElementById("pipeline-table-body");
153
+ expect(tbody!.innerHTML).toContain("#50");
154
+ expect(tbody!.innerHTML).toContain("#51");
155
+ expect(tbody!.innerHTML).toContain("COMPLETED");
156
+ expect(tbody!.innerHTML).toContain("FAILED");
157
+ });
158
+
159
+ it("filters by status when pipelineFilter is set", async () => {
160
+ const { pipelinesView } = await import("./pipelines");
161
+ store.set("pipelineFilter", "active");
162
+ pipelinesView.init();
163
+ const data = emptyFleetState();
164
+ data.pipelines = [
165
+ {
166
+ issue: 1,
167
+ title: "Active",
168
+ stage: "code",
169
+ stagesDone: [],
170
+ elapsed_s: 60,
171
+ iteration: 1,
172
+ maxIterations: 20,
173
+ },
174
+ ];
175
+ data.events = [
176
+ { type: "pipeline.completed", issue: 2, issueTitle: "Done" },
177
+ ];
178
+ pipelinesView.render(data);
179
+ const tbody = document.getElementById("pipeline-table-body");
180
+ expect(tbody!.innerHTML).toContain("#1");
181
+ expect(tbody!.innerHTML).not.toContain("#2");
182
+ });
183
+ });