@hoox-sh/hoox-tui 0.2.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 (104) hide show
  1. package/README.md +247 -0
  2. package/bin/hoox-tui.js +35 -0
  3. package/dist/main.js +17594 -0
  4. package/package.json +72 -0
  5. package/src/app.tsx +696 -0
  6. package/src/cli-bridge-test-double.ts +212 -0
  7. package/src/components/layout/layout.test.tsx +205 -0
  8. package/src/components/layout/sidebar.tsx +69 -0
  9. package/src/components/layout/statusbar.test.tsx +423 -0
  10. package/src/components/layout/statusbar.tsx +348 -0
  11. package/src/components/shared/animated-border.tsx +58 -0
  12. package/src/components/shared/chrome.test.tsx +29 -0
  13. package/src/components/shared/command-palette.tsx +361 -0
  14. package/src/components/shared/cool-brackets.tsx +75 -0
  15. package/src/components/shared/crash-screen.tsx +167 -0
  16. package/src/components/shared/error-boundary.tsx +99 -0
  17. package/src/components/shared/keybinding-hint.tsx +37 -0
  18. package/src/components/shared/panel.tsx +61 -0
  19. package/src/components/shared/quit-modal.tsx +65 -0
  20. package/src/components/shared/shared.test.tsx +538 -0
  21. package/src/components/shared/spinner.tsx +85 -0
  22. package/src/components/shared/status-dot.tsx +54 -0
  23. package/src/components/shared/view-header.tsx +53 -0
  24. package/src/components/ui/connection-toasts.test.ts +50 -0
  25. package/src/components/ui/connection-toasts.ts +215 -0
  26. package/src/components/ui/dialog.tsx +281 -0
  27. package/src/components/ui/select.tsx +197 -0
  28. package/src/components/ui/toast.tsx +185 -0
  29. package/src/components/views/ai-chat.test.tsx +81 -0
  30. package/src/components/views/ai-chat.tsx +576 -0
  31. package/src/components/views/config-editor/action-bar.tsx +102 -0
  32. package/src/components/views/config-editor/code-editor.tsx +597 -0
  33. package/src/components/views/config-editor/file-tree.tsx +113 -0
  34. package/src/components/views/config-editor/types.ts +37 -0
  35. package/src/components/views/config-editor.test.tsx +465 -0
  36. package/src/components/views/config-editor.tsx +709 -0
  37. package/src/components/views/dashboard/alerts-panel.tsx +154 -0
  38. package/src/components/views/dashboard/auto-repair-panel.tsx +229 -0
  39. package/src/components/views/dashboard/model-health-section.tsx +276 -0
  40. package/src/components/views/dashboard/pyne-health-section.tsx +112 -0
  41. package/src/components/views/dashboard.test.tsx +347 -0
  42. package/src/components/views/dashboard.tsx +647 -0
  43. package/src/components/views/db-query.test.tsx +115 -0
  44. package/src/components/views/db-query.tsx +849 -0
  45. package/src/components/views/edge-topology.test.tsx +198 -0
  46. package/src/components/views/edge-topology.tsx +572 -0
  47. package/src/components/views/kv-viewer.test.tsx +241 -0
  48. package/src/components/views/kv-viewer.tsx +717 -0
  49. package/src/components/views/logs-viewer.test.tsx +273 -0
  50. package/src/components/views/logs-viewer.tsx +616 -0
  51. package/src/components/views/queue-depth.test.tsx +221 -0
  52. package/src/components/views/queue-depth.tsx +376 -0
  53. package/src/components/views/secrets-viewer.test.tsx +65 -0
  54. package/src/components/views/secrets-viewer.tsx +548 -0
  55. package/src/components/views/service-manager.test.tsx +358 -0
  56. package/src/components/views/service-manager.tsx +946 -0
  57. package/src/components/views/settings.test.tsx +648 -0
  58. package/src/components/views/settings.tsx +1497 -0
  59. package/src/components/views/setup-wizard.test.tsx +297 -0
  60. package/src/components/views/setup-wizard.tsx +1321 -0
  61. package/src/components/views/trade-monitor.test.tsx +592 -0
  62. package/src/components/views/trade-monitor.tsx +745 -0
  63. package/src/components/views/worker-detail.test.tsx +289 -0
  64. package/src/components/views/worker-detail.tsx +676 -0
  65. package/src/components/views/worker-settings.test.tsx +43 -0
  66. package/src/components/views/worker-settings.tsx +563 -0
  67. package/src/components/views/workers-overview.test.tsx +260 -0
  68. package/src/components/views/workers-overview.tsx +631 -0
  69. package/src/hooks/hooks.test.ts +72 -0
  70. package/src/hooks/index.ts +11 -0
  71. package/src/hooks/renderer-ref.ts +22 -0
  72. package/src/hooks/use-keyboard.ts +130 -0
  73. package/src/hooks/use-polling.ts +73 -0
  74. package/src/hooks/use-service-data.ts +18 -0
  75. package/src/main.tsx +117 -0
  76. package/src/network-test-double.ts +133 -0
  77. package/src/opentui-augment.d.ts +45 -0
  78. package/src/opentui-jsx.d.ts +70 -0
  79. package/src/services/cli-bridge/index.ts +1662 -0
  80. package/src/services/cli-bridge/standalone.ts +315 -0
  81. package/src/services/cli-bridge/types.ts +307 -0
  82. package/src/services/cli-bridge.test.ts +406 -0
  83. package/src/services/clipboard.test.ts +57 -0
  84. package/src/services/clipboard.ts +180 -0
  85. package/src/services/dashboard-settings-loader.ts +31 -0
  86. package/src/services/dev-log.test.ts +168 -0
  87. package/src/services/dev-log.ts +194 -0
  88. package/src/services/hoox-path-service.test.ts +183 -0
  89. package/src/services/hoox-path-service.ts +185 -0
  90. package/src/services/tui-connection.test.ts +212 -0
  91. package/src/services/tui-connection.ts +236 -0
  92. package/src/services/tui-storage.test.ts +92 -0
  93. package/src/services/tui-storage.ts +86 -0
  94. package/src/stores/config-store.test.ts +360 -0
  95. package/src/stores/service-store.test.ts +1026 -0
  96. package/src/stores/session-restore.test.ts +134 -0
  97. package/src/stores/ui-store.test.ts +276 -0
  98. package/src/test-setup.ts +265 -0
  99. package/src/test-utils.ts +179 -0
  100. package/src/types.ts +153 -0
  101. package/src/utils/colors.test.ts +326 -0
  102. package/src/utils/formatters.test.ts +236 -0
  103. package/src/view-registry.test.ts +132 -0
  104. package/src/view-registry.tsx +368 -0
