@frockbot/plugin-settings 0.0.0 → 0.1.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 +55 -0
- package/package.json +43 -6
- package/src/backend.test.ts +397 -0
- package/src/backend.ts +262 -0
- package/src/client/BotPanel.vue +57 -0
- package/src/client/BotSettingsSurface.vue +1076 -0
- package/src/client/BotSettingsTrigger.vue +26 -0
- package/src/client/ConnectionsSurface.vue +813 -0
- package/src/client/ModelsSurface.vue +419 -0
- package/src/client/PackageAccounts.vue +330 -0
- package/src/client/PackageCatalogSurface.vue +584 -0
- package/src/client/PackageSettingsForm.vue +150 -0
- package/src/client/PackageSettingsSection.vue +66 -0
- package/src/client/PluginsSurface.vue +412 -0
- package/src/client/PluginsTrigger.vue +62 -0
- package/src/client/UserProfileTrigger.vue +223 -0
- package/src/client/UserSettingsSurface.vue +242 -0
- package/src/client/assignment-operations.ts +22 -0
- package/src/client/bot-settings.test.ts +218 -0
- package/src/client/bot-settings.ts +150 -0
- package/src/client/index.test.ts +113 -0
- package/src/client/index.ts +78 -0
- package/src/client/package-settings.test.ts +63 -0
- package/src/client/package-settings.ts +77 -0
- package/src/client/package-surfaces.test.ts +147 -0
- package/src/client/package-surfaces.ts +93 -0
- package/src/client/user-display-name.test.ts +44 -0
- package/src/client/user-display-name.ts +16 -0
- package/src/env.d.ts +6 -0
- package/src/index.ts +1 -0
- package/src/manifest.ts +3 -0
- package/src/user.test.ts +1277 -0
- package/src/user.ts +1221 -0
- package/tsconfig.json +15 -0
- package/vite.config.ts +30 -0
- package/README.md +0 -3
package/src/user.test.ts
ADDED
|
@@ -0,0 +1,1277 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import type { CatalogEntryV1, CatalogIndexV1 } from "@frockbot/catalog-core";
|
|
3
|
+
import {
|
|
4
|
+
capabilityAssignmentFailureV1,
|
|
5
|
+
initializeBotSettingsV1,
|
|
6
|
+
resolveBotExecutionPlanV1,
|
|
7
|
+
USER_PROFILE_PLACEHOLDER_NAME_V1,
|
|
8
|
+
type UserSettingsViewV1,
|
|
9
|
+
} from "@frockbot/configuration-core";
|
|
10
|
+
import {
|
|
11
|
+
createUserSettingsBackendContribution,
|
|
12
|
+
type UserPackageCatalogHost,
|
|
13
|
+
type UserSettingsStorage,
|
|
14
|
+
} from "./user.js";
|
|
15
|
+
|
|
16
|
+
class MemoryStorage implements UserSettingsStorage {
|
|
17
|
+
readonly values = new Map<string, unknown>();
|
|
18
|
+
|
|
19
|
+
get<T>(key: string): Promise<T | undefined> {
|
|
20
|
+
return Promise.resolve(this.values.get(key) as T | undefined);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
put<T>(key: string, value: T): Promise<void>;
|
|
24
|
+
put(entries: Record<string, unknown>): Promise<void>;
|
|
25
|
+
put(key: string | Record<string, unknown>, value?: unknown): Promise<void> {
|
|
26
|
+
if (typeof key === "string") this.values.set(key, structuredClone(value));
|
|
27
|
+
else {
|
|
28
|
+
for (const [entry, item] of Object.entries(key)) {
|
|
29
|
+
this.values.set(entry, structuredClone(item));
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return Promise.resolve();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
transaction<T>(callback: (storage: MemoryStorage) => Promise<T>): Promise<T> {
|
|
36
|
+
return callback(this);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function contribution(storage = new MemoryStorage()) {
|
|
41
|
+
return createUserSettingsBackendContribution({
|
|
42
|
+
storage,
|
|
43
|
+
availablePackages: [
|
|
44
|
+
{ packageId: "flock", version: "0.0.1" },
|
|
45
|
+
{ packageId: "settings", version: "0.0.1" },
|
|
46
|
+
{
|
|
47
|
+
packageId: "provider-ollama-cloud",
|
|
48
|
+
version: "0.0.1",
|
|
49
|
+
settings: [
|
|
50
|
+
{
|
|
51
|
+
id: "web-search-max-results",
|
|
52
|
+
schemaVersion: 1,
|
|
53
|
+
scopes: ["user"],
|
|
54
|
+
schema: { type: "integer", minimum: 1, maximum: 10 },
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
id: "label",
|
|
58
|
+
schemaVersion: 1,
|
|
59
|
+
scopes: ["user"],
|
|
60
|
+
schema: { type: "string", maxLength: 16 },
|
|
61
|
+
},
|
|
62
|
+
],
|
|
63
|
+
},
|
|
64
|
+
],
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Install the Ollama Package, so a settings command has a row to write to. */
|
|
69
|
+
async function installOllama(
|
|
70
|
+
settings: ReturnType<typeof contribution>,
|
|
71
|
+
userId: string,
|
|
72
|
+
): Promise<void> {
|
|
73
|
+
await settings.executeConfiguration({
|
|
74
|
+
schemaVersion: 1,
|
|
75
|
+
userId,
|
|
76
|
+
command: {
|
|
77
|
+
schemaVersion: 1,
|
|
78
|
+
type: "user/install-package",
|
|
79
|
+
commandId: `install-${userId}`,
|
|
80
|
+
expectedRevision: 0,
|
|
81
|
+
packageId: "provider-ollama-cloud",
|
|
82
|
+
version: "0.0.1",
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
describe("User settings backend Contribution", () => {
|
|
88
|
+
test("bootstraps first-party Packages once and preserves a later uninstall", async () => {
|
|
89
|
+
const storage = new MemoryStorage();
|
|
90
|
+
const settings = createUserSettingsBackendContribution({
|
|
91
|
+
storage,
|
|
92
|
+
availablePackages: [
|
|
93
|
+
{
|
|
94
|
+
packageId: "web",
|
|
95
|
+
version: "0.0.1",
|
|
96
|
+
installByDefault: true,
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
packageId: "provider-ollama-cloud",
|
|
100
|
+
version: "0.0.1",
|
|
101
|
+
installByDefault: true,
|
|
102
|
+
},
|
|
103
|
+
{ packageId: "settings", version: "0.0.1" },
|
|
104
|
+
],
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
const first = await settings.readConfiguration({
|
|
108
|
+
schemaVersion: 1,
|
|
109
|
+
userId: "user-1",
|
|
110
|
+
});
|
|
111
|
+
expect(first).toMatchObject({
|
|
112
|
+
revision: 1,
|
|
113
|
+
packages: [
|
|
114
|
+
{
|
|
115
|
+
packageId: "web",
|
|
116
|
+
version: "0.0.1",
|
|
117
|
+
state: "installed",
|
|
118
|
+
provenance: "first-party",
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
packageId: "provider-ollama-cloud",
|
|
122
|
+
version: "0.0.1",
|
|
123
|
+
state: "installed",
|
|
124
|
+
provenance: "first-party",
|
|
125
|
+
},
|
|
126
|
+
],
|
|
127
|
+
});
|
|
128
|
+
expect(
|
|
129
|
+
await settings.readConfiguration({ schemaVersion: 1, userId: "user-1" }),
|
|
130
|
+
).toEqual(first);
|
|
131
|
+
|
|
132
|
+
await settings.executeConfiguration({
|
|
133
|
+
schemaVersion: 1,
|
|
134
|
+
userId: "user-1",
|
|
135
|
+
command: {
|
|
136
|
+
schemaVersion: 1,
|
|
137
|
+
type: "user/uninstall-package",
|
|
138
|
+
commandId: "uninstall-web",
|
|
139
|
+
expectedRevision: 1,
|
|
140
|
+
packageId: "web",
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
const afterUninstall = await settings.readConfiguration({
|
|
144
|
+
schemaVersion: 1,
|
|
145
|
+
userId: "user-1",
|
|
146
|
+
});
|
|
147
|
+
expect(afterUninstall.revision).toBe(2);
|
|
148
|
+
expect(afterUninstall.packages.map((pkg) => pkg.packageId)).toEqual([
|
|
149
|
+
"provider-ollama-cloud",
|
|
150
|
+
]);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test("owns durable User configuration independently of providers", async () => {
|
|
154
|
+
const storage = new MemoryStorage();
|
|
155
|
+
const settings = contribution(storage);
|
|
156
|
+
|
|
157
|
+
expect(
|
|
158
|
+
await settings.readConfiguration({ schemaVersion: 1, userId: "user-1" }),
|
|
159
|
+
).toEqual({
|
|
160
|
+
schemaVersion: 1,
|
|
161
|
+
revision: 0,
|
|
162
|
+
profile: { name: USER_PROFILE_PLACEHOLDER_NAME_V1 },
|
|
163
|
+
packages: [],
|
|
164
|
+
connections: [],
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
await settings.executeConfiguration({
|
|
168
|
+
schemaVersion: 1,
|
|
169
|
+
userId: "user-1",
|
|
170
|
+
command: {
|
|
171
|
+
schemaVersion: 1,
|
|
172
|
+
type: "user/update-profile",
|
|
173
|
+
commandId: "rename-user",
|
|
174
|
+
expectedRevision: 0,
|
|
175
|
+
profile: { name: "Tim" },
|
|
176
|
+
},
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
expect(
|
|
180
|
+
await storage.get<UserSettingsViewV1>("user-configuration"),
|
|
181
|
+
).toMatchObject({ revision: 1, profile: { name: "Tim" } });
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test("admits only Packages declared by the immutable application", async () => {
|
|
185
|
+
const settings = contribution();
|
|
186
|
+
const install = {
|
|
187
|
+
schemaVersion: 1 as const,
|
|
188
|
+
userId: "user-1",
|
|
189
|
+
command: {
|
|
190
|
+
schemaVersion: 1 as const,
|
|
191
|
+
type: "user/install-package" as const,
|
|
192
|
+
commandId: "install-flock",
|
|
193
|
+
expectedRevision: 0,
|
|
194
|
+
packageId: "flock",
|
|
195
|
+
version: "0.0.1",
|
|
196
|
+
},
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
await expect(settings.executeConfiguration(install)).resolves.toMatchObject(
|
|
200
|
+
{
|
|
201
|
+
status: "applied",
|
|
202
|
+
revision: 1,
|
|
203
|
+
},
|
|
204
|
+
);
|
|
205
|
+
await expect(settings.executeConfiguration(install)).resolves.toMatchObject(
|
|
206
|
+
{
|
|
207
|
+
status: "applied",
|
|
208
|
+
revision: 1,
|
|
209
|
+
},
|
|
210
|
+
);
|
|
211
|
+
expect(await settings.isPackageInstalled("user-1", "flock")).toBe(true);
|
|
212
|
+
|
|
213
|
+
await expect(
|
|
214
|
+
settings.executeConfiguration({
|
|
215
|
+
...install,
|
|
216
|
+
command: {
|
|
217
|
+
...install.command,
|
|
218
|
+
commandId: "install-composio",
|
|
219
|
+
expectedRevision: 1,
|
|
220
|
+
packageId: "composio",
|
|
221
|
+
},
|
|
222
|
+
}),
|
|
223
|
+
).rejects.toThrow("Package is not available in this application");
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
test("coordinates Connection dependencies in provider-neutral User state", async () => {
|
|
227
|
+
const settings = contribution();
|
|
228
|
+
await settings.executeConfiguration({
|
|
229
|
+
schemaVersion: 1,
|
|
230
|
+
userId: "user-1",
|
|
231
|
+
command: {
|
|
232
|
+
schemaVersion: 1,
|
|
233
|
+
type: "user/install-package",
|
|
234
|
+
commandId: "install-ollama",
|
|
235
|
+
expectedRevision: 0,
|
|
236
|
+
packageId: "provider-ollama-cloud",
|
|
237
|
+
version: "0.0.1",
|
|
238
|
+
},
|
|
239
|
+
});
|
|
240
|
+
await settings.createConnection("user-1", {
|
|
241
|
+
connectionId: "ollama-1",
|
|
242
|
+
packageId: "provider-ollama-cloud",
|
|
243
|
+
connectionTypeId: "ollama-cloud-account",
|
|
244
|
+
displayName: "Work",
|
|
245
|
+
state: "ready",
|
|
246
|
+
providerType: "ollama-cloud",
|
|
247
|
+
generation: "connection-generation",
|
|
248
|
+
safeMetadata: {},
|
|
249
|
+
});
|
|
250
|
+
const requirement = {
|
|
251
|
+
schemaVersion: 1 as const,
|
|
252
|
+
packageId: "provider-ollama-cloud",
|
|
253
|
+
packageVersion: "0.0.1",
|
|
254
|
+
capabilityId: "ollama-cloud-models",
|
|
255
|
+
connectionTypeIds: ["ollama-cloud-account"],
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
await expect(
|
|
259
|
+
settings.claimConnectionDependency(
|
|
260
|
+
"user-1",
|
|
261
|
+
"ollama-1",
|
|
262
|
+
"bot-1",
|
|
263
|
+
"assignment-1",
|
|
264
|
+
requirement,
|
|
265
|
+
),
|
|
266
|
+
).resolves.toBe(true);
|
|
267
|
+
await expect(
|
|
268
|
+
settings.acknowledgeConnectionDependency(
|
|
269
|
+
"user-1",
|
|
270
|
+
"ollama-1",
|
|
271
|
+
"bot-1",
|
|
272
|
+
"assignment-1",
|
|
273
|
+
),
|
|
274
|
+
).resolves.toBe(true);
|
|
275
|
+
expect(
|
|
276
|
+
(await settings.getConnection("user-1", "ollama-1"))?.safeMetadata,
|
|
277
|
+
).toMatchObject({
|
|
278
|
+
dependentAssignments: [
|
|
279
|
+
{
|
|
280
|
+
botId: "bot-1",
|
|
281
|
+
generation: "assignment-1",
|
|
282
|
+
packageId: "provider-ollama-cloud",
|
|
283
|
+
capabilityId: "ollama-cloud-models",
|
|
284
|
+
status: "acknowledged",
|
|
285
|
+
},
|
|
286
|
+
],
|
|
287
|
+
});
|
|
288
|
+
await expect(
|
|
289
|
+
settings.compensateConnectionDependency(
|
|
290
|
+
"user-1",
|
|
291
|
+
"ollama-1",
|
|
292
|
+
"bot-1",
|
|
293
|
+
"assignment-1",
|
|
294
|
+
),
|
|
295
|
+
).resolves.toBe(false);
|
|
296
|
+
|
|
297
|
+
await settings.createConnection("user-1", {
|
|
298
|
+
connectionId: "ollama-2",
|
|
299
|
+
packageId: "provider-ollama-cloud",
|
|
300
|
+
connectionTypeId: "ollama-cloud-account",
|
|
301
|
+
displayName: "Personal",
|
|
302
|
+
state: "ready",
|
|
303
|
+
providerType: "ollama-cloud",
|
|
304
|
+
generation: "connection-generation-2",
|
|
305
|
+
safeMetadata: {},
|
|
306
|
+
});
|
|
307
|
+
await settings.claimConnectionDependency(
|
|
308
|
+
"user-1",
|
|
309
|
+
"ollama-2",
|
|
310
|
+
"bot-1",
|
|
311
|
+
"assignment-2",
|
|
312
|
+
requirement,
|
|
313
|
+
);
|
|
314
|
+
await settings.acknowledgeConnectionDependency(
|
|
315
|
+
"user-1",
|
|
316
|
+
"ollama-2",
|
|
317
|
+
"bot-1",
|
|
318
|
+
"assignment-2",
|
|
319
|
+
);
|
|
320
|
+
|
|
321
|
+
expect(
|
|
322
|
+
(await settings.getConnection("user-1", "ollama-1"))?.safeMetadata,
|
|
323
|
+
).toMatchObject({ dependentAssignments: [] });
|
|
324
|
+
expect(
|
|
325
|
+
(await settings.getConnection("user-1", "ollama-2"))?.safeMetadata,
|
|
326
|
+
).toMatchObject({
|
|
327
|
+
dependentAssignments: [
|
|
328
|
+
{
|
|
329
|
+
botId: "bot-1",
|
|
330
|
+
generation: "assignment-2",
|
|
331
|
+
packageId: "provider-ollama-cloud",
|
|
332
|
+
capabilityId: "ollama-cloud-models",
|
|
333
|
+
status: "acknowledged",
|
|
334
|
+
},
|
|
335
|
+
],
|
|
336
|
+
});
|
|
337
|
+
await expect(
|
|
338
|
+
settings.releaseConnectionDependency(
|
|
339
|
+
"user-1",
|
|
340
|
+
"ollama-2",
|
|
341
|
+
"bot-1",
|
|
342
|
+
"assignment-2",
|
|
343
|
+
),
|
|
344
|
+
).resolves.toBe(true);
|
|
345
|
+
await expect(
|
|
346
|
+
settings.releaseConnectionDependency(
|
|
347
|
+
"user-1",
|
|
348
|
+
"ollama-2",
|
|
349
|
+
"bot-1",
|
|
350
|
+
"assignment-2",
|
|
351
|
+
),
|
|
352
|
+
).resolves.toBe(true);
|
|
353
|
+
expect(
|
|
354
|
+
(await settings.getConnection("user-1", "ollama-2"))?.safeMetadata,
|
|
355
|
+
).toMatchObject({ dependentAssignments: [] });
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
test("rejects acknowledgement from a superseded dependency claim", async () => {
|
|
359
|
+
const settings = contribution();
|
|
360
|
+
await settings.executeConfiguration({
|
|
361
|
+
schemaVersion: 1,
|
|
362
|
+
userId: "user-1",
|
|
363
|
+
command: {
|
|
364
|
+
schemaVersion: 1,
|
|
365
|
+
type: "user/install-package",
|
|
366
|
+
commandId: "install-fenced-package",
|
|
367
|
+
expectedRevision: 0,
|
|
368
|
+
packageId: "provider-ollama-cloud",
|
|
369
|
+
version: "0.0.1",
|
|
370
|
+
},
|
|
371
|
+
});
|
|
372
|
+
for (const connectionId of ["ollama-old", "ollama-new"]) {
|
|
373
|
+
await settings.createConnection("user-1", {
|
|
374
|
+
connectionId,
|
|
375
|
+
packageId: "provider-ollama-cloud",
|
|
376
|
+
connectionTypeId: "ollama-cloud-account",
|
|
377
|
+
displayName: connectionId,
|
|
378
|
+
state: "ready",
|
|
379
|
+
providerType: "ollama-cloud",
|
|
380
|
+
generation: `${connectionId}-generation`,
|
|
381
|
+
safeMetadata: {},
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
const requirement = {
|
|
385
|
+
schemaVersion: 1 as const,
|
|
386
|
+
packageId: "provider-ollama-cloud",
|
|
387
|
+
packageVersion: "0.0.1",
|
|
388
|
+
capabilityId: "ollama-cloud-models",
|
|
389
|
+
connectionTypeIds: ["ollama-cloud-account"],
|
|
390
|
+
};
|
|
391
|
+
await settings.claimConnectionDependency(
|
|
392
|
+
"user-1",
|
|
393
|
+
"ollama-old",
|
|
394
|
+
"bot-1",
|
|
395
|
+
"assignment-old",
|
|
396
|
+
requirement,
|
|
397
|
+
);
|
|
398
|
+
await settings.claimConnectionDependency(
|
|
399
|
+
"user-1",
|
|
400
|
+
"ollama-new",
|
|
401
|
+
"bot-1",
|
|
402
|
+
"assignment-new",
|
|
403
|
+
requirement,
|
|
404
|
+
);
|
|
405
|
+
await settings.claimConnectionDependency(
|
|
406
|
+
"user-1",
|
|
407
|
+
"ollama-old",
|
|
408
|
+
"bot-1",
|
|
409
|
+
"assignment-old",
|
|
410
|
+
requirement,
|
|
411
|
+
);
|
|
412
|
+
|
|
413
|
+
await expect(
|
|
414
|
+
settings.acknowledgeConnectionDependency(
|
|
415
|
+
"user-1",
|
|
416
|
+
"ollama-old",
|
|
417
|
+
"bot-1",
|
|
418
|
+
"assignment-old",
|
|
419
|
+
),
|
|
420
|
+
).resolves.toBe(false);
|
|
421
|
+
expect(
|
|
422
|
+
(await settings.getConnection("user-1", "ollama-new"))?.safeMetadata,
|
|
423
|
+
).toMatchObject({
|
|
424
|
+
dependentAssignments: [
|
|
425
|
+
{ generation: "assignment-new", status: "pending" },
|
|
426
|
+
],
|
|
427
|
+
});
|
|
428
|
+
await expect(
|
|
429
|
+
settings.compensateConnectionDependency(
|
|
430
|
+
"user-1",
|
|
431
|
+
"ollama-old",
|
|
432
|
+
"bot-1",
|
|
433
|
+
"assignment-old",
|
|
434
|
+
),
|
|
435
|
+
).resolves.toBe(true);
|
|
436
|
+
expect(
|
|
437
|
+
(await settings.getConnection("user-1", "ollama-old"))?.safeMetadata,
|
|
438
|
+
).toMatchObject({ dependentAssignments: [] });
|
|
439
|
+
|
|
440
|
+
await expect(
|
|
441
|
+
settings.acknowledgeConnectionDependency(
|
|
442
|
+
"user-1",
|
|
443
|
+
"ollama-new",
|
|
444
|
+
"bot-1",
|
|
445
|
+
"assignment-new",
|
|
446
|
+
),
|
|
447
|
+
).resolves.toBe(true);
|
|
448
|
+
expect(
|
|
449
|
+
(await settings.getConnection("user-1", "ollama-old"))?.safeMetadata,
|
|
450
|
+
).toMatchObject({ dependentAssignments: [] });
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
test("compacts revoked Connections and bounds active Connections", async () => {
|
|
454
|
+
const settings = contribution();
|
|
455
|
+
await settings.read("user-1");
|
|
456
|
+
const connection = (connectionId: string, state: "ready" | "revoked") =>
|
|
457
|
+
({
|
|
458
|
+
connectionId,
|
|
459
|
+
packageId: "provider-ollama-cloud",
|
|
460
|
+
connectionTypeId: "ollama-cloud-account",
|
|
461
|
+
displayName: connectionId,
|
|
462
|
+
state,
|
|
463
|
+
providerType: "ollama-cloud",
|
|
464
|
+
generation: `generation-${connectionId}`,
|
|
465
|
+
safeMetadata: {},
|
|
466
|
+
}) as const;
|
|
467
|
+
await settings.createConnection(
|
|
468
|
+
"user-1",
|
|
469
|
+
connection("revoked-1", "revoked"),
|
|
470
|
+
);
|
|
471
|
+
await settings.createConnection("user-1", connection("ready-0", "ready"));
|
|
472
|
+
expect((await settings.read("user-1")).connections).toHaveLength(1);
|
|
473
|
+
|
|
474
|
+
for (let index = 1; index < 100; index += 1) {
|
|
475
|
+
await settings.createConnection(
|
|
476
|
+
"user-1",
|
|
477
|
+
connection(`ready-${index}`, "ready"),
|
|
478
|
+
);
|
|
479
|
+
}
|
|
480
|
+
await expect(
|
|
481
|
+
settings.createConnection(
|
|
482
|
+
"user-1",
|
|
483
|
+
connection("ready-over-limit", "ready"),
|
|
484
|
+
),
|
|
485
|
+
).rejects.toThrow("User Connection limit reached");
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
test("adjudicates Connection command authority without naming a provider", async () => {
|
|
489
|
+
const settings = contribution();
|
|
490
|
+
await settings.read("user-1");
|
|
491
|
+
await settings.createConnection("user-1", {
|
|
492
|
+
connectionId: "connection-1",
|
|
493
|
+
packageId: "provider-ollama-cloud",
|
|
494
|
+
connectionTypeId: "ollama-cloud-account",
|
|
495
|
+
displayName: "Work",
|
|
496
|
+
state: "ready",
|
|
497
|
+
providerType: "ollama-cloud",
|
|
498
|
+
generation: "generation-1",
|
|
499
|
+
safeMetadata: {},
|
|
500
|
+
});
|
|
501
|
+
const owner = (packageId: string, retained: readonly string[]) => ({
|
|
502
|
+
packageId,
|
|
503
|
+
lookupConnectionCommand: (_accountId: string, commandId: string) =>
|
|
504
|
+
Promise.resolve(retained.includes(commandId) ? {} : undefined),
|
|
505
|
+
});
|
|
506
|
+
const release = settings.registerConnectionCommandOwner(
|
|
507
|
+
owner("provider-ollama-cloud", ["retained-1"]),
|
|
508
|
+
);
|
|
509
|
+
|
|
510
|
+
await expect(
|
|
511
|
+
settings.resolveConnectionCommandOwner("user-1", {
|
|
512
|
+
schemaVersion: 1,
|
|
513
|
+
type: "connection/create-api-key",
|
|
514
|
+
commandId: "create-1",
|
|
515
|
+
packageId: "provider-other",
|
|
516
|
+
connectionTypeId: "other-account",
|
|
517
|
+
label: "Other",
|
|
518
|
+
apiKey: "secret",
|
|
519
|
+
}),
|
|
520
|
+
).resolves.toBe("provider-other");
|
|
521
|
+
await expect(
|
|
522
|
+
settings.resolveConnectionCommandOwner("user-1", {
|
|
523
|
+
schemaVersion: 1,
|
|
524
|
+
type: "connection/refresh-models",
|
|
525
|
+
commandId: "refresh-1",
|
|
526
|
+
connectionId: "connection-1",
|
|
527
|
+
}),
|
|
528
|
+
).resolves.toBe("provider-ollama-cloud");
|
|
529
|
+
await expect(
|
|
530
|
+
settings.resolveConnectionCommandOwner("user-1", {
|
|
531
|
+
schemaVersion: 1,
|
|
532
|
+
type: "connection/disconnect",
|
|
533
|
+
commandId: "retained-1",
|
|
534
|
+
connectionId: "connection-compacted",
|
|
535
|
+
revokeUpstream: false,
|
|
536
|
+
}),
|
|
537
|
+
).resolves.toBe("provider-ollama-cloud");
|
|
538
|
+
await expect(
|
|
539
|
+
settings.resolveConnectionCommandOwner("user-1", {
|
|
540
|
+
schemaVersion: 1,
|
|
541
|
+
type: "connection/disconnect",
|
|
542
|
+
commandId: "unknown-1",
|
|
543
|
+
connectionId: "connection-compacted",
|
|
544
|
+
revokeUpstream: false,
|
|
545
|
+
}),
|
|
546
|
+
).rejects.toThrow("Connection is unavailable");
|
|
547
|
+
|
|
548
|
+
const releaseSecond = settings.registerConnectionCommandOwner(
|
|
549
|
+
owner("provider-other", ["retained-1"]),
|
|
550
|
+
);
|
|
551
|
+
expect(() =>
|
|
552
|
+
settings.registerConnectionCommandOwner(
|
|
553
|
+
owner("provider-other", ["retained-1"]),
|
|
554
|
+
),
|
|
555
|
+
).toThrow('Connection Package "provider-other" is already registered');
|
|
556
|
+
await expect(
|
|
557
|
+
settings.resolveConnectionCommandOwner("user-1", {
|
|
558
|
+
schemaVersion: 1,
|
|
559
|
+
type: "connection/disconnect",
|
|
560
|
+
commandId: "retained-1",
|
|
561
|
+
connectionId: "connection-compacted",
|
|
562
|
+
revokeUpstream: false,
|
|
563
|
+
}),
|
|
564
|
+
).rejects.toThrow("Connection command authority is ambiguous");
|
|
565
|
+
|
|
566
|
+
releaseSecond();
|
|
567
|
+
release();
|
|
568
|
+
await expect(
|
|
569
|
+
settings.resolveConnectionCommandOwner("user-1", {
|
|
570
|
+
schemaVersion: 1,
|
|
571
|
+
type: "connection/disconnect",
|
|
572
|
+
commandId: "retained-1",
|
|
573
|
+
connectionId: "connection-compacted",
|
|
574
|
+
revokeUpstream: false,
|
|
575
|
+
}),
|
|
576
|
+
).rejects.toThrow("Connection is unavailable");
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
test("binds one durable state object to one User authority", async () => {
|
|
580
|
+
const settings = contribution();
|
|
581
|
+
await settings.readConfiguration({ schemaVersion: 1, userId: "user-1" });
|
|
582
|
+
|
|
583
|
+
await expect(
|
|
584
|
+
settings.readConfiguration({ schemaVersion: 1, userId: "user-2" }),
|
|
585
|
+
).rejects.toThrow("User authority does not match durable identity");
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
test("rejects corrupt durable settings and receipts at the storage seam", async () => {
|
|
589
|
+
const corruptSettings = new MemoryStorage();
|
|
590
|
+
await corruptSettings.put("user-configuration", { schemaVersion: 1 });
|
|
591
|
+
await expect(
|
|
592
|
+
contribution(corruptSettings).readConfiguration({
|
|
593
|
+
schemaVersion: 1,
|
|
594
|
+
userId: "user-1",
|
|
595
|
+
}),
|
|
596
|
+
).rejects.toThrow();
|
|
597
|
+
|
|
598
|
+
const corruptReceipt = new MemoryStorage();
|
|
599
|
+
await corruptReceipt.put("configuration-receipt:rename-user", {
|
|
600
|
+
commandFingerprint: "invalid",
|
|
601
|
+
receipt: { status: "applied" },
|
|
602
|
+
});
|
|
603
|
+
await expect(
|
|
604
|
+
contribution(corruptReceipt).executeConfiguration({
|
|
605
|
+
schemaVersion: 1,
|
|
606
|
+
userId: "user-1",
|
|
607
|
+
command: {
|
|
608
|
+
schemaVersion: 1,
|
|
609
|
+
type: "user/update-profile",
|
|
610
|
+
commandId: "rename-user",
|
|
611
|
+
expectedRevision: 0,
|
|
612
|
+
profile: { name: "Tim" },
|
|
613
|
+
},
|
|
614
|
+
}),
|
|
615
|
+
).rejects.toThrow();
|
|
616
|
+
});
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
const CATALOG_HASH = "c".repeat(64);
|
|
620
|
+
const CATALOG_MANIFEST_HASH = "d".repeat(64);
|
|
621
|
+
|
|
622
|
+
function catalogEntry(overrides: Partial<CatalogEntryV1> = {}): CatalogEntryV1 {
|
|
623
|
+
return {
|
|
624
|
+
schemaVersion: 1,
|
|
625
|
+
catalogId: "clock",
|
|
626
|
+
packageId: "clock",
|
|
627
|
+
displayName: "Clock",
|
|
628
|
+
description: "Tells the time.",
|
|
629
|
+
version: "0.0.1",
|
|
630
|
+
kind: "package",
|
|
631
|
+
manifestHash: CATALOG_MANIFEST_HASH,
|
|
632
|
+
servers: [],
|
|
633
|
+
setupFields: [],
|
|
634
|
+
skills: [],
|
|
635
|
+
...overrides,
|
|
636
|
+
};
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
/**
|
|
640
|
+
* A Catalog whose pointer the test can move, so an install can be aimed at a
|
|
641
|
+
* generation the User is no longer pinned to.
|
|
642
|
+
*/
|
|
643
|
+
class FakeCatalog implements UserPackageCatalogHost {
|
|
644
|
+
generation = "gen-one";
|
|
645
|
+
indexHash = CATALOG_HASH;
|
|
646
|
+
entries: CatalogEntryV1[] = [catalogEntry()];
|
|
647
|
+
reads = 0;
|
|
648
|
+
|
|
649
|
+
readCurrentIndex(): Promise<{
|
|
650
|
+
pin: { generation: string; indexHash: string };
|
|
651
|
+
index: CatalogIndexV1;
|
|
652
|
+
}> {
|
|
653
|
+
this.reads += 1;
|
|
654
|
+
return Promise.resolve({
|
|
655
|
+
pin: { generation: this.generation, indexHash: this.indexHash },
|
|
656
|
+
index: {
|
|
657
|
+
schemaVersion: 1,
|
|
658
|
+
generation: this.generation,
|
|
659
|
+
entries: this.entries.map((entry) => ({
|
|
660
|
+
catalogId: entry.catalogId,
|
|
661
|
+
packageId: entry.packageId,
|
|
662
|
+
displayName: entry.displayName,
|
|
663
|
+
description: entry.description,
|
|
664
|
+
version: entry.version,
|
|
665
|
+
manifestHash: entry.manifestHash,
|
|
666
|
+
kind: entry.kind,
|
|
667
|
+
})),
|
|
668
|
+
},
|
|
669
|
+
});
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
readEntry(
|
|
673
|
+
generation: string,
|
|
674
|
+
catalogId: string,
|
|
675
|
+
): Promise<CatalogEntryV1 | undefined> {
|
|
676
|
+
if (generation !== this.generation) return Promise.resolve(undefined);
|
|
677
|
+
return Promise.resolve(
|
|
678
|
+
this.entries.find((entry) => entry.catalogId === catalogId),
|
|
679
|
+
);
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
function catalogContribution(
|
|
684
|
+
storage = new MemoryStorage(),
|
|
685
|
+
catalog = new FakeCatalog(),
|
|
686
|
+
) {
|
|
687
|
+
return {
|
|
688
|
+
storage,
|
|
689
|
+
catalog,
|
|
690
|
+
settings: createUserSettingsBackendContribution({
|
|
691
|
+
storage,
|
|
692
|
+
availablePackages: [{ packageId: "settings", version: "0.0.1" }],
|
|
693
|
+
catalog,
|
|
694
|
+
}),
|
|
695
|
+
};
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
describe("remote Package Catalog", () => {
|
|
699
|
+
test("pins a generation on the first read and holds it across reads", async () => {
|
|
700
|
+
const { catalog, settings, storage } = catalogContribution();
|
|
701
|
+
|
|
702
|
+
const first = await settings.readConfiguration({
|
|
703
|
+
schemaVersion: 1,
|
|
704
|
+
userId: "user-1",
|
|
705
|
+
});
|
|
706
|
+
expect(first.catalogGeneration).toBe("gen-one");
|
|
707
|
+
expect(first.catalogIndexHash).toBe(CATALOG_HASH);
|
|
708
|
+
// Pinning is not a settings change: a client's `expectedRevision` survives.
|
|
709
|
+
expect(first.revision).toBe(0);
|
|
710
|
+
expect(
|
|
711
|
+
(await storage.get<UserSettingsViewV1>("user-configuration")) ?? "absent",
|
|
712
|
+
).toBe("absent");
|
|
713
|
+
|
|
714
|
+
catalog.generation = "gen-two";
|
|
715
|
+
const second = await settings.readConfiguration({
|
|
716
|
+
schemaVersion: 1,
|
|
717
|
+
userId: "user-1",
|
|
718
|
+
});
|
|
719
|
+
expect(second.catalogGeneration).toBe("gen-one");
|
|
720
|
+
expect(catalog.reads).toBe(1);
|
|
721
|
+
});
|
|
722
|
+
|
|
723
|
+
test("admits a Catalog install against the pinned generation", async () => {
|
|
724
|
+
const { settings, storage } = catalogContribution();
|
|
725
|
+
await settings.readConfiguration({ schemaVersion: 1, userId: "user-1" });
|
|
726
|
+
|
|
727
|
+
await expect(
|
|
728
|
+
settings.executeConfiguration({
|
|
729
|
+
schemaVersion: 1,
|
|
730
|
+
userId: "user-1",
|
|
731
|
+
command: {
|
|
732
|
+
schemaVersion: 1,
|
|
733
|
+
type: "user/install-package",
|
|
734
|
+
commandId: "install-clock",
|
|
735
|
+
expectedRevision: 0,
|
|
736
|
+
packageId: "clock",
|
|
737
|
+
version: "0.0.1",
|
|
738
|
+
catalogId: "clock",
|
|
739
|
+
catalogGeneration: "gen-one",
|
|
740
|
+
values: { region: "au" },
|
|
741
|
+
},
|
|
742
|
+
}),
|
|
743
|
+
).resolves.toMatchObject({ status: "applied", revision: 1 });
|
|
744
|
+
|
|
745
|
+
expect(
|
|
746
|
+
await storage.get<UserSettingsViewV1>("user-configuration"),
|
|
747
|
+
).toMatchObject({
|
|
748
|
+
packages: [
|
|
749
|
+
{
|
|
750
|
+
packageId: "clock",
|
|
751
|
+
version: "0.0.1",
|
|
752
|
+
state: "installed",
|
|
753
|
+
catalogId: "clock",
|
|
754
|
+
catalogGeneration: "gen-one",
|
|
755
|
+
provenance: "catalog",
|
|
756
|
+
values: { region: "au" },
|
|
757
|
+
},
|
|
758
|
+
],
|
|
759
|
+
});
|
|
760
|
+
});
|
|
761
|
+
|
|
762
|
+
test("refuses an install off the pinned generation", async () => {
|
|
763
|
+
const { catalog, settings } = catalogContribution();
|
|
764
|
+
await settings.readConfiguration({ schemaVersion: 1, userId: "user-1" });
|
|
765
|
+
catalog.generation = "gen-two";
|
|
766
|
+
|
|
767
|
+
await expect(
|
|
768
|
+
settings.executeConfiguration({
|
|
769
|
+
schemaVersion: 1,
|
|
770
|
+
userId: "user-1",
|
|
771
|
+
command: {
|
|
772
|
+
schemaVersion: 1,
|
|
773
|
+
type: "user/install-package",
|
|
774
|
+
commandId: "install-stale",
|
|
775
|
+
expectedRevision: 0,
|
|
776
|
+
packageId: "clock",
|
|
777
|
+
version: "0.0.1",
|
|
778
|
+
catalogId: "clock",
|
|
779
|
+
catalogGeneration: "gen-two",
|
|
780
|
+
},
|
|
781
|
+
}),
|
|
782
|
+
).rejects.toThrow(
|
|
783
|
+
'Package Catalog generation "gen-two" is not the pinned generation "gen-one"',
|
|
784
|
+
);
|
|
785
|
+
});
|
|
786
|
+
|
|
787
|
+
test("refuses a Catalog entry the pinned generation does not offer", async () => {
|
|
788
|
+
const { settings } = catalogContribution();
|
|
789
|
+
await settings.readConfiguration({ schemaVersion: 1, userId: "user-1" });
|
|
790
|
+
|
|
791
|
+
await expect(
|
|
792
|
+
settings.executeConfiguration({
|
|
793
|
+
schemaVersion: 1,
|
|
794
|
+
userId: "user-1",
|
|
795
|
+
command: {
|
|
796
|
+
schemaVersion: 1,
|
|
797
|
+
type: "user/install-package",
|
|
798
|
+
commandId: "install-absent",
|
|
799
|
+
expectedRevision: 0,
|
|
800
|
+
packageId: "weather",
|
|
801
|
+
version: "0.0.1",
|
|
802
|
+
catalogId: "weather",
|
|
803
|
+
catalogGeneration: "gen-one",
|
|
804
|
+
},
|
|
805
|
+
}),
|
|
806
|
+
).rejects.toThrow('Catalog entry "weather" is not in pinned');
|
|
807
|
+
});
|
|
808
|
+
|
|
809
|
+
test("refuses a Catalog install whose version does not match the entry", async () => {
|
|
810
|
+
const { settings } = catalogContribution();
|
|
811
|
+
await settings.readConfiguration({ schemaVersion: 1, userId: "user-1" });
|
|
812
|
+
|
|
813
|
+
await expect(
|
|
814
|
+
settings.executeConfiguration({
|
|
815
|
+
schemaVersion: 1,
|
|
816
|
+
userId: "user-1",
|
|
817
|
+
command: {
|
|
818
|
+
schemaVersion: 1,
|
|
819
|
+
type: "user/install-package",
|
|
820
|
+
commandId: "install-mismatch",
|
|
821
|
+
expectedRevision: 0,
|
|
822
|
+
packageId: "clock",
|
|
823
|
+
version: "9.9.9",
|
|
824
|
+
catalogId: "clock",
|
|
825
|
+
catalogGeneration: "gen-one",
|
|
826
|
+
},
|
|
827
|
+
}),
|
|
828
|
+
).rejects.toThrow("does not offer Package");
|
|
829
|
+
});
|
|
830
|
+
|
|
831
|
+
test("leaves the compiled-in install path untouched", async () => {
|
|
832
|
+
const { settings } = catalogContribution();
|
|
833
|
+
await settings.readConfiguration({ schemaVersion: 1, userId: "user-1" });
|
|
834
|
+
|
|
835
|
+
await expect(
|
|
836
|
+
settings.executeConfiguration({
|
|
837
|
+
schemaVersion: 1,
|
|
838
|
+
userId: "user-1",
|
|
839
|
+
command: {
|
|
840
|
+
schemaVersion: 1,
|
|
841
|
+
type: "user/install-package",
|
|
842
|
+
commandId: "install-settings",
|
|
843
|
+
expectedRevision: 0,
|
|
844
|
+
packageId: "settings",
|
|
845
|
+
version: "0.0.1",
|
|
846
|
+
},
|
|
847
|
+
}),
|
|
848
|
+
).resolves.toMatchObject({ status: "applied" });
|
|
849
|
+
|
|
850
|
+
// A compiled-in Package that the Catalog does not carry still installs,
|
|
851
|
+
// and a Package neither source offers still does not.
|
|
852
|
+
await expect(
|
|
853
|
+
settings.executeConfiguration({
|
|
854
|
+
schemaVersion: 1,
|
|
855
|
+
userId: "user-1",
|
|
856
|
+
command: {
|
|
857
|
+
schemaVersion: 1,
|
|
858
|
+
type: "user/install-package",
|
|
859
|
+
commandId: "install-unknown",
|
|
860
|
+
expectedRevision: 1,
|
|
861
|
+
packageId: "unknown",
|
|
862
|
+
version: "0.0.1",
|
|
863
|
+
},
|
|
864
|
+
}),
|
|
865
|
+
).rejects.toThrow("Package is not available in this application");
|
|
866
|
+
});
|
|
867
|
+
|
|
868
|
+
test("a Catalog install without a Catalog configured is refused", async () => {
|
|
869
|
+
const settings = contribution();
|
|
870
|
+
|
|
871
|
+
await expect(
|
|
872
|
+
settings.executeConfiguration({
|
|
873
|
+
schemaVersion: 1,
|
|
874
|
+
userId: "user-1",
|
|
875
|
+
command: {
|
|
876
|
+
schemaVersion: 1,
|
|
877
|
+
type: "user/install-package",
|
|
878
|
+
commandId: "install-clock",
|
|
879
|
+
expectedRevision: 0,
|
|
880
|
+
packageId: "clock",
|
|
881
|
+
version: "0.0.1",
|
|
882
|
+
catalogId: "clock",
|
|
883
|
+
catalogGeneration: "gen-one",
|
|
884
|
+
},
|
|
885
|
+
}),
|
|
886
|
+
).rejects.toThrow("Package Catalog is not available");
|
|
887
|
+
});
|
|
888
|
+
});
|
|
889
|
+
|
|
890
|
+
describe("uninstall", () => {
|
|
891
|
+
test("removes the installation and leaves Connections alone", async () => {
|
|
892
|
+
const storage = new MemoryStorage();
|
|
893
|
+
const settings = contribution(storage);
|
|
894
|
+
await settings.executeConfiguration({
|
|
895
|
+
schemaVersion: 1,
|
|
896
|
+
userId: "user-1",
|
|
897
|
+
command: {
|
|
898
|
+
schemaVersion: 1,
|
|
899
|
+
type: "user/install-package",
|
|
900
|
+
commandId: "install-provider",
|
|
901
|
+
expectedRevision: 0,
|
|
902
|
+
packageId: "provider-ollama-cloud",
|
|
903
|
+
version: "0.0.1",
|
|
904
|
+
},
|
|
905
|
+
});
|
|
906
|
+
await settings.createConnection("user-1", {
|
|
907
|
+
connectionId: "connection-1",
|
|
908
|
+
packageId: "provider-ollama-cloud",
|
|
909
|
+
connectionTypeId: "ollama-cloud-account",
|
|
910
|
+
displayName: "Work",
|
|
911
|
+
state: "ready",
|
|
912
|
+
safeMetadata: {},
|
|
913
|
+
});
|
|
914
|
+
const before = await settings.readSnapshot();
|
|
915
|
+
|
|
916
|
+
await expect(
|
|
917
|
+
settings.executeConfiguration({
|
|
918
|
+
schemaVersion: 1,
|
|
919
|
+
userId: "user-1",
|
|
920
|
+
command: {
|
|
921
|
+
schemaVersion: 1,
|
|
922
|
+
type: "user/uninstall-package",
|
|
923
|
+
commandId: "uninstall-provider",
|
|
924
|
+
expectedRevision: before.revision,
|
|
925
|
+
packageId: "provider-ollama-cloud",
|
|
926
|
+
},
|
|
927
|
+
}),
|
|
928
|
+
).resolves.toMatchObject({ status: "applied" });
|
|
929
|
+
|
|
930
|
+
const after = await settings.readSnapshot();
|
|
931
|
+
expect(after.packages).toEqual([]);
|
|
932
|
+
// A Connection is the User's own account and outlives any Package.
|
|
933
|
+
expect(after.connections.map((item) => item.connectionId)).toEqual([
|
|
934
|
+
"connection-1",
|
|
935
|
+
]);
|
|
936
|
+
});
|
|
937
|
+
|
|
938
|
+
test("dependent Assignments become unavailable tombstones, not deletions", async () => {
|
|
939
|
+
const storage = new MemoryStorage();
|
|
940
|
+
const settings = contribution(storage);
|
|
941
|
+
await settings.executeConfiguration({
|
|
942
|
+
schemaVersion: 1,
|
|
943
|
+
userId: "user-1",
|
|
944
|
+
command: {
|
|
945
|
+
schemaVersion: 1,
|
|
946
|
+
type: "user/install-package",
|
|
947
|
+
commandId: "install-provider",
|
|
948
|
+
expectedRevision: 0,
|
|
949
|
+
packageId: "provider-ollama-cloud",
|
|
950
|
+
version: "0.0.1",
|
|
951
|
+
},
|
|
952
|
+
});
|
|
953
|
+
await settings.createConnection("user-1", {
|
|
954
|
+
connectionId: "connection-1",
|
|
955
|
+
packageId: "provider-ollama-cloud",
|
|
956
|
+
connectionTypeId: "ollama-cloud-account",
|
|
957
|
+
displayName: "Work",
|
|
958
|
+
state: "ready",
|
|
959
|
+
safeMetadata: {},
|
|
960
|
+
});
|
|
961
|
+
const assignment = {
|
|
962
|
+
assignmentId: "assignment-1",
|
|
963
|
+
packageId: "provider-ollama-cloud",
|
|
964
|
+
capabilityId: "ollama-cloud-models",
|
|
965
|
+
connectionId: "connection-1",
|
|
966
|
+
};
|
|
967
|
+
const packages = [
|
|
968
|
+
{
|
|
969
|
+
packageId: "provider-ollama-cloud",
|
|
970
|
+
version: "0.0.1",
|
|
971
|
+
capabilities: [
|
|
972
|
+
{
|
|
973
|
+
id: "ollama-cloud-models",
|
|
974
|
+
kind: "model" as const,
|
|
975
|
+
connectionTypes: ["ollama-cloud-account"],
|
|
976
|
+
},
|
|
977
|
+
],
|
|
978
|
+
connectionTypes: [
|
|
979
|
+
{
|
|
980
|
+
id: "ollama-cloud-account",
|
|
981
|
+
capabilities: ["ollama-cloud-models"],
|
|
982
|
+
},
|
|
983
|
+
],
|
|
984
|
+
},
|
|
985
|
+
];
|
|
986
|
+
|
|
987
|
+
const installed = await settings.readSnapshot();
|
|
988
|
+
expect(
|
|
989
|
+
capabilityAssignmentFailureV1({ assignment, user: installed, packages }),
|
|
990
|
+
).toBeUndefined();
|
|
991
|
+
|
|
992
|
+
await settings.executeConfiguration({
|
|
993
|
+
schemaVersion: 1,
|
|
994
|
+
userId: "user-1",
|
|
995
|
+
command: {
|
|
996
|
+
schemaVersion: 1,
|
|
997
|
+
type: "user/uninstall-package",
|
|
998
|
+
commandId: "uninstall-provider",
|
|
999
|
+
expectedRevision: installed.revision,
|
|
1000
|
+
packageId: "provider-ollama-cloud",
|
|
1001
|
+
},
|
|
1002
|
+
});
|
|
1003
|
+
|
|
1004
|
+
const uninstalled = await settings.readSnapshot();
|
|
1005
|
+
expect(
|
|
1006
|
+
capabilityAssignmentFailureV1({
|
|
1007
|
+
assignment,
|
|
1008
|
+
user: uninstalled,
|
|
1009
|
+
packages,
|
|
1010
|
+
}),
|
|
1011
|
+
).toContain("is not installed and enabled");
|
|
1012
|
+
expect(
|
|
1013
|
+
resolveBotExecutionPlanV1({
|
|
1014
|
+
bot: {
|
|
1015
|
+
...initializeBotSettingsV1("bot-1"),
|
|
1016
|
+
assignments: [{ ...assignment, state: "enabled" }],
|
|
1017
|
+
},
|
|
1018
|
+
user: uninstalled,
|
|
1019
|
+
packages,
|
|
1020
|
+
}).assignments,
|
|
1021
|
+
).toEqual([{ ...assignment, state: "unavailable" }]);
|
|
1022
|
+
});
|
|
1023
|
+
|
|
1024
|
+
test("refuses to uninstall a Package that is not installed", async () => {
|
|
1025
|
+
const settings = contribution();
|
|
1026
|
+
|
|
1027
|
+
await expect(
|
|
1028
|
+
settings.executeConfiguration({
|
|
1029
|
+
schemaVersion: 1,
|
|
1030
|
+
userId: "user-1",
|
|
1031
|
+
command: {
|
|
1032
|
+
schemaVersion: 1,
|
|
1033
|
+
type: "user/uninstall-package",
|
|
1034
|
+
commandId: "uninstall-absent",
|
|
1035
|
+
expectedRevision: 0,
|
|
1036
|
+
packageId: "flock",
|
|
1037
|
+
},
|
|
1038
|
+
}),
|
|
1039
|
+
).rejects.toThrow('Package "flock" is not installed');
|
|
1040
|
+
});
|
|
1041
|
+
});
|
|
1042
|
+
|
|
1043
|
+
describe("Package-level setting values", () => {
|
|
1044
|
+
test("applies a partial update and leaves the settings it does not name", async () => {
|
|
1045
|
+
const storage = new MemoryStorage();
|
|
1046
|
+
const settings = contribution(storage);
|
|
1047
|
+
await installOllama(settings, "user-1");
|
|
1048
|
+
|
|
1049
|
+
await settings.executeConfiguration({
|
|
1050
|
+
schemaVersion: 1,
|
|
1051
|
+
userId: "user-1",
|
|
1052
|
+
command: {
|
|
1053
|
+
schemaVersion: 1,
|
|
1054
|
+
type: "user/set-package-settings",
|
|
1055
|
+
commandId: "set-both",
|
|
1056
|
+
expectedRevision: 1,
|
|
1057
|
+
packageId: "provider-ollama-cloud",
|
|
1058
|
+
values: { "web-search-max-results": 2, label: "work" },
|
|
1059
|
+
},
|
|
1060
|
+
});
|
|
1061
|
+
await settings.executeConfiguration({
|
|
1062
|
+
schemaVersion: 1,
|
|
1063
|
+
userId: "user-1",
|
|
1064
|
+
command: {
|
|
1065
|
+
schemaVersion: 1,
|
|
1066
|
+
type: "user/set-package-settings",
|
|
1067
|
+
commandId: "set-one",
|
|
1068
|
+
expectedRevision: 2,
|
|
1069
|
+
packageId: "provider-ollama-cloud",
|
|
1070
|
+
values: { "web-search-max-results": 5 },
|
|
1071
|
+
},
|
|
1072
|
+
});
|
|
1073
|
+
|
|
1074
|
+
const view = await settings.read("user-1");
|
|
1075
|
+
// The projection the client reads *is* the store: one bag, one shape.
|
|
1076
|
+
expect(view.packages[0]).toMatchObject({
|
|
1077
|
+
packageId: "provider-ollama-cloud",
|
|
1078
|
+
values: { "web-search-max-results": 5, label: "work" },
|
|
1079
|
+
});
|
|
1080
|
+
});
|
|
1081
|
+
|
|
1082
|
+
test("a replayed command id returns its receipt without applying twice", async () => {
|
|
1083
|
+
const settings = contribution();
|
|
1084
|
+
await installOllama(settings, "user-1");
|
|
1085
|
+
const command = {
|
|
1086
|
+
schemaVersion: 1 as const,
|
|
1087
|
+
type: "user/set-package-settings" as const,
|
|
1088
|
+
commandId: "set-once",
|
|
1089
|
+
expectedRevision: 1,
|
|
1090
|
+
packageId: "provider-ollama-cloud",
|
|
1091
|
+
values: { "web-search-max-results": 4 },
|
|
1092
|
+
};
|
|
1093
|
+
|
|
1094
|
+
const first = await settings.executeConfiguration({
|
|
1095
|
+
schemaVersion: 1,
|
|
1096
|
+
userId: "user-1",
|
|
1097
|
+
command,
|
|
1098
|
+
});
|
|
1099
|
+
const replay = await settings.executeConfiguration({
|
|
1100
|
+
schemaVersion: 1,
|
|
1101
|
+
userId: "user-1",
|
|
1102
|
+
command,
|
|
1103
|
+
});
|
|
1104
|
+
|
|
1105
|
+
expect(replay).toEqual(first);
|
|
1106
|
+
expect((await settings.read("user-1")).revision).toBe(2);
|
|
1107
|
+
});
|
|
1108
|
+
|
|
1109
|
+
test("a reused command id carrying different values is refused", async () => {
|
|
1110
|
+
const settings = contribution();
|
|
1111
|
+
await installOllama(settings, "user-1");
|
|
1112
|
+
await settings.executeConfiguration({
|
|
1113
|
+
schemaVersion: 1,
|
|
1114
|
+
userId: "user-1",
|
|
1115
|
+
command: {
|
|
1116
|
+
schemaVersion: 1,
|
|
1117
|
+
type: "user/set-package-settings",
|
|
1118
|
+
commandId: "set-once",
|
|
1119
|
+
expectedRevision: 1,
|
|
1120
|
+
packageId: "provider-ollama-cloud",
|
|
1121
|
+
values: { "web-search-max-results": 4 },
|
|
1122
|
+
},
|
|
1123
|
+
});
|
|
1124
|
+
|
|
1125
|
+
await expect(
|
|
1126
|
+
settings.executeConfiguration({
|
|
1127
|
+
schemaVersion: 1,
|
|
1128
|
+
userId: "user-1",
|
|
1129
|
+
command: {
|
|
1130
|
+
schemaVersion: 1,
|
|
1131
|
+
type: "user/set-package-settings",
|
|
1132
|
+
commandId: "set-once",
|
|
1133
|
+
expectedRevision: 1,
|
|
1134
|
+
packageId: "provider-ollama-cloud",
|
|
1135
|
+
values: { "web-search-max-results": 6 },
|
|
1136
|
+
},
|
|
1137
|
+
}),
|
|
1138
|
+
).rejects.toThrow(/reused for a different command/);
|
|
1139
|
+
});
|
|
1140
|
+
|
|
1141
|
+
test("refuses a value the Package's declared schema does not allow", async () => {
|
|
1142
|
+
const settings = contribution();
|
|
1143
|
+
await installOllama(settings, "user-1");
|
|
1144
|
+
|
|
1145
|
+
await expect(
|
|
1146
|
+
settings.executeConfiguration({
|
|
1147
|
+
schemaVersion: 1,
|
|
1148
|
+
userId: "user-1",
|
|
1149
|
+
command: {
|
|
1150
|
+
schemaVersion: 1,
|
|
1151
|
+
type: "user/set-package-settings",
|
|
1152
|
+
commandId: "set-out-of-range",
|
|
1153
|
+
expectedRevision: 1,
|
|
1154
|
+
packageId: "provider-ollama-cloud",
|
|
1155
|
+
values: { "web-search-max-results": 99 },
|
|
1156
|
+
},
|
|
1157
|
+
}),
|
|
1158
|
+
).rejects.toThrow(/is above 10/);
|
|
1159
|
+
expect((await settings.read("user-1")).packages[0]?.values).toBeUndefined();
|
|
1160
|
+
});
|
|
1161
|
+
|
|
1162
|
+
test("refuses a setting the Package does not declare", async () => {
|
|
1163
|
+
const settings = contribution();
|
|
1164
|
+
await installOllama(settings, "user-1");
|
|
1165
|
+
|
|
1166
|
+
await expect(
|
|
1167
|
+
settings.executeConfiguration({
|
|
1168
|
+
schemaVersion: 1,
|
|
1169
|
+
userId: "user-1",
|
|
1170
|
+
command: {
|
|
1171
|
+
schemaVersion: 1,
|
|
1172
|
+
type: "user/set-package-settings",
|
|
1173
|
+
commandId: "set-unknown",
|
|
1174
|
+
expectedRevision: 1,
|
|
1175
|
+
packageId: "provider-ollama-cloud",
|
|
1176
|
+
values: { "not-a-setting": "x" },
|
|
1177
|
+
},
|
|
1178
|
+
}),
|
|
1179
|
+
).rejects.toThrow(/not declared by this Package/);
|
|
1180
|
+
});
|
|
1181
|
+
|
|
1182
|
+
test("refuses settings for a Package that is not installed", async () => {
|
|
1183
|
+
const settings = contribution();
|
|
1184
|
+
|
|
1185
|
+
await expect(
|
|
1186
|
+
settings.executeConfiguration({
|
|
1187
|
+
schemaVersion: 1,
|
|
1188
|
+
userId: "user-1",
|
|
1189
|
+
command: {
|
|
1190
|
+
schemaVersion: 1,
|
|
1191
|
+
type: "user/set-package-settings",
|
|
1192
|
+
commandId: "set-uninstalled",
|
|
1193
|
+
expectedRevision: 0,
|
|
1194
|
+
packageId: "provider-ollama-cloud",
|
|
1195
|
+
values: { "web-search-max-results": 2 },
|
|
1196
|
+
},
|
|
1197
|
+
}),
|
|
1198
|
+
).rejects.toThrow(/is not installed/);
|
|
1199
|
+
});
|
|
1200
|
+
|
|
1201
|
+
test("uninstalling drops the values, and reinstalling starts clean", async () => {
|
|
1202
|
+
const settings = contribution();
|
|
1203
|
+
await installOllama(settings, "user-1");
|
|
1204
|
+
await settings.executeConfiguration({
|
|
1205
|
+
schemaVersion: 1,
|
|
1206
|
+
userId: "user-1",
|
|
1207
|
+
command: {
|
|
1208
|
+
schemaVersion: 1,
|
|
1209
|
+
type: "user/set-package-settings",
|
|
1210
|
+
commandId: "set-before-uninstall",
|
|
1211
|
+
expectedRevision: 1,
|
|
1212
|
+
packageId: "provider-ollama-cloud",
|
|
1213
|
+
values: { "web-search-max-results": 2 },
|
|
1214
|
+
},
|
|
1215
|
+
});
|
|
1216
|
+
|
|
1217
|
+
await settings.executeConfiguration({
|
|
1218
|
+
schemaVersion: 1,
|
|
1219
|
+
userId: "user-1",
|
|
1220
|
+
command: {
|
|
1221
|
+
schemaVersion: 1,
|
|
1222
|
+
type: "user/uninstall-package",
|
|
1223
|
+
commandId: "uninstall",
|
|
1224
|
+
expectedRevision: 2,
|
|
1225
|
+
packageId: "provider-ollama-cloud",
|
|
1226
|
+
},
|
|
1227
|
+
});
|
|
1228
|
+
expect((await settings.read("user-1")).packages).toEqual([]);
|
|
1229
|
+
|
|
1230
|
+
await settings.executeConfiguration({
|
|
1231
|
+
schemaVersion: 1,
|
|
1232
|
+
userId: "user-1",
|
|
1233
|
+
command: {
|
|
1234
|
+
schemaVersion: 1,
|
|
1235
|
+
type: "user/install-package",
|
|
1236
|
+
commandId: "reinstall",
|
|
1237
|
+
expectedRevision: 3,
|
|
1238
|
+
packageId: "provider-ollama-cloud",
|
|
1239
|
+
version: "0.0.1",
|
|
1240
|
+
},
|
|
1241
|
+
});
|
|
1242
|
+
expect((await settings.read("user-1")).packages[0]?.values).toBeUndefined();
|
|
1243
|
+
});
|
|
1244
|
+
|
|
1245
|
+
test("a reinstall over a configured Package carries its values forward", async () => {
|
|
1246
|
+
const settings = contribution();
|
|
1247
|
+
await installOllama(settings, "user-1");
|
|
1248
|
+
await settings.executeConfiguration({
|
|
1249
|
+
schemaVersion: 1,
|
|
1250
|
+
userId: "user-1",
|
|
1251
|
+
command: {
|
|
1252
|
+
schemaVersion: 1,
|
|
1253
|
+
type: "user/set-package-settings",
|
|
1254
|
+
commandId: "set-kept",
|
|
1255
|
+
expectedRevision: 1,
|
|
1256
|
+
packageId: "provider-ollama-cloud",
|
|
1257
|
+
values: { label: "work" },
|
|
1258
|
+
},
|
|
1259
|
+
});
|
|
1260
|
+
await settings.executeConfiguration({
|
|
1261
|
+
schemaVersion: 1,
|
|
1262
|
+
userId: "user-1",
|
|
1263
|
+
command: {
|
|
1264
|
+
schemaVersion: 1,
|
|
1265
|
+
type: "user/install-package",
|
|
1266
|
+
commandId: "reinstall-same-version",
|
|
1267
|
+
expectedRevision: 2,
|
|
1268
|
+
packageId: "provider-ollama-cloud",
|
|
1269
|
+
version: "0.0.1",
|
|
1270
|
+
},
|
|
1271
|
+
});
|
|
1272
|
+
|
|
1273
|
+
expect((await settings.read("user-1")).packages[0]?.values).toEqual({
|
|
1274
|
+
label: "work",
|
|
1275
|
+
});
|
|
1276
|
+
});
|
|
1277
|
+
});
|