@vellumai/assistant 0.11.1-dev.202608032034.aebd2da → 0.11.1-dev.202608032226.351a4e2

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 (47) hide show
  1. package/openapi.yaml +1 -0
  2. package/package.json +1 -1
  3. package/src/__tests__/agent-loop-resume-interrupted.test.ts +223 -0
  4. package/src/__tests__/conversation-agent-loop.test.ts +16 -7
  5. package/src/__tests__/conversation-error.test.ts +106 -0
  6. package/src/__tests__/llm-resolver.test.ts +16 -0
  7. package/src/__tests__/provider-send-message-override-profile.test.ts +95 -0
  8. package/src/__tests__/workspace-migration-137-repair-retired-fireworks-minimax-model-id.test.ts +6 -6
  9. package/src/__tests__/workspace-migration-138-backfill-home-feed-titles.test.ts +373 -0
  10. package/src/agent/loop.ts +36 -7
  11. package/src/cli/commands/notifications.help.ts +3 -2
  12. package/src/config/llm-resolver.ts +19 -3
  13. package/src/daemon/conversation-agent-loop-handlers.ts +15 -6
  14. package/src/daemon/conversation-agent-loop.ts +31 -18
  15. package/src/daemon/conversation-error.ts +40 -24
  16. package/src/home/__tests__/feed-types.test.ts +43 -8
  17. package/src/home/__tests__/feed-writer.test.ts +59 -0
  18. package/src/home/feed-types.ts +34 -7
  19. package/src/home/feed-writer.ts +15 -6
  20. package/src/live-voice/__tests__/activity-label.test.ts +68 -0
  21. package/src/live-voice/__tests__/live-activity-reporter.test.ts +86 -5
  22. package/src/live-voice/__tests__/live-voice-agent-turn.test.ts +14 -5
  23. package/src/live-voice/activity-label.ts +106 -0
  24. package/src/live-voice/live-activity-reporter.ts +35 -6
  25. package/src/live-voice/live-voice-session.ts +58 -0
  26. package/src/live-voice/protocol.ts +23 -0
  27. package/src/notifications/__tests__/copy-composer.test.ts +53 -3
  28. package/src/notifications/__tests__/decision-engine.test.ts +150 -20
  29. package/src/notifications/__tests__/edit-notification.test.ts +330 -0
  30. package/src/notifications/__tests__/home-feed-side-effect.test.ts +170 -12
  31. package/src/notifications/copy-composer.ts +23 -3
  32. package/src/notifications/decision-engine.ts +29 -7
  33. package/src/notifications/edit-notification.ts +8 -4
  34. package/src/notifications/home-feed-side-effect.ts +43 -6
  35. package/src/notifications/notification-utils.ts +8 -2
  36. package/src/persistence/conversation-title-service.ts +5 -170
  37. package/src/providers/__tests__/dispatch-connection-routing.test.ts +43 -2
  38. package/src/providers/__tests__/vellum-mismatch-routing.test.ts +8 -0
  39. package/src/providers/call-site-routing.ts +77 -14
  40. package/src/providers/connection-resolution.ts +42 -2
  41. package/src/providers/types.ts +7 -1
  42. package/src/runtime/routes/notification-routes.ts +4 -1
  43. package/src/util/__tests__/short-title.test.ts +229 -0
  44. package/src/util/errors.ts +15 -0
  45. package/src/util/short-title.ts +189 -0
  46. package/src/workspace/migrations/138-backfill-home-feed-titles.ts +179 -0
  47. package/src/workspace/migrations/registry.ts +2 -0
package/openapi.yaml CHANGED
@@ -21436,6 +21436,7 @@ paths:
21436
21436
  minLength: 1
21437
21437
  description: Feed item id (notif:<uuid>) or bare uuid
21438
21438
  title:
21439
+ description: New title. An empty value is ignored, never cleared.
21439
21440
  type: string
21440
21441
  body:
