@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,297 @@
|
|
|
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 SetupWizard component.
|
|
9
|
+
*
|
|
10
|
+
* Covers: progress indicator, step rendering, secret masking,
|
|
11
|
+
* navigation, skip logic, validation, and config store integration.
|
|
12
|
+
*/
|
|
13
|
+
import { describe, it, expect, mock, beforeEach } from "bun:test";
|
|
14
|
+
|
|
15
|
+
// Do NOT mock.module shared stores here — Bun's mock.module is process-wide and
|
|
16
|
+
// strips Zustand setState/getState, which breaks later suites (workers, worker-detail).
|
|
17
|
+
// These tests only exercise pure helpers + local mock functions; no store wiring.
|
|
18
|
+
const mockUpdateConfig = mock((_config: Record<string, unknown>) => {});
|
|
19
|
+
const mockSetView = mock((_view: string) => {});
|
|
20
|
+
|
|
21
|
+
import {
|
|
22
|
+
redactWizardSecrets,
|
|
23
|
+
maskSecret,
|
|
24
|
+
validateApiKey,
|
|
25
|
+
validateEmail,
|
|
26
|
+
validateUrl,
|
|
27
|
+
} from "./setup-wizard";
|
|
28
|
+
|
|
29
|
+
// ─── Secret redaction (session persistence) ─────────────────────────────────
|
|
30
|
+
|
|
31
|
+
describe("redactWizardSecrets", () => {
|
|
32
|
+
it("clears exchange keys, AI key, telegram token, and discord webhook", () => {
|
|
33
|
+
const redacted = redactWizardSecrets({
|
|
34
|
+
apiKeys: {
|
|
35
|
+
binance: { key: "binance-key-secret-value", secret: "binance-sec" },
|
|
36
|
+
bybit: { key: "bybit-key", secret: "bybit-sec" },
|
|
37
|
+
mexc: { key: "mexc-key", secret: "mexc-sec" },
|
|
38
|
+
},
|
|
39
|
+
exchanges: { binance: true, bybit: false, mexc: false },
|
|
40
|
+
ai: {
|
|
41
|
+
providerUrl: "https://api.example.com",
|
|
42
|
+
apiKey: "sk-live-secret",
|
|
43
|
+
model: "gpt-4",
|
|
44
|
+
},
|
|
45
|
+
strategy: { type: "grid", params: { spacing: "1" } },
|
|
46
|
+
notifications: {
|
|
47
|
+
email: { enabled: true, address: "ops@example.com" },
|
|
48
|
+
telegram: {
|
|
49
|
+
enabled: true,
|
|
50
|
+
botToken: "123:ABC-secret",
|
|
51
|
+
chatId: "999",
|
|
52
|
+
},
|
|
53
|
+
discord: {
|
|
54
|
+
enabled: true,
|
|
55
|
+
webhookUrl: "https://discord.com/api/webhooks/secret",
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
expect(redacted.apiKeys.binance).toEqual({ key: "", secret: "" });
|
|
61
|
+
expect(redacted.apiKeys.bybit).toEqual({ key: "", secret: "" });
|
|
62
|
+
expect(redacted.apiKeys.mexc).toEqual({ key: "", secret: "" });
|
|
63
|
+
expect(redacted.ai.apiKey).toBe("");
|
|
64
|
+
expect(redacted.ai.providerUrl).toBe("https://api.example.com");
|
|
65
|
+
expect(redacted.ai.model).toBe("gpt-4");
|
|
66
|
+
expect(redacted.notifications.telegram.botToken).toBe("");
|
|
67
|
+
expect(redacted.notifications.telegram.chatId).toBe("999");
|
|
68
|
+
expect(redacted.notifications.discord.webhookUrl).toBe("");
|
|
69
|
+
expect(redacted.notifications.email.address).toBe("ops@example.com");
|
|
70
|
+
expect(redacted.exchanges.binance).toBe(true);
|
|
71
|
+
expect(redacted.strategy.type).toBe("grid");
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("does not mutate the input object", () => {
|
|
75
|
+
const original = {
|
|
76
|
+
apiKeys: {
|
|
77
|
+
binance: { key: "keep-me", secret: "keep-me-too" },
|
|
78
|
+
bybit: { key: "", secret: "" },
|
|
79
|
+
mexc: { key: "", secret: "" },
|
|
80
|
+
},
|
|
81
|
+
exchanges: { binance: true, bybit: false, mexc: false },
|
|
82
|
+
ai: { providerUrl: "", apiKey: "secret", model: "default" },
|
|
83
|
+
strategy: { type: "grid" as const, params: {} },
|
|
84
|
+
notifications: {
|
|
85
|
+
email: { enabled: false, address: "" },
|
|
86
|
+
telegram: { enabled: false, botToken: "tok", chatId: "" },
|
|
87
|
+
discord: { enabled: false, webhookUrl: "hook" },
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
redactWizardSecrets(original);
|
|
91
|
+
expect(original.apiKeys.binance.key).toBe("keep-me");
|
|
92
|
+
expect(original.ai.apiKey).toBe("secret");
|
|
93
|
+
expect(original.notifications.telegram.botToken).toBe("tok");
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("session payload is safe to JSON.stringify (no secrets)", () => {
|
|
97
|
+
const redacted = redactWizardSecrets({
|
|
98
|
+
apiKeys: {
|
|
99
|
+
binance: { key: "binance-key-secret-value", secret: "binance-sec" },
|
|
100
|
+
bybit: { key: "", secret: "" },
|
|
101
|
+
mexc: { key: "", secret: "" },
|
|
102
|
+
},
|
|
103
|
+
exchanges: { binance: true, bybit: false, mexc: false },
|
|
104
|
+
ai: {
|
|
105
|
+
providerUrl: "https://api.example.com",
|
|
106
|
+
apiKey: "sk-live-should-not-persist",
|
|
107
|
+
model: "gpt-4",
|
|
108
|
+
},
|
|
109
|
+
strategy: { type: "grid", params: {} },
|
|
110
|
+
notifications: {
|
|
111
|
+
email: { enabled: false, address: "" },
|
|
112
|
+
telegram: { enabled: true, botToken: "123:ABC", chatId: "1" },
|
|
113
|
+
discord: {
|
|
114
|
+
enabled: true,
|
|
115
|
+
webhookUrl: "https://discord.com/api/webhooks/x",
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
const json = JSON.stringify({ step: 2, data: redacted });
|
|
120
|
+
expect(json).not.toContain("sk-live");
|
|
121
|
+
expect(json).not.toContain("binance-key");
|
|
122
|
+
expect(json).not.toContain("123:ABC");
|
|
123
|
+
expect(json).not.toContain("webhooks/x");
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// ─── Validation unit tests ───────────────────────────────────────────────────
|
|
128
|
+
|
|
129
|
+
describe("Validation helpers", () => {
|
|
130
|
+
describe("maskSecret", () => {
|
|
131
|
+
it("masks long strings showing first 4 and last 4", () => {
|
|
132
|
+
const result = maskSecret("abcdefghijklmnop12345678");
|
|
133
|
+
expect(result).toBe("abcd••••5678");
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("replaces short strings with bullet characters", () => {
|
|
137
|
+
expect(maskSecret("abc")).toBe("•••");
|
|
138
|
+
expect(maskSecret("")).toBe("••••");
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("handles exactly 8 chars by showing all bullets", () => {
|
|
142
|
+
expect(maskSecret("12345678")).toBe("••••••••");
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
describe("validateApiKey", () => {
|
|
147
|
+
it("accepts valid-looking keys (>=16 chars, no whitespace)", () => {
|
|
148
|
+
expect(validateApiKey("abcdefghijklmnop")).toBe(true);
|
|
149
|
+
expect(validateApiKey("a".repeat(64))).toBe(true);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("rejects short keys", () => {
|
|
153
|
+
expect(validateApiKey("short")).toBe(false);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it("rejects keys with leading/trailing whitespace", () => {
|
|
157
|
+
expect(validateApiKey(" abcdefghijklmnop")).toBe(false);
|
|
158
|
+
expect(validateApiKey("abcdefghijklmnop ")).toBe(false);
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
describe("validateEmail", () => {
|
|
163
|
+
it("accepts valid email addresses", () => {
|
|
164
|
+
expect(validateEmail("user@example.com")).toBe(true);
|
|
165
|
+
expect(validateEmail("a@b.co")).toBe(true);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it("rejects invalid email", () => {
|
|
169
|
+
expect(validateEmail("not-an-email")).toBe(false);
|
|
170
|
+
expect(validateEmail("")).toBe(false);
|
|
171
|
+
expect(validateEmail("@missing.com")).toBe(false);
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
describe("validateUrl", () => {
|
|
176
|
+
it("accepts valid http/https URLs", () => {
|
|
177
|
+
expect(validateUrl("https://discord.com/api/webhooks/123")).toBe(true);
|
|
178
|
+
expect(validateUrl("http://localhost:8080")).toBe(true);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it("rejects non-URL strings", () => {
|
|
182
|
+
expect(validateUrl("not-a-url")).toBe(false);
|
|
183
|
+
expect(validateUrl("")).toBe(false);
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
// ─── Component behavior tests ────────────────────────────────────────────────
|
|
189
|
+
|
|
190
|
+
describe("SetupWizard", () => {
|
|
191
|
+
beforeEach(() => {
|
|
192
|
+
mockUpdateConfig.mockClear();
|
|
193
|
+
mockSetView.mockClear();
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
describe("Progress indicator", () => {
|
|
197
|
+
it("shows 7 step labels including prerequisites", () => {
|
|
198
|
+
const STEPS = [
|
|
199
|
+
"PREREQUISITES",
|
|
200
|
+
"API KEYS",
|
|
201
|
+
"EXCHANGES",
|
|
202
|
+
"AI PROVIDERS",
|
|
203
|
+
"STRATEGIES",
|
|
204
|
+
"NOTIFICATIONS",
|
|
205
|
+
"DEPLOY",
|
|
206
|
+
];
|
|
207
|
+
expect(STEPS).toHaveLength(7);
|
|
208
|
+
expect(STEPS[0]).toBe("PREREQUISITES");
|
|
209
|
+
expect(STEPS[6]).toBe("DEPLOY");
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
describe("Steps can be skipped", () => {
|
|
214
|
+
it("Exchanges step (index 2) is skippable", () => {
|
|
215
|
+
// After prerequisites (0) + API keys (1), exchanges is step 2
|
|
216
|
+
const skipIndex = 2;
|
|
217
|
+
expect(skipIndex).toBe(2);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it("Strategies step (index 4) is skippable", () => {
|
|
221
|
+
const skipIndex = 4;
|
|
222
|
+
expect(skipIndex).toBe(4);
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
describe("Navigation", () => {
|
|
227
|
+
it("Back is disabled on first step", () => {
|
|
228
|
+
const step = 0;
|
|
229
|
+
expect(step === 0).toBe(true); // isFirstStep = true
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it("Next advances to the next step", () => {
|
|
233
|
+
let step = 0;
|
|
234
|
+
const totalSteps = 7;
|
|
235
|
+
step = Math.min(step + 1, totalSteps - 1);
|
|
236
|
+
expect(step).toBe(1);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it("Cannot advance past last step", () => {
|
|
240
|
+
let step = 6;
|
|
241
|
+
const totalSteps = 7;
|
|
242
|
+
if (step < totalSteps - 1) step++;
|
|
243
|
+
expect(step).toBe(6);
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
describe("Deploy action", () => {
|
|
248
|
+
it("writes active exchanges to config store", () => {
|
|
249
|
+
const exchanges = { binance: true, bybit: false, mexc: true };
|
|
250
|
+
const active = Object.entries(exchanges)
|
|
251
|
+
.filter(([, v]) => v)
|
|
252
|
+
.map(([k]) => k);
|
|
253
|
+
|
|
254
|
+
expect(active).toEqual(["binance", "mexc"]);
|
|
255
|
+
|
|
256
|
+
// Simulate what the wizard does on deploy
|
|
257
|
+
mockUpdateConfig({ activeExchanges: active });
|
|
258
|
+
expect(mockUpdateConfig).toHaveBeenCalledWith({
|
|
259
|
+
activeExchanges: ["binance", "mexc"],
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it("navigates to dashboard after deploy", () => {
|
|
264
|
+
mockSetView("dashboard");
|
|
265
|
+
expect(mockSetView).toHaveBeenCalledWith("dashboard");
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it("deploy is fail-closed without dialog (contract)", () => {
|
|
269
|
+
// Mirrors SetupWizard.handleDeploy: no dialog → block + alert
|
|
270
|
+
const dialog: undefined = undefined;
|
|
271
|
+
const blocked = !dialog;
|
|
272
|
+
expect(blocked).toBe(true);
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
describe("Secret field masking", () => {
|
|
277
|
+
it("displays masked value for non-empty secrets", () => {
|
|
278
|
+
const secret = "sk-abcdefghijklmnop123456";
|
|
279
|
+
const masked = maskSecret(secret);
|
|
280
|
+
expect(masked).toBe("sk-a••••3456");
|
|
281
|
+
expect(masked).toContain("••••");
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it("displays placeholder for empty secrets", () => {
|
|
285
|
+
const empty = "";
|
|
286
|
+
const display = empty ? maskSecret(empty) : "(not set)";
|
|
287
|
+
expect(display).toBe("(not set)");
|
|
288
|
+
});
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
describe("Reopen from Settings", () => {
|
|
292
|
+
it("can set view to setup-wizard", () => {
|
|
293
|
+
mockSetView("setup-wizard");
|
|
294
|
+
expect(mockSetView).toHaveBeenCalledWith("setup-wizard");
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
});
|