@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,597 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test"
|
|
2
|
+
import type { StorageBackend } from "./storage/backend"
|
|
3
|
+
import type { SnapshotFile } from "./events"
|
|
4
|
+
import { createEmptyState } from "./events"
|
|
5
|
+
import { STORE_VERSION } from "../shared/types"
|
|
6
|
+
import {
|
|
7
|
+
buildSnapshotFile,
|
|
8
|
+
calcShouldTruncateLogs,
|
|
9
|
+
computeLegacyTranscriptStats,
|
|
10
|
+
loadAndReplayLogs,
|
|
11
|
+
loadSidebarOrder,
|
|
12
|
+
loadSnapshotIntoState,
|
|
13
|
+
migrateLegacyTranscripts,
|
|
14
|
+
readSidebarOrderFromProjectsLog,
|
|
15
|
+
truncateLogsAfterSnapshot,
|
|
16
|
+
writeSidebarOrderFile,
|
|
17
|
+
type LegacyTranscriptStats,
|
|
18
|
+
type SnapshotLogPaths,
|
|
19
|
+
} from "./event-store-snapshot"
|
|
20
|
+
import type { TranscriptEntry } from "../shared/types"
|
|
21
|
+
import type { StoreEvent } from "./events"
|
|
22
|
+
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
// Mock StorageBackend
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
|
|
27
|
+
function makeStorage(initial: Record<string, string> = {}): StorageBackend & {
|
|
28
|
+
written: Map<string, string>
|
|
29
|
+
appended: Map<string, string[]>
|
|
30
|
+
renamed: Array<[string, string]>
|
|
31
|
+
} {
|
|
32
|
+
const files = new Map<string, string>(Object.entries(initial))
|
|
33
|
+
const written = new Map<string, string>()
|
|
34
|
+
const appended = new Map<string, string[]>()
|
|
35
|
+
const renamed: Array<[string, string]> = []
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
written,
|
|
39
|
+
appended,
|
|
40
|
+
renamed,
|
|
41
|
+
mkdir: async () => {},
|
|
42
|
+
exists: async (p) => files.has(p),
|
|
43
|
+
existsSync: (p) => files.has(p),
|
|
44
|
+
size: async (p) => {
|
|
45
|
+
const content = files.get(p)
|
|
46
|
+
return content ? content.length : 0
|
|
47
|
+
},
|
|
48
|
+
readText: async (p) => {
|
|
49
|
+
const content = files.get(p)
|
|
50
|
+
if (content === undefined) throw new Error(`File not found: ${p}`)
|
|
51
|
+
return content
|
|
52
|
+
},
|
|
53
|
+
readTextSync: (p) => {
|
|
54
|
+
const content = files.get(p)
|
|
55
|
+
if (content === undefined) throw new Error(`File not found: ${p}`)
|
|
56
|
+
return content
|
|
57
|
+
},
|
|
58
|
+
writeText: async (p, content) => {
|
|
59
|
+
files.set(p, content)
|
|
60
|
+
written.set(p, content)
|
|
61
|
+
},
|
|
62
|
+
appendText: async (p, content) => {
|
|
63
|
+
const existing = files.get(p) ?? ""
|
|
64
|
+
files.set(p, existing + content)
|
|
65
|
+
const list = appended.get(p) ?? []
|
|
66
|
+
list.push(content)
|
|
67
|
+
appended.set(p, list)
|
|
68
|
+
},
|
|
69
|
+
rename: async (from, to) => {
|
|
70
|
+
const content = files.get(from)
|
|
71
|
+
if (content !== undefined) {
|
|
72
|
+
files.set(to, content)
|
|
73
|
+
files.delete(from)
|
|
74
|
+
}
|
|
75
|
+
renamed.push([from, to])
|
|
76
|
+
},
|
|
77
|
+
remove: async (p) => { files.delete(p) },
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const PATHS: SnapshotLogPaths = {
|
|
82
|
+
snapshotPath: "/data/snapshot.json",
|
|
83
|
+
projectsLogPath: "/data/projects.jsonl",
|
|
84
|
+
chatsLogPath: "/data/chats.jsonl",
|
|
85
|
+
messagesLogPath: "/data/messages.jsonl",
|
|
86
|
+
queuedMessagesLogPath: "/data/queued-messages.jsonl",
|
|
87
|
+
turnsLogPath: "/data/turns.jsonl",
|
|
88
|
+
schedulesLogPath: "/data/schedules.jsonl",
|
|
89
|
+
stacksLogPath: "/data/stacks.jsonl",
|
|
90
|
+
toolRequestsLogPath: "/data/tool-requests.jsonl",
|
|
91
|
+
orchLogPath: "/data/orch.jsonl",
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ---------------------------------------------------------------------------
|
|
95
|
+
// loadSnapshotIntoState
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
describe("loadSnapshotIntoState", () => {
|
|
98
|
+
test("returns empty result when snapshot file does not exist", async () => {
|
|
99
|
+
const storage = makeStorage()
|
|
100
|
+
const state = createEmptyState()
|
|
101
|
+
const legacyMessages = new Map<string, TranscriptEntry[]>()
|
|
102
|
+
let clearCalled = false
|
|
103
|
+
const result = await loadSnapshotIntoState(
|
|
104
|
+
storage, "/data/snapshot.json", state, legacyMessages,
|
|
105
|
+
async () => { clearCalled = true },
|
|
106
|
+
)
|
|
107
|
+
expect(result).toEqual({ snapshotHasLegacyMessages: false, legacySidebarProjectOrder: [] })
|
|
108
|
+
expect(clearCalled).toBe(false)
|
|
109
|
+
expect(state.projectsById.size).toBe(0)
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
test("returns empty result when snapshot file is empty", async () => {
|
|
113
|
+
const storage = makeStorage({ "/data/snapshot.json": " " })
|
|
114
|
+
const state = createEmptyState()
|
|
115
|
+
const result = await loadSnapshotIntoState(
|
|
116
|
+
storage, "/data/snapshot.json", state, new Map(), async () => {},
|
|
117
|
+
)
|
|
118
|
+
expect(result).toEqual({ snapshotHasLegacyMessages: false, legacySidebarProjectOrder: [] })
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
test("calls clearStorage and returns empty when version mismatches", async () => {
|
|
122
|
+
const snapshot = { v: 999, generatedAt: 0, projects: [], chats: [] } as unknown as SnapshotFile
|
|
123
|
+
const storage = makeStorage({ "/data/snapshot.json": JSON.stringify(snapshot) })
|
|
124
|
+
const state = createEmptyState()
|
|
125
|
+
let clearCalled = false
|
|
126
|
+
const result = await loadSnapshotIntoState(
|
|
127
|
+
storage, "/data/snapshot.json", state, new Map(),
|
|
128
|
+
async () => { clearCalled = true },
|
|
129
|
+
)
|
|
130
|
+
expect(clearCalled).toBe(true)
|
|
131
|
+
expect(result).toEqual({ snapshotHasLegacyMessages: false, legacySidebarProjectOrder: [] })
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
test("populates state.projectsById and projectIdsByPath from snapshot", async () => {
|
|
135
|
+
const snapshot: SnapshotFile = {
|
|
136
|
+
v: STORE_VERSION,
|
|
137
|
+
generatedAt: 100,
|
|
138
|
+
projects: [{
|
|
139
|
+
id: "proj-1",
|
|
140
|
+
localPath: "/home/user/project",
|
|
141
|
+
title: "My Project",
|
|
142
|
+
createdAt: 1,
|
|
143
|
+
updatedAt: 1,
|
|
144
|
+
}],
|
|
145
|
+
chats: [],
|
|
146
|
+
}
|
|
147
|
+
const storage = makeStorage({ "/data/snapshot.json": JSON.stringify(snapshot) })
|
|
148
|
+
const state = createEmptyState()
|
|
149
|
+
await loadSnapshotIntoState(storage, "/data/snapshot.json", state, new Map(), async () => {})
|
|
150
|
+
expect(state.projectsById.has("proj-1")).toBe(true)
|
|
151
|
+
expect(state.projectIdsByPath.get("/home/user/project")).toBe("proj-1")
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
test("populates chatsById from snapshot", async () => {
|
|
155
|
+
const snapshot: SnapshotFile = {
|
|
156
|
+
v: STORE_VERSION,
|
|
157
|
+
generatedAt: 100,
|
|
158
|
+
projects: [],
|
|
159
|
+
chats: [{
|
|
160
|
+
id: "chat-1",
|
|
161
|
+
projectId: "proj-1",
|
|
162
|
+
title: "Test Chat",
|
|
163
|
+
createdAt: 1,
|
|
164
|
+
updatedAt: 1,
|
|
165
|
+
unread: false,
|
|
166
|
+
provider: null,
|
|
167
|
+
planMode: false,
|
|
168
|
+
sourceHash: null,
|
|
169
|
+
lastTurnOutcome: null,
|
|
170
|
+
sessionTokensByProvider: {},
|
|
171
|
+
pendingForkSessionToken: null,
|
|
172
|
+
}],
|
|
173
|
+
}
|
|
174
|
+
const storage = makeStorage({ "/data/snapshot.json": JSON.stringify(snapshot) })
|
|
175
|
+
const state = createEmptyState()
|
|
176
|
+
await loadSnapshotIntoState(storage, "/data/snapshot.json", state, new Map(), async () => {})
|
|
177
|
+
expect(state.chatsById.has("chat-1")).toBe(true)
|
|
178
|
+
expect(state.chatsById.get("chat-1")?.unread).toBe(false)
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
test("sets snapshotHasLegacyMessages when messages field present", async () => {
|
|
182
|
+
const snapshot: SnapshotFile = {
|
|
183
|
+
v: STORE_VERSION,
|
|
184
|
+
generatedAt: 100,
|
|
185
|
+
projects: [],
|
|
186
|
+
chats: [],
|
|
187
|
+
messages: [{ chatId: "c1", entries: [] }],
|
|
188
|
+
}
|
|
189
|
+
const storage = makeStorage({ "/data/snapshot.json": JSON.stringify(snapshot) })
|
|
190
|
+
const legacyMessages = new Map<string, TranscriptEntry[]>()
|
|
191
|
+
const result = await loadSnapshotIntoState(
|
|
192
|
+
storage, "/data/snapshot.json", createEmptyState(), legacyMessages, async () => {},
|
|
193
|
+
)
|
|
194
|
+
expect(result.snapshotHasLegacyMessages).toBe(true)
|
|
195
|
+
expect(legacyMessages.has("c1")).toBe(true)
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
test("returns legacySidebarProjectOrder from snapshot", async () => {
|
|
199
|
+
const snapshot: SnapshotFile = {
|
|
200
|
+
v: STORE_VERSION,
|
|
201
|
+
generatedAt: 100,
|
|
202
|
+
projects: [],
|
|
203
|
+
chats: [],
|
|
204
|
+
sidebarProjectOrder: ["proj-2", "proj-1"],
|
|
205
|
+
}
|
|
206
|
+
const storage = makeStorage({ "/data/snapshot.json": JSON.stringify(snapshot) })
|
|
207
|
+
const result = await loadSnapshotIntoState(
|
|
208
|
+
storage, "/data/snapshot.json", createEmptyState(), new Map(), async () => {},
|
|
209
|
+
)
|
|
210
|
+
expect(result.legacySidebarProjectOrder).toEqual(["proj-2", "proj-1"])
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
test("calls clearStorage and returns empty on JSON parse error", async () => {
|
|
214
|
+
const storage = makeStorage({ "/data/snapshot.json": "{ invalid json" })
|
|
215
|
+
const state = createEmptyState()
|
|
216
|
+
let clearCalled = false
|
|
217
|
+
const result = await loadSnapshotIntoState(
|
|
218
|
+
storage, "/data/snapshot.json", state, new Map(),
|
|
219
|
+
async () => { clearCalled = true },
|
|
220
|
+
)
|
|
221
|
+
expect(clearCalled).toBe(true)
|
|
222
|
+
expect(result).toEqual({ snapshotHasLegacyMessages: false, legacySidebarProjectOrder: [] })
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
test("migrates legacy sessionToken to sessionTokensByProvider", async () => {
|
|
226
|
+
const legacyChat = {
|
|
227
|
+
id: "chat-1",
|
|
228
|
+
projectId: "proj-1",
|
|
229
|
+
title: "T",
|
|
230
|
+
createdAt: 1,
|
|
231
|
+
updatedAt: 1,
|
|
232
|
+
unread: false,
|
|
233
|
+
provider: "claude" as const,
|
|
234
|
+
sessionToken: "tok-123",
|
|
235
|
+
sessionTokensByProvider: undefined,
|
|
236
|
+
pendingForkSessionToken: null,
|
|
237
|
+
}
|
|
238
|
+
const snapshot = { v: STORE_VERSION, generatedAt: 1, projects: [], chats: [legacyChat] }
|
|
239
|
+
const storage = makeStorage({ "/data/snapshot.json": JSON.stringify(snapshot) })
|
|
240
|
+
const state = createEmptyState()
|
|
241
|
+
await loadSnapshotIntoState(storage, "/data/snapshot.json", state, new Map(), async () => {})
|
|
242
|
+
const chat = state.chatsById.get("chat-1")
|
|
243
|
+
expect(chat?.sessionTokensByProvider?.claude).toBe("tok-123")
|
|
244
|
+
})
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
// ---------------------------------------------------------------------------
|
|
248
|
+
// buildSnapshotFile
|
|
249
|
+
// ---------------------------------------------------------------------------
|
|
250
|
+
describe("buildSnapshotFile", () => {
|
|
251
|
+
test("returns snapshot with correct version and empty arrays", () => {
|
|
252
|
+
const state = createEmptyState()
|
|
253
|
+
const result = buildSnapshotFile(state, [])
|
|
254
|
+
expect(result.v).toBe(STORE_VERSION)
|
|
255
|
+
expect(result.projects).toEqual([])
|
|
256
|
+
expect(result.chats).toEqual([])
|
|
257
|
+
expect(result.queuedMessages).toEqual([])
|
|
258
|
+
expect(result.autoContinueEvents).toEqual([])
|
|
259
|
+
expect(result.stacks).toEqual([])
|
|
260
|
+
})
|
|
261
|
+
|
|
262
|
+
test("includes only non-deleted chats", () => {
|
|
263
|
+
const state = createEmptyState()
|
|
264
|
+
state.chatsById.set("c1", {
|
|
265
|
+
id: "c1", projectId: "p1", title: "A", createdAt: 1, updatedAt: 1, unread: false,
|
|
266
|
+
provider: null, planMode: false, sourceHash: null, lastTurnOutcome: null,
|
|
267
|
+
sessionTokensByProvider: {}, pendingForkSessionToken: null,
|
|
268
|
+
})
|
|
269
|
+
state.chatsById.set("c2", {
|
|
270
|
+
id: "c2", projectId: "p1", title: "B", createdAt: 1, updatedAt: 1, unread: false,
|
|
271
|
+
provider: null, planMode: false, sourceHash: null, lastTurnOutcome: null,
|
|
272
|
+
sessionTokensByProvider: {}, pendingForkSessionToken: null,
|
|
273
|
+
deletedAt: 99,
|
|
274
|
+
})
|
|
275
|
+
const result = buildSnapshotFile(state, [])
|
|
276
|
+
expect(result.chats.map((c) => c.id)).toEqual(["c1"])
|
|
277
|
+
})
|
|
278
|
+
|
|
279
|
+
test("serializes stacks correctly", () => {
|
|
280
|
+
const state = createEmptyState()
|
|
281
|
+
state.stacksById.set("stack-1", {
|
|
282
|
+
id: "stack-1",
|
|
283
|
+
title: "My Stack",
|
|
284
|
+
projectIds: ["p1", "p2"],
|
|
285
|
+
createdAt: 1,
|
|
286
|
+
updatedAt: 1,
|
|
287
|
+
})
|
|
288
|
+
const result = buildSnapshotFile(state, [])
|
|
289
|
+
expect(result.stacks).toHaveLength(1)
|
|
290
|
+
expect(result.stacks![0].projectIds).toEqual(["p1", "p2"])
|
|
291
|
+
})
|
|
292
|
+
})
|
|
293
|
+
|
|
294
|
+
// ---------------------------------------------------------------------------
|
|
295
|
+
// truncateLogsAfterSnapshot
|
|
296
|
+
// ---------------------------------------------------------------------------
|
|
297
|
+
describe("truncateLogsAfterSnapshot", () => {
|
|
298
|
+
test("writes snapshot JSON and clears all compactable log files", async () => {
|
|
299
|
+
const storage = makeStorage({
|
|
300
|
+
"/data/snapshot.json": "old",
|
|
301
|
+
"/data/projects.jsonl": "data",
|
|
302
|
+
"/data/chats.jsonl": "data",
|
|
303
|
+
"/data/messages.jsonl": "data",
|
|
304
|
+
"/data/queued-messages.jsonl": "data",
|
|
305
|
+
"/data/turns.jsonl": "data",
|
|
306
|
+
"/data/schedules.jsonl": "data",
|
|
307
|
+
"/data/stacks.jsonl": "data",
|
|
308
|
+
"/data/tool-requests.jsonl": "data",
|
|
309
|
+
})
|
|
310
|
+
await truncateLogsAfterSnapshot(storage, PATHS, '{"v":3}')
|
|
311
|
+
expect(storage.written.get("/data/snapshot.json")).toBe('{"v":3}')
|
|
312
|
+
expect(storage.written.get("/data/projects.jsonl")).toBe("")
|
|
313
|
+
expect(storage.written.get("/data/chats.jsonl")).toBe("")
|
|
314
|
+
expect(storage.written.get("/data/messages.jsonl")).toBe("")
|
|
315
|
+
expect(storage.written.get("/data/tool-requests.jsonl")).toBe("")
|
|
316
|
+
})
|
|
317
|
+
})
|
|
318
|
+
|
|
319
|
+
// ---------------------------------------------------------------------------
|
|
320
|
+
// calcShouldTruncateLogs
|
|
321
|
+
// ---------------------------------------------------------------------------
|
|
322
|
+
describe("calcShouldTruncateLogs", () => {
|
|
323
|
+
test("returns false when combined size is below threshold", async () => {
|
|
324
|
+
const storage = makeStorage(
|
|
325
|
+
Object.fromEntries(Object.values(PATHS).map((p) => [p, "small"])),
|
|
326
|
+
)
|
|
327
|
+
const result = await calcShouldTruncateLogs(storage, PATHS)
|
|
328
|
+
expect(result).toBe(false)
|
|
329
|
+
})
|
|
330
|
+
|
|
331
|
+
test("returns true when combined size meets 2 MiB threshold", async () => {
|
|
332
|
+
// 2 MiB spread across files: put all in the projects log
|
|
333
|
+
const bigData = "x".repeat(2 * 1024 * 1024)
|
|
334
|
+
const storage = makeStorage({ "/data/projects.jsonl": bigData })
|
|
335
|
+
const result = await calcShouldTruncateLogs(storage, PATHS)
|
|
336
|
+
expect(result).toBe(true)
|
|
337
|
+
})
|
|
338
|
+
})
|
|
339
|
+
|
|
340
|
+
// ---------------------------------------------------------------------------
|
|
341
|
+
// readSidebarOrderFromProjectsLog
|
|
342
|
+
// ---------------------------------------------------------------------------
|
|
343
|
+
describe("readSidebarOrderFromProjectsLog", () => {
|
|
344
|
+
test("returns empty array if file does not exist", async () => {
|
|
345
|
+
const storage = makeStorage()
|
|
346
|
+
const result = await readSidebarOrderFromProjectsLog(storage, "/data/projects.jsonl")
|
|
347
|
+
expect(result).toEqual([])
|
|
348
|
+
})
|
|
349
|
+
|
|
350
|
+
test("returns empty array if file is empty", async () => {
|
|
351
|
+
const storage = makeStorage({ "/data/projects.jsonl": "" })
|
|
352
|
+
const result = await readSidebarOrderFromProjectsLog(storage, "/data/projects.jsonl")
|
|
353
|
+
expect(result).toEqual([])
|
|
354
|
+
})
|
|
355
|
+
|
|
356
|
+
test("extracts projectIds from sidebar_project_order_set events", async () => {
|
|
357
|
+
const event = JSON.stringify({
|
|
358
|
+
v: STORE_VERSION, type: "sidebar_project_order_set",
|
|
359
|
+
timestamp: 1, projectIds: ["p2", "p1"],
|
|
360
|
+
})
|
|
361
|
+
const storage = makeStorage({ "/data/projects.jsonl": `${event}\n` })
|
|
362
|
+
const result = await readSidebarOrderFromProjectsLog(storage, "/data/projects.jsonl")
|
|
363
|
+
expect(result).toEqual(["p2", "p1"])
|
|
364
|
+
})
|
|
365
|
+
|
|
366
|
+
test("returns last seen order if multiple events present", async () => {
|
|
367
|
+
const e1 = JSON.stringify({ v: STORE_VERSION, type: "sidebar_project_order_set", timestamp: 1, projectIds: ["p1"] })
|
|
368
|
+
const e2 = JSON.stringify({ v: STORE_VERSION, type: "sidebar_project_order_set", timestamp: 2, projectIds: ["p2", "p1"] })
|
|
369
|
+
const storage = makeStorage({ "/data/projects.jsonl": `${e1}\n${e2}\n` })
|
|
370
|
+
const result = await readSidebarOrderFromProjectsLog(storage, "/data/projects.jsonl")
|
|
371
|
+
expect(result).toEqual(["p2", "p1"])
|
|
372
|
+
})
|
|
373
|
+
|
|
374
|
+
test("skips non-sidebar events", async () => {
|
|
375
|
+
const projectEvent = JSON.stringify({ v: STORE_VERSION, type: "project_opened", timestamp: 1, projectId: "p1", localPath: "/", title: "t" })
|
|
376
|
+
const sidebarEvent = JSON.stringify({ v: STORE_VERSION, type: "sidebar_project_order_set", timestamp: 2, projectIds: ["p1"] })
|
|
377
|
+
const storage = makeStorage({ "/data/projects.jsonl": `${projectEvent}\n${sidebarEvent}\n` })
|
|
378
|
+
const result = await readSidebarOrderFromProjectsLog(storage, "/data/projects.jsonl")
|
|
379
|
+
expect(result).toEqual(["p1"])
|
|
380
|
+
})
|
|
381
|
+
})
|
|
382
|
+
|
|
383
|
+
// ---------------------------------------------------------------------------
|
|
384
|
+
// writeSidebarOrderFile
|
|
385
|
+
// ---------------------------------------------------------------------------
|
|
386
|
+
describe("writeSidebarOrderFile", () => {
|
|
387
|
+
test("writes projectIds as JSON to the sidebar order file", async () => {
|
|
388
|
+
const storage = makeStorage()
|
|
389
|
+
await writeSidebarOrderFile(storage, "/data", "/data/sidebar-order.json", ["p1", "p2"])
|
|
390
|
+
const written = storage.written.get("/data/sidebar-order.json")
|
|
391
|
+
expect(written).toBeDefined()
|
|
392
|
+
const parsed = JSON.parse(written!)
|
|
393
|
+
expect(parsed).toEqual(["p1", "p2"])
|
|
394
|
+
})
|
|
395
|
+
})
|
|
396
|
+
|
|
397
|
+
// ---------------------------------------------------------------------------
|
|
398
|
+
// loadSidebarOrder
|
|
399
|
+
// ---------------------------------------------------------------------------
|
|
400
|
+
describe("loadSidebarOrder", () => {
|
|
401
|
+
test("reads from dedicated sidebar-order.json when it exists", async () => {
|
|
402
|
+
const storage = makeStorage({
|
|
403
|
+
"/data/sidebar-order.json": JSON.stringify(["p3", "p1", "p2"]),
|
|
404
|
+
})
|
|
405
|
+
const result = await loadSidebarOrder(storage, "/data/sidebar-order.json", "/data/projects.jsonl", "/data", [])
|
|
406
|
+
expect(result).toEqual(["p3", "p1", "p2"])
|
|
407
|
+
})
|
|
408
|
+
|
|
409
|
+
test("returns empty array if sidebar-order.json is blank", async () => {
|
|
410
|
+
const storage = makeStorage({ "/data/sidebar-order.json": " " })
|
|
411
|
+
const result = await loadSidebarOrder(storage, "/data/sidebar-order.json", "/data/projects.jsonl", "/data", [])
|
|
412
|
+
expect(result).toEqual([])
|
|
413
|
+
})
|
|
414
|
+
|
|
415
|
+
test("migrates from projects.jsonl when sidebar-order.json absent", async () => {
|
|
416
|
+
const sidebarEvent = JSON.stringify({
|
|
417
|
+
v: STORE_VERSION, type: "sidebar_project_order_set", timestamp: 1, projectIds: ["p2", "p1"],
|
|
418
|
+
})
|
|
419
|
+
const storage = makeStorage({ "/data/projects.jsonl": `${sidebarEvent}\n` })
|
|
420
|
+
const result = await loadSidebarOrder(storage, "/data/sidebar-order.json", "/data/projects.jsonl", "/data", [])
|
|
421
|
+
expect(result).toEqual(["p2", "p1"])
|
|
422
|
+
// Should have written the sidebar-order.json file as a migration
|
|
423
|
+
expect(storage.written.has("/data/sidebar-order.json")).toBe(true)
|
|
424
|
+
})
|
|
425
|
+
|
|
426
|
+
test("falls back to legacySidebarProjectOrder when no file or log events", async () => {
|
|
427
|
+
const storage = makeStorage()
|
|
428
|
+
const result = await loadSidebarOrder(storage, "/data/sidebar-order.json", "/data/projects.jsonl", "/data", ["p4"])
|
|
429
|
+
expect(result).toEqual(["p4"])
|
|
430
|
+
})
|
|
431
|
+
})
|
|
432
|
+
|
|
433
|
+
// ---------------------------------------------------------------------------
|
|
434
|
+
// computeLegacyTranscriptStats
|
|
435
|
+
// ---------------------------------------------------------------------------
|
|
436
|
+
describe("computeLegacyTranscriptStats", () => {
|
|
437
|
+
test("returns hasLegacyData false when nothing present", async () => {
|
|
438
|
+
const storage = makeStorage({ "/data/messages.jsonl": "" })
|
|
439
|
+
const result = await computeLegacyTranscriptStats(storage, "/data/messages.jsonl", false, new Map())
|
|
440
|
+
expect(result.hasLegacyData).toBe(false)
|
|
441
|
+
expect(result.sources).toEqual([])
|
|
442
|
+
})
|
|
443
|
+
|
|
444
|
+
test("includes snapshot source when snapshotHasLegacyMessages is true", async () => {
|
|
445
|
+
const storage = makeStorage({ "/data/messages.jsonl": "" })
|
|
446
|
+
const result = await computeLegacyTranscriptStats(storage, "/data/messages.jsonl", true, new Map())
|
|
447
|
+
expect(result.hasLegacyData).toBe(true)
|
|
448
|
+
expect(result.sources).toContain("snapshot")
|
|
449
|
+
})
|
|
450
|
+
|
|
451
|
+
test("includes messages_log source when messages.jsonl is non-empty", async () => {
|
|
452
|
+
const storage = makeStorage({ "/data/messages.jsonl": "some content" })
|
|
453
|
+
const result = await computeLegacyTranscriptStats(storage, "/data/messages.jsonl", false, new Map())
|
|
454
|
+
expect(result.hasLegacyData).toBe(true)
|
|
455
|
+
expect(result.sources).toContain("messages_log")
|
|
456
|
+
})
|
|
457
|
+
|
|
458
|
+
test("counts entries and chats from legacyMessages map", async () => {
|
|
459
|
+
const storage = makeStorage({ "/data/messages.jsonl": "" })
|
|
460
|
+
const legacyMessages = new Map<string, TranscriptEntry[]>([
|
|
461
|
+
["c1", [{ _id: "a", createdAt: 1, kind: "user_prompt", content: "" }]],
|
|
462
|
+
["c2", [
|
|
463
|
+
{ _id: "b", createdAt: 2, kind: "user_prompt", content: "" },
|
|
464
|
+
{ _id: "c", createdAt: 3, kind: "user_prompt", content: "" },
|
|
465
|
+
]],
|
|
466
|
+
])
|
|
467
|
+
const result = await computeLegacyTranscriptStats(storage, "/data/messages.jsonl", false, legacyMessages)
|
|
468
|
+
expect(result.chatCount).toBe(2)
|
|
469
|
+
expect(result.entryCount).toBe(3)
|
|
470
|
+
expect(result.hasLegacyData).toBe(true)
|
|
471
|
+
})
|
|
472
|
+
})
|
|
473
|
+
|
|
474
|
+
// ---------------------------------------------------------------------------
|
|
475
|
+
// migrateLegacyTranscripts
|
|
476
|
+
// ---------------------------------------------------------------------------
|
|
477
|
+
describe("migrateLegacyTranscripts", () => {
|
|
478
|
+
test("returns false immediately when no legacy data", async () => {
|
|
479
|
+
const storage = makeStorage()
|
|
480
|
+
const stats: LegacyTranscriptStats = { hasLegacyData: false, sources: [], chatCount: 0, entryCount: 0 }
|
|
481
|
+
const result = await migrateLegacyTranscripts(
|
|
482
|
+
storage, "/data/transcripts", stats,
|
|
483
|
+
new Map(), (_id) => `/data/transcripts/${_id}.jsonl`,
|
|
484
|
+
() => {}, async () => {}, () => {},
|
|
485
|
+
)
|
|
486
|
+
expect(result).toBe(false)
|
|
487
|
+
})
|
|
488
|
+
|
|
489
|
+
test("writes per-chat transcript files and calls callbacks on success", async () => {
|
|
490
|
+
const storage = makeStorage()
|
|
491
|
+
const legacyMessages = new Map<string, TranscriptEntry[]>([
|
|
492
|
+
["chat-1", [{ _id: "e1", createdAt: 1, kind: "user_prompt", content: "hello" }]],
|
|
493
|
+
])
|
|
494
|
+
const stats: LegacyTranscriptStats = {
|
|
495
|
+
hasLegacyData: true,
|
|
496
|
+
sources: ["snapshot"],
|
|
497
|
+
chatCount: 1,
|
|
498
|
+
entryCount: 1,
|
|
499
|
+
}
|
|
500
|
+
let clearCalled = false
|
|
501
|
+
let snapshotCalled = false
|
|
502
|
+
let cacheCalled = false
|
|
503
|
+
const messages: string[] = []
|
|
504
|
+
|
|
505
|
+
const result = await migrateLegacyTranscripts(
|
|
506
|
+
storage,
|
|
507
|
+
"/data/transcripts",
|
|
508
|
+
stats,
|
|
509
|
+
legacyMessages,
|
|
510
|
+
(id) => `/data/transcripts/${id}.jsonl`,
|
|
511
|
+
() => { clearCalled = true },
|
|
512
|
+
async () => { snapshotCalled = true },
|
|
513
|
+
() => { cacheCalled = true },
|
|
514
|
+
(msg) => messages.push(msg),
|
|
515
|
+
)
|
|
516
|
+
|
|
517
|
+
expect(result).toBe(true)
|
|
518
|
+
expect(clearCalled).toBe(true)
|
|
519
|
+
expect(snapshotCalled).toBe(true)
|
|
520
|
+
expect(cacheCalled).toBe(true)
|
|
521
|
+
|
|
522
|
+
// The renamed file (tmp→final) should exist
|
|
523
|
+
expect(storage.renamed).toHaveLength(1)
|
|
524
|
+
expect(storage.renamed[0][1]).toBe("/data/transcripts/chat-1.jsonl")
|
|
525
|
+
expect(messages.some((m) => m.includes("migration complete"))).toBe(true)
|
|
526
|
+
})
|
|
527
|
+
})
|
|
528
|
+
|
|
529
|
+
// ---------------------------------------------------------------------------
|
|
530
|
+
// loadAndReplayLogs
|
|
531
|
+
// ---------------------------------------------------------------------------
|
|
532
|
+
describe("loadAndReplayLogs", () => {
|
|
533
|
+
test("exits early when storageReset is true before loading", async () => {
|
|
534
|
+
const storage = makeStorage()
|
|
535
|
+
let applied = 0
|
|
536
|
+
await loadAndReplayLogs(
|
|
537
|
+
storage, PATHS,
|
|
538
|
+
() => true,
|
|
539
|
+
() => { applied++ },
|
|
540
|
+
async () => {},
|
|
541
|
+
() => {},
|
|
542
|
+
)
|
|
543
|
+
expect(applied).toBe(0)
|
|
544
|
+
})
|
|
545
|
+
|
|
546
|
+
test("applies events from multiple log files in timestamp order", async () => {
|
|
547
|
+
const e1 = JSON.stringify({ v: STORE_VERSION, type: "project_opened", timestamp: 200, projectId: "p1", localPath: "/", title: "T" })
|
|
548
|
+
const e2 = JSON.stringify({ v: STORE_VERSION, type: "project_opened", timestamp: 100, projectId: "p2", localPath: "/2", title: "T2" })
|
|
549
|
+
const storage = makeStorage({
|
|
550
|
+
"/data/projects.jsonl": `${e1}\n`,
|
|
551
|
+
"/data/stacks.jsonl": `${e2}\n`,
|
|
552
|
+
})
|
|
553
|
+
|
|
554
|
+
const applied: StoreEvent[] = []
|
|
555
|
+
await loadAndReplayLogs(
|
|
556
|
+
storage, PATHS,
|
|
557
|
+
() => false,
|
|
558
|
+
(event) => { applied.push(event) },
|
|
559
|
+
async () => {},
|
|
560
|
+
() => {},
|
|
561
|
+
)
|
|
562
|
+
|
|
563
|
+
// Should be sorted by timestamp: p2 (100) before p1 (200)
|
|
564
|
+
expect(applied).toHaveLength(2)
|
|
565
|
+
const first = applied[0] as Extract<typeof applied[0], { type: "project_opened" }>
|
|
566
|
+
expect(first.type).toBe("project_opened")
|
|
567
|
+
expect((first as { projectId?: string }).projectId).toBe("p2")
|
|
568
|
+
})
|
|
569
|
+
|
|
570
|
+
test("calls clearStorage and exits on version mismatch in a log file", async () => {
|
|
571
|
+
const badEvent = JSON.stringify({ v: 999, type: "project_opened", timestamp: 1 })
|
|
572
|
+
const storage = makeStorage({ "/data/projects.jsonl": `${badEvent}\n` })
|
|
573
|
+
let clearCalled = false
|
|
574
|
+
let storageReset = false
|
|
575
|
+
await loadAndReplayLogs(
|
|
576
|
+
storage, PATHS,
|
|
577
|
+
() => storageReset,
|
|
578
|
+
() => {},
|
|
579
|
+
async () => { clearCalled = true; storageReset = true },
|
|
580
|
+
() => {},
|
|
581
|
+
)
|
|
582
|
+
expect(clearCalled).toBe(true)
|
|
583
|
+
})
|
|
584
|
+
|
|
585
|
+
test("calls onReplayChatProviderClear after successful replay", async () => {
|
|
586
|
+
const storage = makeStorage()
|
|
587
|
+
let cleared = false
|
|
588
|
+
await loadAndReplayLogs(
|
|
589
|
+
storage, PATHS,
|
|
590
|
+
() => false,
|
|
591
|
+
() => {},
|
|
592
|
+
async () => {},
|
|
593
|
+
() => { cleared = true },
|
|
594
|
+
)
|
|
595
|
+
expect(cleared).toBe(true)
|
|
596
|
+
})
|
|
597
|
+
})
|