21441
21442
  type: string
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vellumai/assistant",
3
- "version": "0.11.1-dev.202608032034.aebd2da",
3
+ "version": "0.11.1-dev.202608032226.351a4e2",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "exports": {
@@ -0,0 +1,223 @@
1
+ /**
2
+ * Cover for the loop's interrupted-call recovery.
3
+ *
4
+ * The reported failure: the model runs several tools, the next call streams for
5
+ * a while and then dies (the upstream killed a generation that blew its decode
6
+ * deadline), and the loop surfaces the error and ends the turn — leaving the
7
+ * task half-done until the user types "continue". These tests assert the loop
8
+ * now carries on by itself, and that it still stops when carrying on would be
9
+ * pointless.
10
+ *
11
+ * The recovery is built into {@link AgentLoop}, not contributed by a plugin, so
12
+ * these run it as the loop's own behavior. The default plugin stack is
13
+ * registered because the loop fires hook chains during a turn regardless.
14
+ */
15
+
16
+ import { beforeEach, describe, expect, test } from "bun:test";
17
+
18
+ import type { AgentEvent } from "../agent/loop.js";
19
+ import { AgentLoop } from "../agent/loop.js";
20
+ import type { LLMCallSite } from "../config/schemas/llm.js";
21
+ import { resetPluginRegistryAndRegisterDefaults } from "../plugins/defaults/index.js";
22
+ import type {
23
+ Message,
24
+ Provider,
25
+ ProviderResponse,
26
+ SendMessageOptions,
27
+ ToolDefinition,
28
+ } from "../providers/types.js";
29
+ import { ProviderError } from "../util/errors.js";
30
+
31
+ const tools: ToolDefinition[] = [
32
+ {
33
+ name: "read_file",
34
+ description: "Read a file",
35
+ input_schema: { type: "object", properties: { path: { type: "string" } } },
36
+ },
37
+ ];
38
+
39
+ const userMessage: Message = {
40
+ role: "user",
41
+ content: [{ type: "text", text: "finish the course page" }],
42
+ };
43
+
44
+ /** The rejection from the reported turn, thrown from inside a live stream. */
45
+ function midStreamRejection(): ProviderError {
46
+ return new ProviderError(
47
+ "OpenAI-compatible API error (unknown status): litellm.MidStreamFallbackError: " +
48
+ "litellm.APIConnectionError: OpenAIException - Decode wall clock timeout after 600s",
49
+ "openai-compatible",
50
+ );
51
+ }
52
+
53
+ /** A scripted turn: return a response, or stream `thinking` and then throw. */
54
+ type Turn =
55
+ | { kind: "reply"; response: ProviderResponse }
56
+ | { kind: "interrupt"; thinking: string }
57
+ | { kind: "refuse" };
58
+
59
+ function scriptedProvider(turns: Turn[]): {
60
+ provider: Provider;
61
+ callCount: () => number;
62
+ } {
63
+ let index = 0;
64
+ const provider: Provider = {
65
+ name: "openai-compatible",
66
+ async sendMessage(
67
+ _messages: Message[],
68
+ options?: SendMessageOptions,
69
+ ): Promise<ProviderResponse> {
70
+ const turn = turns[Math.min(index, turns.length - 1)]!;
71
+ index++;
72
+ if (turn.kind === "refuse") {
73
+ // Rejected before generating: nothing ever streams.
74
+ throw midStreamRejection();
75
+ }
76
+ if (turn.kind === "interrupt") {
77
+ options?.onEvent?.({ type: "thinking_delta", thinking: turn.thinking });
78
+ throw midStreamRejection();
79
+ }
80
+ for (const block of turn.response.content) {
81
+ if (block.type === "text") {
82
+ options?.onEvent?.({ type: "text_delta", text: block.text });
83
+ }
84
+ }
85
+ return turn.response;
86
+ },
87
+ };
88
+ return { provider, callCount: () => index };
89
+ }
90
+
91
+ function reply(text: string): Turn {
92
+ return {
93
+ kind: "reply",
94
+ response: {
95
+ content: [{ type: "text", text }],
96
+ model: "glm-5.2",
97
+ usage: { inputTokens: 10, outputTokens: 5 },
98
+ stopReason: "end_turn",
99
+ },
100
+ };
101
+ }
102
+
103
+ function callsTool(id: string): Turn {
104
+ return {
105
+ kind: "reply",
106
+ response: {
107
+ content: [
108
+ { type: "tool_use", id, name: "read_file", input: { path: "app.js" } },
109
+ ],
110
+ model: "glm-5.2",
111
+ usage: { inputTokens: 10, outputTokens: 5 },
112
+ stopReason: "tool_use",
113
+ },
114
+ };
115
+ }
116
+
117
+ function runLoop(provider: Provider, callSite: LLMCallSite = "mainAgent") {
118
+ const events: AgentEvent[] = [];
119
+ const loop = new AgentLoop({
120
+ provider,
121
+ systemPrompt: "system",
122
+ conversationId: "conv-resume-e2e",
123
+ tools,
124
+ toolExecutor: async () => ({ content: "file data", isError: false }),
125
+ });
126
+ return loop
127
+ .run({
128
+ requestId: "req-resume-e2e",
129
+ messages: [userMessage],
130
+ callSite,
131
+ onEvent: (event) => {
132
+ events.push(event);
133
+ },
134
+ trust: { sourceChannel: "vellum", trustClass: "unknown" },
135
+ })
136
+ .then((result) => ({ ...result, events }));
137
+ }
138
+
139
+ function assistantTextOf(history: Message[]): string {
140
+ return history
141
+ .filter((message) => message.role === "assistant")
142
+ .flatMap((message) => message.content)
143
+ .map((block) => (block.type === "text" ? block.text : ""))
144
+ .join("");
145
+ }
146
+
147
+ describe("AgentLoop — a call interrupted mid-generation", () => {
148
+ beforeEach(() => {
149
+ resetPluginRegistryAndRegisterDefaults();
150
+ });
151
+
152
+ test("carries on instead of ending the turn", async () => {
153
+ const { provider, callCount } = scriptedProvider([
154
+ callsTool("call-1"),
155
+ { kind: "interrupt", thinking: "Now I need to understand the layout" },
156
+ reply("Done — the page is wired up."),
157
+ ]);
158
+
159
+ const { history, events } = await runLoop(provider);
160
+
161
+ // The turn finished on its own: tool call, interrupted call, resumed call.
162
+ expect(callCount()).toBe(3);
163
+ expect(assistantTextOf(history)).toContain("Done — the page is wired up.");
164
+ expect(events.filter((event) => event.type === "error")).toHaveLength(0);
165
+ });
166
+
167
+ test("keeps the tool work the interrupted turn had already done", async () => {
168
+ const { provider } = scriptedProvider([
169
+ callsTool("call-1"),
170
+ { kind: "interrupt", thinking: "reading the file" },
171
+ reply("Summary of the file."),
172
+ ]);
173
+
174
+ const { history } = await runLoop(provider);
175
+
176
+ // The resumed call re-sends the same history, so the completed tool call
177
+ // and its result are still there rather than being replayed or dropped.
178
+ const toolUses = history
179
+ .flatMap((message) => message.content)
180
+ .filter((block) => block.type === "tool_use");
181
+ const toolResults = history
182
+ .flatMap((message) => message.content)
183
+ .filter((block) => block.type === "tool_result");
184
+ expect(toolUses).toHaveLength(1);
185
+ expect(toolResults).toHaveLength(1);
186
+ });
187
+
188
+ test("surfaces the error when the resumed call is interrupted too", async () => {
189
+ const { provider, callCount } = scriptedProvider([
190
+ { kind: "interrupt", thinking: "starting" },
191
+ ]);
192
+
193
+ const { events } = await runLoop(provider);
194
+
195
+ // One resume, then the error stands rather than looping.
196
+ expect(callCount()).toBe(2);
197
+ expect(events.filter((event) => event.type === "error")).toHaveLength(1);
198
+ });
199
+
200
+ test("does not resume a background call site", async () => {
201
+ // A compaction, subagent, or other background call answers to a caller
202
+ // that owns its own failure handling; nobody is sitting there to type
203
+ // "continue" at it.
204
+ const { provider, callCount } = scriptedProvider([
205
+ { kind: "interrupt", thinking: "summarizing" },
206
+ ]);
207
+
208
+ await runLoop(provider, "compactionAgent");
209
+
210
+ expect(callCount()).toBe(1);
211
+ });
212
+
213
+ test("does not resume a request the provider refused outright", async () => {
214
+ // Nothing streamed, so re-sending the identical request would fail the same
215
+ // way; the error surfaces on the first rejection.
216
+ const { provider, callCount } = scriptedProvider([{ kind: "refuse" }]);
217
+
218
+ const { events } = await runLoop(provider);
219
+
220
+ expect(callCount()).toBe(1);
221
+ expect(events.filter((event) => event.type === "error")).toHaveLength(1);
222
+ });
223
+ });
@@ -3022,12 +3022,20 @@ describe("session-agent-loop", () => {
3022
3022
  }));
