@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
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/frockbot.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 3,
|
|
3
|
+
"id": "flock",
|
|
4
|
+
"displayName": "Flock",
|
|
5
|
+
"version": "0.0.1",
|
|
6
|
+
"compatibility": { "frockbot": ">=0.0.1" },
|
|
7
|
+
"dependencies": {
|
|
8
|
+
"shell": ">=0.0.1",
|
|
9
|
+
"ui-theme": ">=0.0.1"
|
|
10
|
+
},
|
|
11
|
+
"contributions": {
|
|
12
|
+
"backend": [
|
|
13
|
+
{ "entry": "./backend", "host": "gateway" },
|
|
14
|
+
{ "entry": "./user", "host": "user" },
|
|
15
|
+
{ "entry": "./bot", "host": "bot" }
|
|
16
|
+
],
|
|
17
|
+
"runtime": { "entry": "./agent" },
|
|
18
|
+
"client": {
|
|
19
|
+
"entry": "./client",
|
|
20
|
+
"mounts": [
|
|
21
|
+
{ "slot": "frockbot.sidebar-bots", "order": 10 },
|
|
22
|
+
{ "slot": "frockbot.overlays", "order": 10 },
|
|
23
|
+
{ "slot": "frockbot.bot-identity", "order": 10 },
|
|
24
|
+
{ "slot": "frockbot.bot-avatar", "order": 10 },
|
|
25
|
+
{ "slot": "frockbot.bot-avatar-editor", "order": 10 }
|
|
26
|
+
],
|
|
27
|
+
"outlets": []
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"permissions": ["bot:self-manage"],
|
|
31
|
+
"configuration": {
|
|
32
|
+
"settings": [],
|
|
33
|
+
"connectionTypes": [],
|
|
34
|
+
"capabilities": [
|
|
35
|
+
{ "id": "bot-self-management", "kind": "tool", "connectionTypes": [] }
|
|
36
|
+
]
|
|
37
|
+
}
|
|
38
|
+
}
|
package/package.json
CHANGED
|
@@ -1,14 +1,55 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@frockbot/plugin-flock",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": "./src/index.ts",
|
|
8
|
+
"./agent": "./src/agent.ts",
|
|
9
|
+
"./assets/manifest.json": "./assets/manifest.json",
|
|
10
|
+
"./backend": "./src/backend.ts",
|
|
11
|
+
"./bot": "./src/bot.ts",
|
|
12
|
+
"./client": "./src/client/index.ts",
|
|
13
|
+
"./client/BotAvatar.vue": "./src/client/BotAvatar.vue",
|
|
14
|
+
"./client/state": "./src/client/state.ts",
|
|
15
|
+
"./frockbot.json": "./frockbot.json",
|
|
16
|
+
"./manifest": "./src/manifest.ts",
|
|
17
|
+
"./package.json": "./package.json",
|
|
18
|
+
"./shared": "./src/shared.ts",
|
|
19
|
+
"./user": "./src/user.ts"
|
|
20
|
+
},
|
|
21
|
+
"frockbot": {
|
|
22
|
+
"manifest": "./frockbot.json"
|
|
23
|
+
},
|
|
24
|
+
"scripts": {
|
|
25
|
+
"test": "bun test src",
|
|
26
|
+
"build": "vite build",
|
|
27
|
+
"typecheck": "vue-tsc --noEmit -p tsconfig.json"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@frockbot/client-core": "0.1.0",
|
|
31
|
+
"@frockbot/client-ui": "0.1.0",
|
|
32
|
+
"@frockbot/configuration-core": "0.1.0",
|
|
33
|
+
"@frockbot/kernel-contracts": "0.1.0",
|
|
34
|
+
"@frockbot/plugin-shell": "0.1.0",
|
|
35
|
+
"cordis": "4.0.0-rc.8",
|
|
36
|
+
"vue": "3.5.41"
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"@frockbot/plugin-testkit": "0.1.0",
|
|
40
|
+
"@types/bun": "1.4.0",
|
|
41
|
+
"@vitejs/plugin-vue": "6.0.8",
|
|
42
|
+
"css-tree": "2.3.1",
|
|
43
|
+
"typescript": "5.9.3",
|
|
44
|
+
"vite": "8.2.2",
|
|
45
|
+
"vue-tsc": "3.3.10"
|
|
46
|
+
},
|
|
47
|
+
"publishConfig": {
|
|
48
|
+
"access": "public"
|
|
49
|
+
},
|
|
6
50
|
"repository": {
|
|
7
51
|
"type": "git",
|
|
8
52
|
"url": "git+https://github.com/timoconnellaus/frockbot.git",
|
|
9
53
|
"directory": "packages/plugin-flock"
|
|
10
|
-
},
|
|
11
|
-
"publishConfig": {
|
|
12
|
-
"access": "public"
|
|
13
54
|
}
|
|
14
55
|
}
|
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
// The Flock runtime Contribution: what a Bot may change about itself, what it
|
|
2
|
+
// records when it does, and what it refuses.
|
|
3
|
+
//
|
|
4
|
+
// The host here is a small in-memory stand-in for the two Durable Objects,
|
|
5
|
+
// built out of the production `applyBotProfilePatchV1` and the production
|
|
6
|
+
// `FlockUserBackendContribution`, so what these prove is the real partial-patch
|
|
7
|
+
// and directory behaviour rather than a double's.
|
|
8
|
+
import { describe, expect, test } from "bun:test";
|
|
9
|
+
import {
|
|
10
|
+
applyBotProfilePatchV1,
|
|
11
|
+
ConfigurationConflictError,
|
|
12
|
+
initializeBotSettingsV1,
|
|
13
|
+
type BotSettingsViewV1,
|
|
14
|
+
type ConfigurationCommandV1,
|
|
15
|
+
type OperationReceiptV1,
|
|
16
|
+
} from "@frockbot/configuration-core";
|
|
17
|
+
import type { SessionEvent } from "@frockbot/kernel-contracts";
|
|
18
|
+
import {
|
|
19
|
+
createBotCreateTool,
|
|
20
|
+
createBotUpdateTool,
|
|
21
|
+
createdBotIdV1,
|
|
22
|
+
decodeBotUpdateInputV1,
|
|
23
|
+
type FlockSelfRuntimeHostV1,
|
|
24
|
+
} from "./agent.ts";
|
|
25
|
+
import { createFlockUserBackendContribution } from "./user.ts";
|
|
26
|
+
import { decodeCreateBotCommandV1 } from "./shared.ts";
|
|
27
|
+
|
|
28
|
+
const OWNER = { userId: "user-1", botId: "bot-1" };
|
|
29
|
+
const WRITER = {
|
|
30
|
+
kind: "bot" as const,
|
|
31
|
+
botId: "bot-1",
|
|
32
|
+
sessionId: "user-1:bot-1",
|
|
33
|
+
turnId: "turn-4",
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const CONTEXT = {
|
|
37
|
+
botId: "bot-1",
|
|
38
|
+
agentId: "bot-1",
|
|
39
|
+
sessionId: "user-1:bot-1",
|
|
40
|
+
compositionGenerationId: "2026-08-31T00:00:00.000Z:0123456789abcdef",
|
|
41
|
+
turnType: "chat" as const,
|
|
42
|
+
effectId: "tool:1:1:0",
|
|
43
|
+
signal: new AbortController().signal,
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/** An in-memory Durable Object storage with the transaction seam Flock uses. */
|
|
47
|
+
function memoryStorage() {
|
|
48
|
+
const map = new Map<string, unknown>();
|
|
49
|
+
const surface = {
|
|
50
|
+
get: async <T>(key: string) => map.get(key) as T | undefined,
|
|
51
|
+
put: async (key: unknown, value?: unknown) => {
|
|
52
|
+
if (typeof key === "string") map.set(key, value);
|
|
53
|
+
else
|
|
54
|
+
for (const [entry, item] of Object.entries(
|
|
55
|
+
key as Record<string, unknown>,
|
|
56
|
+
))
|
|
57
|
+
map.set(entry, item);
|
|
58
|
+
},
|
|
59
|
+
delete: async (key: string) => map.delete(key),
|
|
60
|
+
list: async <T>({ prefix }: { prefix: string }) =>
|
|
61
|
+
new Map(
|
|
62
|
+
[...map.entries()]
|
|
63
|
+
.filter(([key]) => key.startsWith(prefix))
|
|
64
|
+
.map(([key, value]) => [key, value as T]),
|
|
65
|
+
),
|
|
66
|
+
setAlarm: async () => {},
|
|
67
|
+
};
|
|
68
|
+
return {
|
|
69
|
+
...surface,
|
|
70
|
+
transaction: async <T>(callback: (tx: typeof surface) => Promise<T>) =>
|
|
71
|
+
callback(surface),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
interface Harness {
|
|
76
|
+
host: FlockSelfRuntimeHostV1;
|
|
77
|
+
settings(): BotSettingsViewV1;
|
|
78
|
+
announcements(): SessionEvent[];
|
|
79
|
+
commands(): Array<Extract<ConfigurationCommandV1, { botId: string }>>;
|
|
80
|
+
/** Forces the next `commandSelf` to lose an optimistic race exactly once. */
|
|
81
|
+
raceOnce(): void;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function harness(initial?: Partial<BotSettingsViewV1>): Harness {
|
|
85
|
+
let settings: BotSettingsViewV1 = {
|
|
86
|
+
...initializeBotSettingsV1("bot-1"),
|
|
87
|
+
profile: { name: "General" },
|
|
88
|
+
...initial,
|
|
89
|
+
};
|
|
90
|
+
const announcements: SessionEvent[] = [];
|
|
91
|
+
const commands: Array<Extract<ConfigurationCommandV1, { botId: string }>> =
|
|
92
|
+
[];
|
|
93
|
+
let race = false;
|
|
94
|
+
const storage = memoryStorage();
|
|
95
|
+
const flock = createFlockUserBackendContribution({
|
|
96
|
+
storage,
|
|
97
|
+
readUserSettings: () =>
|
|
98
|
+
Promise.resolve({
|
|
99
|
+
schemaVersion: 1,
|
|
100
|
+
revision: 0,
|
|
101
|
+
profile: { name: "User" },
|
|
102
|
+
packages: [],
|
|
103
|
+
connections: [],
|
|
104
|
+
}),
|
|
105
|
+
now: () => new Date("2026-08-31T10:00:00.000Z"),
|
|
106
|
+
random: () => 0,
|
|
107
|
+
commandBotLifecycle: () => {
|
|
108
|
+
throw new Error("not used");
|
|
109
|
+
},
|
|
110
|
+
readBotLifecycle: () => {
|
|
111
|
+
throw new Error("not used");
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
return {
|
|
115
|
+
settings: () => settings,
|
|
116
|
+
announcements: () => announcements,
|
|
117
|
+
commands: () => commands,
|
|
118
|
+
raceOnce: () => {
|
|
119
|
+
race = true;
|
|
120
|
+
},
|
|
121
|
+
host: {
|
|
122
|
+
owner: OWNER,
|
|
123
|
+
writer: WRITER,
|
|
124
|
+
readSelf: async () => structuredClone(settings),
|
|
125
|
+
commandSelf: async (command): Promise<OperationReceiptV1> => {
|
|
126
|
+
if (race) {
|
|
127
|
+
race = false;
|
|
128
|
+
settings = { ...settings, revision: settings.revision + 1 };
|
|
129
|
+
throw new ConfigurationConflictError(settings.revision);
|
|
130
|
+
}
|
|
131
|
+
if (command.expectedRevision !== settings.revision) {
|
|
132
|
+
throw new ConfigurationConflictError(settings.revision);
|
|
133
|
+
}
|
|
134
|
+
commands.push(command);
|
|
135
|
+
const revision = settings.revision + 1;
|
|
136
|
+
if (command.type === "bot/set-profile") {
|
|
137
|
+
const profile = applyBotProfilePatchV1(
|
|
138
|
+
settings.profile,
|
|
139
|
+
command.profile,
|
|
140
|
+
command.namedBy ?? "user",
|
|
141
|
+
);
|
|
142
|
+
// The Bot Durable Object appends the announcement in the same
|
|
143
|
+
// transaction that writes the name; the seam is reproduced here.
|
|
144
|
+
if (profile.name !== settings.profile.name) {
|
|
145
|
+
announcements.push({
|
|
146
|
+
type: "bot/renamed",
|
|
147
|
+
seq: announcements.length,
|
|
148
|
+
timestamp: "2026-08-31T10:00:00.000Z",
|
|
149
|
+
from: settings.profile.name,
|
|
150
|
+
to: profile.name,
|
|
151
|
+
namedBy: profile.namedBy ?? "user",
|
|
152
|
+
...(command.writer ? { writer: command.writer } : {}),
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
settings = { ...settings, revision, profile };
|
|
156
|
+
} else if (command.type === "bot/update-notifications") {
|
|
157
|
+
settings = {
|
|
158
|
+
...settings,
|
|
159
|
+
revision,
|
|
160
|
+
notifications: command.notifications,
|
|
161
|
+
};
|
|
162
|
+
} else {
|
|
163
|
+
throw new Error(`unexpected command ${command.type}`);
|
|
164
|
+
}
|
|
165
|
+
return {
|
|
166
|
+
schemaVersion: 1,
|
|
167
|
+
commandId: command.commandId,
|
|
168
|
+
revision,
|
|
169
|
+
status: "applied",
|
|
170
|
+
};
|
|
171
|
+
},
|
|
172
|
+
listBots: () => flock.listBots(),
|
|
173
|
+
createBot: (command) =>
|
|
174
|
+
// The command crosses the User Durable Object seam, so it decodes on
|
|
175
|
+
// the way in exactly as the production RPC does.
|
|
176
|
+
flock.createBot(OWNER.userId, decodeCreateBotCommandV1(command)),
|
|
177
|
+
},
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
describe("bot_update", () => {
|
|
182
|
+
test("changes only the fields it was given", async () => {
|
|
183
|
+
const test1 = harness({
|
|
184
|
+
profile: { name: "General", description: "A helper.", title: "Aide" },
|
|
185
|
+
});
|
|
186
|
+
const tool = createBotUpdateTool(test1.host);
|
|
187
|
+
|
|
188
|
+
const result = await tool.execute({ title: "Chief of staff" }, CONTEXT);
|
|
189
|
+
|
|
190
|
+
expect(result.isError).toBe(false);
|
|
191
|
+
expect(test1.settings().profile).toEqual({
|
|
192
|
+
name: "General",
|
|
193
|
+
description: "A helper.",
|
|
194
|
+
title: "Chief of staff",
|
|
195
|
+
});
|
|
196
|
+
expect(test1.announcements()).toEqual([]);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test("clears an optional field with the empty string", async () => {
|
|
200
|
+
const test1 = harness({
|
|
201
|
+
profile: { name: "General", title: "Aide", description: "A helper." },
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
await createBotUpdateTool(test1.host).execute({ title: "" }, CONTEXT);
|
|
205
|
+
|
|
206
|
+
expect(test1.settings().profile).toEqual({
|
|
207
|
+
name: "General",
|
|
208
|
+
description: "A helper.",
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
test("a self-rename records the Bot as the writer and announces it", async () => {
|
|
213
|
+
const test1 = harness();
|
|
214
|
+
|
|
215
|
+
const result = await createBotUpdateTool(test1.host).execute(
|
|
216
|
+
{ name: "Chief of staff" },
|
|
217
|
+
CONTEXT,
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
expect(result.isError).toBe(false);
|
|
221
|
+
expect(test1.settings().profile).toEqual({
|
|
222
|
+
name: "Chief of staff",
|
|
223
|
+
namedBy: "bot",
|
|
224
|
+
});
|
|
225
|
+
expect(test1.announcements()).toEqual([
|
|
226
|
+
{
|
|
227
|
+
type: "bot/renamed",
|
|
228
|
+
seq: 0,
|
|
229
|
+
timestamp: "2026-08-31T10:00:00.000Z",
|
|
230
|
+
from: "General",
|
|
231
|
+
to: "Chief of staff",
|
|
232
|
+
namedBy: "bot",
|
|
233
|
+
writer: WRITER,
|
|
234
|
+
},
|
|
235
|
+
]);
|
|
236
|
+
// The provenance the command carried is the Bot itself, never a target.
|
|
237
|
+
const command = test1.commands()[0]!;
|
|
238
|
+
expect(command.type).toBe("bot/set-profile");
|
|
239
|
+
expect(
|
|
240
|
+
command.type === "bot/set-profile" ? command.namedBy : undefined,
|
|
241
|
+
).toBe("bot");
|
|
242
|
+
expect(
|
|
243
|
+
command.type === "bot/set-profile" ? command.writer : undefined,
|
|
244
|
+
).toEqual(WRITER);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
test("writes nothing when the profile already holds every value", async () => {
|
|
248
|
+
const test1 = harness({ profile: { name: "General", title: "Aide" } });
|
|
249
|
+
|
|
250
|
+
const result = await createBotUpdateTool(test1.host).execute(
|
|
251
|
+
{ title: "Aide" },
|
|
252
|
+
CONTEXT,
|
|
253
|
+
);
|
|
254
|
+
|
|
255
|
+
expect(result.isError).toBe(false);
|
|
256
|
+
expect(result.content).toContain("Nothing changed");
|
|
257
|
+
expect(test1.commands()).toEqual([]);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
test("a replay after eviction re-runs without a second announcement", async () => {
|
|
261
|
+
const test1 = harness();
|
|
262
|
+
const tool = createBotUpdateTool(test1.host);
|
|
263
|
+
|
|
264
|
+
await tool.execute({ name: "Chief of staff" }, CONTEXT);
|
|
265
|
+
// The registry recovers an idempotent tool by executing it again under the
|
|
266
|
+
// same effect id.
|
|
267
|
+
expect(tool.idempotent).toBe(true);
|
|
268
|
+
const replay = await tool.execute({ name: "Chief of staff" }, CONTEXT);
|
|
269
|
+
|
|
270
|
+
expect(replay.isError).toBe(false);
|
|
271
|
+
expect(test1.announcements()).toHaveLength(1);
|
|
272
|
+
expect(test1.commands()).toHaveLength(1);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
test("changes notifications alongside the profile", async () => {
|
|
276
|
+
const test1 = harness();
|
|
277
|
+
|
|
278
|
+
const result = await createBotUpdateTool(test1.host).execute(
|
|
279
|
+
{ title: "Aide", notify_on_updates: false },
|
|
280
|
+
CONTEXT,
|
|
281
|
+
);
|
|
282
|
+
|
|
283
|
+
expect(result.isError).toBe(false);
|
|
284
|
+
expect(test1.settings().notifications).toEqual({ enabled: false });
|
|
285
|
+
expect(test1.commands().map((command) => command.type)).toEqual([
|
|
286
|
+
"bot/set-profile",
|
|
287
|
+
"bot/update-notifications",
|
|
288
|
+
]);
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
test("re-issues the command after losing an optimistic race", async () => {
|
|
292
|
+
const test1 = harness();
|
|
293
|
+
test1.raceOnce();
|
|
294
|
+
|
|
295
|
+
const result = await createBotUpdateTool(test1.host).execute(
|
|
296
|
+
{ title: "Aide" },
|
|
297
|
+
CONTEXT,
|
|
298
|
+
);
|
|
299
|
+
|
|
300
|
+
expect(result.isError).toBe(false);
|
|
301
|
+
expect(test1.settings().profile.title).toBe("Aide");
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
test("refuses an empty call, an unknown field, and a blank name", () => {
|
|
305
|
+
expect(() => decodeBotUpdateInputV1({})).toThrow();
|
|
306
|
+
expect(() => decodeBotUpdateInputV1({ archived: true })).toThrow();
|
|
307
|
+
expect(() => decodeBotUpdateInputV1({ name: " " })).toThrow();
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
test("offers no way to archive, restore, or delete a Bot", () => {
|
|
311
|
+
const tool = createBotUpdateTool(harness().host);
|
|
312
|
+
const schema = tool.inputSchema as { properties: Record<string, unknown> };
|
|
313
|
+
|
|
314
|
+
expect(Object.keys(schema.properties)).toEqual([
|
|
315
|
+
"name",
|
|
316
|
+
"description",
|
|
317
|
+
"title",
|
|
318
|
+
"hidden_from_sidebar",
|
|
319
|
+
"notify_on_updates",
|
|
320
|
+
]);
|
|
321
|
+
});
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
describe("bot_create", () => {
|
|
325
|
+
test("registers one Bot in the User's flock with its description", async () => {
|
|
326
|
+
const test1 = harness();
|
|
327
|
+
|
|
328
|
+
const result = await createBotCreateTool(test1.host).execute(
|
|
329
|
+
{ name: "Budget", description: "Watches the money." },
|
|
330
|
+
CONTEXT,
|
|
331
|
+
);
|
|
332
|
+
|
|
333
|
+
expect(result.isError).toBe(false);
|
|
334
|
+
const directory = await test1.host.listBots();
|
|
335
|
+
expect(directory.bots).toHaveLength(1);
|
|
336
|
+
const created = directory.bots[0]!;
|
|
337
|
+
expect(created.initialName).toBe("Budget");
|
|
338
|
+
expect(created.initialDescription).toBe("Watches the money.");
|
|
339
|
+
expect(created.botId).toBe(
|
|
340
|
+
await createdBotIdV1(OWNER, CONTEXT.effectId, "Budget"),
|
|
341
|
+
);
|
|
342
|
+
// The new Bot receives no authority beyond what `bot/create` grants: no
|
|
343
|
+
// model of its own and no Assignment.
|
|
344
|
+
expect(created.initialModel).toBeUndefined();
|
|
345
|
+
expect(created.initialModelBinding).toBeUndefined();
|
|
346
|
+
expect(result.content).toContain(created.botId);
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
test("records the creating Bot and Turn on the registration", async () => {
|
|
350
|
+
const test1 = harness();
|
|
351
|
+
|
|
352
|
+
await createBotCreateTool(test1.host).execute({ name: "Budget" }, CONTEXT);
|
|
353
|
+
|
|
354
|
+
expect((await test1.host.listBots()).bots[0]!.createdBy).toEqual(WRITER);
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
test("a replay after eviction creates exactly one Bot", async () => {
|
|
358
|
+
const test1 = harness();
|
|
359
|
+
const tool = createBotCreateTool(test1.host);
|
|
360
|
+
expect(tool.idempotent).toBe(true);
|
|
361
|
+
|
|
362
|
+
const first = await tool.execute({ name: "Budget" }, CONTEXT);
|
|
363
|
+
const replay = await tool.execute({ name: "Budget" }, CONTEXT);
|
|
364
|
+
|
|
365
|
+
expect(first.isError).toBe(false);
|
|
366
|
+
expect(replay.isError).toBe(false);
|
|
367
|
+
expect(replay.content).toContain("already exists");
|
|
368
|
+
expect((await test1.host.listBots()).bots).toHaveLength(1);
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
test("a different occurrence creates a distinct Bot", async () => {
|
|
372
|
+
const test1 = harness();
|
|
373
|
+
const tool = createBotCreateTool(test1.host);
|
|
374
|
+
|
|
375
|
+
await tool.execute({ name: "Budget" }, CONTEXT);
|
|
376
|
+
await tool.execute(
|
|
377
|
+
{ name: "Budget" },
|
|
378
|
+
{ ...CONTEXT, effectId: "tool:1:3:0" },
|
|
379
|
+
);
|
|
380
|
+
|
|
381
|
+
const ids = (await test1.host.listBots()).bots.map((bot) => bot.botId);
|
|
382
|
+
expect(new Set(ids).size).toBe(2);
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
test("refuses a nameless call and an unknown field", async () => {
|
|
386
|
+
const tool = createBotCreateTool(harness().host);
|
|
387
|
+
|
|
388
|
+
expect(tool.validate?.({})).toBe(false);
|
|
389
|
+
expect(tool.validate?.({ name: "Budget", model: "glm" })).toBe(false);
|
|
390
|
+
expect(tool.validate?.({ name: "Budget" })).toBe(true);
|
|
391
|
+
});
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
describe("the self-management seam", () => {
|
|
395
|
+
test("both tools are work tools, offered on every turn type", () => {
|
|
396
|
+
const host = harness().host;
|
|
397
|
+
|
|
398
|
+
expect(createBotUpdateTool(host).admission).toBeUndefined();
|
|
399
|
+
expect(createBotCreateTool(host).admission).toBeUndefined();
|
|
400
|
+
});
|
|
401
|
+
});
|