@cuongtran001/kanna 1.63.2 → 1.63.3
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 +5 -9
- package/dist/client/assets/{BoardsPage-BJalB-bc.js → BoardsPage-AckCuswi.js} +1 -1
- package/dist/client/assets/BoardsRoutePage-DzKwKy3f.js +1 -0
- package/dist/client/assets/ChatPage-DsaH8CTz.js +93 -0
- package/dist/client/assets/{CronJobRow-CPbO_jwU.js → CronJobRow-Bx9gRqKW.js} +1 -1
- package/dist/client/assets/{CronJobsPage-Ck3SNgGi.js → CronJobsPage-v0-2X1oX.js} +1 -1
- package/dist/client/assets/{FilePreviewSheet-XEuUtDPn.js → FilePreviewSheet-B6SDxSD0.js} +2 -2
- package/dist/client/assets/{FlowSurface-BCbeMTJe.js → FlowSurface-CLC-ZFar.js} +1 -1
- package/dist/client/assets/{GenUIBlock-BJy_dfs-.js → GenUIBlock-CC79Ujro.js} +2 -2
- package/dist/client/assets/{KannaSocketProvider-D7Lzlt35.js → KannaSocketProvider-B2sHsWP5.js} +1 -1
- package/dist/client/assets/{LocalProjectsPage-9LJkQjhT.js → LocalProjectsPage-DYBkcEAm.js} +1 -1
- package/dist/client/assets/SettingsPage-CZqMkMRW.js +9 -0
- package/dist/client/assets/{StackBoardsRoutePage-t_EaFNG4.js → StackBoardsRoutePage-Clviz0hX.js} +1 -1
- package/dist/client/assets/{ToolCallMessage-RbF9XT2B.js → ToolCallMessage-BfNbWGmc.js} +1 -1
- package/dist/client/assets/{VChartSurface-CKuPelsn.js → VChartSurface-CUbxyBo3.js} +1 -1
- package/dist/client/assets/{WorkflowsPage-B6qUmQA3.js → WorkflowsPage-rengzb97.js} +1 -1
- package/dist/client/assets/{WorkflowsSection-DxO0S4An.js → WorkflowsSection-DuXdLsMO.js} +1 -1
- package/dist/client/assets/{chart-renderer-DjYZvNxh.js → chart-renderer-D7ukB2wg.js} +1 -1
- package/dist/client/assets/{chatSounds-FD2DQzF6.js → chatSounds-BLg2xPDZ.js} +1 -1
- package/dist/client/assets/chevron-left-RbFvmeyK.js +1 -0
- package/dist/client/assets/{clipboard.adapter-GN9nnYak.js → clipboard.adapter-CgdNwn-1.js} +1 -1
- package/dist/client/assets/{columnStyle-DjA7hA9k.js → columnStyle-CMCF0ds6.js} +1 -1
- package/dist/client/assets/{createScopedStore-BoLB9Rhc.js → createScopedStore-BrD5NHWx.js} +1 -1
- package/dist/client/assets/{dist-CimtOoGK.js → dist-DNSTHlSo.js} +1 -1
- package/dist/client/assets/editor-icons-DY4uKeEY.js +1 -0
- package/dist/client/assets/{ellipsis-Cd7sO0LO.js → ellipsis-OHg0x_x1.js} +1 -1
- package/dist/client/assets/{flow-kinds-CW2TU_83.js → flow-kinds-Ki2DP9nP.js} +1 -1
- package/dist/client/assets/{folder-C7sXCaHj.js → folder-CnWzWIG5.js} +1 -1
- package/dist/client/assets/{formatters-wSVyvDFZ.js → formatters-BGORekd-.js} +1 -1
- package/dist/client/assets/{genui-EzBIAYFE.js → genui-C9i6j0XZ.js} +1 -1
- package/dist/client/assets/{globe-xmRpmpma.js → globe-BjR1tyE4.js} +1 -1
- package/dist/client/assets/{host-DCaA9W-Z.js → host-D9tJwZXl.js} +1 -1
- package/dist/client/assets/{index-CDqjh7nt.js → index-CesRxZ0X.js} +7 -7
- package/dist/client/assets/index-CxDE9VOy.css +1 -0
- package/dist/client/assets/{info-D4MMvZFe.js → info-DNYekoE-.js} +1 -1
- package/dist/client/assets/{input-C4fvDiQ4.js → input-DgW9uRZN.js} +1 -1
- package/dist/client/assets/{lexicalToReact-sPUyOWk6.js → lexicalToReact-CZvp9Pgz.js} +3 -3
- package/dist/client/assets/{log-aTaDVF_e.js → log-DnEvbp3E.js} +1 -1
- package/dist/client/assets/{monitor-_CRKcV1h.js → monitor-qbWfUdBG.js} +1 -1
- package/dist/client/assets/{pencil-fNBeUUCf.js → pencil-C965rrpd.js} +1 -1
- package/dist/client/assets/{pendingActionsStore-COTh4Ztq.js → pendingActionsStore-CxhI19V9.js} +1 -1
- package/dist/client/assets/{play-BAmkJfn6.js → play-uofLR7yT.js} +1 -1
- package/dist/client/assets/{plus-c75CkNEO.js → plus-D4CI6rj1.js} +1 -1
- package/dist/client/assets/{popover-CHM4skzh.js → popover-Bc646hKn.js} +1 -1
- package/dist/client/assets/{pushClient-BJH_ITRB.js → pushClient-CxnHu9h4.js} +1 -1
- package/dist/client/assets/runDetached-DfQXHAaY.js +1 -0
- package/dist/client/assets/{segmented-control-DNOwRupT.js → segmented-control-CKGX29Kl.js} +1 -1
- package/dist/client/assets/{select-dLHiAls6.js → select-BjeajpVt.js} +1 -1
- package/dist/client/assets/{settings-header-button-CGKCzFyk.js → settings-header-button-BYvHR6Xl.js} +1 -1
- package/dist/client/assets/{sidebarPendingActions-Dyp80Lv_.js → sidebarPendingActions-xgMTnfRY.js} +1 -1
- package/dist/client/assets/{square-pen-B1IxmScM.js → square-pen-DmppQdTd.js} +1 -1
- package/dist/client/assets/{state-mark-BhTZfcYe.js → state-mark-C3kEL1hX.js} +1 -1
- package/dist/client/assets/{status-pill-CwhJhaMy.js → status-pill-Bo1Nb6HX.js} +1 -1
- package/dist/client/assets/{terminal-Bnj7qFGw.js → terminal-DfCF-J4p.js} +1 -1
- package/dist/client/assets/{timer.adapter-DHt2zwsL.js → timer.adapter-C2RhP4d6.js} +1 -1
- package/dist/client/assets/{tools-3gJcj3hK.js → tools-t5T_3UBg.js} +1 -1
- package/dist/client/assets/{triangle-alert-CWrc0CIO.js → triangle-alert-CYQhobyY.js} +1 -1
- package/dist/client/assets/useAppGlobalState-DUa3pkCF.js +1 -0
- package/dist/client/assets/useKannaState-FcieR5qb.js +5 -0
- package/dist/client/assets/useTheme-D9sKDPEm.js +1 -0
- package/dist/client/index.html +35 -35
- package/package.json +2 -2
- package/src/server/agent-coordinator-types.ts +0 -6
- package/src/server/agent-coordinator.ts +6 -45
- package/src/server/agent.compaction.test.ts +1 -13
- package/src/server/agent.oauth-account-info.test.ts +0 -3
- package/src/server/agent.oauth-release.test.ts +0 -2
- package/src/server/agent.oauth-rotation.test.ts +0 -9
- package/src/server/agent.oauth-token-share-cap.test.ts +0 -1
- package/src/server/agent.openrouter-model.test.ts +0 -2
- package/src/server/agent.openrouter-watchdog.test.ts +0 -3
- package/src/server/agent.sdk-workflow-register.test.ts +2 -64
- package/src/server/agent.test.ts +1 -319
- package/src/server/app-settings.test.ts +14 -17
- package/src/server/app-settings.ts +22 -37
- package/src/server/auto-continue/e2e.test.ts +0 -1
- package/src/server/background-task-output-registry.test.ts +1 -1
- package/src/server/background-task-output-registry.ts +1 -1
- package/src/server/claude-cancel-handler.test.ts +0 -58
- package/src/server/claude-cancel-handler.ts +1 -17
- package/src/server/claude-harness-stream.test.ts +134 -0
- package/src/server/{claude-pty/jsonl-path.test.ts → claude-projects-path.adapter.test.ts} +1 -19
- package/src/server/{claude-pty/jsonl-path.adapter.ts → claude-projects-path.adapter.ts} +0 -11
- package/src/server/claude-send-command.test.ts +0 -1
- package/src/server/claude-session-config-helpers.test.ts +1 -90
- package/src/server/claude-session-config-helpers.ts +1 -38
- package/src/server/claude-session-error-handler.test.ts +0 -1
- package/src/server/claude-session-lifecycle.test.ts +2 -36
- package/src/server/claude-session-lifecycle.ts +2 -8
- package/src/server/claude-session-runner.test.ts +8 -36
- package/src/server/claude-session-runner.ts +1 -26
- package/src/server/claude-session-spawner.test.ts +5 -64
- package/src/server/claude-session-spawner.ts +49 -115
- package/src/server/claude-session-start.test.ts +1 -14
- package/src/server/claude-session-start.ts +0 -8
- package/src/server/claude-session-state-queries.test.ts +1 -2
- package/src/server/claude-session-state.test.ts +0 -1
- package/src/server/claude-session-state.ts +0 -4
- package/src/server/claude-subagent-wiring.test.ts +2 -68
- package/src/server/claude-subagent-wiring.ts +3 -50
- package/src/server/claude-turn-starter-types.ts +0 -3
- package/src/server/claude-turn-starter.test.ts +0 -28
- package/src/server/claude-turn-starter.ts +0 -5
- package/src/server/event-store.test.ts +0 -2
- package/src/server/events.ts +1 -2
- package/src/server/harness-types.ts +1 -2
- package/src/server/import-subagent-drill.e2e.test.ts +1 -1
- package/src/server/imported-subagents-dir.test.ts +1 -1
- package/src/server/imported-subagents-dir.ts +1 -1
- package/src/server/kanna-mcp.test.ts +0 -75
- package/src/server/kanna-mcp.ts +1 -47
- package/src/server/server.test.ts +5 -7
- package/src/server/server.ts +2 -18
- package/src/server/subagent-orchestrator.test.ts +0 -1
- package/src/server/subagent-provider-run.test.ts +48 -10
- package/src/server/subagent-provider-run.ts +12 -3
- package/src/server/tool-callback.test.ts +1 -1
- package/src/server/ws-router-agent-ctrl.test.ts +0 -45
- package/src/server/ws-router-agent-ctrl.ts +1 -31
- package/src/server/ws-router-broadcast.ts +0 -27
- package/src/server/ws-router-defaults.test.ts +2 -3
- package/src/server/ws-router-defaults.ts +2 -3
- package/src/server/ws-router-envelope.ts +0 -15
- package/src/server/ws-router-observability.test.ts +1 -1
- package/src/server/ws-router-settings.test.ts +2 -2
- package/src/server/ws-router.test.ts +2 -2
- package/src/server/ws-router.ts +0 -8
- package/src/shared/app-settings-types.ts +9 -20
- package/src/shared/claude-api-error-classification.ts +1 -1
- package/src/shared/protocol.ts +1 -11
- package/dist/client/assets/BoardsRoutePage-BTTjJX-Y.js +0 -1
- package/dist/client/assets/ChatPage-DH8wtQyD.js +0 -93
- package/dist/client/assets/SettingsPage-CKrZ6dGQ.js +0 -9
- package/dist/client/assets/chevron-left-CJ0qv9kF.js +0 -1
- package/dist/client/assets/editor-icons-CONDQZof.js +0 -1
- package/dist/client/assets/index-C-UYaDk6.css +0 -1
- package/dist/client/assets/runDetached-DqQ3k2ji.js +0 -1
- package/dist/client/assets/useAppGlobalState-DKOGrO5l.js +0 -1
- package/dist/client/assets/useKannaState-D7R_-dM6.js +0 -5
- package/dist/client/assets/useTheme-B57LIKyx.js +0 -1
- package/src/server/agent.pty-rotation.test.ts +0 -322
- package/src/server/claude-pty/auth.test.ts +0 -38
- package/src/server/claude-pty/auth.ts +0 -17
- package/src/server/claude-pty/claude-session-registry.adapter.ts +0 -68
- package/src/server/claude-pty/claude-session-registry.test.ts +0 -149
- package/src/server/claude-pty/driver.test.ts +0 -1153
- package/src/server/claude-pty/driver.ts +0 -829
- package/src/server/claude-pty/env.test.ts +0 -79
- package/src/server/claude-pty/env.ts +0 -20
- package/src/server/claude-pty/jsonl-to-event.test.ts +0 -882
- package/src/server/claude-pty/jsonl-to-event.ts +0 -357
- package/src/server/claude-pty/parity-matrix.test.ts +0 -437
- package/src/server/claude-pty/pid-registry.adapter.ts +0 -164
- package/src/server/claude-pty/pid-registry.test.ts +0 -178
- package/src/server/claude-pty/preflight/binary-fingerprint.adapter.ts +0 -20
- package/src/server/claude-pty/preflight/binary-fingerprint.test.ts +0 -32
- package/src/server/claude-pty/pty-instance-registry.test.ts +0 -208
- package/src/server/claude-pty/pty-instance-registry.ts +0 -172
- package/src/server/claude-pty/pty-memory-sampler.adapter.test.ts +0 -103
- package/src/server/claude-pty/pty-memory-sampler.adapter.ts +0 -86
- package/src/server/claude-pty/pty-process.adapter.ts +0 -60
- package/src/server/claude-pty/pty-process.test.ts +0 -49
- package/src/server/claude-pty/resolve-binary.adapter.ts +0 -106
- package/src/server/claude-pty/resolve-binary.test.ts +0 -118
- package/src/server/claude-pty/runtime-dir.adapter.ts +0 -19
- package/src/server/claude-pty/sandbox/detect.adapter.ts +0 -17
- package/src/server/claude-pty/sandbox/detect.test.ts +0 -17
- package/src/server/claude-pty/sandbox/platform.test.ts +0 -35
- package/src/server/claude-pty/sandbox/platform.ts +0 -27
- package/src/server/claude-pty/sandbox/preflight-io.adapter.ts +0 -14
- package/src/server/claude-pty/sandbox/preflight.test.ts +0 -69
- package/src/server/claude-pty/sandbox/preflight.ts +0 -44
- package/src/server/claude-pty/sandbox/profile-linux.adapter.ts +0 -63
- package/src/server/claude-pty/sandbox/profile-linux.test.ts +0 -90
- package/src/server/claude-pty/sandbox/profile-macos.adapter.ts +0 -72
- package/src/server/claude-pty/sandbox/profile-macos.test.ts +0 -68
- package/src/server/claude-pty/sandbox/wrap.adapter.ts +0 -58
- package/src/server/claude-pty/sandbox/wrap.test.ts +0 -82
- package/src/server/claude-pty/settings-writer.adapter.ts +0 -23
- package/src/server/claude-pty/settings-writer.test.ts +0 -22
- package/src/server/claude-pty/smoke-test-io.adapter.ts +0 -28
- package/src/server/claude-pty/smoke-test.test.ts +0 -199
- package/src/server/claude-pty/smoke-test.ts +0 -182
- package/src/server/claude-pty/tui-control.test.ts +0 -300
- package/src/server/claude-pty/tui-control.ts +0 -174
- package/src/server/claude-pty/tui-source.adapter.test.ts +0 -374
- package/src/server/claude-pty/tui-source.adapter.ts +0 -294
- package/src/server/http-server.adapter.test.ts +0 -18
- package/src/server/http-server.adapter.ts +0 -39
- package/src/server/kanna-mcp-http.test.ts +0 -274
- package/src/server/kanna-mcp-http.ts +0 -194
- package/src/server/kanna-mcp-tools/ask-user-question.test.ts +0 -108
- package/src/server/kanna-mcp-tools/ask-user-question.ts +0 -55
- package/src/server/kanna-mcp-tools/exit-plan-mode.test.ts +0 -61
- package/src/server/kanna-mcp-tools/exit-plan-mode.ts +0 -49
- package/src/server/mcp-zod-compat.adapter.ts +0 -20
- package/src/shared/pty-instance.ts +0 -43
- /package/src/server/{claude-pty/output-ring.test.ts → output-ring.test.ts} +0 -0
- /package/src/server/{claude-pty/output-ring.ts → output-ring.ts} +0 -0
|
@@ -1,199 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test, beforeEach, afterEach } from "bun:test"
|
|
2
|
-
import { mkdtemp, rm, writeFile } from "node:fs/promises"
|
|
3
|
-
import { tmpdir } from "node:os"
|
|
4
|
-
import path from "node:path"
|
|
5
|
-
import { createSmokeTestGate, createFileSmokeTestCache, type SmokeTestProbeFn, type SmokeTestCache } from "./smoke-test"
|
|
6
|
-
|
|
7
|
-
let workHome: string
|
|
8
|
-
|
|
9
|
-
function inMemoryCache(): SmokeTestCache {
|
|
10
|
-
const store = new Map<string, { result: "pass" | "fail"; ts: number }>()
|
|
11
|
-
return {
|
|
12
|
-
async get(key) { return store.get(key) ?? null },
|
|
13
|
-
async set(key, entry) { store.set(key, entry) },
|
|
14
|
-
async invalidate() { store.clear() },
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
beforeEach(async () => {
|
|
19
|
-
workHome = await mkdtemp(path.join(tmpdir(), "kanna-smoke-"))
|
|
20
|
-
await writeFile(path.join(workHome, "fake-claude"), "#!/bin/sh\necho fake\n", { mode: 0o755 })
|
|
21
|
-
})
|
|
22
|
-
|
|
23
|
-
afterEach(async () => {
|
|
24
|
-
await rm(workHome, { recursive: true, force: true })
|
|
25
|
-
})
|
|
26
|
-
|
|
27
|
-
describe("createSmokeTestGate", () => {
|
|
28
|
-
test("cached PASS skips probe", async () => {
|
|
29
|
-
let probeRan = false
|
|
30
|
-
const probe: SmokeTestProbeFn = async () => { probeRan = true; return "pass" }
|
|
31
|
-
const cache = inMemoryCache()
|
|
32
|
-
await cache.set("aaa|claude-opus-4-7", { result: "pass", ts: Date.now() })
|
|
33
|
-
const gate = createSmokeTestGate({ probe, cache, ttlMs: 24 * 3600 * 1000, now: () => Date.now() })
|
|
34
|
-
const result = await gate.canSpawn({ binarySha256: "aaa", model: "claude-opus-4-7" })
|
|
35
|
-
expect(result.ok).toBe(true)
|
|
36
|
-
expect(probeRan).toBe(false)
|
|
37
|
-
})
|
|
38
|
-
|
|
39
|
-
test("a cached result for one endpoint is not reused for another", async () => {
|
|
40
|
-
let probeRan = 0
|
|
41
|
-
const probe: SmokeTestProbeFn = async () => { probeRan += 1; return "pass" }
|
|
42
|
-
const cache = inMemoryCache()
|
|
43
|
-
const gate = createSmokeTestGate({ probe, cache, ttlMs: 24 * 3600 * 1000, now: () => Date.now() })
|
|
44
|
-
|
|
45
|
-
await gate.canSpawn({ binarySha256: "aaa", model: "m1" })
|
|
46
|
-
expect(probeRan).toBe(1)
|
|
47
|
-
|
|
48
|
-
await gate.canSpawn({ binarySha256: "aaa", model: "m1", baseUrl: "https://proxy.example" })
|
|
49
|
-
expect(probeRan).toBe(2)
|
|
50
|
-
|
|
51
|
-
await gate.canSpawn({ binarySha256: "aaa", model: "m1", baseUrl: "https://proxy.example" })
|
|
52
|
-
expect(probeRan).toBe(2)
|
|
53
|
-
})
|
|
54
|
-
|
|
55
|
-
test("an absent endpoint keeps the legacy cache key, so existing entries still hit", async () => {
|
|
56
|
-
let probeRan = false
|
|
57
|
-
const probe: SmokeTestProbeFn = async () => { probeRan = true; return "pass" }
|
|
58
|
-
const cache = inMemoryCache()
|
|
59
|
-
await cache.set("ccc|m1", { result: "pass", ts: Date.now() })
|
|
60
|
-
const gate = createSmokeTestGate({ probe, cache, ttlMs: 24 * 3600 * 1000, now: () => Date.now() })
|
|
61
|
-
|
|
62
|
-
expect((await gate.canSpawn({ binarySha256: "ccc", model: "m1" })).ok).toBe(true)
|
|
63
|
-
expect((await gate.canSpawn({ binarySha256: "ccc", model: "m1", baseUrl: null })).ok).toBe(true)
|
|
64
|
-
expect(probeRan).toBe(false)
|
|
65
|
-
})
|
|
66
|
-
|
|
67
|
-
test("cached FAIL refuses spawn without running probe", async () => {
|
|
68
|
-
let probeRan = false
|
|
69
|
-
const probe: SmokeTestProbeFn = async () => { probeRan = true; return "pass" }
|
|
70
|
-
const cache = inMemoryCache()
|
|
71
|
-
await cache.set("bbb|claude-opus-4-7", { result: "fail", ts: Date.now() })
|
|
72
|
-
const gate = createSmokeTestGate({ probe, cache, ttlMs: 24 * 3600 * 1000, now: () => Date.now() })
|
|
73
|
-
const result = await gate.canSpawn({ binarySha256: "bbb", model: "claude-opus-4-7" })
|
|
74
|
-
expect(result.ok).toBe(false)
|
|
75
|
-
if (!result.ok) expect(result.reason).toMatch(/disallowedTools/i)
|
|
76
|
-
expect(probeRan).toBe(false)
|
|
77
|
-
})
|
|
78
|
-
|
|
79
|
-
test("cache miss runs probe and caches PASS", async () => {
|
|
80
|
-
let probeRan = false
|
|
81
|
-
const probe: SmokeTestProbeFn = async () => { probeRan = true; return "pass" }
|
|
82
|
-
const cache = inMemoryCache()
|
|
83
|
-
const gate = createSmokeTestGate({ probe, cache, ttlMs: 24 * 3600 * 1000, now: () => Date.now() })
|
|
84
|
-
const result = await gate.canSpawn({ binarySha256: "ccc", model: "m1" })
|
|
85
|
-
expect(result.ok).toBe(true)
|
|
86
|
-
expect(probeRan).toBe(true)
|
|
87
|
-
const cached = await cache.get("ccc|m1")
|
|
88
|
-
expect(cached?.result).toBe("pass")
|
|
89
|
-
})
|
|
90
|
-
|
|
91
|
-
test("cache miss runs probe and refuses spawn on FAIL", async () => {
|
|
92
|
-
const probe: SmokeTestProbeFn = async () => "fail"
|
|
93
|
-
const cache = inMemoryCache()
|
|
94
|
-
const gate = createSmokeTestGate({ probe, cache, ttlMs: 24 * 3600 * 1000, now: () => Date.now() })
|
|
95
|
-
const result = await gate.canSpawn({ binarySha256: "ddd", model: "m1" })
|
|
96
|
-
expect(result.ok).toBe(false)
|
|
97
|
-
const cached = await cache.get("ddd|m1")
|
|
98
|
-
expect(cached?.result).toBe("fail")
|
|
99
|
-
})
|
|
100
|
-
|
|
101
|
-
test("expired cache entry triggers re-probe", async () => {
|
|
102
|
-
let probeRan = 0
|
|
103
|
-
const probe: SmokeTestProbeFn = async () => { probeRan++; return "pass" }
|
|
104
|
-
const cache = inMemoryCache()
|
|
105
|
-
let nowMs = 1_000_000
|
|
106
|
-
await cache.set("eee|m1", { result: "pass", ts: nowMs })
|
|
107
|
-
const gate = createSmokeTestGate({ probe, cache, ttlMs: 1000, now: () => nowMs })
|
|
108
|
-
await gate.canSpawn({ binarySha256: "eee", model: "m1" })
|
|
109
|
-
expect(probeRan).toBe(0)
|
|
110
|
-
nowMs += 2000
|
|
111
|
-
await gate.canSpawn({ binarySha256: "eee", model: "m1" })
|
|
112
|
-
expect(probeRan).toBe(1)
|
|
113
|
-
})
|
|
114
|
-
})
|
|
115
|
-
|
|
116
|
-
describe("createSmokeTestGate singleflight (adr-20260522-oauth-token-share-cap)", () => {
|
|
117
|
-
test("concurrent canSpawn calls on same (sha,model) collapse to one probe", async () => {
|
|
118
|
-
let probeStartCount = 0
|
|
119
|
-
const resolvers: Array<(r: "pass" | "fail") => void> = []
|
|
120
|
-
const probe: SmokeTestProbeFn = () => {
|
|
121
|
-
probeStartCount += 1
|
|
122
|
-
return new Promise<"pass" | "fail">((r) => { resolvers.push(r) })
|
|
123
|
-
}
|
|
124
|
-
const cache = inMemoryCache()
|
|
125
|
-
const gate = createSmokeTestGate({ probe, cache, ttlMs: 24 * 3600 * 1000, now: () => Date.now() })
|
|
126
|
-
|
|
127
|
-
const results = [
|
|
128
|
-
gate.canSpawn({ binarySha256: "fff", model: "m1" }),
|
|
129
|
-
gate.canSpawn({ binarySha256: "fff", model: "m1" }),
|
|
130
|
-
gate.canSpawn({ binarySha256: "fff", model: "m1" }),
|
|
131
|
-
gate.canSpawn({ binarySha256: "fff", model: "m1" }),
|
|
132
|
-
gate.canSpawn({ binarySha256: "fff", model: "m1" }),
|
|
133
|
-
]
|
|
134
|
-
await Promise.resolve()
|
|
135
|
-
expect(probeStartCount).toBe(1)
|
|
136
|
-
expect(resolvers).toHaveLength(1)
|
|
137
|
-
resolvers[0]("pass")
|
|
138
|
-
const resolved = await Promise.all(results)
|
|
139
|
-
for (const r of resolved) expect(r.ok).toBe(true)
|
|
140
|
-
expect(probeStartCount).toBe(1)
|
|
141
|
-
})
|
|
142
|
-
|
|
143
|
-
test("after a probe resolves, future cache-miss callers run a fresh probe", async () => {
|
|
144
|
-
let probeStartCount = 0
|
|
145
|
-
const probe: SmokeTestProbeFn = async () => { probeStartCount += 1; return "pass" }
|
|
146
|
-
const cache: SmokeTestCache = {
|
|
147
|
-
async get() { return null },
|
|
148
|
-
async set() { },
|
|
149
|
-
async invalidate() { },
|
|
150
|
-
}
|
|
151
|
-
const gate = createSmokeTestGate({ probe, cache, ttlMs: 24 * 3600 * 1000, now: () => Date.now() })
|
|
152
|
-
await gate.canSpawn({ binarySha256: "ggg", model: "m1" })
|
|
153
|
-
await gate.canSpawn({ binarySha256: "ggg", model: "m1" })
|
|
154
|
-
expect(probeStartCount).toBe(2)
|
|
155
|
-
})
|
|
156
|
-
|
|
157
|
-
test("singleflight is keyed by (sha,model) — different keys probe independently", async () => {
|
|
158
|
-
let probeStartCount = 0
|
|
159
|
-
const resolvers: Array<(r: "pass" | "fail") => void> = []
|
|
160
|
-
const probe: SmokeTestProbeFn = () => {
|
|
161
|
-
probeStartCount += 1
|
|
162
|
-
return new Promise<"pass" | "fail">((r) => { resolvers.push(r) })
|
|
163
|
-
}
|
|
164
|
-
const cache = inMemoryCache()
|
|
165
|
-
const gate = createSmokeTestGate({ probe, cache, ttlMs: 24 * 3600 * 1000, now: () => Date.now() })
|
|
166
|
-
const a = gate.canSpawn({ binarySha256: "hhh", model: "m1" })
|
|
167
|
-
const b = gate.canSpawn({ binarySha256: "hhh", model: "m2" })
|
|
168
|
-
await Promise.resolve()
|
|
169
|
-
expect(probeStartCount).toBe(2)
|
|
170
|
-
expect(resolvers).toHaveLength(2)
|
|
171
|
-
resolvers[0]("pass")
|
|
172
|
-
resolvers[1]("pass")
|
|
173
|
-
await Promise.all([a, b])
|
|
174
|
-
})
|
|
175
|
-
})
|
|
176
|
-
|
|
177
|
-
describe("createFileSmokeTestCache", () => {
|
|
178
|
-
test("round-trips an entry through disk", async () => {
|
|
179
|
-
const dir = path.join(workHome, "smoke-cache")
|
|
180
|
-
const cache = createFileSmokeTestCache({ cacheDir: dir })
|
|
181
|
-
await cache.set("abc|m1", { result: "pass", ts: 1234 })
|
|
182
|
-
const got = await cache.get("abc|m1")
|
|
183
|
-
expect(got).toEqual({ result: "pass", ts: 1234 })
|
|
184
|
-
})
|
|
185
|
-
|
|
186
|
-
test("returns null on missing key", async () => {
|
|
187
|
-
const cache = createFileSmokeTestCache({ cacheDir: path.join(workHome, "smoke-cache-2") })
|
|
188
|
-
const got = await cache.get("missing|m1")
|
|
189
|
-
expect(got).toBeNull()
|
|
190
|
-
})
|
|
191
|
-
|
|
192
|
-
test("invalidate wipes the dir", async () => {
|
|
193
|
-
const dir = path.join(workHome, "smoke-cache-3")
|
|
194
|
-
const cache = createFileSmokeTestCache({ cacheDir: dir })
|
|
195
|
-
await cache.set("xxx|m", { result: "pass", ts: 1 })
|
|
196
|
-
await cache.invalidate()
|
|
197
|
-
expect(await cache.get("xxx|m")).toBeNull()
|
|
198
|
-
})
|
|
199
|
-
})
|
|
@@ -1,182 +0,0 @@
|
|
|
1
|
-
import path from "node:path"
|
|
2
|
-
import {
|
|
3
|
-
fileExists,
|
|
4
|
-
makeTempCwd,
|
|
5
|
-
mkdirRecursive,
|
|
6
|
-
readTextFile,
|
|
7
|
-
rmDirRecursive,
|
|
8
|
-
writeFile0600,
|
|
9
|
-
} from "./smoke-test-io.adapter"
|
|
10
|
-
import { isRecord } from "../../shared/errors"
|
|
11
|
-
import { buildPtyEnv } from "./env"
|
|
12
|
-
import { OutputRing } from "./output-ring"
|
|
13
|
-
import { spawnPtyProcess as defaultSpawnPtyProcess } from "./pty-process.adapter"
|
|
14
|
-
import { waitForTuiReadyWithTrustDismiss, sendUserPrompt, sendExitCommand } from "./tui-control"
|
|
15
|
-
import { startTranscriptStream, waitForResultEntry } from "./tui-source.adapter"
|
|
16
|
-
import { computeProjectDir } from "./jsonl-path.adapter"
|
|
17
|
-
import { log } from "../../shared/log"
|
|
18
|
-
|
|
19
|
-
export type SmokeTestProbeFn = () => Promise<"pass" | "fail">
|
|
20
|
-
|
|
21
|
-
export interface SmokeTestCacheEntry {
|
|
22
|
-
result: "pass" | "fail"
|
|
23
|
-
ts: number
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export interface SmokeTestCache {
|
|
27
|
-
get(key: string): Promise<SmokeTestCacheEntry | null>
|
|
28
|
-
set(key: string, entry: SmokeTestCacheEntry): Promise<void>
|
|
29
|
-
invalidate(): Promise<void>
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export interface SmokeTestGateArgs {
|
|
33
|
-
probe: SmokeTestProbeFn
|
|
34
|
-
cache: SmokeTestCache
|
|
35
|
-
ttlMs: number
|
|
36
|
-
now: () => number
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export interface CanSpawnArgs {
|
|
40
|
-
binarySha256: string
|
|
41
|
-
model: string
|
|
42
|
-
baseUrl?: string | null
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export interface SmokeTestGate {
|
|
46
|
-
canSpawn(args: CanSpawnArgs): Promise<{ ok: true } | { ok: false; reason: string }>
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export function createSmokeTestGate(args: SmokeTestGateArgs): SmokeTestGate {
|
|
50
|
-
const { probe, cache, ttlMs, now } = args
|
|
51
|
-
const inFlight = new Map<string, Promise<{ ok: true } | { ok: false; reason: string }>>()
|
|
52
|
-
return {
|
|
53
|
-
async canSpawn(spawnArgs: CanSpawnArgs) {
|
|
54
|
-
const key = spawnArgs.baseUrl
|
|
55
|
-
? `${spawnArgs.binarySha256}|${spawnArgs.model}|${spawnArgs.baseUrl}`
|
|
56
|
-
: `${spawnArgs.binarySha256}|${spawnArgs.model}`
|
|
57
|
-
const cached = await cache.get(key)
|
|
58
|
-
const currentTs = now()
|
|
59
|
-
if (cached && currentTs - cached.ts < ttlMs) {
|
|
60
|
-
if (cached.result === "pass") return { ok: true }
|
|
61
|
-
return { ok: false, reason: "cached smoke test FAIL: --disallowedTools not enforced for this claude binary + model" }
|
|
62
|
-
}
|
|
63
|
-
const existing = inFlight.get(key)
|
|
64
|
-
if (existing) return existing
|
|
65
|
-
const run = (async () => {
|
|
66
|
-
const probeResult = await probe()
|
|
67
|
-
await cache.set(key, { result: probeResult, ts: now() })
|
|
68
|
-
if (probeResult === "pass") return { ok: true } as const
|
|
69
|
-
return { ok: false, reason: "smoke test FAIL: claude invoked a disallowedTool — refusing spawn" } as const
|
|
70
|
-
})()
|
|
71
|
-
inFlight.set(key, run)
|
|
72
|
-
try {
|
|
73
|
-
return await run
|
|
74
|
-
} finally {
|
|
75
|
-
inFlight.delete(key)
|
|
76
|
-
}
|
|
77
|
-
},
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
export interface BuildLiveSmokeProbeArgs {
|
|
82
|
-
claudeBinPath: string
|
|
83
|
-
model: string
|
|
84
|
-
oauthToken: string
|
|
85
|
-
homeDir: string
|
|
86
|
-
baseUrl?: string | null
|
|
87
|
-
spawnPtyProcess?: typeof defaultSpawnPtyProcess
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
export function buildLiveSmokeProbe(args: BuildLiveSmokeProbeArgs): SmokeTestProbeFn {
|
|
91
|
-
const spawnPty = args.spawnPtyProcess ?? defaultSpawnPtyProcess
|
|
92
|
-
return async () => {
|
|
93
|
-
const tmpCwd = await makeTempCwd("kanna-smoke-cwd-")
|
|
94
|
-
const ring = new OutputRing()
|
|
95
|
-
const cliArgs = [
|
|
96
|
-
"--model", args.model,
|
|
97
|
-
"--permission-mode", "acceptEdits",
|
|
98
|
-
"--dangerously-skip-permissions",
|
|
99
|
-
"--disallowedTools", "Bash",
|
|
100
|
-
]
|
|
101
|
-
const spawnEnv = buildPtyEnv({
|
|
102
|
-
baseEnv: process.env,
|
|
103
|
-
homeDir: args.homeDir,
|
|
104
|
-
oauthToken: args.oauthToken,
|
|
105
|
-
baseUrl: args.baseUrl,
|
|
106
|
-
})
|
|
107
|
-
const pty = await spawnPty({
|
|
108
|
-
command: args.claudeBinPath,
|
|
109
|
-
args: cliArgs,
|
|
110
|
-
cwd: tmpCwd,
|
|
111
|
-
env: spawnEnv,
|
|
112
|
-
onOutput: (chunk) => ring.append(chunk),
|
|
113
|
-
})
|
|
114
|
-
let probeResult: "pass" | "fail" = "pass"
|
|
115
|
-
try {
|
|
116
|
-
await waitForTuiReadyWithTrustDismiss(pty, ring, { hardCapMs: 15_000 })
|
|
117
|
-
const projectDir = computeProjectDir({ homeDir: args.homeDir, cwd: tmpCwd })
|
|
118
|
-
const stream = await startTranscriptStream({ projectDir, firstFileTimeoutMs: 20_000 })
|
|
119
|
-
try {
|
|
120
|
-
await sendUserPrompt(
|
|
121
|
-
pty,
|
|
122
|
-
ring,
|
|
123
|
-
"Use the Bash tool to run: ls /tmp. If the Bash tool is not available, reply with exactly BASH_UNAVAILABLE and end your turn. Do not use any other tool and do not look for alternatives.",
|
|
124
|
-
)
|
|
125
|
-
const filePath = await stream.filePath
|
|
126
|
-
await waitForResultEntry(stream, { timeoutMs: 30_000 })
|
|
127
|
-
const raw = await readTextFile(filePath)
|
|
128
|
-
for (const line of raw.split("\n")) {
|
|
129
|
-
if (!line.trim()) continue
|
|
130
|
-
let parsed: { message?: { content?: Array<{ type?: string; name?: string }> } }
|
|
131
|
-
try { parsed = JSON.parse(line) } catch { continue }
|
|
132
|
-
const blocks = parsed.message?.content
|
|
133
|
-
if (!Array.isArray(blocks)) continue
|
|
134
|
-
for (const b of blocks) {
|
|
135
|
-
if (b?.type === "tool_use" && b.name === "Bash") {
|
|
136
|
-
probeResult = "fail"
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
} finally {
|
|
141
|
-
stream.close()
|
|
142
|
-
}
|
|
143
|
-
} catch (err) {
|
|
144
|
-
const errWithCode: { code?: string } = isRecord(err) ? err : {}
|
|
145
|
-
if (err instanceof Error && errWithCode.code === "rate_limited") throw err
|
|
146
|
-
log.warn("[kanna/pty] smoke probe errored, treating as FAIL", String(err))
|
|
147
|
-
probeResult = "fail"
|
|
148
|
-
} finally {
|
|
149
|
-
try { await sendExitCommand(pty) } catch { }
|
|
150
|
-
try { pty.close() } catch { }
|
|
151
|
-
try { await rmDirRecursive(tmpCwd) } catch { }
|
|
152
|
-
}
|
|
153
|
-
return probeResult
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
export function createFileSmokeTestCache(args: { cacheDir: string }): SmokeTestCache {
|
|
158
|
-
const dir = args.cacheDir
|
|
159
|
-
const fileFor = (key: string) => path.join(dir, `${key.replace(/[^a-z0-9._-]/gi, "_")}.json`)
|
|
160
|
-
return {
|
|
161
|
-
async get(key) {
|
|
162
|
-
const fp = fileFor(key)
|
|
163
|
-
if (!fileExists(fp)) return null
|
|
164
|
-
try {
|
|
165
|
-
const raw = await readTextFile(fp)
|
|
166
|
-
const parsed: SmokeTestCacheEntry = JSON.parse(raw)
|
|
167
|
-
if (parsed.result !== "pass" && parsed.result !== "fail") return null
|
|
168
|
-
if (typeof parsed.ts !== "number") return null
|
|
169
|
-
return parsed
|
|
170
|
-
} catch {
|
|
171
|
-
return null
|
|
172
|
-
}
|
|
173
|
-
},
|
|
174
|
-
async set(key, entry) {
|
|
175
|
-
await mkdirRecursive(dir)
|
|
176
|
-
await writeFile0600(fileFor(key), JSON.stringify(entry))
|
|
177
|
-
},
|
|
178
|
-
async invalidate() {
|
|
179
|
-
try { await rmDirRecursive(dir) } catch { }
|
|
180
|
-
},
|
|
181
|
-
}
|
|
182
|
-
}
|
|
@@ -1,300 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from "bun:test"
|
|
2
|
-
import {
|
|
3
|
-
sendUserPrompt,
|
|
4
|
-
sendExitCommand,
|
|
5
|
-
dismissTrustDialogIfPresent,
|
|
6
|
-
dismissDevChannelsDialogIfPresent,
|
|
7
|
-
waitForTuiReady,
|
|
8
|
-
waitForTuiReadyWithTrustDismiss,
|
|
9
|
-
waitForTuiReadyDismissingDialogs,
|
|
10
|
-
TRUST_DIALOG_MARKER,
|
|
11
|
-
TUI_READY_MARKER,
|
|
12
|
-
DEV_CHANNELS_DIALOG_MARKER,
|
|
13
|
-
} from "./tui-control"
|
|
14
|
-
import { OutputRing } from "./output-ring"
|
|
15
|
-
import type { PtyProcess } from "./pty-process.adapter"
|
|
16
|
-
|
|
17
|
-
function fakePty(): PtyProcess & { sent: string[] } {
|
|
18
|
-
const sent: string[] = []
|
|
19
|
-
return {
|
|
20
|
-
sent,
|
|
21
|
-
pid: 99997,
|
|
22
|
-
async sendInput(data: string) { sent.push(data) },
|
|
23
|
-
resize() { },
|
|
24
|
-
exited: new Promise<number>(() => { }),
|
|
25
|
-
close() { },
|
|
26
|
-
kill() { },
|
|
27
|
-
} as PtyProcess & { sent: string[] }
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
describe("sendUserPrompt", () => {
|
|
31
|
-
test("writes bracketed-paste wrapped text then separate carriage return once ring grows", async () => {
|
|
32
|
-
const pty = fakePty()
|
|
33
|
-
const ring = new OutputRing()
|
|
34
|
-
setTimeout(() => ring.append("[Pasted text #1 +56 lines]"), 5)
|
|
35
|
-
await sendUserPrompt(pty, ring, "say hi", { commitTimeoutMs: 500, pollMs: 1 })
|
|
36
|
-
expect(pty.sent).toEqual(["\x1b[200~say hi\x1b[201~", "\r"])
|
|
37
|
-
})
|
|
38
|
-
|
|
39
|
-
test("empty string still emits paste markers + carriage return after commit", async () => {
|
|
40
|
-
const pty = fakePty()
|
|
41
|
-
const ring = new OutputRing()
|
|
42
|
-
setTimeout(() => ring.append("x"), 5)
|
|
43
|
-
await sendUserPrompt(pty, ring, "", { commitTimeoutMs: 500, pollMs: 1 })
|
|
44
|
-
expect(pty.sent).toEqual(["\x1b[200~\x1b[201~", "\r"])
|
|
45
|
-
})
|
|
46
|
-
|
|
47
|
-
test("sends Enter after commitTimeoutMs even if the ring never grows (degraded fallback)", async () => {
|
|
48
|
-
const pty = fakePty()
|
|
49
|
-
const ring = new OutputRing()
|
|
50
|
-
const start = Date.now()
|
|
51
|
-
await sendUserPrompt(pty, ring, "no echo", { commitTimeoutMs: 50, pollMs: 5 })
|
|
52
|
-
const elapsed = Date.now() - start
|
|
53
|
-
expect(pty.sent).toEqual(["\x1b[200~no echo\x1b[201~", "\r"])
|
|
54
|
-
expect(elapsed).toBeGreaterThanOrEqual(45)
|
|
55
|
-
})
|
|
56
|
-
})
|
|
57
|
-
|
|
58
|
-
describe("sendExitCommand", () => {
|
|
59
|
-
test("writes /exit + carriage return", async () => {
|
|
60
|
-
const pty = fakePty()
|
|
61
|
-
await sendExitCommand(pty)
|
|
62
|
-
expect(pty.sent).toEqual(["/exit\r"])
|
|
63
|
-
})
|
|
64
|
-
})
|
|
65
|
-
|
|
66
|
-
describe("dismissTrustDialogIfPresent", () => {
|
|
67
|
-
test("sends carriage return when ringbuf contains trust marker", async () => {
|
|
68
|
-
const pty = fakePty()
|
|
69
|
-
const ring = new OutputRing()
|
|
70
|
-
ring.append("Quick safety check: Do you trust this folder? trust this folder")
|
|
71
|
-
const dismissed = await dismissTrustDialogIfPresent(pty, ring)
|
|
72
|
-
expect(dismissed).toBe(true)
|
|
73
|
-
expect(pty.sent).toEqual(["\r"])
|
|
74
|
-
})
|
|
75
|
-
|
|
76
|
-
test("does nothing when ringbuf lacks trust marker", async () => {
|
|
77
|
-
const pty = fakePty()
|
|
78
|
-
const ring = new OutputRing()
|
|
79
|
-
ring.append("Welcome back c!")
|
|
80
|
-
const dismissed = await dismissTrustDialogIfPresent(pty, ring)
|
|
81
|
-
expect(dismissed).toBe(false)
|
|
82
|
-
expect(pty.sent).toEqual([])
|
|
83
|
-
})
|
|
84
|
-
|
|
85
|
-
test("exported TRUST_DIALOG_MARKER is the substring matched", () => {
|
|
86
|
-
expect(TRUST_DIALOG_MARKER).toBe("trust this folder")
|
|
87
|
-
})
|
|
88
|
-
})
|
|
89
|
-
|
|
90
|
-
describe("waitForTuiReady", () => {
|
|
91
|
-
test("returns 'marker' when ringbuf already contains the input-box marker", async () => {
|
|
92
|
-
const ring = new OutputRing()
|
|
93
|
-
ring.append("❯ ")
|
|
94
|
-
const result = await waitForTuiReady(ring, { hardCapMs: 1000, pollMs: 10, quietPeriodMs: 0 })
|
|
95
|
-
expect(result).toBe("marker")
|
|
96
|
-
})
|
|
97
|
-
|
|
98
|
-
test("returns 'timeout' when no marker appears within hardCapMs", async () => {
|
|
99
|
-
const ring = new OutputRing()
|
|
100
|
-
const result = await waitForTuiReady(ring, { hardCapMs: 200, pollMs: 10, quietPeriodMs: 0 })
|
|
101
|
-
expect(result).toBe("timeout")
|
|
102
|
-
})
|
|
103
|
-
|
|
104
|
-
test("polls until marker appears", async () => {
|
|
105
|
-
const ring = new OutputRing()
|
|
106
|
-
setTimeout(() => ring.append("❯ "), 50)
|
|
107
|
-
const start = Date.now()
|
|
108
|
-
const result = await waitForTuiReady(ring, { hardCapMs: 1000, pollMs: 10, quietPeriodMs: 0 })
|
|
109
|
-
const elapsed = Date.now() - start
|
|
110
|
-
expect(result).toBe("marker")
|
|
111
|
-
expect(elapsed).toBeLessThan(300)
|
|
112
|
-
})
|
|
113
|
-
|
|
114
|
-
test("waits for ring to stay quiet for quietPeriodMs after marker hit", async () => {
|
|
115
|
-
const ring = new OutputRing()
|
|
116
|
-
ring.append("splash banner ❯ ")
|
|
117
|
-
const interval = setInterval(() => ring.append("."), 30)
|
|
118
|
-
setTimeout(() => clearInterval(interval), 200)
|
|
119
|
-
const start = Date.now()
|
|
120
|
-
const result = await waitForTuiReady(ring, { hardCapMs: 2000, pollMs: 10, quietPeriodMs: 150 })
|
|
121
|
-
const elapsed = Date.now() - start
|
|
122
|
-
expect(result).toBe("marker")
|
|
123
|
-
expect(elapsed).toBeGreaterThanOrEqual(300)
|
|
124
|
-
})
|
|
125
|
-
|
|
126
|
-
test("resolves immediately when ring stays quiet from the start", async () => {
|
|
127
|
-
const ring = new OutputRing()
|
|
128
|
-
ring.append("❯ ")
|
|
129
|
-
const start = Date.now()
|
|
130
|
-
const result = await waitForTuiReady(ring, { hardCapMs: 2000, pollMs: 10, quietPeriodMs: 80 })
|
|
131
|
-
const elapsed = Date.now() - start
|
|
132
|
-
expect(result).toBe("marker")
|
|
133
|
-
expect(elapsed).toBeGreaterThanOrEqual(70)
|
|
134
|
-
expect(elapsed).toBeLessThan(250)
|
|
135
|
-
})
|
|
136
|
-
|
|
137
|
-
test("exported TUI_READY_MARKER is the input-box prompt", () => {
|
|
138
|
-
expect(TUI_READY_MARKER).toBe("❯ ")
|
|
139
|
-
})
|
|
140
|
-
|
|
141
|
-
test("returns 'marker' when TUI renders ❯ with \\x1b[1C instead of space", async () => {
|
|
142
|
-
const ring = new OutputRing()
|
|
143
|
-
ring.append("❯\x1b[1C")
|
|
144
|
-
const result = await waitForTuiReady(ring, { hardCapMs: 1000, pollMs: 10 })
|
|
145
|
-
expect(result).toBe("marker")
|
|
146
|
-
})
|
|
147
|
-
|
|
148
|
-
test("returns 'marker' when TUI renders ❯ followed by U+00A0 (non-breaking space)", async () => {
|
|
149
|
-
const ring = new OutputRing()
|
|
150
|
-
ring.append("❯ ")
|
|
151
|
-
const result = await waitForTuiReady(ring, { hardCapMs: 1000, pollMs: 10 })
|
|
152
|
-
expect(result).toBe("marker")
|
|
153
|
-
})
|
|
154
|
-
})
|
|
155
|
-
|
|
156
|
-
describe("dismissTrustDialogIfPresent (ANSI-encoded ring)", () => {
|
|
157
|
-
test("detects trust dialog when words separated by \\x1b[1C (TUI rendering)", async () => {
|
|
158
|
-
const pty = fakePty()
|
|
159
|
-
const ring = new OutputRing()
|
|
160
|
-
ring.append("\x1b[1C❯\x1b[1C1.\x1b[1CYes,\x1b[1CI\x1b[1Ctrust\x1b[1Cthis\x1b[1Cfolder\r\n")
|
|
161
|
-
const dismissed = await dismissTrustDialogIfPresent(pty, ring)
|
|
162
|
-
expect(dismissed).toBe(true)
|
|
163
|
-
expect(pty.sent).toEqual(["\r"])
|
|
164
|
-
})
|
|
165
|
-
|
|
166
|
-
test("does not false-trigger on plain text without trust marker", async () => {
|
|
167
|
-
const pty = fakePty()
|
|
168
|
-
const ring = new OutputRing()
|
|
169
|
-
ring.append("\x1b[1CWelcome\x1b[1Cback!\x1b[0m❯ ")
|
|
170
|
-
const dismissed = await dismissTrustDialogIfPresent(pty, ring)
|
|
171
|
-
expect(dismissed).toBe(false)
|
|
172
|
-
expect(pty.sent).toEqual([])
|
|
173
|
-
})
|
|
174
|
-
})
|
|
175
|
-
|
|
176
|
-
describe("waitForTuiReadyWithTrustDismiss", () => {
|
|
177
|
-
test("returns 'ready' immediately when input box already present", async () => {
|
|
178
|
-
const pty = fakePty()
|
|
179
|
-
const ring = new OutputRing()
|
|
180
|
-
ring.append("❯ ")
|
|
181
|
-
const result = await waitForTuiReadyWithTrustDismiss(pty, ring, { hardCapMs: 500, pollMs: 10 })
|
|
182
|
-
expect(result).toBe("ready")
|
|
183
|
-
expect(pty.sent).toEqual([])
|
|
184
|
-
})
|
|
185
|
-
|
|
186
|
-
test("dismisses ANSI trust dialog then resolves when input box appears", async () => {
|
|
187
|
-
const pty = fakePty()
|
|
188
|
-
const ring = new OutputRing()
|
|
189
|
-
ring.append("\x1b[1Ctrust\x1b[1Cthis\x1b[1Cfolder")
|
|
190
|
-
setTimeout(() => ring.append("❯ "), 80)
|
|
191
|
-
const result = await waitForTuiReadyWithTrustDismiss(pty, ring, { hardCapMs: 1000, pollMs: 10 })
|
|
192
|
-
expect(result).toBe("ready")
|
|
193
|
-
expect(pty.sent).toContain("\r")
|
|
194
|
-
})
|
|
195
|
-
|
|
196
|
-
test("returns 'timeout' when neither marker appears", async () => {
|
|
197
|
-
const pty = fakePty()
|
|
198
|
-
const ring = new OutputRing()
|
|
199
|
-
const result = await waitForTuiReadyWithTrustDismiss(pty, ring, { hardCapMs: 150, pollMs: 10 })
|
|
200
|
-
expect(result).toBe("timeout")
|
|
201
|
-
expect(pty.sent).toEqual([])
|
|
202
|
-
})
|
|
203
|
-
|
|
204
|
-
test("returns 'ready' when TUI renders ❯ with \\x1b[1C (ANSI cursor-forward)", async () => {
|
|
205
|
-
const pty = fakePty()
|
|
206
|
-
const ring = new OutputRing()
|
|
207
|
-
ring.append("❯\x1b[1C")
|
|
208
|
-
const result = await waitForTuiReadyWithTrustDismiss(pty, ring, { hardCapMs: 500, pollMs: 10 })
|
|
209
|
-
expect(result).toBe("ready")
|
|
210
|
-
expect(pty.sent).toEqual([])
|
|
211
|
-
})
|
|
212
|
-
|
|
213
|
-
test("does not false-trigger on trust dialog's own ❯ selection cursor", async () => {
|
|
214
|
-
const pty = fakePty()
|
|
215
|
-
const ring = new OutputRing()
|
|
216
|
-
ring.append("\x1b[1C❯\x1b[1C1.\x1b[1CYes,\x1b[1CI\x1b[1Ctrust\x1b[1Cthis\x1b[1Cfolder\r\n")
|
|
217
|
-
setTimeout(() => ring.append("❯\x1b[1C"), 80)
|
|
218
|
-
const result = await waitForTuiReadyWithTrustDismiss(pty, ring, { hardCapMs: 1000, pollMs: 10 })
|
|
219
|
-
expect(result).toBe("ready")
|
|
220
|
-
expect(pty.sent).toContain("\r")
|
|
221
|
-
})
|
|
222
|
-
|
|
223
|
-
describe("dismissDevChannelsDialogIfPresent", () => {
|
|
224
|
-
test("sends carriage return when ring contains the dev-channels marker", async () => {
|
|
225
|
-
const pty = fakePty()
|
|
226
|
-
const ring = new OutputRing()
|
|
227
|
-
ring.append("WARNING: Loading development channels ... for local channel development only")
|
|
228
|
-
const dismissed = await dismissDevChannelsDialogIfPresent(pty, ring)
|
|
229
|
-
expect(dismissed).toBe(true)
|
|
230
|
-
expect(pty.sent).toEqual(["\r"])
|
|
231
|
-
})
|
|
232
|
-
test("does nothing when the marker is absent", async () => {
|
|
233
|
-
const pty = fakePty()
|
|
234
|
-
const ring = new OutputRing()
|
|
235
|
-
ring.append("some unrelated output")
|
|
236
|
-
const dismissed = await dismissDevChannelsDialogIfPresent(pty, ring)
|
|
237
|
-
expect(dismissed).toBe(false)
|
|
238
|
-
expect(pty.sent).toEqual([])
|
|
239
|
-
})
|
|
240
|
-
test("DEV_CHANNELS_DIALOG_MARKER is the substring matched", () => {
|
|
241
|
-
expect("for local channel development only").toContain(DEV_CHANNELS_DIALOG_MARKER)
|
|
242
|
-
})
|
|
243
|
-
})
|
|
244
|
-
|
|
245
|
-
test("dismisses trust dialog only once even if marker persists in ring", async () => {
|
|
246
|
-
const pty = fakePty()
|
|
247
|
-
const ring = new OutputRing()
|
|
248
|
-
ring.append("\x1b[1Ctrust\x1b[1Cthis\x1b[1Cfolder")
|
|
249
|
-
setTimeout(() => ring.append("❯ "), 80)
|
|
250
|
-
await waitForTuiReadyWithTrustDismiss(pty, ring, { hardCapMs: 1000, pollMs: 10 })
|
|
251
|
-
expect(pty.sent.filter((s) => s === "\r")).toHaveLength(1)
|
|
252
|
-
})
|
|
253
|
-
|
|
254
|
-
test("waits for ring to stay quiet for quietPeriodMs after post-dismiss marker hit", async () => {
|
|
255
|
-
const pty = fakePty()
|
|
256
|
-
const ring = new OutputRing()
|
|
257
|
-
ring.append("\x1b[1Ctrust\x1b[1Cthis\x1b[1Cfolder")
|
|
258
|
-
setTimeout(() => ring.append("❯ "), 50)
|
|
259
|
-
const interval = setInterval(() => ring.append("."), 30)
|
|
260
|
-
setTimeout(() => clearInterval(interval), 250)
|
|
261
|
-
const start = Date.now()
|
|
262
|
-
const result = await waitForTuiReadyWithTrustDismiss(pty, ring, {
|
|
263
|
-
hardCapMs: 2000,
|
|
264
|
-
pollMs: 10,
|
|
265
|
-
quietPeriodMs: 150,
|
|
266
|
-
})
|
|
267
|
-
const elapsed = Date.now() - start
|
|
268
|
-
expect(result).toBe("ready")
|
|
269
|
-
expect(elapsed).toBeGreaterThanOrEqual(350)
|
|
270
|
-
})
|
|
271
|
-
})
|
|
272
|
-
|
|
273
|
-
describe("waitForTuiReadyDismissingDialogs", () => {
|
|
274
|
-
test("dismisses trust + dev dialogs, returns 'marker' once ❯ is clean", async () => {
|
|
275
|
-
const pty = fakePty()
|
|
276
|
-
const ring = new OutputRing()
|
|
277
|
-
ring.append("Is this a project you trust this folder?")
|
|
278
|
-
setTimeout(() => ring.append(" for local channel development only ❯ 1. I am using this for local development"), 5)
|
|
279
|
-
setTimeout(() => ring.append("\n❯ "), 25)
|
|
280
|
-
const res = await waitForTuiReadyDismissingDialogs(pty, ring, { hardCapMs: 1000, pollMs: 2 })
|
|
281
|
-
expect(res).toBe("marker")
|
|
282
|
-
expect(pty.sent.filter((s) => s === "\r").length).toBeGreaterThanOrEqual(2)
|
|
283
|
-
})
|
|
284
|
-
test("returns 'timeout' if the input box never becomes clean", async () => {
|
|
285
|
-
const pty = fakePty()
|
|
286
|
-
const ring = new OutputRing()
|
|
287
|
-
const res = await waitForTuiReadyDismissingDialogs(pty, ring, { hardCapMs: 60, pollMs: 5 })
|
|
288
|
-
expect(res).toBe("timeout")
|
|
289
|
-
})
|
|
290
|
-
test("does not return ready on the trust dialog's leftover ❯ before the dev dialog appears", async () => {
|
|
291
|
-
const pty = fakePty()
|
|
292
|
-
const ring = new OutputRing()
|
|
293
|
-
ring.append("Is this a project you trust this folder? ❯ 1. Yes, I trust this folder")
|
|
294
|
-
setTimeout(() => ring.append(" WARNING for local channel development only ❯ 1. I am using this for local development"), 15)
|
|
295
|
-
setTimeout(() => ring.append("\n❯ "), 40)
|
|
296
|
-
const res = await waitForTuiReadyDismissingDialogs(pty, ring, { hardCapMs: 2000, pollMs: 2 })
|
|
297
|
-
expect(res).toBe("marker")
|
|
298
|
-
expect(pty.sent.filter((s) => s === "\r").length).toBe(2)
|
|
299
|
-
})
|
|
300
|
-
})
|