shipwright-cli 2.2.2 → 2.3.1
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.
- package/README.md +12 -11
- package/dashboard/public/index.html +224 -8
- package/dashboard/public/styles.css +1078 -4
- package/dashboard/server.ts +1100 -15
- package/dashboard/src/canvas/interactions.ts +74 -0
- package/dashboard/src/canvas/layout.ts +85 -0
- package/dashboard/src/canvas/overlays.ts +117 -0
- package/dashboard/src/canvas/particles.ts +105 -0
- package/dashboard/src/canvas/renderer.ts +191 -0
- package/dashboard/src/components/charts/bar.ts +54 -0
- package/dashboard/src/components/charts/donut.ts +25 -0
- package/dashboard/src/components/charts/pipeline-rail.ts +105 -0
- package/dashboard/src/components/charts/sparkline.ts +82 -0
- package/dashboard/src/components/header.ts +616 -0
- package/dashboard/src/components/modal.ts +413 -0
- package/dashboard/src/components/terminal.ts +144 -0
- package/dashboard/src/core/api.test.ts +362 -0
- package/dashboard/src/core/api.ts +381 -0
- package/dashboard/src/core/helpers.ts +118 -0
- package/dashboard/src/core/router.test.ts +266 -0
- package/dashboard/src/core/router.ts +190 -0
- package/dashboard/src/core/sse.ts +38 -0
- package/dashboard/src/core/state.test.ts +235 -0
- package/dashboard/src/core/state.ts +150 -0
- package/dashboard/src/core/ws.test.ts +216 -0
- package/dashboard/src/core/ws.ts +143 -0
- package/dashboard/src/design/icons.test.ts +105 -0
- package/dashboard/src/design/icons.ts +131 -0
- package/dashboard/src/design/tokens.test.ts +204 -0
- package/dashboard/src/design/tokens.ts +160 -0
- package/dashboard/src/main.ts +68 -0
- package/dashboard/src/types/api.ts +337 -0
- package/dashboard/src/views/activity.ts +185 -0
- package/dashboard/src/views/agent-cockpit.ts +236 -0
- package/dashboard/src/views/agents.ts +72 -0
- package/dashboard/src/views/fleet-map.ts +299 -0
- package/dashboard/src/views/insights.ts +298 -0
- package/dashboard/src/views/machines.ts +162 -0
- package/dashboard/src/views/metrics.ts +420 -0
- package/dashboard/src/views/overview.ts +409 -0
- package/dashboard/src/views/pipeline-theater.ts +219 -0
- package/dashboard/src/views/pipelines.ts +595 -0
- package/dashboard/src/views/team.ts +362 -0
- package/dashboard/src/views/timeline.ts +389 -0
- package/dashboard/tsconfig.json +21 -0
- package/dashboard/vitest.config.ts +27 -0
- package/docs/AGI-WHATS-NEXT.md +15 -15
- package/package.json +16 -2
- package/scripts/lib/helpers.sh +30 -0
- package/scripts/lib/pipeline-quality-checks.sh +1 -1
- package/scripts/lib/pipeline-stages.sh +59 -0
- package/scripts/sw +86 -167
- package/scripts/sw-activity.sh +1 -1
- package/scripts/sw-adaptive.sh +1 -1
- package/scripts/sw-adversarial.sh +1 -1
- package/scripts/sw-architecture-enforcer.sh +1 -1
- package/scripts/sw-auth.sh +14 -6
- package/scripts/sw-autonomous.sh +230 -13
- package/scripts/sw-changelog.sh +2 -2
- package/scripts/sw-checkpoint.sh +1 -1
- package/scripts/sw-ci.sh +1 -1
- package/scripts/sw-cleanup.sh +1 -1
- package/scripts/sw-code-review.sh +1 -1
- package/scripts/sw-connect.sh +1 -1
- package/scripts/sw-context.sh +1 -1
- package/scripts/sw-cost.sh +1 -1
- package/scripts/sw-daemon.sh +2 -2
- package/scripts/sw-dashboard.sh +1 -1
- package/scripts/sw-db.sh +1 -1
- package/scripts/sw-decompose.sh +1 -1
- package/scripts/sw-deps.sh +1 -1
- package/scripts/sw-developer-simulation.sh +1 -1
- package/scripts/sw-discovery.sh +1 -1
- package/scripts/sw-doc-fleet.sh +1 -1
- package/scripts/sw-docs-agent.sh +1 -1
- package/scripts/sw-docs.sh +1 -1
- package/scripts/sw-doctor.sh +8 -1
- package/scripts/sw-dora.sh +1 -1
- package/scripts/sw-durable.sh +1 -1
- package/scripts/sw-e2e-orchestrator.sh +1 -1
- package/scripts/sw-eventbus.sh +1 -1
- package/scripts/sw-feedback.sh +1 -1
- package/scripts/sw-fix.sh +6 -5
- package/scripts/sw-fleet-discover.sh +1 -1
- package/scripts/sw-fleet-viz.sh +1 -1
- package/scripts/sw-fleet.sh +1 -1
- package/scripts/sw-github-app.sh +5 -2
- package/scripts/sw-github-checks.sh +1 -1
- package/scripts/sw-github-deploy.sh +1 -1
- package/scripts/sw-github-graphql.sh +1 -1
- package/scripts/sw-guild.sh +1 -1
- package/scripts/sw-heartbeat.sh +1 -1
- package/scripts/sw-hygiene.sh +1 -1
- package/scripts/sw-incident.sh +1 -1
- package/scripts/sw-init.sh +112 -9
- package/scripts/sw-instrument.sh +6 -1
- package/scripts/sw-intelligence.sh +5 -1
- package/scripts/sw-jira.sh +1 -1
- package/scripts/sw-launchd.sh +1 -1
- package/scripts/sw-linear.sh +20 -9
- package/scripts/sw-logs.sh +1 -1
- package/scripts/sw-loop.sh +2 -1
- package/scripts/sw-memory.sh +10 -1
- package/scripts/sw-mission-control.sh +1 -1
- package/scripts/sw-model-router.sh +4 -1
- package/scripts/sw-otel.sh +1 -1
- package/scripts/sw-oversight.sh +1 -1
- package/scripts/sw-pipeline-composer.sh +3 -1
- package/scripts/sw-pipeline-vitals.sh +4 -6
- package/scripts/sw-pipeline.sh +4 -1
- package/scripts/sw-pm.sh +5 -2
- package/scripts/sw-pr-lifecycle.sh +1 -1
- package/scripts/sw-predictive.sh +4 -1
- package/scripts/sw-prep.sh +3 -2
- package/scripts/sw-ps.sh +1 -1
- package/scripts/sw-public-dashboard.sh +10 -4
- package/scripts/sw-quality.sh +1 -1
- package/scripts/sw-reaper.sh +1 -1
- package/scripts/sw-recruit.sh +16 -0
- package/scripts/sw-regression.sh +2 -1
- package/scripts/sw-release-manager.sh +1 -1
- package/scripts/sw-release.sh +7 -5
- package/scripts/sw-remote.sh +1 -1
- package/scripts/sw-replay.sh +1 -1
- package/scripts/sw-retro.sh +4 -1
- package/scripts/sw-scale.sh +4 -1
- package/scripts/sw-security-audit.sh +1 -1
- package/scripts/sw-self-optimize.sh +113 -1
- package/scripts/sw-session.sh +1 -1
- package/scripts/sw-setup.sh +1 -1
- package/scripts/sw-standup.sh +2 -1
- package/scripts/sw-status.sh +1 -1
- package/scripts/sw-strategic.sh +2 -1
- package/scripts/sw-stream.sh +1 -1
- package/scripts/sw-swarm.sh +6 -1
- package/scripts/sw-team-stages.sh +1 -1
- package/scripts/sw-templates.sh +1 -1
- package/scripts/sw-testgen.sh +3 -2
- package/scripts/sw-tmux-pipeline.sh +2 -1
- package/scripts/sw-tmux.sh +1 -1
- package/scripts/sw-trace.sh +1 -1
- package/scripts/sw-tracker-jira.sh +1 -0
- package/scripts/sw-tracker-linear.sh +1 -0
- package/scripts/sw-tracker.sh +1 -1
- package/scripts/sw-triage.sh +198 -11
- package/scripts/sw-upgrade.sh +1 -1
- package/scripts/sw-ux.sh +1 -1
- package/scripts/sw-webhook.sh +1 -1
- package/scripts/sw-widgets.sh +2 -2
- package/scripts/sw-worktree.sh +1 -1
- package/dashboard/public/app.js +0 -4422
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi, afterEach } from "vitest";
|
|
2
|
+
import * as api from "./api";
|
|
3
|
+
|
|
4
|
+
// Mock global fetch
|
|
5
|
+
const mockFetch = vi.fn();
|
|
6
|
+
global.fetch = mockFetch;
|
|
7
|
+
|
|
8
|
+
function jsonResponse(data: unknown, status = 200) {
|
|
9
|
+
return Promise.resolve({
|
|
10
|
+
ok: status >= 200 && status < 300,
|
|
11
|
+
status,
|
|
12
|
+
json: () => Promise.resolve(data),
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function errorResponse(status: number, body?: unknown) {
|
|
17
|
+
return Promise.resolve({
|
|
18
|
+
ok: false,
|
|
19
|
+
status,
|
|
20
|
+
json: () => Promise.resolve(body || { error: `HTTP ${status}` }),
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
describe("API Client", () => {
|
|
25
|
+
beforeEach(() => {
|
|
26
|
+
mockFetch.mockReset();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
afterEach(() => {
|
|
30
|
+
vi.restoreAllMocks();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
describe("fetchMe", () => {
|
|
34
|
+
it("calls GET /api/me", async () => {
|
|
35
|
+
const userData = { username: "test", role: "admin" };
|
|
36
|
+
mockFetch.mockReturnValueOnce(jsonResponse(userData));
|
|
37
|
+
|
|
38
|
+
const result = await api.fetchMe();
|
|
39
|
+
expect(result).toEqual(userData);
|
|
40
|
+
expect(mockFetch).toHaveBeenCalledWith("/api/me", undefined);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe("fetchPipelineDetail", () => {
|
|
45
|
+
it("calls GET /api/pipeline/:issue", async () => {
|
|
46
|
+
const detail = { issue: 42, status: "building" };
|
|
47
|
+
mockFetch.mockReturnValueOnce(jsonResponse(detail));
|
|
48
|
+
|
|
49
|
+
const result = await api.fetchPipelineDetail(42);
|
|
50
|
+
expect(result).toEqual(detail);
|
|
51
|
+
expect(mockFetch).toHaveBeenCalledWith("/api/pipeline/42", undefined);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("encodes special characters in issue", async () => {
|
|
55
|
+
mockFetch.mockReturnValueOnce(jsonResponse({}));
|
|
56
|
+
await api.fetchPipelineDetail("test/123");
|
|
57
|
+
expect(mockFetch).toHaveBeenCalledWith(
|
|
58
|
+
"/api/pipeline/test%2F123",
|
|
59
|
+
undefined,
|
|
60
|
+
);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe("fetchMetricsHistory", () => {
|
|
65
|
+
it("defaults to 30 day period", async () => {
|
|
66
|
+
mockFetch.mockReturnValueOnce(jsonResponse({ history: [] }));
|
|
67
|
+
await api.fetchMetricsHistory();
|
|
68
|
+
expect(mockFetch).toHaveBeenCalledWith(
|
|
69
|
+
"/api/metrics/history?period=30",
|
|
70
|
+
undefined,
|
|
71
|
+
);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("respects custom period", async () => {
|
|
75
|
+
mockFetch.mockReturnValueOnce(jsonResponse({ history: [] }));
|
|
76
|
+
await api.fetchMetricsHistory(7);
|
|
77
|
+
expect(mockFetch).toHaveBeenCalledWith(
|
|
78
|
+
"/api/metrics/history?period=7",
|
|
79
|
+
undefined,
|
|
80
|
+
);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe("fetchTimeline", () => {
|
|
85
|
+
it("defaults to 24h range", async () => {
|
|
86
|
+
mockFetch.mockReturnValueOnce(jsonResponse([]));
|
|
87
|
+
await api.fetchTimeline();
|
|
88
|
+
expect(mockFetch).toHaveBeenCalledWith(
|
|
89
|
+
"/api/timeline?range=24h",
|
|
90
|
+
undefined,
|
|
91
|
+
);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
describe("fetchActivity", () => {
|
|
96
|
+
it("builds query string from params", async () => {
|
|
97
|
+
mockFetch.mockReturnValueOnce(
|
|
98
|
+
jsonResponse({ events: [], hasMore: false }),
|
|
99
|
+
);
|
|
100
|
+
await api.fetchActivity({ limit: 50, offset: 10, type: "error" });
|
|
101
|
+
|
|
102
|
+
const url = mockFetch.mock.calls[0][0] as string;
|
|
103
|
+
expect(url).toContain("/api/activity?");
|
|
104
|
+
expect(url).toContain("limit=50");
|
|
105
|
+
expect(url).toContain("offset=10");
|
|
106
|
+
expect(url).toContain("type=error");
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("excludes type=all from query string", async () => {
|
|
110
|
+
mockFetch.mockReturnValueOnce(
|
|
111
|
+
jsonResponse({ events: [], hasMore: false }),
|
|
112
|
+
);
|
|
113
|
+
await api.fetchActivity({ type: "all" });
|
|
114
|
+
|
|
115
|
+
const url = mockFetch.mock.calls[0][0] as string;
|
|
116
|
+
expect(url).not.toContain("type=");
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
describe("machine operations", () => {
|
|
121
|
+
it("fetchMachines calls GET /api/machines", async () => {
|
|
122
|
+
mockFetch.mockReturnValueOnce(jsonResponse([]));
|
|
123
|
+
await api.fetchMachines();
|
|
124
|
+
expect(mockFetch).toHaveBeenCalledWith("/api/machines", undefined);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("addMachine calls POST /api/machines", async () => {
|
|
128
|
+
mockFetch.mockReturnValueOnce(jsonResponse({ name: "m1" }));
|
|
129
|
+
await api.addMachine({ name: "m1", host: "localhost" });
|
|
130
|
+
|
|
131
|
+
expect(mockFetch).toHaveBeenCalledWith("/api/machines", {
|
|
132
|
+
method: "POST",
|
|
133
|
+
headers: { "Content-Type": "application/json" },
|
|
134
|
+
body: JSON.stringify({ name: "m1", host: "localhost" }),
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("updateMachine calls PATCH /api/machines/:name", async () => {
|
|
139
|
+
mockFetch.mockReturnValueOnce(jsonResponse({ name: "m1" }));
|
|
140
|
+
await api.updateMachine("m1", { status: "active" });
|
|
141
|
+
|
|
142
|
+
expect(mockFetch).toHaveBeenCalledWith("/api/machines/m1", {
|
|
143
|
+
method: "PATCH",
|
|
144
|
+
headers: { "Content-Type": "application/json" },
|
|
145
|
+
body: JSON.stringify({ status: "active" }),
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("removeMachine calls DELETE /api/machines/:name", async () => {
|
|
150
|
+
mockFetch.mockReturnValueOnce(jsonResponse({ ok: true }));
|
|
151
|
+
await api.removeMachine("m1");
|
|
152
|
+
|
|
153
|
+
expect(mockFetch).toHaveBeenCalledWith("/api/machines/m1", {
|
|
154
|
+
method: "DELETE",
|
|
155
|
+
headers: { "Content-Type": "application/json" },
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
describe("fetchQueueDetailed", () => {
|
|
161
|
+
it("transforms queue property to items", async () => {
|
|
162
|
+
mockFetch.mockReturnValueOnce(
|
|
163
|
+
jsonResponse({ queue: [{ id: 1 }, { id: 2 }] }),
|
|
164
|
+
);
|
|
165
|
+
const result = await api.fetchQueueDetailed();
|
|
166
|
+
expect(result).toEqual({ items: [{ id: 1 }, { id: 2 }] });
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it("handles missing queue property gracefully", async () => {
|
|
170
|
+
mockFetch.mockReturnValueOnce(jsonResponse({}));
|
|
171
|
+
const result = await api.fetchQueueDetailed();
|
|
172
|
+
expect(result).toEqual({ items: [] });
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
describe("emergency brake", () => {
|
|
177
|
+
it("calls POST /api/emergency-brake", async () => {
|
|
178
|
+
mockFetch.mockReturnValueOnce(jsonResponse({ ok: true }));
|
|
179
|
+
await api.emergencyBrake();
|
|
180
|
+
|
|
181
|
+
expect(mockFetch).toHaveBeenCalledWith("/api/emergency-brake", {
|
|
182
|
+
method: "POST",
|
|
183
|
+
headers: { "Content-Type": "application/json" },
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
describe("sendIntervention", () => {
|
|
189
|
+
it("calls POST /api/intervention/:issue/:action with body", async () => {
|
|
190
|
+
mockFetch.mockReturnValueOnce(jsonResponse({ ok: true }));
|
|
191
|
+
await api.sendIntervention(42, "pause", { reason: "testing" });
|
|
192
|
+
|
|
193
|
+
expect(mockFetch).toHaveBeenCalledWith("/api/intervention/42/pause", {
|
|
194
|
+
method: "POST",
|
|
195
|
+
headers: { "Content-Type": "application/json" },
|
|
196
|
+
body: JSON.stringify({ reason: "testing" }),
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
describe("insights endpoints", () => {
|
|
202
|
+
it("fetchPatterns returns empty array on error", async () => {
|
|
203
|
+
mockFetch.mockReturnValueOnce(errorResponse(500));
|
|
204
|
+
const result = await api.fetchPatterns();
|
|
205
|
+
expect(result).toEqual({ patterns: [] });
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it("fetchDecisions returns empty array on error", async () => {
|
|
209
|
+
mockFetch.mockReturnValueOnce(errorResponse(500));
|
|
210
|
+
const result = await api.fetchDecisions();
|
|
211
|
+
expect(result).toEqual({ decisions: [] });
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it("fetchHeatmap returns null on error", async () => {
|
|
215
|
+
mockFetch.mockReturnValueOnce(errorResponse(500));
|
|
216
|
+
const result = await api.fetchHeatmap();
|
|
217
|
+
expect(result).toBeNull();
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
describe("pipeline live changes", () => {
|
|
222
|
+
it("fetchPipelineDiff calls correct endpoint", async () => {
|
|
223
|
+
mockFetch.mockReturnValueOnce(
|
|
224
|
+
jsonResponse({
|
|
225
|
+
diff: "diff output",
|
|
226
|
+
stats: { files_changed: 1, insertions: 10, deletions: 5 },
|
|
227
|
+
worktree: "/path",
|
|
228
|
+
}),
|
|
229
|
+
);
|
|
230
|
+
await api.fetchPipelineDiff(142);
|
|
231
|
+
expect(mockFetch).toHaveBeenCalledWith(
|
|
232
|
+
"/api/pipeline/142/diff",
|
|
233
|
+
undefined,
|
|
234
|
+
);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it("fetchPipelineFiles calls correct endpoint", async () => {
|
|
238
|
+
mockFetch.mockReturnValueOnce(
|
|
239
|
+
jsonResponse({
|
|
240
|
+
files: [{ path: "src/main.ts", status: "modified" }],
|
|
241
|
+
}),
|
|
242
|
+
);
|
|
243
|
+
await api.fetchPipelineFiles(142);
|
|
244
|
+
expect(mockFetch).toHaveBeenCalledWith(
|
|
245
|
+
"/api/pipeline/142/files",
|
|
246
|
+
undefined,
|
|
247
|
+
);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it("fetchPipelineReasoning calls correct endpoint", async () => {
|
|
251
|
+
mockFetch.mockReturnValueOnce(jsonResponse({ reasoning: [] }));
|
|
252
|
+
await api.fetchPipelineReasoning(142);
|
|
253
|
+
expect(mockFetch).toHaveBeenCalledWith(
|
|
254
|
+
"/api/pipeline/142/reasoning",
|
|
255
|
+
undefined,
|
|
256
|
+
);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it("fetchPipelineFailures calls correct endpoint", async () => {
|
|
260
|
+
mockFetch.mockReturnValueOnce(jsonResponse({ failures: [] }));
|
|
261
|
+
await api.fetchPipelineFailures(142);
|
|
262
|
+
expect(mockFetch).toHaveBeenCalledWith(
|
|
263
|
+
"/api/pipeline/142/failures",
|
|
264
|
+
undefined,
|
|
265
|
+
);
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
describe("approval gates", () => {
|
|
270
|
+
it("approveGate sends POST with stage", async () => {
|
|
271
|
+
mockFetch.mockReturnValueOnce(jsonResponse({ ok: true }));
|
|
272
|
+
await api.approveGate(42, "build");
|
|
273
|
+
|
|
274
|
+
expect(mockFetch).toHaveBeenCalledWith("/api/approval-gates/42/approve", {
|
|
275
|
+
method: "POST",
|
|
276
|
+
body: JSON.stringify({ stage: "build" }),
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it("rejectGate sends POST with stage and reason", async () => {
|
|
281
|
+
mockFetch.mockReturnValueOnce(jsonResponse({ ok: true }));
|
|
282
|
+
await api.rejectGate(42, "build", "Failed QA");
|
|
283
|
+
|
|
284
|
+
expect(mockFetch).toHaveBeenCalledWith("/api/approval-gates/42/reject", {
|
|
285
|
+
method: "POST",
|
|
286
|
+
body: JSON.stringify({ stage: "build", reason: "Failed QA" }),
|
|
287
|
+
});
|
|
288
|
+
});
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
describe("notifications", () => {
|
|
292
|
+
it("addWebhook sends POST", async () => {
|
|
293
|
+
mockFetch.mockReturnValueOnce(jsonResponse({ ok: true }));
|
|
294
|
+
await api.addWebhook("https://slack.com/hook", "Slack", ["failure"]);
|
|
295
|
+
|
|
296
|
+
expect(mockFetch).toHaveBeenCalledWith("/api/notifications/webhook", {
|
|
297
|
+
method: "POST",
|
|
298
|
+
body: JSON.stringify({
|
|
299
|
+
url: "https://slack.com/hook",
|
|
300
|
+
label: "Slack",
|
|
301
|
+
events: ["failure"],
|
|
302
|
+
}),
|
|
303
|
+
});
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
it("removeWebhook sends DELETE", async () => {
|
|
307
|
+
mockFetch.mockReturnValueOnce(jsonResponse({ ok: true }));
|
|
308
|
+
await api.removeWebhook("https://slack.com/hook");
|
|
309
|
+
|
|
310
|
+
expect(mockFetch).toHaveBeenCalledWith("/api/notifications/webhook", {
|
|
311
|
+
method: "DELETE",
|
|
312
|
+
body: JSON.stringify({ url: "https://slack.com/hook" }),
|
|
313
|
+
});
|
|
314
|
+
});
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
describe("machine claim/release", () => {
|
|
318
|
+
it("claimIssue sends POST with issue and machine", async () => {
|
|
319
|
+
mockFetch.mockReturnValueOnce(
|
|
320
|
+
jsonResponse({ approved: true, claimed_by: "m1" }),
|
|
321
|
+
);
|
|
322
|
+
const result = await api.claimIssue(42, "m1");
|
|
323
|
+
expect(result.approved).toBe(true);
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
it("releaseIssue sends POST", async () => {
|
|
327
|
+
mockFetch.mockReturnValueOnce(jsonResponse({ ok: true }));
|
|
328
|
+
await api.releaseIssue(42, "m1");
|
|
329
|
+
|
|
330
|
+
expect(mockFetch).toHaveBeenCalledWith("/api/claim/release", {
|
|
331
|
+
method: "POST",
|
|
332
|
+
body: JSON.stringify({ issue: 42, machine: "m1" }),
|
|
333
|
+
});
|
|
334
|
+
});
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
describe("error handling", () => {
|
|
338
|
+
it("throws on non-ok response", async () => {
|
|
339
|
+
mockFetch.mockReturnValueOnce(errorResponse(404, { error: "Not found" }));
|
|
340
|
+
|
|
341
|
+
await expect(api.fetchMe()).rejects.toThrow("Not found");
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
it("falls back to HTTP status on unparseable error", async () => {
|
|
345
|
+
mockFetch.mockReturnValueOnce(
|
|
346
|
+
Promise.resolve({
|
|
347
|
+
ok: false,
|
|
348
|
+
status: 500,
|
|
349
|
+
json: () => Promise.reject(new Error("parse error")),
|
|
350
|
+
}),
|
|
351
|
+
);
|
|
352
|
+
|
|
353
|
+
await expect(api.fetchMe()).rejects.toThrow("HTTP 500");
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
it("fetchPredictions returns empty object on error", async () => {
|
|
357
|
+
mockFetch.mockReturnValueOnce(errorResponse(500));
|
|
358
|
+
const result = await api.fetchPredictions(42);
|
|
359
|
+
expect(result).toEqual({});
|
|
360
|
+
});
|
|
361
|
+
});
|
|
362
|
+
});
|