@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,190 @@
|
|
|
1
|
+
import { describe, expect, mock, test } from "bun:test"
|
|
2
|
+
import type { OrchCommandDeps, OrchAgentDep } from "./ws-router-orch"
|
|
3
|
+
import { handleOrchCommand } from "./ws-router-orch"
|
|
4
|
+
import type { ClientCommand } from "../shared/protocol"
|
|
5
|
+
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
// Helpers
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
|
|
10
|
+
function makeAgent(overrides: Partial<OrchAgentDep> = {}): OrchAgentDep {
|
|
11
|
+
return {
|
|
12
|
+
runOrchestration: mock(async () => ({ ok: true as const, runId: "run-1" })),
|
|
13
|
+
cancelOrchRun: mock(async () => {}),
|
|
14
|
+
getOrchRunDetail: mock(() => null),
|
|
15
|
+
...overrides,
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function makeDeps(
|
|
20
|
+
agentOverrides?: Partial<OrchAgentDep>,
|
|
21
|
+
wfOverride?: OrchCommandDeps["workflowRegistry"],
|
|
22
|
+
saOverride?: OrchCommandDeps["subagentTranscriptRegistry"],
|
|
23
|
+
): OrchCommandDeps & { sent: unknown[] } {
|
|
24
|
+
const sent: unknown[] = []
|
|
25
|
+
return {
|
|
26
|
+
agent: makeAgent(agentOverrides),
|
|
27
|
+
workflowRegistry: wfOverride,
|
|
28
|
+
subagentTranscriptRegistry: saOverride,
|
|
29
|
+
send: (envelope) => { sent.push(envelope) },
|
|
30
|
+
sent,
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
// Unrecognized command
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
|
|
38
|
+
describe("handleOrchCommand", () => {
|
|
39
|
+
test("returns false for a non-orch command", async () => {
|
|
40
|
+
const deps = makeDeps()
|
|
41
|
+
const handled = await handleOrchCommand(
|
|
42
|
+
deps,
|
|
43
|
+
{ type: "settings.readAppSettings" } as unknown as ClientCommand,
|
|
44
|
+
"r0",
|
|
45
|
+
)
|
|
46
|
+
expect(handled).toBe(false)
|
|
47
|
+
expect(deps.sent).toHaveLength(0)
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
// workflows.getRun
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
|
|
54
|
+
test("workflows.getRun — returns run from registry", async () => {
|
|
55
|
+
const run = { runId: "wf-1", taskId: "t-1", workflowName: "test" } as unknown as ReturnType<
|
|
56
|
+
NonNullable<OrchCommandDeps["workflowRegistry"]>["getRun"]
|
|
57
|
+
>
|
|
58
|
+
const wf: NonNullable<OrchCommandDeps["workflowRegistry"]> = {
|
|
59
|
+
getRun: mock(() => run),
|
|
60
|
+
getAgentTranscript: mock(() => []),
|
|
61
|
+
}
|
|
62
|
+
const deps = makeDeps(undefined, wf)
|
|
63
|
+
const handled = await handleOrchCommand(
|
|
64
|
+
deps,
|
|
65
|
+
{ type: "workflows.getRun", chatId: "c-1", runId: "wf-1" },
|
|
66
|
+
"r1",
|
|
67
|
+
)
|
|
68
|
+
expect(handled).toBe(true)
|
|
69
|
+
expect(wf.getRun).toHaveBeenCalledWith("c-1", "wf-1")
|
|
70
|
+
expect(deps.sent).toHaveLength(1)
|
|
71
|
+
expect((deps.sent[0] as { result: unknown }).result).toBe(run)
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
test("workflows.getRun — returns null when registry absent", async () => {
|
|
75
|
+
const deps = makeDeps(undefined, undefined)
|
|
76
|
+
const handled = await handleOrchCommand(
|
|
77
|
+
deps,
|
|
78
|
+
{ type: "workflows.getRun", chatId: "c-1", runId: "wf-1" },
|
|
79
|
+
"r2",
|
|
80
|
+
)
|
|
81
|
+
expect(handled).toBe(true)
|
|
82
|
+
expect((deps.sent[0] as { result: unknown }).result).toBeNull()
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
// ---------------------------------------------------------------------------
|
|
86
|
+
// workflows.getAgentTranscript
|
|
87
|
+
// ---------------------------------------------------------------------------
|
|
88
|
+
|
|
89
|
+
test("workflows.getAgentTranscript — returns entries", async () => {
|
|
90
|
+
const entries = [{ type: "assistant", content: "hi" }] as unknown as ReturnType<
|
|
91
|
+
NonNullable<OrchCommandDeps["workflowRegistry"]>["getAgentTranscript"]
|
|
92
|
+
>
|
|
93
|
+
const wf: NonNullable<OrchCommandDeps["workflowRegistry"]> = {
|
|
94
|
+
getRun: mock(() => null),
|
|
95
|
+
getAgentTranscript: mock(() => entries),
|
|
96
|
+
}
|
|
97
|
+
const deps = makeDeps(undefined, wf)
|
|
98
|
+
const handled = await handleOrchCommand(
|
|
99
|
+
deps,
|
|
100
|
+
{ type: "workflows.getAgentTranscript", chatId: "c-1", runId: "wf-1", agentId: "ag-1" },
|
|
101
|
+
"r3",
|
|
102
|
+
)
|
|
103
|
+
expect(handled).toBe(true)
|
|
104
|
+
expect(wf.getAgentTranscript).toHaveBeenCalledWith("c-1", "wf-1", "ag-1")
|
|
105
|
+
expect((deps.sent[0] as { result: unknown }).result).toBe(entries)
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
// ---------------------------------------------------------------------------
|
|
109
|
+
// subagents.getRun
|
|
110
|
+
// ---------------------------------------------------------------------------
|
|
111
|
+
|
|
112
|
+
test("subagents.getRun — returns entries from transcript registry", async () => {
|
|
113
|
+
const entries = [{ type: "assistant", content: "hello" }] as unknown as ReturnType<
|
|
114
|
+
NonNullable<OrchCommandDeps["subagentTranscriptRegistry"]>["getAgentTranscript"]
|
|
115
|
+
>
|
|
116
|
+
const sa: NonNullable<OrchCommandDeps["subagentTranscriptRegistry"]> = {
|
|
117
|
+
getAgentTranscript: mock(() => entries),
|
|
118
|
+
}
|
|
119
|
+
const deps = makeDeps(undefined, undefined, sa)
|
|
120
|
+
const handled = await handleOrchCommand(
|
|
121
|
+
deps,
|
|
122
|
+
{ type: "subagents.getRun", chatId: "c-1", agentId: "ag-2" },
|
|
123
|
+
"r4",
|
|
124
|
+
)
|
|
125
|
+
expect(handled).toBe(true)
|
|
126
|
+
expect(sa.getAgentTranscript).toHaveBeenCalledWith("c-1", "ag-2")
|
|
127
|
+
expect((deps.sent[0] as { result: unknown }).result).toBe(entries)
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
test("subagents.getRun — returns empty array when registry absent", async () => {
|
|
131
|
+
const deps = makeDeps(undefined, undefined, undefined)
|
|
132
|
+
const handled = await handleOrchCommand(
|
|
133
|
+
deps,
|
|
134
|
+
{ type: "subagents.getRun", chatId: "c-1", agentId: "ag-2" },
|
|
135
|
+
"r5",
|
|
136
|
+
)
|
|
137
|
+
expect(handled).toBe(true)
|
|
138
|
+
expect((deps.sent[0] as { result: unknown }).result).toEqual([])
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
// ---------------------------------------------------------------------------
|
|
142
|
+
// orch.run
|
|
143
|
+
// ---------------------------------------------------------------------------
|
|
144
|
+
|
|
145
|
+
test("orch.run — delegates to agent.runOrchestration and acks with result", async () => {
|
|
146
|
+
const orchResult = { ok: true as const, runId: "run-42" }
|
|
147
|
+
const deps = makeDeps({ runOrchestration: mock(async () => orchResult) })
|
|
148
|
+
const handled = await handleOrchCommand(
|
|
149
|
+
deps,
|
|
150
|
+
{ type: "orch.run", chatId: "c-1", input: { tasks: ["task A"] } },
|
|
151
|
+
"r6",
|
|
152
|
+
)
|
|
153
|
+
expect(handled).toBe(true)
|
|
154
|
+
expect(deps.agent.runOrchestration).toHaveBeenCalledWith("c-1", { tasks: ["task A"] })
|
|
155
|
+
expect((deps.sent[0] as { result: unknown }).result).toEqual(orchResult)
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
// ---------------------------------------------------------------------------
|
|
159
|
+
// orch.cancelRun
|
|
160
|
+
// ---------------------------------------------------------------------------
|
|
161
|
+
|
|
162
|
+
test("orch.cancelRun — delegates to agent.cancelOrchRun and acks ok", async () => {
|
|
163
|
+
const deps = makeDeps()
|
|
164
|
+
const handled = await handleOrchCommand(
|
|
165
|
+
deps,
|
|
166
|
+
{ type: "orch.cancelRun", runId: "run-42" },
|
|
167
|
+
"r7",
|
|
168
|
+
)
|
|
169
|
+
expect(handled).toBe(true)
|
|
170
|
+
expect(deps.agent.cancelOrchRun).toHaveBeenCalledWith("run-42")
|
|
171
|
+
expect((deps.sent[0] as { result: unknown }).result).toEqual({ ok: true })
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
// ---------------------------------------------------------------------------
|
|
175
|
+
// orch.getRun
|
|
176
|
+
// ---------------------------------------------------------------------------
|
|
177
|
+
|
|
178
|
+
test("orch.getRun — delegates to agent.getOrchRunDetail and acks with detail", async () => {
|
|
179
|
+
const detail = { runId: "run-42", status: "running" } as unknown as ReturnType<OrchAgentDep["getOrchRunDetail"]>
|
|
180
|
+
const deps = makeDeps({ getOrchRunDetail: mock(() => detail) })
|
|
181
|
+
const handled = await handleOrchCommand(
|
|
182
|
+
deps,
|
|
183
|
+
{ type: "orch.getRun", runId: "run-42" },
|
|
184
|
+
"r8",
|
|
185
|
+
)
|
|
186
|
+
expect(handled).toBe(true)
|
|
187
|
+
expect(deps.agent.getOrchRunDetail).toHaveBeenCalledWith("run-42")
|
|
188
|
+
expect((deps.sent[0] as { result: unknown }).result).toBe(detail)
|
|
189
|
+
})
|
|
190
|
+
})
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ws-router-orch.ts
|
|
3
|
+
*
|
|
4
|
+
* WS command handlers for orchestration, workflow observability, and
|
|
5
|
+
* subagent-transcript queries:
|
|
6
|
+
* orch.run, orch.cancelRun, orch.getRun,
|
|
7
|
+
* workflows.getRun, workflows.getAgentTranscript,
|
|
8
|
+
* subagents.getRun
|
|
9
|
+
*
|
|
10
|
+
* Extracted from ws-router.ts.
|
|
11
|
+
*/
|
|
12
|
+
import { PROTOCOL_VERSION } from "../shared/types"
|
|
13
|
+
import type { ClientCommand, ServerEnvelope } from "../shared/protocol"
|
|
14
|
+
import type { OrchRunInput, OrchRunDetail } from "../shared/orchestration-types"
|
|
15
|
+
import type { WorkflowRegistry } from "./workflow-registry"
|
|
16
|
+
import type { SubagentTranscriptRegistry } from "./subagent-transcript-registry"
|
|
17
|
+
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
// Dep interface (duck-typed; avoids circular imports with ws-router.ts)
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
|
|
22
|
+
/** The subset of AgentCoordinator methods consumed by orch/workflow WS commands. */
|
|
23
|
+
export interface OrchAgentDep {
|
|
24
|
+
runOrchestration(
|
|
25
|
+
chatId: string,
|
|
26
|
+
input: OrchRunInput,
|
|
27
|
+
): Promise<{ ok: true; runId: string } | { ok: false; errors: string[] }>
|
|
28
|
+
cancelOrchRun(runId: string): Promise<void>
|
|
29
|
+
getOrchRunDetail(runId: string): OrchRunDetail | null
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface OrchCommandDeps {
|
|
33
|
+
/** Orchestration methods from AgentCoordinator. */
|
|
34
|
+
agent: OrchAgentDep
|
|
35
|
+
/** Optional workflow registry (may be absent if not configured). */
|
|
36
|
+
workflowRegistry: Pick<WorkflowRegistry, "getRun" | "getAgentTranscript"> | undefined
|
|
37
|
+
/** Optional subagent transcript registry. */
|
|
38
|
+
subagentTranscriptRegistry: Pick<SubagentTranscriptRegistry, "getAgentTranscript"> | undefined
|
|
39
|
+
/** Pre-bound to the current WebSocket; called to send an ack envelope. */
|
|
40
|
+
send: (envelope: ServerEnvelope) => void
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
// Command dispatcher
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Handle one orchestration/workflow/subagent-transcript WS command.
|
|
49
|
+
*
|
|
50
|
+
* Returns `true` when the command was handled (caller should `return`).
|
|
51
|
+
* Returns `false` when the command type is outside this module's scope.
|
|
52
|
+
*/
|
|
53
|
+
export async function handleOrchCommand(
|
|
54
|
+
deps: OrchCommandDeps,
|
|
55
|
+
command: ClientCommand,
|
|
56
|
+
id: string,
|
|
57
|
+
): Promise<boolean> {
|
|
58
|
+
const { agent, workflowRegistry, subagentTranscriptRegistry, send } = deps
|
|
59
|
+
|
|
60
|
+
switch (command.type) {
|
|
61
|
+
case "workflows.getRun": {
|
|
62
|
+
const run = workflowRegistry?.getRun(command.chatId, command.runId) ?? null
|
|
63
|
+
send({ v: PROTOCOL_VERSION, type: "ack", id, result: run })
|
|
64
|
+
return true
|
|
65
|
+
}
|
|
66
|
+
case "workflows.getAgentTranscript": {
|
|
67
|
+
const entries = workflowRegistry?.getAgentTranscript(command.chatId, command.runId, command.agentId) ?? []
|
|
68
|
+
send({ v: PROTOCOL_VERSION, type: "ack", id, result: entries })
|
|
69
|
+
return true
|
|
70
|
+
}
|
|
71
|
+
case "subagents.getRun": {
|
|
72
|
+
const entries = subagentTranscriptRegistry?.getAgentTranscript(command.chatId, command.agentId) ?? []
|
|
73
|
+
send({ v: PROTOCOL_VERSION, type: "ack", id, result: entries })
|
|
74
|
+
return true
|
|
75
|
+
}
|
|
76
|
+
case "orch.run": {
|
|
77
|
+
const result = await agent.runOrchestration(command.chatId, command.input)
|
|
78
|
+
send({ v: PROTOCOL_VERSION, type: "ack", id, result })
|
|
79
|
+
return true
|
|
80
|
+
}
|
|
81
|
+
case "orch.cancelRun": {
|
|
82
|
+
await agent.cancelOrchRun(command.runId)
|
|
83
|
+
send({ v: PROTOCOL_VERSION, type: "ack", id, result: { ok: true } })
|
|
84
|
+
return true
|
|
85
|
+
}
|
|
86
|
+
case "orch.getRun": {
|
|
87
|
+
send({ v: PROTOCOL_VERSION, type: "ack", id, result: agent.getOrchRunDetail(command.runId) })
|
|
88
|
+
return true
|
|
89
|
+
}
|
|
90
|
+
default:
|
|
91
|
+
return false
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ws-router-project.test.ts
|
|
3
|
+
*
|
|
4
|
+
* Unit tests for the project / session / sidebar / system / update WS command handlers.
|
|
5
|
+
*/
|
|
6
|
+
import { describe, expect, mock, test } from "bun:test"
|
|
7
|
+
import type {
|
|
8
|
+
ProjectAnalyticsDep,
|
|
9
|
+
ProjectCommandDeps,
|
|
10
|
+
ProjectDiffStoreDep,
|
|
11
|
+
ProjectStoreDep,
|
|
12
|
+
ProjectTerminalsDep,
|
|
13
|
+
ProjectUpdateManagerDep,
|
|
14
|
+
} from "./ws-router-project"
|
|
15
|
+
import { handleProjectCommand } from "./ws-router-project"
|
|
16
|
+
import type { ClientCommand, ServerEnvelope } from "../shared/protocol"
|
|
17
|
+
import type { UpdateInstallResult, UpdateSnapshot } from "../shared/types"
|
|
18
|
+
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
// Helpers
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
function makeStore(overrides: Partial<ProjectStoreDep> = {}): ProjectStoreDep {
|
|
24
|
+
return {
|
|
25
|
+
state: { projectIdsByPath: new Map() },
|
|
26
|
+
openProject: mock(async () => ({ id: "proj-1" })),
|
|
27
|
+
removeProject: mock(async () => {}),
|
|
28
|
+
getProject: mock(() => ({ id: "proj-1", localPath: "/tmp/proj" })),
|
|
29
|
+
setProjectStar: mock(async () => {}),
|
|
30
|
+
setSidebarProjectOrder: mock(async () => {}),
|
|
31
|
+
...overrides,
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function makeUpdateManager(overrides: Partial<ProjectUpdateManagerDep> = {}): ProjectUpdateManagerDep {
|
|
36
|
+
const base: UpdateSnapshot = {
|
|
37
|
+
currentVersion: "1.0.0",
|
|
38
|
+
latestVersion: "1.1.0",
|
|
39
|
+
status: "idle",
|
|
40
|
+
updateAvailable: true,
|
|
41
|
+
lastCheckedAt: null,
|
|
42
|
+
error: null,
|
|
43
|
+
installAction: "restart",
|
|
44
|
+
reloadRequestedAt: null,
|
|
45
|
+
}
|
|
46
|
+
const installResult: UpdateInstallResult = {
|
|
47
|
+
ok: true,
|
|
48
|
+
action: "restart",
|
|
49
|
+
errorCode: null,
|
|
50
|
+
userTitle: null,
|
|
51
|
+
userMessage: null,
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
checkForUpdates: mock(async () => base),
|
|
55
|
+
installUpdate: mock(async () => installResult),
|
|
56
|
+
forceReload: mock(async () => installResult),
|
|
57
|
+
...overrides,
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function makeAnalytics(): ProjectAnalyticsDep & { events: string[] } {
|
|
62
|
+
const events: string[] = []
|
|
63
|
+
return {
|
|
64
|
+
events,
|
|
65
|
+
track: (event: string) => { events.push(event) },
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function makeDiffStore(): ProjectDiffStoreDep {
|
|
70
|
+
return {
|
|
71
|
+
readPatch: mock(async () => ({ patch: "diff content" })),
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function makeTerminals(): ProjectTerminalsDep {
|
|
76
|
+
return { closeByCwd: mock(() => {}) }
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
interface TestDeps extends ProjectCommandDeps {
|
|
80
|
+
sent: ServerEnvelope[]
|
|
81
|
+
sidebarBroadcasts: number
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function makeDeps(
|
|
85
|
+
options: {
|
|
86
|
+
storeOverrides?: Partial<ProjectStoreDep>
|
|
87
|
+
updateManager?: ProjectUpdateManagerDep | null
|
|
88
|
+
importResult?: Partial<{ imported: number; updated: number; skipped: number; failed: number; newProjects: number }>
|
|
89
|
+
openExternalFn?: () => Promise<void>
|
|
90
|
+
} = {},
|
|
91
|
+
): TestDeps {
|
|
92
|
+
const sent: ServerEnvelope[] = []
|
|
93
|
+
let sidebarBroadcasts = 0
|
|
94
|
+
const analytics = makeAnalytics()
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
store: makeStore(options.storeOverrides),
|
|
98
|
+
updateManager: options.updateManager !== undefined ? options.updateManager : makeUpdateManager(),
|
|
99
|
+
diffStore: makeDiffStore(),
|
|
100
|
+
analytics,
|
|
101
|
+
terminals: makeTerminals(),
|
|
102
|
+
refreshDiscovery: mock(async () => []),
|
|
103
|
+
ensureProjectDirectory: mock(async () => {}),
|
|
104
|
+
resolveLocalPath: (p) => p,
|
|
105
|
+
importClaudeSessionsFn: mock(async () => ({
|
|
106
|
+
imported: 1,
|
|
107
|
+
updated: 0,
|
|
108
|
+
skipped: 0,
|
|
109
|
+
failed: 0,
|
|
110
|
+
newProjects: options.importResult?.newProjects ?? 0,
|
|
111
|
+
...options.importResult,
|
|
112
|
+
})),
|
|
113
|
+
openExternalFn: options.openExternalFn ?? mock(async () => {}),
|
|
114
|
+
send: (envelope) => { sent.push(envelope) },
|
|
115
|
+
broadcastSidebar: mock(async () => { sidebarBroadcasts++ }),
|
|
116
|
+
sent,
|
|
117
|
+
get sidebarBroadcasts() { return sidebarBroadcasts },
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ---------------------------------------------------------------------------
|
|
122
|
+
// Tests
|
|
123
|
+
// ---------------------------------------------------------------------------
|
|
124
|
+
|
|
125
|
+
describe("handleProjectCommand", () => {
|
|
126
|
+
// -------------------------------------------------------------------------
|
|
127
|
+
// Unknown / out-of-scope
|
|
128
|
+
// -------------------------------------------------------------------------
|
|
129
|
+
|
|
130
|
+
test("returns false for a non-project command", async () => {
|
|
131
|
+
const deps = makeDeps()
|
|
132
|
+
const handled = await handleProjectCommand(
|
|
133
|
+
deps,
|
|
134
|
+
{ type: "chat.create" } as unknown as ClientCommand,
|
|
135
|
+
"r0",
|
|
136
|
+
)
|
|
137
|
+
expect(handled).toBe(false)
|
|
138
|
+
expect(deps.sent).toHaveLength(0)
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
// -------------------------------------------------------------------------
|
|
142
|
+
// system.ping
|
|
143
|
+
// -------------------------------------------------------------------------
|
|
144
|
+
|
|
145
|
+
test("system.ping — sends ack, returns true, no broadcast", async () => {
|
|
146
|
+
const deps = makeDeps()
|
|
147
|
+
const handled = await handleProjectCommand(deps, { type: "system.ping" }, "r1")
|
|
148
|
+
expect(handled).toBe(true)
|
|
149
|
+
expect(deps.sent).toHaveLength(1)
|
|
150
|
+
expect(deps.sent[0]).toMatchObject({ type: "ack", id: "r1" })
|
|
151
|
+
expect(deps.sidebarBroadcasts).toBe(0)
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
// -------------------------------------------------------------------------
|
|
155
|
+
// update.check
|
|
156
|
+
// -------------------------------------------------------------------------
|
|
157
|
+
|
|
158
|
+
test("update.check — no updateManager — returns fallback snapshot", async () => {
|
|
159
|
+
const deps = makeDeps({ updateManager: null })
|
|
160
|
+
const handled = await handleProjectCommand(
|
|
161
|
+
deps,
|
|
162
|
+
{ type: "update.check", force: false },
|
|
163
|
+
"r2",
|
|
164
|
+
)
|
|
165
|
+
expect(handled).toBe(true)
|
|
166
|
+
expect(deps.sent).toHaveLength(1)
|
|
167
|
+
const ack = deps.sent[0] as { type: string; result: { status: string; error: string } }
|
|
168
|
+
expect(ack.result.status).toBe("error")
|
|
169
|
+
expect(ack.result.error).toContain("unavailable")
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
test("update.check — with updateManager — calls checkForUpdates", async () => {
|
|
173
|
+
const um = makeUpdateManager()
|
|
174
|
+
const deps = makeDeps({ updateManager: um })
|
|
175
|
+
const handled = await handleProjectCommand(
|
|
176
|
+
deps,
|
|
177
|
+
{ type: "update.check", force: true },
|
|
178
|
+
"r3",
|
|
179
|
+
)
|
|
180
|
+
expect(handled).toBe(true)
|
|
181
|
+
expect(um.checkForUpdates as ReturnType<typeof mock>).toHaveBeenCalledWith({ force: true })
|
|
182
|
+
const ack = deps.sent[0] as { type: string; result: { currentVersion: string } }
|
|
183
|
+
expect(ack.result.currentVersion).toBe("1.0.0")
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
test("update.install — no updateManager — throws", async () => {
|
|
187
|
+
const deps = makeDeps({ updateManager: null })
|
|
188
|
+
await expect(
|
|
189
|
+
handleProjectCommand(deps, { type: "update.install", version: "1.1.0" }, "r4"),
|
|
190
|
+
).rejects.toThrow("unavailable")
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
test("update.reload — calls forceReload, acks", async () => {
|
|
194
|
+
const um = makeUpdateManager()
|
|
195
|
+
const deps = makeDeps({ updateManager: um })
|
|
196
|
+
const handled = await handleProjectCommand(deps, { type: "update.reload" }, "r5")
|
|
197
|
+
expect(handled).toBe(true)
|
|
198
|
+
expect(um.forceReload as ReturnType<typeof mock>).toHaveBeenCalled()
|
|
199
|
+
expect(deps.sent[0]).toMatchObject({ type: "ack", id: "r5" })
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
// -------------------------------------------------------------------------
|
|
203
|
+
// project.open
|
|
204
|
+
// -------------------------------------------------------------------------
|
|
205
|
+
|
|
206
|
+
test("project.open — new project — tracks analytics, acks with projectId", async () => {
|
|
207
|
+
const deps = makeDeps({
|
|
208
|
+
storeOverrides: {
|
|
209
|
+
state: { projectIdsByPath: new Map() }, // path not in map → new
|
|
210
|
+
openProject: mock(async () => ({ id: "new-proj" })),
|
|
211
|
+
},
|
|
212
|
+
})
|
|
213
|
+
const handled = await handleProjectCommand(
|
|
214
|
+
deps,
|
|
215
|
+
{ type: "project.open", localPath: "/tmp/newproj" },
|
|
216
|
+
"r6",
|
|
217
|
+
)
|
|
218
|
+
expect(handled).toBe(true)
|
|
219
|
+
const ack = deps.sent[0] as { type: string; result: { projectId: string } }
|
|
220
|
+
expect(ack.result.projectId).toBe("new-proj")
|
|
221
|
+
expect((deps.analytics as ReturnType<typeof makeAnalytics>).events).toContain("project_opened")
|
|
222
|
+
expect(deps.ensureProjectDirectory as ReturnType<typeof mock>).toHaveBeenCalledWith("/tmp/newproj")
|
|
223
|
+
expect(deps.refreshDiscovery as ReturnType<typeof mock>).toHaveBeenCalled()
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
test("project.open — existing project — skips analytics", async () => {
|
|
227
|
+
const deps = makeDeps({
|
|
228
|
+
storeOverrides: {
|
|
229
|
+
state: { projectIdsByPath: new Map([["/tmp/existing", "old-id"]]) },
|
|
230
|
+
openProject: mock(async () => ({ id: "old-id" })),
|
|
231
|
+
},
|
|
232
|
+
})
|
|
233
|
+
await handleProjectCommand(
|
|
234
|
+
deps,
|
|
235
|
+
{ type: "project.open", localPath: "/tmp/existing" },
|
|
236
|
+
"r7",
|
|
237
|
+
)
|
|
238
|
+
expect((deps.analytics as ReturnType<typeof makeAnalytics>).events).not.toContain("project_opened")
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
// -------------------------------------------------------------------------
|
|
242
|
+
// project.create
|
|
243
|
+
// -------------------------------------------------------------------------
|
|
244
|
+
|
|
245
|
+
test("project.create — new project — tracks both analytics events, acks", async () => {
|
|
246
|
+
const deps = makeDeps({
|
|
247
|
+
storeOverrides: {
|
|
248
|
+
state: { projectIdsByPath: new Map() },
|
|
249
|
+
openProject: mock(async () => ({ id: "created-proj" })),
|
|
250
|
+
},
|
|
251
|
+
})
|
|
252
|
+
const handled = await handleProjectCommand(
|
|
253
|
+
deps,
|
|
254
|
+
{ type: "project.create", localPath: "/tmp/created", title: "My App" },
|
|
255
|
+
"r8",
|
|
256
|
+
)
|
|
257
|
+
expect(handled).toBe(true)
|
|
258
|
+
const evts = (deps.analytics as ReturnType<typeof makeAnalytics>).events
|
|
259
|
+
expect(evts).toContain("project_opened")
|
|
260
|
+
expect(evts).toContain("project_created")
|
|
261
|
+
const ack = deps.sent[0] as { type: string; result: { projectId: string } }
|
|
262
|
+
expect(ack.result.projectId).toBe("created-proj")
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
// -------------------------------------------------------------------------
|
|
266
|
+
// project.remove
|
|
267
|
+
// -------------------------------------------------------------------------
|
|
268
|
+
|
|
269
|
+
test("project.remove — removes project, closes terminal, tracks analytics", async () => {
|
|
270
|
+
const closeByCwd = mock(() => {})
|
|
271
|
+
const removeProject = mock(async () => {})
|
|
272
|
+
const deps = makeDeps({
|
|
273
|
+
storeOverrides: {
|
|
274
|
+
getProject: mock(() => ({ id: "p1", localPath: "/tmp/proj" })),
|
|
275
|
+
removeProject,
|
|
276
|
+
},
|
|
277
|
+
})
|
|
278
|
+
;(deps.terminals as ProjectTerminalsDep).closeByCwd = closeByCwd
|
|
279
|
+
|
|
280
|
+
const handled = await handleProjectCommand(
|
|
281
|
+
deps,
|
|
282
|
+
{ type: "project.remove", projectId: "p1" },
|
|
283
|
+
"r9",
|
|
284
|
+
)
|
|
285
|
+
expect(handled).toBe(true)
|
|
286
|
+
expect(removeProject as ReturnType<typeof mock>).toHaveBeenCalledWith("p1")
|
|
287
|
+
expect(closeByCwd as ReturnType<typeof mock>).toHaveBeenCalledWith("/tmp/proj")
|
|
288
|
+
expect((deps.analytics as ReturnType<typeof makeAnalytics>).events).toContain("project_removed")
|
|
289
|
+
expect(deps.sent[0]).toMatchObject({ type: "ack", id: "r9" })
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
// -------------------------------------------------------------------------
|
|
293
|
+
// project.setStar
|
|
294
|
+
// -------------------------------------------------------------------------
|
|
295
|
+
|
|
296
|
+
test("project.setStar — sets star, acks, broadcasts sidebar", async () => {
|
|
297
|
+
const setProjectStar = mock(async () => {})
|
|
298
|
+
const deps = makeDeps({ storeOverrides: { setProjectStar } })
|
|
299
|
+
const handled = await handleProjectCommand(
|
|
300
|
+
deps,
|
|
301
|
+
{ type: "project.setStar", projectId: "p2", starred: true },
|
|
302
|
+
"r10",
|
|
303
|
+
)
|
|
304
|
+
expect(handled).toBe(true)
|
|
305
|
+
expect(setProjectStar as ReturnType<typeof mock>).toHaveBeenCalledWith("p2", true)
|
|
306
|
+
expect(deps.sidebarBroadcasts).toBe(1)
|
|
307
|
+
expect(deps.sent[0]).toMatchObject({ type: "ack", id: "r10" })
|
|
308
|
+
})
|
|
309
|
+
|
|
310
|
+
// -------------------------------------------------------------------------
|
|
311
|
+
// project.readDiffPatch
|
|
312
|
+
// -------------------------------------------------------------------------
|
|
313
|
+
|
|
314
|
+
test("project.readDiffPatch — returns patch content", async () => {
|
|
315
|
+
const deps = makeDeps({
|
|
316
|
+
storeOverrides: {
|
|
317
|
+
getProject: mock(() => ({ id: "p3", localPath: "/tmp/proj3" })),
|
|
318
|
+
},
|
|
319
|
+
})
|
|
320
|
+
const handled = await handleProjectCommand(
|
|
321
|
+
deps,
|
|
322
|
+
{ type: "project.readDiffPatch", projectId: "p3", path: "src/foo.ts" },
|
|
323
|
+
"r11",
|
|
324
|
+
)
|
|
325
|
+
expect(handled).toBe(true)
|
|
326
|
+
const ack = deps.sent[0] as { type: string; result: { patch: string } }
|
|
327
|
+
expect(ack.result.patch).toBe("diff content")
|
|
328
|
+
expect(deps.diffStore.readPatch as ReturnType<typeof mock>).toHaveBeenCalledWith({
|
|
329
|
+
projectPath: "/tmp/proj3",
|
|
330
|
+
path: "src/foo.ts",
|
|
331
|
+
})
|
|
332
|
+
})
|
|
333
|
+
|
|
334
|
+
test("project.readDiffPatch — missing project — throws Project not found", async () => {
|
|
335
|
+
const deps = makeDeps({
|
|
336
|
+
storeOverrides: {
|
|
337
|
+
getProject: mock(() => undefined),
|
|
338
|
+
},
|
|
339
|
+
})
|
|
340
|
+
await expect(
|
|
341
|
+
handleProjectCommand(
|
|
342
|
+
deps,
|
|
343
|
+
{ type: "project.readDiffPatch", projectId: "ghost", path: "foo.ts" },
|
|
344
|
+
"r12",
|
|
345
|
+
),
|
|
346
|
+
).rejects.toThrow("Project not found")
|
|
347
|
+
})
|
|
348
|
+
|
|
349
|
+
// -------------------------------------------------------------------------
|
|
350
|
+
// sessions.importClaude
|
|
351
|
+
// -------------------------------------------------------------------------
|
|
352
|
+
|
|
353
|
+
test("sessions.importClaude — imports sessions, acks, broadcasts sidebar", async () => {
|
|
354
|
+
const deps = makeDeps({ importResult: { imported: 3, newProjects: 0 } })
|
|
355
|
+
const handled = await handleProjectCommand(deps, { type: "sessions.importClaude" }, "r13")
|
|
356
|
+
expect(handled).toBe(true)
|
|
357
|
+
const ack = deps.sent[0] as { type: string; result: { imported: number } }
|
|
358
|
+
expect(ack.result.imported).toBe(3)
|
|
359
|
+
expect(deps.sidebarBroadcasts).toBe(1)
|
|
360
|
+
})
|
|
361
|
+
|
|
362
|
+
test("sessions.importClaude — newProjects > 0 — calls refreshDiscovery", async () => {
|
|
363
|
+
const deps = makeDeps({ importResult: { imported: 1, newProjects: 2 } })
|
|
364
|
+
await handleProjectCommand(deps, { type: "sessions.importClaude" }, "r14")
|
|
365
|
+
expect(deps.refreshDiscovery as ReturnType<typeof mock>).toHaveBeenCalled()
|
|
366
|
+
})
|
|
367
|
+
|
|
368
|
+
// -------------------------------------------------------------------------
|
|
369
|
+
// sidebar.reorderProjectGroups
|
|
370
|
+
// -------------------------------------------------------------------------
|
|
371
|
+
|
|
372
|
+
test("sidebar.reorderProjectGroups — sets order, acks, broadcasts sidebar", async () => {
|
|
373
|
+
const setSidebarProjectOrder = mock(async () => {})
|
|
374
|
+
const deps = makeDeps({ storeOverrides: { setSidebarProjectOrder } })
|
|
375
|
+
const handled = await handleProjectCommand(
|
|
376
|
+
deps,
|
|
377
|
+
{ type: "sidebar.reorderProjectGroups", projectIds: ["p1", "p2"] },
|
|
378
|
+
"r15",
|
|
379
|
+
)
|
|
380
|
+
expect(handled).toBe(true)
|
|
381
|
+
expect(setSidebarProjectOrder as ReturnType<typeof mock>).toHaveBeenCalledWith(["p1", "p2"])
|
|
382
|
+
expect(deps.sidebarBroadcasts).toBe(1)
|
|
383
|
+
expect(deps.sent[0]).toMatchObject({ type: "ack", id: "r15" })
|
|
384
|
+
})
|
|
385
|
+
|
|
386
|
+
// -------------------------------------------------------------------------
|
|
387
|
+
// system.openExternal
|
|
388
|
+
// -------------------------------------------------------------------------
|
|
389
|
+
|
|
390
|
+
test("system.openExternal — calls openExternalFn, acks", async () => {
|
|
391
|
+
const openExternalFn = mock(async () => {})
|
|
392
|
+
const deps = makeDeps({ openExternalFn })
|
|
393
|
+
const cmd: ClientCommand = {
|
|
394
|
+
type: "system.openExternal",
|
|
395
|
+
kind: "external",
|
|
396
|
+
target: "https://example.com",
|
|
397
|
+
} as unknown as ClientCommand
|
|
398
|
+
const handled = await handleProjectCommand(deps, cmd, "r16")
|
|
399
|
+
expect(handled).toBe(true)
|
|
400
|
+
expect(openExternalFn as ReturnType<typeof mock>).toHaveBeenCalledWith(cmd)
|
|
401
|
+
expect(deps.sent[0]).toMatchObject({ type: "ack", id: "r16" })
|
|
402
|
+
})
|
|
403
|
+
})
|