@mycadet/assistant 0.12.10 → 0.13.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.
Files changed (180) hide show
  1. package/node_modules/@mycadet/ces-client/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +10 -10
  2. package/node_modules/@mycadet/ces-client/node_modules/@mycadet/service-contracts/src/ingress.ts +3 -3
  3. package/node_modules/@mycadet/ces-client/src/http-credentials.ts +18 -1
  4. package/node_modules/@mycadet/gateway-client/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +10 -10
  5. package/node_modules/@mycadet/gateway-client/node_modules/@mycadet/service-contracts/src/ingress.ts +3 -3
  6. package/node_modules/@mycadet/gateway-client/src/gateway-ipc-contracts.ts +0 -20
  7. package/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +10 -10
  8. package/node_modules/@mycadet/service-contracts/src/ingress.ts +3 -3
  9. package/openapi.yaml +2 -708
  10. package/package.json +2 -3
  11. package/scripts/sync-web-search-catalog.ts +1 -2
  12. package/src/__tests__/cloud-profile-guard.test.ts +41 -5
  13. package/src/__tests__/config-schema.test.ts +6 -38
  14. package/src/__tests__/conversation-queue.test.ts +45 -6
  15. package/src/__tests__/db-readiness-http-gate.test.ts +0 -6
  16. package/src/__tests__/persist-onboarding-artifacts.test.ts +0 -1
  17. package/src/__tests__/persona-resolver.test.ts +0 -51
  18. package/src/__tests__/provider-cloud-proxy-integration.test.ts +2 -110
  19. package/src/__tests__/run-due-schedules.test.ts +0 -4
  20. package/src/__tests__/schedule-store.test.ts +0 -12
  21. package/src/__tests__/stt-stream-session.test.ts +4 -4
  22. package/src/__tests__/telephony-tts-guard.test.ts +2 -50
  23. package/src/__tests__/voice-config-update.test.ts +12 -167
  24. package/src/__tests__/web-search-catalog-parity.test.ts +0 -2
  25. package/src/__tests__/workspace-migration-130-speech-mode-to-provider.test.ts +9 -7
  26. package/src/__tests__/workspace-migration-132-web-search-mode-to-provider.test.ts +6 -6
  27. package/src/calls/__tests__/voice-session-bridge.test.ts +1 -4
  28. package/src/calls/resolve-call-tts-provider.ts +6 -25
  29. package/src/calls/telephony-credential-preflight.ts +0 -10
  30. package/src/calls/telephony-tts-capability.ts +1 -17
  31. package/src/cli/commands/tts.ts +1 -2
  32. package/src/cli/index.help.ts +0 -2
  33. package/src/cli/program.ts +0 -2
  34. package/src/config/bundled-skills/settings/tools/voice-config-update.ts +4 -42
  35. package/src/config/schemas/__tests__/stt.test.ts +10 -14
  36. package/src/config/schemas/llm.ts +3 -5
  37. package/src/config/schemas/services.ts +5 -6
  38. package/src/config/schemas/stt.ts +1 -3
  39. package/src/config/schemas/tts.ts +1 -30
  40. package/src/daemon/daemon-readiness.ts +4 -29
  41. package/src/daemon/lifecycle.ts +0 -31
  42. package/src/daemon/providers-setup.ts +0 -8
  43. package/src/heartbeat/__tests__/heartbeat-service.test.ts +0 -1
  44. package/src/heartbeat/heartbeat-service.ts +1 -66
  45. package/src/ipc/__tests__/db-readiness-ipc-gate.test.ts +1 -9
  46. package/src/live-voice/__tests__/live-voice-credential-preflight.test.ts +0 -44
  47. package/src/live-voice/__tests__/live-voice-flux-turn-end.test.ts +4 -6
  48. package/src/live-voice/__tests__/live-voice-stt.test.ts +1 -1
  49. package/src/live-voice/live-voice-credential-preflight.ts +9 -33
  50. package/src/live-voice/live-voice-session.ts +10 -6
  51. package/src/live-voice/live-voice-tts.ts +1 -8
  52. package/src/plugins/defaults/memory/worker.ts +3 -3
  53. package/src/prompts/persona-resolver.ts +0 -35
  54. package/src/providers/__tests__/search-provider-catalog.test.ts +5 -8
  55. package/src/providers/__tests__/voice-error-copy.test.ts +9 -10
  56. package/src/providers/search-provider-catalog.ts +7 -39
  57. package/src/providers/speech-to-text/__tests__/provider-catalog.test.ts +3 -47
  58. package/src/providers/speech-to-text/__tests__/resolve.test.ts +6 -372
  59. package/src/providers/speech-to-text/provider-catalog.ts +2 -73
  60. package/src/providers/speech-to-text/resolve.ts +3 -120
  61. package/src/runtime/http-router-types.ts +1 -3
  62. package/src/runtime/http-server.ts +1 -1
  63. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +13 -11
  64. package/src/runtime/routes/__tests__/tts-routes.test.ts +2 -6
  65. package/src/runtime/routes/conversation-query-routes.ts +0 -96
  66. package/src/runtime/routes/inbound-stages/transcribe-audio.test.ts +7 -7
  67. package/src/runtime/routes/index.ts +0 -4
  68. package/src/runtime/routes/live-voice-routes.test.ts +4 -11
  69. package/src/runtime/routes/live-voice-routes.ts +8 -11
  70. package/src/runtime/routes/secret-routes.ts +0 -6
  71. package/src/runtime/routes/tts-routes.ts +0 -38
  72. package/src/schedule/__tests__/plugin-schedule-reconciler.test.ts +1 -7
  73. package/src/schedule/__tests__/worker-mcp-readiness.test.ts +0 -4
  74. package/src/schedule/schedule-store.ts +0 -7
  75. package/src/schedule/scheduler.ts +0 -5
  76. package/src/schedule/worker.ts +1 -1
  77. package/src/stt/__tests__/daemon-batch-transcriber.test.ts +2 -10
  78. package/src/stt/daemon-batch-transcriber.ts +4 -38
  79. package/src/stt/types.ts +1 -9
  80. package/src/tools/network/__tests__/web-search.test.ts +3 -250
  81. package/src/tools/network/web-search.ts +4 -165
  82. package/src/tts/__tests__/provider-resolution.test.ts +0 -5
  83. package/src/tts/provider-catalog.ts +0 -4
  84. package/src/tts/types.ts +0 -1
  85. package/src/__tests__/cloud-speech-defaults.test.ts +0 -330
  86. package/src/__tests__/migration-cross-version-compatibility.test.ts +0 -1708
  87. package/src/__tests__/migration-export-http.test.ts +0 -557
  88. package/src/__tests__/migration-export-streaming.test.ts +0 -350
  89. package/src/__tests__/migration-export-to-gcs.test.ts +0 -706
  90. package/src/__tests__/migration-import-commit-http.test.ts +0 -1176
  91. package/src/__tests__/migration-import-from-gcs.test.ts +0 -534
  92. package/src/__tests__/migration-import-from-path.test.ts +0 -324
  93. package/src/__tests__/migration-import-from-url.test.ts +0 -588
  94. package/src/__tests__/migration-import-preflight-http.test.ts +0 -739
  95. package/src/__tests__/migration-jobs-status.test.ts +0 -139
  96. package/src/__tests__/migration-parity-persistence.test.ts +0 -1891
  97. package/src/__tests__/migration-transport.test.ts +0 -1089
  98. package/src/__tests__/migration-validate-http.test.ts +0 -634
  99. package/src/__tests__/migration-wizard.test.ts +0 -1395
  100. package/src/__tests__/rebind-secrets-screen.test.ts +0 -876
  101. package/src/__tests__/sanitize-config-for-transfer.test.ts +0 -259
  102. package/src/__tests__/transfer-progress-screen.test.ts +0 -1217
  103. package/src/__tests__/validation-results-screen.test.ts +0 -1130
  104. package/src/__tests__/vbundle-pax-and-symlink.test.ts +0 -147
  105. package/src/background-wake/background-wake-routes.test.ts +0 -906
  106. package/src/background-wake/cloud-client.test.ts +0 -328
  107. package/src/background-wake/cloud-client.ts +0 -196
  108. package/src/background-wake/next-wake.test.ts +0 -335
  109. package/src/background-wake/next-wake.ts +0 -188
  110. package/src/background-wake/publisher.ts +0 -93
  111. package/src/background-wake/wake-intent-hooks.test.ts +0 -234
  112. package/src/cli/commands/__tests__/backup.test.ts +0 -363
  113. package/src/cli/commands/backup.help.ts +0 -180
  114. package/src/cli/commands/backup.ts +0 -398
  115. package/src/cloud/cloud-speech.test.ts +0 -335
  116. package/src/cloud/cloud-speech.ts +0 -300
  117. package/src/config/cloud-speech-defaults.ts +0 -300
  118. package/src/config/sanitize-for-transfer.ts +0 -111
  119. package/src/live-voice/__tests__/live-voice-cloud-speech-fallback.test.ts +0 -250
  120. package/src/providers/speech-to-text/__tests__/cadet-cloud-realtime.test.ts +0 -457
  121. package/src/providers/speech-to-text/__tests__/cadet-cloud.test.ts +0 -271
  122. package/src/providers/speech-to-text/__tests__/cadet-speech-relay-connection.test.ts +0 -126
  123. package/src/providers/speech-to-text/cadet-cloud-flux-realtime.ts +0 -309
  124. package/src/providers/speech-to-text/cadet-cloud-realtime.ts +0 -315
  125. package/src/providers/speech-to-text/cadet-cloud.ts +0 -108
  126. package/src/providers/speech-to-text/cadet-speech-relay-connection.ts +0 -176
  127. package/src/runtime/migrations/__tests__/gcs-signed-url.test.ts +0 -254
  128. package/src/runtime/migrations/__tests__/job-registry.test.ts +0 -346
  129. package/src/runtime/migrations/__tests__/origin-mode.test.ts +0 -62
  130. package/src/runtime/migrations/__tests__/rebind-secrets-credentials.test.ts +0 -184
  131. package/src/runtime/migrations/__tests__/staged-import-path.test.ts +0 -110
  132. package/src/runtime/migrations/__tests__/v1-test-helpers.ts +0 -112
  133. package/src/runtime/migrations/__tests__/vbundle-builder-credentials.test.ts +0 -285
  134. package/src/runtime/migrations/__tests__/vbundle-builder-fd-leak.test.ts +0 -126
  135. package/src/runtime/migrations/__tests__/vbundle-builder-v1-shape.test.ts +0 -253
  136. package/src/runtime/migrations/__tests__/vbundle-import-credentials.test.ts +0 -208
  137. package/src/runtime/migrations/__tests__/vbundle-import-parity.test.ts +0 -485
  138. package/src/runtime/migrations/__tests__/vbundle-import-policy.test.ts +0 -349
  139. package/src/runtime/migrations/__tests__/vbundle-import-version-compat.test.ts +0 -196
  140. package/src/runtime/migrations/__tests__/vbundle-legacy-user-md.test.ts +0 -551
  141. package/src/runtime/migrations/__tests__/vbundle-metadata-merge-integration.test.ts +0 -428
  142. package/src/runtime/migrations/__tests__/vbundle-metadata-merge.test.ts +0 -220
  143. package/src/runtime/migrations/__tests__/vbundle-streaming-importer.test.ts +0 -1823
  144. package/src/runtime/migrations/__tests__/vbundle-streaming-validator.test.ts +0 -598
  145. package/src/runtime/migrations/__tests__/vbundle-symlink-importer.test.ts +0 -463
  146. package/src/runtime/migrations/__tests__/vbundle-symlink-streaming-importer.test.ts +0 -538
  147. package/src/runtime/migrations/__tests__/vbundle-symlink-streaming.test.ts +0 -515
  148. package/src/runtime/migrations/__tests__/vbundle-symlink-tar.test.ts +0 -453
  149. package/src/runtime/migrations/__tests__/vbundle-symlink-walker.test.ts +0 -319
  150. package/src/runtime/migrations/__tests__/vbundle-tar-stream.test.ts +0 -240
  151. package/src/runtime/migrations/__tests__/vbundle-validator-v1-schema.test.ts +0 -473
  152. package/src/runtime/migrations/gcs-signed-url.ts +0 -190
  153. package/src/runtime/migrations/job-registry.ts +0 -297
  154. package/src/runtime/migrations/migration-transport.ts +0 -612
  155. package/src/runtime/migrations/migration-wizard.ts +0 -747
  156. package/src/runtime/migrations/origin-mode.ts +0 -42
  157. package/src/runtime/migrations/rebind-secrets-screen.ts +0 -412
  158. package/src/runtime/migrations/staged-import-path.ts +0 -116
  159. package/src/runtime/migrations/transfer-progress-screen.ts +0 -323
  160. package/src/runtime/migrations/validation-results-screen.ts +0 -469
  161. package/src/runtime/migrations/vbundle-builder.ts +0 -1382
  162. package/src/runtime/migrations/vbundle-import-analyzer.ts +0 -405
  163. package/src/runtime/migrations/vbundle-import-policy.ts +0 -241
  164. package/src/runtime/migrations/vbundle-importer.ts +0 -826
  165. package/src/runtime/migrations/vbundle-metadata-merge.ts +0 -113
  166. package/src/runtime/migrations/vbundle-streaming-importer.ts +0 -2844
  167. package/src/runtime/migrations/vbundle-streaming-validator.ts +0 -426
  168. package/src/runtime/migrations/vbundle-tar-stream.ts +0 -244
  169. package/src/runtime/migrations/vbundle-validator.ts +0 -724
  170. package/src/runtime/routes/__tests__/migration-cadet-metadata-reconcile.test.ts +0 -227
  171. package/src/runtime/routes/__tests__/migration-export-secrets-redacted.test.ts +0 -54
  172. package/src/runtime/routes/__tests__/migration-import-allowed-hosts-env.test.ts +0 -70
  173. package/src/runtime/routes/__tests__/migration-import-credential-filter.test.ts +0 -257
  174. package/src/runtime/routes/background-wake-routes.ts +0 -387
  175. package/src/runtime/routes/migration-routes.ts +0 -2584
  176. package/src/tools/network/__tests__/cloud-search-proxy.test.ts +0 -282
  177. package/src/tools/network/cloud-search-proxy.ts +0 -211
  178. package/src/tts/__tests__/cadet-provider.test.ts +0 -253
  179. package/src/tts/providers/cadet-provider.ts +0 -244
  180. package/src/types/tar-stream.d.ts +0 -66