@@ -0,0 +1,648 @@
1
+ /**
2
+ * Copyright (c) 2026 HOOX · HOOX · jango-blockchained (hoox-sh)
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /** @jsxImportSource @opentui/react */
7
+ /**
8
+ * Tests for SettingsView — validates all 4 panels render correctly,
9
+ * config store subscription, theme/notification display, and Check Setup panel.
10
+ *
11
+ * Uses the real config store (setState) and the process-wide CLI bridge test
12
+ * double from test-setup.ts. No mock.module for stores or cli-bridge.
13
+ */
14
+ import { describe, it, expect, beforeEach, afterEach } from "bun:test";
15
+ import { testRender } from "@opentui/react/test-utils";
16
+ import type { ViewId, NotificationPreferences } from "@hoox-sh/hoox-shared";
17
+ import { useConfigStore } from "@hoox-sh/hoox-shared/stores/config-store";
18
+ import { cliBridgeDouble, resetCliBridgeDouble } from "../../test-utils";
19
+ import { SettingsView } from "./settings";
20
+
21
+ // ─── Check Setup fixtures ────────────────────────────────────────────────────
22
+
23
+ const defaultCheckReport = {
24
+ success: false,
25
+ categories: [
26
+ {
27
+ name: "Config",
28
+ checks: [
29
+ {
30
+ name: "wrangler.jsonc validation",
31
+ success: true,
32
+ errors: [],
33
+ warnings: [],
34
+ },
35
+ {
36
+ name: "Global config",
37
+ success: false,
38
+ errors: ["global.cloudflare_account_id is required"],
39
+ warnings: [],
40
+ },
41
+ {
42
+ name: "Worker paths",
43
+ success: true,
44
+ errors: [],
45
+ warnings: [],
46
+ },
47
+ ],
48
+ },
49
+ {
50
+ name: "Infrastructure",
51
+ checks: [
52
+ {
53
+ name: "D1 Database",
54
+ success: false,
55
+ errors: ["Database 'hoox-db' not found in remote account"],
56
+ warnings: [],
57
+ },
58
+ {
59
+ name: "KV Namespaces",
60
+ success: true,
61
+ errors: [],
62
+ warnings: [],
63
+ },
64
+ {
65
+ name: "R2 Buckets",
66
+ success: true,
67
+ errors: [],
68
+ warnings: [],
69
+ },
70
+ {
71
+ name: "Queues",
72
+ success: true,
73
+ errors: [],
74
+ warnings: ["Queue 'trade-queue' uses default settings"],
75
+ },
76
+ ],
77
+ },
78
+ {
79
+ name: "Secrets",
80
+ checks: [
81
+ {
82
+ name: "Secrets (local)",
83
+ success: true,
84
+ errors: [],
85
+ warnings: [],
86
+ },
87
+ {
88
+ name: "Secrets (remote)",
89
+ success: false,
90
+ errors: ["Missing secret INTERNAL_KEY on worker hoox"],
91
+ warnings: [],
92
+ },
93
+ ],
94
+ },
95
+ {
96
+ name: "Database",
97
+ checks: [
98
+ {
99
+ name: "Database",
100
+ success: true,
101
+ errors: [],
102
+ warnings: [],
103
+ },
104
+ ],
105
+ },
106
+ ],
107
+ summary: {
108
+ total: 10,
109
+ passed: 6,
110
+ failed: 3,
111
+ warnings: 1,
112
+ },
113
+ };
114
+
115
+ function configureCheckSetup(opts?: {
116
+ success?: boolean;
117
+ data?: unknown;
118
+ stderr?: string;
119
+ duration?: number;
120
+ }): void {
121
+ const success = opts?.success ?? true;
122
+ const data = opts?.data === undefined ? defaultCheckReport : opts.data;
123
+ const stderr = opts?.stderr ?? "";
124
+ const duration = opts?.duration ?? 1234;
125
+ cliBridgeDouble.checkSetup.mockImplementation(
126
+ () =>
127
+ Promise.resolve({
128
+ success,
129
+ exitCode: success ? 0 : 1,
130
+ stdout: success && data ? JSON.stringify(data) : "",
131
+ stderr,
132
+ data,
133
+ duration,
134
+ command: "hoox check setup (test double)",
135
+ errorType: success ? null : "non-zero-exit",
136
+ }) as never
137
+ );
138
+ }
139
+
140
+ function resetConfigStore(): void {
141
+ useConfigStore.setState({
142
+ theme: "dark",
143
+ refreshIntervalMs: 500,
144
+ defaultView: "dashboard" as ViewId,
145
+ activeExchanges: [],
146
+ notifications: {
147
+ alerts: true,
148
+ trades: false,
149
+ debug: false,
150
+ system: true,
151
+ } as NotificationPreferences,
152
+ soundEnabled: false,
153
+ });
154
+ }
155
+
156
+ // ─── Helpers ────────────────────────────────────────────────────────────────
157
+
158
+ /** Render the SettingsView and return the captured frame as a string. */
159
+ async function renderSettings(width = 120, height = 24): Promise<string> {
160
+ const { captureCharFrame, renderOnce } = await testRender(<SettingsView />, {
161
+ width,
162
+ height,
163
+ exitOnCtrlC: false,
164
+ });
165
+ await renderOnce();
166
+ return captureCharFrame();
167
+ }
168
+
169
+ /** Render the SettingsView with the Check Setup check auto-triggered. */
170
+ async function renderSettingsWithCheck(
171
+ width = 140,
172
+ height = 60
173
+ ): Promise<string> {
174
+ // Tell the view to auto-run the check on mount via a useEffect.
175
+ (
176
+ globalThis as unknown as { __hooxTestAutoCheckSetup?: boolean }
177
+ ).__hooxTestAutoCheckSetup = true;
178
+ try {
179
+ const { captureCharFrame, renderOnce } = await testRender(
180
+ <SettingsView />,
181
+ {
182
+ width,
183
+ height,
184
+ exitOnCtrlC: false,
185
+ }
186
+ );
187
+ // Initial render - triggers useEffect which starts checkSetup
188
+ await renderOnce();
189
+
190
+ // Wait until the check settles. The panel header appears immediately in
191
+ // the "running..." state, so only treat report/error content as ready.
192
+ for (let i = 0; i < 40; i++) {
193
+ await new Promise((r) => queueMicrotask(r));
194
+ await new Promise((r) => setTimeout(r, 15));
195
+ await renderOnce();
196
+ const frame = captureCharFrame();
197
+ const settled =
198
+ frame.includes("SETUP CHECK RESULTS") &&
199
+ !frame.includes("running...") &&
200
+ (frame.includes("passed") ||
201
+ frame.includes("failed to run") ||
202
+ frame.includes("did not return a valid CheckReport") ||
203
+ frame.includes("[PASS]") ||
204
+ frame.includes("[FAIL]"));
205
+ if (settled) {
206
+ return frame;
207
+ }
208
+ }
209
+ // Fallback - return whatever we have
210
+ return captureCharFrame();
211
+ } finally {
212
+ delete (globalThis as unknown as { __hooxTestAutoCheckSetup?: boolean })
213
+ .__hooxTestAutoCheckSetup;
214
+ }
215
+ }
216
+
217
+ // ─── Test Suite ──────────────────────────────────────────────────────────────
218
+
219
+ describe("SettingsView", () => {
220
+ beforeEach(() => {
221
+ resetCliBridgeDouble();
222
+ configureCheckSetup();
223
+ resetConfigStore();
224
+ });
225
+
226
+ // ── Rendering basics ─────────────────────────────────────────────────────
227
+
228
+ it("renders the settings header with SETTINGS title", async () => {
229
+ const output = await renderSettings();
230
+ expect(output).toContain("SETTINGS");
231
+ });
232
+
233
+ it("renders all four panel titles", async () => {
234
+ const output = await renderSettings(120);
235
+ expect(output).toContain("THEME");
236
+ expect(output).toContain("NOTIFICATIONS");
237
+ expect(output).toContain("KEYBOARD");
238
+ expect(output).toContain("DATA");
239
+ });
240
+
241
+ it("renders within an error boundary wrapper", async () => {
242
+ const output = await renderSettings();
243
+ expect(output).toContain("SETTINGS");
244
+ // If error boundary had caught an error, we'd see "Failed to load Settings"
245
+ expect(output).not.toContain("Failed to load Settings");
246
+ });
247
+
248
+ // ── Theme Panel ──────────────────────────────────────────────────────────
249
+
250
+ it("shows dark-only theme (light not implemented)", async () => {
251
+ useConfigStore.setState({ theme: "dark" });
252
+ const output = await renderSettings(120);
253
+ // OpenTUI may collapse spaces in the frame capture; match stable tokens.
254
+ expect(output).toMatch(/\(•\)\s*DARK/);
255
+ expect(output).toContain("Light not implemented");
256
+ expect(output).not.toMatch(/\(•\)\s*LIGHT/);
257
+ expect(output).not.toMatch(/\( \)\s*LIGHT/);
258
+ });
259
+
260
+ it("shows connection readout without secrets", async () => {
261
+ const prevToken = process.env.HOOX_API_TOKEN;
262
+ process.env.HOOX_API_TOKEN = "super-secret-test-token-do-not-leak";
263
+ try {
264
+ const output = await renderSettings(120);
265
+ expect(output).toContain("CONNECTION");
266
+ expect(output).toMatch(/LOCAL|REMOTE/);
267
+ expect(output).toMatch(/public|access|mtls|tunnel/);
268
+ expect(output).toContain("Auth");
269
+ expect(output).toContain("hx config transport");
270
+ // Must never echo the raw token or Bearer value
271
+ expect(output).not.toContain("super-secret-test-token-do-not-leak");
272
+ expect(output).not.toMatch(/Bearer\s+super-secret/i);
273
+ } finally {
274
+ if (prevToken === undefined) delete process.env.HOOX_API_TOKEN;
275
+ else process.env.HOOX_API_TOKEN = prevToken;
276
+ }
277
+ });
278
+
279
+ it("shows current refresh rate with 500ms default", async () => {
280
+ useConfigStore.setState({ refreshIntervalMs: 500 });
281
+ const output = await renderSettings();
282
+ expect(output).toContain("500ms");
283
+ });
284
+
285
+ it("shows refresh rate in seconds for 1s+ values", async () => {
286
+ useConfigStore.setState({ refreshIntervalMs: 2000 });
287
+ const output = await renderSettings();
288
+ expect(output).toContain("2s");
289
+ });
290
+
291
+ it("shows current default view label", async () => {
292
+ useConfigStore.setState({ defaultView: "trade-monitor" });
293
+ const output = await renderSettings();
294
+ expect(output).toContain("Trade Monitor");
295
+ });
296
+
297
+ it("renders the Reset to Defaults button", async () => {
298
+ const output = await renderSettings();
299
+ expect(output).toContain("RESET TO DEFAULTS");
300
+ });
301
+
302
+ // ── Notifications Panel ──────────────────────────────────────────────────
303
+
304
+ it("shows all four notification channels", async () => {
305
+ const output = await renderSettings();
306
+ expect(output).toContain("ALERTS");
307
+ expect(output).toContain("TRADES");
308
+ expect(output).toContain("DEBUG");
309
+ expect(output).toContain("SYSTEM");
310
+ });
311
+
312
+ it("shows checked [x] for enabled notifications", async () => {
313
+ useConfigStore.setState({
314
+ notifications: {
315
+ alerts: true,
316
+ trades: false,
317
+ debug: false,
318
+ system: true,
319
+ },
320
+ });
321
+ const output = await renderSettings();
322
+ // The output should contain [x] for alerts and system
323
+ expect(output).toContain("[x] ALERTS");
324
+ expect(output).toContain("[x] SYSTEM");
325
+ });
326
+
327
+ it("shows unchecked [ ] for disabled notifications", async () => {
328
+ useConfigStore.setState({
329
+ notifications: {
330
+ alerts: true,
331
+ trades: false,
332
+ debug: false,
333
+ system: true,
334
+ },
335
+ });
336
+ const output = await renderSettings();
337
+ expect(output).toContain("[ ] TRADES");
338
+ expect(output).toContain("[ ] DEBUG");
339
+ });
340
+
341
+ it("shows Sound toggle", async () => {
342
+ const output = await renderSettings();
343
+ expect(output).toContain("SOUND");
344
+ });
345
+
346
+ // ── Keyboard Panel ───────────────────────────────────────────────────────
347
+
348
+ it("shows Ctrl+1..9 shortcut", async () => {
349
+ const output = await renderSettings();
350
+ expect(output).toContain("Ctrl+1..9");
351
+ expect(output).toContain("Switch to view");
352
+ });
353
+
354
+ it("shows Ctrl+P command palette shortcut", async () => {
355
+ const output = await renderSettings();
356
+ expect(output).toContain("Ctrl+P");
357
+ expect(output).toContain("Command Palette");
358
+ });
359
+
360
+ it("shows Ctrl+Q quit shortcut", async () => {
361
+ const output = await renderSettings();
362
+ expect(output).toContain("Ctrl+Q");
363
+ expect(output).toContain("Quit");
364
+ });
365
+
366
+ it("shows Esc back shortcut", async () => {
367
+ const output = await renderSettings();
368
+ expect(output).toContain("Esc");
369
+ });
370
+
371
+ it("shows Tab navigation shortcut", async () => {
372
+ const output = await renderSettings();
373
+ expect(output).toContain("Tab");
374
+ expect(output).toContain("Next panel");
375
+ });
376
+
377
+ it("shows Space toggle shortcut", async () => {
378
+ const output = await renderSettings();
379
+ expect(output).toContain("Space");
380
+ expect(output).toContain("Toggle");
381
+ });
382
+
383
+ it("shows read-only disclaimer", async () => {
384
+ const output = await renderSettings();
385
+ // The disclaimer text wraps inside the keyboard panel; the first line
386
+ // is the most reliable substring across viewport widths.
387
+ expect(output).toContain("These shortcuts are global");
388
+ });
389
+
390
+ // ── Data Panel ───────────────────────────────────────────────────────────
391
+
392
+ it("shows data management action buttons", async () => {
393
+ const output = await renderSettings(120);
394
+ expect(output).toContain("CLEAR CACHE");
395
+ expect(output).toContain("EXPORT DATA");
396
+ expect(output).toContain("IMPORT DATA");
397
+ });
398
+
399
+ it("shows About section with version", async () => {
400
+ const output = await renderSettings(120);
401
+ expect(output).toContain("HOOX");
402
+ });
403
+
404
+ it("shows tech stack in About", async () => {
405
+ const output = await renderSettings(120);
406
+ expect(output).toContain("OpenTUI + Bun");
407
+ });
408
+
409
+ it("shows GitHub link in About", async () => {
410
+ const output = await renderSettings(120);
411
+ expect(output).toContain("github.com/hoox/");
412
+ });
413
+
414
+ // ── Config Store Integration ─────────────────────────────────────────────
415
+
416
+ it("coerces stored light theme to dark on mount", async () => {
417
+ useConfigStore.setState({ theme: "light" });
418
+ await renderSettings();
419
+ expect(useConfigStore.getState().theme).toBe("dark");
420
+ });
421
+
422
+ it("shows refresh rate as 500ms when config defaults to 500", async () => {
423
+ useConfigStore.setState({ refreshIntervalMs: 500 });
424
+ const output = await renderSettings();
425
+ expect(output).toContain("500ms");
426
+ });
427
+
428
+ // ── Layout ───────────────────────────────────────────────────────────────
429
+
430
+ it("renders the keyboard hint bar", async () => {
431
+ const output = await renderSettings();
432
+ expect(output).toContain("Tab to switch panels");
433
+ });
434
+
435
+ // ── Check Setup results panel ───────────────────────────────────────────
436
+
437
+ describe("Check Setup results panel", () => {
438
+ /** Restore default report between cases. */
439
+ afterEach(() => {
440
+ configureCheckSetup();
441
+ });
442
+
443
+ it("renders the CHECK SETUP button in the data panel", async () => {
444
+ const output = await renderSettings(120);
445
+ expect(output).toContain("CHECK SETUP");
446
+ });
447
+
448
+ it("does not show the results panel before the check runs", async () => {
449
+ const output = await renderSettings(120);
450
+ expect(output).not.toContain("SETUP CHECK RESULTS");
451
+ });
452
+
453
+ it("shows the results panel after the check completes", async () => {
454
+ const output = await renderSettingsWithCheck();
455
+ expect(output).toContain("SETUP CHECK RESULTS");
456
+ });
457
+
458
+ it("displays the summary counts from the report", async () => {
459
+ const output = await renderSettingsWithCheck();
460
+ // 6 passed, 3 failed, 1 warning
461
+ expect(output).toContain("6 passed");
462
+ expect(output).toContain("3 failed");
463
+ expect(output).toContain("1 warnings");
464
+ });
465
+
466
+ it("renders each category header (Config, Infrastructure, Secrets, Database)", async () => {
467
+ const output = await renderSettingsWithCheck();
468
+ expect(output).toContain("CONFIG");
469
+ expect(output).toContain("INFRASTRUCTURE");
470
+ expect(output).toContain("SECRETS");
471
+ expect(output).toContain("DATABASE");
472
+ });
473
+
474
+ it("renders PASS badge for successful checks", async () => {
475
+ const output = await renderSettingsWithCheck();
476
+ // wrangler.jsonc validation passed in the default report
477
+ expect(output).toContain("[PASS]");
478
+ });
479
+
480
+ it("renders FAIL badge for failed checks", async () => {
481
+ const output = await renderSettingsWithCheck();
482
+ // Global config failed in the default report
483
+ expect(output).toContain("[FAIL]");
484
+ });
485
+
486
+ it("renders WARN badge for checks with warnings", async () => {
487
+ const output = await renderSettingsWithCheck();
488
+ // Queues check has a warning in the default report
489
+ expect(output).toContain("[WARN]");
490
+ });
491
+
492
+ it("shows each check name in the panel", async () => {
493
+ const output = await renderSettingsWithCheck();
494
+ expect(output).toContain("wrangler.jsonc validation");
495
+ expect(output).toContain("Global config");
496
+ expect(output).toContain("D1 Database");
497
+ expect(output).toContain("Secrets (remote)");
498
+ });
499
+
500
+ it("shows error details for failed checks", async () => {
501
+ const output = await renderSettingsWithCheck();
502
+ expect(output).toContain("global.cloudflare_account_id is required");
503
+ expect(output).toContain("Database 'hoox-db' not found");
504
+ expect(output).toContain("Missing secret INTERNAL_KEY on worker hoox");
505
+ });
506
+
507
+ it("shows warning details for checks with warnings", async () => {
508
+ const output = await renderSettingsWithCheck();
509
+ expect(output).toContain("Queue 'trade-queue' uses default settings");
510
+ });
511
+
512
+ it("includes a 'what it means' line for each failed check", async () => {
513
+ const output = await renderSettingsWithCheck();
514
+ // "Global config" is in SUGGESTED_FIXES — its `what:` line should render
515
+ expect(output).toContain("what:");
516
+ });
517
+
518
+ it("includes a 'suggested fix' line for each failed check", async () => {
519
+ const output = await renderSettingsWithCheck();
520
+ // Global config failed check has a known fix
521
+ expect(output).toContain("Set `global.cloudflare_account_id`");
522
+ });
523
+
524
+ it("includes a fix for missing D1 databases", async () => {
525
+ const output = await renderSettingsWithCheck();
526
+ // D1 Database is in SUGGESTED_FIXES — its fix mentions infra provision
527
+ expect(output).toContain("hoox infra provision");
528
+ });
529
+
530
+ it("includes a fix for missing remote secrets", async () => {
531
+ const output = await renderSettingsWithCheck();
532
+ // Secrets (remote) is in SUGGESTED_FIXES — its fix mentions secrets update-cf
533
+ expect(output).toContain("hoox secrets update-cf");
534
+ });
535
+
536
+ it("renders the [DISMISS] and [RE-RUN] buttons", async () => {
537
+ const output = await renderSettingsWithCheck();
538
+ expect(output).toContain("[ DISMISS ]");
539
+ expect(output).toContain("[ RE-RUN ]");
540
+ });
541
+
542
+ it("shows duration in seconds for the check", async () => {
543
+ // Mocked duration is 1234ms → ~1.2s
544
+ const output = await renderSettingsWithCheck();
545
+ expect(output).toContain("(1.2s)");
546
+ });
547
+
548
+ it("shows an error state when the CLI process fails", async () => {
549
+ configureCheckSetup({
550
+ success: false,
551
+ stderr: "hoox binary not found",
552
+ data: null,
553
+ });
554
+ const output = await renderSettingsWithCheck();
555
+ expect(output).toContain("SETUP CHECK RESULTS");
556
+ expect(output).toContain("hoox binary not found");
557
+ });
558
+
559
+ it("shows a graceful error when CLI returns invalid data shape", async () => {
560
+ configureCheckSetup({ data: { not: "a report" } });
561
+ const output = await renderSettingsWithCheck();
562
+ expect(output).toContain("SETUP CHECK RESULTS");
563
+ expect(output).toContain("did not return a valid CheckReport");
564
+ });
565
+
566
+ it("shows a generic 'what it means' for unknown failed check names", async () => {
567
+ configureCheckSetup({
568
+ data: {
569
+ success: false,
570
+ categories: [
571
+ {
572
+ name: "Custom",
573
+ checks: [
574
+ {
575
+ name: "Mystery check",
576
+ success: false,
577
+ errors: ["something broke"],
578
+ warnings: [],
579
+ },
580
+ ],
581
+ },
582
+ ],
583
+ summary: { total: 1, passed: 0, failed: 1, warnings: 0 },
584
+ },
585
+ });
586
+ const output = await renderSettingsWithCheck();
587
+ expect(output).toContain("Mystery check");
588
+ // The generic fallback message
589
+ expect(output).toContain("This check reported one or more errors");
590
+ });
591
+
592
+ it("still renders all 4 columns when the panel is open", async () => {
593
+ const output = await renderSettingsWithCheck();
594
+ expect(output).toContain("THEME");
595
+ expect(output).toContain("NOTIFICATIONS");
596
+ expect(output).toContain("KEYBOARD");
597
+ expect(output).toContain("DATA");
598
+ expect(output).toContain("SETUP CHECK RESULTS");
599
+ });
600
+
601
+ it("does not show the panel when checkSetup is not triggered", async () => {
602
+ // Default renderSettings() does NOT set the auto-trigger flag
603
+ const output = await renderSettings(120, 60);
604
+ expect(output).not.toContain("SETUP CHECK RESULTS");
605
+ });
606
+
607
+ it("renders correctly with an all-pass report", async () => {
608
+ configureCheckSetup({
609
+ data: {
610
+ success: true,
611
+ categories: [
612
+ {
613
+ name: "Config",
614
+ checks: [
615
+ {
616
+ name: "wrangler.jsonc validation",
617
+ success: true,
618
+ errors: [],
619
+ warnings: [],
620
+ },
621
+ {
622
+ name: "Global config",
623
+ success: true,
624
+ errors: [],
625
+ warnings: [],
626
+ },
627
+ ],
628
+ },
629
+ ],
630
+ summary: { total: 2, passed: 2, failed: 0, warnings: 0 },
631
+ },
632
+ });
633
+ const output = await renderSettingsWithCheck();
634
+ expect(output).toContain("2 passed");
635
+ expect(output).toContain("0 failed");
636
+ // No [FAIL] should appear
637
+ expect(output).not.toContain("[FAIL]");
638
+ });
639
+
640
+ it("renders the [CHECK SETUP] button alongside the panel after a run", async () => {
641
+ const output = await renderSettingsWithCheck();
642
+ // The original button in the DATA panel is still visible
643
+ expect(output).toContain("CHECK SETUP");
644
+ // And the panel is also visible
645
+ expect(output).toContain("SETUP CHECK RESULTS");
646
+ });
647
+ });
648
+ });