@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.
- package/README.md +247 -0
- package/bin/hoox-tui.js +35 -0
- package/dist/main.js +17594 -0
- package/package.json +72 -0
- package/src/app.tsx +696 -0
- package/src/cli-bridge-test-double.ts +212 -0
- package/src/components/layout/layout.test.tsx +205 -0
- package/src/components/layout/sidebar.tsx +69 -0
- package/src/components/layout/statusbar.test.tsx +423 -0
- package/src/components/layout/statusbar.tsx +348 -0
- package/src/components/shared/animated-border.tsx +58 -0
- package/src/components/shared/chrome.test.tsx +29 -0
- package/src/components/shared/command-palette.tsx +361 -0
- package/src/components/shared/cool-brackets.tsx +75 -0
- package/src/components/shared/crash-screen.tsx +167 -0
- package/src/components/shared/error-boundary.tsx +99 -0
- package/src/components/shared/keybinding-hint.tsx +37 -0
- package/src/components/shared/panel.tsx +61 -0
- package/src/components/shared/quit-modal.tsx +65 -0
- package/src/components/shared/shared.test.tsx +538 -0
- package/src/components/shared/spinner.tsx +85 -0
- package/src/components/shared/status-dot.tsx +54 -0
- package/src/components/shared/view-header.tsx +53 -0
- package/src/components/ui/connection-toasts.test.ts +50 -0
- package/src/components/ui/connection-toasts.ts +215 -0
- package/src/components/ui/dialog.tsx +281 -0
- package/src/components/ui/select.tsx +197 -0
- package/src/components/ui/toast.tsx +185 -0
- package/src/components/views/ai-chat.test.tsx +81 -0
- package/src/components/views/ai-chat.tsx +576 -0
- package/src/components/views/config-editor/action-bar.tsx +102 -0
- package/src/components/views/config-editor/code-editor.tsx +597 -0
- package/src/components/views/config-editor/file-tree.tsx +113 -0
- package/src/components/views/config-editor/types.ts +37 -0
- package/src/components/views/config-editor.test.tsx +465 -0
- package/src/components/views/config-editor.tsx +709 -0
- package/src/components/views/dashboard/alerts-panel.tsx +154 -0
- package/src/components/views/dashboard/auto-repair-panel.tsx +229 -0
- package/src/components/views/dashboard/model-health-section.tsx +276 -0
- package/src/components/views/dashboard/pyne-health-section.tsx +112 -0
- package/src/components/views/dashboard.test.tsx +347 -0
- package/src/components/views/dashboard.tsx +647 -0
- package/src/components/views/db-query.test.tsx +115 -0
- package/src/components/views/db-query.tsx +849 -0
- package/src/components/views/edge-topology.test.tsx +198 -0
- package/src/components/views/edge-topology.tsx +572 -0
- package/src/components/views/kv-viewer.test.tsx +241 -0
- package/src/components/views/kv-viewer.tsx +717 -0
- package/src/components/views/logs-viewer.test.tsx +273 -0
- package/src/components/views/logs-viewer.tsx +616 -0
- package/src/components/views/queue-depth.test.tsx +221 -0
- package/src/components/views/queue-depth.tsx +376 -0
- package/src/components/views/secrets-viewer.test.tsx +65 -0
- package/src/components/views/secrets-viewer.tsx +548 -0
- package/src/components/views/service-manager.test.tsx +358 -0
- package/src/components/views/service-manager.tsx +946 -0
- package/src/components/views/settings.test.tsx +648 -0
- package/src/components/views/settings.tsx +1497 -0
- package/src/components/views/setup-wizard.test.tsx +297 -0
- package/src/components/views/setup-wizard.tsx +1321 -0
- package/src/components/views/trade-monitor.test.tsx +592 -0
- package/src/components/views/trade-monitor.tsx +745 -0
- package/src/components/views/worker-detail.test.tsx +289 -0
- package/src/components/views/worker-detail.tsx +676 -0
- package/src/components/views/worker-settings.test.tsx +43 -0
- package/src/components/views/worker-settings.tsx +563 -0
- package/src/components/views/workers-overview.test.tsx +260 -0
- package/src/components/views/workers-overview.tsx +631 -0
- package/src/hooks/hooks.test.ts +72 -0
- package/src/hooks/index.ts +11 -0
- package/src/hooks/renderer-ref.ts +22 -0
- package/src/hooks/use-keyboard.ts +130 -0
- package/src/hooks/use-polling.ts +73 -0
- package/src/hooks/use-service-data.ts +18 -0
- package/src/main.tsx +117 -0
- package/src/network-test-double.ts +133 -0
- package/src/opentui-augment.d.ts +45 -0
- package/src/opentui-jsx.d.ts +70 -0
- package/src/services/cli-bridge/index.ts +1662 -0
- package/src/services/cli-bridge/standalone.ts +315 -0
- package/src/services/cli-bridge/types.ts +307 -0
- package/src/services/cli-bridge.test.ts +406 -0
- package/src/services/clipboard.test.ts +57 -0
- package/src/services/clipboard.ts +180 -0
- package/src/services/dashboard-settings-loader.ts +31 -0
- package/src/services/dev-log.test.ts +168 -0
- package/src/services/dev-log.ts +194 -0
- package/src/services/hoox-path-service.test.ts +183 -0
- package/src/services/hoox-path-service.ts +185 -0
- package/src/services/tui-connection.test.ts +212 -0
- package/src/services/tui-connection.ts +236 -0
- package/src/services/tui-storage.test.ts +92 -0
- package/src/services/tui-storage.ts +86 -0
- package/src/stores/config-store.test.ts +360 -0
- package/src/stores/service-store.test.ts +1026 -0
- package/src/stores/session-restore.test.ts +134 -0
- package/src/stores/ui-store.test.ts +276 -0
- package/src/test-setup.ts +265 -0
- package/src/test-utils.ts +179 -0
- package/src/types.ts +153 -0
- package/src/utils/colors.test.ts +326 -0
- package/src/utils/formatters.test.ts +236 -0
- package/src/view-registry.test.ts +132 -0
- 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
|
+
});
|