@cuongtran001/kanna 1.3.0 → 1.4.0
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-BHj370W7.js} +1 -1
- package/dist/client/assets/{architectureDiagram-3BPJPVTR-BKqRYsm4.js → architectureDiagram-3BPJPVTR-1G88q1PY.js} +1 -1
- package/dist/client/assets/{blockDiagram-GPEHLZMM-Ce7Eorec.js → blockDiagram-GPEHLZMM-D0L3Co5v.js} +1 -1
- package/dist/client/assets/{c4Diagram-AAUBKEIU-CJ77zauH.js → c4Diagram-AAUBKEIU-CQtERl3H.js} +1 -1
- package/dist/client/assets/channel-BP9IMEvd.js +1 -0
- package/dist/client/assets/{chunk-2J33WTMH-Rk_3zLxJ.js → chunk-2J33WTMH-DxOwt09f.js} +1 -1
- package/dist/client/assets/{chunk-4BX2VUAB-DPIz6Nvf.js → chunk-4BX2VUAB-Wry66y9R.js} +1 -1
- package/dist/client/assets/{chunk-55IACEB6-D-TkbGh2.js → chunk-55IACEB6-D6p-Hi4x.js} +1 -1
- package/dist/client/assets/{chunk-727SXJPM-B4NOSIZV.js → chunk-727SXJPM-CRQZt23N.js} +1 -1
- package/dist/client/assets/{chunk-AQP2D5EJ-Cm_TGLYm.js → chunk-AQP2D5EJ-BzgTKOM9.js} +1 -1
- package/dist/client/assets/{chunk-FMBD7UC4-Bu45BUVh.js → chunk-FMBD7UC4-CKfD-91C.js} +1 -1
- package/dist/client/assets/{chunk-ND2GUHAM-C1Bw8_gg.js → chunk-ND2GUHAM-a71B27wc.js} +1 -1
- package/dist/client/assets/{chunk-QZHKN3VN-DueG15o0.js → chunk-QZHKN3VN-vv9gJwx0.js} +1 -1
- package/dist/client/assets/classDiagram-4FO5ZUOK--W6Wt7FF.js +1 -0
- package/dist/client/assets/classDiagram-v2-Q7XG4LA2--W6Wt7FF.js +1 -0
- package/dist/client/assets/{cose-bilkent-S5V4N54A-YgpMurXf.js → cose-bilkent-S5V4N54A-Cu-dCCaI.js} +1 -1
- package/dist/client/assets/{dagre-BM42HDAG-Dop7Oq67.js → dagre-BM42HDAG-D8coOWLK.js} +1 -1
- package/dist/client/assets/{diagram-2AECGRRQ-unMfBEJH.js → diagram-2AECGRRQ-NIwZ65jE.js} +1 -1
- package/dist/client/assets/{diagram-5GNKFQAL-D9EJ9qMI.js → diagram-5GNKFQAL-d46aE_le.js} +1 -1
- package/dist/client/assets/{diagram-KO2AKTUF-C5IykOIR.js → diagram-KO2AKTUF-xdm2UxWc.js} +1 -1
- package/dist/client/assets/{diagram-LMA3HP47-DJAuPngz.js → diagram-LMA3HP47-hDbLwC5K.js} +1 -1
- package/dist/client/assets/{diagram-OG6HWLK6-B3wHFtS-.js → diagram-OG6HWLK6-MHUGG7ki.js} +1 -1
- package/dist/client/assets/{erDiagram-TEJ5UH35-Dm7kt9o5.js → erDiagram-TEJ5UH35-Ro6n9X70.js} +1 -1
- package/dist/client/assets/{flowDiagram-I6XJVG4X-s9Lds3ny.js → flowDiagram-I6XJVG4X-DOZqvRMu.js} +1 -1
- package/dist/client/assets/{ganttDiagram-6RSMTGT7-CRQNYdAf.js → ganttDiagram-6RSMTGT7-Bn2NXYIy.js} +1 -1
- package/dist/client/assets/{gitGraphDiagram-PVQCEYII-DhYjK4Jb.js → gitGraphDiagram-PVQCEYII-DK44bfbC.js} +1 -1
- package/dist/client/assets/{index-C0U2kwuB.js → index-CWJpHyQy.js} +115 -115
- package/dist/client/assets/{index-CLsOWXff.css → index-Cyzd4zZx.css} +1 -1
- package/dist/client/assets/{index-CbSY9HfS.js → index-DU56d4oC.js} +1 -1
- package/dist/client/assets/{infoDiagram-5YYISTIA-CRKJ0PSH.js → infoDiagram-5YYISTIA-C2tW_Dwj.js} +1 -1
- package/dist/client/assets/{ishikawaDiagram-YF4QCWOH-CEAkB0IY.js → ishikawaDiagram-YF4QCWOH-C__4Z1wz.js} +1 -1
- package/dist/client/assets/{journeyDiagram-JHISSGLW-CyOK5_DG.js → journeyDiagram-JHISSGLW-Bbn5EawR.js} +1 -1
- package/dist/client/assets/{kanban-definition-UN3LZRKU-BfKjiJSo.js → kanban-definition-UN3LZRKU-RgS5IRWl.js} +1 -1
- package/dist/client/assets/{linear-DLR6miy3.js → linear-DfI0qBO_.js} +1 -1
- package/dist/client/assets/{mermaid.core-D7AdC_lE.js → mermaid.core-BiN10VHc.js} +4 -4
- package/dist/client/assets/{mindmap-definition-RKZ34NQL-zYyJtDSa.js → mindmap-definition-RKZ34NQL-BQhibhh3.js} +1 -1
- package/dist/client/assets/{pieDiagram-4H26LBE5-BgP8PFlo.js → pieDiagram-4H26LBE5-BEfLRn7a.js} +1 -1
- package/dist/client/assets/{quadrantDiagram-W4KKPZXB-Di8h7J96.js → quadrantDiagram-W4KKPZXB-ba641Y2x.js} +1 -1
- package/dist/client/assets/{requirementDiagram-4Y6WPE33-DuTzFl3R.js → requirementDiagram-4Y6WPE33-BGtl-tVO.js} +1 -1
- package/dist/client/assets/{sankeyDiagram-5OEKKPKP-DBRoMm_T.js → sankeyDiagram-5OEKKPKP-DyziaSFr.js} +1 -1
- package/dist/client/assets/{sequenceDiagram-3UESZ5HK-Bmb-1HUN.js → sequenceDiagram-3UESZ5HK-BfYBAYga.js} +1 -1
- package/dist/client/assets/{stateDiagram-AJRCARHV-fnv5ayn-.js → stateDiagram-AJRCARHV-BfUfmLYA.js} +1 -1
- package/dist/client/assets/stateDiagram-v2-BHNVJYJU-CdGcgYEq.js +1 -0
- package/dist/client/assets/{timeline-definition-PNZ67QCA-ByWRdpQX.js → timeline-definition-PNZ67QCA-T6z7dEFl.js} +1 -1
- package/dist/client/assets/{vennDiagram-CIIHVFJN--2WGpVXW.js → vennDiagram-CIIHVFJN-BAwQbFFd.js} +1 -1
- package/dist/client/assets/{wardley-L42UT6IY-24zlnzxK.js → wardley-L42UT6IY-DF1cRKsP.js} +1 -1
- package/dist/client/assets/{wardleyDiagram-YWT4CUSO-Cllvqb3K.js → wardleyDiagram-YWT4CUSO-BRtAZ8za.js} +1 -1
- package/dist/client/assets/{xychartDiagram-2RQKCTM6-ZcdC6nPE.js → xychartDiagram-2RQKCTM6-Brb-q76h.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.test.ts +22 -6
- package/src/server/agent.ts +35 -4987
- package/src/server/chat-op-log.test.ts +63 -0
- package/src/server/chat-op-log.ts +71 -0
- package/src/server/chat-ops-diff.test.ts +120 -0
- package/src/server/chat-ops-diff.ts +97 -0
- package/src/server/chat-ops-parity.test.ts +140 -0
- 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 +532 -0
- package/src/server/event-store-messages.adapter.ts +455 -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 +291 -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 +75 -0
- package/src/server/event-store.ts +257 -2285
- package/src/server/events.ts +1 -1
- package/src/server/harness-types.ts +15 -1
- package/src/server/oauth-errors.ts +19 -0
- package/src/server/storage/backend.ts +7 -0
- package/src/server/storage/fs-storage.adapter.ts +21 -1
- package/src/server/storage/in-memory-storage.ts +12 -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 +560 -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 +360 -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 +238 -0
- package/src/server/ws-router.test.ts +90 -0
- package/src/server/ws-router.ts +340 -2254
- package/src/shared/app-settings-types.ts +365 -0
- package/src/shared/chat-ops.test.ts +138 -0
- package/src/shared/chat-ops.ts +71 -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/protocol.ts +3 -2
- 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 +31 -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,498 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the extracted session error-response handlers.
|
|
3
|
+
*
|
|
4
|
+
* Each test builds a minimal `SessionErrorHandlerDeps` fake and asserts the
|
|
5
|
+
* correct behaviour of the function under test. No real IO or OS calls.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, test, expect } from "bun:test"
|
|
9
|
+
import { AUTO_CONTINUE_EVENT_VERSION } from "./auto-continue/events"
|
|
10
|
+
import {
|
|
11
|
+
acquireRotationSlot,
|
|
12
|
+
handleLimitError,
|
|
13
|
+
handleLimitDetection,
|
|
14
|
+
handleAuthFailure,
|
|
15
|
+
TOKEN_ROTATION_DEDUPE_WINDOW_MS,
|
|
16
|
+
TOKEN_ROTATION_HERD_STAGGER_MS,
|
|
17
|
+
TOKEN_ROTATION_SCHEDULE_DELAY_MS,
|
|
18
|
+
type SessionErrorHandlerDeps,
|
|
19
|
+
type TokenRotationDedupeEntry,
|
|
20
|
+
} from "./claude-session-error-handler"
|
|
21
|
+
import type { ClaudeSessionState } from "./claude-session-state"
|
|
22
|
+
import type { ActiveTurn } from "./claude-session-state"
|
|
23
|
+
import type { AutoContinueEvent } from "./auto-continue/events"
|
|
24
|
+
import type { LimitDetection, LimitDetector } from "./auto-continue/limit-detector"
|
|
25
|
+
import type { AuthErrorDetection } from "./auto-continue/auth-error-detector"
|
|
26
|
+
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
// Helpers
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
|
|
31
|
+
function makeHandle() {
|
|
32
|
+
return {
|
|
33
|
+
provider: "claude" as const,
|
|
34
|
+
stream: (async function* () {})() as AsyncIterable<never>,
|
|
35
|
+
interrupt: async () => {},
|
|
36
|
+
close: () => {},
|
|
37
|
+
sendPrompt: async () => {},
|
|
38
|
+
setModel: async () => {},
|
|
39
|
+
setPermissionMode: async () => {},
|
|
40
|
+
getSupportedCommands: async () => [],
|
|
41
|
+
pushChannelPrompt: async () => {},
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function makeSession(overrides: Partial<ClaudeSessionState> = {}): ClaudeSessionState {
|
|
46
|
+
return {
|
|
47
|
+
id: "sess-1",
|
|
48
|
+
chatId: "chat-1",
|
|
49
|
+
session: makeHandle(),
|
|
50
|
+
localPath: "/tmp/project",
|
|
51
|
+
additionalDirectories: [],
|
|
52
|
+
model: "claude-opus-4",
|
|
53
|
+
planMode: false,
|
|
54
|
+
sessionToken: null,
|
|
55
|
+
accountInfoLoaded: false,
|
|
56
|
+
nextPromptSeq: 1,
|
|
57
|
+
pendingPromptSeqs: [],
|
|
58
|
+
activeTokenId: null,
|
|
59
|
+
oauthKeyMasked: null,
|
|
60
|
+
oauthLabel: null,
|
|
61
|
+
openrouterKeyMasked: null,
|
|
62
|
+
openrouterModel: null,
|
|
63
|
+
lastUsedAt: Date.now(),
|
|
64
|
+
backgroundTaskIds: new Set<string>(),
|
|
65
|
+
backgroundTaskDeadlineAt: 0,
|
|
66
|
+
loopArmedAtSpawn: false,
|
|
67
|
+
cancelledResultPending: 0,
|
|
68
|
+
suppressSessionTokenPersist: false,
|
|
69
|
+
...overrides,
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function makeActiveTurn(chatId = "chat-1"): ActiveTurn {
|
|
74
|
+
return {
|
|
75
|
+
chatId,
|
|
76
|
+
provider: "claude",
|
|
77
|
+
turn: null as never,
|
|
78
|
+
model: "claude-opus-4",
|
|
79
|
+
planMode: false,
|
|
80
|
+
status: "running",
|
|
81
|
+
pendingTool: null,
|
|
82
|
+
postToolFollowUp: null,
|
|
83
|
+
hasFinalResult: false,
|
|
84
|
+
cancelRequested: false,
|
|
85
|
+
cancelRecorded: false,
|
|
86
|
+
waitStartedAt: null,
|
|
87
|
+
userMessageId: null,
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Fake LimitDetection */
|
|
92
|
+
function makeLimitDetection(resetAt = Date.now() + 60_000): LimitDetection {
|
|
93
|
+
return { chatId: "chat-1", resetAt, tz: "system", raw: {} }
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Build a minimal SessionErrorHandlerDeps. Override fields as needed. */
|
|
97
|
+
function makeDeps(overrides: Partial<SessionErrorHandlerDeps> = {}): SessionErrorHandlerDeps {
|
|
98
|
+
const emittedEvents: AutoContinueEvent[] = []
|
|
99
|
+
const appendedMessages: Array<{ chatId: string; entry: unknown }> = []
|
|
100
|
+
const closedSessions: string[] = []
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
tokenRotationDedupe: new Map<string, TokenRotationDedupeEntry>(),
|
|
104
|
+
claudeSessions: new Map(),
|
|
105
|
+
activeTurns: new Map(),
|
|
106
|
+
oauthPool: null,
|
|
107
|
+
store: {
|
|
108
|
+
getAutoContinueEvents: () => [],
|
|
109
|
+
appendAutoContinueEvent: async (ev) => { emittedEvents.push(ev) },
|
|
110
|
+
recordTurnFailed: async () => {},
|
|
111
|
+
appendMessage: async (chatId, entry) => { appendedMessages.push({ chatId, entry }) },
|
|
112
|
+
},
|
|
113
|
+
resolveAutoResumeFor: () => false,
|
|
114
|
+
emitAutoContinueEvent: async (ev) => { emittedEvents.push(ev) },
|
|
115
|
+
closeClaudeSession: (chatId) => { closedSessions.push(chatId) },
|
|
116
|
+
...overrides,
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ---------------------------------------------------------------------------
|
|
121
|
+
// acquireRotationSlot
|
|
122
|
+
// ---------------------------------------------------------------------------
|
|
123
|
+
|
|
124
|
+
describe("acquireRotationSlot", () => {
|
|
125
|
+
test("null tokenId → always returns isFirst:true and 0 delay", () => {
|
|
126
|
+
const deps = makeDeps()
|
|
127
|
+
const result = acquireRotationSlot(deps, null)
|
|
128
|
+
expect(result).toEqual({ extraDelayMs: 0, isFirst: true })
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
test("first call for a tokenId → isFirst:true, 0 delay, creates dedupe entry", () => {
|
|
132
|
+
const deps = makeDeps()
|
|
133
|
+
const result = acquireRotationSlot(deps, "tok-1")
|
|
134
|
+
expect(result).toEqual({ extraDelayMs: 0, isFirst: true })
|
|
135
|
+
expect(deps.tokenRotationDedupe.has("tok-1")).toBe(true)
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
test("second call within window → isFirst:false, extra delay = 1 × HERD_STAGGER", () => {
|
|
139
|
+
const deps = makeDeps()
|
|
140
|
+
acquireRotationSlot(deps, "tok-1") // first
|
|
141
|
+
const result = acquireRotationSlot(deps, "tok-1") // second
|
|
142
|
+
expect(result.isFirst).toBe(false)
|
|
143
|
+
expect(result.extraDelayMs).toBe(TOKEN_ROTATION_HERD_STAGGER_MS)
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
test("third call within window → extra delay = 2 × HERD_STAGGER", () => {
|
|
147
|
+
const deps = makeDeps()
|
|
148
|
+
acquireRotationSlot(deps, "tok-1")
|
|
149
|
+
acquireRotationSlot(deps, "tok-1")
|
|
150
|
+
const result = acquireRotationSlot(deps, "tok-1")
|
|
151
|
+
expect(result.extraDelayMs).toBe(2 * TOKEN_ROTATION_HERD_STAGGER_MS)
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
test("call after dedupe window expires → treated as first again", () => {
|
|
155
|
+
const deps = makeDeps()
|
|
156
|
+
// Seed an entry that's already expired
|
|
157
|
+
deps.tokenRotationDedupe.set("tok-1", {
|
|
158
|
+
firstSeenAt: Date.now() - TOKEN_ROTATION_DEDUPE_WINDOW_MS - 100,
|
|
159
|
+
staggerCount: 5,
|
|
160
|
+
})
|
|
161
|
+
const result = acquireRotationSlot(deps, "tok-1")
|
|
162
|
+
expect(result).toEqual({ extraDelayMs: 0, isFirst: true })
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
test("different token IDs are tracked independently", () => {
|
|
166
|
+
const deps = makeDeps()
|
|
167
|
+
acquireRotationSlot(deps, "tok-A")
|
|
168
|
+
const result = acquireRotationSlot(deps, "tok-B")
|
|
169
|
+
expect(result).toEqual({ extraDelayMs: 0, isFirst: true })
|
|
170
|
+
})
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
// ---------------------------------------------------------------------------
|
|
174
|
+
// handleLimitError
|
|
175
|
+
// ---------------------------------------------------------------------------
|
|
176
|
+
|
|
177
|
+
describe("handleLimitError", () => {
|
|
178
|
+
test("returns false when detector returns null (not a rate-limit error)", async () => {
|
|
179
|
+
const deps = makeDeps()
|
|
180
|
+
const detector: LimitDetector = { detect: () => null }
|
|
181
|
+
const result = await handleLimitError(deps, "chat-1", detector, new Error("other"))
|
|
182
|
+
expect(result).toBe(false)
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
test("returns true and delegates to handleLimitDetection when detection fires", async () => {
|
|
186
|
+
const deps = makeDeps()
|
|
187
|
+
const detection = makeLimitDetection()
|
|
188
|
+
const detector: LimitDetector = { detect: () => detection }
|
|
189
|
+
const emitted: AutoContinueEvent[] = []
|
|
190
|
+
deps.emitAutoContinueEvent = async (ev) => { emitted.push(ev) }
|
|
191
|
+
|
|
192
|
+
const result = await handleLimitError(deps, "chat-1", detector, new Error("rate limited"))
|
|
193
|
+
expect(result).toBe(true)
|
|
194
|
+
expect(emitted.length).toBe(1)
|
|
195
|
+
})
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
// ---------------------------------------------------------------------------
|
|
199
|
+
// handleLimitDetection
|
|
200
|
+
// ---------------------------------------------------------------------------
|
|
201
|
+
|
|
202
|
+
describe("handleLimitDetection", () => {
|
|
203
|
+
test("returns true early when a live schedule already exists (deduplication guard)", async () => {
|
|
204
|
+
const deps = makeDeps({
|
|
205
|
+
store: {
|
|
206
|
+
getAutoContinueEvents: () => [{
|
|
207
|
+
v: AUTO_CONTINUE_EVENT_VERSION,
|
|
208
|
+
kind: "auto_continue_accepted",
|
|
209
|
+
chatId: "chat-1",
|
|
210
|
+
scheduleId: "sched-1",
|
|
211
|
+
timestamp: Date.now(),
|
|
212
|
+
scheduledAt: Date.now() + 60_000,
|
|
213
|
+
tz: "system",
|
|
214
|
+
source: "auto_setting",
|
|
215
|
+
resetAt: Date.now() + 60_000,
|
|
216
|
+
detectedAt: Date.now(),
|
|
217
|
+
}],
|
|
218
|
+
appendAutoContinueEvent: async () => {},
|
|
219
|
+
recordTurnFailed: async () => {},
|
|
220
|
+
appendMessage: async () => {},
|
|
221
|
+
},
|
|
222
|
+
})
|
|
223
|
+
const emitted: AutoContinueEvent[] = []
|
|
224
|
+
deps.emitAutoContinueEvent = async (ev) => { emitted.push(ev) }
|
|
225
|
+
|
|
226
|
+
const result = await handleLimitDetection(deps, "chat-1", makeLimitDetection())
|
|
227
|
+
expect(result).toBe(true)
|
|
228
|
+
// No new event should be emitted — the guard bailed early
|
|
229
|
+
expect(emitted.length).toBe(0)
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
test("emits auto_continue_proposed when no pool and no auto-resume", async () => {
|
|
233
|
+
const deps = makeDeps({ oauthPool: null, resolveAutoResumeFor: () => false })
|
|
234
|
+
const emitted: AutoContinueEvent[] = []
|
|
235
|
+
deps.emitAutoContinueEvent = async (ev) => { emitted.push(ev) }
|
|
236
|
+
|
|
237
|
+
const result = await handleLimitDetection(deps, "chat-1", makeLimitDetection())
|
|
238
|
+
expect(result).toBe(true)
|
|
239
|
+
expect(emitted[0]?.kind).toBe("auto_continue_proposed")
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
test("emits auto_continue_accepted with source auto_setting when auto-resume is on", async () => {
|
|
243
|
+
const deps = makeDeps({ oauthPool: null, resolveAutoResumeFor: () => true })
|
|
244
|
+
const emitted: AutoContinueEvent[] = []
|
|
245
|
+
deps.emitAutoContinueEvent = async (ev) => { emitted.push(ev) }
|
|
246
|
+
|
|
247
|
+
const result = await handleLimitDetection(deps, "chat-1", makeLimitDetection())
|
|
248
|
+
expect(result).toBe(true)
|
|
249
|
+
const ev = emitted[0]
|
|
250
|
+
expect(ev?.kind).toBe("auto_continue_accepted")
|
|
251
|
+
if (ev?.kind === "auto_continue_accepted") {
|
|
252
|
+
expect(ev.source).toBe("auto_setting")
|
|
253
|
+
}
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
test("emits auto_continue_accepted with source token_rotation and closes session when pool can rotate", async () => {
|
|
257
|
+
const session = makeSession({ chatId: "chat-1", activeTokenId: "tok-old" })
|
|
258
|
+
const closedSessions: string[] = []
|
|
259
|
+
const emitted: AutoContinueEvent[] = []
|
|
260
|
+
const turnsFailed: string[] = []
|
|
261
|
+
|
|
262
|
+
const deps = makeDeps({
|
|
263
|
+
oauthPool: {
|
|
264
|
+
markLimited: () => {},
|
|
265
|
+
markError: () => {},
|
|
266
|
+
pickActive: () => ({ id: "tok-new" } as never),
|
|
267
|
+
earliestUnlimit: () => null,
|
|
268
|
+
},
|
|
269
|
+
claudeSessions: new Map([["chat-1", session]]),
|
|
270
|
+
activeTurns: new Map(),
|
|
271
|
+
closeClaudeSession: (chatId) => { closedSessions.push(chatId) },
|
|
272
|
+
store: {
|
|
273
|
+
getAutoContinueEvents: () => [],
|
|
274
|
+
appendAutoContinueEvent: async () => {},
|
|
275
|
+
recordTurnFailed: async (chatId) => { turnsFailed.push(chatId) },
|
|
276
|
+
appendMessage: async () => {},
|
|
277
|
+
},
|
|
278
|
+
})
|
|
279
|
+
deps.emitAutoContinueEvent = async (ev) => { emitted.push(ev) }
|
|
280
|
+
|
|
281
|
+
const result = await handleLimitDetection(deps, "chat-1", makeLimitDetection())
|
|
282
|
+
expect(result).toBe(true)
|
|
283
|
+
expect(emitted[0]?.kind).toBe("auto_continue_accepted")
|
|
284
|
+
if (emitted[0]?.kind === "auto_continue_accepted") {
|
|
285
|
+
expect(emitted[0].source).toBe("token_rotation")
|
|
286
|
+
expect(emitted[0].scheduledAt).toBeGreaterThanOrEqual(Date.now() + TOKEN_ROTATION_SCHEDULE_DELAY_MS - 1)
|
|
287
|
+
}
|
|
288
|
+
expect(closedSessions).toContain("chat-1")
|
|
289
|
+
// No active turn → recordTurnFailed not called
|
|
290
|
+
expect(turnsFailed).toHaveLength(0)
|
|
291
|
+
})
|
|
292
|
+
|
|
293
|
+
test("records turn failed and removes active turn when active turn exists", async () => {
|
|
294
|
+
const session = makeSession({ chatId: "chat-1", activeTokenId: "tok-old" })
|
|
295
|
+
const activeTurns = new Map([["chat-1", makeActiveTurn()]])
|
|
296
|
+
const turnsFailed: string[] = []
|
|
297
|
+
|
|
298
|
+
const deps = makeDeps({
|
|
299
|
+
oauthPool: {
|
|
300
|
+
markLimited: () => {},
|
|
301
|
+
markError: () => {},
|
|
302
|
+
pickActive: () => ({ id: "tok-new" } as never),
|
|
303
|
+
earliestUnlimit: () => null,
|
|
304
|
+
},
|
|
305
|
+
claudeSessions: new Map([["chat-1", session]]),
|
|
306
|
+
activeTurns,
|
|
307
|
+
closeClaudeSession: () => {},
|
|
308
|
+
store: {
|
|
309
|
+
getAutoContinueEvents: () => [],
|
|
310
|
+
appendAutoContinueEvent: async () => {},
|
|
311
|
+
recordTurnFailed: async (chatId) => { turnsFailed.push(chatId) },
|
|
312
|
+
appendMessage: async () => {},
|
|
313
|
+
},
|
|
314
|
+
})
|
|
315
|
+
deps.emitAutoContinueEvent = async () => {}
|
|
316
|
+
|
|
317
|
+
await handleLimitDetection(deps, "chat-1", makeLimitDetection())
|
|
318
|
+
expect(turnsFailed).toContain("chat-1")
|
|
319
|
+
expect(activeTurns.has("chat-1")).toBe(false)
|
|
320
|
+
})
|
|
321
|
+
|
|
322
|
+
test("appends auto_continue_prompt transcript entry when not rotating", async () => {
|
|
323
|
+
const appendedMessages: Array<{ chatId: string; entry: { kind: string } }> = []
|
|
324
|
+
const deps = makeDeps({
|
|
325
|
+
oauthPool: null,
|
|
326
|
+
resolveAutoResumeFor: () => false,
|
|
327
|
+
store: {
|
|
328
|
+
getAutoContinueEvents: () => [],
|
|
329
|
+
appendAutoContinueEvent: async () => {},
|
|
330
|
+
recordTurnFailed: async () => {},
|
|
331
|
+
appendMessage: async (chatId, entry) => {
|
|
332
|
+
appendedMessages.push({ chatId, entry: entry as { kind: string } })
|
|
333
|
+
},
|
|
334
|
+
},
|
|
335
|
+
})
|
|
336
|
+
deps.emitAutoContinueEvent = async () => {}
|
|
337
|
+
|
|
338
|
+
await handleLimitDetection(deps, "chat-1", makeLimitDetection())
|
|
339
|
+
expect(appendedMessages.some(m => m.chatId === "chat-1" && m.entry.kind === "auto_continue_prompt")).toBe(true)
|
|
340
|
+
})
|
|
341
|
+
})
|
|
342
|
+
|
|
343
|
+
// ---------------------------------------------------------------------------
|
|
344
|
+
// handleAuthFailure
|
|
345
|
+
// ---------------------------------------------------------------------------
|
|
346
|
+
|
|
347
|
+
describe("handleAuthFailure", () => {
|
|
348
|
+
const fakeDetection: AuthErrorDetection = { chatId: "chat-1", reason: "401 Unauthorized", raw: {} }
|
|
349
|
+
|
|
350
|
+
test("returns true early when a live schedule already exists", async () => {
|
|
351
|
+
const session = makeSession({ chatId: "chat-1" })
|
|
352
|
+
const deps = makeDeps({
|
|
353
|
+
store: {
|
|
354
|
+
getAutoContinueEvents: () => [{
|
|
355
|
+
v: AUTO_CONTINUE_EVENT_VERSION,
|
|
356
|
+
kind: "auto_continue_accepted",
|
|
357
|
+
chatId: "chat-1",
|
|
358
|
+
scheduleId: "sched-1",
|
|
359
|
+
timestamp: Date.now(),
|
|
360
|
+
scheduledAt: Date.now() + 60_000,
|
|
361
|
+
tz: "system",
|
|
362
|
+
source: "auto_setting",
|
|
363
|
+
resetAt: Date.now() + 60_000,
|
|
364
|
+
detectedAt: Date.now(),
|
|
365
|
+
}],
|
|
366
|
+
appendAutoContinueEvent: async () => {},
|
|
367
|
+
recordTurnFailed: async () => {},
|
|
368
|
+
appendMessage: async () => {},
|
|
369
|
+
},
|
|
370
|
+
})
|
|
371
|
+
const emitted: AutoContinueEvent[] = []
|
|
372
|
+
deps.emitAutoContinueEvent = async (ev) => { emitted.push(ev) }
|
|
373
|
+
|
|
374
|
+
const result = await handleAuthFailure(deps, session, fakeDetection)
|
|
375
|
+
expect(result).toBe(true)
|
|
376
|
+
expect(emitted.length).toBe(0)
|
|
377
|
+
})
|
|
378
|
+
|
|
379
|
+
test("emits auto_continue_proposed when pool cannot rotate", async () => {
|
|
380
|
+
const session = makeSession({ chatId: "chat-1", activeTokenId: "tok-dead" })
|
|
381
|
+
const emitted: AutoContinueEvent[] = []
|
|
382
|
+
const deps = makeDeps({
|
|
383
|
+
oauthPool: {
|
|
384
|
+
markLimited: () => {},
|
|
385
|
+
markError: () => {},
|
|
386
|
+
// pool has no other token
|
|
387
|
+
pickActive: () => ({ id: "tok-dead" } as never),
|
|
388
|
+
earliestUnlimit: () => null,
|
|
389
|
+
},
|
|
390
|
+
})
|
|
391
|
+
deps.emitAutoContinueEvent = async (ev) => { emitted.push(ev) }
|
|
392
|
+
|
|
393
|
+
const result = await handleAuthFailure(deps, session, fakeDetection)
|
|
394
|
+
expect(result).toBe(true)
|
|
395
|
+
expect(emitted[0]?.kind).toBe("auto_continue_proposed")
|
|
396
|
+
})
|
|
397
|
+
|
|
398
|
+
test("emits auto_continue_accepted with token_rotation and closes session when pool can rotate", async () => {
|
|
399
|
+
const session = makeSession({ chatId: "chat-1", activeTokenId: "tok-dead" })
|
|
400
|
+
const closedSessions: string[] = []
|
|
401
|
+
const emitted: AutoContinueEvent[] = []
|
|
402
|
+
|
|
403
|
+
const deps = makeDeps({
|
|
404
|
+
oauthPool: {
|
|
405
|
+
markLimited: () => {},
|
|
406
|
+
markError: () => {},
|
|
407
|
+
pickActive: () => ({ id: "tok-new" } as never),
|
|
408
|
+
earliestUnlimit: () => null,
|
|
409
|
+
},
|
|
410
|
+
closeClaudeSession: (chatId) => { closedSessions.push(chatId) },
|
|
411
|
+
activeTurns: new Map(),
|
|
412
|
+
})
|
|
413
|
+
deps.emitAutoContinueEvent = async (ev) => { emitted.push(ev) }
|
|
414
|
+
|
|
415
|
+
const result = await handleAuthFailure(deps, session, fakeDetection)
|
|
416
|
+
expect(result).toBe(true)
|
|
417
|
+
const ev = emitted[0]
|
|
418
|
+
expect(ev?.kind).toBe("auto_continue_accepted")
|
|
419
|
+
if (ev?.kind === "auto_continue_accepted") {
|
|
420
|
+
expect(ev.source).toBe("token_rotation")
|
|
421
|
+
expect(ev.tz).toBe("system")
|
|
422
|
+
}
|
|
423
|
+
expect(closedSessions).toContain("chat-1")
|
|
424
|
+
})
|
|
425
|
+
|
|
426
|
+
test("records turn failed when active turn exists at time of auth failure", async () => {
|
|
427
|
+
const session = makeSession({ chatId: "chat-1", activeTokenId: "tok-dead" })
|
|
428
|
+
const activeTurns = new Map([["chat-1", makeActiveTurn()]])
|
|
429
|
+
const turnsFailed: string[] = []
|
|
430
|
+
|
|
431
|
+
const deps = makeDeps({
|
|
432
|
+
oauthPool: {
|
|
433
|
+
markLimited: () => {},
|
|
434
|
+
markError: () => {},
|
|
435
|
+
pickActive: () => ({ id: "tok-new" } as never),
|
|
436
|
+
earliestUnlimit: () => null,
|
|
437
|
+
},
|
|
438
|
+
activeTurns,
|
|
439
|
+
closeClaudeSession: () => {},
|
|
440
|
+
store: {
|
|
441
|
+
getAutoContinueEvents: () => [],
|
|
442
|
+
appendAutoContinueEvent: async () => {},
|
|
443
|
+
recordTurnFailed: async (chatId) => { turnsFailed.push(chatId) },
|
|
444
|
+
appendMessage: async () => {},
|
|
445
|
+
},
|
|
446
|
+
})
|
|
447
|
+
deps.emitAutoContinueEvent = async () => {}
|
|
448
|
+
|
|
449
|
+
await handleAuthFailure(deps, session, fakeDetection)
|
|
450
|
+
expect(turnsFailed).toContain("chat-1")
|
|
451
|
+
expect(activeTurns.has("chat-1")).toBe(false)
|
|
452
|
+
})
|
|
453
|
+
|
|
454
|
+
test("appends auto_continue_prompt transcript entry when not rotating", async () => {
|
|
455
|
+
const session = makeSession({ chatId: "chat-1", activeTokenId: "tok-dead" })
|
|
456
|
+
const appendedMessages: Array<{ chatId: string; entry: { kind: string } }> = []
|
|
457
|
+
|
|
458
|
+
const deps = makeDeps({
|
|
459
|
+
oauthPool: {
|
|
460
|
+
markLimited: () => {},
|
|
461
|
+
markError: () => {},
|
|
462
|
+
// same token id → no rotation
|
|
463
|
+
pickActive: () => ({ id: "tok-dead" } as never),
|
|
464
|
+
earliestUnlimit: () => null,
|
|
465
|
+
},
|
|
466
|
+
store: {
|
|
467
|
+
getAutoContinueEvents: () => [],
|
|
468
|
+
appendAutoContinueEvent: async () => {},
|
|
469
|
+
recordTurnFailed: async () => {},
|
|
470
|
+
appendMessage: async (chatId, entry) => {
|
|
471
|
+
appendedMessages.push({ chatId, entry: entry as { kind: string } })
|
|
472
|
+
},
|
|
473
|
+
},
|
|
474
|
+
})
|
|
475
|
+
deps.emitAutoContinueEvent = async () => {}
|
|
476
|
+
|
|
477
|
+
await handleAuthFailure(deps, session, fakeDetection)
|
|
478
|
+
expect(appendedMessages.some(m => m.entry.kind === "auto_continue_prompt")).toBe(true)
|
|
479
|
+
})
|
|
480
|
+
|
|
481
|
+
test("markError is called only for the first detector in the herd window", async () => {
|
|
482
|
+
const session = makeSession({ chatId: "chat-1", activeTokenId: "tok-dead" })
|
|
483
|
+
const markErrorCalls: string[] = []
|
|
484
|
+
const deps = makeDeps({
|
|
485
|
+
oauthPool: {
|
|
486
|
+
markLimited: () => {},
|
|
487
|
+
markError: (id) => { markErrorCalls.push(id) },
|
|
488
|
+
pickActive: () => null,
|
|
489
|
+
earliestUnlimit: () => null,
|
|
490
|
+
},
|
|
491
|
+
})
|
|
492
|
+
deps.emitAutoContinueEvent = async () => {}
|
|
493
|
+
|
|
494
|
+
await handleAuthFailure(deps, session, fakeDetection) // first → calls markError
|
|
495
|
+
await handleAuthFailure(deps, session, fakeDetection) // second → skipped
|
|
496
|
+
expect(markErrorCalls).toHaveLength(1)
|
|
497
|
+
})
|
|
498
|
+
})
|