switchroom 0.7.13 → 0.7.15

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.
@@ -0,0 +1,134 @@
1
+ /**
2
+ * Tests for the vault-CLI error parser + renderer (issue #969 P0b).
3
+ */
4
+
5
+ import { describe, it, expect } from "vitest";
6
+ import { parseVaultCliError, renderVaultCliError } from "./vault-error.js";
7
+
8
+ describe("parseVaultCliError", () => {
9
+ it("classifies VAULT-SANDBOX-CONTEXT", () => {
10
+ const stderr =
11
+ "VAULT-SANDBOX-CONTEXT: direct vault access is unavailable inside an " +
12
+ "agent sandbox. The vault file is not mounted into agent containers; " +
13
+ "only the broker socket is. Run 'switchroom vault init' on the host shell, " +
14
+ "or use a broker-supported operation.";
15
+ const err = parseVaultCliError(stderr);
16
+ expect(err.kind).toBe("sandbox_context");
17
+ // The hint contains 'switchroom vault init' — parser must skip it.
18
+ expect(err.key).toBeUndefined();
19
+ });
20
+
21
+ it("classifies VAULT-NEEDS-APPROVAL and extracts the affected key", () => {
22
+ const stderr =
23
+ "VAULT-NEEDS-APPROVAL [unknown_key]: secret 'telegram_bot_token_klanker_20260510' " +
24
+ "does not exist in the vault yet. Agents can rotate existing keys via " +
25
+ "the broker but cannot create new ones; this requires operator approval.";
26
+ const err = parseVaultCliError(stderr);
27
+ expect(err.kind).toBe("needs_approval");
28
+ expect(err.key).toBe("telegram_bot_token_klanker_20260510");
29
+ });
30
+
31
+ it("classifies VAULT-BROKER-UNREACHABLE", () => {
32
+ const stderr =
33
+ "VAULT-BROKER-UNREACHABLE: cannot reach vault broker " +
34
+ "(ENOENT: no such file or directory, connect '/run/switchroom/broker/sock'). " +
35
+ "From inside the agent sandbox, direct vault access is not possible.";
36
+ const err = parseVaultCliError(stderr);
37
+ expect(err.kind).toBe("broker_unreachable");
38
+ });
39
+
40
+ it("classifies VAULT-BROKER-DENIED and extracts the KEY (not the agent name)", () => {
41
+ // Regression test: the canonical stderr has TWO single-quoted
42
+ // tokens — the agent name and the key name. The parser must pick
43
+ // the key (anchored after "key '") so the rendered host hint
44
+ // suggests granting access to the right key.
45
+ const stderr =
46
+ "VAULT-BROKER-DENIED [DENIED]: agent 'klanker' is not in the allow list for key 'shared_token'";
47
+ const err = parseVaultCliError(stderr);
48
+ expect(err.kind).toBe("broker_denied");
49
+ expect(err.key).toBe("shared_token");
50
+ });
51
+
52
+ it("returns 'other' for unrecognised errors", () => {
53
+ const err = parseVaultCliError("Error: something broke\nstack trace …");
54
+ expect(err.kind).toBe("other");
55
+ expect(err.key).toBeUndefined();
56
+ });
57
+
58
+ it("handles empty / undefined stderr gracefully", () => {
59
+ expect(parseVaultCliError("").kind).toBe("other");
60
+ expect(parseVaultCliError(undefined as unknown as string).kind).toBe("other");
61
+ });
62
+ });
63
+
64
+ describe("renderVaultCliError", () => {
65
+ it("renders sandbox_context with a host-CLI suggestion", () => {
66
+ const out = renderVaultCliError(
67
+ { kind: "sandbox_context", original: "x" },
68
+ { verb: "set", key: "my_key" },
69
+ );
70
+ expect(out.suppressRaw).toBe(true);
71
+ expect(out.html).toContain("must run on the host");
72
+ expect(out.html).toContain("switchroom vault set my_key");
73
+ });
74
+
75
+ it("renders needs_approval with the affected key + host hint + P1a teaser", () => {
76
+ const out = renderVaultCliError(
77
+ { kind: "needs_approval", original: "x", key: "telegram_bot_token" },
78
+ { verb: "save" },
79
+ );
80
+ expect(out.suppressRaw).toBe(true);
81
+ expect(out.html).toContain("operator approval required");
82
+ expect(out.html).toContain("<code>telegram_bot_token</code>");
83
+ expect(out.html).toContain("switchroom vault set telegram_bot_token");
84
+ expect(out.html).toContain("P1a"); // forward-pointer to the upcoming flow
85
+ });
86
+
87
+ it("renders broker_unreachable with the status command", () => {
88
+ const out = renderVaultCliError(
89
+ { kind: "broker_unreachable", original: "x" },
90
+ { verb: "set" },
91
+ );
92
+ expect(out.suppressRaw).toBe(true);
93
+ expect(out.html).toContain("broker isn't reachable");
94
+ expect(out.html).toContain("switchroom vault broker status");
95
+ });
96
+
97
+ it("renders broker_denied with a grant command + key", () => {
98
+ const out = renderVaultCliError(
99
+ { kind: "broker_denied", original: "x", key: "shared_token" },
100
+ { verb: "set" },
101
+ );
102
+ expect(out.suppressRaw).toBe(true);
103
+ expect(out.html).toContain("refused the request");
104
+ expect(out.html).toContain("switchroom vault grant");
105
+ expect(out.html).toContain("shared_token");
106
+ });
107
+
108
+ it("prefers verbHint.key over parser-extracted key (verbHint wins for host suggestion)", () => {
109
+ // The gateway always knows the key the user asked for; rendering
110
+ // must use that over any heuristic extraction so a parser glitch
111
+ // can't surface the wrong key in the host command suggestion.
112
+ const out = renderVaultCliError(
113
+ { kind: "broker_denied", original: "x", key: "parser-extracted" },
114
+ { verb: "set", key: "gateway-supplied" },
115
+ );
116
+ expect(out.html).toContain("gateway-supplied");
117
+ expect(out.html).not.toContain("parser-extracted");
118
+ });
119
+
120
+ it("returns suppressRaw=false for 'other' so the gateway falls back to a raw pre-block", () => {
121
+ const out = renderVaultCliError({ kind: "other", original: "weird error" }, { verb: "set" });
122
+ expect(out.suppressRaw).toBe(false);
123
+ expect(out.html).toBe("");
124
+ });
125
+
126
+ it("escapes HTML special characters in the key", () => {
127
+ const out = renderVaultCliError(
128
+ { kind: "needs_approval", original: "x", key: "key<with>html" },
129
+ { verb: "save" },
130
+ );
131
+ expect(out.html).not.toContain("<with>");
132
+ expect(out.html).toContain("key&lt;with&gt;html");
133
+ });
134
+ });
@@ -0,0 +1,202 @@
1
+ /**
2
+ * Vault CLI error classification (issue #969 P0b).
3
+ *
4
+ * `switchroom vault` emits stable stderr markers + exit codes when it
5
+ * detects a failure mode the Telegram gateway should surface with
6
+ * specific UX rather than a raw pre-block of error text. This module
7
+ * parses those markers (introduced in #971 / P0a) and translates them
8
+ * into a structured result the gateway can render against.
9
+ *
10
+ * Markers (from `src/cli/vault.ts`):
11
+ *
12
+ * VAULT-SANDBOX-CONTEXT (exit 7) — direct vault file IO is
13
+ * unavailable inside an agent
14
+ * container; the requested verb
15
+ * has no broker equivalent.
16
+ *
17
+ * VAULT-NEEDS-APPROVAL (exit 5) — agent tried to write to a key
18
+ * that doesn't exist yet. Broker
19
+ * PUT cannot introduce new keys;
20
+ * operator approval is required.
21
+ * (P1a will wire up an inline
22
+ * approval card; until then we
23
+ * surface the host-CLI hint.)
24
+ *
25
+ * VAULT-BROKER-UNREACHABLE (exit 6) — broker socket missing/dead;
26
+ * operator needs to inspect on
27
+ * the host.
28
+ *
29
+ * VAULT-BROKER-DENIED (exit 2) — broker reachable but ACL or
30
+ * grant refused; operator needs
31
+ * to add an explicit grant.
32
+ */
33
+
34
+ export type VaultCliErrorKind =
35
+ | "sandbox_context"
36
+ | "needs_approval"
37
+ | "broker_unreachable"
38
+ | "broker_denied"
39
+ | "other";
40
+
41
+ export interface VaultCliError {
42
+ kind: VaultCliErrorKind;
43
+ /** The original stderr text (kept for fallback rendering / audit log). */
44
+ original: string;
45
+ /**
46
+ * Best-effort extraction of the affected key name, if surfaced by the
47
+ * marker. Used to compose host-CLI hints like
48
+ * `switchroom vault set <key>`.
49
+ */
50
+ key?: string;
51
+ }
52
+
53
+ const MARKER_TO_KIND: ReadonlyArray<readonly [string, VaultCliErrorKind]> = [
54
+ ["VAULT-SANDBOX-CONTEXT", "sandbox_context"],
55
+ ["VAULT-NEEDS-APPROVAL", "needs_approval"],
56
+ ["VAULT-BROKER-UNREACHABLE", "broker_unreachable"],
57
+ ["VAULT-BROKER-DENIED", "broker_denied"],
58
+ ];
59
+
60
+ /**
61
+ * Classify a vault-CLI stderr blob into a structured error. Returns
62
+ * `kind: "other"` when no recognized marker is present — caller should
63
+ * fall back to a raw pre-block.
64
+ *
65
+ * Key extraction is marker-aware. The four markers don't all surface
66
+ * the key the same way:
67
+ *
68
+ * VAULT-NEEDS-APPROVAL [unknown_key]: secret '<KEY>' does not …
69
+ * VAULT-BROKER-DENIED [<CODE>]: agent '<AGENT>' is not in the
70
+ * allow list for key '<KEY>'
71
+ * VAULT-SANDBOX-CONTEXT: … 'switchroom vault set <KEY>' on the host.
72
+ *
73
+ * A naïve "first single-quoted token" regex would grab the agent name
74
+ * for broker_denied. Prefer the token after a `key '` anchor when the
75
+ * marker is broker_denied / needs_approval; for sandbox_context the
76
+ * gateway supplies the key directly via verbHint (extraction here is
77
+ * best-effort and may stay undefined).
78
+ */
79
+ export function parseVaultCliError(stderr: string): VaultCliError {
80
+ const text = stderr ?? "";
81
+ for (const [marker, kind] of MARKER_TO_KIND) {
82
+ if (text.includes(marker)) {
83
+ const key = extractKey(text, kind);
84
+ return { kind, original: text, key };
85
+ }
86
+ }
87
+ return { kind: "other", original: text };
88
+ }
89
+
90
+ function extractKey(text: string, kind: VaultCliErrorKind): string | undefined {
91
+ // Prefer the explicit `key '<X>'` anchor when present — it's the
92
+ // unambiguous form used in broker_denied's stderr.
93
+ const anchored = text.match(/key '([A-Za-z0-9_.-]+)'/);
94
+ if (anchored) return anchored[1];
95
+
96
+ // needs_approval: `secret '<X>' does not …` is the canonical form;
97
+ // the only other quoted token in that message is the agent name in
98
+ // the suggestion clause (which is never present today, but guard
99
+ // against drift).
100
+ if (kind === "needs_approval") {
101
+ const m = text.match(/secret '([A-Za-z0-9_.-]+)'/);
102
+ if (m) return m[1];
103
+ }
104
+
105
+ // Fall back to first single-quoted token, skipping the
106
+ // `'switchroom vault …'` hint that sandbox_context emits.
107
+ const allQuoted = [...text.matchAll(/'([^']+)'/g)];
108
+ for (const m of allQuoted) {
109
+ const candidate = m[1];
110
+ if (candidate.includes("switchroom")) continue;
111
+ if (!/^[A-Za-z0-9_.-]+$/.test(candidate)) continue;
112
+ return candidate;
113
+ }
114
+ return undefined;
115
+ }
116
+
117
+ export interface VaultErrorRendering {
118
+ /** Telegram HTML message (no surrounding decoration). */
119
+ html: string;
120
+ /**
121
+ * When true, the gateway should NOT print the raw stderr blob — the
122
+ * rendered message already conveys the actionable next step. When
123
+ * false (kind="other"), the gateway should append the original
124
+ * output in a <pre> block as before.
125
+ */
126
+ suppressRaw: boolean;
127
+ }
128
+
129
+ function htmlEscape(s: string): string {
130
+ return s
131
+ .replace(/&/g, "&amp;")
132
+ .replace(/</g, "&lt;")
133
+ .replace(/>/g, "&gt;");
134
+ }
135
+
136
+ /**
137
+ * Compose the user-facing Telegram HTML for a parsed vault error.
138
+ *
139
+ * @param err Result from parseVaultCliError.
140
+ * @param verbHint Optional descriptor of the in-progress action used
141
+ * to compose the host-side command suggestion (e.g.
142
+ * "set", "remove", "init").
143
+ */
144
+ export function renderVaultCliError(
145
+ err: VaultCliError,
146
+ verbHint: { verb: "set" | "get" | "list" | "init" | "remove" | "save"; key?: string } = { verb: "set" },
147
+ ): VaultErrorRendering {
148
+ // The gateway always knows which key the user was acting on for
149
+ // get/set/remove — prefer that over the parser's best-effort
150
+ // extraction so a renderer mistake can't surface the wrong key in
151
+ // the host command suggestion (e.g. an agent name from a
152
+ // broker_denied message). Fall back to parser extraction only when
153
+ // the gateway didn't pass a key (e.g. list).
154
+ const key = verbHint.key ?? err.key;
155
+ switch (err.kind) {
156
+ case "sandbox_context":
157
+ return {
158
+ suppressRaw: true,
159
+ html:
160
+ `⚠️ <b>This action must run on the host.</b>\n` +
161
+ `The vault file isn't mounted inside the agent sandbox; only ` +
162
+ `the broker socket is. Open a host shell and run:\n` +
163
+ `<pre>switchroom vault ${verbHint.verb}${key ? ` ${htmlEscape(key)}` : ""}</pre>`,
164
+ };
165
+ case "needs_approval":
166
+ return {
167
+ suppressRaw: true,
168
+ html:
169
+ `⚠️ <b>New vault key — operator approval required.</b>\n` +
170
+ (key
171
+ ? `The agent tried to save <code>${htmlEscape(key)}</code>, but `
172
+ : `The agent tried to save a new key, but `) +
173
+ `agents can only rotate existing keys via the broker; introducing ` +
174
+ `a new key needs an operator action.\n\n` +
175
+ `For now, run on a host shell:\n` +
176
+ `<pre>switchroom vault set${key ? ` ${htmlEscape(key)}` : " &lt;key&gt;"}</pre>\n` +
177
+ `<i>A one-tap approval card is on the way (#969 P1a).</i>`,
178
+ };
179
+ case "broker_unreachable":
180
+ return {
181
+ suppressRaw: true,
182
+ html:
183
+ `⚠️ <b>Vault broker isn't reachable.</b>\n` +
184
+ `From inside the agent sandbox there's no fallback path. ` +
185
+ `Operator can check on the host:\n` +
186
+ `<pre>switchroom vault broker status</pre>`,
187
+ };
188
+ case "broker_denied":
189
+ return {
190
+ suppressRaw: true,
191
+ html:
192
+ `⚠️ <b>Vault broker refused the request.</b>\n` +
193
+ (key
194
+ ? `The agent isn't authorized to access <code>${htmlEscape(key)}</code>. `
195
+ : `The agent isn't authorized to access this key. `) +
196
+ `Operator can grant access from a host shell:\n` +
197
+ `<pre>switchroom vault grant &lt;agent&gt; --keys ${key ? htmlEscape(key) : "&lt;key&gt;"}</pre>`,
198
+ };
199
+ case "other":
200
+ return { suppressRaw: false, html: "" };
201
+ }
202
+ }
@@ -1 +0,0 @@
1
- {"version":"3.2.4","results":[[":tests/progress-card-driver.test.ts",{"duration":82.55005700000004,"failed":false}],[":tests/progress-card.test.ts",{"duration":50.04072000000002,"failed":false}],[":tests/telegram-format.test.ts",{"duration":0,"failed":false}],[":tests/stream-reply-handler.test.ts",{"duration":0,"failed":false}],[":tests/progress-card-harness.test.ts",{"duration":4955.245276000001,"failed":false}],[":tests/streaming-orchestration.test.ts",{"duration":0,"failed":false}],[":tests/races.test.ts",{"duration":0,"failed":false}],[":tests/subagent-watcher.test.ts",{"duration":0,"failed":false}],[":tests/gateway-bridge.test.ts",{"duration":0,"failed":true}],[":tests/answer-stream.test.ts",{"duration":0,"failed":false}],[":tests/progress-card-pin-manager.test.ts",{"duration":25.365711999999974,"failed":false}],[":tests/pty-tail.test.ts",{"duration":0,"failed":false}],[":tests/turn-end-regressions.test.ts",{"duration":0,"failed":false}],[":tests/session-tail.test.ts",{"duration":0,"failed":false}],[":tests/gateway-clean-shutdown-marker.test.ts",{"duration":0,"failed":true}],[":tests/e2e.test.ts",{"duration":0,"failed":false}],[":tests/setup-flow.test.ts",{"duration":0,"failed":false}],[":tests/tool-labels.test.ts",{"duration":0,"failed":false}],[":tests/registry-turns.test.ts",{"duration":0,"failed":true}],[":tests/welcome-text.test.ts",{"duration":0,"failed":false}],[":tests/secret-detect-oauth-code.test.ts",{"duration":0,"failed":false}],[":tests/draft-stream.test.ts",{"duration":0,"failed":false}],[":tests/progress-card-stuck-warning.test.ts",{"duration":21.23773799999998,"failed":false}],[":tests/history.test.ts",{"duration":0,"failed":false}],[":tests/streaming-e2e.test.ts",{"duration":0,"failed":false}],[":tests/foreman-handlers.test.ts",{"duration":0,"failed":false}],[":tests/foreman-create-flow.test.ts",{"duration":0,"failed":false}],[":tests/operator-events.test.ts",{"duration":0,"failed":false}],[":tests/vault-grants-revoke.test.ts",{"duration":0,"failed":false}],[":tests/boot-probes.test.ts",{"duration":0,"failed":true}],[":tests/pty-partial-handler.test.ts",{"duration":0,"failed":false}],[":tests/auth-slot-commands.test.ts",{"duration":0,"failed":false}],[":tests/vault-subcommands.test.ts",{"duration":0,"failed":false}],[":tests/auth-dashboard.test.ts",{"duration":0,"failed":false}],[":tests/active-pins-sweep.test.ts",{"duration":0,"failed":false}],[":tests/turns-writer.test.ts",{"duration":0,"failed":true}],[":tests/retry-api-call.test.ts",{"duration":0,"failed":false}],[":tests/steering.test.ts",{"duration":0,"failed":false}],[":tests/outbound-ordering.test.ts",{"duration":0,"failed":false}],[":tests/ipc-server-client.test.ts",{"duration":0,"failed":false}],[":tests/gateway-startup-mutex.test.ts",{"duration":0,"failed":true}],[":tests/auth-dashboard-edge-cases.test.ts",{"duration":0,"failed":false}],[":tests/status-reactions.test.ts",{"duration":0,"failed":false}],[":tests/auto-fallback.test.ts",{"duration":0,"failed":false}],[":tests/secret-detect.test.ts",{"duration":0,"failed":false}],[":tests/foreman-write-ops.test.ts",{"duration":0,"failed":false}],[":tests/boot-card-render.test.ts",{"duration":0,"failed":false}],[":tests/handoff-continuity.test.ts",{"duration":0,"failed":false}],[":tests/false-restart-banner.test.ts",{"duration":0,"failed":false}],[":tests/answer-stream-silent-markers.test.ts",{"duration":0,"failed":false}],[":tests/ipc-validator.test.ts",{"duration":0,"failed":false}],[":tests/stream-controller.test.ts",{"duration":0,"failed":false}],[":tests/gateway-409-retry-banner.test.ts",{"duration":0,"failed":false}],[":tests/stream-reply-error-paths.test.ts",{"duration":0,"failed":false}],[":tests/ipc-server-race.test.ts",{"duration":0,"failed":false}],[":tests/progress-update.test.ts",{"duration":0,"failed":true}],[":tests/restart-watchdog.test.ts",{"duration":0,"failed":false}],[":tests/progress-card-cross-turn.test.ts",{"duration":0,"failed":false}],[":tests/boot-card-probe-target.test.ts",{"duration":0,"failed":false}],[":tests/active-reactions.test.ts",{"duration":0,"failed":false}],[":tests/quota-cache.test.ts",{"duration":0,"failed":true}],[":tests/streaming-metrics.test.ts",{"duration":0,"failed":false}],[":tests/operator-events-session-tail.test.ts",{"duration":0,"failed":false}],[":tests/foreman-state.test.ts",{"duration":0,"failed":true}],[":tests/ipc-protocol.test.ts",{"duration":0,"failed":false}],[":tests/progress-card-pin-watchdog.test.ts",{"duration":0,"failed":false}],[":tests/telegram-button-constraints.test.ts",{"duration":0,"failed":false}],[":tests/bot-runtime.test.ts",{"duration":0,"failed":false}],[":tests/gateway-startup-network-retry.test.ts",{"duration":0,"failed":false}],[":tests/attachment-path.test.ts",{"duration":0,"failed":false}],[":tests/active-pins.test.ts",{"duration":0,"failed":false}],[":tests/subagent-tracker-hooks.test.ts",{"duration":0,"failed":true}],[":tests/fake-bot-api.test.ts",{"duration":0,"failed":false}],[":tests/quota-check.test.ts",{"duration":0,"failed":false}],[":tests/parse-mode-rotation.test.ts",{"duration":0,"failed":false}],[":tests/gateway-secret-detect.test.ts",{"duration":0,"failed":false}],[":tests/turn-flush-safety.test.ts",{"duration":0,"failed":false}],[":tests/multi-turn-continuity.test.ts",{"duration":0,"failed":false}],[":tests/status-accent.test.ts",{"duration":0,"failed":false}],[":tests/auth-code-auto-capture.test.ts",{"duration":0,"failed":false}],[":tests/auth-login-url-button.test.ts",{"duration":0,"failed":false}],[":tests/auth-dashboard-restart-flow.test.ts",{"duration":0,"failed":false}],[":tests/setup-state.test.ts",{"duration":0,"failed":true}],[":tests/progress-card-golden.test.ts",{"duration":6.658348999999987,"failed":false}],[":tests/unhandled-rejection-policy.test.ts",{"duration":0,"failed":true}],[":tests/typing-wrap.test.ts",{"duration":0,"failed":false}],[":tests/auth-account-identity-surface.test.ts",{"duration":0,"failed":false}],[":tests/secret-detect-secretlint.test.ts",{"duration":0,"failed":false}],[":tests/secret-detect-pipeline.test.ts",{"duration":0,"failed":false}],[":tests/operator-events-history.test.ts",{"duration":0,"failed":false}],[":tests/gateway-message-validator.test.ts",{"duration":0,"failed":false}],[":tests/silent-reply-guard.test.ts",{"duration":0,"failed":false}],[":tests/active-reactions-sweep.test.ts",{"duration":0,"failed":false}],[":tests/pin-event-log.test.ts",{"duration":0,"failed":false}],[":tests/secret-detect-fail-closed.test.ts",{"duration":0,"failed":false}],[":tests/ipc-server-validate-operator.test.ts",{"duration":0,"failed":false}],[":tests/idle-footer-wiring.test.ts",{"duration":0,"failed":true}],[":tests/boot-card-reason.test.ts",{"duration":0,"failed":true}],[":tests/plugin-logger.test.ts",{"duration":0,"failed":false}],[":tests/vault-grant-wizard.test.ts",{"duration":0,"failed":false}],[":tests/turn-signal-tracker.test.ts",{"duration":0,"failed":false}],[":tests/context-exhaustion.test.ts",{"duration":0,"failed":false}],[":tests/gateway-startup-reset.test.ts",{"duration":0,"failed":false}],[":tests/idle-footer.test.ts",{"duration":0,"failed":false}],[":tests/poll-health.test.ts",{"duration":0,"failed":false}],[":tests/turn-flush-prose-recovery.test.ts",{"duration":0,"failed":false}],[":tests/secret-detect-suppressor-no-silent-allow.test.ts",{"duration":0,"failed":false}],[":tests/boot-card-dedupe.test.ts",{"duration":0,"failed":true}],[":tests/protocol-fixtures.test.ts",{"duration":0,"failed":false}],[":tests/secret-detect-staging.test.ts",{"duration":0,"failed":false}],[":tests/secret-detect-audit.test.ts",{"duration":0,"failed":false}],[":tests/secret-detect-gitleaks.test.ts",{"duration":0,"failed":false}],[":tests/subagent-registry-bugs.test.ts",{"duration":0,"failed":false}],[":tests/progress-card-driver-eviction.test.ts",{"duration":22.822707999999977,"failed":false}],[":tests/progress-card-driver-fleet-shadow.test.ts",{"duration":7.463677000000018,"failed":false}],[":tests/two-zone-card-lifecycle.test.ts",{"duration":0,"failed":false}],[":tests/two-zone-concurrent-turns-isolation.test.ts",{"duration":0,"failed":false}],[":tests/two-zone-bg-survives-next-turn.test.ts",{"duration":0,"failed":false}],[":tests/two-zone-card-snapshot.test.ts",{"duration":0,"failed":false}],[":tests/two-zone-stuck-edit-throttle.test.ts",{"duration":0,"failed":false}],[":tests/two-zone-bg-done-when-all-terminal.test.ts",{"duration":0,"failed":false}],[":tests/two-zone-card-html-balance.test.ts",{"duration":0,"failed":false}],[":tests/two-zone-bg-detection.test.ts",{"duration":0,"failed":false}],[":tests/progress-card-draft-flag.test.ts",{"duration":0,"failed":false}],[":tests/two-zone-stuck-per-member.test.ts",{"duration":0,"failed":false}],[":tests/two-zone-card-header-phases.test.ts",{"duration":0,"failed":false}],[":tests/two-zone-stuck-recovery.test.ts",{"duration":0,"failed":false}],[":tests/two-zone-stuck-header-escalation.test.ts",{"duration":0,"failed":false}],[":tests/two-zone-card-fleet-row.test.ts",{"duration":0,"failed":false}],[":tests/two-zone-card-cap.test.ts",{"duration":0,"failed":false}],[":tests/two-zone-card-sanitise.test.ts",{"duration":0,"failed":false}],[":tests/progress-card-close-paths-converge.test.ts",{"duration":10.167681000000016,"failed":false}],[":registry/subagents.test.ts",{"duration":0,"failed":true}],[":tests/issues-card.test.ts",{"duration":0,"failed":false}],[":registry/subagents-bugs.test.ts",{"duration":0,"failed":true}],[":tests/answer-stream-dedup.test.ts",{"duration":0,"failed":false}],[":tests/model-unavailable.test.ts",{"duration":0,"failed":false}],[":tests/preamble-suppressor.test.ts",{"duration":0,"failed":false}],[":tests/harness-ordering-invariants.test.ts",{"duration":0,"failed":false}],[":tests/real-gateway-spec.test.ts",{"duration":0,"failed":false}],[":tests/real-gateway-ipc-lifecycle.test.ts",{"duration":0,"failed":false}],[":tests/real-gateway-i6-turn-flush-replay-dedup.test.ts",{"duration":0,"failed":false}],[":tests/slot-banner-driver.e2e.test.ts",{"duration":0,"failed":false}],[":tests/waiting-ux.e2e.test.ts",{"duration":0,"failed":false}],[":tests/subagent-watcher-parent-marker.test.ts",{"duration":0,"failed":false}],[":tests/auth-code-redact.test.ts",{"duration":0,"failed":false}],[":tests/subagent-watcher-stall-notification.test.ts",{"duration":0,"failed":false}],[":tests/telegraph.test.ts",{"duration":0,"failed":true}],[":tests/recent-outbound-dedup.test.ts",{"duration":0,"failed":true}],[":tests/turn-flush-card-takeover.test.ts",{"duration":0,"failed":false}],[":tests/resolve-calling-subagent.test.ts",{"duration":0,"failed":true}],[":tests/secret-guard-pretool.test.ts",{"duration":0,"failed":true}],[":tests/first-paint.test.ts",{"duration":0,"failed":false}],[":tests/ask-user.test.ts",{"duration":0,"failed":true}],[":registry/api-registry.test.ts",{"duration":0,"failed":true}],[":tests/credits-watch.test.ts",{"duration":0,"failed":false}],[":admin-commands/dispatch.test.ts",{"duration":0,"failed":false}],[":tests/fleet-state.test.ts",{"duration":0,"failed":false}],[":tests/voice-transcribe.test.ts",{"duration":0,"failed":true}],[":tests/turn-active-marker.test.ts",{"duration":0,"failed":false}],[":tests/inline-keyboard-callbacks.test.ts",{"duration":0,"failed":false}],[":tests/issues-watcher.test.ts",{"duration":0,"failed":false}],[":tests/real-gateway-f1-ladder-integrity.test.ts",{"duration":0,"failed":false}],[":tests/gateway-disconnect-flush.test.ts",{"duration":0,"failed":false}],[":tests/reply-terminal-reaction.test.ts",{"duration":0,"failed":false}],[":tests/turn-flush-dedup-controller.test.ts",{"duration":0,"failed":false}],[":tests/real-gateway-f3-late-card.test.ts",{"duration":0,"failed":false}],[":tests/status-reactions-allowed-filter.test.ts",{"duration":0,"failed":false}],[":tests/pty-tail-real-fixture.test.ts",{"duration":0,"failed":false}],[":tests/real-gateway.smoke.test.ts",{"duration":0,"failed":false}],[":tests/harness-parse-mode-validation.test.ts",{"duration":0,"failed":false}],[":tests/inbound-coalesce.test.ts",{"duration":0,"failed":false}],[":tests/gateway-update-placeholder-dispatch.test.ts",{"duration":0,"failed":true}],[":tests/interrupt-marker.test.ts",{"duration":0,"failed":true}],[":tests/dm-command-gate.test.ts",{"duration":0,"failed":false}],[":tests/auto-fallback-dispatcher.e2e.test.ts",{"duration":0,"failed":false}],[":tests/sync-chat-running-subagents.test.ts",{"duration":0,"failed":false}],[":tests/subagents-schema-init-order.test.ts",{"duration":0,"failed":true}],[":tests/draft-transport.test.ts",{"duration":0,"failed":false}],[":gateway/access-validator.test.ts",{"duration":0,"failed":false}],[":registry/turns-schema.test.ts",{"duration":0,"failed":true}],[":tests/update-factory-edited-and-reactions.test.ts",{"duration":0,"failed":false}],[":tests/gateway-no-reply-single-emit.test.ts",{"duration":0,"failed":false}],[":tests/real-gateway-f2-instant-draft.test.ts",{"duration":0,"failed":false}],[":tests/gateway-boot-marker-clear.test.ts",{"duration":0,"failed":false}],[":tests/fleet-state-watcher.test.ts",{"duration":0,"failed":false}],[":tests/ipc-server-validate-update-placeholder.test.ts",{"duration":0,"failed":false}],[":tests/permission-title.test.ts",{"duration":0,"failed":false}],[":tests/slot-banner.test.ts",{"duration":0,"failed":false}],[":tests/sticker-aliases.test.ts",{"duration":0,"failed":true}],[":tests/ipc-server-anonymous-refuse.test.ts",{"duration":0,"failed":false}],[":tests/ipc-server-validate-pty-partial.test.ts",{"duration":0,"failed":false}],[":channel-envelope-safety.test.ts",{"duration":0,"failed":false}],[":tests/bridge-anonymous-refuse.test.ts",{"duration":0,"failed":false}],[":tests/send-typing-action-validation.test.ts",{"duration":0,"failed":false}],[":tests/spawn-detached-cgroup-escape.test.ts",{"duration":0,"failed":false}],[":gateway/boot-sweep-filter.test.ts",{"duration":0,"failed":false}]]}