@frockbot/plugin-flock 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/README.md +8 -1
- package/assets/amber-glasses.webp +0 -0
- package/assets/background-bright-orange.webp +0 -0
- package/assets/background-canary-yellow.webp +0 -0
- package/assets/background-electric-blue.webp +0 -0
- package/assets/background-hot-pink.webp +0 -0
- package/assets/background-lime-green.webp +0 -0
- package/assets/background-vivid-purple.webp +0 -0
- package/assets/blue-sailor-cap.webp +0 -0
- package/assets/bronze-aviator-goggles.webp +0 -0
- package/assets/burgundy-beret.webp +0 -0
- package/assets/canary-party-hat.webp +0 -0
- package/assets/canary-rain-cap.webp +0 -0
- package/assets/canonical.webp +0 -0
- package/assets/chef-toque.webp +0 -0
- package/assets/cobalt-round-glasses.webp +0 -0
- package/assets/coral-bucket-hat.webp +0 -0
- package/assets/emerald-necktie.webp +0 -0
- package/assets/forest-ranger-fedora.webp +0 -0
- package/assets/gold-royal-crown.webp +0 -0
- package/assets/gold-sun-medallion.webp +0 -0
- package/assets/graduation-cap.webp +0 -0
- package/assets/green-square-glasses.webp +0 -0
- package/assets/layers.css +159 -0
- package/assets/leather-bell.webp +0 -0
- package/assets/lilac-flower-crown.webp +0 -0
- package/assets/manifest.json +769 -0
- package/assets/matte-black-top-hat.webp +0 -0
- package/assets/navy-wizard-hat.webp +0 -0
- package/assets/orange-hard-hat.webp +0 -0
- package/assets/plum-glasses.webp +0 -0
- package/assets/purple-scarf.webp +0 -0
- package/assets/purple-witch-hat.webp +0 -0
- package/assets/rainbow-propeller-cap.webp +0 -0
- package/assets/rainbow-visor.webp +0 -0
- package/assets/ranger-fedora-feather.webp +0 -0
- package/assets/red-polka-bowtie.webp +0 -0
- package/assets/retro-3d-glasses.webp +0 -0
- package/assets/rose-heart-sunglasses.webp +0 -0
- package/assets/royal-crown-rubies.webp +0 -0
- package/assets/sailor-cap-anchor.webp +0 -0
- package/assets/silver-space-helmet.webp +0 -0
- package/assets/straw-boater.webp +0 -0
- package/assets/teal-bowtie.webp +0 -0
- package/assets/teal-glasses.webp +0 -0
- package/assets/terracotta-beanie.webp +0 -0
- package/assets/tortoiseshell-readers.webp +0 -0
- package/assets/turquoise-bandana.webp +0 -0
- package/assets/violet-masquerade-mask.webp +0 -0
- package/assets/white-snow-goggles.webp +0 -0
- package/assets/white-tennis-visor.webp +0 -0
- package/assets/witch-hat-moon.webp +0 -0
- package/assets/yellow-star-glasses.webp +0 -0
- package/frockbot.json +38 -0
- package/package.json +47 -6
- package/src/agent.test.ts +401 -0
- package/src/agent.ts +571 -0
- package/src/assets.test.ts +31 -0
- package/src/backend.test.ts +205 -0
- package/src/backend.ts +283 -0
- package/src/bot.test.ts +190 -0
- package/src/bot.ts +255 -0
- package/src/client/BotAvatar.vue +20 -0
- package/src/client/FlockAvatar.vue +33 -0
- package/src/client/FlockAvatarEditor.vue +44 -0
- package/src/client/FlockCreateButton.vue +18 -0
- package/src/client/FlockIdentity.vue +31 -0
- package/src/client/FlockOverlay.vue +209 -0
- package/src/client/FlockSidebar.vue +254 -0
- package/src/client/SheepAvatar.vue +28 -0
- package/src/client/dialog-focus.test.ts +15 -0
- package/src/client/dialog-focus.ts +13 -0
- package/src/client/index.test.ts +502 -0
- package/src/client/index.ts +619 -0
- package/src/client/mobile-dialog-layout.test.ts +50 -0
- package/src/client/pending-create.test.ts +71 -0
- package/src/client/pending-create.ts +93 -0
- package/src/client/sidebar.test.ts +77 -0
- package/src/client/sidebar.ts +77 -0
- package/src/client/state.ts +53 -0
- package/src/client/styles.css +611 -0
- package/src/env.d.ts +9 -0
- package/src/index.ts +4 -0
- package/src/manifest.ts +3 -0
- package/src/package.test.ts +12 -0
- package/src/shared.test.ts +197 -0
- package/src/shared.ts +735 -0
- package/src/user.test.ts +455 -0
- package/src/user.ts +465 -0
- package/tsconfig.json +15 -0
- package/validate_readability.py +64 -0
- package/vite.config.ts +30 -0
package/src/user.test.ts
ADDED
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { createFlockUserBackendContribution } from "./user.js";
|
|
3
|
+
import {
|
|
4
|
+
FlockConflictError,
|
|
5
|
+
randomSheepRecipeV1,
|
|
6
|
+
type BotDirectoryViewV1,
|
|
7
|
+
} from "./shared.js";
|
|
8
|
+
|
|
9
|
+
class MemoryStorage {
|
|
10
|
+
values = new Map<string, unknown>();
|
|
11
|
+
get<T>(key: string): Promise<T | undefined> {
|
|
12
|
+
return Promise.resolve(
|
|
13
|
+
structuredClone(this.values.get(key)) as T | undefined,
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
put<T>(key: string | Record<string, unknown>, value?: T): Promise<void> {
|
|
17
|
+
if (typeof key === "string") this.values.set(key, structuredClone(value));
|
|
18
|
+
else
|
|
19
|
+
for (const [name, entry] of Object.entries(key))
|
|
20
|
+
this.values.set(name, structuredClone(entry));
|
|
21
|
+
return Promise.resolve();
|
|
22
|
+
}
|
|
23
|
+
transaction<T>(callback: (storage: MemoryStorage) => Promise<T>): Promise<T> {
|
|
24
|
+
return callback(this);
|
|
25
|
+
}
|
|
26
|
+
list<T>({ prefix }: { prefix: string }): Promise<Map<string, T>> {
|
|
27
|
+
return Promise.resolve(
|
|
28
|
+
new Map(
|
|
29
|
+
[...this.values.entries()].filter(([key]) =>
|
|
30
|
+
key.startsWith(prefix),
|
|
31
|
+
) as Array<[string, T]>,
|
|
32
|
+
),
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
delete(key: string): Promise<boolean> {
|
|
36
|
+
return Promise.resolve(this.values.delete(key));
|
|
37
|
+
}
|
|
38
|
+
alarm: number | Date | undefined;
|
|
39
|
+
setAlarm(timestamp: number | Date): Promise<void> {
|
|
40
|
+
this.alarm = timestamp;
|
|
41
|
+
return Promise.resolve();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
const settings = {
|
|
45
|
+
schemaVersion: 1 as const,
|
|
46
|
+
revision: 4,
|
|
47
|
+
profile: { name: "User" },
|
|
48
|
+
packages: [
|
|
49
|
+
{ packageId: "web", version: "0.0.1", state: "installed" as const },
|
|
50
|
+
{
|
|
51
|
+
packageId: "provider-ollama-cloud",
|
|
52
|
+
version: "0.0.1",
|
|
53
|
+
state: "installed" as const,
|
|
54
|
+
},
|
|
55
|
+
],
|
|
56
|
+
connections: [],
|
|
57
|
+
newBotModelTemplate: { connectionId: "provider", providerModelId: "model" },
|
|
58
|
+
newBotModelTemplateSource: "auto" as const,
|
|
59
|
+
};
|
|
60
|
+
function command(commandId = "create-1", expectedRevision = 0) {
|
|
61
|
+
return {
|
|
62
|
+
schemaVersion: 1 as const,
|
|
63
|
+
type: "bot/create" as const,
|
|
64
|
+
commandId,
|
|
65
|
+
expectedRevision,
|
|
66
|
+
botId: "alpha",
|
|
67
|
+
name: "Alpha",
|
|
68
|
+
sheep: randomSheepRecipeV1(() => 0),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
describe("Flock User contribution", () => {
|
|
73
|
+
test("atomically admits, snapshots defaults, and replays duplicate delivery", async () => {
|
|
74
|
+
const storage = new MemoryStorage();
|
|
75
|
+
const contribution = createFlockUserBackendContribution({
|
|
76
|
+
storage,
|
|
77
|
+
availablePackages: [
|
|
78
|
+
{
|
|
79
|
+
packageId: "web",
|
|
80
|
+
version: "0.0.1",
|
|
81
|
+
capabilities: [
|
|
82
|
+
{ id: "web-fetch", kind: "tool", connectionTypes: [] },
|
|
83
|
+
],
|
|
84
|
+
connectionTypes: [],
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
packageId: "provider-ollama-cloud",
|
|
88
|
+
version: "0.0.1",
|
|
89
|
+
capabilities: [
|
|
90
|
+
{
|
|
91
|
+
id: "ollama-cloud-models",
|
|
92
|
+
kind: "model",
|
|
93
|
+
connectionTypes: ["ollama-cloud-account"],
|
|
94
|
+
},
|
|
95
|
+
],
|
|
96
|
+
connectionTypes: [
|
|
97
|
+
{
|
|
98
|
+
id: "ollama-cloud-account",
|
|
99
|
+
capabilities: ["ollama-cloud-models"],
|
|
100
|
+
},
|
|
101
|
+
],
|
|
102
|
+
},
|
|
103
|
+
],
|
|
104
|
+
readUserSettings: () => Promise.resolve(settings),
|
|
105
|
+
claimInitialModelBinding: (_storage, input) =>
|
|
106
|
+
Promise.resolve({
|
|
107
|
+
assignmentId: input.generation,
|
|
108
|
+
packageId: "provider-ollama-cloud",
|
|
109
|
+
capabilityId: "ollama-cloud-models",
|
|
110
|
+
connectionId: input.model.connectionId,
|
|
111
|
+
state: "enabled",
|
|
112
|
+
}),
|
|
113
|
+
now: () => new Date("2026-08-29T00:00:00.000Z"),
|
|
114
|
+
commandBotLifecycle: () => Promise.reject(new Error("not used")),
|
|
115
|
+
readBotLifecycle: () => Promise.reject(new Error("not used")),
|
|
116
|
+
});
|
|
117
|
+
const first = await contribution.createBot("user-1", command());
|
|
118
|
+
const replay = await contribution.createBot("user-1", command());
|
|
119
|
+
expect(replay).toEqual(first);
|
|
120
|
+
expect(await contribution.registration("alpha")).toMatchObject({
|
|
121
|
+
schemaVersion: 1,
|
|
122
|
+
initialName: "Alpha",
|
|
123
|
+
registeredAt: "2026-08-29T00:00:00.000Z",
|
|
124
|
+
});
|
|
125
|
+
// A new Bot follows the User's default model instead of copying it, so
|
|
126
|
+
// the registration seed carries no model and no claimed binding.
|
|
127
|
+
expect(await contribution.registration("alpha")).toMatchObject({
|
|
128
|
+
initialModel: undefined,
|
|
129
|
+
initialModelBinding: undefined,
|
|
130
|
+
initialAssignments: [
|
|
131
|
+
{
|
|
132
|
+
assignmentId: "default-0-0",
|
|
133
|
+
packageId: "web",
|
|
134
|
+
capabilityId: "web-fetch",
|
|
135
|
+
state: "enabled",
|
|
136
|
+
},
|
|
137
|
+
],
|
|
138
|
+
});
|
|
139
|
+
await expect(
|
|
140
|
+
contribution.createBot("user-1", { ...command(), name: "Different" }),
|
|
141
|
+
).rejects.toThrow("command ID collision");
|
|
142
|
+
await expect(
|
|
143
|
+
contribution.createBot("user-1", {
|
|
144
|
+
...command("create-2", 0),
|
|
145
|
+
botId: "beta",
|
|
146
|
+
}),
|
|
147
|
+
).rejects.toBeInstanceOf(FlockConflictError);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test("admits a Bot while the User default model is unclaimable", async () => {
|
|
151
|
+
const storage = new MemoryStorage();
|
|
152
|
+
let claims = 0;
|
|
153
|
+
const contribution = createFlockUserBackendContribution({
|
|
154
|
+
storage,
|
|
155
|
+
readUserSettings: () => Promise.resolve(settings),
|
|
156
|
+
claimInitialModelBinding: () => {
|
|
157
|
+
claims += 1;
|
|
158
|
+
return Promise.resolve(undefined);
|
|
159
|
+
},
|
|
160
|
+
commandBotLifecycle: () => Promise.reject(new Error("not used")),
|
|
161
|
+
readBotLifecycle: () => Promise.reject(new Error("not used")),
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
await expect(
|
|
165
|
+
contribution.createBot("user-1", command()),
|
|
166
|
+
).resolves.toMatchObject({ status: "applied" });
|
|
167
|
+
expect(claims).toBe(0);
|
|
168
|
+
expect((await contribution.listBots()).bots).toMatchObject([
|
|
169
|
+
{ botId: "alpha", initialModel: undefined },
|
|
170
|
+
]);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test("rejects malformed durable directories and receipts at the storage seam", async () => {
|
|
174
|
+
const storage = new MemoryStorage();
|
|
175
|
+
const contribution = createFlockUserBackendContribution({
|
|
176
|
+
storage,
|
|
177
|
+
readUserSettings: () => Promise.resolve(settings),
|
|
178
|
+
commandBotLifecycle: () => Promise.reject(new Error("not used")),
|
|
179
|
+
readBotLifecycle: () => Promise.reject(new Error("not used")),
|
|
180
|
+
claimInitialModelBinding: () => Promise.resolve(undefined),
|
|
181
|
+
});
|
|
182
|
+
await storage.put("flock:directory:v1", {
|
|
183
|
+
schemaVersion: 1,
|
|
184
|
+
revision: 0,
|
|
185
|
+
bots: [],
|
|
186
|
+
unknown: true,
|
|
187
|
+
});
|
|
188
|
+
await expect(contribution.listBots()).rejects.toThrow(
|
|
189
|
+
"unknown or missing field",
|
|
190
|
+
);
|
|
191
|
+
storage.values.delete("flock:directory:v1");
|
|
192
|
+
await storage.put("flock:create-receipt:create-1", {
|
|
193
|
+
fingerprint: JSON.stringify(command()),
|
|
194
|
+
receipt: { schemaVersion: 1, status: "applied" },
|
|
195
|
+
});
|
|
196
|
+
await expect(contribution.createBot("user-1", command())).rejects.toThrow(
|
|
197
|
+
"unknown or missing field",
|
|
198
|
+
);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
test("coordinates archive through durable intent and reconciles a lost response", async () => {
|
|
202
|
+
const storage = new MemoryStorage();
|
|
203
|
+
let botStatus: "active" | "archived" = "active";
|
|
204
|
+
let calls = 0;
|
|
205
|
+
const contribution = createFlockUserBackendContribution({
|
|
206
|
+
storage,
|
|
207
|
+
readUserSettings: () => Promise.resolve(settings),
|
|
208
|
+
claimInitialModelBinding: (_storage, input) =>
|
|
209
|
+
Promise.resolve({
|
|
210
|
+
assignmentId: input.generation,
|
|
211
|
+
packageId: "provider-ollama-cloud",
|
|
212
|
+
capabilityId: "ollama-cloud-models",
|
|
213
|
+
connectionId: input.model.connectionId,
|
|
214
|
+
state: "enabled" as const,
|
|
215
|
+
}),
|
|
216
|
+
commandBotLifecycle: (_userId, lifecycleCommand) => {
|
|
217
|
+
calls += 1;
|
|
218
|
+
botStatus =
|
|
219
|
+
lifecycleCommand.type === "bot/archive" ? "archived" : "active";
|
|
220
|
+
return Promise.reject(new Error("response lost"));
|
|
221
|
+
},
|
|
222
|
+
readBotLifecycle: (_userId, botId) =>
|
|
223
|
+
Promise.resolve({
|
|
224
|
+
schemaVersion: 1,
|
|
225
|
+
botId,
|
|
226
|
+
status: botStatus,
|
|
227
|
+
revision: calls,
|
|
228
|
+
}),
|
|
229
|
+
});
|
|
230
|
+
await expect(
|
|
231
|
+
contribution.executeLifecycle("user-1", {
|
|
232
|
+
schemaVersion: 1,
|
|
233
|
+
type: "bot/archive",
|
|
234
|
+
commandId: "archive-missing",
|
|
235
|
+
botId: "missing",
|
|
236
|
+
}),
|
|
237
|
+
).rejects.toThrow("not registered");
|
|
238
|
+
await contribution.createBot("user-1", command());
|
|
239
|
+
const seedBefore = structuredClone(
|
|
240
|
+
storage.values.get("flock:directory:v1"),
|
|
241
|
+
);
|
|
242
|
+
const archive = {
|
|
243
|
+
schemaVersion: 1 as const,
|
|
244
|
+
type: "bot/archive" as const,
|
|
245
|
+
commandId: "archive-1",
|
|
246
|
+
botId: "alpha",
|
|
247
|
+
};
|
|
248
|
+
expect(
|
|
249
|
+
await contribution.executeLifecycle("user-1", archive),
|
|
250
|
+
).toMatchObject({
|
|
251
|
+
status: "applied",
|
|
252
|
+
lifecycle: { status: "archived" },
|
|
253
|
+
});
|
|
254
|
+
expect(
|
|
255
|
+
await contribution.executeLifecycle("user-1", archive),
|
|
256
|
+
).toMatchObject({
|
|
257
|
+
status: "applied",
|
|
258
|
+
});
|
|
259
|
+
expect(storage.values.get("flock:directory:v1")).toEqual(seedBefore);
|
|
260
|
+
await expect(
|
|
261
|
+
contribution.executeLifecycle("user-1", {
|
|
262
|
+
...archive,
|
|
263
|
+
type: "bot/restore",
|
|
264
|
+
}),
|
|
265
|
+
).rejects.toThrow("command ID collision");
|
|
266
|
+
expect((await contribution.listBotLifecycles()).lifecycles).toEqual([
|
|
267
|
+
{ schemaVersion: 1, botId: "alpha", status: "archived", revision: 1 },
|
|
268
|
+
]);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
test("resumes an uncertain lifecycle saga from its User alarm", async () => {
|
|
272
|
+
const storage = new MemoryStorage();
|
|
273
|
+
let recovered = false;
|
|
274
|
+
const contribution = createFlockUserBackendContribution({
|
|
275
|
+
storage,
|
|
276
|
+
readUserSettings: () => Promise.resolve(settings),
|
|
277
|
+
claimInitialModelBinding: (_storage, input) =>
|
|
278
|
+
Promise.resolve({
|
|
279
|
+
assignmentId: input.generation,
|
|
280
|
+
packageId: "provider-ollama-cloud",
|
|
281
|
+
capabilityId: "ollama-cloud-models",
|
|
282
|
+
connectionId: input.model.connectionId,
|
|
283
|
+
state: "enabled" as const,
|
|
284
|
+
}),
|
|
285
|
+
commandBotLifecycle: () => Promise.reject(new Error("response lost")),
|
|
286
|
+
readBotLifecycle: (_userId, botId) =>
|
|
287
|
+
recovered
|
|
288
|
+
? Promise.resolve({
|
|
289
|
+
schemaVersion: 1,
|
|
290
|
+
botId,
|
|
291
|
+
status: "archived",
|
|
292
|
+
revision: 1,
|
|
293
|
+
})
|
|
294
|
+
: Promise.reject(new Error("Bot unavailable")),
|
|
295
|
+
});
|
|
296
|
+
await contribution.createBot("user-1", command());
|
|
297
|
+
const lifecycleCommand = {
|
|
298
|
+
schemaVersion: 1 as const,
|
|
299
|
+
type: "bot/archive" as const,
|
|
300
|
+
commandId: "archive-alarm",
|
|
301
|
+
botId: "alpha",
|
|
302
|
+
};
|
|
303
|
+
expect(
|
|
304
|
+
await contribution.executeLifecycle("user-1", lifecycleCommand),
|
|
305
|
+
).toMatchObject({ status: "pending" });
|
|
306
|
+
expect(storage.alarm).toBeDefined();
|
|
307
|
+
await expect(
|
|
308
|
+
contribution.executeLifecycle("user-1", {
|
|
309
|
+
schemaVersion: 1,
|
|
310
|
+
type: "bot/restore",
|
|
311
|
+
commandId: "restore-too-soon",
|
|
312
|
+
botId: "alpha",
|
|
313
|
+
}),
|
|
314
|
+
).rejects.toThrow("operation retrying");
|
|
315
|
+
recovered = true;
|
|
316
|
+
const reconstructed = createFlockUserBackendContribution({
|
|
317
|
+
storage,
|
|
318
|
+
readUserSettings: () => Promise.resolve(settings),
|
|
319
|
+
claimInitialModelBinding: (_storage, input) =>
|
|
320
|
+
Promise.resolve({
|
|
321
|
+
assignmentId: input.generation,
|
|
322
|
+
packageId: "provider-ollama-cloud",
|
|
323
|
+
capabilityId: "ollama-cloud-models",
|
|
324
|
+
connectionId: input.model.connectionId,
|
|
325
|
+
state: "enabled" as const,
|
|
326
|
+
}),
|
|
327
|
+
commandBotLifecycle: () => Promise.reject(new Error("response lost")),
|
|
328
|
+
readBotLifecycle: (_userId, botId) =>
|
|
329
|
+
Promise.resolve({
|
|
330
|
+
schemaVersion: 1,
|
|
331
|
+
botId,
|
|
332
|
+
status: "archived",
|
|
333
|
+
revision: 1,
|
|
334
|
+
}),
|
|
335
|
+
});
|
|
336
|
+
await reconstructed.alarm();
|
|
337
|
+
expect(
|
|
338
|
+
await reconstructed.executeLifecycle("user-1", lifecycleCommand),
|
|
339
|
+
).toMatchObject({ status: "applied", lifecycle: { status: "archived" } });
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
test("reconciles uncorrelated, pending, and wrong-target Bot replies", async () => {
|
|
343
|
+
for (const variant of [
|
|
344
|
+
"wrong-command",
|
|
345
|
+
"pending",
|
|
346
|
+
"wrong-target",
|
|
347
|
+
] as const) {
|
|
348
|
+
const storage = new MemoryStorage();
|
|
349
|
+
let marker: "active" | "archived" = "active";
|
|
350
|
+
const lifecycleCommand = {
|
|
351
|
+
schemaVersion: 1 as const,
|
|
352
|
+
type: "bot/archive" as const,
|
|
353
|
+
commandId: `archive-${variant}`,
|
|
354
|
+
botId: "alpha",
|
|
355
|
+
};
|
|
356
|
+
const create = () =>
|
|
357
|
+
createFlockUserBackendContribution({
|
|
358
|
+
storage,
|
|
359
|
+
readUserSettings: () => Promise.resolve(settings),
|
|
360
|
+
claimInitialModelBinding: (_storage, input) =>
|
|
361
|
+
Promise.resolve({
|
|
362
|
+
assignmentId: input.generation,
|
|
363
|
+
packageId: "provider-ollama-cloud",
|
|
364
|
+
capabilityId: "ollama-cloud-models",
|
|
365
|
+
connectionId: input.model.connectionId,
|
|
366
|
+
state: "enabled" as const,
|
|
367
|
+
}),
|
|
368
|
+
commandBotLifecycle: () =>
|
|
369
|
+
Promise.resolve({
|
|
370
|
+
schemaVersion: 1,
|
|
371
|
+
commandId:
|
|
372
|
+
variant === "wrong-command"
|
|
373
|
+
? "another-command"
|
|
374
|
+
: lifecycleCommand.commandId,
|
|
375
|
+
botId: "alpha",
|
|
376
|
+
status: variant === "pending" ? "pending" : "applied",
|
|
377
|
+
lifecycle: {
|
|
378
|
+
schemaVersion: 1,
|
|
379
|
+
botId: "alpha",
|
|
380
|
+
status: variant === "wrong-target" ? "active" : marker,
|
|
381
|
+
revision: 0,
|
|
382
|
+
},
|
|
383
|
+
}),
|
|
384
|
+
readBotLifecycle: (_userId, botId) =>
|
|
385
|
+
Promise.resolve({
|
|
386
|
+
schemaVersion: 1,
|
|
387
|
+
botId,
|
|
388
|
+
status: marker,
|
|
389
|
+
revision: marker === "archived" ? 1 : 0,
|
|
390
|
+
}),
|
|
391
|
+
});
|
|
392
|
+
const contribution = create();
|
|
393
|
+
await contribution.createBot("user-1", command());
|
|
394
|
+
expect(
|
|
395
|
+
await contribution.executeLifecycle("user-1", lifecycleCommand),
|
|
396
|
+
).toMatchObject({ status: "pending", lifecycle: { status: "active" } });
|
|
397
|
+
marker = "archived";
|
|
398
|
+
const reconstructed = create();
|
|
399
|
+
await reconstructed.alarm();
|
|
400
|
+
expect(
|
|
401
|
+
await reconstructed.executeLifecycle("user-1", lifecycleCommand),
|
|
402
|
+
).toMatchObject({
|
|
403
|
+
commandId: lifecycleCommand.commandId,
|
|
404
|
+
status: "applied",
|
|
405
|
+
lifecycle: { status: "archived" },
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
test("durably rejects duplicate IDs and the bounded directory limit", async () => {
|
|
411
|
+
const storage = new MemoryStorage();
|
|
412
|
+
const contribution = createFlockUserBackendContribution({
|
|
413
|
+
storage,
|
|
414
|
+
readUserSettings: () => Promise.resolve(settings),
|
|
415
|
+
commandBotLifecycle: () => Promise.reject(new Error("not used")),
|
|
416
|
+
readBotLifecycle: () => Promise.reject(new Error("not used")),
|
|
417
|
+
claimInitialModelBinding: (_storage, input) =>
|
|
418
|
+
Promise.resolve({
|
|
419
|
+
assignmentId: input.generation,
|
|
420
|
+
packageId: "provider-ollama-cloud",
|
|
421
|
+
capabilityId: "ollama-cloud-models",
|
|
422
|
+
connectionId: input.model.connectionId,
|
|
423
|
+
state: "enabled",
|
|
424
|
+
}),
|
|
425
|
+
});
|
|
426
|
+
await contribution.createBot("user-1", command());
|
|
427
|
+
expect(
|
|
428
|
+
await contribution.createBot("user-1", {
|
|
429
|
+
...command("duplicate", 1),
|
|
430
|
+
botId: "alpha",
|
|
431
|
+
}),
|
|
432
|
+
).toMatchObject({ status: "rejected", revision: 1 });
|
|
433
|
+
const full: BotDirectoryViewV1 = {
|
|
434
|
+
schemaVersion: 1,
|
|
435
|
+
revision: 100,
|
|
436
|
+
bots: Array.from({ length: 100 }, (_, index) => ({
|
|
437
|
+
schemaVersion: 1,
|
|
438
|
+
botId: `bot-${index}`,
|
|
439
|
+
registeredAt: "2026-08-29T00:00:00.000Z",
|
|
440
|
+
initialName: `Bot ${index}`,
|
|
441
|
+
sheep: randomSheepRecipeV1(() => 0),
|
|
442
|
+
})),
|
|
443
|
+
};
|
|
444
|
+
await storage.put("flock:directory:v1", full);
|
|
445
|
+
expect(
|
|
446
|
+
await contribution.createBot("user-1", {
|
|
447
|
+
...command("limit", 100),
|
|
448
|
+
botId: "overflow",
|
|
449
|
+
}),
|
|
450
|
+
).toMatchObject({
|
|
451
|
+
status: "rejected",
|
|
452
|
+
failure: "Bot directory limit reached",
|
|
453
|
+
});
|
|
454
|
+
});
|
|
455
|
+
});
|