@cuongtran001/kanna 1.2.0 → 1.3.1
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/client/assets/{arc-BLk1CZKB.js → arc-DXtN3EWJ.js} +1 -1
- package/dist/client/assets/{architectureDiagram-3BPJPVTR-B01ZQYte.js → architectureDiagram-3BPJPVTR-CIGqW1z-.js} +1 -1
- package/dist/client/assets/{blockDiagram-GPEHLZMM-diL6a3cq.js → blockDiagram-GPEHLZMM-BafNS_H6.js} +1 -1
- package/dist/client/assets/{c4Diagram-AAUBKEIU-BY1verL8.js → c4Diagram-AAUBKEIU-YdAgRSis.js} +1 -1
- package/dist/client/assets/channel-BEYTrUAm.js +1 -0
- package/dist/client/assets/{chunk-2J33WTMH-psdZuajw.js → chunk-2J33WTMH-eiP5y5G-.js} +1 -1
- package/dist/client/assets/{chunk-4BX2VUAB-CrwQVBc5.js → chunk-4BX2VUAB-CvLGzPMF.js} +1 -1
- package/dist/client/assets/{chunk-55IACEB6-9-aj4jKO.js → chunk-55IACEB6-Y-S9Fb8A.js} +1 -1
- package/dist/client/assets/{chunk-727SXJPM-1FNK9_nB.js → chunk-727SXJPM-6sJH1RI9.js} +1 -1
- package/dist/client/assets/{chunk-AQP2D5EJ-DFhvd_Ek.js → chunk-AQP2D5EJ-hV1r_IXO.js} +1 -1
- package/dist/client/assets/{chunk-FMBD7UC4-B7MLrDa7.js → chunk-FMBD7UC4-CC3L28S5.js} +1 -1
- package/dist/client/assets/{chunk-ND2GUHAM-D4Df05QE.js → chunk-ND2GUHAM-BZ3czhdH.js} +1 -1
- package/dist/client/assets/{chunk-QZHKN3VN-Iq17Dznm.js → chunk-QZHKN3VN-Bjyd6Yr-.js} +1 -1
- package/dist/client/assets/classDiagram-4FO5ZUOK-CvnF5Ud6.js +1 -0
- package/dist/client/assets/classDiagram-v2-Q7XG4LA2-CvnF5Ud6.js +1 -0
- package/dist/client/assets/{cose-bilkent-S5V4N54A-BmaZhtXy.js → cose-bilkent-S5V4N54A-CfxdDghE.js} +1 -1
- package/dist/client/assets/{dagre-BM42HDAG-BCmpfnfP.js → dagre-BM42HDAG-B8CddsbB.js} +1 -1
- package/dist/client/assets/{diagram-2AECGRRQ-BIEcKowS.js → diagram-2AECGRRQ-F0yS0fVB.js} +1 -1
- package/dist/client/assets/{diagram-5GNKFQAL-CRBABBfv.js → diagram-5GNKFQAL-cW0i0qlh.js} +1 -1
- package/dist/client/assets/{diagram-KO2AKTUF-BRxUk16N.js → diagram-KO2AKTUF-BzAKMeJT.js} +1 -1
- package/dist/client/assets/{diagram-LMA3HP47-DPBs4xl1.js → diagram-LMA3HP47-qdC4XqIl.js} +1 -1
- package/dist/client/assets/{diagram-OG6HWLK6-BE9rgVv8.js → diagram-OG6HWLK6-CGcpi6Dh.js} +1 -1
- package/dist/client/assets/{erDiagram-TEJ5UH35-D3vgA1xJ.js → erDiagram-TEJ5UH35-CxNTDicM.js} +1 -1
- package/dist/client/assets/{flowDiagram-I6XJVG4X-Cgu-8XXC.js → flowDiagram-I6XJVG4X-qkGa5TqY.js} +1 -1
- package/dist/client/assets/{ganttDiagram-6RSMTGT7-CgjXVyTR.js → ganttDiagram-6RSMTGT7-D9mVPoQi.js} +1 -1
- package/dist/client/assets/{gitGraphDiagram-PVQCEYII-zD5D8B-i.js → gitGraphDiagram-PVQCEYII-DA0grAXe.js} +1 -1
- package/dist/client/assets/{index-DBTdhMbJ.js → index-BqUY1Bm4.js} +1 -1
- package/dist/client/assets/index-CLsOWXff.css +32 -0
- package/dist/client/assets/{index-zQLLeAfl.js → index-TU1k6ov6.js} +253 -243
- package/dist/client/assets/{infoDiagram-5YYISTIA-BaFHqT4u.js → infoDiagram-5YYISTIA-CGljtLs3.js} +1 -1
- package/dist/client/assets/{ishikawaDiagram-YF4QCWOH-2yAC1br2.js → ishikawaDiagram-YF4QCWOH-CtC0xrmG.js} +1 -1
- package/dist/client/assets/{journeyDiagram-JHISSGLW-DacQ-ND5.js → journeyDiagram-JHISSGLW-URLnVZeN.js} +1 -1
- package/dist/client/assets/{kanban-definition-UN3LZRKU-Qlbwm3z4.js → kanban-definition-UN3LZRKU-C2Q6aqQh.js} +1 -1
- package/dist/client/assets/{linear-DeafqpgO.js → linear-ByKHtsIx.js} +1 -1
- package/dist/client/assets/{mermaid.core-KYiCPJDj.js → mermaid.core-Dl_f2KLc.js} +4 -4
- package/dist/client/assets/{mindmap-definition-RKZ34NQL-B1V-T6d3.js → mindmap-definition-RKZ34NQL-CaJ9IDLI.js} +1 -1
- package/dist/client/assets/{pieDiagram-4H26LBE5-DOF9OFRu.js → pieDiagram-4H26LBE5-mmYaf2Oa.js} +1 -1
- package/dist/client/assets/{quadrantDiagram-W4KKPZXB-lH1mB7ko.js → quadrantDiagram-W4KKPZXB-B_PO6OCC.js} +1 -1
- package/dist/client/assets/{requirementDiagram-4Y6WPE33-BaTsJ_sS.js → requirementDiagram-4Y6WPE33-M4huwJt1.js} +1 -1
- package/dist/client/assets/{sankeyDiagram-5OEKKPKP-Pfxib5iI.js → sankeyDiagram-5OEKKPKP-B0hb1sWh.js} +1 -1
- package/dist/client/assets/{sequenceDiagram-3UESZ5HK-BQMEKkBC.js → sequenceDiagram-3UESZ5HK-BAqSptnA.js} +1 -1
- package/dist/client/assets/{stateDiagram-AJRCARHV-CuldNnzp.js → stateDiagram-AJRCARHV-DTNkvPi5.js} +1 -1
- package/dist/client/assets/stateDiagram-v2-BHNVJYJU-CVN3oGTF.js +1 -0
- package/dist/client/assets/{timeline-definition-PNZ67QCA-DP7u0YAg.js → timeline-definition-PNZ67QCA-MgAjHz8Q.js} +1 -1
- package/dist/client/assets/{vennDiagram-CIIHVFJN-QlrjpNeQ.js → vennDiagram-CIIHVFJN-DmKoO51W.js} +1 -1
- package/dist/client/assets/{wardley-L42UT6IY-BqdJ_sAM.js → wardley-L42UT6IY-CWcyHnYs.js} +1 -1
- package/dist/client/assets/{wardleyDiagram-YWT4CUSO-Wi3kaQ5X.js → wardleyDiagram-YWT4CUSO-BGIGmFP9.js} +1 -1
- package/dist/client/assets/{xychartDiagram-2RQKCTM6-D-KXMyCG.js → xychartDiagram-2RQKCTM6-D20YIvEl.js} +1 -1
- package/dist/client/index.html +2 -2
- package/package.json +1 -1
- package/src/server/agent-coordinator-types.ts +158 -0
- package/src/server/agent-coordinator.ts +919 -0
- package/src/server/agent-deps-builders.ts +425 -0
- package/src/server/agent.oauth-rotation.test.ts +101 -0
- package/src/server/agent.test.ts +24 -8
- package/src/server/agent.ts +35 -4963
- package/src/server/auto-continue/read-model.test.ts +2 -2
- package/src/server/auto-continue/read-model.ts +4 -1
- package/src/server/claude-autocontinue-commands.test.ts +494 -0
- package/src/server/claude-autocontinue-commands.ts +256 -0
- package/src/server/claude-cancel-handler.test.ts +472 -0
- package/src/server/claude-cancel-handler.ts +241 -0
- package/src/server/claude-chat-management.test.ts +395 -0
- package/src/server/claude-chat-management.ts +251 -0
- package/src/server/claude-harness-stream.test.ts +273 -0
- package/src/server/claude-harness-stream.ts +158 -0
- package/src/server/claude-loop-orch-commands.test.ts +579 -0
- package/src/server/claude-loop-orch-commands.ts +466 -0
- package/src/server/claude-message-normalizer.test.ts +213 -0
- package/src/server/claude-message-normalizer.ts +436 -0
- package/src/server/claude-prompt-helpers.test.ts +248 -0
- package/src/server/claude-prompt-helpers.ts +139 -0
- package/src/server/claude-sdk-queue.test.ts +169 -0
- package/src/server/claude-sdk-queue.ts +82 -0
- package/src/server/claude-send-command.test.ts +414 -0
- package/src/server/claude-send-command.ts +420 -0
- package/src/server/claude-session-config-helpers.test.ts +279 -0
- package/src/server/claude-session-config-helpers.ts +151 -0
- package/src/server/claude-session-config.test.ts +250 -0
- package/src/server/claude-session-config.ts +159 -0
- package/src/server/claude-session-error-handler.test.ts +498 -0
- package/src/server/claude-session-error-handler.ts +361 -0
- package/src/server/claude-session-lifecycle.test.ts +669 -0
- package/src/server/claude-session-lifecycle.ts +283 -0
- package/src/server/claude-session-rebuild.test.ts +224 -0
- package/src/server/claude-session-rebuild.ts +101 -0
- package/src/server/claude-session-runner.test.ts +523 -0
- package/src/server/claude-session-runner.ts +521 -0
- package/src/server/claude-session-spawner.test.ts +401 -0
- package/src/server/claude-session-spawner.ts +401 -0
- package/src/server/claude-session-start.test.ts +205 -0
- package/src/server/claude-session-start.ts +277 -0
- package/src/server/claude-session-state-queries.test.ts +344 -0
- package/src/server/claude-session-state-queries.ts +160 -0
- package/src/server/claude-session-state.ts +104 -0
- package/src/server/claude-slash-commands.test.ts +277 -0
- package/src/server/claude-slash-commands.ts +234 -0
- package/src/server/claude-spawn-helpers.test.ts +221 -0
- package/src/server/claude-spawn-helpers.ts +179 -0
- package/src/server/claude-steer-log.test.ts +243 -0
- package/src/server/claude-steer-log.ts +58 -0
- package/src/server/claude-subagent-tool-response.test.ts +213 -0
- package/src/server/claude-subagent-tool-response.ts +165 -0
- package/src/server/claude-subagent-wiring.test.ts +263 -0
- package/src/server/claude-subagent-wiring.ts +324 -0
- package/src/server/claude-tool-respond.test.ts +321 -0
- package/src/server/claude-tool-respond.ts +142 -0
- package/src/server/claude-turn-runner.test.ts +310 -0
- package/src/server/claude-turn-runner.ts +228 -0
- package/src/server/claude-turn-starter.test.ts +266 -0
- package/src/server/claude-turn-starter.ts +566 -0
- package/src/server/claude-usage-math.test.ts +169 -0
- package/src/server/claude-usage-math.ts +132 -0
- package/src/server/diff-store-branch-ops.adapter.test.ts +332 -0
- package/src/server/diff-store-branch-ops.adapter.ts +602 -0
- package/src/server/diff-store-commit-ops.adapter.test.ts +358 -0
- package/src/server/diff-store-commit-ops.adapter.ts +258 -0
- package/src/server/diff-store-errors.ts +85 -0
- package/src/server/diff-store-file-ops.adapter.ts +212 -0
- package/src/server/diff-store-git-branch.adapter.ts +686 -0
- package/src/server/diff-store-io.adapter.ts +27 -0
- package/src/server/diff-store-parse.test.ts +212 -0
- package/src/server/diff-store-parse.ts +122 -0
- package/src/server/diff-store-state.ts +82 -0
- package/src/server/diff-store.ts +76 -1854
- package/src/server/event-store-apply.test.ts +280 -0
- package/src/server/event-store-apply.ts +132 -0
- package/src/server/event-store-chat-lifecycle.test.ts +408 -0
- package/src/server/event-store-chat-lifecycle.ts +441 -0
- package/src/server/event-store-entity-write.ts +356 -0
- package/src/server/event-store-helpers.test.ts +230 -0
- package/src/server/event-store-helpers.ts +235 -0
- package/src/server/event-store-init.ts +276 -0
- package/src/server/event-store-messages.adapter.test.ts +365 -0
- package/src/server/event-store-messages.adapter.ts +213 -0
- package/src/server/event-store-orch-subscription.ts +124 -0
- package/src/server/event-store-orch.test.ts +462 -0
- package/src/server/event-store-orch.ts +312 -0
- package/src/server/event-store-peripheral-events.adapter.test.ts +254 -0
- package/src/server/event-store-peripheral-events.adapter.ts +105 -0
- package/src/server/event-store-snapshot.test.ts +597 -0
- package/src/server/event-store-snapshot.ts +658 -0
- package/src/server/event-store-subagent.test.ts +408 -0
- package/src/server/event-store-subagent.ts +198 -0
- package/src/server/event-store-tool-requests.test.ts +210 -0
- package/src/server/event-store-tool-requests.ts +131 -0
- package/src/server/event-store-transcript-write.adapter.ts +285 -0
- package/src/server/event-store-write-ops.test.ts +554 -0
- package/src/server/event-store-write-ops.ts +529 -0
- package/src/server/event-store.test.ts +35 -0
- package/src/server/event-store.ts +251 -2284
- package/src/server/events.ts +3 -0
- package/src/server/harness-types.ts +15 -1
- package/src/server/oauth-errors.ts +19 -0
- package/src/server/read-models.ts +26 -1
- package/src/server/subagent-orchestrator.ts +3 -0
- package/src/server/ws-router-agent-ctrl.test.ts +187 -0
- package/src/server/ws-router-agent-ctrl.ts +137 -0
- package/src/server/ws-router-broadcast.ts +488 -0
- package/src/server/ws-router-chat.test.ts +461 -0
- package/src/server/ws-router-chat.ts +279 -0
- package/src/server/ws-router-defaults.test.ts +283 -0
- package/src/server/ws-router-defaults.ts +434 -0
- package/src/server/ws-router-diff.test.ts +167 -0
- package/src/server/ws-router-diff.ts +250 -0
- package/src/server/ws-router-envelope.ts +329 -0
- package/src/server/ws-router-misc.test.ts +422 -0
- package/src/server/ws-router-misc.ts +265 -0
- package/src/server/ws-router-orch.test.ts +190 -0
- package/src/server/ws-router-orch.ts +93 -0
- package/src/server/ws-router-project.test.ts +403 -0
- package/src/server/ws-router-project.ts +256 -0
- package/src/server/ws-router-push.test.ts +193 -0
- package/src/server/ws-router-push.ts +120 -0
- package/src/server/ws-router-settings.test.ts +173 -0
- package/src/server/ws-router-settings.ts +416 -0
- package/src/server/ws-router-skills.test.ts +194 -0
- package/src/server/ws-router-skills.ts +204 -0
- package/src/server/ws-router-utils.ts +224 -0
- package/src/server/ws-router.ts +339 -2254
- package/src/shared/app-settings-types.ts +365 -0
- package/src/shared/core-types.ts +27 -0
- package/src/shared/git-diff-types.ts +165 -0
- package/src/shared/loop-progress.test.ts +106 -0
- package/src/shared/loop-progress.ts +88 -0
- package/src/shared/mcp-types.ts +105 -0
- package/src/shared/provider-model-types.ts +437 -0
- package/src/shared/subagent-types.ts +198 -0
- package/src/shared/tool-call-types.ts +314 -0
- package/src/shared/transcript-types.ts +270 -0
- package/src/shared/types.ts +27 -1668
- package/dist/client/assets/channel-BMkhjiLI.js +0 -1
- package/dist/client/assets/classDiagram-4FO5ZUOK-C5Kg1XOD.js +0 -1
- package/dist/client/assets/classDiagram-v2-Q7XG4LA2-C5Kg1XOD.js +0 -1
- package/dist/client/assets/index-H6C-f2Vp.css +0 -32
- package/dist/client/assets/stateDiagram-v2-BHNVJYJU-Ca2U11af.js +0 -1
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the extracted spawnClaudeTurn function (claude-session-spawner.ts).
|
|
3
|
+
* Covers session creation, reuse, eviction, OAuth pool handling, and driver selection
|
|
4
|
+
* without touching AgentCoordinator internals.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// NOTE: do NOT mock.module("../shared/log") here — Bun's mock.module mutates
|
|
8
|
+
// the global registry for the whole test run, turning shared/log into noops
|
|
9
|
+
// for every later test file (analytics.test.ts asserts real log output).
|
|
10
|
+
|
|
11
|
+
import { describe, test, expect } from "bun:test"
|
|
12
|
+
import { spawnClaudeTurn, type SpawnClaudeTurnArgs, type SpawnClaudeTurnDeps } from "./claude-session-spawner"
|
|
13
|
+
import { OAuthPoolUnavailableError } from "./oauth-errors"
|
|
14
|
+
import { POLICY_DEFAULT } from "../shared/permission-policy"
|
|
15
|
+
import type { ClaudeSessionState, ActiveTurn } from "./claude-session-state"
|
|
16
|
+
import type { ClaudeSessionHandle } from "./harness-types"
|
|
17
|
+
import type { LlmProviderSnapshot } from "../shared/types"
|
|
18
|
+
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
// Helpers
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
function makeFakeHandle(overrides: Partial<ClaudeSessionHandle> = {}): ClaudeSessionHandle {
|
|
24
|
+
return {
|
|
25
|
+
provider: "claude",
|
|
26
|
+
stream: (async function* () {})(),
|
|
27
|
+
interrupt: async () => {},
|
|
28
|
+
close: () => {},
|
|
29
|
+
sendPrompt: async () => {},
|
|
30
|
+
setModel: async () => {},
|
|
31
|
+
setPermissionMode: async () => {},
|
|
32
|
+
getSupportedCommands: async () => [],
|
|
33
|
+
// getAccountInfo is optional — omit to keep the fake minimal
|
|
34
|
+
pushChannelPrompt: undefined,
|
|
35
|
+
...overrides,
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** A valid LlmProviderSnapshot for tests that exercise the OpenRouter path. */
|
|
40
|
+
function makeOpenRouterProvider(apiKey = "or-key"): LlmProviderSnapshot {
|
|
41
|
+
return {
|
|
42
|
+
provider: "openrouter",
|
|
43
|
+
apiKey,
|
|
44
|
+
model: "openai/gpt-4o",
|
|
45
|
+
baseUrl: "https://openrouter.ai/api/v1",
|
|
46
|
+
resolvedBaseUrl: "https://openrouter.ai/api/v1",
|
|
47
|
+
enabled: true,
|
|
48
|
+
warning: null,
|
|
49
|
+
filePathDisplay: "",
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function makeSession(overrides: Partial<ClaudeSessionState> = {}): ClaudeSessionState {
|
|
54
|
+
return {
|
|
55
|
+
id: "sess-1",
|
|
56
|
+
chatId: "chat-1",
|
|
57
|
+
session: makeFakeHandle(),
|
|
58
|
+
localPath: "/tmp/project",
|
|
59
|
+
additionalDirectories: [],
|
|
60
|
+
model: "claude-opus-4",
|
|
61
|
+
effort: undefined,
|
|
62
|
+
planMode: false,
|
|
63
|
+
sessionToken: null,
|
|
64
|
+
accountInfoLoaded: false,
|
|
65
|
+
nextPromptSeq: 0,
|
|
66
|
+
pendingPromptSeqs: [],
|
|
67
|
+
activeTokenId: null,
|
|
68
|
+
oauthKeyMasked: null,
|
|
69
|
+
oauthLabel: null,
|
|
70
|
+
openrouterKeyMasked: null,
|
|
71
|
+
openrouterModel: null,
|
|
72
|
+
lastUsedAt: 0,
|
|
73
|
+
backgroundTaskIds: new Set(),
|
|
74
|
+
backgroundTaskDeadlineAt: 0,
|
|
75
|
+
loopArmedAtSpawn: false,
|
|
76
|
+
cancelledResultPending: 0,
|
|
77
|
+
suppressSessionTokenPersist: false,
|
|
78
|
+
...overrides,
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function makeArgs(overrides: Partial<SpawnClaudeTurnArgs> = {}): SpawnClaudeTurnArgs {
|
|
83
|
+
return {
|
|
84
|
+
chatId: "chat-1",
|
|
85
|
+
projectId: "proj-1",
|
|
86
|
+
localPath: "/tmp/project",
|
|
87
|
+
model: "claude-opus-4",
|
|
88
|
+
planMode: false,
|
|
89
|
+
sessionToken: null,
|
|
90
|
+
forkSession: false,
|
|
91
|
+
onToolRequest: async () => null,
|
|
92
|
+
provider: "claude",
|
|
93
|
+
...overrides,
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function makeDeps(overrides: Partial<SpawnClaudeTurnDeps> = {}): SpawnClaudeTurnDeps {
|
|
98
|
+
const claudeSessions = new Map<string, ClaudeSessionState>()
|
|
99
|
+
const activeTurns = new Map<string, ActiveTurn>()
|
|
100
|
+
const mentionedSubagentIdsByChat = new Map<string, string[]>()
|
|
101
|
+
|
|
102
|
+
const fakeHandle = makeFakeHandle()
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
claudeSessions,
|
|
106
|
+
activeTurns,
|
|
107
|
+
mentionedSubagentIdsByChat,
|
|
108
|
+
oauthPool: null,
|
|
109
|
+
store: {
|
|
110
|
+
recordSessionCommandsLoaded: async () => {},
|
|
111
|
+
},
|
|
112
|
+
startClaudeSessionFn: async () => fakeHandle,
|
|
113
|
+
startClaudeSessionPTYFn: async () => fakeHandle,
|
|
114
|
+
subagentOrchestrator: {} as SpawnClaudeTurnDeps["subagentOrchestrator"],
|
|
115
|
+
toolCallback: null,
|
|
116
|
+
tunnelGateway: null,
|
|
117
|
+
claudePtyRegistry: null,
|
|
118
|
+
ptyInstanceRegistry: null,
|
|
119
|
+
workflowRegistry: null,
|
|
120
|
+
subagentTranscriptRegistry: null,
|
|
121
|
+
resolveClaudeDriverPreference: () => "sdk",
|
|
122
|
+
isLoopArmed: () => null,
|
|
123
|
+
closeClaudeSession: () => {},
|
|
124
|
+
enforceClaudeSessionBudget: () => {},
|
|
125
|
+
// readLlmProvider is only called in the OpenRouter path — default throws to
|
|
126
|
+
// surface accidental calls; override per-test when exercising that path.
|
|
127
|
+
readLlmProvider: async () => { throw new Error("readLlmProvider called unexpectedly") },
|
|
128
|
+
buildPoolUnavailableMessage: () => "pool unavailable",
|
|
129
|
+
listOpenRouterModelsFn: null,
|
|
130
|
+
getSubagents: () => [],
|
|
131
|
+
getAppSettingsSnapshot: () => ({}),
|
|
132
|
+
getEnabledCustomMcpServers: () => [],
|
|
133
|
+
buildOAuthBearers: async () => new Map(),
|
|
134
|
+
setupLoop: async () => ({ ok: false as const, errors: [] }),
|
|
135
|
+
stopLoop: async () => {},
|
|
136
|
+
runOrchestration: async () => ({ ok: true as const, runId: "run-1" }),
|
|
137
|
+
cancelOrchRun: async () => {},
|
|
138
|
+
getOrchRunDetail: () => null,
|
|
139
|
+
resolveChatPolicy: () => POLICY_DEFAULT,
|
|
140
|
+
runClaudeSession: () => {},
|
|
141
|
+
mergeLocalCatalog: (commands) => commands,
|
|
142
|
+
emitStateChange: () => {},
|
|
143
|
+
...overrides,
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ---------------------------------------------------------------------------
|
|
148
|
+
// Tests
|
|
149
|
+
// ---------------------------------------------------------------------------
|
|
150
|
+
|
|
151
|
+
describe("spawnClaudeTurn", () => {
|
|
152
|
+
describe("fresh session spawn", () => {
|
|
153
|
+
test("creates and registers a new ClaudeSessionState when no session exists", async () => {
|
|
154
|
+
const deps = makeDeps()
|
|
155
|
+
const args = makeArgs()
|
|
156
|
+
|
|
157
|
+
const turn = await spawnClaudeTurn(deps, args)
|
|
158
|
+
|
|
159
|
+
expect(deps.claudeSessions.size).toBe(1)
|
|
160
|
+
const session = deps.claudeSessions.get("chat-1")
|
|
161
|
+
expect(session).toBeDefined()
|
|
162
|
+
expect(session?.chatId).toBe("chat-1")
|
|
163
|
+
expect(session?.localPath).toBe("/tmp/project")
|
|
164
|
+
expect(session?.model).toBe("claude-opus-4")
|
|
165
|
+
expect(turn.provider).toBe("claude")
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
test("uses SDK driver when resolveClaudeDriverPreference returns sdk", async () => {
|
|
169
|
+
let sdkCalled = false
|
|
170
|
+
let ptyCalled = false
|
|
171
|
+
const deps = makeDeps({
|
|
172
|
+
resolveClaudeDriverPreference: () => "sdk",
|
|
173
|
+
startClaudeSessionFn: async () => { sdkCalled = true; return makeFakeHandle() },
|
|
174
|
+
startClaudeSessionPTYFn: async () => { ptyCalled = true; return makeFakeHandle() },
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
await spawnClaudeTurn(deps, makeArgs({ provider: "claude" }))
|
|
178
|
+
|
|
179
|
+
expect(sdkCalled).toBe(true)
|
|
180
|
+
expect(ptyCalled).toBe(false)
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
test("uses PTY driver when resolveClaudeDriverPreference returns pty", async () => {
|
|
184
|
+
let sdkCalled = false
|
|
185
|
+
let ptyCalled = false
|
|
186
|
+
const deps = makeDeps({
|
|
187
|
+
resolveClaudeDriverPreference: () => "pty",
|
|
188
|
+
startClaudeSessionFn: async () => { sdkCalled = true; return makeFakeHandle() },
|
|
189
|
+
startClaudeSessionPTYFn: async () => { ptyCalled = true; return makeFakeHandle() },
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
await spawnClaudeTurn(deps, makeArgs({ provider: "claude" }))
|
|
193
|
+
|
|
194
|
+
expect(sdkCalled).toBe(false)
|
|
195
|
+
expect(ptyCalled).toBe(true)
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
test("always uses SDK driver for openrouter provider even when pty is preferred", async () => {
|
|
199
|
+
let sdkCalled = false
|
|
200
|
+
let ptyCalled = false
|
|
201
|
+
const deps = makeDeps({
|
|
202
|
+
resolveClaudeDriverPreference: () => "pty",
|
|
203
|
+
startClaudeSessionFn: async () => { sdkCalled = true; return makeFakeHandle() },
|
|
204
|
+
startClaudeSessionPTYFn: async () => { ptyCalled = true; return makeFakeHandle() },
|
|
205
|
+
readLlmProvider: async () => makeOpenRouterProvider("or-key"),
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
await spawnClaudeTurn(deps, makeArgs({ provider: "openrouter" }))
|
|
209
|
+
|
|
210
|
+
expect(sdkCalled).toBe(true)
|
|
211
|
+
expect(ptyCalled).toBe(false)
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
test("fires runClaudeSession in fire-and-forget manner", async () => {
|
|
215
|
+
let sessionPassed: ClaudeSessionState | undefined
|
|
216
|
+
const deps = makeDeps({
|
|
217
|
+
runClaudeSession: (session) => { sessionPassed = session },
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
await spawnClaudeTurn(deps, makeArgs())
|
|
221
|
+
|
|
222
|
+
expect(sessionPassed).toBeDefined()
|
|
223
|
+
expect(sessionPassed?.chatId).toBe("chat-1")
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
test("calls enforceClaudeSessionBudget with chatId", async () => {
|
|
227
|
+
const calls: Array<string | undefined> = []
|
|
228
|
+
const deps = makeDeps({
|
|
229
|
+
enforceClaudeSessionBudget: (chatId) => { calls.push(chatId) },
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
await spawnClaudeTurn(deps, makeArgs())
|
|
233
|
+
|
|
234
|
+
expect(calls.length).toBeGreaterThanOrEqual(1)
|
|
235
|
+
expect(calls[0]).toBe("chat-1")
|
|
236
|
+
})
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
describe("session reuse", () => {
|
|
240
|
+
test("reuses existing session when localPath and effort are the same", async () => {
|
|
241
|
+
const deps = makeDeps()
|
|
242
|
+
const existingSession = makeSession({ lastUsedAt: 100 })
|
|
243
|
+
deps.claudeSessions.set("chat-1", existingSession)
|
|
244
|
+
|
|
245
|
+
let sdkCalled = false
|
|
246
|
+
deps.startClaudeSessionFn = async () => { sdkCalled = true; return makeFakeHandle() }
|
|
247
|
+
|
|
248
|
+
await spawnClaudeTurn(deps, makeArgs())
|
|
249
|
+
|
|
250
|
+
expect(sdkCalled).toBe(false)
|
|
251
|
+
expect(existingSession.lastUsedAt).toBeGreaterThan(100)
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
test("calls setModel on reuse when model changes", async () => {
|
|
255
|
+
let modelSet: string | undefined
|
|
256
|
+
const fakeHandle = makeFakeHandle({
|
|
257
|
+
setModel: async (model) => { modelSet = model },
|
|
258
|
+
})
|
|
259
|
+
const deps = makeDeps()
|
|
260
|
+
const existingSession = makeSession({ session: fakeHandle, model: "claude-opus-4" })
|
|
261
|
+
deps.claudeSessions.set("chat-1", existingSession)
|
|
262
|
+
|
|
263
|
+
await spawnClaudeTurn(deps, makeArgs({ model: "claude-sonnet-4" }))
|
|
264
|
+
|
|
265
|
+
expect(modelSet).toBe("claude-sonnet-4")
|
|
266
|
+
expect(existingSession.model).toBe("claude-sonnet-4")
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
test("calls setPermissionMode on reuse when planMode changes", async () => {
|
|
270
|
+
let planModeSet: boolean | undefined
|
|
271
|
+
const fakeHandle = makeFakeHandle({
|
|
272
|
+
setPermissionMode: async (pm) => { planModeSet = pm },
|
|
273
|
+
})
|
|
274
|
+
const deps = makeDeps()
|
|
275
|
+
const existingSession = makeSession({ session: fakeHandle, planMode: false })
|
|
276
|
+
deps.claudeSessions.set("chat-1", existingSession)
|
|
277
|
+
|
|
278
|
+
await spawnClaudeTurn(deps, makeArgs({ planMode: true }))
|
|
279
|
+
|
|
280
|
+
expect(planModeSet).toBe(true)
|
|
281
|
+
expect(existingSession.planMode).toBe(true)
|
|
282
|
+
})
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
describe("session eviction", () => {
|
|
286
|
+
test("evicts existing session and spawns fresh one when localPath changes", async () => {
|
|
287
|
+
let closedSession: ClaudeSessionState | undefined
|
|
288
|
+
const deps = makeDeps({
|
|
289
|
+
closeClaudeSession: (_, session) => { closedSession = session },
|
|
290
|
+
})
|
|
291
|
+
const existingSession = makeSession({ localPath: "/tmp/project" })
|
|
292
|
+
deps.claudeSessions.set("chat-1", existingSession)
|
|
293
|
+
|
|
294
|
+
await spawnClaudeTurn(deps, makeArgs({ localPath: "/tmp/other" }))
|
|
295
|
+
|
|
296
|
+
expect(closedSession).toBe(existingSession)
|
|
297
|
+
const newSession = deps.claudeSessions.get("chat-1")
|
|
298
|
+
expect(newSession?.localPath).toBe("/tmp/other")
|
|
299
|
+
})
|
|
300
|
+
|
|
301
|
+
test("evicts existing session when forkSession is true", async () => {
|
|
302
|
+
let closedSession: ClaudeSessionState | undefined
|
|
303
|
+
const deps = makeDeps({
|
|
304
|
+
closeClaudeSession: (_, session) => { closedSession = session },
|
|
305
|
+
})
|
|
306
|
+
const existingSession = makeSession()
|
|
307
|
+
deps.claudeSessions.set("chat-1", existingSession)
|
|
308
|
+
|
|
309
|
+
await spawnClaudeTurn(deps, makeArgs({ forkSession: true }))
|
|
310
|
+
|
|
311
|
+
expect(closedSession).toBe(existingSession)
|
|
312
|
+
})
|
|
313
|
+
|
|
314
|
+
test("evicts existing session when loop armed state flips from false to true", async () => {
|
|
315
|
+
let closedSession: ClaudeSessionState | undefined
|
|
316
|
+
const deps = makeDeps({
|
|
317
|
+
closeClaudeSession: (_, session) => { closedSession = session },
|
|
318
|
+
isLoopArmed: () => ({ subagentId: "sa-1", prompt: "loop", armedAt: 0, scheduleId: "s" }),
|
|
319
|
+
})
|
|
320
|
+
const existingSession = makeSession({ loopArmedAtSpawn: false })
|
|
321
|
+
deps.claudeSessions.set("chat-1", existingSession)
|
|
322
|
+
|
|
323
|
+
await spawnClaudeTurn(deps, makeArgs())
|
|
324
|
+
|
|
325
|
+
expect(closedSession).toBe(existingSession)
|
|
326
|
+
const newSession = deps.claudeSessions.get("chat-1")
|
|
327
|
+
expect(newSession?.loopArmedAtSpawn).toBe(true)
|
|
328
|
+
})
|
|
329
|
+
})
|
|
330
|
+
|
|
331
|
+
describe("OAuth pool handling", () => {
|
|
332
|
+
test("throws OAuthPoolUnavailableError when pool has tokens but none available", async () => {
|
|
333
|
+
const deps = makeDeps({
|
|
334
|
+
oauthPool: {
|
|
335
|
+
pickActive: () => null,
|
|
336
|
+
hasAnyToken: () => true,
|
|
337
|
+
markUsed: () => {},
|
|
338
|
+
release: () => {},
|
|
339
|
+
},
|
|
340
|
+
})
|
|
341
|
+
|
|
342
|
+
await expect(spawnClaudeTurn(deps, makeArgs({ provider: "claude" }))).rejects.toThrow(
|
|
343
|
+
OAuthPoolUnavailableError,
|
|
344
|
+
)
|
|
345
|
+
})
|
|
346
|
+
|
|
347
|
+
test("releases OAuth token when session spawn fails", async () => {
|
|
348
|
+
let released = false
|
|
349
|
+
const deps = makeDeps({
|
|
350
|
+
oauthPool: {
|
|
351
|
+
pickActive: () => ({ id: "tok-1", token: "tok-value", label: "test" }),
|
|
352
|
+
hasAnyToken: () => true,
|
|
353
|
+
markUsed: () => {},
|
|
354
|
+
release: () => { released = true },
|
|
355
|
+
},
|
|
356
|
+
startClaudeSessionFn: async () => { throw new Error("spawn failed") },
|
|
357
|
+
startClaudeSessionPTYFn: async () => { throw new Error("spawn failed") },
|
|
358
|
+
})
|
|
359
|
+
|
|
360
|
+
await expect(spawnClaudeTurn(deps, makeArgs())).rejects.toThrow("spawn failed")
|
|
361
|
+
expect(released).toBe(true)
|
|
362
|
+
})
|
|
363
|
+
|
|
364
|
+
test("marks picked token as used and stores masked key in new session", async () => {
|
|
365
|
+
let markedId: string | undefined
|
|
366
|
+
const deps = makeDeps({
|
|
367
|
+
oauthPool: {
|
|
368
|
+
pickActive: () => ({ id: "tok-abc", token: "sk-ant-abcdef1234", label: "my-token" }),
|
|
369
|
+
hasAnyToken: () => true,
|
|
370
|
+
markUsed: (id) => { markedId = id },
|
|
371
|
+
release: () => {},
|
|
372
|
+
},
|
|
373
|
+
})
|
|
374
|
+
|
|
375
|
+
await spawnClaudeTurn(deps, makeArgs({ provider: "claude" }))
|
|
376
|
+
|
|
377
|
+
expect(markedId).toBe("tok-abc")
|
|
378
|
+
const session = deps.claudeSessions.get("chat-1")
|
|
379
|
+
expect(session?.activeTokenId).toBe("tok-abc")
|
|
380
|
+
expect(session?.oauthLabel).toBe("my-token")
|
|
381
|
+
})
|
|
382
|
+
})
|
|
383
|
+
|
|
384
|
+
describe("return value", () => {
|
|
385
|
+
test("returned HarnessTurn has provider=claude", async () => {
|
|
386
|
+
const deps = makeDeps()
|
|
387
|
+
const turn = await spawnClaudeTurn(deps, makeArgs())
|
|
388
|
+
expect(turn.provider).toBe("claude")
|
|
389
|
+
})
|
|
390
|
+
|
|
391
|
+
test("returned HarnessTurn has an empty async-iterable stream", async () => {
|
|
392
|
+
const deps = makeDeps()
|
|
393
|
+
const turn = await spawnClaudeTurn(deps, makeArgs())
|
|
394
|
+
const events: unknown[] = []
|
|
395
|
+
for await (const e of turn.stream) {
|
|
396
|
+
events.push(e)
|
|
397
|
+
}
|
|
398
|
+
expect(events).toHaveLength(0)
|
|
399
|
+
})
|
|
400
|
+
})
|
|
401
|
+
})
|