3023
3023
 
3024
3024
  // GIVEN a real loop whose provider streams a delta — landing a debounced
3025
- // partial flush on the reserved row — then rejects, so the loop emits
3026
- // `provider_error` and `error` and exits with no `message_complete`.
3025
+ // partial flush on the reserved row — then rejects. The loop resumes a
3026
+ // call that died after it had started streaming, so the second attempt
3027
+ // is scripted to fail before streaming anything: that keeps exactly one
3028
+ // row carrying partial content, and the turn then exits via
3029
+ // `provider_error` with no `message_complete`.
3030
+ let attempt = 0;
3027
3031
  const ctx = makeCtx({
3028
3032
  loopProvider: {
3029
3033
  name: "mock-provider",
3030
3034
  async sendMessage(_messages, options) {
3035
+ attempt++;
3036
+ if (attempt > 1) {
3037
+ throw new Error("upstream 500");
3038
+ }
3031
3039
  options?.onEvent?.({ type: "text_delta", text: "hello world" });
3032
3040
  await new Promise((resolve) => setTimeout(resolve, 1100));
3033
3041
  throw new Error("upstream 500");
@@ -3051,11 +3059,12 @@ describe("session-agent-loop", () => {
3051
3059
  .split("\n");
3052
3060
  expect(orphanLines).toHaveLength(1);
3053
3061
  expect(updateMessageContentMock).toHaveBeenCalledTimes(0);
3054
- expect(deleteMessageByIdMock).toHaveBeenCalledTimes(1);
3055
- const deleteCall = deleteMessageByIdMock.mock.calls[0] as unknown as [
3056
- string,
3057
- ];
3058
- expect(deleteCall[0]).toBe("msg-orphan-with-partial");
3062
+ // Scoped to the row under test: the resumed attempt reserves a row of
3063
+ // its own, which its own cleanup deletes.
3064
+ const deletedIds = deleteMessageByIdMock.mock.calls.map(
3065
+ (call) => (call as unknown as [string])[0],
3066
+ );
3067
+ expect(deletedIds).toContain("msg-orphan-with-partial");
3059
3068
  });
3060
3069
  });
3061
3070
 
@@ -170,6 +170,112 @@ describe("classifyConversationError", () => {
170
170
  expect(result.userMessage).toContain("AI provider");
171
171
  expect(result.errorCategory).toBe("rate_limit");
172
172
  });
173
+
174
+ it("uses the ChatGPT OAuth route instead of the OpenAI registry default", () => {
175
+ providerRoutingSources.openai = "managed-proxy";
176
+ const err = new ProviderError(
177
+ "OpenAI API error (429): Too many requests",
178
+ "openai",
179
+ 429,
180
+ { reason: "rate_limited" },
181
+ );
182
+
183
+ const result = classifyConversationError(err, {
184
+ ...baseCtx,
185
+ connectionName: "chatgpt-subscription",
186
+ isManagedRoute: false,
187
+ });
188
+
189
+ expect(result.code).toBe("PROVIDER_RATE_LIMIT");
190
+ expect(result.userMessage).toContain("AI provider");
191
+ expect(result.errorCategory).toBe("rate_limit");
192
+ });
193
+
194
+ it("prefers the failed call's direct route over stale managed turn attribution", () => {
195
+ providerRoutingSources.openai = "managed-proxy";
196
+ const err = new ProviderError(
197
+ "OpenAI API error (429): Too many requests",
198
+ "openai",
199
+ 429,
200
+ { reason: "rate_limited" },
201
+ );
202
+ err.attachRouteAttribution({
203
+ connectionName: "chatgpt-subscription",
204
+ profileName: "chatgpt",
205
+ isManagedRoute: false,
206
+ });
207
+
208
+ const result = classifyConversationError(err, {
209
+ ...baseCtx,
210
+ connectionName: "vellum",
211
+ profileName: "managed",
212
+ isManagedRoute: true,
213
+ });
214
+
215
+ expect(result.code).toBe("PROVIDER_RATE_LIMIT");
216
+ expect(result.userMessage).toContain("AI provider");
217
+ expect(result.errorCategory).toBe("rate_limit");
218
+ });
219
+
220
+ it("prefers the failed call's managed fallback over stale direct turn attribution", () => {
221
+ const err = new ProviderError(
222
+ "Anthropic API error (429): Too many requests",
223
+ "anthropic",
224
+ 429,
225
+ { reason: "rate_limited" },
226
+ );
227
+ err.attachRouteAttribution({
228
+ profileName: "direct",
229
+ isManagedRoute: true,
230
+ });
231
+
232
+ const result = classifyConversationError(err, {
233
+ ...baseCtx,
234
+ connectionName: "anthropic-key",
235
+ profileName: "direct",
236
+ isManagedRoute: false,
237
+ });
238
+
239
+ expect(result.code).toBe("MANAGED_USAGE_LIMIT");
240
+ expect(result.userMessage).toContain("Vellum managed inference");
241
+ expect(result.errorCategory).toBe("managed_usage_limit");
242
+ });
243
+
244
+ it("does not apply a managed LLM route to a plain rate-limit error", () => {
245
+ const result = classifyConversationError(
246
+ new Error("429 Too Many Requests from a tool API"),
247
+ {
248
+ ...baseCtx,
249
+ isManagedRoute: true,
250
+ },
251
+ );
252
+
253
+ expect(result.code).toBe("PROVIDER_RATE_LIMIT");
254
+ expect(result.errorCategory).toBe("rate_limit");
255
+ });
256
+
257
+ it("falls back to context for fields the failed call's route omits", () => {
258
+ const err = new ProviderError("Unauthorized", "anthropic", 401);
259
+ err.attachRouteAttribution({ profileName: "direct" });
260
+
261
+ const result = classifyConversationError(err, {
262
+ ...baseCtx,
263
+ connectionName: "anthropic-key",
264
+ });
265
+
266
+ expect(result.connectionName).toBe("anthropic-key");
267
+ expect(result.profileName).toBe("direct");
268
+ });
269
+
270
+ it("still recognizes a rewrapped Vellum quota body", () => {
271
+ const result = classifyConversationError(
272
+ new Error('429 {"code":"daily_quota_exceeded"}'),
273
+ baseCtx,
274
+ );
275
+
276
+ expect(result.code).toBe("MANAGED_USAGE_LIMIT");
277
+ expect(result.errorCategory).toBe("managed_usage_limit");
278
+ });
173
279
  });
