@frockbot/plugin-provider-frock-ai 0.0.0 → 0.3.13
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 +34 -0
- package/package.json +34 -6
- package/src/catalog.test.ts +82 -0
- package/src/catalog.ts +112 -0
- package/src/index.ts +2 -0
- package/src/manifest.ts +3 -0
- package/src/runtime.test.ts +526 -0
- package/src/runtime.ts +122 -0
- package/src/user.test.ts +378 -0
- package/src/user.ts +401 -0
- package/tsconfig.json +15 -0
- package/README.md +0 -3
package/src/user.test.ts
ADDED
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import type {
|
|
3
|
+
ConnectionView,
|
|
4
|
+
OperationReceiptV1,
|
|
5
|
+
UserConfigurationCommandV1,
|
|
6
|
+
UserSettingsViewV1,
|
|
7
|
+
} from "@frockbot/configuration-core";
|
|
8
|
+
import {
|
|
9
|
+
FROCK_AI_CONNECTION_ID,
|
|
10
|
+
FROCK_AI_DEFAULT_MODEL,
|
|
11
|
+
FROCK_AI_PACKAGE_ID,
|
|
12
|
+
} from "./catalog.js";
|
|
13
|
+
import { createFrockAiUserBackendContribution } from "./user.js";
|
|
14
|
+
|
|
15
|
+
interface TestTransaction {
|
|
16
|
+
get<T>(key: string): Promise<T | undefined>;
|
|
17
|
+
put<T>(key: string, value: T): Promise<void>;
|
|
18
|
+
put(entries: Record<string, unknown>): Promise<void>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
class MemoryStorage implements TestTransaction {
|
|
22
|
+
readonly values = new Map<string, unknown>();
|
|
23
|
+
|
|
24
|
+
get<T>(key: string): Promise<T | undefined> {
|
|
25
|
+
return Promise.resolve(this.values.get(key) as T | undefined);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
put<T>(key: string, value: T): Promise<void>;
|
|
29
|
+
put(entries: Record<string, unknown>): Promise<void>;
|
|
30
|
+
put<T>(
|
|
31
|
+
keyOrEntries: string | Record<string, unknown>,
|
|
32
|
+
value?: T,
|
|
33
|
+
): Promise<void> {
|
|
34
|
+
if (typeof keyOrEntries === "string") {
|
|
35
|
+
this.values.set(keyOrEntries, value);
|
|
36
|
+
} else {
|
|
37
|
+
for (const [key, entry] of Object.entries(keyOrEntries)) {
|
|
38
|
+
this.values.set(key, entry);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return Promise.resolve();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async transaction<T>(
|
|
45
|
+
callback: (storage: TestTransaction) => Promise<T>,
|
|
46
|
+
): Promise<T> {
|
|
47
|
+
const before = new Map(this.values);
|
|
48
|
+
try {
|
|
49
|
+
return await callback(this);
|
|
50
|
+
} catch (error) {
|
|
51
|
+
this.values.clear();
|
|
52
|
+
for (const [key, value] of before) this.values.set(key, value);
|
|
53
|
+
throw error;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
class FakeSettings {
|
|
59
|
+
readonly commands: UserConfigurationCommandV1[] = [];
|
|
60
|
+
private bootstrap:
|
|
61
|
+
| { readonly packageId: string; bootstrap(userId: string): Promise<void> }
|
|
62
|
+
| undefined;
|
|
63
|
+
private state: UserSettingsViewV1 = {
|
|
64
|
+
schemaVersion: 1,
|
|
65
|
+
revision: 0,
|
|
66
|
+
profile: { name: "User" },
|
|
67
|
+
packages: [],
|
|
68
|
+
connections: [],
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
registerConfigurationReadBootstrap(bootstrap: {
|
|
72
|
+
readonly packageId: string;
|
|
73
|
+
bootstrap(userId: string): Promise<void>;
|
|
74
|
+
}): () => void {
|
|
75
|
+
this.bootstrap = bootstrap;
|
|
76
|
+
return () => {
|
|
77
|
+
if (this.bootstrap === bootstrap) this.bootstrap = undefined;
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async readConfiguration(userId: string): Promise<UserSettingsViewV1> {
|
|
82
|
+
await this.bootstrap?.bootstrap(userId);
|
|
83
|
+
return structuredClone(this.state);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
read(): Promise<UserSettingsViewV1> {
|
|
87
|
+
return Promise.resolve(structuredClone(this.state));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
executeConfigurationCommand(
|
|
91
|
+
_userId: string,
|
|
92
|
+
command: UserConfigurationCommandV1,
|
|
93
|
+
): Promise<OperationReceiptV1> {
|
|
94
|
+
if (command.expectedRevision !== this.state.revision) {
|
|
95
|
+
throw new Error("settings revision changed");
|
|
96
|
+
}
|
|
97
|
+
this.commands.push(structuredClone(command));
|
|
98
|
+
if (command.type === "user/install-package") {
|
|
99
|
+
const existing = this.state.packages.find(
|
|
100
|
+
(installation) => installation.packageId === command.packageId,
|
|
101
|
+
);
|
|
102
|
+
if (!existing) {
|
|
103
|
+
this.state.packages.push({
|
|
104
|
+
packageId: command.packageId,
|
|
105
|
+
version: command.version,
|
|
106
|
+
state: command.enabled === false ? "disabled" : "installed",
|
|
107
|
+
});
|
|
108
|
+
} else {
|
|
109
|
+
existing.version = command.version;
|
|
110
|
+
existing.state = command.enabled === false ? "disabled" : "installed";
|
|
111
|
+
delete existing.failure;
|
|
112
|
+
}
|
|
113
|
+
this.state.revision += 1;
|
|
114
|
+
return Promise.resolve({
|
|
115
|
+
schemaVersion: 1,
|
|
116
|
+
commandId: command.commandId,
|
|
117
|
+
revision: this.state.revision,
|
|
118
|
+
status: "applied",
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
if (command.type === "user/set-platform-model") {
|
|
122
|
+
this.state.platformModel = structuredClone(command.model);
|
|
123
|
+
this.state.revision += 1;
|
|
124
|
+
return Promise.resolve({
|
|
125
|
+
schemaVersion: 1,
|
|
126
|
+
commandId: command.commandId,
|
|
127
|
+
revision: this.state.revision,
|
|
128
|
+
status: "applied",
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
throw new Error(`unexpected command ${command.type}`);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
createConnection(
|
|
135
|
+
_userId: string,
|
|
136
|
+
connection: ConnectionView,
|
|
137
|
+
): Promise<ConnectionView> {
|
|
138
|
+
const existing = this.state.connections.find(
|
|
139
|
+
(candidate) => candidate.connectionId === connection.connectionId,
|
|
140
|
+
);
|
|
141
|
+
if (existing) return Promise.resolve(structuredClone(existing));
|
|
142
|
+
this.state.connections.push(structuredClone(connection));
|
|
143
|
+
this.state.revision += 1;
|
|
144
|
+
return Promise.resolve(structuredClone(connection));
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
replaceConnection(
|
|
148
|
+
_userId: string,
|
|
149
|
+
connectionId: string,
|
|
150
|
+
expectedGeneration: string | undefined,
|
|
151
|
+
connection: ConnectionView,
|
|
152
|
+
): Promise<ConnectionView> {
|
|
153
|
+
const index = this.state.connections.findIndex(
|
|
154
|
+
(candidate) => candidate.connectionId === connectionId,
|
|
155
|
+
);
|
|
156
|
+
if (
|
|
157
|
+
index < 0 ||
|
|
158
|
+
this.state.connections[index]?.generation !== expectedGeneration
|
|
159
|
+
) {
|
|
160
|
+
throw new Error("Connection generation changed");
|
|
161
|
+
}
|
|
162
|
+
this.state.connections[index] = structuredClone(connection);
|
|
163
|
+
this.state.revision += 1;
|
|
164
|
+
return Promise.resolve(structuredClone(connection));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
getConnection(
|
|
168
|
+
_userId: string,
|
|
169
|
+
connectionId: string,
|
|
170
|
+
): Promise<ConnectionView | undefined> {
|
|
171
|
+
const connection = this.state.connections.find(
|
|
172
|
+
(candidate) => candidate.connectionId === connectionId,
|
|
173
|
+
);
|
|
174
|
+
return Promise.resolve(
|
|
175
|
+
connection ? structuredClone(connection) : undefined,
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
setPlatformModel(connectionId: string, providerModelId: string): void {
|
|
180
|
+
this.state.platformModel = { connectionId, providerModelId };
|
|
181
|
+
this.state.revision += 1;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
removeFrockPackageAndPlatformModel(): void {
|
|
185
|
+
this.state.packages = this.state.packages.filter(
|
|
186
|
+
(candidate) => candidate.packageId !== FROCK_AI_PACKAGE_ID,
|
|
187
|
+
);
|
|
188
|
+
delete this.state.platformModel;
|
|
189
|
+
this.state.revision += 1;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
seedResolvablePlatformModel(): void {
|
|
193
|
+
this.state.packages.push({
|
|
194
|
+
packageId: "provider-another",
|
|
195
|
+
version: "0.0.1",
|
|
196
|
+
state: "installed",
|
|
197
|
+
});
|
|
198
|
+
this.state.connections.push({
|
|
199
|
+
connectionId: "another-connection",
|
|
200
|
+
packageId: "provider-another",
|
|
201
|
+
connectionTypeId: "another-account",
|
|
202
|
+
displayName: "Another provider",
|
|
203
|
+
state: "ready",
|
|
204
|
+
providerType: "another",
|
|
205
|
+
modelCatalog: {
|
|
206
|
+
schemaVersion: 1,
|
|
207
|
+
generation: "another-catalog-1",
|
|
208
|
+
state: "fresh",
|
|
209
|
+
models: [
|
|
210
|
+
{
|
|
211
|
+
providerModelId: "another-model",
|
|
212
|
+
displayName: "Another model",
|
|
213
|
+
capabilities: { tools: true, vision: false, reasoning: false },
|
|
214
|
+
source: "discovered",
|
|
215
|
+
},
|
|
216
|
+
],
|
|
217
|
+
},
|
|
218
|
+
safeMetadata: {},
|
|
219
|
+
});
|
|
220
|
+
this.state.platformModel = {
|
|
221
|
+
connectionId: "another-connection",
|
|
222
|
+
providerModelId: "another-model",
|
|
223
|
+
};
|
|
224
|
+
this.state.revision += 1;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
seedFrockAiPackageValues(): void {
|
|
228
|
+
this.state.packages.push({
|
|
229
|
+
packageId: FROCK_AI_PACKAGE_ID,
|
|
230
|
+
version: "0.0.1",
|
|
231
|
+
state: "installed",
|
|
232
|
+
values: { userChoice: "keep-me" },
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function fixture() {
|
|
238
|
+
const storage = new MemoryStorage();
|
|
239
|
+
const settings = new FakeSettings();
|
|
240
|
+
const frockAi = createFrockAiUserBackendContribution({ storage, settings });
|
|
241
|
+
settings.registerConfigurationReadBootstrap(frockAi);
|
|
242
|
+
return { storage, settings };
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
describe("Frock AI User Contribution", () => {
|
|
246
|
+
test("ambiently installs, connects, and sets Auto as the platform model exactly once", async () => {
|
|
247
|
+
const { settings } = fixture();
|
|
248
|
+
const first = await settings.readConfiguration("user-1");
|
|
249
|
+
|
|
250
|
+
expect(first).toMatchObject({
|
|
251
|
+
revision: 3,
|
|
252
|
+
packages: [{ packageId: FROCK_AI_PACKAGE_ID, state: "installed" }],
|
|
253
|
+
connections: [
|
|
254
|
+
{
|
|
255
|
+
connectionId: FROCK_AI_CONNECTION_ID,
|
|
256
|
+
state: "ready",
|
|
257
|
+
providerType: "flock-ai",
|
|
258
|
+
authorization: {
|
|
259
|
+
kind: "ambient-native",
|
|
260
|
+
credential: { configured: true, writable: false },
|
|
261
|
+
},
|
|
262
|
+
modelCatalog: {
|
|
263
|
+
state: "fresh",
|
|
264
|
+
models: [
|
|
265
|
+
{
|
|
266
|
+
providerModelId: FROCK_AI_DEFAULT_MODEL,
|
|
267
|
+
displayName: "Auto (recommended)",
|
|
268
|
+
},
|
|
269
|
+
{
|
|
270
|
+
providerModelId: "@frock/deepseek-ai/deepseek-v4-flash-0731",
|
|
271
|
+
displayName: "DeepSeek V4 Flash",
|
|
272
|
+
},
|
|
273
|
+
],
|
|
274
|
+
},
|
|
275
|
+
},
|
|
276
|
+
],
|
|
277
|
+
platformModel: {
|
|
278
|
+
connectionId: FROCK_AI_CONNECTION_ID,
|
|
279
|
+
providerModelId: FROCK_AI_DEFAULT_MODEL,
|
|
280
|
+
},
|
|
281
|
+
});
|
|
282
|
+
expect(
|
|
283
|
+
settings.commands.filter(
|
|
284
|
+
(command) => command.type === "user/set-platform-model",
|
|
285
|
+
),
|
|
286
|
+
).toHaveLength(1);
|
|
287
|
+
|
|
288
|
+
const second = await settings.readConfiguration("user-1");
|
|
289
|
+
expect(second).toEqual(first);
|
|
290
|
+
expect(
|
|
291
|
+
settings.commands.filter(
|
|
292
|
+
(command) => command.type === "user/set-platform-model",
|
|
293
|
+
),
|
|
294
|
+
).toHaveLength(1);
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
test("repairs a platform model that no longer resolves", async () => {
|
|
298
|
+
const { settings } = fixture();
|
|
299
|
+
await settings.readConfiguration("user-1");
|
|
300
|
+
settings.setPlatformModel("another-connection", "another-model");
|
|
301
|
+
|
|
302
|
+
const second = await settings.readConfiguration("user-1");
|
|
303
|
+
expect(second.platformModel).toEqual({
|
|
304
|
+
connectionId: FROCK_AI_CONNECTION_ID,
|
|
305
|
+
providerModelId: FROCK_AI_DEFAULT_MODEL,
|
|
306
|
+
});
|
|
307
|
+
expect(
|
|
308
|
+
settings.commands.filter(
|
|
309
|
+
(command) => command.type === "user/set-platform-model",
|
|
310
|
+
),
|
|
311
|
+
).toHaveLength(2);
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
test("preserves a platform model that still resolves", async () => {
|
|
315
|
+
const { settings } = fixture();
|
|
316
|
+
await settings.readConfiguration("user-1");
|
|
317
|
+
settings.seedResolvablePlatformModel();
|
|
318
|
+
|
|
319
|
+
const second = await settings.readConfiguration("user-1");
|
|
320
|
+
expect(second.platformModel).toEqual({
|
|
321
|
+
connectionId: "another-connection",
|
|
322
|
+
providerModelId: "another-model",
|
|
323
|
+
});
|
|
324
|
+
expect(
|
|
325
|
+
settings.commands.filter(
|
|
326
|
+
(command) => command.type === "user/set-platform-model",
|
|
327
|
+
),
|
|
328
|
+
).toHaveLength(1);
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
test("self-heals after the marker when the installation and model are removed", async () => {
|
|
332
|
+
const { settings } = fixture();
|
|
333
|
+
await settings.readConfiguration("user-1");
|
|
334
|
+
settings.removeFrockPackageAndPlatformModel();
|
|
335
|
+
|
|
336
|
+
const repaired = await settings.readConfiguration("user-1");
|
|
337
|
+
expect(repaired.packages).toContainEqual(
|
|
338
|
+
expect.objectContaining({
|
|
339
|
+
packageId: FROCK_AI_PACKAGE_ID,
|
|
340
|
+
state: "installed",
|
|
341
|
+
}),
|
|
342
|
+
);
|
|
343
|
+
expect(repaired.platformModel).toEqual({
|
|
344
|
+
connectionId: FROCK_AI_CONNECTION_ID,
|
|
345
|
+
providerModelId: FROCK_AI_DEFAULT_MODEL,
|
|
346
|
+
});
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
test("does not touch Package settings already written by the User", async () => {
|
|
350
|
+
const { settings } = fixture();
|
|
351
|
+
settings.seedFrockAiPackageValues();
|
|
352
|
+
|
|
353
|
+
const view = await settings.readConfiguration("user-1");
|
|
354
|
+
expect(view.packages).toContainEqual(
|
|
355
|
+
expect.objectContaining({
|
|
356
|
+
packageId: FROCK_AI_PACKAGE_ID,
|
|
357
|
+
values: { userChoice: "keep-me" },
|
|
358
|
+
}),
|
|
359
|
+
);
|
|
360
|
+
expect(
|
|
361
|
+
settings.commands.filter(
|
|
362
|
+
(command) => command.type === "user/install-package",
|
|
363
|
+
),
|
|
364
|
+
).toHaveLength(0);
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
test("fails closed on a corrupt Package-owned bootstrap marker", async () => {
|
|
368
|
+
const { storage, settings } = fixture();
|
|
369
|
+
storage.values.set("provider-flock-ai:bootstrap-v1", {
|
|
370
|
+
schemaVersion: 1,
|
|
371
|
+
userId: "user-1",
|
|
372
|
+
unexpected: true,
|
|
373
|
+
});
|
|
374
|
+
await expect(settings.readConfiguration("user-1")).rejects.toThrow(
|
|
375
|
+
"Stored Frock AI bootstrap marker is invalid",
|
|
376
|
+
);
|
|
377
|
+
});
|
|
378
|
+
});
|