@frockbot/plugin-applets 0.0.0 → 0.3.12

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.
@@ -0,0 +1,345 @@
1
+ // The Applets Package's isolate module, against the `ctx` the wrapper hands it.
2
+ //
3
+ // The fake is the narrow context and nothing else: `execute` is the production
4
+ // module, and what is asserted is what a model would read back, because the
5
+ // answer text *is* the tool's interface. A tool that returns JSON and a tool
6
+ // that returns a sentence naming the next command are different products, and
7
+ // only the second one is testable this way.
8
+ import { describe, expect, test } from "bun:test";
9
+ import type {
10
+ BotPackageContextV1,
11
+ IsolateAppletsOutcomeV1,
12
+ IsolateWorkspaceWriteRequestV1,
13
+ } from "@frockbot/kernel-contracts";
14
+ import { execute, tools } from "./package.js";
15
+
16
+ const APPLET_ID = "u1abc.todo";
17
+
18
+ interface Recorded {
19
+ applets: unknown[];
20
+ writes: IsolateWorkspaceWriteRequestV1[];
21
+ }
22
+
23
+ function fakeContext(
24
+ answers: Partial<Record<string, unknown>> = {},
25
+ options: { workspace?: "available" | "unavailable" } = {},
26
+ ): { ctx: BotPackageContextV1; recorded: Recorded } {
27
+ const recorded: Recorded = { applets: [], writes: [] };
28
+ const answer = (op: string): IsolateAppletsOutcomeV1 => {
29
+ recorded.applets.push(op);
30
+ if (!(op in answers)) {
31
+ return { status: "unavailable", reason: `no fake answer for "${op}"` };
32
+ }
33
+ return { status: "available", value: answers[op] };
34
+ };
35
+ const ctx = {
36
+ applets: {
37
+ list: () => Promise.resolve(answer("list")),
38
+ create: (input: { displayName: string }) => {
39
+ recorded.applets.push(input.displayName);
40
+ return Promise.resolve(answer("create"));
41
+ },
42
+ publish: () => Promise.resolve(answer("publish")),
43
+ revert: () => Promise.resolve(answer("revert")),
44
+ delete: () => Promise.resolve(answer("delete")),
45
+ focus: (input: { appletId: string | null }) => {
46
+ recorded.applets.push(input.appletId);
47
+ return Promise.resolve(answer("focus"));
48
+ },
49
+ generations: () => Promise.resolve(answer("generations")),
50
+ },
51
+ workspace: {
52
+ write: (request: IsolateWorkspaceWriteRequestV1) => {
53
+ recorded.writes.push(request);
54
+ return Promise.resolve(
55
+ options.workspace === "unavailable"
56
+ ? ({
57
+ status: "unavailable",
58
+ reason: "the Workspace is unavailable",
59
+ } as const)
60
+ : ({ status: "available", value: { generationId: "g1" } } as const),
61
+ );
62
+ },
63
+ },
64
+ } as unknown as BotPackageContextV1;
65
+ return { ctx, recorded };
66
+ }
67
+
68
+ function summary(overrides: Record<string, unknown> = {}) {
69
+ return {
70
+ appletId: APPLET_ID,
71
+ displayName: "Todo",
72
+ status: "draft",
73
+ tools: [],
74
+ createdAt: "2026-09-03T00:00:00.000Z",
75
+ ...overrides,
76
+ };
77
+ }
78
+
79
+ describe("the Applets Package's declared tools", () => {
80
+ test("declares exactly the seven verbs, each with an exact input schema", () => {
81
+ expect(tools.map((tool) => tool.name)).toEqual([
82
+ "applet_list",
83
+ "applet_create",
84
+ "applet_publish",
85
+ "applet_revert",
86
+ "applet_delete",
87
+ "applet_focus",
88
+ "applet_generations",
89
+ ]);
90
+ for (const tool of tools) {
91
+ expect(tool.description.length).toBeGreaterThan(40);
92
+ expect(tool.inputSchema).toMatchObject({
93
+ type: "object",
94
+ additionalProperties: false,
95
+ });
96
+ }
97
+ });
98
+
99
+ test("only the two read verbs are idempotent", () => {
100
+ expect(
101
+ tools.filter((tool) => tool.idempotent).map((tool) => tool.name),
102
+ ).toEqual(["applet_list", "applet_generations"]);
103
+ });
104
+ });
105
+
106
+ describe("applet_list", () => {
107
+ test("names every Applet, its status, and its generation", async () => {
108
+ const { ctx } = fakeContext({
109
+ list: [
110
+ summary({
111
+ status: "published",
112
+ currentGenerationId: "gen-2",
113
+ tools: ["add_todo"],
114
+ }),
115
+ ],
116
+ });
117
+
118
+ const answer = await execute("applet_list", {}, ctx);
119
+
120
+ expect(answer).toContain("Todo (u1abc.todo)");
121
+ expect(answer).toContain("published");
122
+ expect(answer).toContain("generation gen-2");
123
+ expect(answer).toContain("add_todo");
124
+ });
125
+
126
+ test("an empty account is told what to do about it, not shown nothing", async () => {
127
+ const { ctx } = fakeContext({ list: [] });
128
+
129
+ expect(await execute("applet_list", {}, ctx)).toContain("applet_create");
130
+ });
131
+
132
+ test("an unavailable capability surfaces its reason verbatim", async () => {
133
+ const { ctx } = fakeContext({});
134
+
135
+ await expect(execute("applet_list", {}, ctx)).rejects.toThrow(
136
+ /Applets are unavailable: no fake answer for "list"/,
137
+ );
138
+ });
139
+ });
140
+
141
+ describe("applet_create", () => {
142
+ test("scaffolds the SDK template into the Applet's own directory", async () => {
143
+ const { ctx, recorded } = fakeContext({ create: summary() });
144
+
145
+ const answer = await execute("applet_create", { displayName: "Todo" }, ctx);
146
+
147
+ // `server.ts` last: the canvas opens on the newest file, and that is the
148
+ // one a Bot edits first.
149
+ expect(recorded.writes.map((write) => write.path.path)).toEqual([
150
+ `${APPLET_ID}/README.md`,
151
+ `${APPLET_ID}/applet.json`,
152
+ `${APPLET_ID}/ui.tsx`,
153
+ `${APPLET_ID}/server.ts`,
154
+ ]);
155
+ for (const write of recorded.writes) {
156
+ expect(write.path.root).toEqual({
157
+ kind: "package-declared",
158
+ packageId: "applets",
159
+ rootId: "source",
160
+ });
161
+ expect(write.expectedGenerationId).toBeNull();
162
+ }
163
+ // The template's placeholders are filled in, so the scaffold reads as this
164
+ // Applet rather than as the template.
165
+ const descriptor = new TextDecoder().decode(
166
+ recorded.writes.find((write) => write.path.path.endsWith("/applet.json"))!
167
+ .bytes,
168
+ );
169
+ expect(descriptor).toContain('"displayName": "Todo"');
170
+ expect(descriptor).not.toContain("__APPLET_NAME__");
171
+ expect(new TextDecoder().decode(recorded.writes[1]!.bytes)).not.toContain(
172
+ "__APPLET_NAME__",
173
+ );
174
+
175
+ expect(answer).toContain(
176
+ "/home/box/agent-data/user-packages/applets/source/u1abc.todo",
177
+ );
178
+ expect(answer).toContain("applet check");
179
+ expect(answer).toContain("applet build");
180
+ expect(answer).toContain("applet_publish");
181
+ });
182
+
183
+ test("a Workspace that cannot be written says so instead of implying success", async () => {
184
+ const { ctx } = fakeContext(
185
+ { create: summary() },
186
+ { workspace: "unavailable" },
187
+ );
188
+
189
+ await expect(
190
+ execute("applet_create", { displayName: "Todo" }, ctx),
191
+ ).rejects.toThrow(/source could not be written/);
192
+ });
193
+
194
+ test("a missing displayName is refused before anything is created", async () => {
195
+ const { ctx, recorded } = fakeContext({ create: summary() });
196
+
197
+ await expect(execute("applet_create", {}, ctx)).rejects.toThrow(
198
+ /displayName is required/,
199
+ );
200
+ expect(recorded.applets).toEqual([]);
201
+ });
202
+ });
203
+
204
+ describe("applet_publish and applet_revert", () => {
205
+ test("a publish reports the generation and the tools it now offers", async () => {
206
+ const { ctx } = fakeContext({
207
+ publish: {
208
+ status: "published",
209
+ appletId: APPLET_ID,
210
+ generationId: "gen-3",
211
+ tools: ["add_todo"],
212
+ compositionGenerationId: "composition-9",
213
+ },
214
+ });
215
+
216
+ const answer = await execute(
217
+ "applet_publish",
218
+ { appletId: APPLET_ID },
219
+ ctx,
220
+ );
221
+
222
+ expect(answer).toContain("gen-3");
223
+ expect(answer).toContain("add_todo");
224
+ expect(answer).toContain("composition-9");
225
+ });
226
+
227
+ test("a failure carries its diagnostics verbatim and says nothing changed", async () => {
228
+ const { ctx } = fakeContext({
229
+ publish: {
230
+ status: "failed",
231
+ appletId: APPLET_ID,
232
+ generationId: "unbuilt",
233
+ reason: '"dist/server.js" is missing',
234
+ diagnostics: ["server declared:aaa actual:bbb"],
235
+ },
236
+ });
237
+
238
+ const answer = await execute(
239
+ "applet_publish",
240
+ { appletId: APPLET_ID },
241
+ ctx,
242
+ );
243
+
244
+ expect(answer).toContain('"dist/server.js" is missing');
245
+ expect(answer).toContain("server declared:aaa actual:bbb");
246
+ expect(answer).toContain("Nothing changed");
247
+ });
248
+
249
+ test("a revert says it reverted, not that it published", async () => {
250
+ const { ctx } = fakeContext({
251
+ revert: {
252
+ status: "published",
253
+ appletId: APPLET_ID,
254
+ generationId: "gen-1",
255
+ tools: [],
256
+ },
257
+ });
258
+
259
+ const answer = await execute(
260
+ "applet_revert",
261
+ { appletId: APPLET_ID, generationId: "gen-1" },
262
+ ctx,
263
+ );
264
+
265
+ expect(answer).toStartWith("Reverted");
266
+ expect(answer).toContain("declares no tools");
267
+ });
268
+ });
269
+
270
+ describe("applet_focus, applet_delete, and applet_generations", () => {
271
+ test("null closes the panel and is a value, not a missing argument", async () => {
272
+ const { ctx, recorded } = fakeContext({ focus: { appletId: null } });
273
+
274
+ expect(await execute("applet_focus", { appletId: null }, ctx)).toContain(
275
+ "Closed",
276
+ );
277
+ expect(recorded.applets[0]).toBeNull();
278
+ });
279
+
280
+ test("a non-id, non-null focus is refused", async () => {
281
+ const { ctx } = fakeContext({ focus: { appletId: null } });
282
+
283
+ await expect(execute("applet_focus", { appletId: 7 }, ctx)).rejects.toThrow(
284
+ /appletId must be an Applet id or null/,
285
+ );
286
+ });
287
+
288
+ test("a delete says plainly that it cannot be undone", async () => {
289
+ const { ctx } = fakeContext({ delete: { status: "deleted" } });
290
+
291
+ expect(
292
+ await execute("applet_delete", { appletId: APPLET_ID }, ctx),
293
+ ).toContain("cannot be undone");
294
+ });
295
+
296
+ test("generations name the current one", async () => {
297
+ const { ctx } = fakeContext({
298
+ generations: [
299
+ {
300
+ generationId: "gen-2",
301
+ origin: "publish",
302
+ status: "active",
303
+ tools: ["add_todo"],
304
+ createdAt: "2026-09-03T00:00:00.000Z",
305
+ isCurrent: true,
306
+ },
307
+ {
308
+ generationId: "gen-1",
309
+ origin: "publish",
310
+ status: "superseded",
311
+ tools: [],
312
+ createdAt: "2026-09-02T00:00:00.000Z",
313
+ isCurrent: false,
314
+ },
315
+ ],
316
+ });
317
+
318
+ const answer = await execute(
319
+ "applet_generations",
320
+ { appletId: APPLET_ID },
321
+ ctx,
322
+ );
323
+
324
+ expect(answer).toContain("gen-2 — publish, active, current");
325
+ expect(answer).toContain("gen-1 — publish, superseded");
326
+ });
327
+
328
+ test("an Applet with no history is told what to do about it", async () => {
329
+ const { ctx } = fakeContext({ generations: [] });
330
+
331
+ expect(
332
+ await execute("applet_generations", { appletId: APPLET_ID }, ctx),
333
+ ).toContain("applet_publish");
334
+ });
335
+ });
336
+
337
+ describe("an unknown tool", () => {
338
+ test("is refused by name", async () => {
339
+ const { ctx } = fakeContext({});
340
+
341
+ await expect(execute("applet_teleport", {}, ctx)).rejects.toThrow(
342
+ /does not implement "applet_teleport"/,
343
+ );
344
+ });
345
+ });