@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.
- package/frockbot.json +4 -0
- package/package.json +30 -28
- package/src/agent.ts +10 -13
- package/src/backend-authoring.test.ts +356 -2
- package/src/backend-authoring.ts +585 -20
- package/src/backend-composition-input.test.ts +65 -0
- package/src/backend-composition-input.ts +58 -0
- package/src/backend-composition.ts +23 -5
- package/src/backend-configuration.test.ts +549 -1468
- package/src/backend-contracts.test.ts +28 -0
- package/src/backend-contracts.ts +3 -1
- package/src/backend-execution.ts +0 -4
- package/src/backend-iframe-ui.test.ts +131 -0
- package/src/backend-image.test.ts +8 -8
- package/src/backend-image.ts +13 -13
- package/src/backend-isolate.test.ts +230 -89
- package/src/backend-isolate.ts +103 -200
- package/src/backend-package-catalog.test.ts +451 -0
- package/src/backend-package-catalog.ts +923 -0
- package/src/backend-recovery-integration.test.ts +17 -67
- package/src/backend-routines.ts +1 -1
- package/src/backend-runner-iframe.test.ts +74 -0
- package/src/backend-runner.ts +192 -0
- package/src/backend.ts +1198 -1781
- package/src/client/FrockBotApp.vue +44 -73
- package/src/client/PackageIframeHost.vue +218 -0
- package/src/client/PackageIframeSettings.vue +52 -0
- package/src/client/SendPayloadView.vue +0 -60
- package/src/client/index.test.ts +439 -380
- package/src/client/index.ts +163 -257
- package/src/client/model-presentation.test.ts +21 -8
- package/src/client/model-presentation.ts +14 -7
- package/src/client/package-iframe-host-message.test.ts +41 -0
- package/src/client/package-iframe-host-message.ts +27 -0
- package/src/client/styles.css +0 -27
- package/src/composition-views.ts +54 -0
- package/src/settings-links.test.ts +2 -10
- package/src/settings-links.ts +1 -13
- package/src/shared.ts +10 -13
- package/src/backend-assignment.test.ts +0 -161
- 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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
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("
|
|
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: "
|
|
87
|
-
specifier: "@test/
|
|
97
|
+
id: "custom-models",
|
|
98
|
+
specifier: "@test/custom-models",
|
|
88
99
|
version: "0.0.1",
|
|
89
100
|
manifest: {
|
|
90
101
|
...template.manifest,
|
|
91
|
-
id: "
|
|
92
|
-
displayName: "
|
|
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
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
-
|
|
149
|
-
|
|
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
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
215
|
-
|
|
335
|
+
await contribution.materializeSettings(
|
|
336
|
+
{ userId: "user-1", botId: "primary" },
|
|
337
|
+
{ name: "Primary" },
|
|
338
|
+
);
|
|
339
|
+
|
|
216
340
|
await expect(
|
|
217
|
-
contribution.executeConfiguration(
|
|
218
|
-
|
|
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:
|
|
346
|
+
botId: "primary",
|
|
226
347
|
expectedRevision: 0,
|
|
227
348
|
profile: { name: "Changed" },
|
|
228
|
-
},
|
|
229
|
-
|
|
349
|
+
}),
|
|
350
|
+
),
|
|
230
351
|
).rejects.toThrow("archived");
|
|
231
|
-
expect(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
},
|
|
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
|
|
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
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
const
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
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
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
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
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
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.
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
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
|
-
).
|
|
476
|
-
|
|
477
|
-
|
|
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
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
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
|
-
)
|
|
490
|
-
expect(await read()).toMatchObject({ revision: 0, assignments: [] });
|
|
624
|
+
);
|
|
491
625
|
|
|
492
|
-
|
|
493
|
-
|
|
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
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
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
|
-
|
|
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
|
-
...
|
|
518
|
-
|
|
519
|
-
|
|
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
|
-
|
|
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
|
-
...
|
|
534
|
-
packages:
|
|
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
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
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
|
-
|
|
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
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
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
|
-
|
|
553
|
-
|
|
554
|
-
const
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
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
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
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
|
-
).
|
|
579
|
-
expect(
|
|
580
|
-
|
|
581
|
-
|
|
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
|
-
|
|
584
|
-
{
|
|
585
|
-
|
|
586
|
-
|
|
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
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
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
|
-
|
|
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
|
-
)
|
|
607
|
-
|
|
608
|
-
revision:
|
|
609
|
-
|
|
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
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
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
|
-
|
|
799
|
+
values: undefined,
|
|
800
|
+
unset: ["model"],
|
|
1113
801
|
}),
|
|
1114
|
-
)
|
|
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
|
-
|
|
1138
|
-
assignmentOperations: [],
|
|
805
|
+
packageValues: { "custom-models": { tone: "concise" } },
|
|
1139
806
|
});
|
|
807
|
+
|
|
1140
808
|
await expect(
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
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
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
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
|
-
|
|
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" });
|
|
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
|
});
|