@hoox-sh/hoox-tui 0.3.2 → 0.4.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/dist/main.js +1069 -2734
- package/package.json +3 -3
- package/src/app.tsx +15 -11
- package/src/components/shared/shared.test.tsx +2 -2
- package/src/components/ui/connection-toasts.ts +6 -0
- package/src/components/ui/dialog-state.ts +2 -0
- package/src/components/ui/dialog.tsx +0 -13
- package/src/components/views/ai-chat.tsx +6 -1
- package/src/components/views/config-editor/code-editor.tsx +8 -6
- package/src/components/views/config-editor.test.tsx +20 -17
- package/src/components/views/config-editor.tsx +8 -6
- package/src/components/views/dashboard/model-health-section.tsx +38 -46
- package/src/components/views/dashboard/pyne-health-section.tsx +36 -41
- package/src/components/views/dashboard.tsx +25 -28
- package/src/components/views/kv-viewer.test.tsx +4 -16
- package/src/components/views/kv-viewer.tsx +9 -13
- package/src/components/views/logs-viewer.test.tsx +4 -4
- package/src/components/views/queue-depth.test.tsx +5 -7
- package/src/components/views/queue-depth.tsx +10 -14
- package/src/components/views/secrets-viewer.test.tsx +1 -1
- package/src/components/views/secrets-viewer.tsx +11 -15
- package/src/components/views/service-manager.tsx +1 -0
- package/src/components/views/settings.test.tsx +2 -7
- package/src/components/views/settings.tsx +43 -60
- package/src/components/views/setup-wizard.test.tsx +12 -14
- package/src/components/views/setup-wizard.tsx +61 -59
- package/src/components/views/worker-detail.test.tsx +2 -2
- package/src/components/views/worker-settings.tsx +6 -6
- package/src/components/views/workers-overview.tsx +1 -1
- package/src/hooks/hooks.test.ts +10 -174
- package/src/hooks/index.ts +1 -3
- package/src/hooks/renderer-ref.ts +1 -0
- package/src/hooks/use-polling.ts +12 -0
- package/src/services/cli-bridge/index.ts +56 -12
- package/src/services/cli-bridge/standalone.ts +4 -1
- package/src/services/cli-bridge/types.ts +6 -9
- package/src/services/cli-bridge.test.ts +20 -4
- package/src/services/clipboard.ts +2 -2
- package/src/stores/service-store.test.ts +29 -26
- package/src/stores/session-restore.test.ts +2 -0
- package/src/test-setup.ts +5 -13
- package/src/test-utils.ts +4 -24
- package/src/types.ts +9 -47
- package/src/utils/colors.test.ts +8 -3
- package/src/view-registry.test.ts +2 -19
- package/src/view-registry.tsx +7 -20
- package/src/components/shared/animated-border.tsx +0 -58
- package/src/components/shared/keybinding-hint.tsx +0 -37
- package/src/components/ui/select.tsx +0 -49
- package/src/hooks/use-keyboard.ts +0 -130
- package/src/hooks/use-service-data.ts +0 -35
|
@@ -21,7 +21,11 @@
|
|
|
21
21
|
* for any CLI failure, regardless of which view triggered it.
|
|
22
22
|
*/
|
|
23
23
|
import * as path from "path";
|
|
24
|
-
import {
|
|
24
|
+
import {
|
|
25
|
+
getHooxRepoPath,
|
|
26
|
+
resolveHooxRuntimeRoot,
|
|
27
|
+
isTruthyKillSwitchFlag,
|
|
28
|
+
} from "@hoox-sh/hoox-shared";
|
|
25
29
|
import type { CliResult, CliErrorType, CliErrorDetails } from "../../types";
|
|
26
30
|
import type {
|
|
27
31
|
ExecOptions,
|
|
@@ -129,6 +133,21 @@ export function tryParseJsonLoose(stdout: string): unknown | null {
|
|
|
129
133
|
return null;
|
|
130
134
|
}
|
|
131
135
|
|
|
136
|
+
/** Unwrap `hoox config kv get --json` `{ key, value }` into the stored string. */
|
|
137
|
+
function unwrapKvGetPayload(payload: unknown): string | null | undefined {
|
|
138
|
+
if (payload == null) return undefined;
|
|
139
|
+
if (typeof payload !== "object" || Array.isArray(payload)) return undefined;
|
|
140
|
+
if (!("value" in payload)) return undefined;
|
|
141
|
+
const v = (payload as { value: unknown }).value;
|
|
142
|
+
if (v == null) return null;
|
|
143
|
+
if (typeof v === "string") return v;
|
|
144
|
+
try {
|
|
145
|
+
return JSON.stringify(v);
|
|
146
|
+
} catch {
|
|
147
|
+
return String(v);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
132
151
|
/**
|
|
133
152
|
* Redact CLI argv values that are likely secrets before logging or
|
|
134
153
|
* surfacing in the status bar (`command` field).
|
|
@@ -241,8 +260,8 @@ export function parseDbQueryResult(stdout: string): DbQueryResult {
|
|
|
241
260
|
}
|
|
242
261
|
|
|
243
262
|
// Derive column order from the first row's keys.
|
|
244
|
-
const
|
|
245
|
-
|
|
263
|
+
const firstRow = rows[0];
|
|
264
|
+
const columns = firstRow ? Object.keys(firstRow) : [];
|
|
246
265
|
|
|
247
266
|
// wrangler's `meta.duration` is reported in seconds (float). Convert to ms.
|
|
248
267
|
let executionTimeMs: number | null = null;
|
|
@@ -369,6 +388,7 @@ export function parseSecretsList(stdout: string): SecretMetadata[] {
|
|
|
369
388
|
if (typeof item === "string") {
|
|
370
389
|
result.push({
|
|
371
390
|
name: item,
|
|
391
|
+
worker: envelope.worker,
|
|
372
392
|
type: inferSecretType(item),
|
|
373
393
|
source: "config",
|
|
374
394
|
});
|
|
@@ -383,12 +403,13 @@ export function parseSecretsList(stdout: string): SecretMetadata[] {
|
|
|
383
403
|
if (typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
384
404
|
const entries = Object.entries(parsed as Record<string, unknown>);
|
|
385
405
|
const result: SecretMetadata[] = [];
|
|
386
|
-
for (const [, value] of entries) {
|
|
406
|
+
for (const [worker, value] of entries) {
|
|
387
407
|
if (Array.isArray(value)) {
|
|
388
408
|
for (const item of value) {
|
|
389
409
|
if (typeof item === "string") {
|
|
390
410
|
result.push({
|
|
391
411
|
name: item,
|
|
412
|
+
worker,
|
|
392
413
|
type: inferSecretType(item),
|
|
393
414
|
source: "config",
|
|
394
415
|
});
|
|
@@ -613,14 +634,14 @@ export function parseKillSwitchStatus(
|
|
|
613
634
|
if (valueMatch) {
|
|
614
635
|
const raw = valueMatch[1] ?? null;
|
|
615
636
|
return {
|
|
616
|
-
engaged: raw
|
|
637
|
+
engaged: isTruthyKillSwitchFlag(raw),
|
|
617
638
|
rawValue: raw,
|
|
618
639
|
timestamp,
|
|
619
640
|
action,
|
|
620
641
|
};
|
|
621
642
|
}
|
|
622
643
|
|
|
623
|
-
// Unknown output —
|
|
644
|
+
// Unknown output — do not claim the switch is released
|
|
624
645
|
return { engaged: false, rawValue: null, timestamp, action };
|
|
625
646
|
}
|
|
626
647
|
|
|
@@ -963,7 +984,22 @@ class CliBridgeImpl {
|
|
|
963
984
|
let data: T | null = null;
|
|
964
985
|
if (options?.json && exitCode === 0 && stdout.trim()) {
|
|
965
986
|
const parsed = tryParseJsonLoose(stdout);
|
|
966
|
-
if (parsed !== null)
|
|
987
|
+
if (parsed !== null) {
|
|
988
|
+
data = parsed as T;
|
|
989
|
+
} else {
|
|
990
|
+
return {
|
|
991
|
+
success: false,
|
|
992
|
+
exitCode,
|
|
993
|
+
stdout: this.truncateOutput(stdout),
|
|
994
|
+
stderr: this.truncateOutput(
|
|
995
|
+
stderrResult || "CLI --json output was not valid JSON"
|
|
996
|
+
),
|
|
997
|
+
data: null,
|
|
998
|
+
duration: performance.now() - start,
|
|
999
|
+
command,
|
|
1000
|
+
errorType: "non-zero-exit",
|
|
1001
|
+
};
|
|
1002
|
+
}
|
|
967
1003
|
}
|
|
968
1004
|
|
|
969
1005
|
// Classify the failure (success leaves errorType=null).
|
|
@@ -1386,16 +1422,24 @@ class CliBridgeImpl {
|
|
|
1386
1422
|
tag: `config:kv:get:${key}`,
|
|
1387
1423
|
}).then((result) => {
|
|
1388
1424
|
if (!result.success) {
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
if (
|
|
1425
|
+
const combined = `${result.stdout}\n${result.stderr}`;
|
|
1426
|
+
const missingKey = /Key\s+"[^"]+"\s+not found in KV namespace/i.test(
|
|
1427
|
+
combined
|
|
1428
|
+
);
|
|
1429
|
+
if (missingKey && result.errorType !== "binary-not-found") {
|
|
1394
1430
|
return { ...result, data: null, success: true };
|
|
1395
1431
|
}
|
|
1396
1432
|
return { ...result, data: null };
|
|
1397
1433
|
}
|
|
1434
|
+
const fromData = unwrapKvGetPayload(result.data);
|
|
1435
|
+
if (fromData !== undefined) {
|
|
1436
|
+
return { ...result, data: fromData };
|
|
1437
|
+
}
|
|
1398
1438
|
const cleaned = result.stdout.replace(/\n$/, "").trim();
|
|
1439
|
+
const fromStdout = unwrapKvGetPayload(tryParseJsonLoose(cleaned));
|
|
1440
|
+
if (fromStdout !== undefined) {
|
|
1441
|
+
return { ...result, data: fromStdout };
|
|
1442
|
+
}
|
|
1399
1443
|
return {
|
|
1400
1444
|
...result,
|
|
1401
1445
|
data: cleaned.length > 0 ? cleaned : null,
|
|
@@ -270,7 +270,10 @@ export function agentChatStream(
|
|
|
270
270
|
|
|
271
271
|
while (!abortController.signal.aborted) {
|
|
272
272
|
const { done, value } = await reader.read();
|
|
273
|
-
if (done)
|
|
273
|
+
if (done) {
|
|
274
|
+
completed = true;
|
|
275
|
+
break;
|
|
276
|
+
}
|
|
274
277
|
|
|
275
278
|
buffer += decoder.decode(value, { stream: true });
|
|
276
279
|
const lines = buffer.split("\n");
|
|
@@ -80,11 +80,7 @@ export interface KillSwitchStatus {
|
|
|
80
80
|
* - no data → "unknown"
|
|
81
81
|
*/
|
|
82
82
|
export type QueueDepthStatus =
|
|
83
|
-
| "
|
|
84
|
-
| "backlogged"
|
|
85
|
-
| "critical"
|
|
86
|
-
| "paused"
|
|
87
|
-
| "unknown";
|
|
83
|
+
"healthy" | "backlogged" | "critical" | "paused" | "unknown";
|
|
88
84
|
|
|
89
85
|
/** Per-queue depth info. See {@link QueueDepthStatus} for status semantics. */
|
|
90
86
|
export interface QueueDepth {
|
|
@@ -139,8 +135,7 @@ export interface DbQueryResult {
|
|
|
139
135
|
* command to the local `hoox` binary.
|
|
140
136
|
*/
|
|
141
137
|
export type SqlValidationResult =
|
|
142
|
-
| { readonly:
|
|
143
|
-
| { readonly: false; reason: string };
|
|
138
|
+
{ readonly: true } | { readonly: false; reason: string };
|
|
144
139
|
|
|
145
140
|
/**
|
|
146
141
|
* A single Cloudflare Workers secret metadata (read-only, no values exposed).
|
|
@@ -150,8 +145,10 @@ export type SqlValidationResult =
|
|
|
150
145
|
* (`hoox config secrets list`) only surfaces names/types anyway.
|
|
151
146
|
*/
|
|
152
147
|
export interface SecretMetadata {
|
|
153
|
-
/** Secret name / key, e.g. "
|
|
148
|
+
/** Secret name / key, e.g. "EXCHANGE_KEY_BINDING" or "OPENAI_API_KEY". */
|
|
154
149
|
name: string;
|
|
150
|
+
/** Worker that declared the secret (all-workers list). */
|
|
151
|
+
worker?: string;
|
|
155
152
|
/**
|
|
156
153
|
* Inferred type based on naming conventions. Cloudflare secrets store
|
|
157
154
|
* does not expose per-secret type metadata, so we derive it from the
|
|
@@ -239,7 +236,7 @@ export interface KvKeySnapshot {
|
|
|
239
236
|
export interface ModelHealth {
|
|
240
237
|
/** Provider name, e.g. "Workers AI", "OpenAI", "Anthropic", "Google", "Azure" */
|
|
241
238
|
name: string;
|
|
242
|
-
/** Provider-specific model identifier, e.g. "@cf/meta/llama-3.1-8b-instruct" */
|
|
239
|
+
/** Provider-specific model identifier, e.g. "@cf/meta/llama-3.1-8b-instruct-fp8" */
|
|
243
240
|
model: string;
|
|
244
241
|
/** Operational state */
|
|
245
242
|
status: "online" | "degraded" | "offline";
|
|
@@ -624,8 +624,18 @@ describe("parseSecretsList", () => {
|
|
|
624
624
|
})
|
|
625
625
|
);
|
|
626
626
|
expect(out).toEqual([
|
|
627
|
-
{
|
|
628
|
-
|
|
627
|
+
{
|
|
628
|
+
name: "API_KEY",
|
|
629
|
+
worker: "trade-worker",
|
|
630
|
+
type: "api_key",
|
|
631
|
+
source: "config",
|
|
632
|
+
},
|
|
633
|
+
{
|
|
634
|
+
name: "auth_token",
|
|
635
|
+
worker: "trade-worker",
|
|
636
|
+
type: "token",
|
|
637
|
+
source: "config",
|
|
638
|
+
},
|
|
629
639
|
]);
|
|
630
640
|
});
|
|
631
641
|
|
|
@@ -1118,7 +1128,10 @@ describe("CliBridge methods (exec stubbed)", () => {
|
|
|
1118
1128
|
});
|
|
1119
1129
|
|
|
1120
1130
|
it("configKvGet treats not found as success null", async () => {
|
|
1121
|
-
bridge.exec = (async () =>
|
|
1131
|
+
bridge.exec = (async () =>
|
|
1132
|
+
failExec(
|
|
1133
|
+
'Key "missing" not found in KV namespace'
|
|
1134
|
+
)) as typeof bridge.exec;
|
|
1122
1135
|
const result = (await bridge.configKvGet("missing")) as CliResult<
|
|
1123
1136
|
string | null
|
|
1124
1137
|
>;
|
|
@@ -1127,7 +1140,10 @@ describe("CliBridge methods (exec stubbed)", () => {
|
|
|
1127
1140
|
});
|
|
1128
1141
|
|
|
1129
1142
|
it("configKvGet returns trimmed value", async () => {
|
|
1130
|
-
bridge.exec = (async () =>
|
|
1143
|
+
bridge.exec = (async () =>
|
|
1144
|
+
okExec(
|
|
1145
|
+
JSON.stringify({ key: "k", value: "hello" })
|
|
1146
|
+
)) as typeof bridge.exec;
|
|
1131
1147
|
const result = (await bridge.configKvGet("k")) as CliResult<string | null>;
|
|
1132
1148
|
expect(result.data).toBe("hello");
|
|
1133
1149
|
});
|
|
@@ -59,8 +59,8 @@ export async function copyViaSystemClipboard(
|
|
|
59
59
|
stdout: "ignore",
|
|
60
60
|
stderr: "ignore",
|
|
61
61
|
});
|
|
62
|
-
proc.stdin.write(text);
|
|
63
|
-
proc.stdin.end();
|
|
62
|
+
void proc.stdin.write(text);
|
|
63
|
+
void proc.stdin.end();
|
|
64
64
|
|
|
65
65
|
let timedOut = false;
|
|
66
66
|
const timer = setTimeout(() => {
|
|
@@ -134,8 +134,8 @@ describe("useServiceStore", () => {
|
|
|
134
134
|
|
|
135
135
|
const state = useServiceStore.getState();
|
|
136
136
|
expect(state.workers).toHaveLength(2);
|
|
137
|
-
expect(state.workers[0]
|
|
138
|
-
expect(state.workers[1]
|
|
137
|
+
expect(state.workers[0]?.name).toBe("alpha");
|
|
138
|
+
expect(state.workers[1]?.name).toBe("beta");
|
|
139
139
|
expect(state.lastUpdated).toBeGreaterThan(0);
|
|
140
140
|
});
|
|
141
141
|
|
|
@@ -714,13 +714,13 @@ describe("useServiceStore", () => {
|
|
|
714
714
|
|
|
715
715
|
const alerts = useServiceStore.getState().alerts;
|
|
716
716
|
expect(alerts).toHaveLength(1);
|
|
717
|
-
expect(alerts[0]
|
|
718
|
-
expect(alerts[0]
|
|
719
|
-
expect(alerts[0]
|
|
720
|
-
expect(alerts[0]
|
|
721
|
-
expect(alerts[0]
|
|
722
|
-
expect(alerts[0]
|
|
723
|
-
expect(alerts[0]
|
|
717
|
+
expect(alerts[0]?.severity).toBe("error");
|
|
718
|
+
expect(alerts[0]?.type).toBe("connection");
|
|
719
|
+
expect(alerts[0]?.message).toContain("CLI failure");
|
|
720
|
+
expect(alerts[0]?.message).toContain("Build failed: missing import");
|
|
721
|
+
expect(alerts[0]?.acknowledged).toBe(false);
|
|
722
|
+
expect(alerts[0]?.id).toBeTruthy();
|
|
723
|
+
expect(alerts[0]?.timestamp).toBeGreaterThan(0);
|
|
724
724
|
});
|
|
725
725
|
|
|
726
726
|
it("appends multiple alerts without dropping older ones under the cap", () => {
|
|
@@ -733,8 +733,8 @@ describe("useServiceStore", () => {
|
|
|
733
733
|
|
|
734
734
|
const alerts = useServiceStore.getState().alerts;
|
|
735
735
|
expect(alerts).toHaveLength(2);
|
|
736
|
-
expect(alerts[0]
|
|
737
|
-
expect(alerts[1]
|
|
736
|
+
expect(alerts[0]?.message).toContain("Build failed: missing import");
|
|
737
|
+
expect(alerts[1]?.message).toContain("second failure");
|
|
738
738
|
});
|
|
739
739
|
|
|
740
740
|
it("respects the MAX_ALERTS cap of 100 entries", () => {
|
|
@@ -747,7 +747,7 @@ describe("useServiceStore", () => {
|
|
|
747
747
|
expect(useServiceStore.getState().alerts).toHaveLength(100);
|
|
748
748
|
// Newest entries survive
|
|
749
749
|
const alerts = useServiceStore.getState().alerts;
|
|
750
|
-
expect(alerts[alerts.length - 1]
|
|
750
|
+
expect(alerts[alerts.length - 1]?.message).toContain("failure 109");
|
|
751
751
|
});
|
|
752
752
|
|
|
753
753
|
it("also works when stderr is empty (falls back to command summary)", () => {
|
|
@@ -761,21 +761,24 @@ describe("useServiceStore", () => {
|
|
|
761
761
|
expect(lastError).toBe(cliFailure.command);
|
|
762
762
|
// And the alert still contains the command
|
|
763
763
|
const alerts = useServiceStore.getState().alerts;
|
|
764
|
-
expect(alerts[0]
|
|
765
|
-
expect(alerts[0]
|
|
764
|
+
expect(alerts[0]?.message).toContain("CLI failure");
|
|
765
|
+
expect(alerts[0]?.message).toContain("hoox deploy all");
|
|
766
766
|
});
|
|
767
767
|
});
|
|
768
768
|
|
|
769
769
|
// ── Ring Buffer Limits ────────────────────────────────────────────────────
|
|
770
770
|
|
|
771
771
|
describe("ring buffer limits", () => {
|
|
772
|
+
// NOTE: each push through the immer-backed store costs a few ms, so
|
|
773
|
+
// these bulk-fill loops get explicit timeouts instead of bun's 5s
|
|
774
|
+
// default (they time out on loaded machines otherwise).
|
|
772
775
|
it("caps trade stream at 500 entries", () => {
|
|
773
776
|
const store = useServiceStore.getState();
|
|
774
777
|
for (let i = 0; i < 600; i++) {
|
|
775
778
|
store.pushTrade(makeTrade({ id: `t${i}` }));
|
|
776
779
|
}
|
|
777
780
|
expect(useServiceStore.getState().tradeStream).toHaveLength(500);
|
|
778
|
-
});
|
|
781
|
+
}, 30000);
|
|
779
782
|
|
|
780
783
|
it("trade stream keeps newest entries when exceeding cap", () => {
|
|
781
784
|
const store = useServiceStore.getState();
|
|
@@ -788,7 +791,7 @@ describe("useServiceStore", () => {
|
|
|
788
791
|
expect(stream).toHaveLength(500);
|
|
789
792
|
// "first" should be dropped
|
|
790
793
|
expect(stream.find((t) => t.id === "first")).toBeUndefined();
|
|
791
|
-
});
|
|
794
|
+
}, 30000);
|
|
792
795
|
|
|
793
796
|
it("caps alerts at 100 entries", () => {
|
|
794
797
|
for (let i = 0; i < 150; i++) {
|
|
@@ -818,7 +821,7 @@ describe("useServiceStore", () => {
|
|
|
818
821
|
useServiceStore.getState().pushLog(makeLog({ id: `l${i}` }));
|
|
819
822
|
}
|
|
820
823
|
expect(useServiceStore.getState().logs).toHaveLength(1000);
|
|
821
|
-
});
|
|
824
|
+
}, 30000);
|
|
822
825
|
|
|
823
826
|
it("logs buffer keeps newest entries when exceeding cap", () => {
|
|
824
827
|
useServiceStore.getState().pushLog(makeLog({ id: "first-log" }));
|
|
@@ -828,7 +831,7 @@ describe("useServiceStore", () => {
|
|
|
828
831
|
expect(
|
|
829
832
|
useServiceStore.getState().logs.find((l) => l.id === "first-log")
|
|
830
833
|
).toBeUndefined();
|
|
831
|
-
});
|
|
834
|
+
}, 30000);
|
|
832
835
|
|
|
833
836
|
it("trade stream preserves order (FIFO)", () => {
|
|
834
837
|
const store = useServiceStore.getState();
|
|
@@ -837,9 +840,9 @@ describe("useServiceStore", () => {
|
|
|
837
840
|
store.pushTrade(makeTrade({ id: "c", timestamp: 3 }));
|
|
838
841
|
|
|
839
842
|
const stream = useServiceStore.getState().tradeStream;
|
|
840
|
-
expect(stream[0]
|
|
841
|
-
expect(stream[1]
|
|
842
|
-
expect(stream[2]
|
|
843
|
+
expect(stream[0]?.id).toBe("a");
|
|
844
|
+
expect(stream[1]?.id).toBe("b");
|
|
845
|
+
expect(stream[2]?.id).toBe("c");
|
|
843
846
|
});
|
|
844
847
|
});
|
|
845
848
|
|
|
@@ -854,7 +857,7 @@ describe("useServiceStore", () => {
|
|
|
854
857
|
useServiceStore.getState().addAlert(alert);
|
|
855
858
|
|
|
856
859
|
expect(useServiceStore.getState().alerts).toHaveLength(1);
|
|
857
|
-
expect(useServiceStore.getState().alerts[0]
|
|
860
|
+
expect(useServiceStore.getState().alerts[0]?.message).toBe(
|
|
858
861
|
"CPU threshold exceeded"
|
|
859
862
|
);
|
|
860
863
|
});
|
|
@@ -947,7 +950,7 @@ describe("useServiceStore", () => {
|
|
|
947
950
|
it("subscribes to SSE trade stream", async () => {
|
|
948
951
|
await useServiceStore.getState().streamTrades();
|
|
949
952
|
expect(subscribeSSEMock).toHaveBeenCalledTimes(1);
|
|
950
|
-
expect(subscribeSSEMock.mock.calls[0][0]).toBe("/v1/trades/stream");
|
|
953
|
+
expect(subscribeSSEMock.mock.calls[0]?.[0]).toBe("/v1/trades/stream");
|
|
951
954
|
});
|
|
952
955
|
|
|
953
956
|
it("streamTrades does not throw on SSE connection failure", async () => {
|
|
@@ -971,7 +974,7 @@ describe("useServiceStore", () => {
|
|
|
971
974
|
const trade = makeTrade({ id: "sse-t1", symbol: "ETH" });
|
|
972
975
|
emitSseEvent(trade, "/v1/trades");
|
|
973
976
|
expect(useServiceStore.getState().tradeStream).toHaveLength(1);
|
|
974
|
-
expect(useServiceStore.getState().tradeStream[0]
|
|
977
|
+
expect(useServiceStore.getState().tradeStream[0]?.id).toBe("sse-t1");
|
|
975
978
|
});
|
|
976
979
|
});
|
|
977
980
|
|
|
@@ -979,7 +982,7 @@ describe("useServiceStore", () => {
|
|
|
979
982
|
it("subscribes to SSE log stream", async () => {
|
|
980
983
|
await useServiceStore.getState().streamLogs();
|
|
981
984
|
expect(subscribeSSEMock).toHaveBeenCalledTimes(1);
|
|
982
|
-
expect(subscribeSSEMock.mock.calls[0][0]).toBe("/v1/logs/stream");
|
|
985
|
+
expect(subscribeSSEMock.mock.calls[0]?.[0]).toBe("/v1/logs/stream");
|
|
983
986
|
});
|
|
984
987
|
|
|
985
988
|
it("pushes logs from SSE events into the ring buffer", async () => {
|
|
@@ -987,7 +990,7 @@ describe("useServiceStore", () => {
|
|
|
987
990
|
const log = makeLog({ id: "sse-l1", message: "from-sse" });
|
|
988
991
|
emitSseEvent(log, "/v1/logs");
|
|
989
992
|
expect(useServiceStore.getState().logs).toHaveLength(1);
|
|
990
|
-
expect(useServiceStore.getState().logs[0]
|
|
993
|
+
expect(useServiceStore.getState().logs[0]?.message).toBe("from-sse");
|
|
991
994
|
});
|
|
992
995
|
});
|
|
993
996
|
|
|
@@ -63,6 +63,7 @@ describe("session restore / save", () => {
|
|
|
63
63
|
expect(session.activeView).toBe("dashboard");
|
|
64
64
|
expect(session.sidebarExpanded).toBe(true);
|
|
65
65
|
expect(session.lastData).toBe(0);
|
|
66
|
+
expect(session.fromDisk).toBe(false);
|
|
66
67
|
});
|
|
67
68
|
|
|
68
69
|
it("round-trips every ViewId (all 16)", async () => {
|
|
@@ -74,6 +75,7 @@ describe("session restore / save", () => {
|
|
|
74
75
|
expect(restored.sidebarExpanded).toBe(true);
|
|
75
76
|
expect(restored.windowSize).toEqual({ cols: 100, rows: 40 });
|
|
76
77
|
expect(restored.lastData).toBe(12345);
|
|
78
|
+
expect(restored.fromDisk).toBe(true);
|
|
77
79
|
}
|
|
78
80
|
});
|
|
79
81
|
|
package/src/test-setup.ts
CHANGED
|
@@ -65,7 +65,7 @@ try {
|
|
|
65
65
|
// - The Temporal Dead Zone race condition is avoided because we've
|
|
66
66
|
// already pre-loaded @opentui/core above
|
|
67
67
|
|
|
68
|
-
mock.module("@opentui/core/testing", () => {
|
|
68
|
+
void mock.module("@opentui/core/testing", () => {
|
|
69
69
|
if (coreAvailable && realCreateTestRenderer) {
|
|
70
70
|
return {
|
|
71
71
|
// Spread any other exports from the real module (setRendererCapabilities,
|
|
@@ -157,10 +157,7 @@ installSpawnShim();
|
|
|
157
157
|
//
|
|
158
158
|
// Per-test overrides: import { cliBridgeDouble, resetCliBridgeDouble } from
|
|
159
159
|
// "./cli-bridge-test-double" and use mockResolvedValue / mockImplementation.
|
|
160
|
-
import {
|
|
161
|
-
cliBridgeDouble,
|
|
162
|
-
createCliBridgeModuleMock,
|
|
163
|
-
} from "./cli-bridge-test-double";
|
|
160
|
+
import { createCliBridgeModuleMock } from "./cli-bridge-test-double";
|
|
164
161
|
|
|
165
162
|
const cliBridgeModule = await createCliBridgeModuleMock();
|
|
166
163
|
|
|
@@ -180,7 +177,7 @@ function installCliBridgeDouble(): void {
|
|
|
180
177
|
];
|
|
181
178
|
for (const p of paths) {
|
|
182
179
|
try {
|
|
183
|
-
mock.module(p, () => cliBridgeModule);
|
|
180
|
+
void mock.module(p, () => cliBridgeModule);
|
|
184
181
|
} catch {
|
|
185
182
|
// Specifier may be invalid in some environments — ignore
|
|
186
183
|
}
|
|
@@ -189,11 +186,6 @@ function installCliBridgeDouble(): void {
|
|
|
189
186
|
|
|
190
187
|
installCliBridgeDouble();
|
|
191
188
|
|
|
192
|
-
// Expose for tests that need the double without importing the helper path
|
|
193
|
-
(
|
|
194
|
-
globalThis as unknown as { __hooxCliBridgeDouble?: typeof cliBridgeDouble }
|
|
195
|
-
).__hooxCliBridgeDouble = cliBridgeDouble;
|
|
196
|
-
|
|
197
189
|
// ── Shared network doubles (api-client + SSE) ────────────────────────────────
|
|
198
190
|
//
|
|
199
191
|
// service-store (and views that call fetchWorkers / stream*) must not hit a
|
|
@@ -257,14 +249,14 @@ async function installNetworkDoubles(): Promise<void> {
|
|
|
257
249
|
|
|
258
250
|
for (const p of apiPaths) {
|
|
259
251
|
try {
|
|
260
|
-
mock.module(p, apiFactory);
|
|
252
|
+
void mock.module(p, apiFactory);
|
|
261
253
|
} catch {
|
|
262
254
|
// ignore bad specifier
|
|
263
255
|
}
|
|
264
256
|
}
|
|
265
257
|
for (const p of ssePaths) {
|
|
266
258
|
try {
|
|
267
|
-
mock.module(p, sseFactory);
|
|
259
|
+
void mock.module(p, sseFactory);
|
|
268
260
|
} catch {
|
|
269
261
|
// ignore
|
|
270
262
|
}
|
package/src/test-utils.ts
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
22
|
import type { ReactNode } from "react";
|
|
23
|
-
import type
|
|
23
|
+
import { VIEW_IDS, type ViewId } from "@hoox-sh/hoox-shared";
|
|
24
24
|
|
|
25
25
|
export {
|
|
26
26
|
cliBridgeDouble,
|
|
@@ -43,35 +43,15 @@ export {
|
|
|
43
43
|
sseSubscriptions,
|
|
44
44
|
} from "./network-test-double";
|
|
45
45
|
|
|
46
|
-
/** Canonical
|
|
47
|
-
export const ALL_VIEW_IDS: ViewId[] =
|
|
48
|
-
"dashboard",
|
|
49
|
-
"workers",
|
|
50
|
-
"worker-detail",
|
|
51
|
-
"trade-monitor",
|
|
52
|
-
"logs-viewer",
|
|
53
|
-
"service-manager",
|
|
54
|
-
"config-editor",
|
|
55
|
-
"setup-wizard",
|
|
56
|
-
"settings",
|
|
57
|
-
"queue-depth",
|
|
58
|
-
"kv-viewer",
|
|
59
|
-
"secrets-viewer",
|
|
60
|
-
"db-query",
|
|
61
|
-
"ai-chat",
|
|
62
|
-
"edge-topology",
|
|
63
|
-
"worker-settings",
|
|
64
|
-
];
|
|
46
|
+
/** Canonical ViewIds — same tuple as shared `VIEW_IDS`. */
|
|
47
|
+
export const ALL_VIEW_IDS: readonly ViewId[] = VIEW_IDS;
|
|
65
48
|
|
|
66
49
|
// ── Type Aliases ─────────────────────────────────────────────────────────────
|
|
67
50
|
|
|
68
51
|
export type WorkerStatus = "operational" | "degraded" | "down";
|
|
69
52
|
export type LogLevel = "debug" | "info" | "warn" | "error";
|
|
70
53
|
export type ConnectionStatus =
|
|
71
|
-
| "
|
|
72
|
-
| "polling"
|
|
73
|
-
| "offline"
|
|
74
|
-
| "reconnecting";
|
|
54
|
+
"connected" | "polling" | "offline" | "reconnecting";
|
|
75
55
|
export type TradeSide = "buy" | "sell";
|
|
76
56
|
|
|
77
57
|
export interface TestWorkerInfo {
|
package/src/types.ts
CHANGED
|
@@ -7,34 +7,19 @@
|
|
|
7
7
|
* TUI-specific types — Navigation, modals, keyboard shortcuts.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import
|
|
10
|
+
import {
|
|
11
|
+
VIEW_IDS,
|
|
12
|
+
type ViewId,
|
|
13
|
+
type CliErrorType,
|
|
14
|
+
type CliErrorDetails,
|
|
15
|
+
} from "@hoox-sh/hoox-shared";
|
|
11
16
|
|
|
12
17
|
// Re-export shared CLI error types for convenience so consumers can
|
|
13
18
|
// import everything from the tui/types module.
|
|
14
|
-
export type { CliErrorType, CliErrorDetails };
|
|
15
|
-
|
|
16
|
-
/** All primary views registered with the TUI. */
|
|
17
|
-
export const ALL_VIEWS = [
|
|
18
|
-
"dashboard",
|
|
19
|
-
"workers",
|
|
20
|
-
"worker-detail",
|
|
21
|
-
"trade-monitor",
|
|
22
|
-
"logs-viewer",
|
|
23
|
-
"service-manager",
|
|
24
|
-
"config-editor",
|
|
25
|
-
"setup-wizard",
|
|
26
|
-
"settings",
|
|
27
|
-
"queue-depth",
|
|
28
|
-
"kv-viewer",
|
|
29
|
-
"secrets-viewer",
|
|
30
|
-
"db-query",
|
|
31
|
-
"ai-chat",
|
|
32
|
-
"edge-topology",
|
|
33
|
-
"worker-settings",
|
|
34
|
-
] as const;
|
|
35
|
-
|
|
36
|
-
export type ViewId = (typeof ALL_VIEWS)[number];
|
|
19
|
+
export type { CliErrorType, CliErrorDetails, ViewId };
|
|
20
|
+
export { VIEW_IDS as ALL_VIEWS };
|
|
37
21
|
|
|
22
|
+
/** Sidebar / tab labels for each {@link ViewId}. */
|
|
38
23
|
export const VIEW_LABELS: Record<ViewId, string> = {
|
|
39
24
|
dashboard: "DASHBOARD",
|
|
40
25
|
workers: "WORKERS",
|
|
@@ -54,29 +39,6 @@ export const VIEW_LABELS: Record<ViewId, string> = {
|
|
|
54
39
|
"worker-settings": "W SETTINGS",
|
|
55
40
|
};
|
|
56
41
|
|
|
57
|
-
export const VIEW_ORDER: ViewId[] = [
|
|
58
|
-
"dashboard",
|
|
59
|
-
"workers",
|
|
60
|
-
"worker-detail",
|
|
61
|
-
"trade-monitor",
|
|
62
|
-
"logs-viewer",
|
|
63
|
-
"service-manager",
|
|
64
|
-
"config-editor",
|
|
65
|
-
"setup-wizard",
|
|
66
|
-
"settings",
|
|
67
|
-
"queue-depth",
|
|
68
|
-
"kv-viewer",
|
|
69
|
-
"secrets-viewer",
|
|
70
|
-
"db-query",
|
|
71
|
-
"ai-chat",
|
|
72
|
-
"edge-topology",
|
|
73
|
-
"worker-settings",
|
|
74
|
-
];
|
|
75
|
-
|
|
76
|
-
export function viewIndex(view: ViewId): number {
|
|
77
|
-
return VIEW_ORDER.indexOf(view);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
42
|
export type ModalType = "confirm" | "choice" | "loading" | "prompt";
|
|
81
43
|
|
|
82
44
|
export interface ModalState {
|
package/src/utils/colors.test.ts
CHANGED
|
@@ -31,10 +31,15 @@ function hexToRgb(hex: string): { r: number; g: number; b: number } | null {
|
|
|
31
31
|
/^#?([0-9A-Fa-f]{2})([0-9A-Fa-f]{2})([0-9A-Fa-f]{2})$/
|
|
32
32
|
);
|
|
33
33
|
if (!match) return null;
|
|
34
|
+
const rHex = match[1];
|
|
35
|
+
const gHex = match[2];
|
|
36
|
+
const bHex = match[3];
|
|
37
|
+
if (rHex === undefined || gHex === undefined || bHex === undefined)
|
|
38
|
+
return null;
|
|
34
39
|
return {
|
|
35
|
-
r: parseInt(
|
|
36
|
-
g: parseInt(
|
|
37
|
-
b: parseInt(
|
|
40
|
+
r: parseInt(rHex, 16),
|
|
41
|
+
g: parseInt(gHex, 16),
|
|
42
|
+
b: parseInt(bHex, 16),
|
|
38
43
|
};
|
|
39
44
|
}
|
|
40
45
|
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { describe, it, expect } from "bun:test";
|
|
7
|
-
import type
|
|
7
|
+
import { VIEW_IDS, type ViewId } from "@hoox-sh/hoox-shared";
|
|
8
8
|
import {
|
|
9
9
|
VIEW_REGISTRY,
|
|
10
10
|
REGISTERED_VIEW_IDS,
|
|
@@ -19,24 +19,7 @@ import {
|
|
|
19
19
|
ALL_PALETTE_COMMANDS,
|
|
20
20
|
} from "./view-registry";
|
|
21
21
|
|
|
22
|
-
const ALL_VIEWS: ViewId[] = [
|
|
23
|
-
"dashboard",
|
|
24
|
-
"workers",
|
|
25
|
-
"worker-detail",
|
|
26
|
-
"trade-monitor",
|
|
27
|
-
"logs-viewer",
|
|
28
|
-
"service-manager",
|
|
29
|
-
"config-editor",
|
|
30
|
-
"setup-wizard",
|
|
31
|
-
"settings",
|
|
32
|
-
"queue-depth",
|
|
33
|
-
"kv-viewer",
|
|
34
|
-
"secrets-viewer",
|
|
35
|
-
"db-query",
|
|
36
|
-
"ai-chat",
|
|
37
|
-
"edge-topology",
|
|
38
|
-
"worker-settings",
|
|
39
|
-
];
|
|
22
|
+
const ALL_VIEWS: ViewId[] = [...VIEW_IDS];
|
|
40
23
|
|
|
41
24
|
describe("view-registry", () => {
|
|
42
25
|
it("covers every ViewId exactly once in order", () => {
|