@frockbot/plugin-shell 0.1.3 → 0.2.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 (41) hide show
  1. package/frockbot.json +4 -0
  2. package/package.json +30 -28
  3. package/src/agent.ts +10 -13
  4. package/src/backend-authoring.test.ts +356 -2
  5. package/src/backend-authoring.ts +585 -20
  6. package/src/backend-composition-input.test.ts +65 -0
  7. package/src/backend-composition-input.ts +58 -0
  8. package/src/backend-composition.ts +23 -5
  9. package/src/backend-configuration.test.ts +549 -1468
  10. package/src/backend-contracts.test.ts +28 -0
  11. package/src/backend-contracts.ts +3 -1
  12. package/src/backend-execution.ts +0 -4
  13. package/src/backend-iframe-ui.test.ts +131 -0
  14. package/src/backend-image.test.ts +8 -8
  15. package/src/backend-image.ts +13 -13
  16. package/src/backend-isolate.test.ts +230 -89
  17. package/src/backend-isolate.ts +103 -200
  18. package/src/backend-package-catalog.test.ts +451 -0
  19. package/src/backend-package-catalog.ts +923 -0
  20. package/src/backend-recovery-integration.test.ts +17 -67
  21. package/src/backend-routines.ts +1 -1
  22. package/src/backend-runner-iframe.test.ts +74 -0
  23. package/src/backend-runner.ts +192 -0
  24. package/src/backend.ts +1198 -1781
  25. package/src/client/FrockBotApp.vue +44 -73
  26. package/src/client/PackageIframeHost.vue +218 -0
  27. package/src/client/PackageIframeSettings.vue +52 -0
  28. package/src/client/SendPayloadView.vue +0 -60
  29. package/src/client/index.test.ts +439 -380
  30. package/src/client/index.ts +163 -257
  31. package/src/client/model-presentation.test.ts +21 -8
  32. package/src/client/model-presentation.ts +14 -7
  33. package/src/client/package-iframe-host-message.test.ts +41 -0
  34. package/src/client/package-iframe-host-message.ts +27 -0
  35. package/src/client/styles.css +0 -27
  36. package/src/composition-views.ts +54 -0
  37. package/src/settings-links.test.ts +2 -10
  38. package/src/settings-links.ts +1 -13
  39. package/src/shared.ts +10 -13
  40. package/src/backend-assignment.test.ts +0 -161
  41. package/src/backend-assignment.ts +0 -274
@@ -5,7 +5,9 @@ import type {
5
5
  BotSettingsViewV1,
6
6
  UserSettingsViewV1,
7
7
  } from "@frockbot/configuration-core";
8
+ import type { PackageSettingDefinition } from "@frockbot/kernel-composition";
8
9
  import { createShellBotBackendContribution } from "./backend.js";
10
+ import { createIsolateCapabilityHost } from "./backend-isolate.js";
9
11
 
10
12
  class MemoryStorage {
11
13
  readonly values = new Map<string, unknown>();
@@ -51,72 +53,62 @@ class MemoryStorage {
51
53
  }
52
54
  }
53
55
 
54
- function installedUser(): UserSettingsViewV1 {
55
- return {
56
- schemaVersion: 1,
57
- revision: 1,
58
- profile: { name: "User" },
59
- packages: [{ packageId: "composio", version: "0.0.1", state: "installed" }],
60
- connections: [
61
- {
62
- connectionId: "gmail-1",
63
- packageId: "composio",
64
- connectionTypeId: "gmail",
65
- displayName: "Gmail",
66
- state: "ready",
67
- providerType: "fixture-models",
68
- safeMetadata: {},
69
- },
70
- ],
71
- };
72
- }
56
+ const MODEL_SETTING = {
57
+ id: "model",
58
+ schemaVersion: 1,
59
+ scopes: ["user", "bot"],
60
+ role: "model",
61
+ schema: {
62
+ type: "object",
63
+ properties: {
64
+ connectionId: { type: "string" },
65
+ providerModelId: { type: "string" },
66
+ },
67
+ required: ["connectionId", "providerModelId"],
68
+ additionalProperties: false,
69
+ },
70
+ } as const satisfies PackageSettingDefinition;
73
71
 
74
- async function compileAssignmentTestApplication(): ReturnType<
72
+ const TONE_SETTING = {
73
+ id: "tone",
74
+ schemaVersion: 1,
75
+ scopes: ["bot"],
76
+ schema: { type: "string", maxLength: 40 },
77
+ } as const satisfies PackageSettingDefinition;
78
+
79
+ async function compileModelTestApplication(): ReturnType<
75
80
  typeof compileFoundationApplication
