@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,289 @@
1
+ /**
2
+ * Copyright (c) 2026 HOOX · HOOX · jango-blockchained (hoox-sh)
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /**
7
+ * WorkerDetail view tests.
8
+ *
9
+ * Covers:
10
+ * - Worker name and status in header
11
+ * - 4 panes render with correct store data
12
+ * - Tab cycles focus between panes
13
+ * - Esc triggers goBack
14
+ * - Missing worker shows error state
15
+ *
16
+ * Uses Bun test runner. Tests are structural — verifying that
17
+ * the component produces the expected render tree given store state.
18
+ * Not a full TUI render test (which requires a terminal).
19
+ */
20
+ import { describe, test, expect, beforeEach } from "bun:test";
21
+
22
+ import { useServiceStore } from "@hoox-sh/hoox-shared/stores/service-store";
23
+ import { useUIStore } from "@hoox-sh/hoox-shared/stores/ui-store";
24
+ import {
25
+ makeWorker,
26
+ makeLog,
27
+ type TestLogEntry,
28
+ type ConnectionStatus,
29
+ } from "../../test-utils";
30
+ import { redactConfigEntries } from "./worker-detail";
31
+
32
+ /** Reset stores to initial state before each test */
33
+ function resetStores() {
34
+ useServiceStore.setState({
35
+ workers: [],
36
+ logs: [],
37
+ metrics: null,
38
+ connectionStatus: "offline" as ConnectionStatus,
39
+ selectedWorkerId: null,
40
+ lastUpdated: 0,
41
+ alerts: [],
42
+ tradeStream: [],
43
+ });
44
+ useUIStore.setState({
45
+ activeView: "worker-detail",
46
+ previousView: "workers",
47
+ sidebarExpanded: true,
48
+ modal: null,
49
+ commandPaletteOpen: false,
50
+ });
51
+ }
52
+
53
+ // ── Tests ────────────────────────────────────────────────────────────────────
54
+
55
+ describe("WorkerDetail", () => {
56
+ beforeEach(() => {
57
+ resetStores();
58
+ });
59
+
60
+ test("renders worker name and status in header", () => {
61
+ const worker = makeWorker({
62
+ name: "trade-executor",
63
+ status: "operational",
64
+ });
65
+
66
+ useServiceStore.setState({
67
+ workers: [worker],
68
+ selectedWorkerId: worker.id,
69
+ });
70
+
71
+ // The WorkerDetail component shows breadcrumb:
72
+ // "← BACK | trade-executor | █ OPERATIONAL"
73
+ // We verify the store is set correctly (structural test).
74
+ const selectedId = useServiceStore.getState().selectedWorkerId;
75
+ const found = useServiceStore
76
+ .getState()
77
+ .workers.find((w) => w.id === selectedId);
78
+
79
+ expect(selectedId).toBe("test-worker-1");
80
+ expect(found).toBeDefined();
81
+ expect(found!.name).toBe("trade-executor");
82
+ expect(found!.status).toBe("operational");
83
+ });
84
+
85
+ test("4 panes render with correct data from store", () => {
86
+ const worker = makeWorker({
87
+ cpu: 42.5,
88
+ memory: 64,
89
+ requests: 15000,
90
+ durableObjectCount: 3,
91
+ uptime: 86400,
92
+ edgeCount: 12,
93
+ });
94
+
95
+ useServiceStore.setState({
96
+ workers: [worker],
97
+ selectedWorkerId: worker.id,
98
+ });
99
+
100
+ const state = useServiceStore.getState();
101
+ const found = state.workers.find((w) => w.id === state.selectedWorkerId);
102
+
103
+ // Metrics pane data
104
+ expect(found!.cpu).toBe(42.5);
105
+ expect(found!.memory).toBe(64);
106
+ expect(found!.requests).toBe(15000);
107
+ expect(found!.uptime).toBe(86400);
108
+
109
+ // Durable Objects pane
110
+ expect(found!.durableObjectCount).toBe(3);
111
+
112
+ // Config Preview data
113
+ expect(found!.version).toBe("1.2.3");
114
+ expect(found!.edgeCount).toBe(12);
115
+ });
116
+
117
+ test("tab cycles focus between panes", () => {
118
+ // Focus starts at 0 (Metrics)
119
+ // After 4 tabs, it should wrap back to 0
120
+ let focus = 0;
121
+ const PANE_COUNT = 4;
122
+
123
+ focus = (focus + 1) % PANE_COUNT; // → 1 (Live Logs)
124
+ expect(focus).toBe(1);
125
+
126
+ focus = (focus + 1) % PANE_COUNT; // → 2 (Durable Objects)
127
+ expect(focus).toBe(2);
128
+
129
+ focus = (focus + 1) % PANE_COUNT; // → 3 (Config Preview)
130
+ expect(focus).toBe(3);
131
+
132
+ focus = (focus + 1) % PANE_COUNT; // → 0 (Metrics)
133
+ expect(focus).toBe(0);
134
+ });
135
+
136
+ test("esc triggers goBack via UI store", () => {
137
+ // Set up: user navigated from "workers" to "worker-detail"
138
+ useUIStore.setState({
139
+ activeView: "worker-detail",
140
+ previousView: "workers",
141
+ });
142
+
143
+ // Simulate Esc → goBack()
144
+ useUIStore.getState().goBack();
145
+
146
+ const state = useUIStore.getState();
147
+ expect(state.activeView).toBe("workers");
148
+ expect(state.previousView).toBeNull();
149
+ });
150
+
151
+ test("error boundary handles missing worker", () => {
152
+ // No worker selected
153
+ useServiceStore.setState({
154
+ workers: [],
155
+ selectedWorkerId: "non-existent",
156
+ });
157
+
158
+ const state = useServiceStore.getState();
159
+ const found = state.workers.find((w) => w.id === state.selectedWorkerId);
160
+
161
+ expect(found).toBeUndefined();
162
+ expect(state.selectedWorkerId).toBe("non-existent");
163
+ // The component should render its fallback "Worker Not Found" UI
164
+ });
165
+
166
+ test("logs pane shows filtered worker logs", () => {
167
+ const worker = makeWorker({ id: "w1", name: "trade-executor" });
168
+ const logs: TestLogEntry[] = [
169
+ makeLog({ id: "l1", workerId: "w1", level: "info", message: "Started" }),
170
+ makeLog({
171
+ id: "l2",
172
+ workerId: "w1",
173
+ level: "warn",
174
+ message: "High latency",
175
+ }),
176
+ makeLog({
177
+ id: "l3",
178
+ workerId: "w2",
179
+ level: "error",
180
+ message: "Other worker",
181
+ }),
182
+ ];
183
+
184
+ useServiceStore.setState({
185
+ workers: [worker],
186
+ selectedWorkerId: "w1",
187
+ logs,
188
+ });
189
+
190
+ const state = useServiceStore.getState();
191
+
192
+ // Only logs for worker "w1" should be relevant
193
+ const workerLogs = state.logs.filter((l) => l.workerId === "w1");
194
+ expect(workerLogs).toHaveLength(2);
195
+ expect(workerLogs[0].level).toBe("info");
196
+ expect(workerLogs[1].level).toBe("warn");
197
+
198
+ // The log for worker "w2" should be filtered out
199
+ const otherLogs = state.logs.filter((l) => l.workerId === "w2");
200
+ expect(otherLogs).toHaveLength(1);
201
+ });
202
+
203
+ test("metrics pane reflects degraded worker correctly", () => {
204
+ const degraded = makeWorker({
205
+ id: "w-degraded",
206
+ name: "rate-limiter",
207
+ status: "degraded",
208
+ cpu: 85,
209
+ memory: 110,
210
+ requests: 500,
211
+ });
212
+
213
+ useServiceStore.setState({
214
+ workers: [degraded],
215
+ selectedWorkerId: "w-degraded",
216
+ });
217
+
218
+ const state = useServiceStore.getState();
219
+ const found = state.workers.find((w) => w.id === state.selectedWorkerId);
220
+
221
+ expect(found).toBeDefined();
222
+ expect(found!.status).toBe("degraded");
223
+ expect(found!.cpu).toBeGreaterThan(80); // should trigger warning/error colors
224
+ expect(found!.memory).toBeGreaterThan(100);
225
+ });
226
+
227
+ test("formatUptime produces human-readable duration", () => {
228
+ // Test the helper logic (imported inline for isolation)
229
+ function formatUptime(seconds: number): string {
230
+ if (seconds < 60) return `${seconds}s`;
231
+ if (seconds < 3600) return `${Math.floor(seconds / 60)}m`;
232
+ if (seconds < 86400)
233
+ return `${Math.floor(seconds / 3600)}h ${Math.floor((seconds % 3600) / 60)}m`;
234
+ const days = Math.floor(seconds / 86400);
235
+ const hours = Math.floor((seconds % 86400) / 3600);
236
+ return `${days}d ${hours}h`;
237
+ }
238
+
239
+ expect(formatUptime(30)).toBe("30s");
240
+ expect(formatUptime(90)).toBe("1m");
241
+ expect(formatUptime(3661)).toBe("1h 1m");
242
+ expect(formatUptime(90000)).toBe("1d 1h");
243
+ expect(formatUptime(172800)).toBe("2d 0h");
244
+ });
245
+
246
+ test("log level colors match expected tokens", async () => {
247
+ const { LogLevelColor, Colors } = await import("@hoox-sh/hoox-shared");
248
+ expect(LogLevelColor.debug).toBe(Colors.muted);
249
+ expect(LogLevelColor.info).toBe(Colors.foreground);
250
+ expect(LogLevelColor.warn).toBe(Colors.warning);
251
+ expect(LogLevelColor.error).toBe(Colors.error);
252
+ });
253
+
254
+ test("redactConfigEntries never surfaces secret-looking keys cleartext", () => {
255
+ const entries = redactConfigEntries({
256
+ name: "trade-executor",
257
+ status: "operational",
258
+ apiKey: "sk-live-super-secret-value",
259
+ HOOX_API_TOKEN: "token-abc-12345678",
260
+ nested: { client_secret: "shh", mode: "prod" },
261
+ note: "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.payload",
262
+ });
263
+
264
+ const byKey = Object.fromEntries(entries.map((e) => [e.key, e.value]));
265
+ expect(byKey.name).toBe("trade-executor");
266
+ expect(byKey.status).toBe("operational");
267
+ expect(byKey.apiKey).toBe("[redacted]");
268
+ expect(byKey.HOOX_API_TOKEN).toBe("[redacted]");
269
+ // Nested secret keys redacted by redactDevLogContext
270
+ expect(byKey.nested).not.toContain("shh");
271
+ expect(byKey.note).toContain("[redacted]");
272
+ expect(byKey.note).not.toContain("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9");
273
+ });
274
+
275
+ test("redactConfigEntries preserves non-secret identity fields", () => {
276
+ const entries = redactConfigEntries({
277
+ version: "1.2.3",
278
+ edgeCount: 12,
279
+ strategy: "grid",
280
+ });
281
+ expect(entries).toEqual(
282
+ expect.arrayContaining([
283
+ { key: "version", value: "1.2.3" },
284
+ { key: "edgeCount", value: "12" },
285
+ { key: "strategy", value: "grid" },
286
+ ])
287
+ );
288
+ });
289
+ });