@frockbot/configuration-core 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.
@@ -0,0 +1,1387 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import {
3
+ capabilityAssignmentFailureV1,
4
+ ConfigurationDecodeError,
5
+ decodeBotConfigurationExecuteRpcV1,
6
+ decodeBotConfigurationReadRpcV1,
7
+ decodeBotIdV1,
8
+ decodeBotSettingsViewV1,
9
+ decodeCompositionCommandReceiptV1,
10
+ decodeCompositionGenerationListViewV1,
11
+ decodeCompositionGenerationViewV1,
12
+ decodeRevertCompositionCommandV1,
13
+ decodeConnectionDependencyRequirementV1,
14
+ decodeConfigurationCommandV1,
15
+ decodeConfigurationQueryV1,
16
+ decodeOperationReceiptV1,
17
+ decodeRevokeConnectionCommandV1,
18
+ decodeStartConnectionCommandV1,
19
+ decodeUserConfigurationExecuteRpcV1,
20
+ decodeUserConfigurationReadRpcV1,
21
+ decodeUserSettingsViewV1,
22
+ initializeBotSettingsV1,
23
+ resolveBotExecutionPlanV1,
24
+ resolveBotModelBindingV1,
25
+ resolveEffectiveBotModelV1,
26
+ type ModelAssignment,
27
+ } from "./index.js";
28
+ import {
29
+ isApplicationDeploymentHash,
30
+ isConnectionIdentifier,
31
+ isPublicIdentifier,
32
+ isRpcIdentifier,
33
+ } from "./identifiers.js";
34
+
35
+ describe("shared public identifier policy", () => {
36
+ test("accepts the bounded cross-runtime identifier grammar", () => {
37
+ expect(isPublicIdentifier("Bot_1.release-candidate")).toBe(true);
38
+ expect(isPublicIdentifier("a".repeat(128))).toBe(true);
39
+
40
+ for (const candidate of [
41
+ "",
42
+ "-leading-hyphen",
43
+ "slash/value",
44
+ "a".repeat(129),
45
+ 1,
46
+ ]) {
47
+ expect(isPublicIdentifier(candidate)).toBe(false);
48
+ }
49
+ });
50
+
51
+ test("defines bounded RPC identifiers and deployment hashes", () => {
52
+ expect(isRpcIdentifier("user:person@example.com")).toBe(true);
53
+ expect(isRpcIdentifier("bad/value")).toBe(false);
54
+ expect(isRpcIdentifier("r".repeat(129))).toBe(false);
55
+ expect(isApplicationDeploymentHash("sha256:abc-123")).toBe(true);
56
+ expect(isApplicationDeploymentHash("bad@hash")).toBe(false);
57
+ expect(isApplicationDeploymentHash("h".repeat(257))).toBe(false);
58
+ });
59
+
60
+ test("excludes object-prototype names from Connection identifiers", () => {
61
+ expect(isConnectionIdentifier("connection-1")).toBe(true);
62
+ expect(isConnectionIdentifier("__proto__")).toBe(false);
63
+ expect(isConnectionIdentifier("constructor")).toBe(false);
64
+ expect(isConnectionIdentifier("slash/value")).toBe(false);
65
+ });
66
+ });
67
+
68
+ describe("configuration DTO seam", () => {
69
+ test("uses one bounded Bot identifier grammar across hosted seams", () => {
70
+ expect(decodeBotIdV1("bot-1.alpha_beta")).toBe("bot-1.alpha_beta");
71
+ for (const value of ["bad:bot", "bad@bot", "b".repeat(129)])
72
+ expect(() => decodeBotIdV1(value)).toThrow("botId is invalid");
73
+ });
74
+
75
+ test("decodes a versioned Bot profile command", () => {
76
+ expect(
77
+ decodeConfigurationCommandV1({
78
+ schemaVersion: 1,
79
+ type: "bot/update-profile",
80
+ commandId: "command-1",
81
+ botId: "primary",
82
+ expectedRevision: 3,
83
+ profile: {
84
+ name: "Housework",
85
+ label: "Research, marketing, admin",
86
+ description: "Keeps the household organized.",
87
+ },
88
+ }),
89
+ ).toEqual({
90
+ schemaVersion: 1,
91
+ type: "bot/update-profile",
92
+ commandId: "command-1",
93
+ botId: "primary",
94
+ expectedRevision: 3,
95
+ profile: {
96
+ name: "Housework",
97
+ label: "Research, marketing, admin",
98
+ description: "Keeps the household organized.",
99
+ },
100
+ });
101
+ });
102
+
103
+ test("decodes exact Assign, Replace, and Unassign commands", () => {
104
+ const meta = {
105
+ schemaVersion: 1,
106
+ commandId: "operation-1",
107
+ expectedRevision: 2,
108
+ botId: "primary",
109
+ } as const;
110
+ const assignment = {
111
+ assignmentId: "mail",
112
+ packageId: "mail",
113
+ capabilityId: "send",
114
+ connectionId: "mail-1",
115
+ };
116
+ expect(
117
+ decodeConfigurationCommandV1({
118
+ ...meta,
119
+ type: "bot/replace-capability",
120
+ assignment,
121
+ }),
122
+ ).toMatchObject({ type: "bot/replace-capability", assignment });
123
+ expect(
124
+ decodeConfigurationCommandV1({
125
+ ...meta,
126
+ type: "bot/unassign-capability",
127
+ assignmentId: "mail",
128
+ }),
129
+ ).toMatchObject({ type: "bot/unassign-capability", assignmentId: "mail" });
130
+ expect(() =>
131
+ decodeConfigurationCommandV1({
132
+ ...meta,
133
+ type: "bot/unassign-capability",
134
+ assignmentId: "mail",
135
+ assignment,
136
+ }),
137
+ ).toThrow(ConfigurationDecodeError);
138
+ });
139
+
140
+ test("accepts provider model IDs through the catalog limit", () => {
141
+ const providerModelId = "m".repeat(256);
142
+
143
+ expect(
144
+ decodeConfigurationCommandV1({
145
+ schemaVersion: 1,
146
+ type: "bot/select-model",
147
+ commandId: "command-model",
148
+ botId: "primary",
149
+ expectedRevision: 3,
150
+ model: { connectionId: "connection-1", providerModelId },
151
+ }),
152
+ ).toMatchObject({ model: { providerModelId } });
153
+ });
154
+
155
+ test("decodes atomic model binding and explicit unbinding commands", () => {
156
+ expect(
157
+ decodeConfigurationCommandV1({
158
+ schemaVersion: 1,
159
+ type: "bot/assign-capability",
160
+ commandId: "bind-model",
161
+ botId: "primary",
162
+ expectedRevision: 2,
163
+ assignment: {
164
+ assignmentId: "ollama-model",
165
+ packageId: "provider-ollama-cloud",
166
+ capabilityId: "ollama-cloud-models",
167
+ connectionId: "ollama-work",
168
+ },
169
+ model: {
170
+ connectionId: "ollama-work",
171
+ providerModelId: "glm-5.3-flash:cloud",
172
+ },
173
+ }),
174
+ ).toMatchObject({
175
+ type: "bot/assign-capability",
176
+ assignment: { connectionId: "ollama-work" },
177
+ model: { providerModelId: "glm-5.3-flash:cloud" },
178
+ });
179
+ expect(
180
+ decodeConfigurationCommandV1({
181
+ schemaVersion: 1,
182
+ type: "bot/unbind-model",
183
+ commandId: "unbind-model",
184
+ botId: "primary",
185
+ expectedRevision: 3,
186
+ assignmentId: "ollama-model",
187
+ }),
188
+ ).toMatchObject({ type: "bot/unbind-model", assignmentId: "ollama-model" });
189
+ });
190
+
191
+ test("rejects unversioned, malformed, and unknown commands", () => {
192
+ for (const value of [
193
+ { type: "bot/update-profile" },
194
+ {
195
+ schemaVersion: 1,
196
+ type: "bot/update-profile",
197
+ commandId: "command-1",
198
+ botId: "../primary",
199
+ expectedRevision: 0,
200
+ profile: { name: "Primary" },
201
+ },
202
+ { schemaVersion: 1, type: "bot/delete-everything" },
203
+ ]) {
204
+ expect(() => decodeConfigurationCommandV1(value)).toThrow(
205
+ ConfigurationDecodeError,
206
+ );
207
+ }
208
+ });
209
+
210
+ test("requires a source for new-Bot defaults and forbids an empty automatic choice", () => {
211
+ const command = {
212
+ schemaVersion: 1,
213
+ type: "user/set-new-bot-model",
214
+ commandId: "choose-default",
215
+ expectedRevision: 0,
216
+ } as const;
217
+ expect(() =>
218
+ decodeConfigurationCommandV1({
219
+ ...command,
220
+ model: { connectionId: "connection-1", providerModelId: "model-1" },
221
+ }),
222
+ ).toThrow("invalid fields");
223
+ expect(() =>
224
+ decodeConfigurationCommandV1({ ...command, source: "provider" }),
225
+ ).toThrow("source must be user or auto");
226
+ expect(() =>
227
+ decodeConfigurationCommandV1({ ...command, source: "auto" }),
228
+ ).toThrow("must name a model");
229
+ expect(
230
+ decodeConfigurationCommandV1({ ...command, source: "user" }),
231
+ ).toMatchObject({ source: "user", model: undefined });
232
+ });
233
+
234
+ test("rejects unknown fields throughout configuration commands", () => {
235
+ const meta = {
236
+ schemaVersion: 1,
237
+ commandId: "command-1",
238
+ expectedRevision: 0,
239
+ } as const;
240
+ for (const value of [
241
+ {
242
+ ...meta,
243
+ type: "user/update-profile",
244
+ profile: { name: "Alice", extra: true },
245
+ },
246
+ {
247
+ ...meta,
248
+ type: "user/set-new-bot-model",
249
+ source: "user",
250
+ model: {
251
+ connectionId: "provider-1",
252
+ providerModelId: "model-1",
253
+ extra: true,
254
+ },
255
+ },
256
+ {
257
+ ...meta,
258
+ type: "bot/update-notifications",
259
+ botId: "primary",
260
+ notifications: { enabled: true, extra: true },
261
+ },
262
+ {
263
+ ...meta,
264
+ type: "bot/assign-capability",
265
+ botId: "primary",
266
+ assignment: {
267
+ assignmentId: "gmail",
268
+ packageId: "composio",
269
+ capabilityId: "gmail-tools",
270
+ extra: true,
271
+ },
272
+ },
273
+ {
274
+ ...meta,
275
+ type: "bot/update-profile",
276
+ botId: "primary",
277
+ profile: { name: "Primary" },
278
+ extra: true,
279
+ },
280
+ ]) {
281
+ expect(() => decodeConfigurationCommandV1(value)).toThrow(
282
+ ConfigurationDecodeError,
283
+ );
284
+ }
285
+ });
286
+
287
+ test("decodes only explicit User and Bot queries", () => {
288
+ expect(
289
+ decodeConfigurationQueryV1({
290
+ schemaVersion: 1,
291
+ type: "bot/get",
292
+ botId: "primary",
293
+ }),
294
+ ).toEqual({ schemaVersion: 1, type: "bot/get", botId: "primary" });
295
+ for (const value of [
296
+ { schemaVersion: 1, type: "all/get" },
297
+ { schemaVersion: 1, type: "user/get", extra: true },
298
+ { schemaVersion: 1, type: "bot/get", botId: "primary", extra: true },
299
+ ]) {
300
+ expect(() => decodeConfigurationQueryV1(value)).toThrow(
301
+ ConfigurationDecodeError,
302
+ );
303
+ }
304
+ });
305
+
306
+ test("decodes versioned Connection dependency requirements", () => {
307
+ expect(
308
+ decodeConnectionDependencyRequirementV1({
309
+ schemaVersion: 1,
310
+ packageId: "composio",
311
+ packageVersion: "0.0.1",
312
+ capabilityId: "gmail-tools",
313
+ connectionTypeIds: ["gmail"],
314
+ }),
315
+ ).toEqual({
316
+ schemaVersion: 1,
317
+ packageId: "composio",
318
+ packageVersion: "0.0.1",
319
+ capabilityId: "gmail-tools",
320
+ connectionTypeIds: ["gmail"],
321
+ });
322
+ for (const value of [
323
+ {
324
+ schemaVersion: 1,
325
+ packageId: "composio",
326
+ packageVersion: "0.0.1",
327
+ capabilityId: "gmail-tools",
328
+ connectionTypeIds: [],
329
+ },
330
+ {
331
+ schemaVersion: 1,
332
+ packageId: "composio",
333
+ packageVersion: "0.0.1",
334
+ capabilityId: "gmail-tools",
335
+ connectionTypeIds: ["gmail"],
336
+ extra: true,
337
+ },
338
+ ]) {
339
+ expect(() => decodeConnectionDependencyRequirementV1(value)).toThrow(
340
+ ConfigurationDecodeError,
341
+ );
342
+ }
343
+ });
344
+
345
+ test("strictly decodes versioned Connection commands", () => {
346
+ expect(
347
+ decodeStartConnectionCommandV1({
348
+ schemaVersion: 1,
349
+ type: "connection/start",
350
+ commandId: "connection-1",
351
+ connectionTypeId: "gmail",
352
+ alias: "Work",
353
+ }),
354
+ ).toEqual({
355
+ schemaVersion: 1,
356
+ type: "connection/start",
357
+ commandId: "connection-1",
358
+ connectionTypeId: "gmail",
359
+ alias: "Work",
360
+ });
361
+ expect(
362
+ decodeRevokeConnectionCommandV1({
363
+ schemaVersion: 1,
364
+ type: "connection/revoke",
365
+ }),
366
+ ).toEqual({ schemaVersion: 1, type: "connection/revoke" });
367
+ for (const value of [
368
+ {
369
+ schemaVersion: 2,
370
+ type: "connection/start",
371
+ commandId: "connection-1",
372
+ connectionTypeId: "gmail",
373
+ },
374
+ {
375
+ schemaVersion: 1,
376
+ type: "connection/start",
377
+ commandId: "connection-1",
378
+ connectionTypeId: "gmail",
379
+ extra: true,
380
+ },
381
+ { schemaVersion: 1, type: "connection/revoke", extra: true },
382
+ ]) {
383
+ expect(() =>
384
+ value.type === "connection/revoke"
385
+ ? decodeRevokeConnectionCommandV1(value)
386
+ : decodeStartConnectionCommandV1(value),
387
+ ).toThrow(ConfigurationDecodeError);
388
+ }
389
+ });
390
+
391
+ test("rejects hidden and symbol fields across Assignment seams", () => {
392
+ const assignment = {
393
+ assignmentId: "mail",
394
+ packageId: "mail",
395
+ capabilityId: "send",
396
+ connectionId: "mail-1",
397
+ };
398
+ const command = {
399
+ schemaVersion: 1,
400
+ type: "bot/assign-capability",
401
+ commandId: "command-1",
402
+ expectedRevision: 0,
403
+ botId: "primary",
404
+ assignment,
405
+ };
406
+ const receipt = {
407
+ schemaVersion: 1,
408
+ commandId: "command-1",
409
+ revision: 0,
410
+ status: "pending",
411
+ };
412
+ const view = {
413
+ schemaVersion: 1,
414
+ botId: "primary",
415
+ revision: 0,
416
+ profile: { name: "Primary" },
417
+ notifications: { enabled: false },
418
+ assignments: [],
419
+ assignmentOperations: [
420
+ {
421
+ commandId: "command-1",
422
+ kind: "assigning",
423
+ assignmentId: "mail",
424
+ state: "retrying",
425
+ target: assignment,
426
+ },
427
+ ],
428
+ };
429
+ const rpc = {
430
+ schemaVersion: 1,
431
+ userId: "user-1",
432
+ botId: "primary",
433
+ command,
434
+ };
435
+ for (const [decode, candidate] of [
436
+ [decodeConfigurationCommandV1, command],
437
+ [decodeConfigurationCommandV1, { ...command, assignment }],
438
+ [decodeOperationReceiptV1, receipt],
439
+ [decodeBotSettingsViewV1, view],
440
+ [decodeBotConfigurationExecuteRpcV1, rpc],
441
+ ] as const) {
442
+ const hidden = { ...candidate } as Record<PropertyKey, unknown>;
443
+ Object.defineProperty(hidden, "hidden", { value: true });
444
+ expect(() => decode(hidden)).toThrow(ConfigurationDecodeError);
445
+ const symbol = { ...candidate, [Symbol("extra")]: true };
446
+ expect(() => decode(symbol)).toThrow(ConfigurationDecodeError);
447
+ }
448
+ const hiddenTarget = { ...assignment };
449
+ Object.defineProperty(hiddenTarget, "hidden", { value: true });
450
+ expect(() =>
451
+ decodeConfigurationCommandV1({ ...command, assignment: hiddenTarget }),
452
+ ).toThrow(ConfigurationDecodeError);
453
+ const symbolTarget = { ...assignment, [Symbol("extra")]: true };
454
+ expect(() =>
455
+ decodeBotSettingsViewV1({
456
+ ...view,
457
+ assignmentOperations: [
458
+ { ...view.assignmentOperations[0], target: symbolTarget },
459
+ ],
460
+ }),
461
+ ).toThrow(ConfigurationDecodeError);
462
+ });
463
+
464
+ test("rejects unknown configuration RPC envelope fields", () => {
465
+ for (const decode of [
466
+ () =>
467
+ decodeUserConfigurationReadRpcV1({
468
+ schemaVersion: 1,
469
+ userId: "user-1",
470
+ extra: true,
471
+ }),
472
+ () =>
473
+ decodeUserConfigurationExecuteRpcV1({
474
+ schemaVersion: 1,
475
+ userId: "user-1",
476
+ command: {
477
+ schemaVersion: 1,
478
+ type: "user/update-profile",
479
+ commandId: "profile-1",
480
+ expectedRevision: 0,
481
+ profile: { name: "Alice" },
482
+ },
483
+ extra: true,
484
+ }),
485
+ () =>
486
+ decodeBotConfigurationReadRpcV1({
487
+ schemaVersion: 1,
488
+ userId: "user-1",
489
+ botId: "primary",
490
+ extra: true,
491
+ }),
492
+ () =>
493
+ decodeBotConfigurationExecuteRpcV1({
494
+ schemaVersion: 1,
495
+ userId: "user-1",
496
+ botId: "primary",
497
+ command: {
498
+ schemaVersion: 1,
499
+ type: "bot/update-profile",
500
+ commandId: "profile-1",
501
+ botId: "primary",
502
+ expectedRevision: 0,
503
+ profile: { name: "Primary" },
504
+ },
505
+ extra: true,
506
+ }),
507
+ ]) {
508
+ expect(decode).toThrow(ConfigurationDecodeError);
509
+ }
510
+ });
511
+
512
+ test("decodes configuration RPC authority before commands", () => {
513
+ expect(
514
+ decodeUserConfigurationExecuteRpcV1({
515
+ schemaVersion: 1,
516
+ userId: "user-1",
517
+ command: {
518
+ schemaVersion: 1,
519
+ type: "user/update-profile",
520
+ commandId: "profile-1",
521
+ expectedRevision: 0,
522
+ profile: { name: "Alice" },
523
+ },
524
+ }),
525
+ ).toMatchObject({
526
+ userId: "user-1",
527
+ command: { profile: { name: "Alice" } },
528
+ });
529
+ expect(() =>
530
+ decodeUserConfigurationExecuteRpcV1({
531
+ schemaVersion: 1,
532
+ userId: "user-1",
533
+ command: {
534
+ schemaVersion: 1,
535
+ type: "user/update-profile",
536
+ commandId: "profile-1",
537
+ expectedRevision: 0,
538
+ profile: { name: 42 },
539
+ },
540
+ }),
541
+ ).toThrow(ConfigurationDecodeError);
542
+ expect(() =>
543
+ decodeBotConfigurationExecuteRpcV1({
544
+ schemaVersion: 1,
545
+ userId: "user-1",
546
+ botId: "primary",
547
+ command: {
548
+ schemaVersion: 1,
549
+ type: "bot/update-profile",
550
+ commandId: "profile-1",
551
+ botId: "other",
552
+ expectedRevision: 0,
553
+ profile: { name: "Other" },
554
+ },
555
+ }),
556
+ ).toThrow("does not match its authority");
557
+ });
558
+
559
+ test("decodes server projections and rejects malformed nested values", () => {
560
+ expect(
561
+ decodeOperationReceiptV1({
562
+ schemaVersion: 1,
563
+ commandId: "command-1",
564
+ revision: 2,
565
+ status: "applied",
566
+ }),
567
+ ).toEqual({
568
+ schemaVersion: 1,
569
+ commandId: "command-1",
570
+ revision: 2,
571
+ status: "applied",
572
+ });
573
+ expect(
574
+ decodeOperationReceiptV1({
575
+ schemaVersion: 1,
576
+ commandId: "command-2",
577
+ revision: 2,
578
+ status: "rejected",
579
+ failure: "Capability requires a Connection",
580
+ }),
581
+ ).toEqual({
582
+ schemaVersion: 1,
583
+ commandId: "command-2",
584
+ revision: 2,
585
+ status: "rejected",
586
+ failure: "Capability requires a Connection",
587
+ });
588
+ expect(() =>
589
+ decodeUserSettingsViewV1({
590
+ schemaVersion: 1,
591
+ revision: 1,
592
+ profile: { name: "User" },
593
+ packages: [],
594
+ connections: [
595
+ {
596
+ connectionId: "gmail",
597
+ packageId: "composio",
598
+ connectionTypeId: "gmail",
599
+ displayName: "Gmail",
600
+ state: "ready",
601
+ safeMetadata: { unsafe: undefined },
602
+ },
603
+ ],
604
+ }),
605
+ ).toThrow(ConfigurationDecodeError);
606
+ expect(() =>
607
+ decodeUserSettingsViewV1({
608
+ schemaVersion: 1,
609
+ revision: 0,
610
+ profile: { name: "User" },
611
+ packages: [],
612
+ connections: Array.from({ length: 101 }, (_, index) => ({
613
+ connectionId: `connection-${index}`,
614
+ packageId: "provider-ollama-cloud",
615
+ connectionTypeId: "ollama-cloud-account",
616
+ displayName: `Connection ${index}`,
617
+ state: "ready",
618
+ safeMetadata: {},
619
+ })),
620
+ }),
621
+ ).toThrow(ConfigurationDecodeError);
622
+ // Values read over Durable Object RPC carry a `Symbol.dispose` own key;
623
+ // symbol keys are not fields and must not fail the exact-field check.
624
+ const disposableView = {
625
+ schemaVersion: 1,
626
+ revision: 0,
627
+ profile: { name: "User" },
628
+ packages: [],
629
+ connections: [],
630
+ [Symbol.dispose]: () => undefined,
631
+ };
632
+ expect(decodeUserSettingsViewV1(disposableView)).toEqual({
633
+ schemaVersion: 1,
634
+ revision: 0,
635
+ profile: { name: "User" },
636
+ packages: [],
637
+ connections: [],
638
+ });
639
+ for (const value of [
640
+ {
641
+ schemaVersion: 1,
642
+ revision: 0,
643
+ profile: { name: "User" },
644
+ packages: [],
645
+ connections: [],
646
+ extra: true,
647
+ },
648
+ {
649
+ schemaVersion: 1,
650
+ revision: 0,
651
+ profile: { name: "User", extra: true },
652
+ packages: [],
653
+ connections: [],
654
+ },
655
+ {
656
+ schemaVersion: 1,
657
+ revision: 0,
658
+ profile: { name: "User" },
659
+ packages: [
660
+ {
661
+ packageId: "composio",
662
+ version: "0.0.1",
663
+ state: "installed",
664
+ extra: true,
665
+ },
666
+ ],
667
+ connections: [],
668
+ },
669
+ {
670
+ schemaVersion: 1,
671
+ commandId: "command-1",
672
+ revision: 1,
673
+ status: "applied",
674
+ failure: "must not be accepted",
675
+ },
676
+ ]) {
677
+ expect(() =>
678
+ "commandId" in value
679
+ ? decodeOperationReceiptV1(value)
680
+ : decodeUserSettingsViewV1(value),
681
+ ).toThrow(ConfigurationDecodeError);
682
+ }
683
+ });
684
+ });
685
+
686
+ describe("Bot execution-plan authority", () => {
687
+ test("copies the User model template only when Bot settings initialize", () => {
688
+ const template = {
689
+ connectionId: "provider-1",
690
+ providerModelId: "model-1",
691
+ };
692
+ const initialized = initializeBotSettingsV1("primary", template);
693
+ template.providerModelId = "model-2";
694
+ expect(initialized.model).toEqual({
695
+ connectionId: "provider-1",
696
+ providerModelId: "model-1",
697
+ });
698
+ expect(initialized.notifications).toEqual({ enabled: true });
699
+ });
700
+
701
+ const bot = {
702
+ schemaVersion: 1 as const,
703
+ botId: "primary",
704
+ revision: 4,
705
+ profile: { name: "Primary" },
706
+ notifications: { enabled: false },
707
+ assignments: [
708
+ {
709
+ assignmentId: "gmail-assignment",
710
+ packageId: "composio",
711
+ capabilityId: "gmail-tools",
712
+ connectionId: "gmail-connection",
713
+ state: "enabled" as const,
714
+ },
715
+ ],
716
+ assignmentOperations: [],
717
+ };
718
+ const packages = [
719
+ {
720
+ packageId: "composio",
721
+ version: "0.0.1",
722
+ capabilities: [{ id: "gmail-tools", connectionTypes: ["gmail"] }],
723
+ connectionTypes: [{ id: "gmail", capabilities: ["gmail-tools"] }],
724
+ },
725
+ ];
726
+
727
+ test("resolves a Bot model through its explicit ready Connection", () => {
728
+ const user = {
729
+ schemaVersion: 1 as const,
730
+ revision: 1,
731
+ profile: { name: "User" },
732
+ packages: [
733
+ {
734
+ packageId: "provider-ollama-cloud",
735
+ version: "0.0.1",
736
+ state: "installed" as const,
737
+ },
738
+ ],
739
+ connections: [
740
+ {
741
+ connectionId: "ollama-work",
742
+ packageId: "provider-ollama-cloud",
743
+ connectionTypeId: "ollama-cloud-account",
744
+ displayName: "Work",
745
+ state: "ready" as const,
746
+ providerType: "ollama-cloud",
747
+ modelCatalog: {
748
+ schemaVersion: 1 as const,
749
+ generation: "catalog-1",
750
+ state: "fresh" as const,
751
+ models: [
752
+ {
753
+ providerModelId: "glm-5.3-flash:cloud",
754
+ displayName: "GLM 5.3 Flash",
755
+ capabilities: {
756
+ tools: true,
757
+ vision: false,
758
+ reasoning: true,
759
+ },
760
+ source: "discovered" as const,
761
+ },
762
+ ],
763
+ },
764
+ safeMetadata: {},
765
+ },
766
+ ],
767
+ };
768
+ const modelPackages = [
769
+ {
770
+ packageId: "provider-ollama-cloud",
771
+ version: "0.0.1",
772
+ capabilities: [
773
+ {
774
+ id: "ollama-cloud-models",
775
+ kind: "model" as const,
776
+ connectionTypes: ["ollama-cloud-account"],
777
+ },
778
+ ],
779
+ connectionTypes: [
780
+ {
781
+ id: "ollama-cloud-account",
782
+ capabilities: ["ollama-cloud-models"],
783
+ },
784
+ ],
785
+ },
786
+ ];
787
+ const assignments = [
788
+ {
789
+ assignmentId: "ollama-model",
790
+ packageId: "provider-ollama-cloud",
791
+ capabilityId: "ollama-cloud-models",
792
+ connectionId: "ollama-work",
793
+ state: "enabled" as const,
794
+ },
795
+ ];
796
+
797
+ expect(
798
+ resolveBotModelBindingV1({
799
+ model: {
800
+ connectionId: "ollama-work",
801
+ providerModelId: "glm-5.3-flash:cloud",
802
+ },
803
+ assignments,
804
+ user,
805
+ packages: modelPackages,
806
+ }),
807
+ ).toMatchObject({
808
+ state: "ready",
809
+ providerType: "ollama-cloud",
810
+ packageId: "provider-ollama-cloud",
811
+ });
812
+ expect(
813
+ resolveBotModelBindingV1({
814
+ model: {
815
+ connectionId: "ollama-work",
816
+ providerModelId: "glm-5.3-flash:cloud",
817
+ },
818
+ assignments,
819
+ user,
820
+ packages: modelPackages.map((pkg) => ({ ...pkg, version: "0.0.2" })),
821
+ }).state,
822
+ ).toBe("unavailable");
823
+ expect(
824
+ resolveBotModelBindingV1({
825
+ model: {
826
+ connectionId: "ollama-work",
827
+ providerModelId: "new-model:cloud",
828
+ },
829
+ assignments,
830
+ user,
831
+ packages: modelPackages,
832
+ }).state,
833
+ ).toBe("requires-resolution");
834
+ expect(
835
+ resolveBotModelBindingV1({
836
+ model: {
837
+ connectionId: "ollama-work",
838
+ providerModelId: "glm-5.3-flash:cloud",
839
+ },
840
+ assignments: [],
841
+ user,
842
+ packages: modelPackages,
843
+ }),
844
+ ).toMatchObject({
845
+ state: "unavailable",
846
+ failure: "Bot is not assigned the Connection model capability",
847
+ });
848
+ });
849
+
850
+ test("requires an enabled installation, declared capability, and ready typed Connection", () => {
851
+ const user = {
852
+ schemaVersion: 1 as const,
853
+ revision: 2,
854
+ profile: { name: "User" },
855
+ packages: [
856
+ {
857
+ packageId: "composio",
858
+ version: "0.0.1",
859
+ state: "installed" as const,
860
+ },
861
+ ],
862
+ connections: [
863
+ {
864
+ connectionId: "gmail-connection",
865
+ packageId: "composio",
866
+ connectionTypeId: "gmail",
867
+ displayName: "Gmail",
868
+ state: "ready" as const,
869
+ safeMetadata: {},
870
+ },
871
+ ],
872
+ };
873
+ expect(
874
+ resolveBotExecutionPlanV1({ bot, user, packages }).assignments[0]?.state,
875
+ ).toBe("enabled");
876
+ expect(
877
+ resolveBotExecutionPlanV1({
878
+ bot: {
879
+ ...bot,
880
+ assignments: [{ ...bot.assignments[0]!, capabilityId: "anything" }],
881
+ },
882
+ user,
883
+ packages,
884
+ }).assignments[0]?.state,
885
+ ).toBe("unavailable");
886
+ expect(
887
+ capabilityAssignmentFailureV1({
888
+ assignment: { ...bot.assignments[0]!, connectionId: undefined },
889
+ user,
890
+ packages,
891
+ }),
892
+ ).toContain("requires a Connection");
893
+ expect(
894
+ capabilityAssignmentFailureV1({
895
+ assignment: { ...bot.assignments[0]!, capabilityId: "anything" },
896
+ user,
897
+ packages,
898
+ }),
899
+ ).toContain("is not declared");
900
+ expect(
901
+ resolveBotExecutionPlanV1({
902
+ bot,
903
+ user: {
904
+ ...user,
905
+ packages: [{ ...user.packages[0]!, state: "disabled" }],
906
+ },
907
+ packages,
908
+ }).assignments[0]?.state,
909
+ ).toBe("unavailable");
910
+ expect(
911
+ resolveBotExecutionPlanV1({
912
+ bot,
913
+ user: {
914
+ ...user,
915
+ connections: [
916
+ { ...user.connections[0]!, connectionTypeId: "calendar" },
917
+ ],
918
+ },
919
+ packages,
920
+ }).assignments[0]?.state,
921
+ ).toBe("unavailable");
922
+ });
923
+ });
924
+
925
+ const BOOTSTRAP_GENERATION = "2026-08-31T00:00:00.000Z:0123456789abcdef";
926
+ const AUTHORED_GENERATION = "2026-09-01T00:00:00.000Z:fedcba9876543210";
927
+
928
+ const authoredGenerationView = {
929
+ schemaVersion: 1,
930
+ botId: "alpha",
931
+ generationId: AUTHORED_GENERATION,
932
+ createdAt: "2026-09-01T00:00:00.000Z",
933
+ status: "active",
934
+ isCurrent: true,
935
+ failures: [],
936
+ parentGenerationId: BOOTSTRAP_GENERATION,
937
+ origin: {
938
+ kind: "bot-authored",
939
+ runId: "run-1",
940
+ sessionId: "alice:alpha",
941
+ turnId: "turn-1",
942
+ },
943
+ members: [
944
+ {
945
+ packageId: "shell",
946
+ version: "0.0.1",
947
+ provenance: { kind: "first-party" },
948
+ },
949
+ {
950
+ packageId: "greeter",
951
+ version: "0.0.1",
952
+ contentHash: "b".repeat(64),
953
+ source: "export default {}",
954
+ provenance: {
955
+ kind: "bot",
956
+ botId: "alpha",
957
+ sessionId: "alice:alpha",
958
+ turnId: "turn-1",
959
+ runId: "run-1",
960
+ authoredAt: "2026-09-01T00:00:00.000Z",
961
+ },
962
+ },
963
+ ],
964
+ };
965
+
966
+ describe("Composition generation views", () => {
967
+ test("decodes a generation with Bot provenance and artifact identity", () => {
968
+ const view = decodeCompositionGenerationViewV1(authoredGenerationView);
969
+ expect(view.members[1]?.contentHash).toBe("b".repeat(64));
970
+ expect(view.members[1]?.provenance).toEqual({
971
+ kind: "bot",
972
+ botId: "alpha",
973
+ sessionId: "alice:alpha",
974
+ turnId: "turn-1",
975
+ runId: "run-1",
976
+ authoredAt: "2026-09-01T00:00:00.000Z",
977
+ });
978
+ expect(view.isCurrent).toBe(true);
979
+ expect(view.parentGenerationId).toBe(BOOTSTRAP_GENERATION);
980
+ });
981
+
982
+ test("refuses fields the redacted view does not declare", () => {
983
+ for (const invalid of [
984
+ { ...authoredGenerationView, artifactSetHash: "a".repeat(64) },
985
+ { ...authoredGenerationView, status: "unknown" },
986
+ { ...authoredGenerationView, isCurrent: "yes" },
987
+ {
988
+ ...authoredGenerationView,
989
+ members: [
990
+ {
991
+ packageId: "greeter",
992
+ version: "0.0.1",
993
+ provenance: { kind: "first-party" },
994
+ bytes: "AAAA",
995
+ },
996
+ ],
997
+ },
998
+ {
999
+ ...authoredGenerationView,
1000
+ members: [
1001
+ {
1002
+ packageId: "greeter",
1003
+ version: "0.0.1",
1004
+ provenance: { kind: "first-party" },
1005
+ contentHash: "not-a-hash",
1006
+ },
1007
+ ],
1008
+ },
1009
+ {
1010
+ ...authoredGenerationView,
1011
+ members: [
1012
+ authoredGenerationView.members[0],
1013
+ authoredGenerationView.members[0],
1014
+ ],
1015
+ },
1016
+ ]) {
1017
+ expect(() => decodeCompositionGenerationViewV1(invalid)).toThrow(
1018
+ ConfigurationDecodeError,
1019
+ );
1020
+ }
1021
+ });
1022
+
1023
+ test("decodes a list and refuses a generation belonging to another Bot", () => {
1024
+ const list = decodeCompositionGenerationListViewV1({
1025
+ schemaVersion: 1,
1026
+ botId: "alpha",
1027
+ currentGenerationId: AUTHORED_GENERATION,
1028
+ generations: [authoredGenerationView],
1029
+ cursor: "composition:index:x",
1030
+ });
1031
+ expect(list.generations).toHaveLength(1);
1032
+ expect(list.cursor).toBe("composition:index:x");
1033
+ expect(() =>
1034
+ decodeCompositionGenerationListViewV1({
1035
+ schemaVersion: 1,
1036
+ botId: "beta",
1037
+ currentGenerationId: AUTHORED_GENERATION,
1038
+ generations: [authoredGenerationView],
1039
+ }),
1040
+ ).toThrow(ConfigurationDecodeError);
1041
+ });
1042
+
1043
+ test("decodes revert commands and their receipts", () => {
1044
+ const command = {
1045
+ schemaVersion: 1,
1046
+ type: "composition/revert",
1047
+ commandId: "composition-revert-1",
1048
+ botId: "alpha",
1049
+ toGenerationId: BOOTSTRAP_GENERATION,
1050
+ expectedGenerationId: AUTHORED_GENERATION,
1051
+ } as const;
1052
+ expect(decodeRevertCompositionCommandV1(command)).toEqual(command);
1053
+ expect(() =>
1054
+ decodeRevertCompositionCommandV1({
1055
+ ...command,
1056
+ expectedGenerationId: BOOTSTRAP_GENERATION,
1057
+ }),
1058
+ ).toThrow(ConfigurationDecodeError);
1059
+
1060
+ expect(
1061
+ decodeCompositionCommandReceiptV1({
1062
+ schemaVersion: 1,
1063
+ commandId: "composition-revert-1",
1064
+ status: "applied",
1065
+ generationId: "2026-09-02T00:00:00.000Z:0123456789abcdef",
1066
+ currentGenerationId: AUTHORED_GENERATION,
1067
+ }).status,
1068
+ ).toBe("applied");
1069
+ expect(
1070
+ decodeCompositionCommandReceiptV1({
1071
+ schemaVersion: 1,
1072
+ commandId: "composition-revert-1",
1073
+ status: "rejected",
1074
+ failure: "composition generation is newer",
1075
+ currentGenerationId: AUTHORED_GENERATION,
1076
+ }).status,
1077
+ ).toBe("rejected");
1078
+ expect(() =>
1079
+ decodeCompositionCommandReceiptV1({
1080
+ schemaVersion: 1,
1081
+ commandId: "composition-revert-1",
1082
+ status: "applied",
1083
+ currentGenerationId: AUTHORED_GENERATION,
1084
+ }),
1085
+ ).toThrow(ConfigurationDecodeError);
1086
+ });
1087
+ });
1088
+
1089
+ describe("effective Bot model resolution", () => {
1090
+ const modelPackages = [
1091
+ {
1092
+ packageId: "provider-ollama-cloud",
1093
+ version: "0.0.1",
1094
+ capabilities: [
1095
+ {
1096
+ id: "ollama-cloud-models",
1097
+ kind: "model" as const,
1098
+ connectionTypes: ["ollama-cloud-account"],
1099
+ },
1100
+ ],
1101
+ connectionTypes: [
1102
+ { id: "ollama-cloud-account", capabilities: ["ollama-cloud-models"] },
1103
+ ],
1104
+ },
1105
+ ];
1106
+ function user(
1107
+ newBotModelTemplate?: ModelAssignment,
1108
+ ): Parameters<typeof resolveEffectiveBotModelV1>[0]["user"] {
1109
+ return {
1110
+ schemaVersion: 1,
1111
+ revision: 1,
1112
+ profile: { name: "User" },
1113
+ packages: [
1114
+ {
1115
+ packageId: "provider-ollama-cloud",
1116
+ version: "0.0.1",
1117
+ state: "installed",
1118
+ },
1119
+ ],
1120
+ connections: [
1121
+ {
1122
+ connectionId: "ollama-work",
1123
+ packageId: "provider-ollama-cloud",
1124
+ connectionTypeId: "ollama-cloud-account",
1125
+ displayName: "Work",
1126
+ state: "ready",
1127
+ providerType: "ollama-cloud",
1128
+ modelCatalog: {
1129
+ schemaVersion: 1,
1130
+ generation: "catalog-1",
1131
+ state: "fresh",
1132
+ models: [
1133
+ {
1134
+ providerModelId: "glm-5.3-flash:cloud",
1135
+ displayName: "GLM 5.3 Flash",
1136
+ capabilities: { tools: true, vision: false, reasoning: true },
1137
+ source: "discovered",
1138
+ },
1139
+ {
1140
+ providerModelId: "llama-3:cloud",
1141
+ displayName: "Llama 3",
1142
+ capabilities: { tools: true, vision: false, reasoning: false },
1143
+ source: "discovered",
1144
+ },
1145
+ ],
1146
+ },
1147
+ safeMetadata: {},
1148
+ },
1149
+ ],
1150
+ ...(newBotModelTemplate
1151
+ ? { newBotModelTemplate, newBotModelTemplateSource: "auto" as const }
1152
+ : {}),
1153
+ };
1154
+ }
1155
+ const assignments = [
1156
+ {
1157
+ assignmentId: "ollama-model",
1158
+ packageId: "provider-ollama-cloud",
1159
+ capabilityId: "ollama-cloud-models",
1160
+ connectionId: "ollama-work",
1161
+ state: "enabled" as const,
1162
+ },
1163
+ ];
1164
+
1165
+ test("follows the User default when the Bot has no model of its own", () => {
1166
+ const effective = resolveEffectiveBotModelV1({
1167
+ bot: { assignments },
1168
+ user: user({
1169
+ connectionId: "ollama-work",
1170
+ providerModelId: "llama-3:cloud",
1171
+ }),
1172
+ packages: modelPackages,
1173
+ });
1174
+ expect(effective.source).toBe("default");
1175
+ expect(effective.model).toEqual({
1176
+ connectionId: "ollama-work",
1177
+ providerModelId: "llama-3:cloud",
1178
+ });
1179
+ expect(effective.binding?.state).toBe("ready");
1180
+ });
1181
+
1182
+ test("prefers the Bot override over the User default", () => {
1183
+ const effective = resolveEffectiveBotModelV1({
1184
+ bot: {
1185
+ model: {
1186
+ connectionId: "ollama-work",
1187
+ providerModelId: "glm-5.3-flash:cloud",
1188
+ },
1189
+ assignments,
1190
+ },
1191
+ user: user({
1192
+ connectionId: "ollama-work",
1193
+ providerModelId: "llama-3:cloud",
1194
+ }),
1195
+ packages: modelPackages,
1196
+ });
1197
+ expect(effective.source).toBe("bot");
1198
+ expect(effective.model?.providerModelId).toBe("glm-5.3-flash:cloud");
1199
+ expect(effective.binding?.state).toBe("ready");
1200
+ });
1201
+
1202
+ test("reports no model when neither the Bot nor the User names one", () => {
1203
+ expect(
1204
+ resolveEffectiveBotModelV1({
1205
+ bot: { assignments },
1206
+ user: user(),
1207
+ packages: modelPackages,
1208
+ }),
1209
+ ).toEqual({ source: "none" });
1210
+ });
1211
+
1212
+ test("keeps the default fail-closed until the Bot claims the Assignment", () => {
1213
+ const effective = resolveEffectiveBotModelV1({
1214
+ bot: { assignments: [] },
1215
+ user: user({
1216
+ connectionId: "ollama-work",
1217
+ providerModelId: "llama-3:cloud",
1218
+ }),
1219
+ packages: modelPackages,
1220
+ });
1221
+ expect(effective.source).toBe("default");
1222
+ expect(effective.binding).toMatchObject({
1223
+ state: "unavailable",
1224
+ failure: "Bot is not assigned the Connection model capability",
1225
+ });
1226
+ });
1227
+ });
1228
+
1229
+ describe("Catalog installs and uninstall", () => {
1230
+ const meta = {
1231
+ schemaVersion: 1 as const,
1232
+ commandId: "install-1",
1233
+ expectedRevision: 0,
1234
+ };
1235
+
1236
+ test("decodes a Catalog install with its generation and values", () => {
1237
+ expect(
1238
+ decodeConfigurationCommandV1({
1239
+ ...meta,
1240
+ type: "user/install-package",
1241
+ packageId: "mcp-weather",
1242
+ version: "0.0.1",
1243
+ catalogId: "mcp-weather",
1244
+ catalogGeneration: "gen-one",
1245
+ values: { region: "au" },
1246
+ }),
1247
+ ).toEqual({
1248
+ ...meta,
1249
+ type: "user/install-package",
1250
+ packageId: "mcp-weather",
1251
+ version: "0.0.1",
1252
+ catalogId: "mcp-weather",
1253
+ catalogGeneration: "gen-one",
1254
+ values: { region: "au" },
1255
+ });
1256
+ });
1257
+
1258
+ test("keeps the compiled-in install exactly as it was", () => {
1259
+ expect(
1260
+ decodeConfigurationCommandV1({
1261
+ ...meta,
1262
+ type: "user/install-package",
1263
+ packageId: "clock",
1264
+ version: "0.0.1",
1265
+ }),
1266
+ ).toEqual({
1267
+ ...meta,
1268
+ type: "user/install-package",
1269
+ packageId: "clock",
1270
+ version: "0.0.1",
1271
+ });
1272
+ });
1273
+
1274
+ test("refuses half a Catalog install", () => {
1275
+ expect(() =>
1276
+ decodeConfigurationCommandV1({
1277
+ ...meta,
1278
+ type: "user/install-package",
1279
+ packageId: "mcp-weather",
1280
+ version: "0.0.1",
1281
+ catalogId: "mcp-weather",
1282
+ }),
1283
+ ).toThrow("requires both catalogId and catalogGeneration");
1284
+ expect(() =>
1285
+ decodeConfigurationCommandV1({
1286
+ ...meta,
1287
+ type: "user/install-package",
1288
+ packageId: "clock",
1289
+ version: "0.0.1",
1290
+ values: { region: "au" },
1291
+ }),
1292
+ ).toThrow("install values require a Catalog entry");
1293
+ });
1294
+
1295
+ test("refuses install values that are not bounded JSON", () => {
1296
+ expect(() =>
1297
+ decodeConfigurationCommandV1({
1298
+ ...meta,
1299
+ type: "user/install-package",
1300
+ packageId: "mcp-weather",
1301
+ version: "0.0.1",
1302
+ catalogId: "mcp-weather",
1303
+ catalogGeneration: "gen-one",
1304
+ values: { region: "x".repeat(20_000) },
1305
+ }),
1306
+ ).toThrow("values is too large");
1307
+ });
1308
+
1309
+ test("decodes uninstall and refuses an unknown field on it", () => {
1310
+ expect(
1311
+ decodeConfigurationCommandV1({
1312
+ ...meta,
1313
+ commandId: "uninstall-1",
1314
+ type: "user/uninstall-package",
1315
+ packageId: "mcp-weather",
1316
+ }),
1317
+ ).toEqual({
1318
+ ...meta,
1319
+ commandId: "uninstall-1",
1320
+ type: "user/uninstall-package",
1321
+ packageId: "mcp-weather",
1322
+ });
1323
+ expect(() =>
1324
+ decodeConfigurationCommandV1({
1325
+ ...meta,
1326
+ commandId: "uninstall-1",
1327
+ type: "user/uninstall-package",
1328
+ packageId: "mcp-weather",
1329
+ cascade: true,
1330
+ }),
1331
+ ).toThrow(ConfigurationDecodeError);
1332
+ });
1333
+
1334
+ test("the User settings view accepts an absent Catalog pin and a whole one", () => {
1335
+ const base = {
1336
+ schemaVersion: 1 as const,
1337
+ revision: 3,
1338
+ profile: { name: "Tim" },
1339
+ packages: [],
1340
+ connections: [],
1341
+ };
1342
+ expect(decodeUserSettingsViewV1(base)).not.toHaveProperty(
1343
+ "catalogGeneration",
1344
+ );
1345
+ expect(
1346
+ decodeUserSettingsViewV1({
1347
+ ...base,
1348
+ catalogGeneration: "gen-one",
1349
+ catalogIndexHash: "a".repeat(64),
1350
+ }),
1351
+ ).toMatchObject({ catalogGeneration: "gen-one" });
1352
+ // Half a pin is corrupt durable state, not a pin.
1353
+ expect(() =>
1354
+ decodeUserSettingsViewV1({ ...base, catalogGeneration: "gen-one" }),
1355
+ ).toThrow(ConfigurationDecodeError);
1356
+ });
1357
+
1358
+ test("an installation carries its Catalog provenance through the codec", () => {
1359
+ const installation = {
1360
+ packageId: "mcp-weather",
1361
+ version: "0.0.1",
1362
+ state: "installed" as const,
1363
+ catalogId: "mcp-weather",
1364
+ catalogGeneration: "gen-one",
1365
+ provenance: "catalog" as const,
1366
+ values: { region: "au" },
1367
+ };
1368
+ expect(
1369
+ decodeUserSettingsViewV1({
1370
+ schemaVersion: 1,
1371
+ revision: 1,
1372
+ profile: { name: "Tim" },
1373
+ packages: [installation],
1374
+ connections: [],
1375
+ }).packages,
1376
+ ).toEqual([installation]);
1377
+ expect(() =>
1378
+ decodeUserSettingsViewV1({
1379
+ schemaVersion: 1,
1380
+ revision: 1,
1381
+ profile: { name: "Tim" },
1382
+ packages: [{ ...installation, provenance: "bot" }],
1383
+ connections: [],
1384
+ }),
1385
+ ).toThrow("provenance is invalid");
1386
+ });
1387
+ });