postgresai 0.15.0-dev.1 → 0.15.0-dev.10
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 +3 -1
- package/bin/postgres-ai.ts +606 -105
- package/bun.lock +4 -4
- package/dist/bin/postgres-ai.js +2355 -577
- package/instances.demo.yml +14 -0
- package/lib/checkup-api.ts +25 -6
- package/lib/checkup.ts +241 -10
- package/lib/config.ts +3 -0
- package/lib/init.ts +196 -4
- package/lib/issues.ts +72 -72
- package/lib/mcp-server.ts +90 -0
- package/lib/metrics-loader.ts +3 -1
- package/lib/reports.ts +373 -0
- package/lib/storage.ts +291 -0
- package/lib/supabase.ts +8 -1
- package/lib/util.ts +7 -1
- package/package.json +4 -4
- package/scripts/embed-checkup-dictionary.ts +9 -0
- package/scripts/embed-metrics.ts +2 -0
- package/test/PERMISSION_CHECK_TEST_SUMMARY.md +139 -0
- package/test/checkup.test.ts +1316 -2
- package/test/compose-cmd.test.ts +120 -0
- package/test/config-consistency.test.ts +321 -5
- package/test/init.integration.test.ts +27 -28
- package/test/init.test.ts +534 -6
- package/test/issues.cli.test.ts +230 -1
- package/test/mcp-server.test.ts +459 -0
- package/test/monitoring.test.ts +78 -0
- package/test/permission-check-sql.test.ts +116 -0
- package/test/reports.cli.test.ts +793 -0
- package/test/reports.test.ts +977 -0
- package/test/schema-validation.test.ts +81 -0
- package/test/storage.test.ts +761 -0
- package/test/test-utils.ts +51 -2
- package/test/upgrade.test.ts +422 -0
|
@@ -0,0 +1,977 @@
|
|
|
1
|
+
import { describe, test, expect, mock, afterEach, spyOn } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
fetchReports,
|
|
4
|
+
fetchAllReports,
|
|
5
|
+
fetchReportFiles,
|
|
6
|
+
fetchReportFileData,
|
|
7
|
+
renderMarkdownForTerminal,
|
|
8
|
+
parseFlexibleDate,
|
|
9
|
+
} from "../lib/reports";
|
|
10
|
+
|
|
11
|
+
const originalFetch = globalThis.fetch;
|
|
12
|
+
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// fetchReports
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
describe("fetchReports", () => {
|
|
17
|
+
afterEach(() => {
|
|
18
|
+
globalThis.fetch = originalFetch;
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("throws when apiKey is missing", async () => {
|
|
22
|
+
await expect(
|
|
23
|
+
fetchReports({ apiKey: "", apiBaseUrl: "https://api.example.com" })
|
|
24
|
+
).rejects.toThrow("API key is required");
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("constructs correct URL with no filters", async () => {
|
|
28
|
+
let capturedRequest: { url: string; options: RequestInit } | null = null;
|
|
29
|
+
|
|
30
|
+
globalThis.fetch = mock((url: string, options: RequestInit) => {
|
|
31
|
+
capturedRequest = { url, options };
|
|
32
|
+
return Promise.resolve(
|
|
33
|
+
new Response(JSON.stringify([]), {
|
|
34
|
+
status: 200,
|
|
35
|
+
headers: { "Content-Type": "application/json" },
|
|
36
|
+
})
|
|
37
|
+
);
|
|
38
|
+
}) as unknown as typeof fetch;
|
|
39
|
+
|
|
40
|
+
await fetchReports({
|
|
41
|
+
apiKey: "test-key",
|
|
42
|
+
apiBaseUrl: "https://api.example.com",
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
expect(capturedRequest).not.toBeNull();
|
|
46
|
+
const url = new URL(capturedRequest!.url);
|
|
47
|
+
expect(url.pathname).toBe("/checkup_reports");
|
|
48
|
+
expect(url.searchParams.get("order")).toBe("id.desc");
|
|
49
|
+
expect(url.searchParams.get("limit")).toBe("20");
|
|
50
|
+
expect(url.searchParams.has("project_id")).toBe(false);
|
|
51
|
+
expect(url.searchParams.has("status")).toBe(false);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("constructs correct URL with all filters", async () => {
|
|
55
|
+
let capturedRequest: { url: string; options: RequestInit } | null = null;
|
|
56
|
+
|
|
57
|
+
globalThis.fetch = mock((url: string, options: RequestInit) => {
|
|
58
|
+
capturedRequest = { url, options };
|
|
59
|
+
return Promise.resolve(
|
|
60
|
+
new Response(JSON.stringify([]), {
|
|
61
|
+
status: 200,
|
|
62
|
+
headers: { "Content-Type": "application/json" },
|
|
63
|
+
})
|
|
64
|
+
);
|
|
65
|
+
}) as unknown as typeof fetch;
|
|
66
|
+
|
|
67
|
+
await fetchReports({
|
|
68
|
+
apiKey: "test-key",
|
|
69
|
+
apiBaseUrl: "https://api.example.com",
|
|
70
|
+
projectId: 5,
|
|
71
|
+
status: "completed",
|
|
72
|
+
limit: 10,
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
expect(capturedRequest).not.toBeNull();
|
|
76
|
+
const url = new URL(capturedRequest!.url);
|
|
77
|
+
expect(url.searchParams.get("project_id")).toBe("eq.5");
|
|
78
|
+
expect(url.searchParams.get("status")).toBe("eq.completed");
|
|
79
|
+
expect(url.searchParams.get("limit")).toBe("10");
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("sends correct headers", async () => {
|
|
83
|
+
let capturedRequest: { url: string; options: RequestInit } | null = null;
|
|
84
|
+
|
|
85
|
+
globalThis.fetch = mock((url: string, options: RequestInit) => {
|
|
86
|
+
capturedRequest = { url, options };
|
|
87
|
+
return Promise.resolve(
|
|
88
|
+
new Response(JSON.stringify([]), {
|
|
89
|
+
status: 200,
|
|
90
|
+
headers: { "Content-Type": "application/json" },
|
|
91
|
+
})
|
|
92
|
+
);
|
|
93
|
+
}) as unknown as typeof fetch;
|
|
94
|
+
|
|
95
|
+
await fetchReports({
|
|
96
|
+
apiKey: "test-key",
|
|
97
|
+
apiBaseUrl: "https://api.example.com",
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
const headers = capturedRequest!.options.headers as Record<string, string>;
|
|
101
|
+
expect(headers["access-token"]).toBe("test-key");
|
|
102
|
+
expect(headers["Prefer"]).toBe("return=representation");
|
|
103
|
+
expect(headers["Content-Type"]).toBe("application/json");
|
|
104
|
+
expect(headers["Connection"]).toBe("close");
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("returns parsed response array", async () => {
|
|
108
|
+
const mockData = [
|
|
109
|
+
{
|
|
110
|
+
id: 1,
|
|
111
|
+
org_id: 1,
|
|
112
|
+
org_name: "TestOrg",
|
|
113
|
+
project_id: 10,
|
|
114
|
+
project_name: "prod-db",
|
|
115
|
+
created_at: "2025-01-01T00:00:00Z",
|
|
116
|
+
created_formatted: "2025-01-01 00:00:00",
|
|
117
|
+
epoch: 1735689600,
|
|
118
|
+
status: "completed",
|
|
119
|
+
},
|
|
120
|
+
];
|
|
121
|
+
|
|
122
|
+
globalThis.fetch = mock(() =>
|
|
123
|
+
Promise.resolve(
|
|
124
|
+
new Response(JSON.stringify(mockData), {
|
|
125
|
+
status: 200,
|
|
126
|
+
headers: { "Content-Type": "application/json" },
|
|
127
|
+
})
|
|
128
|
+
)
|
|
129
|
+
) as unknown as typeof fetch;
|
|
130
|
+
|
|
131
|
+
const result = await fetchReports({
|
|
132
|
+
apiKey: "test-key",
|
|
133
|
+
apiBaseUrl: "https://api.example.com",
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
expect(result).toEqual(mockData);
|
|
137
|
+
expect(result[0].id).toBe(1);
|
|
138
|
+
expect(result[0].status).toBe("completed");
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test("throws formatted error on non-200 response", async () => {
|
|
142
|
+
globalThis.fetch = mock(() =>
|
|
143
|
+
Promise.resolve(
|
|
144
|
+
new Response('{"message": "Unauthorized"}', {
|
|
145
|
+
status: 401,
|
|
146
|
+
headers: { "Content-Type": "application/json" },
|
|
147
|
+
})
|
|
148
|
+
)
|
|
149
|
+
) as unknown as typeof fetch;
|
|
150
|
+
|
|
151
|
+
await expect(
|
|
152
|
+
fetchReports({
|
|
153
|
+
apiKey: "invalid-key",
|
|
154
|
+
apiBaseUrl: "https://api.example.com",
|
|
155
|
+
})
|
|
156
|
+
).rejects.toThrow(/Failed to fetch reports/);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test("sets created_at filter when beforeDate is provided", async () => {
|
|
160
|
+
let capturedRequest: { url: string; options: RequestInit } | null = null;
|
|
161
|
+
|
|
162
|
+
globalThis.fetch = mock((url: string, options: RequestInit) => {
|
|
163
|
+
capturedRequest = { url, options };
|
|
164
|
+
return Promise.resolve(
|
|
165
|
+
new Response(JSON.stringify([]), {
|
|
166
|
+
status: 200,
|
|
167
|
+
headers: { "Content-Type": "application/json" },
|
|
168
|
+
})
|
|
169
|
+
);
|
|
170
|
+
}) as unknown as typeof fetch;
|
|
171
|
+
|
|
172
|
+
await fetchReports({
|
|
173
|
+
apiKey: "test-key",
|
|
174
|
+
apiBaseUrl: "https://api.example.com",
|
|
175
|
+
beforeDate: "2025-01-15T00:00:00.000Z",
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
expect(capturedRequest).not.toBeNull();
|
|
179
|
+
const url = new URL(capturedRequest!.url);
|
|
180
|
+
expect(url.searchParams.get("created_at")).toBe("lt.2025-01-15T00:00:00.000Z");
|
|
181
|
+
expect(url.searchParams.get("order")).toBe("id.desc");
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test("sets id=lt.beforeId when beforeId is provided (internal pagination)", async () => {
|
|
185
|
+
let capturedRequest: { url: string; options: RequestInit } | null = null;
|
|
186
|
+
|
|
187
|
+
globalThis.fetch = mock((url: string, options: RequestInit) => {
|
|
188
|
+
capturedRequest = { url, options };
|
|
189
|
+
return Promise.resolve(
|
|
190
|
+
new Response(JSON.stringify([]), {
|
|
191
|
+
status: 200,
|
|
192
|
+
headers: { "Content-Type": "application/json" },
|
|
193
|
+
})
|
|
194
|
+
);
|
|
195
|
+
}) as unknown as typeof fetch;
|
|
196
|
+
|
|
197
|
+
await fetchReports({
|
|
198
|
+
apiKey: "test-key",
|
|
199
|
+
apiBaseUrl: "https://api.example.com",
|
|
200
|
+
beforeId: 50,
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
expect(capturedRequest).not.toBeNull();
|
|
204
|
+
const url = new URL(capturedRequest!.url);
|
|
205
|
+
expect(url.searchParams.get("id")).toBe("lt.50");
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
test("logs debug info when debug is true", async () => {
|
|
209
|
+
const consoleSpy = spyOn(console, "error").mockImplementation(() => {});
|
|
210
|
+
|
|
211
|
+
globalThis.fetch = mock(() =>
|
|
212
|
+
Promise.resolve(
|
|
213
|
+
new Response(JSON.stringify([]), {
|
|
214
|
+
status: 200,
|
|
215
|
+
headers: { "Content-Type": "application/json" },
|
|
216
|
+
})
|
|
217
|
+
)
|
|
218
|
+
) as unknown as typeof fetch;
|
|
219
|
+
|
|
220
|
+
await fetchReports({
|
|
221
|
+
apiKey: "test-key",
|
|
222
|
+
apiBaseUrl: "https://api.example.com",
|
|
223
|
+
debug: true,
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
const calls = consoleSpy.mock.calls.map((c) => c[0]);
|
|
227
|
+
expect(calls.some((c: string) => c.includes("Debug: Resolved API base URL"))).toBe(true);
|
|
228
|
+
expect(calls.some((c: string) => c.includes("Debug: GET URL"))).toBe(true);
|
|
229
|
+
expect(calls.some((c: string) => c.includes("Debug: Request headers"))).toBe(true);
|
|
230
|
+
expect(calls.some((c: string) => c.includes("Debug: Response status"))).toBe(true);
|
|
231
|
+
|
|
232
|
+
consoleSpy.mockRestore();
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
test("throws on invalid JSON response", async () => {
|
|
236
|
+
globalThis.fetch = mock(() =>
|
|
237
|
+
Promise.resolve(
|
|
238
|
+
new Response("not valid json", {
|
|
239
|
+
status: 200,
|
|
240
|
+
headers: { "Content-Type": "text/plain" },
|
|
241
|
+
})
|
|
242
|
+
)
|
|
243
|
+
) as unknown as typeof fetch;
|
|
244
|
+
|
|
245
|
+
await expect(
|
|
246
|
+
fetchReports({
|
|
247
|
+
apiKey: "test-key",
|
|
248
|
+
apiBaseUrl: "https://api.example.com",
|
|
249
|
+
})
|
|
250
|
+
).rejects.toThrow(/Failed to parse reports response/);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
test("does not set id or created_at params when no before options provided", async () => {
|
|
254
|
+
let capturedRequest: { url: string; options: RequestInit } | null = null;
|
|
255
|
+
|
|
256
|
+
globalThis.fetch = mock((url: string, options: RequestInit) => {
|
|
257
|
+
capturedRequest = { url, options };
|
|
258
|
+
return Promise.resolve(
|
|
259
|
+
new Response(JSON.stringify([]), {
|
|
260
|
+
status: 200,
|
|
261
|
+
headers: { "Content-Type": "application/json" },
|
|
262
|
+
})
|
|
263
|
+
);
|
|
264
|
+
}) as unknown as typeof fetch;
|
|
265
|
+
|
|
266
|
+
await fetchReports({
|
|
267
|
+
apiKey: "test-key",
|
|
268
|
+
apiBaseUrl: "https://api.example.com",
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
const url = new URL(capturedRequest!.url);
|
|
272
|
+
expect(url.searchParams.has("id")).toBe(false);
|
|
273
|
+
expect(url.searchParams.has("created_at")).toBe(false);
|
|
274
|
+
});
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
// ---------------------------------------------------------------------------
|
|
278
|
+
// fetchAllReports
|
|
279
|
+
// ---------------------------------------------------------------------------
|
|
280
|
+
describe("fetchAllReports", () => {
|
|
281
|
+
afterEach(() => {
|
|
282
|
+
globalThis.fetch = originalFetch;
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
test("fetches all pages until empty response", async () => {
|
|
286
|
+
const page1 = [
|
|
287
|
+
{ id: 100, org_id: 1, org_name: "O", project_id: 1, project_name: "P", created_at: "", created_formatted: "", epoch: 0, status: "completed" },
|
|
288
|
+
{ id: 90, org_id: 1, org_name: "O", project_id: 1, project_name: "P", created_at: "", created_formatted: "", epoch: 0, status: "completed" },
|
|
289
|
+
];
|
|
290
|
+
const page2 = [
|
|
291
|
+
{ id: 80, org_id: 1, org_name: "O", project_id: 1, project_name: "P", created_at: "", created_formatted: "", epoch: 0, status: "completed" },
|
|
292
|
+
];
|
|
293
|
+
|
|
294
|
+
let callCount = 0;
|
|
295
|
+
globalThis.fetch = mock((url: string) => {
|
|
296
|
+
callCount++;
|
|
297
|
+
const u = new URL(url);
|
|
298
|
+
const idParam = u.searchParams.get("id");
|
|
299
|
+
let data;
|
|
300
|
+
if (!idParam) {
|
|
301
|
+
data = page1;
|
|
302
|
+
} else if (idParam === "lt.90") {
|
|
303
|
+
data = page2;
|
|
304
|
+
} else {
|
|
305
|
+
data = [];
|
|
306
|
+
}
|
|
307
|
+
return Promise.resolve(
|
|
308
|
+
new Response(JSON.stringify(data), {
|
|
309
|
+
status: 200,
|
|
310
|
+
headers: { "Content-Type": "application/json" },
|
|
311
|
+
})
|
|
312
|
+
);
|
|
313
|
+
}) as unknown as typeof fetch;
|
|
314
|
+
|
|
315
|
+
const result = await fetchAllReports({
|
|
316
|
+
apiKey: "test-key",
|
|
317
|
+
apiBaseUrl: "https://api.example.com",
|
|
318
|
+
limit: 2,
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
expect(result.length).toBe(3);
|
|
322
|
+
expect(result[0].id).toBe(100);
|
|
323
|
+
expect(result[1].id).toBe(90);
|
|
324
|
+
expect(result[2].id).toBe(80);
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
test("returns empty array when no reports exist", async () => {
|
|
328
|
+
globalThis.fetch = mock(() =>
|
|
329
|
+
Promise.resolve(
|
|
330
|
+
new Response(JSON.stringify([]), {
|
|
331
|
+
status: 200,
|
|
332
|
+
headers: { "Content-Type": "application/json" },
|
|
333
|
+
})
|
|
334
|
+
)
|
|
335
|
+
) as unknown as typeof fetch;
|
|
336
|
+
|
|
337
|
+
const result = await fetchAllReports({
|
|
338
|
+
apiKey: "test-key",
|
|
339
|
+
apiBaseUrl: "https://api.example.com",
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
expect(result).toEqual([]);
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
test("stops at MAX_ALL_REPORTS cap (10000)", async () => {
|
|
346
|
+
const warnSpy = spyOn(console, "warn").mockImplementation(() => {});
|
|
347
|
+
const pageSize = 500;
|
|
348
|
+
let callCount = 0;
|
|
349
|
+
|
|
350
|
+
globalThis.fetch = mock((url: string) => {
|
|
351
|
+
callCount++;
|
|
352
|
+
const u = new URL(url);
|
|
353
|
+
const idParam = u.searchParams.get("id");
|
|
354
|
+
// Generate a full page each time with descending IDs
|
|
355
|
+
const startId = idParam ? parseInt(idParam.replace("lt.", "")) - 1 : 100000;
|
|
356
|
+
const page = Array.from({ length: pageSize }, (_, i) => ({
|
|
357
|
+
id: startId - i,
|
|
358
|
+
org_id: 1,
|
|
359
|
+
org_name: "O",
|
|
360
|
+
project_id: 1,
|
|
361
|
+
project_name: "P",
|
|
362
|
+
created_at: "",
|
|
363
|
+
created_formatted: "",
|
|
364
|
+
epoch: 0,
|
|
365
|
+
status: "completed",
|
|
366
|
+
}));
|
|
367
|
+
return Promise.resolve(
|
|
368
|
+
new Response(JSON.stringify(page), {
|
|
369
|
+
status: 200,
|
|
370
|
+
headers: { "Content-Type": "application/json" },
|
|
371
|
+
})
|
|
372
|
+
);
|
|
373
|
+
}) as unknown as typeof fetch;
|
|
374
|
+
|
|
375
|
+
const result = await fetchAllReports({
|
|
376
|
+
apiKey: "test-key",
|
|
377
|
+
apiBaseUrl: "https://api.example.com",
|
|
378
|
+
limit: pageSize,
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
// Should stop at 10000 (MAX_ALL_REPORTS)
|
|
382
|
+
expect(result.length).toBe(10000);
|
|
383
|
+
// Should have logged a warning
|
|
384
|
+
const warnCalls = warnSpy.mock.calls.map((c) => c[0]);
|
|
385
|
+
expect(warnCalls.some((c: string) => c.includes("maximum of 10000"))).toBe(true);
|
|
386
|
+
// Should have made exactly 20 calls (10000 / 500)
|
|
387
|
+
expect(callCount).toBe(20);
|
|
388
|
+
|
|
389
|
+
warnSpy.mockRestore();
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
test("stops when page has fewer items than limit", async () => {
|
|
393
|
+
const page = [
|
|
394
|
+
{ id: 50, org_id: 1, org_name: "O", project_id: 1, project_name: "P", created_at: "", created_formatted: "", epoch: 0, status: "completed" },
|
|
395
|
+
];
|
|
396
|
+
|
|
397
|
+
let callCount = 0;
|
|
398
|
+
globalThis.fetch = mock(() => {
|
|
399
|
+
callCount++;
|
|
400
|
+
return Promise.resolve(
|
|
401
|
+
new Response(JSON.stringify(page), {
|
|
402
|
+
status: 200,
|
|
403
|
+
headers: { "Content-Type": "application/json" },
|
|
404
|
+
})
|
|
405
|
+
);
|
|
406
|
+
}) as unknown as typeof fetch;
|
|
407
|
+
|
|
408
|
+
const result = await fetchAllReports({
|
|
409
|
+
apiKey: "test-key",
|
|
410
|
+
apiBaseUrl: "https://api.example.com",
|
|
411
|
+
limit: 10,
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
expect(result.length).toBe(1);
|
|
415
|
+
expect(callCount).toBe(1);
|
|
416
|
+
});
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
// ---------------------------------------------------------------------------
|
|
420
|
+
// fetchReportFiles
|
|
421
|
+
// ---------------------------------------------------------------------------
|
|
422
|
+
describe("fetchReportFiles", () => {
|
|
423
|
+
afterEach(() => {
|
|
424
|
+
globalThis.fetch = originalFetch;
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
test("throws when apiKey is missing", async () => {
|
|
428
|
+
await expect(
|
|
429
|
+
fetchReportFiles({
|
|
430
|
+
apiKey: "",
|
|
431
|
+
apiBaseUrl: "https://api.example.com",
|
|
432
|
+
reportId: 1,
|
|
433
|
+
})
|
|
434
|
+
).rejects.toThrow("API key is required");
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
test("throws when neither reportId nor checkId is provided", async () => {
|
|
438
|
+
await expect(
|
|
439
|
+
fetchReportFiles({
|
|
440
|
+
apiKey: "test-key",
|
|
441
|
+
apiBaseUrl: "https://api.example.com",
|
|
442
|
+
})
|
|
443
|
+
).rejects.toThrow("Either reportId or checkId is required");
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
test("works with only checkId (no reportId)", async () => {
|
|
447
|
+
let capturedRequest: { url: string; options: RequestInit } | null = null;
|
|
448
|
+
|
|
449
|
+
globalThis.fetch = mock((url: string, options: RequestInit) => {
|
|
450
|
+
capturedRequest = { url, options };
|
|
451
|
+
return Promise.resolve(
|
|
452
|
+
new Response(JSON.stringify([]), {
|
|
453
|
+
status: 200,
|
|
454
|
+
headers: { "Content-Type": "application/json" },
|
|
455
|
+
})
|
|
456
|
+
);
|
|
457
|
+
}) as unknown as typeof fetch;
|
|
458
|
+
|
|
459
|
+
await fetchReportFiles({
|
|
460
|
+
apiKey: "test-key",
|
|
461
|
+
apiBaseUrl: "https://api.example.com",
|
|
462
|
+
checkId: "H002",
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
expect(capturedRequest).not.toBeNull();
|
|
466
|
+
const url = new URL(capturedRequest!.url);
|
|
467
|
+
expect(url.pathname).toBe("/checkup_report_files");
|
|
468
|
+
expect(url.searchParams.has("checkup_report_id")).toBe(false);
|
|
469
|
+
expect(url.searchParams.get("check_id")).toBe("eq.H002");
|
|
470
|
+
expect(url.searchParams.get("order")).toBe("id.asc");
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
test("constructs URL with required checkup_report_id", async () => {
|
|
474
|
+
let capturedRequest: { url: string; options: RequestInit } | null = null;
|
|
475
|
+
|
|
476
|
+
globalThis.fetch = mock((url: string, options: RequestInit) => {
|
|
477
|
+
capturedRequest = { url, options };
|
|
478
|
+
return Promise.resolve(
|
|
479
|
+
new Response(JSON.stringify([]), {
|
|
480
|
+
status: 200,
|
|
481
|
+
headers: { "Content-Type": "application/json" },
|
|
482
|
+
})
|
|
483
|
+
);
|
|
484
|
+
}) as unknown as typeof fetch;
|
|
485
|
+
|
|
486
|
+
await fetchReportFiles({
|
|
487
|
+
apiKey: "test-key",
|
|
488
|
+
apiBaseUrl: "https://api.example.com",
|
|
489
|
+
reportId: 42,
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
expect(capturedRequest).not.toBeNull();
|
|
493
|
+
const url = new URL(capturedRequest!.url);
|
|
494
|
+
expect(url.pathname).toBe("/checkup_report_files");
|
|
495
|
+
expect(url.searchParams.get("checkup_report_id")).toBe("eq.42");
|
|
496
|
+
expect(url.searchParams.get("order")).toBe("id.asc");
|
|
497
|
+
expect(url.searchParams.has("type")).toBe(false);
|
|
498
|
+
expect(url.searchParams.has("check_id")).toBe(false);
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
test("applies optional type and check_id filters", async () => {
|
|
502
|
+
let capturedRequest: { url: string; options: RequestInit } | null = null;
|
|
503
|
+
|
|
504
|
+
globalThis.fetch = mock((url: string, options: RequestInit) => {
|
|
505
|
+
capturedRequest = { url, options };
|
|
506
|
+
return Promise.resolve(
|
|
507
|
+
new Response(JSON.stringify([]), {
|
|
508
|
+
status: 200,
|
|
509
|
+
headers: { "Content-Type": "application/json" },
|
|
510
|
+
})
|
|
511
|
+
);
|
|
512
|
+
}) as unknown as typeof fetch;
|
|
513
|
+
|
|
514
|
+
await fetchReportFiles({
|
|
515
|
+
apiKey: "test-key",
|
|
516
|
+
apiBaseUrl: "https://api.example.com",
|
|
517
|
+
reportId: 42,
|
|
518
|
+
type: "md",
|
|
519
|
+
checkId: "H002",
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
const url = new URL(capturedRequest!.url);
|
|
523
|
+
expect(url.searchParams.get("type")).toBe("eq.md");
|
|
524
|
+
expect(url.searchParams.get("check_id")).toBe("eq.H002");
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
test("returns parsed response array", async () => {
|
|
528
|
+
const mockData = [
|
|
529
|
+
{
|
|
530
|
+
id: 100,
|
|
531
|
+
checkup_report_id: 42,
|
|
532
|
+
filename: "H002.json",
|
|
533
|
+
check_id: "H002",
|
|
534
|
+
type: "json",
|
|
535
|
+
created_at: "2025-01-01T00:00:00Z",
|
|
536
|
+
created_formatted: "2025-01-01 00:00:00",
|
|
537
|
+
project_id: 10,
|
|
538
|
+
project_name: "prod-db",
|
|
539
|
+
},
|
|
540
|
+
];
|
|
541
|
+
|
|
542
|
+
globalThis.fetch = mock(() =>
|
|
543
|
+
Promise.resolve(
|
|
544
|
+
new Response(JSON.stringify(mockData), {
|
|
545
|
+
status: 200,
|
|
546
|
+
headers: { "Content-Type": "application/json" },
|
|
547
|
+
})
|
|
548
|
+
)
|
|
549
|
+
) as unknown as typeof fetch;
|
|
550
|
+
|
|
551
|
+
const result = await fetchReportFiles({
|
|
552
|
+
apiKey: "test-key",
|
|
553
|
+
apiBaseUrl: "https://api.example.com",
|
|
554
|
+
reportId: 42,
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
expect(result).toEqual(mockData);
|
|
558
|
+
expect(result[0].filename).toBe("H002.json");
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
test("throws on error response", async () => {
|
|
562
|
+
globalThis.fetch = mock(() =>
|
|
563
|
+
Promise.resolve(
|
|
564
|
+
new Response('{"message": "Not found"}', {
|
|
565
|
+
status: 404,
|
|
566
|
+
headers: { "Content-Type": "application/json" },
|
|
567
|
+
})
|
|
568
|
+
)
|
|
569
|
+
) as unknown as typeof fetch;
|
|
570
|
+
|
|
571
|
+
await expect(
|
|
572
|
+
fetchReportFiles({
|
|
573
|
+
apiKey: "test-key",
|
|
574
|
+
apiBaseUrl: "https://api.example.com",
|
|
575
|
+
reportId: 999,
|
|
576
|
+
})
|
|
577
|
+
).rejects.toThrow(/Failed to fetch report files/);
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
test("logs debug info when debug is true", async () => {
|
|
581
|
+
const consoleSpy = spyOn(console, "error").mockImplementation(() => {});
|
|
582
|
+
|
|
583
|
+
globalThis.fetch = mock(() =>
|
|
584
|
+
Promise.resolve(
|
|
585
|
+
new Response(JSON.stringify([]), {
|
|
586
|
+
status: 200,
|
|
587
|
+
headers: { "Content-Type": "application/json" },
|
|
588
|
+
})
|
|
589
|
+
)
|
|
590
|
+
) as unknown as typeof fetch;
|
|
591
|
+
|
|
592
|
+
await fetchReportFiles({
|
|
593
|
+
apiKey: "test-key",
|
|
594
|
+
apiBaseUrl: "https://api.example.com",
|
|
595
|
+
reportId: 1,
|
|
596
|
+
debug: true,
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
const calls = consoleSpy.mock.calls.map((c) => c[0]);
|
|
600
|
+
expect(calls.some((c: string) => c.includes("Debug: Resolved API base URL"))).toBe(true);
|
|
601
|
+
expect(calls.some((c: string) => c.includes("Debug: Response status"))).toBe(true);
|
|
602
|
+
|
|
603
|
+
consoleSpy.mockRestore();
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
test("throws on invalid JSON response", async () => {
|
|
607
|
+
globalThis.fetch = mock(() =>
|
|
608
|
+
Promise.resolve(
|
|
609
|
+
new Response("not valid json", {
|
|
610
|
+
status: 200,
|
|
611
|
+
headers: { "Content-Type": "text/plain" },
|
|
612
|
+
})
|
|
613
|
+
)
|
|
614
|
+
) as unknown as typeof fetch;
|
|
615
|
+
|
|
616
|
+
await expect(
|
|
617
|
+
fetchReportFiles({
|
|
618
|
+
apiKey: "test-key",
|
|
619
|
+
apiBaseUrl: "https://api.example.com",
|
|
620
|
+
reportId: 1,
|
|
621
|
+
})
|
|
622
|
+
).rejects.toThrow(/Failed to parse report files response/);
|
|
623
|
+
});
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
// ---------------------------------------------------------------------------
|
|
627
|
+
// fetchReportFileData
|
|
628
|
+
// ---------------------------------------------------------------------------
|
|
629
|
+
describe("fetchReportFileData", () => {
|
|
630
|
+
afterEach(() => {
|
|
631
|
+
globalThis.fetch = originalFetch;
|
|
632
|
+
});
|
|
633
|
+
|
|
634
|
+
test("throws when apiKey is missing", async () => {
|
|
635
|
+
await expect(
|
|
636
|
+
fetchReportFileData({
|
|
637
|
+
apiKey: "",
|
|
638
|
+
apiBaseUrl: "https://api.example.com",
|
|
639
|
+
reportId: 1,
|
|
640
|
+
})
|
|
641
|
+
).rejects.toThrow("API key is required");
|
|
642
|
+
});
|
|
643
|
+
|
|
644
|
+
test("throws when neither reportId nor checkId is provided", async () => {
|
|
645
|
+
await expect(
|
|
646
|
+
fetchReportFileData({
|
|
647
|
+
apiKey: "test-key",
|
|
648
|
+
apiBaseUrl: "https://api.example.com",
|
|
649
|
+
})
|
|
650
|
+
).rejects.toThrow("Either reportId or checkId is required");
|
|
651
|
+
});
|
|
652
|
+
|
|
653
|
+
test("works with only checkId (no reportId)", async () => {
|
|
654
|
+
let capturedRequest: { url: string; options: RequestInit } | null = null;
|
|
655
|
+
|
|
656
|
+
globalThis.fetch = mock((url: string, options: RequestInit) => {
|
|
657
|
+
capturedRequest = { url, options };
|
|
658
|
+
return Promise.resolve(
|
|
659
|
+
new Response(JSON.stringify([]), {
|
|
660
|
+
status: 200,
|
|
661
|
+
headers: { "Content-Type": "application/json" },
|
|
662
|
+
})
|
|
663
|
+
);
|
|
664
|
+
}) as unknown as typeof fetch;
|
|
665
|
+
|
|
666
|
+
await fetchReportFileData({
|
|
667
|
+
apiKey: "test-key",
|
|
668
|
+
apiBaseUrl: "https://api.example.com",
|
|
669
|
+
checkId: "H002",
|
|
670
|
+
});
|
|
671
|
+
|
|
672
|
+
expect(capturedRequest).not.toBeNull();
|
|
673
|
+
const url = new URL(capturedRequest!.url);
|
|
674
|
+
expect(url.pathname).toBe("/checkup_report_file_data");
|
|
675
|
+
expect(url.searchParams.has("checkup_report_id")).toBe(false);
|
|
676
|
+
expect(url.searchParams.get("check_id")).toBe("eq.H002");
|
|
677
|
+
expect(url.searchParams.get("order")).toBe("id.asc");
|
|
678
|
+
});
|
|
679
|
+
|
|
680
|
+
test("constructs correct URL with reportId", async () => {
|
|
681
|
+
let capturedRequest: { url: string; options: RequestInit } | null = null;
|
|
682
|
+
|
|
683
|
+
globalThis.fetch = mock((url: string, options: RequestInit) => {
|
|
684
|
+
capturedRequest = { url, options };
|
|
685
|
+
return Promise.resolve(
|
|
686
|
+
new Response(JSON.stringify([]), {
|
|
687
|
+
status: 200,
|
|
688
|
+
headers: { "Content-Type": "application/json" },
|
|
689
|
+
})
|
|
690
|
+
);
|
|
691
|
+
}) as unknown as typeof fetch;
|
|
692
|
+
|
|
693
|
+
await fetchReportFileData({
|
|
694
|
+
apiKey: "test-key",
|
|
695
|
+
apiBaseUrl: "https://api.example.com",
|
|
696
|
+
reportId: 123,
|
|
697
|
+
});
|
|
698
|
+
|
|
699
|
+
const url = new URL(capturedRequest!.url);
|
|
700
|
+
expect(url.pathname).toBe("/checkup_report_file_data");
|
|
701
|
+
expect(url.searchParams.get("checkup_report_id")).toBe("eq.123");
|
|
702
|
+
expect(url.searchParams.get("order")).toBe("id.asc");
|
|
703
|
+
});
|
|
704
|
+
|
|
705
|
+
test("applies optional filters", async () => {
|
|
706
|
+
let capturedRequest: { url: string; options: RequestInit } | null = null;
|
|
707
|
+
|
|
708
|
+
globalThis.fetch = mock((url: string, options: RequestInit) => {
|
|
709
|
+
capturedRequest = { url, options };
|
|
710
|
+
return Promise.resolve(
|
|
711
|
+
new Response(JSON.stringify([]), {
|
|
712
|
+
status: 200,
|
|
713
|
+
headers: { "Content-Type": "application/json" },
|
|
714
|
+
})
|
|
715
|
+
);
|
|
716
|
+
}) as unknown as typeof fetch;
|
|
717
|
+
|
|
718
|
+
await fetchReportFileData({
|
|
719
|
+
apiKey: "test-key",
|
|
720
|
+
apiBaseUrl: "https://api.example.com",
|
|
721
|
+
reportId: 123,
|
|
722
|
+
type: "json",
|
|
723
|
+
checkId: "H001",
|
|
724
|
+
});
|
|
725
|
+
|
|
726
|
+
const url = new URL(capturedRequest!.url);
|
|
727
|
+
expect(url.searchParams.get("type")).toBe("eq.json");
|
|
728
|
+
expect(url.searchParams.get("check_id")).toBe("eq.H001");
|
|
729
|
+
});
|
|
730
|
+
|
|
731
|
+
test("returns parsed response with data field", async () => {
|
|
732
|
+
const mockData = [
|
|
733
|
+
{
|
|
734
|
+
id: 200,
|
|
735
|
+
checkup_report_id: 123,
|
|
736
|
+
filename: "H002.md",
|
|
737
|
+
check_id: "H002",
|
|
738
|
+
type: "md",
|
|
739
|
+
created_at: "2025-01-01T00:00:00Z",
|
|
740
|
+
created_formatted: "2025-01-01 00:00:00",
|
|
741
|
+
project_id: 10,
|
|
742
|
+
project_name: "prod-db",
|
|
743
|
+
data: "# H002 Report\n\nUnused indexes found.\n",
|
|
744
|
+
},
|
|
745
|
+
{
|
|
746
|
+
id: 201,
|
|
747
|
+
checkup_report_id: 123,
|
|
748
|
+
filename: "H002.json",
|
|
749
|
+
check_id: "H002",
|
|
750
|
+
type: "json",
|
|
751
|
+
created_at: "2025-01-01T00:00:00Z",
|
|
752
|
+
created_formatted: "2025-01-01 00:00:00",
|
|
753
|
+
project_id: 10,
|
|
754
|
+
project_name: "prod-db",
|
|
755
|
+
data: '{"unused_indexes": []}',
|
|
756
|
+
},
|
|
757
|
+
];
|
|
758
|
+
|
|
759
|
+
globalThis.fetch = mock(() =>
|
|
760
|
+
Promise.resolve(
|
|
761
|
+
new Response(JSON.stringify(mockData), {
|
|
762
|
+
status: 200,
|
|
763
|
+
headers: { "Content-Type": "application/json" },
|
|
764
|
+
})
|
|
765
|
+
)
|
|
766
|
+
) as unknown as typeof fetch;
|
|
767
|
+
|
|
768
|
+
const result = await fetchReportFileData({
|
|
769
|
+
apiKey: "test-key",
|
|
770
|
+
apiBaseUrl: "https://api.example.com",
|
|
771
|
+
reportId: 123,
|
|
772
|
+
});
|
|
773
|
+
|
|
774
|
+
expect(result.length).toBe(2);
|
|
775
|
+
expect(result[0].data).toContain("# H002 Report");
|
|
776
|
+
expect(result[1].data).toBe('{"unused_indexes": []}');
|
|
777
|
+
});
|
|
778
|
+
|
|
779
|
+
test("throws on error response", async () => {
|
|
780
|
+
globalThis.fetch = mock(() =>
|
|
781
|
+
Promise.resolve(
|
|
782
|
+
new Response('{"message": "Unauthorized"}', {
|
|
783
|
+
status: 401,
|
|
784
|
+
headers: { "Content-Type": "application/json" },
|
|
785
|
+
})
|
|
786
|
+
)
|
|
787
|
+
) as unknown as typeof fetch;
|
|
788
|
+
|
|
789
|
+
await expect(
|
|
790
|
+
fetchReportFileData({
|
|
791
|
+
apiKey: "invalid-key",
|
|
792
|
+
apiBaseUrl: "https://api.example.com",
|
|
793
|
+
reportId: 123,
|
|
794
|
+
})
|
|
795
|
+
).rejects.toThrow(/Failed to fetch report file data/);
|
|
796
|
+
});
|
|
797
|
+
|
|
798
|
+
test("logs debug info when debug is true", async () => {
|
|
799
|
+
const consoleSpy = spyOn(console, "error").mockImplementation(() => {});
|
|
800
|
+
|
|
801
|
+
globalThis.fetch = mock(() =>
|
|
802
|
+
Promise.resolve(
|
|
803
|
+
new Response(JSON.stringify([]), {
|
|
804
|
+
status: 200,
|
|
805
|
+
headers: { "Content-Type": "application/json" },
|
|
806
|
+
})
|
|
807
|
+
)
|
|
808
|
+
) as unknown as typeof fetch;
|
|
809
|
+
|
|
810
|
+
await fetchReportFileData({
|
|
811
|
+
apiKey: "test-key",
|
|
812
|
+
apiBaseUrl: "https://api.example.com",
|
|
813
|
+
reportId: 1,
|
|
814
|
+
debug: true,
|
|
815
|
+
});
|
|
816
|
+
|
|
817
|
+
const calls = consoleSpy.mock.calls.map((c) => c[0]);
|
|
818
|
+
expect(calls.some((c: string) => c.includes("Debug: Resolved API base URL"))).toBe(true);
|
|
819
|
+
expect(calls.some((c: string) => c.includes("Debug: Response status"))).toBe(true);
|
|
820
|
+
|
|
821
|
+
consoleSpy.mockRestore();
|
|
822
|
+
});
|
|
823
|
+
|
|
824
|
+
test("throws on invalid JSON response", async () => {
|
|
825
|
+
globalThis.fetch = mock(() =>
|
|
826
|
+
Promise.resolve(
|
|
827
|
+
new Response("not valid json", {
|
|
828
|
+
status: 200,
|
|
829
|
+
headers: { "Content-Type": "text/plain" },
|
|
830
|
+
})
|
|
831
|
+
)
|
|
832
|
+
) as unknown as typeof fetch;
|
|
833
|
+
|
|
834
|
+
await expect(
|
|
835
|
+
fetchReportFileData({
|
|
836
|
+
apiKey: "test-key",
|
|
837
|
+
apiBaseUrl: "https://api.example.com",
|
|
838
|
+
reportId: 1,
|
|
839
|
+
})
|
|
840
|
+
).rejects.toThrow(/Failed to parse report file data response/);
|
|
841
|
+
});
|
|
842
|
+
});
|
|
843
|
+
|
|
844
|
+
// ---------------------------------------------------------------------------
|
|
845
|
+
// renderMarkdownForTerminal
|
|
846
|
+
// ---------------------------------------------------------------------------
|
|
847
|
+
describe("renderMarkdownForTerminal", () => {
|
|
848
|
+
test("returns empty string for empty input", () => {
|
|
849
|
+
expect(renderMarkdownForTerminal("")).toBe("");
|
|
850
|
+
});
|
|
851
|
+
|
|
852
|
+
test("passes plain text through", () => {
|
|
853
|
+
const result = renderMarkdownForTerminal("Just plain text");
|
|
854
|
+
expect(result).toContain("Just plain text");
|
|
855
|
+
});
|
|
856
|
+
|
|
857
|
+
test("renders # heading with bold+underline", () => {
|
|
858
|
+
const result = renderMarkdownForTerminal("# Hello World");
|
|
859
|
+
expect(result).toContain("\x1b[");
|
|
860
|
+
expect(result).toContain("Hello World");
|
|
861
|
+
});
|
|
862
|
+
|
|
863
|
+
test("renders ## heading with bold", () => {
|
|
864
|
+
const result = renderMarkdownForTerminal("## Section Title");
|
|
865
|
+
expect(result).toContain("\x1b[1m");
|
|
866
|
+
expect(result).toContain("Section Title");
|
|
867
|
+
});
|
|
868
|
+
|
|
869
|
+
test("renders **bold** text", () => {
|
|
870
|
+
const result = renderMarkdownForTerminal("This is **bold** text");
|
|
871
|
+
expect(result).toContain("\x1b[1m");
|
|
872
|
+
expect(result).toContain("bold");
|
|
873
|
+
});
|
|
874
|
+
|
|
875
|
+
test("renders `inline code`", () => {
|
|
876
|
+
const result = renderMarkdownForTerminal("Run `SELECT 1`");
|
|
877
|
+
expect(result).toContain("SELECT 1");
|
|
878
|
+
});
|
|
879
|
+
|
|
880
|
+
test("renders code blocks", () => {
|
|
881
|
+
const input = "```\nSELECT 1;\nSELECT 2;\n```";
|
|
882
|
+
const result = renderMarkdownForTerminal(input);
|
|
883
|
+
expect(result).toContain("SELECT 1;");
|
|
884
|
+
expect(result).toContain("SELECT 2;");
|
|
885
|
+
});
|
|
886
|
+
|
|
887
|
+
test("renders horizontal rules", () => {
|
|
888
|
+
const result = renderMarkdownForTerminal("---");
|
|
889
|
+
expect(result.replace(/\x1b\[[0-9;]*m/g, "").trim().length).toBeGreaterThan(3);
|
|
890
|
+
});
|
|
891
|
+
|
|
892
|
+
test("renders bullet lists", () => {
|
|
893
|
+
const result = renderMarkdownForTerminal("- item one\n- item two");
|
|
894
|
+
expect(result).toContain("item one");
|
|
895
|
+
expect(result).toContain("item two");
|
|
896
|
+
});
|
|
897
|
+
|
|
898
|
+
test("does not italicize underscores inside identifiers", () => {
|
|
899
|
+
const result = renderMarkdownForTerminal("goodvibes_local_monitoring_dev");
|
|
900
|
+
// Strip ANSI codes and check the text is unchanged
|
|
901
|
+
const stripped = result.replace(/\x1b\[[0-9;]*m/g, "");
|
|
902
|
+
expect(stripped).toBe("goodvibes_local_monitoring_dev");
|
|
903
|
+
// Must NOT contain italic ANSI code
|
|
904
|
+
expect(result).not.toContain("\x1b[3m");
|
|
905
|
+
});
|
|
906
|
+
|
|
907
|
+
test("renders _word_ as italic at word boundaries", () => {
|
|
908
|
+
const result = renderMarkdownForTerminal("This is _unknown_ value");
|
|
909
|
+
expect(result).toContain("\x1b[3m");
|
|
910
|
+
expect(result).toContain("unknown");
|
|
911
|
+
});
|
|
912
|
+
|
|
913
|
+
test("renders *italic* with single asterisks", () => {
|
|
914
|
+
const result = renderMarkdownForTerminal("This is *italic* text");
|
|
915
|
+
expect(result).toContain("\x1b[3m");
|
|
916
|
+
expect(result).toContain("italic");
|
|
917
|
+
});
|
|
918
|
+
|
|
919
|
+
test("renders __bold__ with double underscores", () => {
|
|
920
|
+
const result = renderMarkdownForTerminal("This is __bold__ text");
|
|
921
|
+
expect(result).toContain("\x1b[1m");
|
|
922
|
+
expect(result).toContain("bold");
|
|
923
|
+
});
|
|
924
|
+
});
|
|
925
|
+
|
|
926
|
+
// ---------------------------------------------------------------------------
|
|
927
|
+
// parseFlexibleDate
|
|
928
|
+
// ---------------------------------------------------------------------------
|
|
929
|
+
describe("parseFlexibleDate", () => {
|
|
930
|
+
test("parses YYYY-MM-DD", () => {
|
|
931
|
+
expect(parseFlexibleDate("2025-01-15")).toBe("2025-01-15T00:00:00.000Z");
|
|
932
|
+
});
|
|
933
|
+
|
|
934
|
+
test("parses YYYY-MM-DDTHH:mm:ss", () => {
|
|
935
|
+
expect(parseFlexibleDate("2025-01-15T10:30:00")).toBe("2025-01-15T10:30:00.000Z");
|
|
936
|
+
});
|
|
937
|
+
|
|
938
|
+
test("parses YYYY-MM-DD HH:mm:ss", () => {
|
|
939
|
+
expect(parseFlexibleDate("2025-01-15 10:30:00")).toBe("2025-01-15T10:30:00.000Z");
|
|
940
|
+
});
|
|
941
|
+
|
|
942
|
+
test("parses YYYY-MM-DD HH:mm", () => {
|
|
943
|
+
expect(parseFlexibleDate("2025-01-15 10:30")).toBe("2025-01-15T10:30:00.000Z");
|
|
944
|
+
});
|
|
945
|
+
|
|
946
|
+
test("parses DD.MM.YYYY", () => {
|
|
947
|
+
expect(parseFlexibleDate("15.01.2025")).toBe("2025-01-15T00:00:00.000Z");
|
|
948
|
+
});
|
|
949
|
+
|
|
950
|
+
test("parses DD.MM.YYYY HH:mm", () => {
|
|
951
|
+
expect(parseFlexibleDate("15.01.2025 10:30")).toBe("2025-01-15T10:30:00.000Z");
|
|
952
|
+
});
|
|
953
|
+
|
|
954
|
+
test("parses DD.MM.YYYY HH:mm:ss", () => {
|
|
955
|
+
expect(parseFlexibleDate("15.01.2025 10:30:45")).toBe("2025-01-15T10:30:45.000Z");
|
|
956
|
+
});
|
|
957
|
+
|
|
958
|
+
test("parses D.M.YYYY (single-digit day/month)", () => {
|
|
959
|
+
expect(parseFlexibleDate("5.1.2025")).toBe("2025-01-05T00:00:00.000Z");
|
|
960
|
+
});
|
|
961
|
+
|
|
962
|
+
test("trims whitespace", () => {
|
|
963
|
+
expect(parseFlexibleDate(" 2025-01-15 ")).toBe("2025-01-15T00:00:00.000Z");
|
|
964
|
+
});
|
|
965
|
+
|
|
966
|
+
test("throws on unrecognized format", () => {
|
|
967
|
+
expect(() => parseFlexibleDate("Jan 15, 2025")).toThrow(/Unrecognized date format/);
|
|
968
|
+
});
|
|
969
|
+
|
|
970
|
+
test("throws on invalid date values", () => {
|
|
971
|
+
expect(() => parseFlexibleDate("2025-13-45")).toThrow(/Invalid date/);
|
|
972
|
+
});
|
|
973
|
+
|
|
974
|
+
test("throws on invalid DD.MM.YYYY values", () => {
|
|
975
|
+
expect(() => parseFlexibleDate("45.13.2025")).toThrow(/Invalid date/);
|
|
976
|
+
});
|
|
977
|
+
});
|