@frockbot/plugin-shell 0.1.2 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +28 -28
- package/src/backend-composition.ts +4 -4
- package/src/backend-configuration.test.ts +366 -1451
- package/src/backend-execution.ts +0 -4
- package/src/backend-image.test.ts +8 -8
- package/src/backend-image.ts +13 -13
- package/src/backend-isolate.test.ts +181 -35
- package/src/backend-isolate.ts +110 -72
- package/src/backend-recovery-integration.test.ts +17 -67
- package/src/backend-routines.ts +1 -1
- package/src/backend.ts +335 -1659
- package/src/client/FrockBotApp.vue +6 -73
- package/src/client/index.test.ts +320 -380
- package/src/client/index.ts +85 -256
- package/src/client/model-presentation.test.ts +21 -8
- package/src/client/model-presentation.ts +14 -7
- package/src/client/styles.css +0 -27
- package/src/settings-links.test.ts +2 -10
- package/src/settings-links.ts +1 -13
- package/src/shared.ts +0 -13
- package/src/backend-assignment.test.ts +0 -161
- package/src/backend-assignment.ts +0 -274
|
@@ -5,6 +5,7 @@ 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";
|
|
9
10
|
|
|
10
11
|
class MemoryStorage {
|
|
@@ -51,72 +52,62 @@ class MemoryStorage {
|
|
|
51
52
|
}
|
|
52
53
|
}
|
|
53
54
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
55
|
+
const MODEL_SETTING = {
|
|
56
|
+
id: "model",
|
|
57
|
+
schemaVersion: 1,
|
|
58
|
+
scopes: ["user", "bot"],
|
|
59
|
+
role: "model",
|
|
60
|
+
schema: {
|
|
61
|
+
type: "object",
|
|
62
|
+
properties: {
|
|
63
|
+
connectionId: { type: "string" },
|
|
64
|
+
providerModelId: { type: "string" },
|
|
65
|
+
},
|
|
66
|
+
required: ["connectionId", "providerModelId"],
|
|
67
|
+
additionalProperties: false,
|
|
68
|
+
},
|
|
69
|
+
} as const satisfies PackageSettingDefinition;
|
|
70
|
+
|
|
71
|
+
const TONE_SETTING = {
|
|
72
|
+
id: "tone",
|
|
73
|
+
schemaVersion: 1,
|
|
74
|
+
scopes: ["bot"],
|
|
75
|
+
schema: { type: "string", maxLength: 40 },
|
|
76
|
+
} as const satisfies PackageSettingDefinition;
|
|
73
77
|
|
|
74
|
-
async function
|
|
78
|
+
async function compileModelTestApplication(): ReturnType<
|
|
75
79
|
typeof compileFoundationApplication
|
|
76
80
|
> {
|
|
77
81
|
const application = await compileFoundationApplication();
|
|
82
|
+
const provider = application.packages.find(
|
|
83
|
+
(pkg) => pkg.id === "provider-flock-ai",
|
|
84
|
+
);
|
|
78
85
|
const template = application.packages.find((pkg) => pkg.id === "settings");
|
|
79
|
-
if (!template) throw new Error("
|
|
86
|
+
if (!provider || !template) throw new Error("Fixture Packages unavailable");
|
|
80
87
|
return {
|
|
81
88
|
...application,
|
|
82
89
|
packages: [
|
|
83
|
-
...application.packages
|
|
90
|
+
...application.packages.filter(
|
|
91
|
+
(pkg) => pkg.id !== provider.id && pkg.id !== "custom-models",
|
|
92
|
+
),
|
|
93
|
+
provider,
|
|
84
94
|
{
|
|
85
95
|
...template,
|
|
86
|
-
id: "
|
|
87
|
-
specifier: "@test/
|
|
96
|
+
id: "custom-models",
|
|
97
|
+
specifier: "@test/custom-models",
|
|
88
98
|
version: "0.0.1",
|
|
89
99
|
manifest: {
|
|
90
100
|
...template.manifest,
|
|
91
|
-
id: "
|
|
92
|
-
displayName: "
|
|
101
|
+
id: "custom-models",
|
|
102
|
+
displayName: "Custom models",
|
|
93
103
|
version: "0.0.1",
|
|
94
104
|
dependencies: {},
|
|
95
105
|
contributions: {},
|
|
96
106
|
permissions: [],
|
|
97
107
|
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
|
-
],
|
|
108
|
+
settings: [MODEL_SETTING, TONE_SETTING],
|
|
109
|
+
connectionTypes: [],
|
|
110
|
+
capabilities: [],
|
|
120
111
|
},
|
|
121
112
|
},
|
|
122
113
|
},
|
|
@@ -124,34 +115,113 @@ async function compileAssignmentTestApplication(): ReturnType<
|
|
|
124
115
|
};
|
|
125
116
|
}
|
|
126
117
|
|
|
127
|
-
function
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
connectionId?: string;
|
|
133
|
-
},
|
|
134
|
-
): BotConfigurationCommandV1 {
|
|
118
|
+
function model(connectionId: string, providerModelId: string) {
|
|
119
|
+
return { connectionId, providerModelId };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function configuredUser(): UserSettingsViewV1 {
|
|
135
123
|
return {
|
|
136
124
|
schemaVersion: 1,
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
125
|
+
revision: 1,
|
|
126
|
+
profile: { name: "User" },
|
|
127
|
+
packages: [
|
|
128
|
+
{
|
|
129
|
+
packageId: "provider-flock-ai",
|
|
130
|
+
version: "0.0.1",
|
|
131
|
+
state: "installed",
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
packageId: "custom-models",
|
|
135
|
+
version: "0.0.1",
|
|
136
|
+
state: "disabled",
|
|
137
|
+
},
|
|
138
|
+
],
|
|
139
|
+
connections: [
|
|
140
|
+
{
|
|
141
|
+
connectionId: "flock-ai-ambient",
|
|
142
|
+
packageId: "provider-flock-ai",
|
|
143
|
+
connectionTypeId: "flock-ai-account",
|
|
144
|
+
displayName: "Flock AI",
|
|
145
|
+
state: "ready",
|
|
146
|
+
providerType: "flock-ai",
|
|
147
|
+
generation: "foundation-generation-1",
|
|
148
|
+
modelCatalog: {
|
|
149
|
+
schemaVersion: 1,
|
|
150
|
+
generation: "catalog-1",
|
|
151
|
+
state: "fresh",
|
|
152
|
+
models: [
|
|
153
|
+
{
|
|
154
|
+
providerModelId: "@flock/auto",
|
|
155
|
+
displayName: "Platform",
|
|
156
|
+
capabilities: {
|
|
157
|
+
tools: true,
|
|
158
|
+
vision: false,
|
|
159
|
+
reasoning: false,
|
|
160
|
+
},
|
|
161
|
+
source: "discovered",
|
|
162
|
+
},
|
|
163
|
+
],
|
|
164
|
+
},
|
|
165
|
+
safeMetadata: {},
|
|
166
|
+
},
|
|
167
|
+
],
|
|
168
|
+
platformModel: model("flock-ai-ambient", "@flock/auto"),
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function host(storage: MemoryStorage, readUser: () => UserSettingsViewV1) {
|
|
173
|
+
return createShellBotBackendContribution({
|
|
174
|
+
state: { storage } as unknown as DurableObjectState,
|
|
175
|
+
env: {
|
|
176
|
+
CREDENTIAL_KEYRING:
|
|
177
|
+
'{"schemaVersion":1,"currentKeyId":"primary","keys":{"primary":"MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY"}}',
|
|
178
|
+
USER_CONFIGURATIONS: {
|
|
179
|
+
idFromName: () => "user-1",
|
|
180
|
+
get: () => ({
|
|
181
|
+
readConfiguration: () => Promise.resolve(structuredClone(readUser())),
|
|
182
|
+
listBots: () =>
|
|
183
|
+
Promise.resolve({ schemaVersion: 1, revision: 0, bots: [] }),
|
|
184
|
+
}),
|
|
185
|
+
},
|
|
186
|
+
MEMORY_FILES: {},
|
|
187
|
+
MEMORY_INDEX: {},
|
|
188
|
+
FLOCK_AI: {
|
|
189
|
+
autoRoute: "flock-auto",
|
|
190
|
+
runChatCompletion: () =>
|
|
191
|
+
Promise.resolve(
|
|
192
|
+
new ReadableStream({
|
|
193
|
+
start(controller) {
|
|
194
|
+
controller.enqueue(
|
|
195
|
+
new TextEncoder().encode(
|
|
196
|
+
'data: {"choices":[{"delta":{"content":"Cordis runtime: hello"}}]}\n\n' +
|
|
197
|
+
'data: {"choices":[{"delta":{},"finish_reason":"stop"}]}\n\n' +
|
|
198
|
+
"data: [DONE]\n\n",
|
|
199
|
+
),
|
|
200
|
+
);
|
|
201
|
+
controller.close();
|
|
202
|
+
},
|
|
203
|
+
}),
|
|
204
|
+
),
|
|
205
|
+
},
|
|
206
|
+
} as never,
|
|
207
|
+
compileApplication: compileModelTestApplication,
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function request(command: BotConfigurationCommandV1) {
|
|
212
|
+
return {
|
|
213
|
+
schemaVersion: 1 as const,
|
|
214
|
+
userId: "user-1",
|
|
215
|
+
botId: command.botId,
|
|
216
|
+
command,
|
|
145
217
|
};
|
|
146
218
|
}
|
|
147
219
|
|
|
148
|
-
describe("Bot
|
|
220
|
+
describe("Bot configuration admission", () => {
|
|
149
221
|
test("rejects an unmaterialized Bot without writing durable state", async () => {
|
|
150
222
|
const storage = new MemoryStorage();
|
|
151
|
-
const contribution =
|
|
152
|
-
|
|
153
|
-
env: {} as never,
|
|
154
|
-
});
|
|
223
|
+
const contribution = host(storage, configuredUser);
|
|
224
|
+
|
|
155
225
|
await expect(
|
|
156
226
|
contribution.readConfiguration({
|
|
157
227
|
schemaVersion: 1,
|
|
@@ -177,20 +247,18 @@ describe("Bot capability assignment admission", () => {
|
|
|
177
247
|
const identity = { userId: "user-1", botId: "primary" };
|
|
178
248
|
await contribution.materializeSettings(identity, { name: "Primary" });
|
|
179
249
|
archived = true;
|
|
250
|
+
|
|
180
251
|
await expect(
|
|
181
|
-
contribution.executeConfiguration(
|
|
182
|
-
|
|
183
|
-
userId: identity.userId,
|
|
184
|
-
botId: identity.botId,
|
|
185
|
-
command: {
|
|
252
|
+
contribution.executeConfiguration(
|
|
253
|
+
request({
|
|
186
254
|
schemaVersion: 1,
|
|
187
255
|
type: "bot/update-profile",
|
|
188
256
|
commandId: "archived-profile",
|
|
189
257
|
botId: identity.botId,
|
|
190
258
|
expectedRevision: 0,
|
|
191
259
|
profile: { name: "Changed" },
|
|
192
|
-
},
|
|
193
|
-
|
|
260
|
+
}),
|
|
261
|
+
),
|
|
194
262
|
).rejects.toThrow("archived");
|
|
195
263
|
expect(await contribution.getSettings(identity)).toMatchObject({
|
|
196
264
|
revision: 0,
|
|
@@ -198,7 +266,7 @@ describe("Bot capability assignment admission", () => {
|
|
|
198
266
|
});
|
|
199
267
|
});
|
|
200
268
|
|
|
201
|
-
test("rechecks lifecycle admission in the
|
|
269
|
+
test("rechecks lifecycle admission in the mutation transaction", async () => {
|
|
202
270
|
const storage = new MemoryStorage();
|
|
203
271
|
let admissions = 0;
|
|
204
272
|
const contribution = createShellBotBackendContribution({
|
|
@@ -211,24 +279,26 @@ describe("Bot capability assignment admission", () => {
|
|
|
211
279
|
: Promise.reject(new Error(`Bot "${botId}" is archived`));
|
|
212
280
|
},
|
|
213
281
|
});
|
|
214
|
-
|
|
215
|
-
|
|
282
|
+
await contribution.materializeSettings(
|
|
283
|
+
{ userId: "user-1", botId: "primary" },
|
|
284
|
+
{ name: "Primary" },
|
|
285
|
+
);
|
|
286
|
+
|
|
216
287
|
await expect(
|
|
217
|
-
contribution.executeConfiguration(
|
|
218
|
-
|
|
219
|
-
userId: identity.userId,
|
|
220
|
-
botId: identity.botId,
|
|
221
|
-
command: {
|
|
288
|
+
contribution.executeConfiguration(
|
|
289
|
+
request({
|
|
222
290
|
schemaVersion: 1,
|
|
223
291
|
type: "bot/update-profile",
|
|
224
292
|
commandId: "racing-profile",
|
|
225
|
-
botId:
|
|
293
|
+
botId: "primary",
|
|
226
294
|
expectedRevision: 0,
|
|
227
295
|
profile: { name: "Changed" },
|
|
228
|
-
},
|
|
229
|
-
|
|
296
|
+
}),
|
|
297
|
+
),
|
|
230
298
|
).rejects.toThrow("archived");
|
|
231
|
-
expect(
|
|
299
|
+
expect(
|
|
300
|
+
await contribution.getSettings({ userId: "user-1", botId: "primary" }),
|
|
301
|
+
).toMatchObject({
|
|
232
302
|
revision: 0,
|
|
233
303
|
profile: { name: "Primary" },
|
|
234
304
|
});
|
|
@@ -242,8 +312,7 @@ describe("Bot capability assignment admission", () => {
|
|
|
242
312
|
revision: 7,
|
|
243
313
|
profile: { name: "Primary" },
|
|
244
314
|
notifications: { enabled: true },
|
|
245
|
-
|
|
246
|
-
assignmentOperations: [],
|
|
315
|
+
packageValues: {},
|
|
247
316
|
} satisfies BotSettingsViewV1;
|
|
248
317
|
await storage.put({
|
|
249
318
|
identity: { userId: "user-1", botId: "primary" },
|
|
@@ -278,58 +347,23 @@ describe("Bot capability assignment admission", () => {
|
|
|
278
347
|
});
|
|
279
348
|
const contribution = createShellBotBackendContribution({
|
|
280
349
|
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,
|
|
350
|
+
env: {} as never,
|
|
297
351
|
});
|
|
352
|
+
const identity = { userId: "user-1", botId: "primary" };
|
|
298
353
|
|
|
299
|
-
await contribution.materializeSettings(
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
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
|
-
},
|
|
354
|
+
await contribution.materializeSettings(identity, { name: "Primary" });
|
|
355
|
+
await expect(contribution.getSettings(identity)).resolves.toMatchObject({
|
|
356
|
+
revision: 0,
|
|
357
|
+
profile: { name: "Primary" },
|
|
358
|
+
packageValues: {},
|
|
317
359
|
});
|
|
318
360
|
});
|
|
319
361
|
|
|
320
362
|
test("binds in-flight and durable receipts to the complete Bot command", async () => {
|
|
321
363
|
const storage = new MemoryStorage();
|
|
322
|
-
const
|
|
323
|
-
readConfiguration: () => Promise.resolve(installedUser()),
|
|
324
|
-
};
|
|
325
|
-
const host = {
|
|
364
|
+
const backendHost = {
|
|
326
365
|
state: { storage } as unknown as DurableObjectState,
|
|
327
|
-
env: {
|
|
328
|
-
USER_CONFIGURATIONS: {
|
|
329
|
-
idFromName: () => "user-1",
|
|
330
|
-
get: () => userConfiguration,
|
|
331
|
-
},
|
|
332
|
-
} as never,
|
|
366
|
+
env: {} as never,
|
|
333
367
|
};
|
|
334
368
|
const original: BotConfigurationCommandV1 = {
|
|
335
369
|
schemaVersion: 1,
|
|
@@ -343,13 +377,7 @@ describe("Bot capability assignment admission", () => {
|
|
|
343
377
|
...original,
|
|
344
378
|
profile: { name: "Collision" },
|
|
345
379
|
};
|
|
346
|
-
const
|
|
347
|
-
schemaVersion: 1 as const,
|
|
348
|
-
userId: "user-1",
|
|
349
|
-
botId: "primary",
|
|
350
|
-
command,
|
|
351
|
-
});
|
|
352
|
-
const contribution = createShellBotBackendContribution(host);
|
|
380
|
+
const contribution = createShellBotBackendContribution(backendHost);
|
|
353
381
|
await contribution.materializeSettings(
|
|
354
382
|
{ userId: "user-1", botId: "primary" },
|
|
355
383
|
{ name: "Primary" },
|
|
@@ -363,7 +391,7 @@ describe("Bot capability assignment admission", () => {
|
|
|
363
391
|
);
|
|
364
392
|
const receipt = await first;
|
|
365
393
|
|
|
366
|
-
const redeployed = createShellBotBackendContribution(
|
|
394
|
+
const redeployed = createShellBotBackendContribution(backendHost);
|
|
367
395
|
await expect(
|
|
368
396
|
redeployed.executeConfiguration(request(original)),
|
|
369
397
|
).resolves.toEqual(receipt);
|
|
@@ -383,1375 +411,262 @@ describe("Bot capability assignment admission", () => {
|
|
|
383
411
|
profile: { name: "Original" },
|
|
384
412
|
});
|
|
385
413
|
});
|
|
414
|
+
});
|
|
386
415
|
|
|
387
|
-
|
|
416
|
+
describe("generic per-Turn model resolution", () => {
|
|
417
|
+
test("runs on the platform model without creating a per-Bot model record", async () => {
|
|
388
418
|
const storage = new MemoryStorage();
|
|
389
|
-
|
|
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
|
-
});
|
|
446
|
-
},
|
|
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
|
-
});
|
|
419
|
+
const contribution = host(storage, configuredUser);
|
|
458
420
|
const identity = { userId: "user-1", botId: "primary" };
|
|
459
421
|
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 });
|
|
468
422
|
|
|
469
|
-
await
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
).rejects.toThrow("botId is invalid");
|
|
476
|
-
await expect(
|
|
477
|
-
contribution.executeConfiguration({
|
|
478
|
-
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
|
-
},
|
|
488
|
-
}),
|
|
489
|
-
).rejects.toThrow("profile.name must be a string");
|
|
490
|
-
expect(await read()).toMatchObject({ revision: 0, assignments: [] });
|
|
491
|
-
|
|
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"),
|
|
501
|
-
});
|
|
502
|
-
const readsAfterFirst = reads;
|
|
503
|
-
expect(await execute(missingConnection)).toEqual(first);
|
|
504
|
-
expect(reads).toBe(readsAfterFirst);
|
|
505
|
-
|
|
506
|
-
expect(
|
|
507
|
-
await execute(
|
|
508
|
-
assignmentCommand("unknown-capability", {
|
|
509
|
-
packageId: "composio",
|
|
510
|
-
capabilityId: "unknown",
|
|
511
|
-
connectionId: "gmail-1",
|
|
512
|
-
}),
|
|
513
|
-
),
|
|
514
|
-
).toMatchObject({ status: "rejected", revision: 0 });
|
|
515
|
-
|
|
516
|
-
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
|
-
}),
|
|
529
|
-
),
|
|
530
|
-
).toMatchObject({ status: "rejected", revision: 0 });
|
|
531
|
-
|
|
532
|
-
user = {
|
|
533
|
-
...installedUser(),
|
|
534
|
-
packages: [{ ...installedUser().packages[0]!, state: "disabled" }],
|
|
535
|
-
};
|
|
536
|
-
expect(
|
|
537
|
-
await execute(
|
|
538
|
-
assignmentCommand("disabled-package", {
|
|
539
|
-
packageId: "composio",
|
|
540
|
-
capabilityId: "gmail-tools",
|
|
541
|
-
connectionId: "gmail-1",
|
|
542
|
-
}),
|
|
543
|
-
),
|
|
544
|
-
).toMatchObject({ status: "rejected", revision: 0 });
|
|
545
|
-
|
|
546
|
-
expect(dependencyClaims).toBe(0);
|
|
547
|
-
expect(await read()).toMatchObject({
|
|
548
|
-
revision: 0,
|
|
549
|
-
assignments: [],
|
|
550
|
-
} satisfies Partial<BotSettingsViewV1>);
|
|
551
|
-
|
|
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
|
-
});
|
|
568
|
-
|
|
569
|
-
claimAuthorized = true;
|
|
570
|
-
expect(
|
|
571
|
-
await execute(
|
|
572
|
-
assignmentCommand("valid-assignment", {
|
|
573
|
-
packageId: "composio",
|
|
574
|
-
capabilityId: "gmail-tools",
|
|
575
|
-
connectionId: "gmail-1",
|
|
576
|
-
}),
|
|
577
|
-
),
|
|
578
|
-
).toMatchObject({ status: "applied", revision: 1 });
|
|
579
|
-
expect(dependencyClaims).toBe(2);
|
|
580
|
-
expect(dependencyAcknowledgements).toBe(1);
|
|
581
|
-
expect(await read()).toMatchObject({
|
|
582
|
-
revision: 1,
|
|
583
|
-
assignments: [
|
|
584
|
-
{
|
|
585
|
-
assignmentId: "valid-assignment",
|
|
586
|
-
state: "enabled",
|
|
587
|
-
connectionId: "gmail-1",
|
|
588
|
-
},
|
|
589
|
-
],
|
|
423
|
+
const result = await contribution.run({
|
|
424
|
+
...identity,
|
|
425
|
+
runId: "platform-model-run",
|
|
426
|
+
sessionId: "user-1:primary",
|
|
427
|
+
acceptedAt: "2026-09-02T00:00:00.000Z",
|
|
428
|
+
text: "hello",
|
|
590
429
|
});
|
|
591
430
|
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
commandId: "reuse-assignment-authority",
|
|
597
|
-
botId: "primary",
|
|
598
|
-
expectedRevision: 1,
|
|
599
|
-
assignment: {
|
|
600
|
-
assignmentId: "valid-assignment",
|
|
601
|
-
packageId: "composio",
|
|
602
|
-
capabilityId: "gmail-send",
|
|
603
|
-
connectionId: "gmail-1",
|
|
604
|
-
},
|
|
605
|
-
}),
|
|
606
|
-
).resolves.toMatchObject({
|
|
607
|
-
status: "rejected",
|
|
608
|
-
revision: 1,
|
|
609
|
-
failure: "Assignment ID cannot change Package Capability authority",
|
|
610
|
-
});
|
|
611
|
-
expect(dependencyClaims).toBe(2);
|
|
431
|
+
expect(result.text).toBe("Cordis runtime: hello");
|
|
432
|
+
const settings = await contribution.getSettings(identity);
|
|
433
|
+
expect(settings).toMatchObject({ revision: 0, packageValues: {} });
|
|
434
|
+
expect(Object.hasOwn(settings, "model")).toBe(false);
|
|
612
435
|
});
|
|
613
436
|
|
|
614
|
-
test("
|
|
437
|
+
test("uses an enabled Bot-scoped model value and preserves it while disabled", async () => {
|
|
615
438
|
const storage = new MemoryStorage();
|
|
616
|
-
let
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
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
|
-
},
|
|
439
|
+
let user = configuredUser();
|
|
440
|
+
user.packages[1] = {
|
|
441
|
+
...user.packages[1]!,
|
|
442
|
+
state: "installed",
|
|
443
|
+
values: { model: model("flock-ai-ambient", "account-model") },
|
|
667
444
|
};
|
|
668
|
-
const contribution =
|
|
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
|
-
});
|
|
445
|
+
const contribution = host(storage, () => user);
|
|
678
446
|
const identity = { userId: "user-1", botId: "primary" };
|
|
679
447
|
await contribution.materializeSettings(identity, { name: "Primary" });
|
|
680
|
-
|
|
681
|
-
|
|
448
|
+
await contribution.executeConfiguration(
|
|
449
|
+
request({
|
|
682
450
|
schemaVersion: 1,
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
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
|
-
});
|
|
451
|
+
type: "bot/set-package-settings",
|
|
452
|
+
commandId: "set-bot-model",
|
|
453
|
+
botId: "primary",
|
|
454
|
+
expectedRevision: 0,
|
|
455
|
+
packageId: "custom-models",
|
|
456
|
+
values: { model: model("flock-ai-ambient", "bot-model") },
|
|
457
|
+
}),
|
|
458
|
+
);
|
|
715
459
|
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
...unavailable,
|
|
719
|
-
assignments: unavailable.assignments.map((assignment) => ({
|
|
720
|
-
...assignment,
|
|
721
|
-
state: "unavailable" as const,
|
|
722
|
-
})),
|
|
460
|
+
expect(await contribution.resolveConfiguration(identity)).toMatchObject({
|
|
461
|
+
model: model("flock-ai-ambient", "bot-model"),
|
|
723
462
|
});
|
|
724
463
|
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
464
|
+
user = {
|
|
465
|
+
...user,
|
|
466
|
+
packages: user.packages.map((pkg) =>
|
|
467
|
+
pkg.packageId === "custom-models"
|
|
468
|
+
? { ...pkg, state: "disabled" as const }
|
|
469
|
+
: pkg,
|
|
470
|
+
),
|
|
732
471
|
};
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
await expect(execute(unbind)).resolves.toMatchObject({
|
|
736
|
-
status: "pending",
|
|
472
|
+
expect(await contribution.resolveConfiguration(identity)).toMatchObject({
|
|
473
|
+
model: model("flock-ai-ambient", "@flock/auto"),
|
|
737
474
|
});
|
|
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
475
|
expect(await contribution.getSettings(identity)).toMatchObject({
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
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,
|
|
476
|
+
packageValues: {
|
|
477
|
+
"custom-models": {
|
|
478
|
+
model: model("flock-ai-ambient", "bot-model"),
|
|
822
479
|
},
|
|
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
480
|
},
|
|
867
|
-
"assignment-generation:gmail-tool": "tool-generation",
|
|
868
481
|
});
|
|
869
|
-
generations.add("tool-generation");
|
|
870
482
|
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
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
|
-
},
|
|
483
|
+
user = {
|
|
484
|
+
...user,
|
|
485
|
+
packages: user.packages.map((pkg) =>
|
|
486
|
+
pkg.packageId === "custom-models"
|
|
487
|
+
? { ...pkg, state: "installed" as const }
|
|
488
|
+
: pkg,
|
|
489
|
+
),
|
|
891
490
|
};
|
|
892
|
-
await
|
|
893
|
-
|
|
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,
|
|
491
|
+
expect(await contribution.resolveConfiguration(identity)).toMatchObject({
|
|
492
|
+
model: model("flock-ai-ambient", "bot-model"),
|
|
901
493
|
});
|
|
494
|
+
});
|
|
902
495
|
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
model: { connectionId: "gmail-2" },
|
|
913
|
-
assignments: [
|
|
914
|
-
{ assignmentId: "gmail-tool", state: "enabled" },
|
|
915
|
-
{ assignmentId: "gmail-model-1", state: "enabled" },
|
|
916
|
-
],
|
|
496
|
+
test("Package disablement and Connection revocation fail every Bot's next Turn closed", async () => {
|
|
497
|
+
let user = configuredUser();
|
|
498
|
+
const bots = ["alpha", "beta"].map((botId) => {
|
|
499
|
+
const storage = new MemoryStorage();
|
|
500
|
+
return {
|
|
501
|
+
botId,
|
|
502
|
+
storage,
|
|
503
|
+
contribution: host(storage, () => user),
|
|
504
|
+
};
|
|
917
505
|
});
|
|
506
|
+
for (const bot of bots) {
|
|
507
|
+
await bot.contribution.materializeSettings(
|
|
508
|
+
{ userId: "user-1", botId: bot.botId },
|
|
509
|
+
{ name: bot.botId },
|
|
510
|
+
);
|
|
511
|
+
}
|
|
918
512
|
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
513
|
+
user = {
|
|
514
|
+
...user,
|
|
515
|
+
packages: user.packages.map((pkg) =>
|
|
516
|
+
pkg.packageId === "provider-flock-ai"
|
|
517
|
+
? { ...pkg, state: "disabled" as const }
|
|
518
|
+
: pkg,
|
|
519
|
+
),
|
|
520
|
+
};
|
|
521
|
+
for (const bot of bots) {
|
|
522
|
+
const runId = `${bot.botId}-disabled-package`;
|
|
925
523
|
await expect(
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
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
|
-
},
|
|
524
|
+
bot.contribution.run({
|
|
525
|
+
userId: "user-1",
|
|
526
|
+
botId: bot.botId,
|
|
527
|
+
runId,
|
|
528
|
+
sessionId: `user-1:${bot.botId}`,
|
|
529
|
+
acceptedAt: "2026-09-02T00:01:00.000Z",
|
|
530
|
+
text: "must fail",
|
|
942
531
|
}),
|
|
943
|
-
).
|
|
944
|
-
|
|
945
|
-
|
|
532
|
+
).rejects.toThrow("not installed and enabled");
|
|
533
|
+
expect(await bot.storage.get(`run:${runId}`)).toMatchObject({
|
|
534
|
+
status: "failed",
|
|
535
|
+
failure: expect.stringContaining("not installed and enabled"),
|
|
946
536
|
});
|
|
947
537
|
}
|
|
948
538
|
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
539
|
+
user = configuredUser();
|
|
540
|
+
user.connections[0] = { ...user.connections[0]!, state: "revoked" };
|
|
541
|
+
for (const bot of bots) {
|
|
542
|
+
const runId = `${bot.botId}-revoked-connection`;
|
|
543
|
+
await expect(
|
|
544
|
+
bot.contribution.run({
|
|
545
|
+
userId: "user-1",
|
|
546
|
+
botId: bot.botId,
|
|
547
|
+
runId,
|
|
548
|
+
sessionId: `user-1:${bot.botId}`,
|
|
549
|
+
acceptedAt: "2026-09-02T00:02:00.000Z",
|
|
550
|
+
text: "must fail",
|
|
551
|
+
}),
|
|
552
|
+
).rejects.toThrow("is revoked");
|
|
553
|
+
expect(await bot.storage.get(`run:${runId}`)).toMatchObject({
|
|
554
|
+
status: "failed",
|
|
555
|
+
failure: expect.stringContaining("is revoked"),
|
|
556
|
+
});
|
|
557
|
+
}
|
|
965
558
|
});
|
|
559
|
+
});
|
|
966
560
|
|
|
967
|
-
|
|
561
|
+
describe("Bot Package setting commands", () => {
|
|
562
|
+
test("validates, revision-fences, merges, and replays durably", async () => {
|
|
968
563
|
const storage = new MemoryStorage();
|
|
969
|
-
const user =
|
|
970
|
-
user
|
|
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
|
-
});
|
|
564
|
+
const user = configuredUser();
|
|
565
|
+
const contribution = host(storage, () => user);
|
|
1035
566
|
const identity = { userId: "user-1", botId: "primary" };
|
|
1036
567
|
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",
|
|
1111
|
-
expectedRevision: 2,
|
|
1112
|
-
assignmentId: "mail",
|
|
1113
|
-
}),
|
|
1114
|
-
).resolves.toEqual(pendingUnassign);
|
|
1115
|
-
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
568
|
|
|
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
|
-
revision: 3,
|
|
1137
|
-
assignments: [],
|
|
1138
|
-
assignmentOperations: [],
|
|
1139
|
-
});
|
|
1140
569
|
await expect(
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
...identity,
|
|
1144
|
-
command: {
|
|
570
|
+
contribution.executeConfiguration(
|
|
571
|
+
request({
|
|
1145
572
|
schemaVersion: 1,
|
|
1146
|
-
type: "bot/
|
|
1147
|
-
commandId: "
|
|
573
|
+
type: "bot/set-package-settings",
|
|
574
|
+
commandId: "invalid-model",
|
|
1148
575
|
botId: "primary",
|
|
1149
|
-
expectedRevision:
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
schemaVersion: 1,
|
|
1157
|
-
...identity,
|
|
1158
|
-
command: {
|
|
1159
|
-
schemaVersion: 1,
|
|
1160
|
-
type: "bot/unassign-capability",
|
|
1161
|
-
commandId: "unassign-1",
|
|
1162
|
-
botId: "primary",
|
|
1163
|
-
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
|
-
});
|
|
576
|
+
expectedRevision: 0,
|
|
577
|
+
packageId: "custom-models",
|
|
578
|
+
values: { model: { connectionId: "flock-ai-ambient" } as never },
|
|
579
|
+
}),
|
|
580
|
+
),
|
|
581
|
+
).rejects.toThrow("model has invalid fields");
|
|
582
|
+
expect((await contribution.getSettings(identity)).revision).toBe(0);
|
|
1345
583
|
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
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
|
-
});
|
|
584
|
+
const first: BotConfigurationCommandV1 = {
|
|
585
|
+
schemaVersion: 1,
|
|
586
|
+
type: "bot/set-package-settings",
|
|
587
|
+
commandId: "set-package-values",
|
|
588
|
+
botId: "primary",
|
|
589
|
+
expectedRevision: 0,
|
|
590
|
+
packageId: "custom-models",
|
|
591
|
+
values: {
|
|
592
|
+
model: model("flock-ai-ambient", "bot-model"),
|
|
593
|
+
tone: "concise",
|
|
1377
594
|
},
|
|
1378
595
|
};
|
|
1379
|
-
const
|
|
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
|
-
});
|
|
596
|
+
const receipt = await contribution.executeConfiguration(request(first));
|
|
1397
597
|
await expect(
|
|
1398
|
-
contribution.executeConfiguration(
|
|
1399
|
-
|
|
1400
|
-
...identity,
|
|
1401
|
-
command,
|
|
1402
|
-
}),
|
|
1403
|
-
).resolves.toEqual({
|
|
1404
|
-
schemaVersion: 1,
|
|
1405
|
-
commandId: "owner-retry",
|
|
1406
|
-
revision: 0,
|
|
1407
|
-
status: "pending",
|
|
1408
|
-
});
|
|
598
|
+
contribution.executeConfiguration(request(first)),
|
|
599
|
+
).resolves.toEqual(receipt);
|
|
1409
600
|
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
601
|
revision: 1,
|
|
1419
|
-
|
|
1420
|
-
|
|
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,
|
|
602
|
+
packageValues: {
|
|
603
|
+
"custom-models": {
|
|
604
|
+
model: model("flock-ai-ambient", "bot-model"),
|
|
605
|
+
tone: "concise",
|
|
1477
606
|
},
|
|
1478
|
-
}
|
|
1479
|
-
compileApplication: compileAssignmentTestApplication,
|
|
607
|
+
},
|
|
1480
608
|
});
|
|
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
609
|
|
|
1512
|
-
await
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
commandId: "unassign-a",
|
|
1521
|
-
botId: "primary",
|
|
1522
|
-
expectedRevision: 3,
|
|
1523
|
-
assignmentId: "mail-a",
|
|
1524
|
-
});
|
|
610
|
+
await contribution.executeConfiguration(
|
|
611
|
+
request({
|
|
612
|
+
...first,
|
|
613
|
+
commandId: "update-model-only",
|
|
614
|
+
expectedRevision: 1,
|
|
615
|
+
values: { model: model("flock-ai-ambient", "new-bot-model") },
|
|
616
|
+
}),
|
|
617
|
+
);
|
|
1525
618
|
expect(await contribution.getSettings(identity)).toMatchObject({
|
|
1526
|
-
revision:
|
|
1527
|
-
|
|
1528
|
-
{
|
|
1529
|
-
|
|
1530
|
-
|
|
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,
|
|
619
|
+
revision: 2,
|
|
620
|
+
packageValues: {
|
|
621
|
+
"custom-models": {
|
|
622
|
+
model: model("flock-ai-ambient", "new-bot-model"),
|
|
623
|
+
tone: "concise",
|
|
1583
624
|
},
|
|
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
625
|
},
|
|
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
626
|
});
|
|
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
627
|
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
628
|
+
await contribution.executeConfiguration(
|
|
629
|
+
request({
|
|
630
|
+
...first,
|
|
631
|
+
commandId: "unset-bot-model",
|
|
632
|
+
expectedRevision: 2,
|
|
633
|
+
values: undefined,
|
|
634
|
+
unset: ["model"],
|
|
635
|
+
}),
|
|
636
|
+
);
|
|
637
|
+
expect(await contribution.getSettings(identity)).toMatchObject({
|
|
638
|
+
revision: 3,
|
|
639
|
+
packageValues: { "custom-models": { tone: "concise" } },
|
|
1700
640
|
});
|
|
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
641
|
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
{ ...installedUser().connections[0]!, state: "revoked" as const },
|
|
1718
|
-
],
|
|
1719
|
-
newBotModelTemplate: {
|
|
1720
|
-
connectionId: "gmail-1",
|
|
1721
|
-
providerModelId: "fixture-model",
|
|
1722
|
-
},
|
|
1723
|
-
newBotModelTemplateSource: "auto",
|
|
642
|
+
await expect(
|
|
643
|
+
contribution.executeConfiguration(
|
|
644
|
+
request({
|
|
645
|
+
...first,
|
|
646
|
+
commandId: "unset-unknown",
|
|
647
|
+
expectedRevision: 3,
|
|
648
|
+
values: undefined,
|
|
649
|
+
unset: ["unknown-setting"],
|
|
1724
650
|
}),
|
|
1725
|
-
|
|
1726
|
-
|
|
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" });
|
|
651
|
+
),
|
|
652
|
+
).rejects.toThrow(/not declared by this Package/);
|
|
1751
653
|
|
|
1752
|
-
expect(
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
654
|
+
await expect(
|
|
655
|
+
contribution.executeConfiguration(
|
|
656
|
+
request({
|
|
657
|
+
...first,
|
|
658
|
+
commandId: "stale-revision",
|
|
659
|
+
expectedRevision: 2,
|
|
660
|
+
}),
|
|
661
|
+
),
|
|
662
|
+
).rejects.toThrow("configuration revision is 3");
|
|
663
|
+
await expect(
|
|
664
|
+
contribution.executeConfiguration(
|
|
665
|
+
request({
|
|
666
|
+
...first,
|
|
667
|
+
values: { tone: "different" },
|
|
668
|
+
}),
|
|
669
|
+
),
|
|
670
|
+
).rejects.toThrow("reused for a different command");
|
|
1756
671
|
});
|
|
1757
672
|
});
|