@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,279 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ws-router-chat.ts
|
|
3
|
+
*
|
|
4
|
+
* WS command handlers for chat lifecycle operations:
|
|
5
|
+
* chat.create, chat.fork, chat.rename, chat.archive, chat.unarchive,
|
|
6
|
+
* chat.delete, chat.markRead, chat.setPolicyOverride, chat.setDraftProtection,
|
|
7
|
+
* chat.send, chat.cancel, chat.stopDraining, chat.loadHistory,
|
|
8
|
+
* chat.respondTool, chat.toolRequestAnswer,
|
|
9
|
+
* chat.respondSubagentTool, chat.cancelSubagentRun
|
|
10
|
+
*
|
|
11
|
+
* Extracted from ws-router.ts.
|
|
12
|
+
*/
|
|
13
|
+
import { PROTOCOL_VERSION } from "../shared/types"
|
|
14
|
+
import type { ChatHistoryPage, StackBinding } from "../shared/types"
|
|
15
|
+
import { isRecord } from "../shared/errors"
|
|
16
|
+
import type { ClientCommand, ServerEnvelope } from "../shared/protocol"
|
|
17
|
+
import type { ChatPermissionPolicyOverride, ToolRequestDecision } from "../shared/permission-policy"
|
|
18
|
+
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
// Dep interfaces (duck-typed; avoids circular imports with ws-router.ts)
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
/** Subset of EventStore methods consumed by chat WS commands. */
|
|
24
|
+
export interface ChatStoreDep {
|
|
25
|
+
createChat(
|
|
26
|
+
projectId: string,
|
|
27
|
+
opts?: { stackId?: string | null; stackBindings?: StackBinding[] },
|
|
28
|
+
): Promise<{ id: string }>
|
|
29
|
+
renameChat(chatId: string, title: string): Promise<void>
|
|
30
|
+
archiveChat(chatId: string): Promise<void>
|
|
31
|
+
unarchiveChat(chatId: string): Promise<void>
|
|
32
|
+
deleteChat(chatId: string): Promise<void>
|
|
33
|
+
setChatReadState(chatId: string, unread: boolean): Promise<void>
|
|
34
|
+
setChatPolicyOverride(chatId: string, policyOverride: ChatPermissionPolicyOverride | null): Promise<void>
|
|
35
|
+
getChat(chatId: string): { id: string } | null | undefined
|
|
36
|
+
getMessagesPageBefore(chatId: string, beforeCursor: string, limit: number): ChatHistoryPage
|
|
37
|
+
getToolRequest(toolRequestId: string): { chatId: string } | undefined | null
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Subset of ToolCallbackService methods consumed by chat WS commands. */
|
|
41
|
+
export interface ChatToolCallbackServiceDep {
|
|
42
|
+
cancelAllForChat(chatId: string, reason: string): Promise<void>
|
|
43
|
+
answer(toolRequestId: string, decision: ToolRequestDecision): Promise<void>
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Subset of AgentCoordinator methods consumed by chat WS commands. */
|
|
47
|
+
export interface ChatAgentDep {
|
|
48
|
+
send(command: Extract<ClientCommand, { type: "chat.send" }>): Promise<{ chatId?: string | null }>
|
|
49
|
+
forkChat(chatId: string): Promise<unknown>
|
|
50
|
+
cancel(chatId: string): Promise<void>
|
|
51
|
+
cancelAutoContinue(chatId: string, scheduleId: string, reason: string): Promise<void>
|
|
52
|
+
listLiveSchedules(chatId: string): Iterable<string>
|
|
53
|
+
closeChat(chatId: string): Promise<void>
|
|
54
|
+
stopDraining(chatId: string): Promise<void>
|
|
55
|
+
respondTool(command: Extract<ClientCommand, { type: "chat.respondTool" }>): Promise<void>
|
|
56
|
+
respondSubagentTool(
|
|
57
|
+
command: Extract<ClientCommand, { type: "chat.respondSubagentTool" }>,
|
|
58
|
+
): Promise<void>
|
|
59
|
+
cancelSubagentRun(
|
|
60
|
+
command: Extract<ClientCommand, { type: "chat.cancelSubagentRun" }>,
|
|
61
|
+
): Promise<void>
|
|
62
|
+
getActiveTurnProfile(
|
|
63
|
+
chatId: string,
|
|
64
|
+
): { traceId?: string | null; startedAt?: number | null } | null | undefined
|
|
65
|
+
toolCallbackService?: ChatToolCallbackServiceDep | null
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Analytics subset consumed by chat WS commands. */
|
|
69
|
+
export interface ChatAnalyticsDep {
|
|
70
|
+
track(event: string): void
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Full deps bundle for chat WS commands. */
|
|
74
|
+
export interface ChatCommandDeps {
|
|
75
|
+
store: ChatStoreDep
|
|
76
|
+
agent: ChatAgentDep
|
|
77
|
+
analytics: ChatAnalyticsDep
|
|
78
|
+
/**
|
|
79
|
+
* Mutate per-connection draft protection — wraps
|
|
80
|
+
* `ws.data.protectedDraftChatIds = new Set(chatIds)` at the call site.
|
|
81
|
+
*/
|
|
82
|
+
setDraftProtection: (chatIds: string[]) => void
|
|
83
|
+
/**
|
|
84
|
+
* Send-to-starting profiling hook (no-op when profiling is disabled).
|
|
85
|
+
* Wraps `logSendToStartingProfile` at the call site.
|
|
86
|
+
*/
|
|
87
|
+
logSendProfilingFn: (
|
|
88
|
+
traceId: string | null | undefined,
|
|
89
|
+
startedAt: number | null | undefined,
|
|
90
|
+
stage: string,
|
|
91
|
+
details?: Record<string, unknown>,
|
|
92
|
+
) => void
|
|
93
|
+
/** Pre-bound to the current WebSocket; returns the byte count sent. */
|
|
94
|
+
send: (envelope: ServerEnvelope) => number
|
|
95
|
+
/** Broadcast a chat-specific snapshot + sidebar snapshot. */
|
|
96
|
+
broadcastChatAndSidebar: (chatId: string) => Promise<void>
|
|
97
|
+
/** Broadcast sidebar snapshot to all connected clients. */
|
|
98
|
+
broadcastSidebar: () => Promise<void>
|
|
99
|
+
/**
|
|
100
|
+
* Broadcast ALL snapshots to all connected clients.
|
|
101
|
+
* Used for the chat.setDraftProtection fall-through broadcast.
|
|
102
|
+
*/
|
|
103
|
+
broadcastAll: () => Promise<void>
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ---------------------------------------------------------------------------
|
|
107
|
+
// Command dispatcher
|
|
108
|
+
// ---------------------------------------------------------------------------
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Handle one chat WS command.
|
|
112
|
+
*
|
|
113
|
+
* Returns `true` when the command was handled (caller should `return`).
|
|
114
|
+
* Returns `false` when the command type is outside this module's scope.
|
|
115
|
+
*/
|
|
116
|
+
export async function handleChatCommand(
|
|
117
|
+
deps: ChatCommandDeps,
|
|
118
|
+
command: ClientCommand,
|
|
119
|
+
id: string,
|
|
120
|
+
): Promise<boolean> {
|
|
121
|
+
const {
|
|
122
|
+
store,
|
|
123
|
+
agent,
|
|
124
|
+
analytics,
|
|
125
|
+
setDraftProtection,
|
|
126
|
+
logSendProfilingFn,
|
|
127
|
+
send,
|
|
128
|
+
broadcastChatAndSidebar,
|
|
129
|
+
broadcastSidebar,
|
|
130
|
+
broadcastAll,
|
|
131
|
+
} = deps
|
|
132
|
+
|
|
133
|
+
switch (command.type) {
|
|
134
|
+
case "chat.create": {
|
|
135
|
+
const chat = await store.createChat(command.projectId, {
|
|
136
|
+
stackId: command.stackId,
|
|
137
|
+
stackBindings: command.stackBindings,
|
|
138
|
+
})
|
|
139
|
+
send({ v: PROTOCOL_VERSION, type: "ack", id, result: { chatId: chat.id } })
|
|
140
|
+
analytics.track("chat_created")
|
|
141
|
+
await broadcastChatAndSidebar(chat.id)
|
|
142
|
+
return true
|
|
143
|
+
}
|
|
144
|
+
case "chat.fork": {
|
|
145
|
+
const result = await agent.forkChat(command.chatId)
|
|
146
|
+
send({ v: PROTOCOL_VERSION, type: "ack", id, result })
|
|
147
|
+
await broadcastSidebar()
|
|
148
|
+
return true
|
|
149
|
+
}
|
|
150
|
+
case "chat.rename": {
|
|
151
|
+
await store.renameChat(command.chatId, command.title)
|
|
152
|
+
send({ v: PROTOCOL_VERSION, type: "ack", id })
|
|
153
|
+
await broadcastChatAndSidebar(command.chatId)
|
|
154
|
+
return true
|
|
155
|
+
}
|
|
156
|
+
case "chat.archive": {
|
|
157
|
+
await store.archiveChat(command.chatId)
|
|
158
|
+
send({ v: PROTOCOL_VERSION, type: "ack", id })
|
|
159
|
+
await broadcastSidebar()
|
|
160
|
+
return true
|
|
161
|
+
}
|
|
162
|
+
case "chat.unarchive": {
|
|
163
|
+
await store.unarchiveChat(command.chatId)
|
|
164
|
+
send({ v: PROTOCOL_VERSION, type: "ack", id })
|
|
165
|
+
await broadcastChatAndSidebar(command.chatId)
|
|
166
|
+
return true
|
|
167
|
+
}
|
|
168
|
+
case "chat.delete": {
|
|
169
|
+
await agent.cancel(command.chatId)
|
|
170
|
+
for (const scheduleId of agent.listLiveSchedules(command.chatId)) {
|
|
171
|
+
await agent.cancelAutoContinue(command.chatId, scheduleId, "chat_deleted")
|
|
172
|
+
}
|
|
173
|
+
await agent.closeChat(command.chatId)
|
|
174
|
+
if (agent.toolCallbackService) {
|
|
175
|
+
await agent.toolCallbackService.cancelAllForChat(command.chatId, "chat_deleted")
|
|
176
|
+
}
|
|
177
|
+
await store.deleteChat(command.chatId)
|
|
178
|
+
send({ v: PROTOCOL_VERSION, type: "ack", id })
|
|
179
|
+
analytics.track("chat_deleted")
|
|
180
|
+
await broadcastSidebar()
|
|
181
|
+
return true
|
|
182
|
+
}
|
|
183
|
+
case "chat.markRead": {
|
|
184
|
+
await store.setChatReadState(command.chatId, false)
|
|
185
|
+
send({ v: PROTOCOL_VERSION, type: "ack", id })
|
|
186
|
+
await broadcastChatAndSidebar(command.chatId)
|
|
187
|
+
return true
|
|
188
|
+
}
|
|
189
|
+
case "chat.setPolicyOverride": {
|
|
190
|
+
await store.setChatPolicyOverride(command.chatId, command.policyOverride ?? null)
|
|
191
|
+
send({ v: PROTOCOL_VERSION, type: "ack", id })
|
|
192
|
+
await broadcastChatAndSidebar(command.chatId)
|
|
193
|
+
return true
|
|
194
|
+
}
|
|
195
|
+
case "chat.setDraftProtection": {
|
|
196
|
+
setDraftProtection(command.chatIds)
|
|
197
|
+
send({ v: PROTOCOL_VERSION, type: "ack", id })
|
|
198
|
+
// Preserves the original break→broadcastSnapshots fall-through behavior.
|
|
199
|
+
await broadcastAll()
|
|
200
|
+
return true
|
|
201
|
+
}
|
|
202
|
+
case "chat.send": {
|
|
203
|
+
const result = await agent.send(command)
|
|
204
|
+
const profile = command.clientTraceId && result.chatId
|
|
205
|
+
? agent.getActiveTurnProfile(result.chatId)
|
|
206
|
+
: null
|
|
207
|
+
logSendProfilingFn(profile?.traceId ?? command.clientTraceId, profile?.startedAt, "ws.chat_send_ack", {
|
|
208
|
+
chatId: result.chatId ?? null,
|
|
209
|
+
})
|
|
210
|
+
const payloadBytes = send({ v: PROTOCOL_VERSION, type: "ack", id, result })
|
|
211
|
+
logSendProfilingFn(profile?.traceId ?? command.clientTraceId, profile?.startedAt, "ws.chat_send_ack_completed", {
|
|
212
|
+
chatId: result.chatId ?? null,
|
|
213
|
+
payloadBytes,
|
|
214
|
+
})
|
|
215
|
+
return true
|
|
216
|
+
}
|
|
217
|
+
case "chat.cancel": {
|
|
218
|
+
await agent.cancel(command.chatId)
|
|
219
|
+
// Resolve any open ask-style tool-callback prompts for this chat
|
|
220
|
+
// so the model's tool_use does not hang on a stranded pending. The
|
|
221
|
+
// session-close path no longer fires this cascade because it also
|
|
222
|
+
// ran on transparent respawns (rotation / idle sweep) — see
|
|
223
|
+
// makeClaudeSessionHandle.close() in agent.ts.
|
|
224
|
+
if (agent.toolCallbackService) {
|
|
225
|
+
await agent.toolCallbackService.cancelAllForChat(command.chatId, "chat_cancelled")
|
|
226
|
+
}
|
|
227
|
+
send({ v: PROTOCOL_VERSION, type: "ack", id })
|
|
228
|
+
return true
|
|
229
|
+
}
|
|
230
|
+
case "chat.stopDraining": {
|
|
231
|
+
await agent.stopDraining(command.chatId)
|
|
232
|
+
send({ v: PROTOCOL_VERSION, type: "ack", id })
|
|
233
|
+
return true
|
|
234
|
+
}
|
|
235
|
+
case "chat.loadHistory": {
|
|
236
|
+
const chat = store.getChat(command.chatId)
|
|
237
|
+
if (!chat) throw new Error("Chat not found")
|
|
238
|
+
const page = store.getMessagesPageBefore(command.chatId, command.beforeCursor, command.limit)
|
|
239
|
+
send({ v: PROTOCOL_VERSION, type: "ack", id, result: page })
|
|
240
|
+
return true
|
|
241
|
+
}
|
|
242
|
+
case "chat.respondTool": {
|
|
243
|
+
await agent.respondTool(command)
|
|
244
|
+
send({ v: PROTOCOL_VERSION, type: "ack", id })
|
|
245
|
+
return true
|
|
246
|
+
}
|
|
247
|
+
case "chat.toolRequestAnswer": {
|
|
248
|
+
const toolCallbackSvc = agent.toolCallbackService
|
|
249
|
+
if (!toolCallbackSvc) throw new Error("tool callback service unavailable")
|
|
250
|
+
const validKinds = new Set(["allow", "deny", "answer"])
|
|
251
|
+
if (
|
|
252
|
+
!isRecord(command.decision) ||
|
|
253
|
+
!validKinds.has(typeof command.decision.kind === "string" ? command.decision.kind : "")
|
|
254
|
+
) {
|
|
255
|
+
throw new Error("Invalid tool request decision kind")
|
|
256
|
+
}
|
|
257
|
+
const existing = store.getToolRequest(command.toolRequestId)
|
|
258
|
+
if (!existing || existing.chatId !== command.chatId) {
|
|
259
|
+
throw new Error("Tool request does not belong to this chat")
|
|
260
|
+
}
|
|
261
|
+
await toolCallbackSvc.answer(command.toolRequestId, command.decision)
|
|
262
|
+
send({ v: PROTOCOL_VERSION, type: "ack", id })
|
|
263
|
+
await broadcastChatAndSidebar(command.chatId)
|
|
264
|
+
return true
|
|
265
|
+
}
|
|
266
|
+
case "chat.respondSubagentTool": {
|
|
267
|
+
await agent.respondSubagentTool(command)
|
|
268
|
+
send({ v: PROTOCOL_VERSION, type: "ack", id })
|
|
269
|
+
return true
|
|
270
|
+
}
|
|
271
|
+
case "chat.cancelSubagentRun": {
|
|
272
|
+
await agent.cancelSubagentRun(command)
|
|
273
|
+
send({ v: PROTOCOL_VERSION, type: "ack", id })
|
|
274
|
+
return true
|
|
275
|
+
}
|
|
276
|
+
default:
|
|
277
|
+
return false
|
|
278
|
+
}
|
|
279
|
+
}
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test"
|
|
2
|
+
import {
|
|
3
|
+
buildFallbackDiffStore,
|
|
4
|
+
buildFallbackLlmProvider,
|
|
5
|
+
buildInitialAppSettingsSnapshot,
|
|
6
|
+
buildResolvedAppSettings,
|
|
7
|
+
mergeAppSettingsPatch,
|
|
8
|
+
} from "./ws-router-defaults"
|
|
9
|
+
import type { AppSettingsSnapshot, SubagentInput } from "../shared/types"
|
|
10
|
+
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// Helpers
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
|
|
15
|
+
function makeSnapshot(): AppSettingsSnapshot {
|
|
16
|
+
return buildInitialAppSettingsSnapshot()
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const BASE_SUBAGENT_INPUT: SubagentInput = {
|
|
20
|
+
name: "Bot",
|
|
21
|
+
description: "",
|
|
22
|
+
provider: "claude",
|
|
23
|
+
model: "claude-opus-4-7",
|
|
24
|
+
systemPrompt: "",
|
|
25
|
+
modelOptions: { reasoningEffort: "high", contextWindow: "200k" },
|
|
26
|
+
contextScope: "full-transcript",
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
// mergeAppSettingsPatch
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
describe("mergeAppSettingsPatch", () => {
|
|
34
|
+
test("subagents.create appends a new subagent with generated id and trims name", () => {
|
|
35
|
+
const snapshot = makeSnapshot()
|
|
36
|
+
const result = mergeAppSettingsPatch(snapshot, {
|
|
37
|
+
subagents: {
|
|
38
|
+
create: { ...BASE_SUBAGENT_INPUT, name: " My Bot ", triggerMode: "manual" },
|
|
39
|
+
},
|
|
40
|
+
})
|
|
41
|
+
expect(result.subagents).toHaveLength(1)
|
|
42
|
+
const s = result.subagents[0]
|
|
43
|
+
expect(s).toBeDefined()
|
|
44
|
+
if (!s) return
|
|
45
|
+
expect(s.name).toBe("My Bot") // trimmed
|
|
46
|
+
expect(s.triggerMode).toBe("manual")
|
|
47
|
+
expect(typeof s.id).toBe("string")
|
|
48
|
+
expect(s.id.length).toBeGreaterThan(0)
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
test("subagents.create defaults triggerMode to 'auto' when omitted", () => {
|
|
52
|
+
const snapshot = makeSnapshot()
|
|
53
|
+
const result = mergeAppSettingsPatch(snapshot, {
|
|
54
|
+
subagents: { create: BASE_SUBAGENT_INPUT },
|
|
55
|
+
})
|
|
56
|
+
expect(result.subagents[0]?.triggerMode).toBe("auto")
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
test("subagents.update patches matching subagent name", () => {
|
|
60
|
+
const withSubagent = mergeAppSettingsPatch(makeSnapshot(), {
|
|
61
|
+
subagents: { create: BASE_SUBAGENT_INPUT },
|
|
62
|
+
})
|
|
63
|
+
const id = withSubagent.subagents[0]!.id
|
|
64
|
+
const updated = mergeAppSettingsPatch(withSubagent, {
|
|
65
|
+
subagents: { update: { id, patch: { name: "New Name" } } },
|
|
66
|
+
})
|
|
67
|
+
expect(updated.subagents[0]?.name).toBe("New Name")
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
test("subagents.delete removes the matching subagent", () => {
|
|
71
|
+
const withSubagent = mergeAppSettingsPatch(makeSnapshot(), {
|
|
72
|
+
subagents: { create: BASE_SUBAGENT_INPUT },
|
|
73
|
+
})
|
|
74
|
+
const id = withSubagent.subagents[0]!.id
|
|
75
|
+
const result = mergeAppSettingsPatch(withSubagent, { subagents: { delete: { id } } })
|
|
76
|
+
expect(result.subagents).toHaveLength(0)
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
test("terminal and editor are deep-merged (unset fields preserved)", () => {
|
|
80
|
+
const snapshot = makeSnapshot()
|
|
81
|
+
const result = mergeAppSettingsPatch(snapshot, {
|
|
82
|
+
terminal: { scrollbackLines: 2_000 },
|
|
83
|
+
editor: { preset: "vscode" },
|
|
84
|
+
})
|
|
85
|
+
expect(result.terminal.scrollbackLines).toBe(2_000)
|
|
86
|
+
expect(result.terminal.minColumnWidth).toBe(450) // preserved from base
|
|
87
|
+
expect(result.editor.preset).toBe("vscode")
|
|
88
|
+
expect(result.editor.commandTemplate).toBe("cursor {path}") // preserved from base
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
test("providerDefaults.claude modelOptions are deep-merged", () => {
|
|
92
|
+
const snapshot = makeSnapshot()
|
|
93
|
+
const result = mergeAppSettingsPatch(snapshot, {
|
|
94
|
+
providerDefaults: {
|
|
95
|
+
claude: { modelOptions: { reasoningEffort: "low", contextWindow: "200k" } },
|
|
96
|
+
},
|
|
97
|
+
})
|
|
98
|
+
expect(result.providerDefaults.claude.modelOptions.reasoningEffort).toBe("low")
|
|
99
|
+
// contextWindow preserved since we included it in the patch
|
|
100
|
+
expect(result.providerDefaults.claude.modelOptions.contextWindow).toBe("200k")
|
|
101
|
+
// unrelated provider unchanged
|
|
102
|
+
expect(result.providerDefaults.codex.modelOptions.reasoningEffort).toBe("high")
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
test("claudeDriver lifecycle is deep-merged and preference is preserved", () => {
|
|
106
|
+
const snapshot = makeSnapshot()
|
|
107
|
+
const result = mergeAppSettingsPatch(snapshot, {
|
|
108
|
+
claudeDriver: { lifecycle: { idleTimeoutMs: 9_000 } },
|
|
109
|
+
})
|
|
110
|
+
expect(result.claudeDriver.preference).toBe(snapshot.claudeDriver.preference)
|
|
111
|
+
expect(result.claudeDriver.lifecycle).toMatchObject({ idleTimeoutMs: 9_000 })
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
test("subagentRuntime.defaultLoopSubagentId null clears the value", () => {
|
|
115
|
+
const snapshot = {
|
|
116
|
+
...makeSnapshot(),
|
|
117
|
+
subagentRuntime: { runTimeoutMs: 600_000, defaultLoopSubagentId: "abc" },
|
|
118
|
+
}
|
|
119
|
+
const result = mergeAppSettingsPatch(snapshot, {
|
|
120
|
+
subagentRuntime: { defaultLoopSubagentId: null },
|
|
121
|
+
})
|
|
122
|
+
expect(result.subagentRuntime.defaultLoopSubagentId).toBeNull()
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
test("returns a new snapshot object (does not mutate input)", () => {
|
|
126
|
+
const snapshot = makeSnapshot()
|
|
127
|
+
const result = mergeAppSettingsPatch(snapshot, { analyticsEnabled: false })
|
|
128
|
+
expect(result).not.toBe(snapshot)
|
|
129
|
+
expect(snapshot.analyticsEnabled).toBe(true) // original unchanged
|
|
130
|
+
expect(result.analyticsEnabled).toBe(false)
|
|
131
|
+
})
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
// ---------------------------------------------------------------------------
|
|
135
|
+
// buildFallbackDiffStore
|
|
136
|
+
// ---------------------------------------------------------------------------
|
|
137
|
+
|
|
138
|
+
describe("buildFallbackDiffStore", () => {
|
|
139
|
+
test("returns an object with all required DiffStore methods", () => {
|
|
140
|
+
const store = buildFallbackDiffStore()
|
|
141
|
+
const methods = [
|
|
142
|
+
"getProjectSnapshot", "refreshSnapshot", "initializeGit",
|
|
143
|
+
"getGitHubPublishInfo", "checkGitHubRepoAvailability", "publishToGitHub",
|
|
144
|
+
"listBranches", "previewMergeBranch", "mergeBranch", "syncBranch",
|
|
145
|
+
"checkoutBranch", "createBranch", "generateCommitMessage", "commitFiles",
|
|
146
|
+
"discardFile", "ignoreFile", "readPatch",
|
|
147
|
+
] as const
|
|
148
|
+
for (const m of methods) {
|
|
149
|
+
expect(typeof store[m]).toBe("function")
|
|
150
|
+
}
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
test("getProjectSnapshot returns an unknown status with empty files", () => {
|
|
154
|
+
const store = buildFallbackDiffStore()
|
|
155
|
+
const snap = store.getProjectSnapshot("any-project-id")
|
|
156
|
+
expect(snap.status).toBe("unknown")
|
|
157
|
+
expect(snap.files).toEqual([])
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
test("listBranches resolves with empty arrays and unavailable pullRequestsStatus", async () => {
|
|
161
|
+
const result = await buildFallbackDiffStore().listBranches()
|
|
162
|
+
expect(result.recent).toEqual([])
|
|
163
|
+
expect(result.pullRequestsStatus).toBe("unavailable")
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
test("readPatch resolves with empty patch string", async () => {
|
|
167
|
+
const result = await buildFallbackDiffStore().readPatch()
|
|
168
|
+
expect(result.patch).toBe("")
|
|
169
|
+
})
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
// ---------------------------------------------------------------------------
|
|
173
|
+
// buildFallbackLlmProvider
|
|
174
|
+
// ---------------------------------------------------------------------------
|
|
175
|
+
|
|
176
|
+
describe("buildFallbackLlmProvider", () => {
|
|
177
|
+
test("returns an object with read, write, validate methods", () => {
|
|
178
|
+
const provider = buildFallbackLlmProvider()
|
|
179
|
+
expect(typeof provider.read).toBe("function")
|
|
180
|
+
expect(typeof provider.write).toBe("function")
|
|
181
|
+
expect(typeof provider.validate).toBe("function")
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
test("read resolves with disabled openai shape", async () => {
|
|
185
|
+
const snap = await buildFallbackLlmProvider().read()
|
|
186
|
+
expect(snap.provider).toBe("openai")
|
|
187
|
+
expect(snap.enabled).toBe(false)
|
|
188
|
+
expect(snap.resolvedBaseUrl).toBe("https://api.openai.com/v1")
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
test("write computes resolvedBaseUrl for openrouter", async () => {
|
|
192
|
+
const snap = await buildFallbackLlmProvider().write({
|
|
193
|
+
provider: "openrouter",
|
|
194
|
+
apiKey: "k",
|
|
195
|
+
model: "m",
|
|
196
|
+
baseUrl: "",
|
|
197
|
+
})
|
|
198
|
+
expect(snap.resolvedBaseUrl).toBe("https://openrouter.ai/api/v1")
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
test("write uses custom baseUrl for 'custom' provider", async () => {
|
|
202
|
+
const snap = await buildFallbackLlmProvider().write({
|
|
203
|
+
provider: "custom",
|
|
204
|
+
apiKey: "",
|
|
205
|
+
model: "m",
|
|
206
|
+
baseUrl: "https://my-endpoint.example.com/v1",
|
|
207
|
+
})
|
|
208
|
+
expect(snap.resolvedBaseUrl).toBe("https://my-endpoint.example.com/v1")
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
test("validate always returns ok:false", async () => {
|
|
212
|
+
const result = await buildFallbackLlmProvider().validate({
|
|
213
|
+
provider: "openai",
|
|
214
|
+
apiKey: "k",
|
|
215
|
+
model: "m",
|
|
216
|
+
baseUrl: "",
|
|
217
|
+
})
|
|
218
|
+
expect(result.ok).toBe(false)
|
|
219
|
+
})
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
// ---------------------------------------------------------------------------
|
|
223
|
+
// buildResolvedAppSettings
|
|
224
|
+
// ---------------------------------------------------------------------------
|
|
225
|
+
|
|
226
|
+
describe("buildResolvedAppSettings", () => {
|
|
227
|
+
test("getSnapshot returns built-in fallback when no appSettings provided", () => {
|
|
228
|
+
const resolved = buildResolvedAppSettings(undefined)
|
|
229
|
+
const snap = resolved.getSnapshot()
|
|
230
|
+
expect(snap.analyticsEnabled).toBe(true)
|
|
231
|
+
expect(snap.subagents).toEqual([])
|
|
232
|
+
expect(snap.customMcpServers).toEqual([])
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
test("writePatch mutates the in-memory fallback when no appSettings", async () => {
|
|
236
|
+
const resolved = buildResolvedAppSettings(undefined)
|
|
237
|
+
expect(resolved.getSnapshot().analyticsEnabled).toBe(true)
|
|
238
|
+
const after = await resolved.writePatch({ analyticsEnabled: false })
|
|
239
|
+
expect(after.analyticsEnabled).toBe(false)
|
|
240
|
+
// getSnapshot reflects the mutation
|
|
241
|
+
expect(resolved.getSnapshot().analyticsEnabled).toBe(false)
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
test("onChange returns callable noop when no appSettings", () => {
|
|
245
|
+
const resolved = buildResolvedAppSettings(undefined)
|
|
246
|
+
const dispose = resolved.onChange(() => {})
|
|
247
|
+
expect(typeof dispose).toBe("function")
|
|
248
|
+
expect(() => dispose()).not.toThrow()
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
test("getSnapshot delegates to real appSettings when provided", () => {
|
|
252
|
+
const mockSnapshot = { ...makeSnapshot(), analyticsEnabled: false }
|
|
253
|
+
const fakeManager = { getSnapshot: () => mockSnapshot, write: async () => mockSnapshot }
|
|
254
|
+
const resolved = buildResolvedAppSettings(fakeManager)
|
|
255
|
+
expect(resolved.getSnapshot().analyticsEnabled).toBe(false)
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
test("createSubagent falls back to writePatch when manager lacks createSubagent", async () => {
|
|
259
|
+
const resolved = buildResolvedAppSettings(undefined)
|
|
260
|
+
const result = await resolved.createSubagent({ ...BASE_SUBAGENT_INPUT, name: "FallbackBot" })
|
|
261
|
+
// Should be the newly created subagent, not a NOT_FOUND error
|
|
262
|
+
if ("code" in result) {
|
|
263
|
+
throw new Error(`Expected subagent but got error: ${result.message}`)
|
|
264
|
+
}
|
|
265
|
+
expect(result.name).toBe("FallbackBot")
|
|
266
|
+
expect(resolved.getSnapshot().subagents).toHaveLength(1)
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
test("write delegates to real appSettings when provided", async () => {
|
|
270
|
+
let written: { analyticsEnabled: boolean } | null = null
|
|
271
|
+
const mockSnapshot = makeSnapshot()
|
|
272
|
+
const fakeManager = {
|
|
273
|
+
getSnapshot: () => mockSnapshot,
|
|
274
|
+
write: async (value: { analyticsEnabled: boolean }) => {
|
|
275
|
+
written = value
|
|
276
|
+
return mockSnapshot
|
|
277
|
+
},
|
|
278
|
+
}
|
|
279
|
+
const resolved = buildResolvedAppSettings(fakeManager)
|
|
280
|
+
await resolved.write({ analyticsEnabled: false })
|
|
281
|
+
expect(written!).toEqual({ analyticsEnabled: false })
|
|
282
|
+
})
|
|
283
|
+
})
|