174
280
 
175
281
  describe("provider overloaded errors", () => {
@@ -6,6 +6,7 @@ import { CODE_DEFAULT_PROFILE_ENTRIES } from "../config/default-profile-catalog.
6
6
  import {
7
7
  type ResolutionFallbackReason,
8
8
  resolveCallSiteConfig,
9
+ resolveCallSiteConfigWithProfile,
9
10
  resolveDefaultProfileKey,
10
11
  resolveEffectiveProfileKey,
11
12
  } from "../config/llm-resolver.js";
@@ -859,6 +860,21 @@ describe("mix profiles", () => {
859
860
  }
860
861
  });
861
862
 
863
+ test("config and profile attribution share one mix selection", () => {
864
+ const selectedArms: string[] = [];
865
+ const resolved = resolveCallSiteConfigWithProfile("mainAgent", mixLlm, {
866
+ onMixSelected: ({ chosenProfile }) => {
867
+ selectedArms.push(chosenProfile);
868
+ },
869
+ });
870
+
871
+ expect(resolved.profileName).toBe("ab");
872
+ expect(selectedArms).toHaveLength(1);
873
+ expect(resolved.config.model).toBe(
874
+ selectedArms[0] === "a" ? "model-a" : "model-b",
875
+ );
876
+ });
877
+
862
878
  test("all dereference spots in a turn agree for the same seed", () => {
863
879
  // mainAgent (mix as activeProfile) and a non-main call site resolving the
864
880
  // same mix as its call-site profile must pick the same arm when given the
@@ -27,6 +27,7 @@ import type {
27
27
  ProviderResponse,
28
28
  SendMessageOptions,
29
29
  } from "../providers/types.js";
30
+ import { ProviderError } from "../util/errors.js";
30
31
  import { setConfig } from "./helpers/set-config.js";
31
32
 
32
33
  const DUMMY_MESSAGES: Message[] = [
@@ -239,6 +240,91 @@ describe("SendMessageOptions.config.overrideProfile", () => {
239
240
  expect(response.model).toBe("openai");
240
241
  });
241
242
 
243
+ test("CallSiteRoutingProvider attributes provider errors to the actual resolved connection", async () => {
244
+ setLlmConfig({
245
+ profiles: {
246
+ fast: {
247
+ provider: "openai",
248
+ provider_connection: "openai-conn",
249
+ model: "gpt-5.4",
250
+ },
251
+ },
252
+ });
253
+
254
+ const providerError = new ProviderError(
255
+ "OpenAI API error (429): Too many requests",
256
+ "openai",
257
+ 429,
258
+ { reason: "rate_limited" },
259
+ );
260
+ const defaultProvider = makeThrowingProvider(
261
+ "anthropic",
262
+ new Error("default provider should not be called"),
263
+ );
264
+ const altProvider = makeThrowingProvider("openai", providerError);
265
+ altProvider.routeAttribution = {
266
+ connectionName: "openai-recovered",
267
+ isManagedRoute: false,
268
+ };
269
+ const wrapped = new CallSiteRoutingProvider(
270
+ defaultProvider,
271
+ async (connectionName) =>
272
+ connectionName === "openai-conn" ? altProvider : null,
273
+ { connectionName: "vellum", isManagedRoute: true },
274
+ );
275
+
276
+ await expect(
277
+ wrapped.sendMessage(DUMMY_MESSAGES, {
278
+ config: { callSite: "mainAgent", overrideProfile: "fast" },
279
+ }),
280
+ ).rejects.toBe(providerError);
281
+ expect(providerError.routeAttribution).toEqual({
282
+ connectionName: "openai-recovered",
283
+ profileName: "fast",
284
+ isManagedRoute: false,
285
+ });
286
+ });
287
+
288
+ test("CallSiteRoutingProvider attributes soft fallback errors to the actual default connection", async () => {
289
+ setLlmConfig({
290
+ profiles: {
291
+ fast: {
292
+ provider: "openai",
293
+ provider_connection: "openai-conn",
294
+ model: "gpt-5.4",
295
+ },
296
+ },
297
+ });
298
+
299
+ const providerError = new ProviderError(
300
+ "Anthropic API error (429): Too many requests",
301
+ "anthropic",
302
+ 429,
303
+ { reason: "rate_limited" },
304
+ );
305
+ const defaultProvider = makeThrowingProvider("anthropic", providerError);
306
+ defaultProvider.routeAttribution = {
307
+ connectionName: "anthropic-default",
308
+ isManagedRoute: false,
309
+ };
310
+ const wrapped = new CallSiteRoutingProvider(
311
+ defaultProvider,
312
+ async () => null,
313
+ defaultProvider.routeAttribution,
314
+ );
315
+
316
+ await expect(
317
+ wrapped.sendMessage(DUMMY_MESSAGES, {
318
+ config: { callSite: "mainAgent", overrideProfile: "fast" },
319
+ }),
320
+ ).rejects.toBe(providerError);
321
+ expect(providerError.routeAttribution).toEqual({
322
+ connectionName: "anthropic-default",
323
+ profileName: "fast",
324
+ isManagedRoute: false,
325
+ });
326
+ });
327
+
242
328
  test("missing overrideProfile name silently falls through to base resolution", async () => {
243
329
  setLlmConfig({
244
330
  // The call-site tweak applies last in resolution, so it pins the model
@@ -328,6 +414,15 @@ describe("SendMessageOptions.config.overrideProfile", () => {
328
414
  });
329
415
  });
330
416
 
417
+ function makeThrowingProvider(name: string, error: Error): Provider {
418
+ return {
419
+ name,
420
+ async sendMessage(): Promise<ProviderResponse> {
421
+ throw error;
422
+ },
423
+ };
424
+ }
425
+
331
426
  describe("SendMessageOptions.config.forceOverrideProfile", () => {
332
427
  test("CallSiteConfiguredProvider forwards forceOverrideProfile into the send config", async () => {
333
428
  let captured: SendMessageOptions | undefined;
@@ -47,15 +47,15 @@ afterEach(() => {
47
47
  });
48
48
 
49
49
  describe("137-repair-retired-fireworks-minimax-model-id migration", () => {
50
- test("has correct migration id and is registered last", () => {
50
+ test("has correct migration id and is registered", () => {
51
51
  expect(repairRetiredFireworksMinimaxModelIdMigration.id).toBe(
52
52
  "137-repair-retired-fireworks-minimax-model-id",
53
53
  );
54
- // getLastWorkspaceMigrationId() reports the final entry as the registry
55
- // ceiling, so the highest id must stay last.
56
- expect(WORKSPACE_MIGRATIONS[WORKSPACE_MIGRATIONS.length - 1]?.id).toBe(
57
- "137-repair-retired-fireworks-minimax-model-id",
58
- );
54
+ expect(
55
+ WORKSPACE_MIGRATIONS.some(
56
+ (m) => m.id === "137-repair-retired-fireworks-minimax-model-id",
57
+ ),
58
+ ).toBe(true);
59
59
  });
60
60
 
61
61
  test("repairs the retired ID in default, call sites, and profiles", () => {