@@ -1,906 +0,0 @@
1
- import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
2
-
3
- import type { HeartbeatService } from "../heartbeat/heartbeat-service.js";
4
- import type {
5
- SchedulerDueWorkResult,
6
- SchedulerHandle,
7
- } from "../schedule/scheduler.js";
8
- import type { BackgroundWakeIntent } from "./next-wake.js";
9
-
10
- // The drain-due route resolves the scheduler and heartbeat from their
11
- // singletons; mock those accessors so each test can inject the doubles it
12
- // asserts against. next-wake.ts also reads HeartbeatService.getInstance(), so
13
- // the heartbeat mock exposes that too.
14
- type BackgroundWakeRuntime = {
15
- scheduler: Pick<SchedulerHandle, "runOnce" | "runDueWorkOnce">;
16
- heartbeat: Pick<HeartbeatService, "nextRunAt" | "runCloudWakeIfDue">;
17
- };
18
-
19
- let mockScheduler: BackgroundWakeRuntime["scheduler"] | null = null;
20
- let mockHeartbeat: BackgroundWakeRuntime["heartbeat"] | null = null;
21
-
22
- mock.module("../schedule/scheduler.js", () => ({
23
- getScheduler: () => mockScheduler,
24
- }));
25
- mock.module("../heartbeat/heartbeat-service.js", () => ({
26
- getHeartbeatService: () => mockHeartbeat,
27
- HeartbeatService: { getInstance: () => mockHeartbeat },
28
- }));
29
-
30
- function setBackgroundWakeRuntime(runtime: BackgroundWakeRuntime): void {
31
- mockScheduler = runtime.scheduler;
32
- mockHeartbeat = runtime.heartbeat;
33
- }
34
-
35
- function clearBackgroundWakeRuntime(): void {
36
- mockScheduler = null;
37
- mockHeartbeat = null;
38
- }
39
-
40
- type MockIntent = BackgroundWakeIntent;
41
- type MockCompletionPayload = {
42
- leaseId: string;
43
- status: "completed" | "failed" | "expired";
44
- error?: string;
45
- nextIntent?: MockIntent | null;
46
- };
47
-
48
- let computedIntent: MockIntent | null;
49
- let computeCalls: number;
50
- const mockRenewBackgroundWakeLease = mock(async (_leaseId: string) => ({
51
- status: "renewed" as const,
52
- httpStatus: 200,
53
- }));
54
- const mockCompleteBackgroundWakeLease = mock(
55
- async (_args: MockCompletionPayload) => ({
56
- status: "completed" as const,
57
- httpStatus: 200,
58
- }),
59
- );
60
-
61
- const {
62
- ROUTES,
63
- flushBackgroundWakeDrainsForTest,
64
- setBackgroundWakeIntentComputerForTest,
65
- setBackgroundWakeLeaseClientForTest,
66
- } = await import("../runtime/routes/background-wake-routes.js");
67
-
68
- function findHandler(operationId: string) {
69
- const route = ROUTES.find(
70
- (candidate) => candidate.operationId === operationId,
71
- );
72
- if (!route) {
73
- throw new Error(`Route ${operationId} not found`);
74
- }
75
- return route.handler;
76
- }
77
-
78
- const NOW = 1_800_000_000_000;
79
-
80
- function schedulerResultFixture(
81
- overrides: Partial<SchedulerDueWorkResult> = {},
82
- ): SchedulerDueWorkResult {
83
- return {
84
- claimed: 0,
85
- completed: 0,
86
- failed: 0,
87
- skipped: 0,
88
- ...overrides,
89
- };
90
- }
91
-
92
- function firstCallArg(fn: {
93
- mock: { calls: unknown[] };
94
- }): Record<string, unknown> {
95
- const call = fn.mock.calls[0] as unknown[] | undefined;
96
- return (call?.[0] ?? {}) as Record<string, unknown>;
97
- }
98
-
99
- function acceptedResponse(
100
- overrides: Partial<{
101
- leaseId: string;
102
- reason: string;
103
- sourceGeneration: string;
104
- startedAt: number;
105
- deadlineAt: number;
106
- }> = {},
107
- ) {
108
- return {
109
- accepted: true,
110
- leaseId: "lease-123",
111
- reason: "schedule",
112
- sourceGeneration: "source-generation",
113
- startedAt: NOW,
114
- deadlineAt: NOW + 30_000,
115
- ...overrides,
116
- };
117
- }
118
-
119
- function lastCompletionPayload(): MockCompletionPayload {
120
- const calls = mockCompleteBackgroundWakeLease.mock.calls;
121
- const call = calls[calls.length - 1] as unknown[] | undefined;
122
- if (!call) {
123
- throw new Error("completeBackgroundWakeLease was not called");
124
- }
125
- return call[0] as MockCompletionPayload;
126
- }
127
-
128
- describe("background wake runtime routes", () => {
129
- beforeEach(() => {
130
- clearBackgroundWakeRuntime();
131
- mockRenewBackgroundWakeLease.mockClear();
132
- mockCompleteBackgroundWakeLease.mockClear();
133
- mockRenewBackgroundWakeLease.mockImplementation(
134
- async (_leaseId: string) => ({
135
- status: "renewed" as const,
136
- httpStatus: 200,
137
- }),
138
- );
139
- mockCompleteBackgroundWakeLease.mockImplementation(
140
- async (_args: MockCompletionPayload) => ({
141
- status: "completed" as const,
142
- httpStatus: 200,
143
- }),
144
- );
145
- setBackgroundWakeLeaseClientForTest({
146
- renew: mockRenewBackgroundWakeLease,
147
- complete: mockCompleteBackgroundWakeLease,
148
- });
149
- computeCalls = 0;
150
- computedIntent = intentFixture({ nextWakeAt: Date.now() + 10 * 60_000 });
151
- setBackgroundWakeIntentComputerForTest(() => {
152
- computeCalls += 1;
153
- return computedIntent;
154
- });
155
- });
156
-
157
- afterEach(async () => {
158
- await flushBackgroundWakeDrainsForTest();
159
- setBackgroundWakeIntentComputerForTest(null);
160
- setBackgroundWakeLeaseClientForTest(null);
161
- clearBackgroundWakeRuntime();
162
- });
163
-
164
- test("intent route returns the current computed wake intent", async () => {
165
- const handler = findHandler("getBackgroundWakeIntent");
166
-
167
- expect(await handler({})).toEqual({ intent: computedIntent });
168
- expect(computeCalls).toBe(1);
169
- });
170
-
171
- test("intent route returns null when no wake intent exists", async () => {
172
- computedIntent = null;
173
- const handler = findHandler("getBackgroundWakeIntent");
174
-
175
- expect(await handler({})).toEqual({ intent: null });
176
- });
177
-
178
- test("prepare-sleep defers when the next wake is inside the local window", async () => {
179
- computedIntent = intentFixture({ nextWakeAt: Date.now() + 30_000 });
180
- const handler = findHandler("prepareBackgroundWakeSleep");
181
-
182
- expect(await handler({})).toEqual({
183
- intent: computedIntent,
184
- deferSleep: true,
185
- });
186
- });
187
-
188
- test("prepare-sleep allows sleep when the next wake is outside the local window", async () => {
189
- computedIntent = intentFixture({ nextWakeAt: Date.now() + 5 * 60_000 });
190
- const handler = findHandler("prepareBackgroundWakeSleep");
191
-
192
- expect(await handler({})).toEqual({
193
- intent: computedIntent,
194
- deferSleep: false,
195
- });
196
- });
197
-
198
- test("drain-due fails when the scheduler or heartbeat is unavailable", async () => {
199
- const handler = findHandler("drainDueBackgroundWake");
200
-
201
- await expect(handler({ body: drainBodyFixture() })).rejects.toThrow(
202
- "Background wake runtime is not available",
203
- );
204
- });
205
-
206
- test("drain-due invokes due heartbeat and scheduler work", async () => {
207
- const dueAt = Date.now() - 1;
208
- const recomputedIntent = intentFixture({
209
- nextWakeAt: Date.now() + 60_000,
210
- sourceGeneration: "next-generation",
211
- });
212
- computedIntent = intentFixture({
213
- nextWakeAt: dueAt,
214
- actualNextDueAt: dueAt,
215
- reason: "mixed",
216
- });
217
- const heartbeatRunCloud = mock(async () => ({
218
- due: true,
219
- completed: 1,
220
- skipped: 0,
221
- }));
222
- const schedulerRunDue = mock(async () => {
223
- computedIntent = recomputedIntent;
224
- return schedulerResultFixture({ claimed: 2, completed: 2 });
225
- });
226
- setBackgroundWakeRuntime({
227
- heartbeat: {
228
- nextRunAt: dueAt,
229
- runCloudWakeIfDue: heartbeatRunCloud,
230
- },
231
- scheduler: {
232
- runOnce: mock(async () => 2),
233
- runDueWorkOnce: schedulerRunDue,
234
- },
235
- });
236
- const handler = findHandler("drainDueBackgroundWake");
237
-
238
- const response = await handler({
239
- body: drainBodyFixture({ reason: "mixed" }),
240
- });
241
- await flushBackgroundWakeDrainsForTest();
242
-
243
- expect(heartbeatRunCloud).toHaveBeenCalledTimes(1);
244
- expect(heartbeatRunCloud).toHaveBeenCalledWith(
245
- expect.objectContaining({ scheduledFor: NOW }),
246
- );
247
- expect(firstCallArg(heartbeatRunCloud)).not.toHaveProperty("assumeDue");
248
- expect(schedulerRunDue).toHaveBeenCalledTimes(1);
249
- expect(response).toEqual(acceptedResponse({ reason: "mixed" }));
250
- expect(lastCompletionPayload()).toEqual({
251
- leaseId: "lease-123",
252
- status: "completed",
253
- nextIntent: recomputedIntent,
254
- });
255
- });
256
-
257
- test("drain-due skips heartbeat when it is not due and reports no scheduler work", async () => {
258
- const heartbeatRunCloud = mock(async () => ({
259
- due: true,
260
- completed: 1,
261
- skipped: 0,
262
- }));
263
- const schedulerRunDue = mock(async () => schedulerResultFixture());
264
- setBackgroundWakeRuntime({
265
- heartbeat: {
266
- nextRunAt: Date.now() + 5 * 60_000,
267
- runCloudWakeIfDue: heartbeatRunCloud,
268
- },
269
- scheduler: {
270
- runOnce: mock(async () => 0),
271
- runDueWorkOnce: schedulerRunDue,
272
- },
273
- });
274
- const handler = findHandler("drainDueBackgroundWake");
275
-
276
- const response = await handler({ body: drainBodyFixture() });
277
- await flushBackgroundWakeDrainsForTest();
278
-
279
- expect(heartbeatRunCloud).not.toHaveBeenCalled();
280
- expect(schedulerRunDue).toHaveBeenCalledTimes(1);
281
- expect(response).toEqual(acceptedResponse());
282
- expect(lastCompletionPayload()).toMatchObject({
283
- leaseId: "lease-123",
284
- status: "completed",
285
- });
286
- });
287
-
288
- test("drain-due recomputes the next intent after due work drains", async () => {
289
- const recomputedIntent = intentFixture({
290
- nextWakeAt: NOW + 2 * 60_000,
291
- sourceGeneration: "after-drain",
292
- });
293
- const schedulerRunOnce = mock(async () => {
294
- computedIntent = recomputedIntent;
295
- return schedulerResultFixture({ claimed: 1, completed: 1 });
296
- });
297
- setBackgroundWakeRuntime({
298
- heartbeat: {
299
- nextRunAt: null,
300
- runCloudWakeIfDue: mock(async () => ({
301
- due: false,
302
- completed: 0,
303
- skipped: 0,
304
- })),
305
- },
306
- scheduler: {
307
- runOnce: mock(async () => 1),
308
- runDueWorkOnce: schedulerRunOnce,
309
- },
310
- });
311
- const handler = findHandler("drainDueBackgroundWake");
312
-
313
- const response = await handler({ body: drainBodyFixture() });
314
- await flushBackgroundWakeDrainsForTest();
315
-
316
- expect(response).toEqual(acceptedResponse());
317
- expect(lastCompletionPayload().nextIntent).toEqual(recomputedIntent);
318
- });
319
-
320
- test("drain-due runs scheduler backlog on heartbeat-only drains", async () => {
321
- const dueAt = Date.now() - 1;
322
- const recomputedIntent = intentFixture({
323
- nextWakeAt: Date.now() + 60_000,
324
- sourceGeneration: "after-heartbeat-drain",
325
- });
326
- computedIntent = intentFixture({
327
- nextWakeAt: dueAt,
328
- actualNextDueAt: dueAt,
329
- reason: "heartbeat",
330
- });
331
- const heartbeatRunCloud = mock(async () => ({
332
- due: true,
333
- completed: 1,
334
- skipped: 0,
335
- }));
336
- const schedulerRunDue = mock(async () => {
337
- computedIntent = recomputedIntent;
338
- return schedulerResultFixture({ claimed: 1, completed: 1 });
339
- });
340
- setBackgroundWakeRuntime({
341
- heartbeat: {
342
- nextRunAt: dueAt,
343
- runCloudWakeIfDue: heartbeatRunCloud,
344
- },
345
- scheduler: {
346
- runOnce: mock(async () => 1),
347
- runDueWorkOnce: schedulerRunDue,
348
- },
349
- });
350
- const handler = findHandler("drainDueBackgroundWake");
351
-
352
- const response = await handler({
353
- body: drainBodyFixture({ reason: "heartbeat" }),
354
- });
355
- await flushBackgroundWakeDrainsForTest();
356
-
357
- expect(heartbeatRunCloud).toHaveBeenCalledWith(
358
- expect.objectContaining({
359
- scheduledFor: NOW,
360
- }),
361
- );
362
- expect(firstCallArg(heartbeatRunCloud)).not.toHaveProperty("assumeDue");
363
- expect(schedulerRunDue).toHaveBeenCalledTimes(1);
364
- expect(response).toEqual(acceptedResponse({ reason: "heartbeat" }));
365
- expect(lastCompletionPayload()).toMatchObject({
366
- leaseId: "lease-123",
367
- status: "completed",
368
- nextIntent: recomputedIntent,
369
- });
370
- });
371
-
372
- test("drain-due does not rerun heartbeat for stale heartbeat drain requests", async () => {
373
- const heartbeatRunCloud = mock(async () => ({
374
- due: true,
375
- completed: 1,
376
- skipped: 0,
377
- }));
378
- const schedulerRunDue = mock(async () => schedulerResultFixture());
379
- setBackgroundWakeRuntime({
380
- heartbeat: {
381
- nextRunAt: Date.now() + 30 * 60_000,
382
- runCloudWakeIfDue: heartbeatRunCloud,
383
- },
384
- scheduler: {
385
- runOnce: mock(async () => 0),
386
- runDueWorkOnce: schedulerRunDue,
387
- },
388
- });
389
- const handler = findHandler("drainDueBackgroundWake");
390
-
391
- const response = await handler({
392
- body: drainBodyFixture({ reason: "heartbeat" }),
393
- });
394
- await flushBackgroundWakeDrainsForTest();
395
-
396
- expect(heartbeatRunCloud).not.toHaveBeenCalled();
397
- expect(schedulerRunDue).not.toHaveBeenCalled();
398
- expect(response).toEqual(acceptedResponse({ reason: "heartbeat" }));
399
- expect(lastCompletionPayload()).toMatchObject({
400
- leaseId: "lease-123",
401
- status: "completed",
402
- });
403
- });
404
-
405
- test("drain-due runs heartbeat when the local intent is due at wake", async () => {
406
- const dueAt = Date.now() - 1;
407
- computedIntent = intentFixture({
408
- nextWakeAt: dueAt,
409
- actualNextDueAt: dueAt,
410
- reason: "heartbeat",
411
- });
412
- const heartbeatRunCloud = mock(async () => ({
413
- due: true,
414
- completed: 1,
415
- skipped: 0,
416
- }));
417
- setBackgroundWakeRuntime({
418
- heartbeat: {
419
- nextRunAt: dueAt,
420
- runCloudWakeIfDue: heartbeatRunCloud,
421
- },
422
- scheduler: {
423
- runOnce: mock(async () => 0),
424
- runDueWorkOnce: mock(async () => schedulerResultFixture()),
425
- },
426
- });
427
- const handler = findHandler("drainDueBackgroundWake");
428
-
429
- const response = await handler({
430
- body: drainBodyFixture({ reason: "refresh" }),
431
- });
432
- await flushBackgroundWakeDrainsForTest();
433
-
434
- expect(heartbeatRunCloud).toHaveBeenCalledWith(
435
- expect.objectContaining({ scheduledFor: NOW }),
436
- );
437
- expect(firstCallArg(heartbeatRunCloud)).not.toHaveProperty("assumeDue");
438
- expect(response).toEqual(acceptedResponse({ reason: "refresh" }));
439
- });
440
-
441
- test("drain-due runs scheduler when schedule work is due at wake", async () => {
442
- const dueAt = Date.now() - 1;
443
- computedIntent = intentFixture({
444
- nextWakeAt: dueAt,
445
- actualNextDueAt: dueAt,
446
- reason: "schedule",
447
- });
448
- const schedulerRunDue = mock(async () =>
449
- schedulerResultFixture({ claimed: 1, completed: 1 }),
450
- );
451
- setBackgroundWakeRuntime({
452
- heartbeat: {
453
- nextRunAt: Date.now() + 60_000,
454
- runCloudWakeIfDue: mock(async () => ({
455
- due: false,
456
- completed: 0,
457
- skipped: 0,
458
- })),
459
- },
460
- scheduler: {
461
- runOnce: mock(async () => 1),
462
- runDueWorkOnce: schedulerRunDue,
463
- },
464
- });
465
- const handler = findHandler("drainDueBackgroundWake");
466
-
467
- const response = await handler({
468
- body: drainBodyFixture({ reason: "refresh" }),
469
- });
470
- await flushBackgroundWakeDrainsForTest();
471
-
472
- expect(schedulerRunDue).toHaveBeenCalledTimes(1);
473
- expect(response).toEqual(acceptedResponse({ reason: "refresh" }));
474
- expect(lastCompletionPayload()).toMatchObject({
475
- leaseId: "lease-123",
476
- status: "completed",
477
- });
478
- });
479
-
480
- test("drain-due runs due heartbeat even when schedule is the earliest intent source", async () => {
481
- const now = Date.now();
482
- const scheduleDueAt = now - 60_000;
483
- const heartbeatDueAt = now - 1_000;
484
- const recomputedIntent = intentFixture({
485
- nextWakeAt: now + 60_000,
486
- sourceGeneration: "after-mixed-backlog",
487
- });
488
- computedIntent = intentFixture({
489
- nextWakeAt: scheduleDueAt,
490
- actualNextDueAt: scheduleDueAt,
491
- reason: "schedule",
492
- });
493
- const heartbeatRunCloud = mock(async () => ({
494
- due: true,
495
- completed: 1,
496
- skipped: 0,
497
- }));
498
- const schedulerRunDue = mock(async () => {
499
- computedIntent = recomputedIntent;
500
- return schedulerResultFixture({ claimed: 1, completed: 1 });
501
- });
502
- setBackgroundWakeRuntime({
503
- heartbeat: {
504
- nextRunAt: heartbeatDueAt,
505
- runCloudWakeIfDue: heartbeatRunCloud,
506
- },
507
- scheduler: {
508
- runOnce: mock(async () => 1),
509
- runDueWorkOnce: schedulerRunDue,
510
- },
511
- });
512
- const handler = findHandler("drainDueBackgroundWake");
513
-
514
- const response = await handler({
515
- body: drainBodyFixture({ reason: "schedule" }),
516
- });
517
- await flushBackgroundWakeDrainsForTest();
518
-
519
- expect(heartbeatRunCloud).toHaveBeenCalledWith(
520
- expect.objectContaining({ scheduledFor: NOW }),
521
- );
522
- expect(schedulerRunDue).toHaveBeenCalledTimes(1);
523
- expect(response).toEqual(acceptedResponse({ reason: "schedule" }));
524
- expect(lastCompletionPayload()).toMatchObject({
525
- leaseId: "lease-123",
526
- status: "completed",
527
- nextIntent: recomputedIntent,
528
- });
529
- });
530
-
531
- test("drain-due does no work for refresh-only wakes with no due intent", async () => {
532
- const heartbeatRunCloud = mock(async () => ({
533
- due: true,
534
- completed: 1,
535
- skipped: 0,
536
- }));
537
- const schedulerRunDue = mock(async () =>
538
- schedulerResultFixture({ completed: 1 }),
539
- );
540
- setBackgroundWakeRuntime({
541
- heartbeat: {
542
- nextRunAt: Date.now() + 60_000,
543
- runCloudWakeIfDue: heartbeatRunCloud,
544
- },
545
- scheduler: {
546
- runOnce: mock(async () => 1),
547
- runDueWorkOnce: schedulerRunDue,
548
- },
549
- });
550
- const handler = findHandler("drainDueBackgroundWake");
551
-
552
- const response = await handler({
553
- body: drainBodyFixture({ reason: "refresh" }),
554
- });
555
- await flushBackgroundWakeDrainsForTest();
556
-
557
- expect(heartbeatRunCloud).not.toHaveBeenCalled();
558
- expect(schedulerRunDue).not.toHaveBeenCalled();
559
- expect(response).toEqual(acceptedResponse({ reason: "refresh" }));
560
- expect(lastCompletionPayload()).toMatchObject({
561
- leaseId: "lease-123",
562
- status: "completed",
563
- });
564
- });
565
-
566
- test("drain-due honors deadline exhaustion before starting heartbeat work", async () => {
567
- const dueAt = Date.now() - 1;
568
- computedIntent = intentFixture({
569
- nextWakeAt: dueAt,
570
- actualNextDueAt: dueAt,
571
- reason: "mixed",
572
- });
573
- const heartbeatRunCloud = mock(async () => ({
574
- due: true,
575
- completed: 1,
576
- skipped: 0,
577
- }));
578
- const schedulerRunDue = mock(async () =>
579
- schedulerResultFixture({ skipped: 2 }),
580
- );
581
- setBackgroundWakeRuntime({
582
- heartbeat: {
583
- nextRunAt: dueAt,
584
- runCloudWakeIfDue: heartbeatRunCloud,
585
- },
586
- scheduler: {
587
- runOnce: mock(async () => 0),
588
- runDueWorkOnce: schedulerRunDue,
589
- },
590
- });
591
- const handler = findHandler("drainDueBackgroundWake");
592
-
593
- const response = await handler({
594
- body: drainBodyFixture({
595
- reason: "mixed",
596
- deadlineAt: Date.now() + 1_000,
597
- }),
598
- });
599
- await flushBackgroundWakeDrainsForTest();
600
-
601
- expect(heartbeatRunCloud).not.toHaveBeenCalled();
602
- expect(schedulerRunDue).toHaveBeenCalledWith(
603
- expect.objectContaining({ minStartBudgetMs: 5_000 }),
604
- );
605
- expect(response).toMatchObject({
606
- accepted: true,
607
- reason: "mixed",
608
- });
609
- expect(lastCompletionPayload()).toMatchObject({
610
- leaseId: "lease-123",
611
- status: "completed",
612
- });
613
- });
614
-
615
- test("drain-due completes expired without starting when the lease deadline already elapsed", async () => {
616
- const dueAt = Date.now() - 1;
617
- computedIntent = intentFixture({
618
- nextWakeAt: dueAt,
619
- actualNextDueAt: dueAt,
620
- reason: "mixed",
621
- });
622
- const heartbeatRunCloud = mock(async () => ({
623
- due: true,
624
- completed: 1,
625
- skipped: 0,
626
- }));
627
- const schedulerRunDue = mock(async () =>
628
- schedulerResultFixture({ claimed: 1, completed: 1 }),
629
- );
630
- setBackgroundWakeRuntime({
631
- heartbeat: {
632
- nextRunAt: dueAt,
633
- runCloudWakeIfDue: heartbeatRunCloud,
634
- },
635
- scheduler: {
636
- runOnce: mock(async () => 1),
637
- runDueWorkOnce: schedulerRunDue,
638
- },
639
- });
640
- const handler = findHandler("drainDueBackgroundWake");
641
-
642
- const response = await handler({
643
- body: drainBodyFixture({
644
- reason: "mixed",
645
- deadlineAt: Date.now() - 1,
646
- }),
647
- });
648
- await flushBackgroundWakeDrainsForTest();
649
-
650
- expect(heartbeatRunCloud).not.toHaveBeenCalled();
651
- expect(schedulerRunDue).not.toHaveBeenCalled();
652
- expect(response).toMatchObject({
653
- accepted: true,
654
- reason: "mixed",
655
- });
656
- expect(lastCompletionPayload()).toMatchObject({
657
- leaseId: "lease-123",
658
- status: "expired",
659
- error: "background wake lease deadline elapsed before starting work",
660
- });
661
- });
662
-
663
- test("drain-due keeps a lease active until slow work settles after the original deadline", async () => {
664
- const schedulerResolvers: Array<() => void> = [];
665
- const schedulerRunDue = mock(async () => {
666
- await new Promise<void>((resolve) => schedulerResolvers.push(resolve));
667
- return schedulerResultFixture({ claimed: 1, completed: 1 });
668
- });
669
- setBackgroundWakeRuntime({
670
- heartbeat: {
671
- nextRunAt: Date.now() + 60_000,
672
- runCloudWakeIfDue: mock(async () => ({
673
- due: false,
674
- completed: 0,
675
- skipped: 0,
676
- })),
677
- },
678
- scheduler: {
679
- runOnce: mock(async () => 1),
680
- runDueWorkOnce: schedulerRunDue,
681
- },
682
- });
683
- const handler = findHandler("drainDueBackgroundWake");
684
- // Keep the deadline far enough ahead that the synchronous start-check always
685
- // sees it as live; the wait below outlasts it, so completion is verified to
686
- // hold until the work settles rather than fire when the deadline elapses.
687
- const deadlineAt = Date.now() + 200;
688
-
689
- const firstResponse = await handler({
690
- body: drainBodyFixture({
691
- leaseId: "lease-long-running",
692
- deadlineAt,
693
- }),
694
- });
695
-
696
- try {
697
- await new Promise((resolve) => setTimeout(resolve, 280));
698
- expect(mockCompleteBackgroundWakeLease).not.toHaveBeenCalled();
699
-
700
- const duplicateResponse = await handler({
701
- body: drainBodyFixture({
702
- leaseId: "lease-long-running",
703
- deadlineAt,
704
- }),
705
- });
706
-
707
- expect(firstResponse).toMatchObject({
708
- accepted: true,
709
- leaseId: "lease-long-running",
710
- });
711
- expect(duplicateResponse).toMatchObject({
712
- accepted: true,
713
- leaseId: "lease-long-running",
714
- });
715
- expect(schedulerRunDue).toHaveBeenCalledTimes(1);
716
- } finally {
717
- for (const resolve of schedulerResolvers) {
718
- resolve();
719
- }
720
- }
721
-
722
- await flushBackgroundWakeDrainsForTest();
723
- expect(schedulerRunDue).toHaveBeenCalledTimes(1);
724
- expect(mockCompleteBackgroundWakeLease).toHaveBeenCalledTimes(1);
725
- expect(lastCompletionPayload()).toMatchObject({
726
- leaseId: "lease-long-running",
727
- status: "completed",
728
- });
729
- });
730
-
731
- test("drain-due runs scheduler work even when pending work remains", async () => {
732
- const schedulerRunDue = mock(async () =>
733
- schedulerResultFixture({ claimed: 1, completed: 1 }),
734
- );
735
- setBackgroundWakeRuntime({
736
- heartbeat: {
737
- nextRunAt: Date.now() + 60_000,
738
- runCloudWakeIfDue: mock(async () => ({
739
- due: false,
740
- completed: 0,
741
- skipped: 0,
742
- })),
743
- },
744
- scheduler: {
745
- runOnce: mock(async () => 1),
746
- runDueWorkOnce: schedulerRunDue,
747
- },
748
- });
749
- const handler = findHandler("drainDueBackgroundWake");
750
-
751
- const response = await handler({ body: drainBodyFixture() });
752
- await flushBackgroundWakeDrainsForTest();
753
-
754
- expect(schedulerRunDue).toHaveBeenCalledTimes(1);
755
- expect(response).toEqual(acceptedResponse());
756
- expect(lastCompletionPayload()).toMatchObject({
757
- leaseId: "lease-123",
758
- status: "completed",
759
- });
760
- });
761
-
762
- test("drain-due completes failed when due work throws", async () => {
763
- const schedulerRunDue = mock(async () => {
764
- throw new Error("scheduler exploded");
765
- });
766
- setBackgroundWakeRuntime({
767
- heartbeat: {
768
- nextRunAt: null,
769
- runCloudWakeIfDue: mock(async () => ({
770
- due: false,
771
- completed: 0,
772
- skipped: 0,
773
- })),
774
- },
775
- scheduler: {
776
- runOnce: mock(async () => 0),
777
- runDueWorkOnce: schedulerRunDue,
778
- },
779
- });
780
- const handler = findHandler("drainDueBackgroundWake");
781
-
782
- const response = await handler({ body: drainBodyFixture() });
783
- await flushBackgroundWakeDrainsForTest();
784
-
785
- expect(response).toEqual(acceptedResponse());
786
- expect(lastCompletionPayload()).toMatchObject({
787
- leaseId: "lease-123",
788
- status: "failed",
789
- error: "scheduler exploded",
790
- });
791
- });
792
-
793
- test("drain-due does not reclassify successful work when completion reporting fails", async () => {
794
- const completionError = new Error("platform unavailable");
795
- mockCompleteBackgroundWakeLease.mockImplementation(async (args) => {
796
- if (args.status === "completed" || args.status === "expired") {
797
- throw completionError;
798
- }
799
- return { status: "completed" as const, httpStatus: 200 };
800
- });
801
- const schedulerRunDue = mock(async () => schedulerResultFixture());
802
- setBackgroundWakeRuntime({
803
- heartbeat: {
804
- nextRunAt: null,
805
- runCloudWakeIfDue: mock(async () => ({
806
- due: false,
807
- completed: 0,
808
- skipped: 0,
809
- })),
810
- },
811
- scheduler: {
812
- runOnce: mock(async () => 0),
813
- runDueWorkOnce: schedulerRunDue,
814
- },
815
- });
816
- const handler = findHandler("drainDueBackgroundWake");
817
-
818
- const response = await handler({ body: drainBodyFixture() });
819
- await flushBackgroundWakeDrainsForTest();
820
-
821
- expect(response).toEqual(acceptedResponse());
822
- expect(mockCompleteBackgroundWakeLease).toHaveBeenCalledTimes(1);
823
- expect(lastCompletionPayload()).toMatchObject({
824
- leaseId: "lease-123",
825
- status: "completed",
826
- });
827
- });
828
-
829
- test("drain-due accepts vembda snake_case payload aliases", async () => {
830
- const schedulerRunDue = mock(async () => schedulerResultFixture());
831
- setBackgroundWakeRuntime({
832
- heartbeat: {
833
- nextRunAt: null,
834
- runCloudWakeIfDue: mock(async () => ({
835
- due: false,
836
- completed: 0,
837
- skipped: 0,
838
- })),
839
- },
840
- scheduler: {
841
- runOnce: mock(async () => 0),
842
- runDueWorkOnce: schedulerRunDue,
843
- },
844
- });
845
- const handler = findHandler("drainDueBackgroundWake");
846
-
847
- const response = await handler({
848
- body: {
849
- lease_id: "lease-snake",
850
- reason: "refresh",
851
- source_generation: "generation-snake",
852
- started_at: new Date(NOW).toISOString(),
853
- deadline_at: String(NOW + 30_000),
854
- },
855
- });
856
- await flushBackgroundWakeDrainsForTest();
857
-
858
- expect(response).toMatchObject({
859
- accepted: true,
860
- leaseId: "lease-snake",
861
- reason: "refresh",
862
- sourceGeneration: "generation-snake",
863
- startedAt: NOW,
864
- deadlineAt: NOW + 30_000,
865
- });
866
- expect(lastCompletionPayload()).toMatchObject({
867
- leaseId: "lease-snake",
868
- status: "completed",
869
- });
870
- });
871
- });
872
-
873
- function drainBodyFixture(
874
- overrides: Partial<{
875
- leaseId: string;
876
- reason: string;
877
- sourceGeneration: string;
878
- startedAt: number;
879
- deadlineAt: number;
880
- }> = {},
881
- ) {
882
- return {
883
- leaseId: "lease-123",
884
- reason: "schedule",
885
- sourceGeneration: "source-generation",
886
- startedAt: NOW,
887
- deadlineAt: NOW + 30_000,
888
- ...overrides,
889
- };
890
- }
891
-
892
- function intentFixture(overrides: Partial<MockIntent>): MockIntent {
893
- const nextWakeAt = overrides.nextWakeAt ?? NOW + 60_000;
894
- return {
895
- nextWakeAt,
896
- actualNextDueAt: nextWakeAt,
897
- reason: "schedule",
898
- sourceGeneration: "source-generation",
899
- computedAt: NOW - 1_000,
900
- sourcePayload: {
901
- heartbeat: null,
902
- schedules: [],
903
- },
904
- ...overrides,
905
- };
906
- }