76
81
  > {
77
82
  const application = await compileFoundationApplication();
83
+ const provider = application.packages.find(
84
+ (pkg) => pkg.id === "provider-flock-ai",
85
+ );
78
86
  const template = application.packages.find((pkg) => pkg.id === "settings");
79
- if (!template) throw new Error("Settings fixture Package is unavailable");
87
+ if (!provider || !template) throw new Error("Fixture Packages unavailable");
80
88
  return {
81
89
  ...application,
82
90
  packages: [
83
- ...application.packages,
91
+ ...application.packages.filter(
92
+ (pkg) => pkg.id !== provider.id && pkg.id !== "custom-models",
93
+ ),
94
+ provider,
84
95
  {
85
96
  ...template,
86
- id: "composio",
87
- specifier: "@test/composio",
97
+ id: "custom-models",
98
+ specifier: "@test/custom-models",
88
99
  version: "0.0.1",
89
100
  manifest: {
90
101
  ...template.manifest,
91
- id: "composio",
92
- displayName: "Connection fixture",
102
+ id: "custom-models",
103
+ displayName: "Custom models",
93
104
  version: "0.0.1",
94
105
  dependencies: {},
95
106
  contributions: {},
96
107
  permissions: [],
97
108
  configuration: {
98
- settings: [],
99
- connectionTypes: [
100
- {
101
- id: "gmail",
102
- displayName: "Gmail",
103
- allowMultiple: true,
104
- authorization: { kind: "grant", driverId: "fixture" },
105
- capabilities: ["gmail-tools", "gmail-send"],
106
- },
107
- ],
108
- capabilities: [
109
- {
110
- id: "gmail-tools",
111
- kind: "model",
112
- connectionTypes: ["gmail"],
113
- },
114
- {
115
- id: "gmail-send",
116
- kind: "tool",
117
- connectionTypes: ["gmail"],
118
- },
119
- ],
109
+ settings: [MODEL_SETTING, TONE_SETTING],
110
+ connectionTypes: [],
111
+ capabilities: [],
120
112
  },
121
113
  },
122
114
  },
@@ -124,34 +116,165 @@ async function compileAssignmentTestApplication(): ReturnType<
124
116
  };
125
117
  }
126
118
 
127
- function assignmentCommand(
128
- commandId: string,
129
- assignment: {
130
- packageId: string;
131
- capabilityId: string;
132
- connectionId?: string;
133
- },
134
- ): BotConfigurationCommandV1 {
119
+ function model(connectionId: string, providerModelId: string) {
120
+ return { connectionId, providerModelId };
121
+ }
122
+
123
+ function configuredUser(): UserSettingsViewV1 {
135
124
  return {
136
125
  schemaVersion: 1,
137
- type: "bot/assign-capability",
138
- commandId,
139
- botId: "primary",
140
- expectedRevision: 0,
141
- assignment: {
142
- assignmentId: commandId,
143
- ...assignment,
144
- },
126
+ revision: 1,
127
+ profile: { name: "User" },
128
+ packages: [
129
+ {
130
+ packageId: "provider-flock-ai",
131
+ version: "0.0.1",
132
+ state: "installed",
133
+ },
134
+ {
135
+ packageId: "custom-models",
136
+ version: "0.0.1",
137
+ state: "disabled",
138
+ },
139
+ ],
140
+ connections: [
141
+ {
142
+ connectionId: "flock-ai-ambient",
143
+ packageId: "provider-flock-ai",
144
+ connectionTypeId: "flock-ai-account",
145
+ displayName: "Flock AI",
146
+ state: "ready",
147
+ providerType: "flock-ai",
148
+ generation: "foundation-generation-1",
149
+ modelCatalog: {
150
+ schemaVersion: 1,
151
+ generation: "catalog-1",
152
+ state: "fresh",
153
+ models: [
154
+ {
155
+ providerModelId: "@flock/auto",
156
+ displayName: "Platform",
157
+ capabilities: {
158
+ tools: true,
159
+ vision: false,
160
+ reasoning: false,
161
+ },
162
+ source: "discovered",
163
+ },
164
+ ],
165
+ },
166
+ safeMetadata: {},
167
+ },
168
+ ],
169
+ platformModel: model("flock-ai-ambient", "@flock/auto"),
145
170
  };
146
171
  }
147
172
 
148
- describe("Bot capability assignment admission", () => {
149
- test("rejects an unmaterialized Bot without writing durable state", async () => {
173
+ function host(storage: MemoryStorage, readUser: () => UserSettingsViewV1) {
174
+ return createShellBotBackendContribution({
175
+ state: { storage } as unknown as DurableObjectState,
176
+ env: {
177
+ CREDENTIAL_KEYRING:
178
+ '{"schemaVersion":1,"currentKeyId":"primary","keys":{"primary":"MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY"}}',
179
+ USER_CONFIGURATIONS: {
180
+ idFromName: () => "user-1",
181
+ get: () => ({
182
+ readConfiguration: () => Promise.resolve(structuredClone(readUser())),
183
+ listBots: () =>
184
+ Promise.resolve({ schemaVersion: 1, revision: 0, bots: [] }),
185
+ }),
186
+ },
187
+ MEMORY_FILES: {},
188
+ MEMORY_INDEX: {},
189
+ FLOCK_AI: {
190
+ autoRoute: "flock-auto",
191
+ runChatCompletion: () =>
192
+ Promise.resolve(
193
+ new ReadableStream({
194
+ start(controller) {
195
+ controller.enqueue(
196
+ new TextEncoder().encode(
197
+ 'data: {"choices":[{"delta":{"content":"Cordis runtime: hello"}}]}\n\n' +
198
+ 'data: {"choices":[{"delta":{},"finish_reason":"stop"}]}\n\n' +
199
+ "data: [DONE]\n\n",
200
+ ),
201
+ );
202
+ controller.close();
203
+ },
204
+ }),
205
+ ),
206
+ },
207
+ } as never,
208
+ compileApplication: compileModelTestApplication,
209
+ });
210
+ }
211
+
212
+ function request(command: BotConfigurationCommandV1) {
213
+ return {
214
+ schemaVersion: 1 as const,
215
+ userId: "user-1",
216
+ botId: command.botId,
217
+ command,
218
+ };
219
+ }
220
+
221
+ describe("Bot configuration admission", () => {
222
+ test("reads old Bot settings purely and writes the migrated shape on the next command", async () => {
150
223
  const storage = new MemoryStorage();
151
- const contribution = createShellBotBackendContribution({
152
- state: { storage } as unknown as DurableObjectState,
153
- env: {} as never,
224
+ // Literal durable shape from eb0283edcce5daea976a21a9f6a6414bedc6e2bc,
225
+ // the first parent of PR #134's merge commit.
226
+ const historical = {
227
+ schemaVersion: 1,
228
+ botId: "primary",
229
+ revision: 4,
230
+ profile: { name: "Primary" },
231
+ notifications: { enabled: true },
232
+ assignments: [],
233
+ assignmentOperations: [],
234
+ model: {
235
+ connectionId: "ollama-1",
236
+ providerModelId: "glm-5.3-flash:cloud",
237
+ },
238
+ };
239
+ await storage.put({
240
+ identity: { userId: "user-1", botId: "primary" },
241
+ "bot-configuration": historical,
242
+ });
243
+ const contribution = host(storage, configuredUser);
244
+ const identity = { userId: "user-1", botId: "primary" };
245
+
246
+ await expect(contribution.getSettings(identity)).resolves.toMatchObject({
247
+ revision: 4,
248
+ packageValues: {},
154
249
  });
250
+ expect(await storage.get<unknown>("bot-configuration")).toEqual(historical);
251
+
252
+ await contribution.executeConfiguration(
253
+ request({
254
+ schemaVersion: 1,
255
+ type: "bot/update-profile",
256
+ commandId: "migrate-bot-settings",
257
+ botId: "primary",
258
+ expectedRevision: 4,
259
+ profile: { name: "Migrated Bot" },
260
+ }),
261
+ );
262
+ const written =
263
+ await storage.get<Record<string, unknown>>("bot-configuration");
264
+ expect(written).toMatchObject({
265
+ revision: 5,
266
+ profile: { name: "Migrated Bot" },
267
+ packageValues: {},
268
+ });
269
+ expect(written).not.toHaveProperty("assignments");
270
+ expect(written).not.toHaveProperty("assignmentOperations");
271
+ expect(written).not.toHaveProperty("model");
272
+ });
273
+
274
+ test("rejects an unmaterialized Bot without writing durable state", async () => {
275
+ const storage = new MemoryStorage();
276
+ const contribution = host(storage, configuredUser);
277
+
155
278
  await expect(
156
279
  contribution.readConfiguration({
157
280
  schemaVersion: 1,
@@ -177,20 +300,18 @@ describe("Bot capability assignment admission", () => {
177
300
  const identity = { userId: "user-1", botId: "primary" };
178
301
  await contribution.materializeSettings(identity, { name: "Primary" });
179
302
  archived = true;
303
+
180
304
  await expect(
181
- contribution.executeConfiguration({
182
- schemaVersion: 1,
183
- userId: identity.userId,
184
- botId: identity.botId,
185
- command: {
305
+ contribution.executeConfiguration(
306
+ request({
186
307
  schemaVersion: 1,
187
308
  type: "bot/update-profile",
188
309
  commandId: "archived-profile",
189
310
  botId: identity.botId,
190
311
  expectedRevision: 0,
191
312
  profile: { name: "Changed" },
192
- },
193
- }),
313
+ }),
314
+ ),
194
315
  ).rejects.toThrow("archived");
195
316
  expect(await contribution.getSettings(identity)).toMatchObject({
196
317
  revision: 0,
@@ -198,7 +319,7 @@ describe("Bot capability assignment admission", () => {
198
319
  });
199
320
  });
200
321
 
201
- test("rechecks lifecycle admission in the configuration mutation transaction", async () => {
322
+ test("rechecks lifecycle admission in the mutation transaction", async () => {
202
323
  const storage = new MemoryStorage();
203
324
  let admissions = 0;
204
325
  const contribution = createShellBotBackendContribution({
@@ -211,24 +332,26 @@ describe("Bot capability assignment admission", () => {
211
332
  : Promise.reject(new Error(`Bot "${botId}" is archived`));
212
333
  },
213
334
  });
214
- const identity = { userId: "user-1", botId: "primary" };
215
- await contribution.materializeSettings(identity, { name: "Primary" });
335
+ await contribution.materializeSettings(
336
+ { userId: "user-1", botId: "primary" },
337
+ { name: "Primary" },
338
+ );
339
+
216
340
  await expect(
217
- contribution.executeConfiguration({
218
- schemaVersion: 1,
219
- userId: identity.userId,
220
- botId: identity.botId,
221
- command: {
341
+ contribution.executeConfiguration(
342
+ request({
222
343
  schemaVersion: 1,
223
344
  type: "bot/update-profile",
224
345
  commandId: "racing-profile",
225
- botId: identity.botId,
346
+ botId: "primary",
226
347
  expectedRevision: 0,
227
348
  profile: { name: "Changed" },
228
- },
229
- }),
349
+ }),
350
+ ),
230
351
  ).rejects.toThrow("archived");
231
- expect(await contribution.getSettings(identity)).toMatchObject({
352
+ expect(
353
+ await contribution.getSettings({ userId: "user-1", botId: "primary" }),
354
+ ).toMatchObject({
232
355
  revision: 0,
233
356
  profile: { name: "Primary" },
234
357
  });
@@ -242,8 +365,7 @@ describe("Bot capability assignment admission", () => {
242
365
  revision: 7,
243
366
  profile: { name: "Primary" },
244
367
  notifications: { enabled: true },
245
- assignments: [],
246
- assignmentOperations: [],
368
+ packageValues: {},
247
369
  } satisfies BotSettingsViewV1;
248
370
  await storage.put({
249
371
  identity: { userId: "user-1", botId: "primary" },
@@ -278,58 +400,23 @@ describe("Bot capability assignment admission", () => {
278
400
  });
279
401
  const contribution = createShellBotBackendContribution({
280
402
  state: { storage } as unknown as DurableObjectState,
281
- env: {
282
- USER_CONFIGURATIONS: {
283
- idFromName: () => "user-1",
284
- get: () => ({
285
- readConfiguration: () =>
286
- Promise.resolve({
287
- ...installedUser(),
288
- newBotModelTemplate: {
289
- connectionId: "provider-1",
290
- providerModelId: "model-1",
291
- },
292
- newBotModelTemplateSource: "auto",
293
- }),
294
- }),
295
- },
296
- } as never,
403
+ env: {} as never,
297
404
  });
405
+ const identity = { userId: "user-1", botId: "primary" };
298
406
 
299
- await contribution.materializeSettings(
300
- { userId: "user-1", botId: "primary" },
301
- {
302
- name: "Primary",
303
- model: { connectionId: "provider-1", providerModelId: "model-1" },
304
- },
305
- );
306
- await expect(
307
- contribution.readConfiguration({
308
- schemaVersion: 1,
309
- userId: "user-1",
310
- botId: "primary",
311
- }),
312
- ).resolves.toMatchObject({
313
- model: {
314
- connectionId: "provider-1",
315
- providerModelId: "model-1",
316
- },
407
+ await contribution.materializeSettings(identity, { name: "Primary" });
408
+ await expect(contribution.getSettings(identity)).resolves.toMatchObject({
409
+ revision: 0,
410
+ profile: { name: "Primary" },
411
+ packageValues: {},
317
412
  });
318
413
  });
319
414
 
320
415
  test("binds in-flight and durable receipts to the complete Bot command", async () => {
321
416
  const storage = new MemoryStorage();
322
- const userConfiguration = {
323
- readConfiguration: () => Promise.resolve(installedUser()),
324
- };
325
- const host = {
417
+ const backendHost = {
326
418
  state: { storage } as unknown as DurableObjectState,
327
- env: {
328
- USER_CONFIGURATIONS: {
329
- idFromName: () => "user-1",
330
- get: () => userConfiguration,
331
- },
332
- } as never,
419
+ env: {} as never,
333
420
  };
334
421
  const original: BotConfigurationCommandV1 = {
335
422
  schemaVersion: 1,
@@ -343,13 +430,7 @@ describe("Bot capability assignment admission", () => {
343
430
  ...original,
344
431
  profile: { name: "Collision" },
345
432
  };
346
- const request = (command: BotConfigurationCommandV1) => ({
347
- schemaVersion: 1 as const,
348
- userId: "user-1",
349
- botId: "primary",
350
- command,
351
- });
352
- const contribution = createShellBotBackendContribution(host);
433
+ const contribution = createShellBotBackendContribution(backendHost);
353
434
  await contribution.materializeSettings(
354
435
  { userId: "user-1", botId: "primary" },
355
436
  { name: "Primary" },
@@ -363,7 +444,7 @@ describe("Bot capability assignment admission", () => {
363
444
  );
364
445
  const receipt = await first;
365
446
 
366
- const redeployed = createShellBotBackendContribution(host);
447
+ const redeployed = createShellBotBackendContribution(backendHost);
367
448
  await expect(
368
449
  redeployed.executeConfiguration(request(original)),
369
450
  ).resolves.toEqual(receipt);
@@ -383,1375 +464,375 @@ describe("Bot capability assignment admission", () => {
383
464
  profile: { name: "Original" },
384
465
  });
385
466
  });
467
+ });
386
468
 
387
- test("durably rejects invalid assignments before dependency claims", async () => {
469
+ describe("generic per-Turn model resolution", () => {
470
+ test("projects the platform model into an otherwise unconfigured Bot's isolate list", async () => {
388
471
  const storage = new MemoryStorage();
389
- let user = installedUser();
390
- let dependencyClaims = 0;
391
- let dependencyAcknowledgements = 0;
392
- let reads = 0;
393
- let claimAuthorized = true;
394
- const dependencyStates = new Map<string, "claimed" | "acknowledged">();
395
- const userConfiguration = {
396
- readConfiguration: () => {
397
- reads += 1;
398
- return Promise.resolve(structuredClone(user));
399
- },
400
- executeConnectionDependency: (request: {
401
- action: "claim" | "read" | "acknowledge" | "release" | "reconcile";
402
- operationId: string;
403
- requirement?: unknown;
404
- }) => {
405
- if (request.action === "read") {
406
- return Promise.resolve({
407
- schemaVersion: 1 as const,
408
- status:
409
- dependencyStates.get(request.operationId) ?? ("absent" as const),
410
- });
411
- }
412
- if (request.action === "claim") {
413
- expect(request.requirement).toEqual({
414
- schemaVersion: 1,
415
- packageId: "composio",
416
- packageVersion: "0.0.1",
417
- capabilityId: "gmail-tools",
418
- connectionTypeIds: ["gmail"],
419
- });
420
- dependencyClaims += 1;
421
- if (claimAuthorized) {
422
- dependencyStates.set(request.operationId, "claimed");
423
- }
424
- return Promise.resolve(
425
- claimAuthorized
426
- ? { schemaVersion: 1 as const, status: "claimed" as const }
427
- : {
428
- schemaVersion: 1 as const,
429
- status: "rejected" as const,
430
- failure: "claim rejected",
431
- },
432
- );
433
- }
434
- if (request.action === "acknowledge") {
435
- dependencyAcknowledgements += 1;
436
- dependencyStates.set(request.operationId, "acknowledged");
437
- return Promise.resolve({
438
- schemaVersion: 1 as const,
439
- status: "acknowledged" as const,
440
- });
441
- }
442
- return Promise.resolve({
443
- schemaVersion: 1 as const,
444
- status: "released" as const,
445
- });
472
+ const contribution = host(storage, configuredUser);
473
+ const identity = { userId: "user-1", botId: "primary" };
474
+ const settings = await contribution.materializeSettings(identity, {
475
+ name: "Primary",
476
+ });
477
+ const snapshot = await (
478
+ contribution as unknown as {
479
+ isolateAuthoritySnapshot(
480
+ identity: { userId: string; botId: string },
481
+ settings: BotSettingsViewV1,
482
+ ): Promise<{
483
+ connections: Array<{
484
+ connectionId: string;
485
+ packageId: string;
486
+ connectionTypeId: string;
487
+ displayName: string;
488
+ generation: string;
489
+ safeMetadata: Record<string, unknown>;
490
+ }>;
491
+ model?: {
492
+ connectionId: string;
493
+ packageId: string;
494
+ provider: string;
495
+ providerModelId: string;
496
+ connectionGeneration: string;
497
+ catalogGeneration?: string;
498
+ };
499
+ memory: boolean;
500
+ workspace: boolean;
501
+ }>;
502
+ }
503
+ ).isolateAuthoritySnapshot(identity, settings);
504
+ const listed = await createIsolateCapabilityHost({
505
+ storage: {
506
+ put: () => Promise.resolve(),
507
+ list: () => Promise.resolve(new Map()),
446
508
  },
447
- };
448
- const contribution = createShellBotBackendContribution({
449
- state: { storage } as unknown as DurableObjectState,
450
- env: {
451
- USER_CONFIGURATIONS: {
452
- idFromName: () => "user-1",
453
- get: () => userConfiguration,
454
- },
455
- } as never,
456
- compileApplication: compileAssignmentTestApplication,
457
- });
509
+ botId: identity.botId,
510
+ packageId: "bot-authored",
511
+ generationId: "composition-1",
512
+ connections: snapshot.connections,
513
+ ...(snapshot.model ? { modelBinding: snapshot.model } : {}),
514
+ memory: snapshot.memory,
515
+ workspace: snapshot.workspace,
516
+ }).list();
517
+
518
+ expect(listed.model).toEqual({
519
+ connectionId: "flock-ai-ambient",
520
+ packageId: "provider-flock-ai",
521
+ provider: "flock-ai",
522
+ providerModelId: "@flock/auto",
523
+ connectionGeneration: "foundation-generation-1",
524
+ catalogGeneration: "catalog-1",
525
+ });
526
+ expect(Object.hasOwn(settings, "model")).toBe(false);
527
+ });
528
+
529
+ test("runs on the platform model without creating a per-Bot model record", async () => {
530
+ const storage = new MemoryStorage();
531
+ const contribution = host(storage, configuredUser);
458
532
  const identity = { userId: "user-1", botId: "primary" };
459
533
  await contribution.materializeSettings(identity, { name: "Primary" });
460
- const execute = (command: BotConfigurationCommandV1) =>
461
- contribution.executeConfiguration({
462
- schemaVersion: 1,
463
- ...identity,
464
- command,
465
- });
466
- const read = () =>
467
- contribution.readConfiguration({ schemaVersion: 1, ...identity });
534
+
535
+ const result = await contribution.run({
536
+ ...identity,
537
+ runId: "platform-model-run",
538
+ sessionId: "user-1:primary",
539
+ acceptedAt: "2026-09-02T00:00:00.000Z",
540
+ text: "hello",
541
+ });
542
+
543
+ expect(result.text).toBe("Cordis runtime: hello");
544
+ const durableEvents = storage.values.get("latest-events") as
545
+ Array<{ seq: number }> | undefined;
546
+ expect(durableEvents?.length).toBeGreaterThan(0);
547
+ expect(durableEvents?.map((event) => event.seq)).toEqual(
548
+ durableEvents?.map((_, index) => index),
549
+ );
550
+ const settings = await contribution.getSettings(identity);
551
+ expect(settings).toMatchObject({ revision: 0, packageValues: {} });
552
+ expect(Object.hasOwn(settings, "model")).toBe(false);
553
+ });
554
+
555
+ test("a Connection disabled after admission is unavailable and records a visible failure", async () => {
556
+ const storage = new MemoryStorage();
557
+ const user = configuredUser();
558
+ const contribution = host(storage, () => user);
559
+ const identity = { userId: "user-1", botId: "primary" };
560
+ await contribution.materializeSettings(identity, { name: "Primary" });
561
+ (
562
+ contribution as unknown as {
563
+ activeTurn: unknown;
564
+ }
565
+ ).activeTurn = {
566
+ runId: "run-1",
567
+ sessionId: "session-1",
568
+ turnId: "turn-1",
569
+ generationId: "composition-1",
570
+ mounted: {
571
+ generation: {
572
+ members: [
573
+ { packageId: "bot-authored", artifact: { contentHash: "hash" } },
574
+ ],
575
+ },
576
+ },
577
+ };
578
+ user.connections[0] = { ...user.connections[0]!, state: "disabled" };
468
579
 
469
580
  await expect(
470
- contribution.readConfiguration({
471
- schemaVersion: 1,
472
- userId: "user-1",
473
- botId: "../primary",
581
+ contribution.isolateConnection({
582
+ ...identity,
583
+ runId: "run-1",
584
+ sessionId: "session-1",
585
+ turnId: "turn-1",
586
+ packageId: "bot-authored",
587
+ generationId: "composition-1",
588
+ request: "flock-ai-ambient",
474
589
  }),
475
- ).rejects.toThrow("botId is invalid");
476
- await expect(
477
- contribution.executeConfiguration({
590
+ ).resolves.toEqual({
591
+ status: "unavailable",
592
+ reason: "the Connection is unavailable",
593
+ });
594
+ await expect(contribution.listNotifications()).resolves.toEqual([
595
+ expect.objectContaining({
596
+ notificationId:
597
+ "package-connection-unavailable:run-1:bot-authored:flock-ai-ambient",
598
+ title: "Connection unavailable",
599
+ }),
600
+ ]);
601
+ });
602
+
603
+ test("uses an enabled Bot-scoped model value and preserves it while disabled", async () => {
604
+ const storage = new MemoryStorage();
605
+ let user = configuredUser();
606
+ user.packages[1] = {
607
+ ...user.packages[1]!,
608
+ state: "installed",
609
+ values: { model: model("flock-ai-ambient", "account-model") },
610
+ };
611
+ const contribution = host(storage, () => user);
612
+ const identity = { userId: "user-1", botId: "primary" };
613
+ await contribution.materializeSettings(identity, { name: "Primary" });
614
+ await contribution.executeConfiguration(
615
+ request({
478
616
  schemaVersion: 1,
479
- ...identity,
480
- command: {
481
- schemaVersion: 1,
482
- type: "bot/update-profile",
483
- commandId: "malformed-profile",
484
- botId: "primary",
485
- expectedRevision: 0,
486
- profile: { name: 42 },
487
- },
617
+ type: "bot/set-package-settings",
618
+ commandId: "set-bot-model",
619
+ botId: "primary",
620
+ expectedRevision: 0,
621
+ packageId: "custom-models",
622
+ values: { model: model("flock-ai-ambient", "bot-model") },
488
623
  }),
489
- ).rejects.toThrow("profile.name must be a string");
490
- expect(await read()).toMatchObject({ revision: 0, assignments: [] });
624
+ );
491
625
 
492
- const missingConnection = assignmentCommand("missing-connection", {
493
- packageId: "composio",
494
- capabilityId: "gmail-tools",
495
- });
496
- const first = await execute(missingConnection);
497
- expect(first).toMatchObject({
498
- status: "rejected",
499
- revision: 0,
500
- failure: expect.stringContaining("requires a Connection"),
626
+ expect(await contribution.resolveConfiguration(identity)).toMatchObject({
627
+ model: model("flock-ai-ambient", "bot-model"),
501
628
  });
502
- const readsAfterFirst = reads;
503
- expect(await execute(missingConnection)).toEqual(first);
504
- expect(reads).toBe(readsAfterFirst);
505
629
 
506
- expect(
507
- await execute(
508
- assignmentCommand("unknown-capability", {
509
- packageId: "composio",
510
- capabilityId: "unknown",
511
- connectionId: "gmail-1",
512
- }),
630
+ user = {
631
+ ...user,
632
+ packages: user.packages.map((pkg) =>
633
+ pkg.packageId === "custom-models"
634
+ ? { ...pkg, state: "disabled" as const }
635
+ : pkg,
513
636
  ),
514
- ).toMatchObject({ status: "rejected", revision: 0 });
637
+ };
638
+ expect(await contribution.resolveConfiguration(identity)).toMatchObject({
639
+ model: model("flock-ai-ambient", "@flock/auto"),
640
+ });
641
+ expect(await contribution.getSettings(identity)).toMatchObject({
642
+ packageValues: {
643
+ "custom-models": {
644
+ model: model("flock-ai-ambient", "bot-model"),
645
+ },
646
+ },
647
+ });
515
648
 
516
649
  user = {
517
- ...installedUser(),
518
- connections: [
519
- { ...installedUser().connections[0]!, connectionTypeId: "calendar" },
520
- ],
521
- };
522
- expect(
523
- await execute(
524
- assignmentCommand("wrong-connection-type", {
525
- packageId: "composio",
526
- capabilityId: "gmail-tools",
527
- connectionId: "gmail-1",
528
- }),
650
+ ...user,
651
+ packages: user.packages.map((pkg) =>
652
+ pkg.packageId === "custom-models"
653
+ ? { ...pkg, state: "installed" as const }
654
+ : pkg,
529
655
  ),
530
- ).toMatchObject({ status: "rejected", revision: 0 });
656
+ };
657
+ expect(await contribution.resolveConfiguration(identity)).toMatchObject({
658
+ model: model("flock-ai-ambient", "bot-model"),
659
+ });
660
+ });
661
+
662
+ test("Package disablement and Connection revocation fail every Bot's next Turn closed", async () => {
663
+ let user = configuredUser();
664
+ const bots = ["alpha", "beta"].map((botId) => {
665
+ const storage = new MemoryStorage();
666
+ return {
667
+ botId,
668
+ storage,
669
+ contribution: host(storage, () => user),
670
+ };
671
+ });
672
+ for (const bot of bots) {
673
+ await bot.contribution.materializeSettings(
674
+ { userId: "user-1", botId: bot.botId },
675
+ { name: bot.botId },
676
+ );
677
+ }
531
678
 
532
679
  user = {
533
- ...installedUser(),
534
- packages: [{ ...installedUser().packages[0]!, state: "disabled" }],
680
+ ...user,
681
+ packages: user.packages.map((pkg) =>
682
+ pkg.packageId === "provider-flock-ai"
683
+ ? { ...pkg, state: "disabled" as const }
684
+ : pkg,
685
+ ),
535
686
  };
536
- expect(
537
- await execute(
538
- assignmentCommand("disabled-package", {
539
- packageId: "composio",
540
- capabilityId: "gmail-tools",
541
- connectionId: "gmail-1",
687
+ for (const bot of bots) {
688
+ const runId = `${bot.botId}-disabled-package`;
689
+ await expect(
690
+ bot.contribution.run({
691
+ userId: "user-1",
692
+ botId: bot.botId,
693
+ runId,
694
+ sessionId: `user-1:${bot.botId}`,
695
+ acceptedAt: "2026-09-02T00:01:00.000Z",
696
+ text: "must fail",
542
697
  }),
543
- ),
544
- ).toMatchObject({ status: "rejected", revision: 0 });
698
+ ).rejects.toThrow("not installed and enabled");
699
+ expect(await bot.storage.get(`run:${runId}`)).toMatchObject({
700
+ status: "failed",
701
+ failure: expect.stringContaining("not installed and enabled"),
702
+ });
703
+ }
545
704
 
546
- expect(dependencyClaims).toBe(0);
547
- expect(await read()).toMatchObject({
548
- revision: 0,
549
- assignments: [],
550
- } satisfies Partial<BotSettingsViewV1>);
705
+ user = configuredUser();
706
+ user.connections[0] = { ...user.connections[0]!, state: "revoked" };
707
+ for (const bot of bots) {
708
+ const runId = `${bot.botId}-revoked-connection`;
709
+ await expect(
710
+ bot.contribution.run({
711
+ userId: "user-1",
712
+ botId: bot.botId,
713
+ runId,
714
+ sessionId: `user-1:${bot.botId}`,
715
+ acceptedAt: "2026-09-02T00:02:00.000Z",
716
+ text: "must fail",
717
+ }),
718
+ ).rejects.toThrow("is revoked");
719
+ expect(await bot.storage.get(`run:${runId}`)).toMatchObject({
720
+ status: "failed",
721
+ failure: expect.stringContaining("is revoked"),
722
+ });
723
+ }
724
+ });
725
+ });
551
726
 
552
- user = installedUser();
553
- claimAuthorized = false;
554
- const changedDuringClaim = assignmentCommand("changed-during-claim", {
555
- packageId: "composio",
556
- capabilityId: "gmail-tools",
557
- connectionId: "gmail-1",
558
- });
559
- const changedReceipt = await execute(changedDuringClaim);
560
- expect(changedReceipt).toMatchObject({ status: "rejected", revision: 0 });
561
- expect(await execute(changedDuringClaim)).toEqual(changedReceipt);
562
- expect(dependencyClaims).toBe(1);
563
- expect(dependencyAcknowledgements).toBe(0);
564
- expect(await read()).toMatchObject({
565
- revision: 0,
566
- assignments: [],
567
- });
727
+ describe("Bot Package setting commands", () => {
728
+ test("validates, revision-fences, merges, and replays durably", async () => {
729
+ const storage = new MemoryStorage();
730
+ const user = configuredUser();
731
+ const contribution = host(storage, () => user);
732
+ const identity = { userId: "user-1", botId: "primary" };
733
+ await contribution.materializeSettings(identity, { name: "Primary" });
568
734
 
569
- claimAuthorized = true;
570
- expect(
571
- await execute(
572
- assignmentCommand("valid-assignment", {
573
- packageId: "composio",
574
- capabilityId: "gmail-tools",
575
- connectionId: "gmail-1",
735
+ await expect(
736
+ contribution.executeConfiguration(
737
+ request({
738
+ schemaVersion: 1,
739
+ type: "bot/set-package-settings",
740
+ commandId: "invalid-model",
741
+ botId: "primary",
742
+ expectedRevision: 0,
743
+ packageId: "custom-models",
744
+ values: { model: { connectionId: "flock-ai-ambient" } as never },
576
745
  }),
577
746
  ),
578
- ).toMatchObject({ status: "applied", revision: 1 });
579
- expect(dependencyClaims).toBe(2);
580
- expect(dependencyAcknowledgements).toBe(1);
581
- expect(await read()).toMatchObject({
747
+ ).rejects.toThrow("model has invalid fields");
748
+ expect((await contribution.getSettings(identity)).revision).toBe(0);
749
+
750
+ const first: BotConfigurationCommandV1 = {
751
+ schemaVersion: 1,
752
+ type: "bot/set-package-settings",
753
+ commandId: "set-package-values",
754
+ botId: "primary",
755
+ expectedRevision: 0,
756
+ packageId: "custom-models",
757
+ values: {
758
+ model: model("flock-ai-ambient", "bot-model"),
759
+ tone: "concise",
760
+ },
761
+ };
762
+ const receipt = await contribution.executeConfiguration(request(first));
763
+ await expect(
764
+ contribution.executeConfiguration(request(first)),
765
+ ).resolves.toEqual(receipt);
766
+ expect(await contribution.getSettings(identity)).toMatchObject({
582
767
  revision: 1,
583
- assignments: [
584
- {
585
- assignmentId: "valid-assignment",
586
- state: "enabled",
587
- connectionId: "gmail-1",
768
+ packageValues: {
769
+ "custom-models": {
770
+ model: model("flock-ai-ambient", "bot-model"),
771
+ tone: "concise",
588
772
  },
589
- ],
773
+ },
590
774
  });
591
775
 
592
- await expect(
593
- execute({
594
- schemaVersion: 1,
595
- type: "bot/assign-capability",
596
- commandId: "reuse-assignment-authority",
597
- botId: "primary",
776
+ await contribution.executeConfiguration(
777
+ request({
778
+ ...first,
779
+ commandId: "update-model-only",
598
780
  expectedRevision: 1,
599
- assignment: {
600
- assignmentId: "valid-assignment",
601
- packageId: "composio",
602
- capabilityId: "gmail-send",
603
- connectionId: "gmail-1",
604
- },
781
+ values: { model: model("flock-ai-ambient", "new-bot-model") },
605
782
  }),
606
- ).resolves.toMatchObject({
607
- status: "rejected",
608
- revision: 1,
609
- failure: "Assignment ID cannot change Package Capability authority",
783
+ );
784
+ expect(await contribution.getSettings(identity)).toMatchObject({
785
+ revision: 2,
786
+ packageValues: {
787
+ "custom-models": {
788
+ model: model("flock-ai-ambient", "new-bot-model"),
789
+ tone: "concise",
790
+ },
791
+ },
610
792
  });
611
- expect(dependencyClaims).toBe(2);
612
- });
613
793
 
614
- test("atomically binds and durably unbinds a Connection model", async () => {
615
- const storage = new MemoryStorage();
616
- let dependencyGeneration: string | undefined;
617
- let releaseAttempts = 0;
618
- const userConfiguration = {
619
- readConfiguration: () => Promise.resolve(installedUser()),
620
- executeConnectionDependency: (request: {
621
- action: "claim" | "read" | "acknowledge" | "release" | "reconcile";
622
- generation: string;
623
- }) => {
624
- if (request.action === "claim") {
625
- dependencyGeneration = request.generation;
626
- return Promise.resolve({
627
- schemaVersion: 1 as const,
628
- status: "claimed" as const,
629
- });
630
- }
631
- if (request.action === "acknowledge") {
632
- return Promise.resolve({
633
- schemaVersion: 1 as const,
634
- status: "acknowledged" as const,
635
- });
636
- }
637
- if (request.action === "read") {
638
- return Promise.resolve({
639
- schemaVersion: 1 as const,
640
- status:
641
- request.generation === dependencyGeneration
642
- ? ("acknowledged" as const)
643
- : ("absent" as const),
644
- });
645
- }
646
- if (request.action === "release") {
647
- releaseAttempts += 1;
648
- if (releaseAttempts === 1) {
649
- return Promise.resolve({
650
- schemaVersion: 1 as const,
651
- status: "pending" as const,
652
- });
653
- }
654
- if (request.generation === dependencyGeneration) {
655
- dependencyGeneration = undefined;
656
- }
657
- return Promise.resolve({
658
- schemaVersion: 1 as const,
659
- status: "released" as const,
660
- });
661
- }
662
- return Promise.resolve({
663
- schemaVersion: 1 as const,
664
- status: "released" as const,
665
- });
666
- },
667
- };
668
- const contribution = createShellBotBackendContribution({
669
- state: { storage } as unknown as DurableObjectState,
670
- env: {
671
- USER_CONFIGURATIONS: {
672
- idFromName: () => "user-1",
673
- get: () => userConfiguration,
674
- },
675
- } as never,
676
- compileApplication: compileAssignmentTestApplication,
677
- });
678
- const identity = { userId: "user-1", botId: "primary" };
679
- await contribution.materializeSettings(identity, { name: "Primary" });
680
- const execute = (command: BotConfigurationCommandV1) =>
681
- contribution.executeConfiguration({
682
- schemaVersion: 1,
683
- ...identity,
684
- command,
685
- });
686
-
687
- const bound = await execute({
688
- schemaVersion: 1,
689
- type: "bot/assign-capability",
690
- commandId: "bind-model",
691
- botId: "primary",
692
- expectedRevision: 0,
693
- assignment: {
694
- assignmentId: "fixture-model",
695
- packageId: "composio",
696
- capabilityId: "gmail-tools",
697
- connectionId: "gmail-1",
698
- },
699
- model: {
700
- connectionId: "gmail-1",
701
- providerModelId: "fixture-model:latest",
702
- },
703
- });
704
-
705
- expect(bound).toMatchObject({ status: "applied", revision: 1 });
706
- expect(dependencyGeneration).toBe("bind-model");
707
- expect(await contribution.getSettings(identity)).toMatchObject({
708
- revision: 1,
709
- model: {
710
- connectionId: "gmail-1",
711
- providerModelId: "fixture-model:latest",
712
- },
713
- assignments: [{ assignmentId: "fixture-model", state: "enabled" }],
714
- });
715
-
716
- const unavailable = await contribution.getSettings(identity);
717
- await storage.put("bot-configuration", {
718
- ...unavailable,
719
- assignments: unavailable.assignments.map((assignment) => ({
720
- ...assignment,
721
- state: "unavailable" as const,
722
- })),
723
- });
724
-
725
- const unbind: BotConfigurationCommandV1 = {
726
- schemaVersion: 1,
727
- type: "bot/unbind-model",
728
- commandId: "unbind-model",
729
- botId: "primary",
730
- expectedRevision: 1,
731
- assignmentId: "fixture-model",
732
- };
733
- // The release is delayed once: the Unassign stays visibly retrying and its
734
- // durable receipt replays until the dependency is actually released.
735
- await expect(execute(unbind)).resolves.toMatchObject({
736
- status: "pending",
737
- });
738
- expect(dependencyGeneration).toBe("bind-model");
739
- await contribution.alarm();
740
-
741
- const unbound = await execute(unbind);
742
-
743
- expect(unbound).toMatchObject({ status: "applied", revision: 2 });
744
- expect(releaseAttempts).toBe(2);
745
- expect(dependencyGeneration).toBeUndefined();
746
- expect(await contribution.getSettings(identity)).toMatchObject({
747
- revision: 2,
748
- model: undefined,
749
- assignments: [],
750
- });
751
- });
752
-
753
- test("releases the superseded model dependency after switching Connections", async () => {
754
- const storage = new MemoryStorage();
755
- const user = installedUser();
756
- user.connections.push({
757
- ...user.connections[0]!,
758
- connectionId: "gmail-2",
759
- displayName: "Gmail 2",
760
- });
761
- const generations = new Set<string>();
762
- const released: Array<{ connectionId: string; generation: string }> = [];
763
- let releaseAttempts = 0;
764
- const userConfiguration = {
765
- readConfiguration: () => Promise.resolve(user),
766
- executeConnectionDependency: (request: {
767
- action: "claim" | "read" | "acknowledge" | "release" | "reconcile";
768
- connectionId: string;
769
- generation: string;
770
- }) => {
771
- if (request.action === "claim") {
772
- generations.add(request.generation);
773
- return Promise.resolve({
774
- schemaVersion: 1 as const,
775
- status: "claimed" as const,
776
- });
777
- }
778
- if (request.action === "acknowledge") {
779
- return Promise.resolve({
780
- schemaVersion: 1 as const,
781
- status: "acknowledged" as const,
782
- });
783
- }
784
- if (request.action === "read") {
785
- return Promise.resolve({
786
- schemaVersion: 1 as const,
787
- status: generations.has(request.generation)
788
- ? ("acknowledged" as const)
789
- : ("absent" as const),
790
- });
791
- }
792
- if (request.action === "release") {
793
- releaseAttempts += 1;
794
- if (releaseAttempts === 1) {
795
- return Promise.resolve({
796
- schemaVersion: 1 as const,
797
- status: "pending" as const,
798
- });
799
- }
800
- released.push({
801
- connectionId: request.connectionId,
802
- generation: request.generation,
803
- });
804
- generations.delete(request.generation);
805
- return Promise.resolve({
806
- schemaVersion: 1 as const,
807
- status: "released" as const,
808
- });
809
- }
810
- return Promise.resolve({
811
- schemaVersion: 1 as const,
812
- status: "released" as const,
813
- });
814
- },
815
- };
816
- const contribution = createShellBotBackendContribution({
817
- state: { storage } as unknown as DurableObjectState,
818
- env: {
819
- USER_CONFIGURATIONS: {
820
- idFromName: () => "user-1",
821
- get: () => userConfiguration,
822
- },
823
- } as never,
824
- compileApplication: compileAssignmentTestApplication,
825
- });
826
- const identity = { userId: "user-1", botId: "primary" };
827
- await contribution.materializeSettings(identity, { name: "Primary" });
828
- const execute = (command: BotConfigurationCommandV1) =>
829
- contribution.executeConfiguration({
830
- schemaVersion: 1,
831
- ...identity,
832
- command,
833
- });
834
-
835
- await execute({
836
- schemaVersion: 1,
837
- type: "bot/assign-capability",
838
- commandId: "bind-gmail-1",
839
- botId: "primary",
840
- expectedRevision: 0,
841
- assignment: {
842
- assignmentId: "gmail-model-1",
843
- packageId: "composio",
844
- capabilityId: "gmail-tools",
845
- connectionId: "gmail-1",
846
- },
847
- model: {
848
- connectionId: "gmail-1",
849
- providerModelId: "fixture-model:latest",
850
- },
851
- });
852
- const beforeSwitch = await contribution.getSettings(identity);
853
- await storage.put({
854
- "bot-configuration": {
855
- ...beforeSwitch,
856
- assignments: [
857
- ...beforeSwitch.assignments,
858
- {
859
- assignmentId: "gmail-tool",
860
- packageId: "composio",
861
- capabilityId: "gmail-send",
862
- connectionId: "gmail-1",
863
- state: "enabled" as const,
864
- },
865
- ],
866
- },
867
- "assignment-generation:gmail-tool": "tool-generation",
868
- });
869
- generations.add("tool-generation");
870
-
871
- // Moving the model to another Connection is an atomic Replace on the same
872
- // Assignment: it claims the new dependency, commits the swap, then
873
- // releases the old one. A delayed release stays visibly retrying rather
874
- // than failing the command.
875
- const switchModel: BotConfigurationCommandV1 = {
876
- schemaVersion: 1,
877
- type: "bot/replace-capability",
878
- commandId: "bind-gmail-2",
879
- botId: "primary",
880
- expectedRevision: 1,
881
- assignment: {
882
- assignmentId: "gmail-model-1",
883
- packageId: "composio",
884
- capabilityId: "gmail-tools",
885
- connectionId: "gmail-2",
886
- },
887
- model: {
888
- connectionId: "gmail-2",
889
- providerModelId: "fixture-model:latest",
890
- },
891
- };
892
- await expect(execute(switchModel)).resolves.toMatchObject({
893
- status: "applied",
894
- revision: 2,
895
- });
896
- // The commit is durable; only the old release is still retrying.
897
- await contribution.alarm();
898
- await expect(execute(switchModel)).resolves.toMatchObject({
899
- status: "applied",
900
- revision: 2,
901
- });
902
-
903
- expect(releaseAttempts).toBe(2);
904
- expect(released).toEqual([
905
- { connectionId: "gmail-1", generation: "bind-gmail-1" },
906
- ]);
907
- expect(generations.has("bind-gmail-1")).toBe(false);
908
- expect(generations.has("tool-generation")).toBe(true);
909
- expect(generations.has("bind-gmail-2")).toBe(true);
910
- expect(await contribution.getSettings(identity)).toMatchObject({
911
- revision: 2,
912
- model: { connectionId: "gmail-2" },
913
- assignments: [
914
- { assignmentId: "gmail-tool", state: "enabled" },
915
- { assignmentId: "gmail-model-1", state: "enabled" },
916
- ],
917
- });
918
-
919
- // Each further move is another Replace on the same Assignment, so each
920
- // one releases exactly the Connection it superseded.
921
- for (const [commandId, connectionId, expectedRevision] of [
922
- ["bind-gmail-1-again", "gmail-1", 2],
923
- ["bind-gmail-2-again", "gmail-2", 3],
924
- ] as const) {
925
- await expect(
926
- execute({
927
- schemaVersion: 1,
928
- type: "bot/replace-capability",
929
- commandId,
930
- botId: "primary",
931
- expectedRevision,
932
- assignment: {
933
- assignmentId: "gmail-model-1",
934
- packageId: "composio",
935
- capabilityId: "gmail-tools",
936
- connectionId,
937
- },
938
- model: {
939
- connectionId,
940
- providerModelId: "fixture-model:latest",
941
- },
942
- }),
943
- ).resolves.toMatchObject({
944
- status: "applied",
945
- revision: expectedRevision + 1,
946
- });
947
- }
948
-
949
- expect(released.slice(-2)).toEqual([
950
- { connectionId: "gmail-2", generation: "bind-gmail-2" },
951
- { connectionId: "gmail-1", generation: "bind-gmail-1-again" },
952
- ]);
953
- expect(generations.has("bind-gmail-1")).toBe(false);
954
- expect(generations.has("bind-gmail-2")).toBe(false);
955
- expect(generations.has("bind-gmail-1-again")).toBe(false);
956
- expect(generations.has("tool-generation")).toBe(true);
957
- expect(await contribution.getSettings(identity)).toMatchObject({
958
- revision: 4,
959
- model: { connectionId: "gmail-2" },
960
- assignments: [
961
- { assignmentId: "gmail-tool", state: "enabled" },
962
- { assignmentId: "gmail-model-1", state: "enabled" },
963
- ],
964
- });
965
- });
966
-
967
- test("orders atomic Replace and keeps Unassign stable until release", async () => {
968
- const storage = new MemoryStorage();
969
- const user = installedUser();
970
- user.connections.push({
971
- ...user.connections[0]!,
972
- connectionId: "gmail-2",
973
- displayName: "Gmail replacement",
974
- });
975
- const dependencies = new Map<
976
- string,
977
- "claimed" | "acknowledged" | "released"
978
- >();
979
- const log: string[] = [];
980
- let holdRelease = false;
981
- const userConfiguration = {
982
- readConfiguration: () => Promise.resolve(structuredClone(user)),
983
- executeConnectionDependency: (request: {
984
- action: "claim" | "read" | "acknowledge" | "release" | "reconcile";
985
- generation: string;
986
- }) => {
987
- log.push(`${request.action}:${request.generation}`);
988
- if (request.action === "read") {
989
- return Promise.resolve({
990
- schemaVersion: 1 as const,
991
- status: dependencies.get(request.generation) ?? ("absent" as const),
992
- });
993
- }
994
- if (request.action === "claim") {
995
- dependencies.set(request.generation, "claimed");
996
- return Promise.resolve({
997
- schemaVersion: 1 as const,
998
- status: "claimed" as const,
999
- });
1000
- }
1001
- if (request.action === "acknowledge") {
1002
- dependencies.set(request.generation, "acknowledged");
1003
- return Promise.resolve({
1004
- schemaVersion: 1 as const,
1005
- status: "acknowledged" as const,
1006
- });
1007
- }
1008
- if (request.action === "release" && holdRelease) {
1009
- return Promise.resolve({
1010
- schemaVersion: 1 as const,
1011
- status: "pending" as const,
1012
- });
1013
- }
1014
- if (request.action === "release")
1015
- dependencies.set(request.generation, "released");
1016
- return Promise.resolve({
1017
- schemaVersion: 1 as const,
1018
- status:
1019
- request.action === "reconcile"
1020
- ? ("pending" as const)
1021
- : ("released" as const),
1022
- });
1023
- },
1024
- };
1025
- const contribution = createShellBotBackendContribution({
1026
- state: { storage } as unknown as DurableObjectState,
1027
- env: {
1028
- USER_CONFIGURATIONS: {
1029
- idFromName: () => "user-1",
1030
- get: () => userConfiguration,
1031
- },
1032
- } as never,
1033
- compileApplication: compileAssignmentTestApplication,
1034
- });
1035
- const identity = { userId: "user-1", botId: "primary" };
1036
- await contribution.materializeSettings(identity, { name: "Primary" });
1037
- const execute = (command: BotConfigurationCommandV1) =>
1038
- contribution.executeConfiguration({
1039
- schemaVersion: 1,
1040
- ...identity,
1041
- command,
1042
- });
1043
-
1044
- await expect(
1045
- execute({
1046
- schemaVersion: 1,
1047
- type: "bot/assign-capability",
1048
- commandId: "assign-1",
1049
- botId: "primary",
1050
- expectedRevision: 0,
1051
- assignment: {
1052
- assignmentId: "mail",
1053
- packageId: "composio",
1054
- capabilityId: "gmail-tools",
1055
- connectionId: "gmail-1",
1056
- },
1057
- }),
1058
- ).resolves.toMatchObject({ status: "applied", revision: 1 });
1059
- log.length = 0;
1060
-
1061
- await expect(
1062
- execute({
1063
- schemaVersion: 1,
1064
- type: "bot/replace-capability",
1065
- commandId: "replace-1",
1066
- botId: "primary",
1067
- expectedRevision: 1,
1068
- assignment: {
1069
- assignmentId: "mail",
1070
- packageId: "composio",
1071
- capabilityId: "gmail-tools",
1072
- connectionId: "gmail-2",
1073
- },
1074
- }),
1075
- ).resolves.toMatchObject({ status: "applied", revision: 2 });
1076
- expect(log).toEqual([
1077
- "read:replace-1",
1078
- "claim:replace-1",
1079
- "read:replace-1",
1080
- "acknowledge:replace-1",
1081
- "read:assign-1",
1082
- "release:assign-1",
1083
- ]);
1084
- expect(await contribution.getSettings(identity)).toMatchObject({
1085
- revision: 2,
1086
- assignments: [{ assignmentId: "mail", connectionId: "gmail-2" }],
1087
- assignmentOperations: [],
1088
- });
1089
-
1090
- holdRelease = true;
1091
- const pendingUnassign = await execute({
1092
- schemaVersion: 1,
1093
- type: "bot/unassign-capability",
1094
- commandId: "unassign-1",
1095
- botId: "primary",
1096
- expectedRevision: 2,
1097
- assignmentId: "mail",
1098
- });
1099
- expect(pendingUnassign).toEqual({
1100
- schemaVersion: 1,
1101
- commandId: "unassign-1",
1102
- revision: 2,
1103
- status: "pending",
1104
- });
1105
- await expect(
1106
- execute({
1107
- schemaVersion: 1,
1108
- type: "bot/unassign-capability",
1109
- commandId: "unassign-1",
1110
- botId: "primary",
794
+ await contribution.executeConfiguration(
795
+ request({
796
+ ...first,
797
+ commandId: "unset-bot-model",
1111
798
  expectedRevision: 2,
1112
- assignmentId: "mail",
799
+ values: undefined,
800
+ unset: ["model"],
1113
801
  }),
1114
- ).resolves.toEqual(pendingUnassign);
802
+ );
1115
803
  expect(await contribution.getSettings(identity)).toMatchObject({
1116
- revision: 2,
1117
- assignments: [{ assignmentId: "mail", connectionId: "gmail-2" }],
1118
- assignmentOperations: [
1119
- { commandId: "unassign-1", kind: "unassigning", state: "retrying" },
1120
- ],
1121
- });
1122
-
1123
- holdRelease = false;
1124
- const reconstructed = createShellBotBackendContribution({
1125
- state: { storage } as unknown as DurableObjectState,
1126
- env: {
1127
- USER_CONFIGURATIONS: {
1128
- idFromName: () => "user-1",
1129
- get: () => userConfiguration,
1130
- },
1131
- } as never,
1132
- compileApplication: compileAssignmentTestApplication,
1133
- });
1134
- await reconstructed.alarm();
1135
- expect(await reconstructed.getSettings(identity)).toMatchObject({
1136
804
  revision: 3,
1137
- assignments: [],
1138
- assignmentOperations: [],
805
+ packageValues: { "custom-models": { tone: "concise" } },
1139
806
  });
807
+
1140
808
  await expect(
1141
- reconstructed.executeConfiguration({
1142
- schemaVersion: 1,
1143
- ...identity,
1144
- command: {
1145
- schemaVersion: 1,
1146
- type: "bot/unassign-capability",
1147
- commandId: "unassign-1",
1148
- botId: "primary",
1149
- expectedRevision: 2,
1150
- assignmentId: "mail",
1151
- },
1152
- }),
1153
- ).resolves.toMatchObject({ status: "applied", revision: 3 });
809
+ contribution.executeConfiguration(
810
+ request({
811
+ ...first,
812
+ commandId: "unset-unknown",
813
+ expectedRevision: 3,
814
+ values: undefined,
815
+ unset: ["unknown-setting"],
816
+ }),
817
+ ),
818
+ ).rejects.toThrow(/not declared by this Package/);
819
+
1154
820
  await expect(
1155
- reconstructed.executeConfiguration({
1156
- schemaVersion: 1,
1157
- ...identity,
1158
- command: {
1159
- schemaVersion: 1,
1160
- type: "bot/unassign-capability",
1161
- commandId: "unassign-1",
1162
- botId: "primary",
821
+ contribution.executeConfiguration(
822
+ request({
823
+ ...first,
824
+ commandId: "stale-revision",
1163
825
  expectedRevision: 2,
1164
- assignmentId: "other",
1165
- },
1166
- }),
1167
- ).rejects.toThrow("reused for a different command");
1168
- });
1169
-
1170
- test("releases the old Replace dependency when new acknowledgement is absent or rejected", async () => {
1171
- for (const acknowledgement of ["absent", "rejected"] as const) {
1172
- const storage = new MemoryStorage();
1173
- const user = installedUser();
1174
- user.connections.push({
1175
- ...user.connections[0]!,
1176
- connectionId: "gmail-2",
1177
- displayName: "Gmail replacement",
1178
- });
1179
- const dependencies = new Map<
1180
- string,
1181
- "claimed" | "acknowledged" | "released"
1182
- >();
1183
- const log: string[] = [];
1184
- let replacementReads = 0;
1185
- let holdOldRelease = true;
1186
- const userConfiguration = {
1187
- readConfiguration: () => Promise.resolve(structuredClone(user)),
1188
- executeConnectionDependency: (request: {
1189
- action: "claim" | "read" | "acknowledge" | "release" | "reconcile";
1190
- generation: string;
1191
- }) => {
1192
- log.push(`${request.action}:${request.generation}`);
1193
- if (request.action === "read") {
1194
- if (request.generation.startsWith("replace-")) {
1195
- replacementReads += 1;
1196
- if (replacementReads > 1 && acknowledgement === "absent") {
1197
- return Promise.resolve({
1198
- schemaVersion: 1 as const,
1199
- status: "absent" as const,
1200
- });
1201
- }
1202
- }
1203
- return Promise.resolve({
1204
- schemaVersion: 1 as const,
1205
- status:
1206
- dependencies.get(request.generation) ?? ("absent" as const),
1207
- });
1208
- }
1209
- if (request.action === "claim") {
1210
- dependencies.set(request.generation, "claimed");
1211
- return Promise.resolve({
1212
- schemaVersion: 1 as const,
1213
- status: "claimed" as const,
1214
- });
1215
- }
1216
- if (request.action === "acknowledge") {
1217
- if (
1218
- request.generation.startsWith("replace-") &&
1219
- acknowledgement === "rejected"
1220
- ) {
1221
- return Promise.resolve({
1222
- schemaVersion: 1 as const,
1223
- status: "rejected" as const,
1224
- failure: "acknowledgement rejected",
1225
- });
1226
- }
1227
- dependencies.set(request.generation, "acknowledged");
1228
- return Promise.resolve({
1229
- schemaVersion: 1 as const,
1230
- status: "acknowledged" as const,
1231
- });
1232
- }
1233
- if (request.action === "release") {
1234
- if (request.generation === "assign-old" && holdOldRelease) {
1235
- return Promise.resolve({
1236
- schemaVersion: 1 as const,
1237
- status: "pending" as const,
1238
- });
1239
- }
1240
- dependencies.set(request.generation, "released");
1241
- return Promise.resolve({
1242
- schemaVersion: 1 as const,
1243
- status: "released" as const,
1244
- });
1245
- }
1246
- return Promise.resolve({
1247
- schemaVersion: 1 as const,
1248
- status: "pending" as const,
1249
- });
1250
- },
1251
- };
1252
- const makeContribution = () =>
1253
- createShellBotBackendContribution({
1254
- state: { storage } as unknown as DurableObjectState,
1255
- env: {
1256
- USER_CONFIGURATIONS: {
1257
- idFromName: () => "user-1",
1258
- get: () => userConfiguration,
1259
- },
1260
- } as never,
1261
- compileApplication: compileAssignmentTestApplication,
1262
- });
1263
- const contribution = makeContribution();
1264
- const identity = { userId: "user-1", botId: "primary" };
1265
- await contribution.materializeSettings(identity, { name: "Primary" });
1266
- const execute = (
1267
- backend: ReturnType<typeof createShellBotBackendContribution>,
1268
- command: BotConfigurationCommandV1,
1269
- ) =>
1270
- backend.executeConfiguration({
1271
- schemaVersion: 1,
1272
- ...identity,
1273
- command,
1274
- });
1275
- await execute(contribution, {
1276
- schemaVersion: 1,
1277
- type: "bot/assign-capability",
1278
- commandId: "assign-old",
1279
- botId: "primary",
1280
- expectedRevision: 0,
1281
- assignment: {
1282
- assignmentId: "mail",
1283
- packageId: "composio",
1284
- capabilityId: "gmail-tools",
1285
- connectionId: "gmail-1",
1286
- },
1287
- });
1288
- const replaceCommand = {
1289
- schemaVersion: 1 as const,
1290
- type: "bot/replace-capability" as const,
1291
- commandId: `replace-${acknowledgement}`,
1292
- botId: "primary",
1293
- expectedRevision: 1,
1294
- assignment: {
1295
- assignmentId: "mail",
1296
- packageId: "composio",
1297
- capabilityId: "gmail-tools",
1298
- connectionId: "gmail-2",
1299
- },
1300
- };
1301
-
1302
- await expect(
1303
- execute(contribution, replaceCommand),
1304
- ).resolves.toMatchObject({ status: "applied", revision: 2 });
1305
- expect(dependencies.get("assign-old")).toBe("acknowledged");
1306
- expect(await contribution.getSettings(identity)).toMatchObject({
1307
- revision: 2,
1308
- assignments: [
1309
- {
1310
- assignmentId: "mail",
1311
- connectionId: "gmail-2",
1312
- state: "unavailable",
1313
- },
1314
- ],
1315
- assignmentOperations: [
1316
- {
1317
- commandId: `replace-${acknowledgement}`,
1318
- kind: "replacing",
1319
- state: "retrying",
1320
- },
1321
- ],
1322
- });
1323
- expect(log).toContain("release:assign-old");
1324
-
1325
- holdOldRelease = false;
1326
- const reconstructed = makeContribution();
1327
- await reconstructed.alarm();
1328
- expect(dependencies.get("assign-old")).toBe("released");
1329
- expect(await reconstructed.getSettings(identity)).toMatchObject({
1330
- revision: 2,
1331
- assignments: [
1332
- {
1333
- assignmentId: "mail",
1334
- connectionId: "gmail-2",
1335
- state: "unavailable",
1336
- },
1337
- ],
1338
- assignmentOperations: [],
1339
- });
1340
- await expect(
1341
- execute(reconstructed, replaceCommand),
1342
- ).resolves.toMatchObject({ status: "applied", revision: 2 });
1343
- }
1344
- });
1345
-
1346
- test("keeps provider absence retrying until the owner becomes available", async () => {
1347
- const storage = new MemoryStorage();
1348
- let available = false;
1349
- let dependency: "absent" | "claimed" | "acknowledged" = "absent";
1350
- const userConfiguration = {
1351
- readConfiguration: () => Promise.resolve(installedUser()),
1352
- executeConnectionDependency: (request: { action: string }) => {
1353
- if (!available) {
1354
- return Promise.resolve({
1355
- schemaVersion: 1 as const,
1356
- status: "unavailable" as const,
1357
- failure: "Connection owner is unavailable",
1358
- });
1359
- }
1360
- if (request.action === "read") {
1361
- return Promise.resolve({
1362
- schemaVersion: 1 as const,
1363
- status: dependency,
1364
- });
1365
- }
1366
- if (request.action === "claim") dependency = "claimed";
1367
- if (request.action === "acknowledge") dependency = "acknowledged";
1368
- return Promise.resolve({
1369
- schemaVersion: 1 as const,
1370
- status:
1371
- request.action === "claim"
1372
- ? ("claimed" as const)
1373
- : request.action === "acknowledge"
1374
- ? ("acknowledged" as const)
1375
- : ("pending" as const),
1376
- });
1377
- },
1378
- };
1379
- const host = {
1380
- state: { storage } as unknown as DurableObjectState,
1381
- env: {
1382
- USER_CONFIGURATIONS: {
1383
- idFromName: () => "user-1",
1384
- get: () => userConfiguration,
1385
- },
1386
- } as never,
1387
- compileApplication: compileAssignmentTestApplication,
1388
- };
1389
- const identity = { userId: "user-1", botId: "primary" };
1390
- const contribution = createShellBotBackendContribution(host);
1391
- await contribution.materializeSettings(identity, { name: "Primary" });
1392
- const command = assignmentCommand("owner-retry", {
1393
- packageId: "composio",
1394
- capabilityId: "gmail-tools",
1395
- connectionId: "gmail-1",
1396
- });
826
+ }),
827
+ ),
828
+ ).rejects.toThrow("configuration revision is 3");
1397
829
  await expect(
1398
- contribution.executeConfiguration({
1399
- schemaVersion: 1,
1400
- ...identity,
1401
- command,
1402
- }),
1403
- ).resolves.toEqual({
1404
- schemaVersion: 1,
1405
- commandId: "owner-retry",
1406
- revision: 0,
1407
- status: "pending",
1408
- });
1409
- expect(await contribution.getSettings(identity)).toMatchObject({
1410
- revision: 0,
1411
- assignments: [],
1412
- assignmentOperations: [{ commandId: "owner-retry", state: "retrying" }],
1413
- });
1414
- available = true;
1415
- const reconstructed = createShellBotBackendContribution(host);
1416
- await reconstructed.alarm();
1417
- expect(await reconstructed.getSettings(identity)).toMatchObject({
1418
- revision: 1,
1419
- assignments: [{ assignmentId: "owner-retry", state: "enabled" }],
1420
- assignmentOperations: [],
1421
- });
1422
- });
1423
-
1424
- test("scopes generations to Assignments that share one Connection", async () => {
1425
- const storage = new MemoryStorage();
1426
- const user = installedUser();
1427
- user.connections.push({
1428
- ...user.connections[0]!,
1429
- connectionId: "gmail-2",
1430
- displayName: "Gmail replacement",
1431
- });
1432
- const dependencies = new Map<
1433
- string,
1434
- "claimed" | "acknowledged" | "released"
1435
- >();
1436
- const userConfiguration = {
1437
- readConfiguration: () => Promise.resolve(structuredClone(user)),
1438
- executeConnectionDependency: (request: {
1439
- action: "claim" | "read" | "acknowledge" | "release" | "reconcile";
1440
- generation: string;
1441
- }) => {
1442
- if (request.action === "read") {
1443
- return Promise.resolve({
1444
- schemaVersion: 1 as const,
1445
- status: dependencies.get(request.generation) ?? ("absent" as const),
1446
- });
1447
- }
1448
- if (request.action === "claim") {
1449
- dependencies.set(request.generation, "claimed");
1450
- return Promise.resolve({
1451
- schemaVersion: 1 as const,
1452
- status: "claimed" as const,
1453
- });
1454
- }
1455
- if (request.action === "acknowledge") {
1456
- dependencies.set(request.generation, "acknowledged");
1457
- return Promise.resolve({
1458
- schemaVersion: 1 as const,
1459
- status: "acknowledged" as const,
1460
- });
1461
- }
1462
- if (request.action === "release") {
1463
- dependencies.set(request.generation, "released");
1464
- }
1465
- return Promise.resolve({
1466
- schemaVersion: 1 as const,
1467
- status: "released" as const,
1468
- });
1469
- },
1470
- };
1471
- const contribution = createShellBotBackendContribution({
1472
- state: { storage } as unknown as DurableObjectState,
1473
- env: {
1474
- USER_CONFIGURATIONS: {
1475
- idFromName: () => "user-1",
1476
- get: () => userConfiguration,
1477
- },
1478
- } as never,
1479
- compileApplication: compileAssignmentTestApplication,
1480
- });
1481
- const identity = { userId: "user-1", botId: "primary" };
1482
- await contribution.materializeSettings(identity, { name: "Primary" });
1483
- const execute = (command: BotConfigurationCommandV1) =>
1484
- contribution.executeConfiguration({
1485
- schemaVersion: 1,
1486
- ...identity,
1487
- command,
1488
- });
1489
- const assign = (
1490
- commandId: string,
1491
- expectedRevision: number,
1492
- assignmentId: string,
1493
- connectionId: string,
1494
- type:
1495
- | "bot/assign-capability"
1496
- | "bot/replace-capability" = "bot/assign-capability",
1497
- ) =>
1498
- execute({
1499
- schemaVersion: 1,
1500
- type,
1501
- commandId,
1502
- botId: "primary",
1503
- expectedRevision,
1504
- assignment: {
1505
- assignmentId,
1506
- packageId: "composio",
1507
- capabilityId: "gmail-tools",
1508
- connectionId,
1509
- },
1510
- });
1511
-
1512
- await assign("assign-a", 0, "mail-a", "gmail-1");
1513
- await assign("assign-b", 1, "mail-b", "gmail-1");
1514
- await assign("replace-a", 2, "mail-a", "gmail-2", "bot/replace-capability");
1515
- expect(dependencies.get("assign-a")).toBe("released");
1516
- expect(dependencies.get("assign-b")).toBe("acknowledged");
1517
- await execute({
1518
- schemaVersion: 1,
1519
- type: "bot/unassign-capability",
1520
- commandId: "unassign-a",
1521
- botId: "primary",
1522
- expectedRevision: 3,
1523
- assignmentId: "mail-a",
1524
- });
1525
- expect(await contribution.getSettings(identity)).toMatchObject({
1526
- revision: 4,
1527
- assignments: [
1528
- {
1529
- assignmentId: "mail-b",
1530
- connectionId: "gmail-1",
1531
- state: "enabled",
1532
- },
1533
- ],
1534
- });
1535
- expect(dependencies.get("assign-b")).toBe("acknowledged");
1536
- });
1537
-
1538
- test("settles a committed assignment saga before replaying its receipt", async () => {
1539
- const storage = new MemoryStorage();
1540
- let acknowledgementAttempts = 0;
1541
- let acknowledged = false;
1542
- let dependencyStatus: "absent" | "claimed" | "acknowledged" = "absent";
1543
- const userConfiguration = {
1544
- readConfiguration: () => Promise.resolve(installedUser()),
1545
- executeConnectionDependency: (request: { action: string }) => {
1546
- if (request.action === "read") {
1547
- return Promise.resolve({
1548
- schemaVersion: 1 as const,
1549
- status: dependencyStatus,
1550
- });
1551
- }
1552
- if (request.action === "claim") {
1553
- dependencyStatus = "claimed";
1554
- return Promise.resolve({
1555
- schemaVersion: 1 as const,
1556
- status: "claimed" as const,
1557
- });
1558
- }
1559
- if (request.action === "acknowledge") {
1560
- acknowledgementAttempts += 1;
1561
- if (acknowledgementAttempts <= 1) {
1562
- return Promise.reject(new Error("acknowledgement response lost"));
1563
- }
1564
- dependencyStatus = "acknowledged";
1565
- acknowledged = true;
1566
- return Promise.resolve({
1567
- schemaVersion: 1 as const,
1568
- status: "acknowledged" as const,
1569
- });
1570
- }
1571
- return Promise.resolve({
1572
- schemaVersion: 1 as const,
1573
- status: "released" as const,
1574
- });
1575
- },
1576
- };
1577
- const contribution = createShellBotBackendContribution({
1578
- state: { storage } as unknown as DurableObjectState,
1579
- env: {
1580
- USER_CONFIGURATIONS: {
1581
- idFromName: () => "user-1",
1582
- get: () => userConfiguration,
1583
- },
1584
- } as never,
1585
- compileApplication: compileAssignmentTestApplication,
1586
- });
1587
- await contribution.materializeSettings(
1588
- { userId: "user-1", botId: "primary" },
1589
- { name: "Primary" },
1590
- );
1591
- const command = assignmentCommand("lost-assignment-response", {
1592
- packageId: "composio",
1593
- capabilityId: "gmail-tools",
1594
- connectionId: "gmail-1",
1595
- });
1596
- const execute = () =>
1597
- contribution.executeConfiguration({
1598
- schemaVersion: 1,
1599
- userId: "user-1",
1600
- botId: "primary",
1601
- command,
1602
- });
1603
-
1604
- await expect(execute()).resolves.toEqual({
1605
- schemaVersion: 1,
1606
- commandId: "lost-assignment-response",
1607
- status: "pending",
1608
- revision: 0,
1609
- });
1610
- expect(acknowledgementAttempts).toBe(1);
1611
- expect(acknowledged).toBe(false);
1612
-
1613
- await expect(execute()).resolves.toMatchObject({
1614
- commandId: "lost-assignment-response",
1615
- status: "applied",
1616
- revision: 1,
1617
- });
1618
- expect(acknowledgementAttempts).toBe(2);
1619
- expect(acknowledged).toBe(true);
1620
- });
1621
- });
1622
-
1623
- describe("User default model", () => {
1624
- test("claims one durable Assignment for a Bot that follows the default", async () => {
1625
- const storage = new MemoryStorage();
1626
- let dependencyClaims = 0;
1627
- let dependencyAcknowledgements = 0;
1628
- let dependencyStatus: "absent" | "claimed" | "acknowledged" = "absent";
1629
- const user: UserSettingsViewV1 = {
1630
- ...installedUser(),
1631
- newBotModelTemplate: {
1632
- connectionId: "gmail-1",
1633
- providerModelId: "fixture-model",
1634
- },
1635
- newBotModelTemplateSource: "auto",
1636
- };
1637
- const userConfiguration = {
1638
- readConfiguration: () => Promise.resolve(structuredClone(user)),
1639
- executeConnectionDependency: (request: { action: string }) => {
1640
- if (request.action === "read") {
1641
- return Promise.resolve({
1642
- schemaVersion: 1 as const,
1643
- status: dependencyStatus,
1644
- });
1645
- }
1646
- if (request.action === "claim") {
1647
- dependencyClaims += 1;
1648
- dependencyStatus = "claimed";
1649
- return Promise.resolve({
1650
- schemaVersion: 1 as const,
1651
- status: "claimed" as const,
1652
- });
1653
- }
1654
- if (request.action === "acknowledge") {
1655
- dependencyAcknowledgements += 1;
1656
- dependencyStatus = "acknowledged";
1657
- return Promise.resolve({
1658
- schemaVersion: 1 as const,
1659
- status: "acknowledged" as const,
1660
- });
1661
- }
1662
- dependencyStatus = "absent";
1663
- return Promise.resolve({
1664
- schemaVersion: 1 as const,
1665
- status: "released" as const,
1666
- });
1667
- },
1668
- };
1669
- const contribution = createShellBotBackendContribution({
1670
- state: { storage } as unknown as DurableObjectState,
1671
- env: {
1672
- USER_CONFIGURATIONS: {
1673
- idFromName: () => "user-1",
1674
- get: () => userConfiguration,
1675
- },
1676
- } as never,
1677
- compileApplication: compileAssignmentTestApplication,
1678
- });
1679
- const identity = { userId: "user-1", botId: "primary" };
1680
- await contribution.materializeSettings(identity, { name: "Primary" });
1681
-
1682
- const plan = await contribution.resolveConfiguration(identity);
1683
- expect(plan.model).toBeUndefined();
1684
- expect(plan.assignments).toMatchObject([
1685
- {
1686
- packageId: "composio",
1687
- capabilityId: "gmail-tools",
1688
- connectionId: "gmail-1",
1689
- state: "enabled",
1690
- },
1691
- ]);
1692
- expect(dependencyClaims).toBe(1);
1693
- expect(dependencyAcknowledgements).toBe(1);
1694
-
1695
- // The claim is made once: a Bot that already holds the Assignment keeps it
1696
- // and follows the default without further durable writes.
1697
- const settings = await contribution.readConfiguration({
1698
- schemaVersion: 1,
1699
- ...identity,
1700
- });
1701
- await contribution.resolveConfiguration(identity);
1702
- expect(dependencyClaims).toBe(1);
1703
- expect(
1704
- await contribution.readConfiguration({ schemaVersion: 1, ...identity }),
1705
- ).toEqual(settings);
1706
- expect(settings.model).toBeUndefined();
1707
- });
1708
-
1709
- test("leaves a Bot unclaimed when the default Connection is unavailable", async () => {
1710
- const storage = new MemoryStorage();
1711
- let dependencyClaims = 0;
1712
- const userConfiguration = {
1713
- readConfiguration: () =>
1714
- Promise.resolve({
1715
- ...installedUser(),
1716
- connections: [
1717
- { ...installedUser().connections[0]!, state: "revoked" as const },
1718
- ],
1719
- newBotModelTemplate: {
1720
- connectionId: "gmail-1",
1721
- providerModelId: "fixture-model",
1722
- },
1723
- newBotModelTemplateSource: "auto",
830
+ contribution.executeConfiguration(
831
+ request({
832
+ ...first,
833
+ values: { tone: "different" },
1724
834
  }),
1725
- executeConnectionDependency: (request: { action: string }) => {
1726
- if (request.action === "read") {
1727
- return Promise.resolve({
1728
- schemaVersion: 1 as const,
1729
- status: "absent" as const,
1730
- });
1731
- }
1732
- if (request.action === "claim") dependencyClaims += 1;
1733
- return Promise.resolve({
1734
- schemaVersion: 1 as const,
1735
- status: "unauthorized" as const,
1736
- });
1737
- },
1738
- };
1739
- const contribution = createShellBotBackendContribution({
1740
- state: { storage } as unknown as DurableObjectState,
1741
- env: {
1742
- USER_CONFIGURATIONS: {
1743
- idFromName: () => "user-1",
1744
- get: () => userConfiguration,
1745
- },
1746
- } as never,
1747
- compileApplication: compileAssignmentTestApplication,
1748
- });
1749
- const identity = { userId: "user-1", botId: "primary" };
1750
- await contribution.materializeSettings(identity, { name: "Primary" });
1751
-
1752
- expect(
1753
- (await contribution.resolveConfiguration(identity)).assignments,
1754
- ).toEqual([]);
1755
- expect(dependencyClaims).toBe(0);
835
+ ),
836
+ ).rejects.toThrow("reused for a different command");
1756
837
  });
1757
838
  });