@cuongtran001/kanna 1.3.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-CAysfmX5.js → arc-DXtN3EWJ.js} +1 -1
- package/dist/client/assets/{architectureDiagram-3BPJPVTR-BKqRYsm4.js → architectureDiagram-3BPJPVTR-CIGqW1z-.js} +1 -1
- package/dist/client/assets/{blockDiagram-GPEHLZMM-Ce7Eorec.js → blockDiagram-GPEHLZMM-BafNS_H6.js} +1 -1
- package/dist/client/assets/{c4Diagram-AAUBKEIU-CJ77zauH.js → c4Diagram-AAUBKEIU-YdAgRSis.js} +1 -1
- package/dist/client/assets/channel-BEYTrUAm.js +1 -0
- package/dist/client/assets/{chunk-2J33WTMH-Rk_3zLxJ.js → chunk-2J33WTMH-eiP5y5G-.js} +1 -1
- package/dist/client/assets/{chunk-4BX2VUAB-DPIz6Nvf.js → chunk-4BX2VUAB-CvLGzPMF.js} +1 -1
- package/dist/client/assets/{chunk-55IACEB6-D-TkbGh2.js → chunk-55IACEB6-Y-S9Fb8A.js} +1 -1
- package/dist/client/assets/{chunk-727SXJPM-B4NOSIZV.js → chunk-727SXJPM-6sJH1RI9.js} +1 -1
- package/dist/client/assets/{chunk-AQP2D5EJ-Cm_TGLYm.js → chunk-AQP2D5EJ-hV1r_IXO.js} +1 -1
- package/dist/client/assets/{chunk-FMBD7UC4-Bu45BUVh.js → chunk-FMBD7UC4-CC3L28S5.js} +1 -1
- package/dist/client/assets/{chunk-ND2GUHAM-C1Bw8_gg.js → chunk-ND2GUHAM-BZ3czhdH.js} +1 -1
- package/dist/client/assets/{chunk-QZHKN3VN-DueG15o0.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-YgpMurXf.js → cose-bilkent-S5V4N54A-CfxdDghE.js} +1 -1
- package/dist/client/assets/{dagre-BM42HDAG-Dop7Oq67.js → dagre-BM42HDAG-B8CddsbB.js} +1 -1
- package/dist/client/assets/{diagram-2AECGRRQ-unMfBEJH.js → diagram-2AECGRRQ-F0yS0fVB.js} +1 -1
- package/dist/client/assets/{diagram-5GNKFQAL-D9EJ9qMI.js → diagram-5GNKFQAL-cW0i0qlh.js} +1 -1
- package/dist/client/assets/{diagram-KO2AKTUF-C5IykOIR.js → diagram-KO2AKTUF-BzAKMeJT.js} +1 -1
- package/dist/client/assets/{diagram-LMA3HP47-DJAuPngz.js → diagram-LMA3HP47-qdC4XqIl.js} +1 -1
- package/dist/client/assets/{diagram-OG6HWLK6-B3wHFtS-.js → diagram-OG6HWLK6-CGcpi6Dh.js} +1 -1
- package/dist/client/assets/{erDiagram-TEJ5UH35-Dm7kt9o5.js → erDiagram-TEJ5UH35-CxNTDicM.js} +1 -1
- package/dist/client/assets/{flowDiagram-I6XJVG4X-s9Lds3ny.js → flowDiagram-I6XJVG4X-qkGa5TqY.js} +1 -1
- package/dist/client/assets/{ganttDiagram-6RSMTGT7-CRQNYdAf.js → ganttDiagram-6RSMTGT7-D9mVPoQi.js} +1 -1
- package/dist/client/assets/{gitGraphDiagram-PVQCEYII-DhYjK4Jb.js → gitGraphDiagram-PVQCEYII-DA0grAXe.js} +1 -1
- package/dist/client/assets/{index-CbSY9HfS.js → index-BqUY1Bm4.js} +1 -1
- package/dist/client/assets/{index-C0U2kwuB.js → index-TU1k6ov6.js} +5 -5
- package/dist/client/assets/{infoDiagram-5YYISTIA-CRKJ0PSH.js → infoDiagram-5YYISTIA-CGljtLs3.js} +1 -1
- package/dist/client/assets/{ishikawaDiagram-YF4QCWOH-CEAkB0IY.js → ishikawaDiagram-YF4QCWOH-CtC0xrmG.js} +1 -1
- package/dist/client/assets/{journeyDiagram-JHISSGLW-CyOK5_DG.js → journeyDiagram-JHISSGLW-URLnVZeN.js} +1 -1
- package/dist/client/assets/{kanban-definition-UN3LZRKU-BfKjiJSo.js → kanban-definition-UN3LZRKU-C2Q6aqQh.js} +1 -1
- package/dist/client/assets/{linear-DLR6miy3.js → linear-ByKHtsIx.js} +1 -1
- package/dist/client/assets/{mermaid.core-D7AdC_lE.js → mermaid.core-Dl_f2KLc.js} +4 -4
- package/dist/client/assets/{mindmap-definition-RKZ34NQL-zYyJtDSa.js → mindmap-definition-RKZ34NQL-CaJ9IDLI.js} +1 -1
- package/dist/client/assets/{pieDiagram-4H26LBE5-BgP8PFlo.js → pieDiagram-4H26LBE5-mmYaf2Oa.js} +1 -1
- package/dist/client/assets/{quadrantDiagram-W4KKPZXB-Di8h7J96.js → quadrantDiagram-W4KKPZXB-B_PO6OCC.js} +1 -1
- package/dist/client/assets/{requirementDiagram-4Y6WPE33-DuTzFl3R.js → requirementDiagram-4Y6WPE33-M4huwJt1.js} +1 -1
- package/dist/client/assets/{sankeyDiagram-5OEKKPKP-DBRoMm_T.js → sankeyDiagram-5OEKKPKP-B0hb1sWh.js} +1 -1
- package/dist/client/assets/{sequenceDiagram-3UESZ5HK-Bmb-1HUN.js → sequenceDiagram-3UESZ5HK-BAqSptnA.js} +1 -1
- package/dist/client/assets/{stateDiagram-AJRCARHV-fnv5ayn-.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-ByWRdpQX.js → timeline-definition-PNZ67QCA-MgAjHz8Q.js} +1 -1
- package/dist/client/assets/{vennDiagram-CIIHVFJN--2WGpVXW.js → vennDiagram-CIIHVFJN-DmKoO51W.js} +1 -1
- package/dist/client/assets/{wardley-L42UT6IY-24zlnzxK.js → wardley-L42UT6IY-CWcyHnYs.js} +1 -1
- package/dist/client/assets/{wardleyDiagram-YWT4CUSO-Cllvqb3K.js → wardleyDiagram-YWT4CUSO-BGIGmFP9.js} +1 -1
- package/dist/client/assets/{xychartDiagram-2RQKCTM6-ZcdC6nPE.js → xychartDiagram-2RQKCTM6-D20YIvEl.js} +1 -1
- package/dist/client/index.html +1 -1
- 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.test.ts +22 -6
- package/src/server/agent.ts +35 -4987
- 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 -2285
- package/src/server/harness-types.ts +15 -1
- package/src/server/oauth-errors.ts +19 -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/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 +25 -1711
- package/dist/client/assets/channel-KQhRwDzJ.js +0 -1
- package/dist/client/assets/classDiagram-4FO5ZUOK-ChxT1GNq.js +0 -1
- package/dist/client/assets/classDiagram-v2-Q7XG4LA2-ChxT1GNq.js +0 -1
- package/dist/client/assets/stateDiagram-v2-BHNVJYJU-D4X-ZnLV.js +0 -1
|
@@ -0,0 +1,554 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test"
|
|
2
|
+
import type { ToolRequest } from "../shared/permission-policy"
|
|
3
|
+
import type { ChatRecord, ProjectRecord, StackRecord } from "./events"
|
|
4
|
+
import {
|
|
5
|
+
buildAddProjectToStackEvent,
|
|
6
|
+
buildArchiveChatEvent,
|
|
7
|
+
buildChatProviderEvent,
|
|
8
|
+
buildChatReadStateEvent,
|
|
9
|
+
buildChatSourceHashEvent,
|
|
10
|
+
buildCompactFailuresEvent,
|
|
11
|
+
buildCreateChatEvent,
|
|
12
|
+
buildCreateStackEvent,
|
|
13
|
+
buildEnqueueMessageResult,
|
|
14
|
+
buildOpenProjectResult,
|
|
15
|
+
buildPendingForkSessionTokenEvent,
|
|
16
|
+
buildPlanModeEvent,
|
|
17
|
+
buildPutToolRequestEvent,
|
|
18
|
+
buildRemoveProjectEvent,
|
|
19
|
+
buildRemoveProjectFromStackEvent,
|
|
20
|
+
buildRemoveQueuedMessageEvent,
|
|
21
|
+
buildRemoveStackEvent,
|
|
22
|
+
buildRenameStackEvent,
|
|
23
|
+
buildRenameChatEvent,
|
|
24
|
+
buildResolveToolRequestEvent,
|
|
25
|
+
buildSetProjectStarEvent,
|
|
26
|
+
buildTurnCancelledEvent,
|
|
27
|
+
buildTurnFailedEvent,
|
|
28
|
+
buildTurnFinishedEvent,
|
|
29
|
+
buildTurnStartedEvent,
|
|
30
|
+
buildUnarchiveChatEvent,
|
|
31
|
+
computeNewSidebarOrder,
|
|
32
|
+
} from "./event-store-write-ops"
|
|
33
|
+
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
// Helpers
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
|
|
38
|
+
const TS = 1_700_000_000_000
|
|
39
|
+
|
|
40
|
+
function makeChat(overrides?: Partial<ChatRecord>): ChatRecord {
|
|
41
|
+
return {
|
|
42
|
+
id: "chat-1",
|
|
43
|
+
projectId: "proj-1",
|
|
44
|
+
title: "Chat One",
|
|
45
|
+
createdAt: TS,
|
|
46
|
+
updatedAt: TS,
|
|
47
|
+
unread: false,
|
|
48
|
+
provider: null,
|
|
49
|
+
planMode: false,
|
|
50
|
+
sessionTokensByProvider: {},
|
|
51
|
+
sourceHash: null,
|
|
52
|
+
pendingForkSessionToken: null,
|
|
53
|
+
hasMessages: false,
|
|
54
|
+
lastTurnOutcome: null,
|
|
55
|
+
...overrides,
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function makeStack(overrides?: Partial<StackRecord>): StackRecord {
|
|
60
|
+
return {
|
|
61
|
+
id: "stack-1",
|
|
62
|
+
title: "Stack One",
|
|
63
|
+
projectIds: ["proj-1", "proj-2"],
|
|
64
|
+
createdAt: TS,
|
|
65
|
+
updatedAt: TS,
|
|
66
|
+
...overrides,
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function makeProject(id: string): ProjectRecord {
|
|
71
|
+
return { id, localPath: `/tmp/${id}`, title: id, createdAt: TS, updatedAt: TS }
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function makeProjectsById(ids: string[] = ["proj-1", "proj-2"]): Map<string, ProjectRecord> {
|
|
75
|
+
return new Map(ids.map((id) => [id, makeProject(id)]))
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function makeToolRequest(overrides?: Partial<ToolRequest>): ToolRequest {
|
|
79
|
+
return {
|
|
80
|
+
id: "req-1",
|
|
81
|
+
chatId: "chat-1",
|
|
82
|
+
sessionId: "sess-1",
|
|
83
|
+
toolUseId: "tool-use-1",
|
|
84
|
+
toolName: "AskUserQuestion",
|
|
85
|
+
arguments: {},
|
|
86
|
+
canonicalArgsHash: "hash",
|
|
87
|
+
policyVerdict: "ask",
|
|
88
|
+
status: "pending",
|
|
89
|
+
createdAt: TS,
|
|
90
|
+
expiresAt: TS + 600_000,
|
|
91
|
+
...overrides,
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ---------------------------------------------------------------------------
|
|
96
|
+
// buildOpenProjectResult
|
|
97
|
+
// ---------------------------------------------------------------------------
|
|
98
|
+
|
|
99
|
+
describe("buildOpenProjectResult", () => {
|
|
100
|
+
test("returns existing project when already known", () => {
|
|
101
|
+
const existing = makeProject("proj-1")
|
|
102
|
+
const state = {
|
|
103
|
+
projectsById: new Map([["proj-1", existing]]),
|
|
104
|
+
projectIdsByPath: new Map([[existing.localPath, "proj-1"]]),
|
|
105
|
+
}
|
|
106
|
+
const result = buildOpenProjectResult(state, "/tmp/proj-1")
|
|
107
|
+
expect(result.kind).toBe("existing")
|
|
108
|
+
if (result.kind === "existing") {
|
|
109
|
+
expect(result.project).toBe(existing)
|
|
110
|
+
}
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
test("returns new event for unknown path", () => {
|
|
114
|
+
const state = {
|
|
115
|
+
projectsById: new Map<string, ProjectRecord>(),
|
|
116
|
+
projectIdsByPath: new Map<string, string>(),
|
|
117
|
+
}
|
|
118
|
+
const result = buildOpenProjectResult(state, "/tmp/new-project", "My Project")
|
|
119
|
+
expect(result.kind).toBe("new")
|
|
120
|
+
if (result.kind === "new") {
|
|
121
|
+
expect(result.event.type).toBe("project_opened")
|
|
122
|
+
expect(result.event.title).toBe("My Project")
|
|
123
|
+
}
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
test("uses path basename when no title given", () => {
|
|
127
|
+
const state = {
|
|
128
|
+
projectsById: new Map<string, ProjectRecord>(),
|
|
129
|
+
projectIdsByPath: new Map<string, string>(),
|
|
130
|
+
}
|
|
131
|
+
const result = buildOpenProjectResult(state, "/home/user/my-repo")
|
|
132
|
+
expect(result.kind).toBe("new")
|
|
133
|
+
if (result.kind === "new") {
|
|
134
|
+
expect(result.event.title).toBe("my-repo")
|
|
135
|
+
}
|
|
136
|
+
})
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
// ---------------------------------------------------------------------------
|
|
140
|
+
// buildRemoveProjectEvent
|
|
141
|
+
// ---------------------------------------------------------------------------
|
|
142
|
+
|
|
143
|
+
describe("buildRemoveProjectEvent", () => {
|
|
144
|
+
test("returns project_removed event", () => {
|
|
145
|
+
const projectsById = new Map([["proj-1", makeProject("proj-1")]])
|
|
146
|
+
const event = buildRemoveProjectEvent(projectsById, "proj-1")
|
|
147
|
+
expect(event.type).toBe("project_removed")
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
test("throws if project not found", () => {
|
|
151
|
+
expect(() => buildRemoveProjectEvent(new Map(), "missing")).toThrow()
|
|
152
|
+
})
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
// ---------------------------------------------------------------------------
|
|
156
|
+
// buildSetProjectStarEvent
|
|
157
|
+
// ---------------------------------------------------------------------------
|
|
158
|
+
|
|
159
|
+
describe("buildSetProjectStarEvent", () => {
|
|
160
|
+
test("returns project_star_set event with starredAt when starring", () => {
|
|
161
|
+
const projectsById = new Map([["proj-1", makeProject("proj-1")]])
|
|
162
|
+
const event = buildSetProjectStarEvent(projectsById, "proj-1", true)
|
|
163
|
+
expect(event.type).toBe("project_star_set")
|
|
164
|
+
if (event.type === "project_star_set") {
|
|
165
|
+
expect(event.starredAt).toBeGreaterThan(0)
|
|
166
|
+
}
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
test("returns null starredAt when unstarring", () => {
|
|
170
|
+
const projectsById = new Map([["proj-1", makeProject("proj-1")]])
|
|
171
|
+
const event = buildSetProjectStarEvent(projectsById, "proj-1", false)
|
|
172
|
+
if (event.type === "project_star_set") {
|
|
173
|
+
expect(event.starredAt).toBeNull()
|
|
174
|
+
}
|
|
175
|
+
})
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
// ---------------------------------------------------------------------------
|
|
179
|
+
// computeNewSidebarOrder
|
|
180
|
+
// ---------------------------------------------------------------------------
|
|
181
|
+
|
|
182
|
+
describe("computeNewSidebarOrder", () => {
|
|
183
|
+
test("returns null when order is unchanged", () => {
|
|
184
|
+
const projectsById = makeProjectsById(["proj-1", "proj-2"])
|
|
185
|
+
const result = computeNewSidebarOrder(projectsById, ["proj-1", "proj-2"], ["proj-1", "proj-2"])
|
|
186
|
+
expect(result).toBeNull()
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
test("returns new order when changed", () => {
|
|
190
|
+
const projectsById = makeProjectsById(["proj-1", "proj-2"])
|
|
191
|
+
const result = computeNewSidebarOrder(projectsById, ["proj-1", "proj-2"], ["proj-2", "proj-1"])
|
|
192
|
+
expect(result).toEqual(["proj-2", "proj-1"])
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
test("filters out deleted and unknown projects", () => {
|
|
196
|
+
const projectsById = new Map([
|
|
197
|
+
["proj-1", makeProject("proj-1")],
|
|
198
|
+
["proj-deleted", { ...makeProject("proj-deleted"), deletedAt: TS }],
|
|
199
|
+
])
|
|
200
|
+
const result = computeNewSidebarOrder(projectsById, [], ["proj-1", "proj-deleted", "unknown"])
|
|
201
|
+
expect(result).toEqual(["proj-1"])
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
test("deduplicates", () => {
|
|
205
|
+
const projectsById = makeProjectsById(["proj-1", "proj-2"])
|
|
206
|
+
const result = computeNewSidebarOrder(projectsById, [], ["proj-1", "proj-1", "proj-2"])
|
|
207
|
+
expect(result).toEqual(["proj-1", "proj-2"])
|
|
208
|
+
})
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
// ---------------------------------------------------------------------------
|
|
212
|
+
// Stack builders
|
|
213
|
+
// ---------------------------------------------------------------------------
|
|
214
|
+
|
|
215
|
+
describe("buildCreateStackEvent", () => {
|
|
216
|
+
test("builds stack_added event", () => {
|
|
217
|
+
const state = { projectsById: makeProjectsById(), stacksById: new Map<string, StackRecord>() }
|
|
218
|
+
const event = buildCreateStackEvent(state, "My Stack", ["proj-1", "proj-2"])
|
|
219
|
+
expect(event.type).toBe("stack_added")
|
|
220
|
+
expect(event.stackId).toBeDefined()
|
|
221
|
+
if (event.type === "stack_added") {
|
|
222
|
+
expect(event.title).toBe("My Stack")
|
|
223
|
+
}
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
test("throws when fewer than 2 projects", () => {
|
|
227
|
+
const state = { projectsById: makeProjectsById(), stacksById: new Map<string, StackRecord>() }
|
|
228
|
+
expect(() => buildCreateStackEvent(state, "S", ["proj-1"])).toThrow()
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
test("throws when title is blank", () => {
|
|
232
|
+
const state = { projectsById: makeProjectsById(), stacksById: new Map<string, StackRecord>() }
|
|
233
|
+
expect(() => buildCreateStackEvent(state, " ", ["proj-1", "proj-2"])).toThrow()
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
test("throws when duplicate projectIds", () => {
|
|
237
|
+
const state = { projectsById: makeProjectsById(["proj-1"]), stacksById: new Map<string, StackRecord>() }
|
|
238
|
+
expect(() => buildCreateStackEvent(state, "S", ["proj-1", "proj-1"])).toThrow()
|
|
239
|
+
})
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
describe("buildRenameStackEvent", () => {
|
|
243
|
+
test("returns null when title unchanged", () => {
|
|
244
|
+
const stacksById = new Map([["stack-1", makeStack()]])
|
|
245
|
+
expect(buildRenameStackEvent(stacksById, "stack-1", "Stack One")).toBeNull()
|
|
246
|
+
})
|
|
247
|
+
|
|
248
|
+
test("returns event when title changed", () => {
|
|
249
|
+
const stacksById = new Map([["stack-1", makeStack()]])
|
|
250
|
+
const event = buildRenameStackEvent(stacksById, "stack-1", "New Name")
|
|
251
|
+
expect(event?.type).toBe("stack_renamed")
|
|
252
|
+
if (event?.type === "stack_renamed") {
|
|
253
|
+
expect(event.title).toBe("New Name")
|
|
254
|
+
}
|
|
255
|
+
})
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
describe("buildRemoveStackEvent", () => {
|
|
259
|
+
test("returns stack_removed event", () => {
|
|
260
|
+
const stacksById = new Map([["stack-1", makeStack()]])
|
|
261
|
+
const event = buildRemoveStackEvent(stacksById, "stack-1")
|
|
262
|
+
expect(event?.type).toBe("stack_removed")
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
test("returns null if already deleted", () => {
|
|
266
|
+
const stacksById = new Map([["stack-1", makeStack({ deletedAt: TS })]])
|
|
267
|
+
expect(buildRemoveStackEvent(stacksById, "stack-1")).toBeNull()
|
|
268
|
+
})
|
|
269
|
+
})
|
|
270
|
+
|
|
271
|
+
describe("buildAddProjectToStackEvent", () => {
|
|
272
|
+
test("returns null if project already in stack", () => {
|
|
273
|
+
const state = {
|
|
274
|
+
projectsById: makeProjectsById(["proj-1", "proj-2"]),
|
|
275
|
+
stacksById: new Map([["stack-1", makeStack({ projectIds: ["proj-1", "proj-2"] })]]),
|
|
276
|
+
}
|
|
277
|
+
expect(buildAddProjectToStackEvent(state, "stack-1", "proj-1")).toBeNull()
|
|
278
|
+
})
|
|
279
|
+
|
|
280
|
+
test("returns event when project not yet in stack", () => {
|
|
281
|
+
const state = {
|
|
282
|
+
projectsById: makeProjectsById(["proj-1", "proj-2", "proj-3"]),
|
|
283
|
+
stacksById: new Map([["stack-1", makeStack({ projectIds: ["proj-1", "proj-2"] })]]),
|
|
284
|
+
}
|
|
285
|
+
const event = buildAddProjectToStackEvent(state, "stack-1", "proj-3")
|
|
286
|
+
expect(event?.type).toBe("stack_project_added")
|
|
287
|
+
})
|
|
288
|
+
})
|
|
289
|
+
|
|
290
|
+
describe("buildRemoveProjectFromStackEvent", () => {
|
|
291
|
+
test("returns null if project not in stack", () => {
|
|
292
|
+
const stacksById = new Map([["stack-1", makeStack({ projectIds: ["proj-1", "proj-2"] })]])
|
|
293
|
+
expect(buildRemoveProjectFromStackEvent(stacksById, "stack-1", "proj-99")).toBeNull()
|
|
294
|
+
})
|
|
295
|
+
|
|
296
|
+
test("throws when would drop below 2 members", () => {
|
|
297
|
+
const stacksById = new Map([["stack-1", makeStack({ projectIds: ["proj-1", "proj-2"] })]])
|
|
298
|
+
expect(() => buildRemoveProjectFromStackEvent(stacksById, "stack-1", "proj-1")).toThrow()
|
|
299
|
+
})
|
|
300
|
+
|
|
301
|
+
test("returns event when stack has 3+ members", () => {
|
|
302
|
+
const stacksById = new Map([["stack-1", makeStack({ projectIds: ["proj-1", "proj-2", "proj-3"] })]])
|
|
303
|
+
const event = buildRemoveProjectFromStackEvent(stacksById, "stack-1", "proj-3")
|
|
304
|
+
expect(event?.type).toBe("stack_project_removed")
|
|
305
|
+
})
|
|
306
|
+
})
|
|
307
|
+
|
|
308
|
+
// ---------------------------------------------------------------------------
|
|
309
|
+
// Chat builders
|
|
310
|
+
// ---------------------------------------------------------------------------
|
|
311
|
+
|
|
312
|
+
describe("buildCreateChatEvent", () => {
|
|
313
|
+
test("builds chat_created event", () => {
|
|
314
|
+
const state = { projectsById: makeProjectsById(), stacksById: new Map<string, StackRecord>() }
|
|
315
|
+
const event = buildCreateChatEvent(state, "proj-1")
|
|
316
|
+
expect(event.type).toBe("chat_created")
|
|
317
|
+
expect(event.chatId).toBeDefined()
|
|
318
|
+
if (event.type === "chat_created") {
|
|
319
|
+
expect(event.projectId).toBe("proj-1")
|
|
320
|
+
}
|
|
321
|
+
})
|
|
322
|
+
|
|
323
|
+
test("throws when project not found", () => {
|
|
324
|
+
const state = { projectsById: new Map<string, ProjectRecord>(), stacksById: new Map<string, StackRecord>() }
|
|
325
|
+
expect(() => buildCreateChatEvent(state, "missing")).toThrow()
|
|
326
|
+
})
|
|
327
|
+
})
|
|
328
|
+
|
|
329
|
+
describe("buildRenameChatEvent", () => {
|
|
330
|
+
test("returns null if title unchanged", () => {
|
|
331
|
+
const chatsById = new Map([["chat-1", makeChat({ title: "Same" })]])
|
|
332
|
+
expect(buildRenameChatEvent(chatsById, "chat-1", "Same")).toBeNull()
|
|
333
|
+
})
|
|
334
|
+
|
|
335
|
+
test("returns null if title is blank", () => {
|
|
336
|
+
const chatsById = new Map([["chat-1", makeChat()]])
|
|
337
|
+
expect(buildRenameChatEvent(chatsById, "chat-1", " ")).toBeNull()
|
|
338
|
+
})
|
|
339
|
+
|
|
340
|
+
test("returns event when title changed", () => {
|
|
341
|
+
const chatsById = new Map([["chat-1", makeChat()]])
|
|
342
|
+
const event = buildRenameChatEvent(chatsById, "chat-1", "New Name")
|
|
343
|
+
expect(event?.type).toBe("chat_renamed")
|
|
344
|
+
})
|
|
345
|
+
})
|
|
346
|
+
|
|
347
|
+
describe("buildArchiveChatEvent / buildUnarchiveChatEvent", () => {
|
|
348
|
+
test("archive returns chat_archived", () => {
|
|
349
|
+
const chatsById = new Map([["chat-1", makeChat()]])
|
|
350
|
+
expect(buildArchiveChatEvent(chatsById, "chat-1").type).toBe("chat_archived")
|
|
351
|
+
})
|
|
352
|
+
|
|
353
|
+
test("unarchive returns chat_unarchived", () => {
|
|
354
|
+
const chatsById = new Map([["chat-1", makeChat()]])
|
|
355
|
+
expect(buildUnarchiveChatEvent(chatsById, "chat-1").type).toBe("chat_unarchived")
|
|
356
|
+
})
|
|
357
|
+
|
|
358
|
+
test("throws if chat not found", () => {
|
|
359
|
+
expect(() => buildArchiveChatEvent(new Map(), "missing")).toThrow()
|
|
360
|
+
})
|
|
361
|
+
})
|
|
362
|
+
|
|
363
|
+
describe("buildChatProviderEvent", () => {
|
|
364
|
+
test("returns null if provider unchanged", () => {
|
|
365
|
+
const chatsById = new Map([["chat-1", makeChat({ provider: "claude" })]])
|
|
366
|
+
expect(buildChatProviderEvent(chatsById, "chat-1", "claude")).toBeNull()
|
|
367
|
+
})
|
|
368
|
+
|
|
369
|
+
test("returns event when provider changes", () => {
|
|
370
|
+
const chatsById = new Map([["chat-1", makeChat({ provider: null })]])
|
|
371
|
+
const event = buildChatProviderEvent(chatsById, "chat-1", "claude")
|
|
372
|
+
expect(event?.type).toBe("chat_provider_set")
|
|
373
|
+
})
|
|
374
|
+
})
|
|
375
|
+
|
|
376
|
+
describe("buildPlanModeEvent", () => {
|
|
377
|
+
test("returns null if planMode unchanged", () => {
|
|
378
|
+
const chatsById = new Map([["chat-1", makeChat({ planMode: false })]])
|
|
379
|
+
expect(buildPlanModeEvent(chatsById, "chat-1", false)).toBeNull()
|
|
380
|
+
})
|
|
381
|
+
|
|
382
|
+
test("returns event when planMode changes", () => {
|
|
383
|
+
const chatsById = new Map([["chat-1", makeChat({ planMode: false })]])
|
|
384
|
+
expect(buildPlanModeEvent(chatsById, "chat-1", true)?.type).toBe("chat_plan_mode_set")
|
|
385
|
+
})
|
|
386
|
+
})
|
|
387
|
+
|
|
388
|
+
describe("buildCompactFailuresEvent", () => {
|
|
389
|
+
test("returns null if count unchanged", () => {
|
|
390
|
+
const chatsById = new Map([["chat-1", makeChat({ compactFailureCount: 0 })]])
|
|
391
|
+
expect(buildCompactFailuresEvent(chatsById, "chat-1", 0)).toBeNull()
|
|
392
|
+
})
|
|
393
|
+
|
|
394
|
+
test("returns event when count changes", () => {
|
|
395
|
+
const chatsById = new Map([["chat-1", makeChat()]])
|
|
396
|
+
expect(buildCompactFailuresEvent(chatsById, "chat-1", 1)?.type).toBe("chat_compact_failures_set")
|
|
397
|
+
})
|
|
398
|
+
})
|
|
399
|
+
|
|
400
|
+
describe("buildChatReadStateEvent", () => {
|
|
401
|
+
test("returns null if unread unchanged", () => {
|
|
402
|
+
const chatsById = new Map([["chat-1", makeChat({ unread: false })]])
|
|
403
|
+
expect(buildChatReadStateEvent(chatsById, "chat-1", false)).toBeNull()
|
|
404
|
+
})
|
|
405
|
+
|
|
406
|
+
test("returns event when unread changes", () => {
|
|
407
|
+
const chatsById = new Map([["chat-1", makeChat({ unread: false })]])
|
|
408
|
+
expect(buildChatReadStateEvent(chatsById, "chat-1", true)?.type).toBe("chat_read_state_set")
|
|
409
|
+
})
|
|
410
|
+
})
|
|
411
|
+
|
|
412
|
+
describe("buildChatSourceHashEvent", () => {
|
|
413
|
+
test("returns null if sourceHash unchanged", () => {
|
|
414
|
+
const chatsById = new Map([["chat-1", makeChat({ sourceHash: "abc" })]])
|
|
415
|
+
expect(buildChatSourceHashEvent(chatsById, "chat-1", "abc")).toBeNull()
|
|
416
|
+
})
|
|
417
|
+
|
|
418
|
+
test("returns event when sourceHash changes", () => {
|
|
419
|
+
const chatsById = new Map([["chat-1", makeChat({ sourceHash: null })]])
|
|
420
|
+
expect(buildChatSourceHashEvent(chatsById, "chat-1", "abc")?.type).toBe("chat_source_hash_set")
|
|
421
|
+
})
|
|
422
|
+
})
|
|
423
|
+
|
|
424
|
+
// ---------------------------------------------------------------------------
|
|
425
|
+
// Queued message builders
|
|
426
|
+
// ---------------------------------------------------------------------------
|
|
427
|
+
|
|
428
|
+
describe("buildEnqueueMessageResult", () => {
|
|
429
|
+
test("builds event and queuedMessage with generated id", () => {
|
|
430
|
+
const chatsById = new Map([["chat-1", makeChat()]])
|
|
431
|
+
const { event, queuedMessage } = buildEnqueueMessageResult(chatsById, "chat-1", {
|
|
432
|
+
content: "hello",
|
|
433
|
+
attachments: [],
|
|
434
|
+
provider: "claude",
|
|
435
|
+
model: "claude-3-5-sonnet",
|
|
436
|
+
})
|
|
437
|
+
expect(event.type).toBe("queued_message_enqueued")
|
|
438
|
+
expect(queuedMessage.id).toBeDefined()
|
|
439
|
+
expect(queuedMessage.content).toBe("hello")
|
|
440
|
+
})
|
|
441
|
+
|
|
442
|
+
test("uses provided id if given", () => {
|
|
443
|
+
const chatsById = new Map([["chat-1", makeChat()]])
|
|
444
|
+
const { queuedMessage } = buildEnqueueMessageResult(chatsById, "chat-1", {
|
|
445
|
+
id: "fixed-id",
|
|
446
|
+
content: "hello",
|
|
447
|
+
attachments: [],
|
|
448
|
+
provider: "claude",
|
|
449
|
+
model: "claude-3-5-sonnet",
|
|
450
|
+
})
|
|
451
|
+
expect(queuedMessage.id).toBe("fixed-id")
|
|
452
|
+
})
|
|
453
|
+
})
|
|
454
|
+
|
|
455
|
+
describe("buildRemoveQueuedMessageEvent", () => {
|
|
456
|
+
test("returns queued_message_removed event", () => {
|
|
457
|
+
const chatsById = new Map([["chat-1", makeChat()]])
|
|
458
|
+
const queuedMessagesByChatId = new Map([
|
|
459
|
+
["chat-1", [{ id: "msg-1", content: "hi", attachments: [], createdAt: TS, provider: "claude" as const, model: "m" }]],
|
|
460
|
+
])
|
|
461
|
+
const event = buildRemoveQueuedMessageEvent(chatsById, queuedMessagesByChatId, "chat-1", "msg-1")
|
|
462
|
+
expect(event.type).toBe("queued_message_removed")
|
|
463
|
+
if (event.type === "queued_message_removed") {
|
|
464
|
+
expect(event.queuedMessageId).toBe("msg-1")
|
|
465
|
+
}
|
|
466
|
+
})
|
|
467
|
+
|
|
468
|
+
test("throws if message not found", () => {
|
|
469
|
+
const chatsById = new Map([["chat-1", makeChat()]])
|
|
470
|
+
const empty = new Map<string, { id: string; content: string; attachments: []; createdAt: number; provider: "claude"; model: string }[]>()
|
|
471
|
+
expect(() => buildRemoveQueuedMessageEvent(chatsById, empty, "chat-1", "missing")).toThrow()
|
|
472
|
+
})
|
|
473
|
+
})
|
|
474
|
+
|
|
475
|
+
// ---------------------------------------------------------------------------
|
|
476
|
+
// Turn builders
|
|
477
|
+
// ---------------------------------------------------------------------------
|
|
478
|
+
|
|
479
|
+
describe("buildTurnStartedEvent", () => {
|
|
480
|
+
test("builds turn_started", () => {
|
|
481
|
+
const chatsById = new Map([["chat-1", makeChat()]])
|
|
482
|
+
expect(buildTurnStartedEvent(chatsById, "chat-1").type).toBe("turn_started")
|
|
483
|
+
})
|
|
484
|
+
|
|
485
|
+
test("throws if chat not found", () => {
|
|
486
|
+
expect(() => buildTurnStartedEvent(new Map(), "missing")).toThrow()
|
|
487
|
+
})
|
|
488
|
+
})
|
|
489
|
+
|
|
490
|
+
describe("buildTurnFinishedEvent", () => {
|
|
491
|
+
test("builds turn_finished", () => {
|
|
492
|
+
const chatsById = new Map([["chat-1", makeChat()]])
|
|
493
|
+
expect(buildTurnFinishedEvent(chatsById, "chat-1").type).toBe("turn_finished")
|
|
494
|
+
})
|
|
495
|
+
})
|
|
496
|
+
|
|
497
|
+
describe("buildTurnFailedEvent", () => {
|
|
498
|
+
test("builds turn_failed with error", () => {
|
|
499
|
+
const chatsById = new Map([["chat-1", makeChat()]])
|
|
500
|
+
const event = buildTurnFailedEvent(chatsById, "chat-1", "some error")
|
|
501
|
+
expect(event.type).toBe("turn_failed")
|
|
502
|
+
if (event.type === "turn_failed") {
|
|
503
|
+
expect(event.error).toBe("some error")
|
|
504
|
+
}
|
|
505
|
+
})
|
|
506
|
+
})
|
|
507
|
+
|
|
508
|
+
describe("buildTurnCancelledEvent", () => {
|
|
509
|
+
test("builds turn_cancelled", () => {
|
|
510
|
+
const chatsById = new Map([["chat-1", makeChat()]])
|
|
511
|
+
expect(buildTurnCancelledEvent(chatsById, "chat-1").type).toBe("turn_cancelled")
|
|
512
|
+
})
|
|
513
|
+
})
|
|
514
|
+
|
|
515
|
+
describe("buildPendingForkSessionTokenEvent", () => {
|
|
516
|
+
test("returns null if token unchanged (both null)", () => {
|
|
517
|
+
const chatsById = new Map([["chat-1", makeChat({ pendingForkSessionToken: null })]])
|
|
518
|
+
expect(buildPendingForkSessionTokenEvent(chatsById, "chat-1", null)).toBeNull()
|
|
519
|
+
})
|
|
520
|
+
|
|
521
|
+
test("returns event when setting a token", () => {
|
|
522
|
+
const chatsById = new Map([["chat-1", makeChat({ pendingForkSessionToken: null })]])
|
|
523
|
+
const event = buildPendingForkSessionTokenEvent(chatsById, "chat-1", { provider: "claude", token: "tok-abc" })
|
|
524
|
+
expect(event?.type).toBe("pending_fork_session_token_set")
|
|
525
|
+
})
|
|
526
|
+
})
|
|
527
|
+
|
|
528
|
+
// ---------------------------------------------------------------------------
|
|
529
|
+
// Tool-request builders
|
|
530
|
+
// ---------------------------------------------------------------------------
|
|
531
|
+
|
|
532
|
+
describe("buildPutToolRequestEvent", () => {
|
|
533
|
+
test("builds tool_request_put", () => {
|
|
534
|
+
const event = buildPutToolRequestEvent(makeToolRequest())
|
|
535
|
+
expect(event.type).toBe("tool_request_put")
|
|
536
|
+
})
|
|
537
|
+
})
|
|
538
|
+
|
|
539
|
+
describe("buildResolveToolRequestEvent", () => {
|
|
540
|
+
test("builds tool_request_resolved", () => {
|
|
541
|
+
const toolRequestsById = new Map([["req-1", makeToolRequest()]])
|
|
542
|
+
const event = buildResolveToolRequestEvent(toolRequestsById, "req-1", {
|
|
543
|
+
status: "answered",
|
|
544
|
+
resolvedAt: TS,
|
|
545
|
+
})
|
|
546
|
+
expect(event.type).toBe("tool_request_resolved")
|
|
547
|
+
})
|
|
548
|
+
|
|
549
|
+
test("throws if id unknown", () => {
|
|
550
|
+
expect(() =>
|
|
551
|
+
buildResolveToolRequestEvent(new Map(), "unknown", { status: "answered", resolvedAt: TS }),
|
|
552
|
+
).toThrow()
|
|
553
|
+
})
|
|
554
|
+
})
|