@peers-app/peers-sdk 0.27.2 → 0.27.4

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.
Files changed (260) hide show
  1. package/dist/contracts/contract-proxy.d.ts +7 -0
  2. package/dist/contracts/contract-proxy.js +19 -2
  3. package/dist/contracts/create-local-consumer.js +16 -1
  4. package/dist/contracts/index.d.ts +2 -0
  5. package/dist/contracts/index.js +2 -0
  6. package/dist/contracts/system/index.d.ts +2 -0
  7. package/dist/contracts/system/index.js +2 -0
  8. package/dist/contracts/system/invites-contract.js +2 -1
  9. package/dist/contracts/system/sync-check-contract.d.ts +97 -0
  10. package/dist/contracts/system/sync-check-contract.js +115 -0
  11. package/dist/contracts/tool-arguments.d.ts +15 -0
  12. package/dist/contracts/tool-arguments.js +41 -0
  13. package/dist/contracts/tool-catalog-service.js +2 -12
  14. package/dist/data/change-tracking.d.ts +40 -0
  15. package/dist/data/change-tracking.js +89 -0
  16. package/dist/data/orm/types.d.ts +24 -7
  17. package/dist/data/orm/types.js +97 -24
  18. package/dist/data/persistent-vars.d.ts +5 -0
  19. package/dist/data/persistent-vars.js +24 -6
  20. package/dist/define/manifest.js +6 -34
  21. package/dist/package-loader/package-loader.js +27 -22
  22. package/dist/system-ids.d.ts +5 -0
  23. package/dist/system-ids.js +6 -1
  24. package/package.json +4 -2
  25. package/dist/context/user-context-identity.test.d.ts +0 -1
  26. package/dist/context/user-context-identity.test.js +0 -220
  27. package/dist/contracts/__tests__/action-dispatch.test.d.ts +0 -1
  28. package/dist/contracts/__tests__/action-dispatch.test.js +0 -319
  29. package/dist/contracts/__tests__/also-implements.test.d.ts +0 -1
  30. package/dist/contracts/__tests__/also-implements.test.js +0 -110
  31. package/dist/contracts/__tests__/builder.test.d.ts +0 -1
  32. package/dist/contracts/__tests__/builder.test.js +0 -669
  33. package/dist/contracts/__tests__/contract-events.test.d.ts +0 -1
  34. package/dist/contracts/__tests__/contract-events.test.js +0 -199
  35. package/dist/contracts/__tests__/contract-observables.test.d.ts +0 -1
  36. package/dist/contracts/__tests__/contract-observables.test.js +0 -290
  37. package/dist/contracts/__tests__/contract-provider-router.test.d.ts +0 -1
  38. package/dist/contracts/__tests__/contract-provider-router.test.js +0 -315
  39. package/dist/contracts/__tests__/contract-proxy.test.d.ts +0 -1
  40. package/dist/contracts/__tests__/contract-proxy.test.js +0 -657
  41. package/dist/contracts/__tests__/contract-resolvers.test.d.ts +0 -1
  42. package/dist/contracts/__tests__/contract-resolvers.test.js +0 -758
  43. package/dist/contracts/__tests__/contract-subscription-lifecycle.test.d.ts +0 -1
  44. package/dist/contracts/__tests__/contract-subscription-lifecycle.test.js +0 -353
  45. package/dist/contracts/__tests__/contract-table-events.test.d.ts +0 -1
  46. package/dist/contracts/__tests__/contract-table-events.test.js +0 -209
  47. package/dist/contracts/__tests__/contract-transport.test.d.ts +0 -1
  48. package/dist/contracts/__tests__/contract-transport.test.js +0 -163
  49. package/dist/contracts/__tests__/extract.test.d.ts +0 -1
  50. package/dist/contracts/__tests__/extract.test.js +0 -145
  51. package/dist/contracts/__tests__/frozen-contracts.test.d.ts +0 -1
  52. package/dist/contracts/__tests__/frozen-contracts.test.js +0 -191
  53. package/dist/contracts/__tests__/generic-tool-dispatch.test.d.ts +0 -1
  54. package/dist/contracts/__tests__/generic-tool-dispatch.test.js +0 -130
  55. package/dist/contracts/__tests__/host-contract-client.test.d.ts +0 -1
  56. package/dist/contracts/__tests__/host-contract-client.test.js +0 -793
  57. package/dist/contracts/__tests__/host-contract-consumer.test.d.ts +0 -1
  58. package/dist/contracts/__tests__/host-contract-consumer.test.js +0 -163
  59. package/dist/contracts/__tests__/host-contract-transport.test.d.ts +0 -1
  60. package/dist/contracts/__tests__/host-contract-transport.test.js +0 -66
  61. package/dist/contracts/__tests__/integration.test.d.ts +0 -1
  62. package/dist/contracts/__tests__/integration.test.js +0 -348
  63. package/dist/contracts/__tests__/legacy-removal.test.d.ts +0 -1
  64. package/dist/contracts/__tests__/legacy-removal.test.js +0 -128
  65. package/dist/contracts/__tests__/persistent-registry.test.d.ts +0 -1
  66. package/dist/contracts/__tests__/persistent-registry.test.js +0 -109
  67. package/dist/contracts/__tests__/playground.test.d.ts +0 -1
  68. package/dist/contracts/__tests__/playground.test.js +0 -216
  69. package/dist/contracts/__tests__/registry.test.d.ts +0 -1
  70. package/dist/contracts/__tests__/registry.test.js +0 -238
  71. package/dist/contracts/__tests__/remote-contract-transport.test.d.ts +0 -1
  72. package/dist/contracts/__tests__/remote-contract-transport.test.js +0 -119
  73. package/dist/contracts/__tests__/system-contracts.test.d.ts +0 -1
  74. package/dist/contracts/__tests__/system-contracts.test.js +0 -247
  75. package/dist/contracts/__tests__/target-dispatch.d.ts +0 -11
  76. package/dist/contracts/__tests__/target-dispatch.js +0 -24
  77. package/dist/contracts/__tests__/tool-catalog-inventory.test.d.ts +0 -1
  78. package/dist/contracts/__tests__/tool-catalog-inventory.test.js +0 -44
  79. package/dist/contracts/__tests__/tool-catalog-service.test.d.ts +0 -1
  80. package/dist/contracts/__tests__/tool-catalog-service.test.js +0 -482
  81. package/dist/contracts/__tests__/tool-catalog.test.d.ts +0 -1
  82. package/dist/contracts/__tests__/tool-catalog.test.js +0 -1199
  83. package/dist/contracts/__tests__/validate.test.d.ts +0 -1
  84. package/dist/contracts/__tests__/validate.test.js +0 -991
  85. package/dist/contracts/__tests__/workflow-context.test.d.ts +0 -1
  86. package/dist/contracts/__tests__/workflow-context.test.js +0 -50
  87. package/dist/data/access-level.test.d.ts +0 -1
  88. package/dist/data/access-level.test.js +0 -53
  89. package/dist/data/change-tracking.test.d.ts +0 -1
  90. package/dist/data/change-tracking.test.js +0 -62
  91. package/dist/data/files/files.test.d.ts +0 -1
  92. package/dist/data/files/files.test.js +0 -770
  93. package/dist/data/group-members.test.d.ts +0 -1
  94. package/dist/data/group-members.test.js +0 -336
  95. package/dist/data/group-share.test.d.ts +0 -1
  96. package/dist/data/group-share.test.js +0 -110
  97. package/dist/data/groups.test.d.ts +0 -1
  98. package/dist/data/groups.test.js +0 -153
  99. package/dist/data/orm/cursor.test.d.ts +0 -1
  100. package/dist/data/orm/cursor.test.js +0 -314
  101. package/dist/data/orm/data-query.mongo.test.d.ts +0 -1
  102. package/dist/data/orm/data-query.mongo.test.js +0 -367
  103. package/dist/data/orm/data-query.sqlite.test.d.ts +0 -1
  104. package/dist/data/orm/data-query.sqlite.test.js +0 -365
  105. package/dist/data/orm/data-query.test.d.ts +0 -1
  106. package/dist/data/orm/data-query.test.js +0 -545
  107. package/dist/data/orm/dependency-injection.test.d.ts +0 -1
  108. package/dist/data/orm/dependency-injection.test.js +0 -171
  109. package/dist/data/orm/event-registry.test.d.ts +0 -1
  110. package/dist/data/orm/event-registry.test.js +0 -44
  111. package/dist/data/orm/multi-cursors.test.d.ts +0 -1
  112. package/dist/data/orm/multi-cursors.test.js +0 -463
  113. package/dist/data/orm/sql.data-source.test.d.ts +0 -1
  114. package/dist/data/orm/sql.data-source.test.js +0 -731
  115. package/dist/data/orm/table-container-events.test.d.ts +0 -1
  116. package/dist/data/orm/table-container-events.test.js +0 -101
  117. package/dist/data/orm/table-container.test.d.ts +0 -1
  118. package/dist/data/orm/table-container.test.js +0 -273
  119. package/dist/data/orm/table.event-source.test.d.ts +0 -1
  120. package/dist/data/orm/table.event-source.test.js +0 -331
  121. package/dist/data/orm/types.test.d.ts +0 -1
  122. package/dist/data/orm/types.test.js +0 -409
  123. package/dist/data/package-table-identity.test.d.ts +0 -1
  124. package/dist/data/package-table-identity.test.js +0 -46
  125. package/dist/data/package-version-permissions.test.d.ts +0 -1
  126. package/dist/data/package-version-permissions.test.js +0 -107
  127. package/dist/data/package-version-resolver.test.d.ts +0 -4
  128. package/dist/data/package-version-resolver.test.js +0 -543
  129. package/dist/data/package-versions.test.d.ts +0 -1
  130. package/dist/data/package-versions.test.js +0 -227
  131. package/dist/data/persistent-vars.conflict.test.d.ts +0 -1
  132. package/dist/data/persistent-vars.conflict.test.js +0 -126
  133. package/dist/data/persistent-vars.migration.test.d.ts +0 -1
  134. package/dist/data/persistent-vars.migration.test.js +0 -133
  135. package/dist/data/persistent-vars.package-scope.test.d.ts +0 -1
  136. package/dist/data/persistent-vars.package-scope.test.js +0 -27
  137. package/dist/data/sync-recovery-queue.test.d.ts +0 -1
  138. package/dist/data/sync-recovery-queue.test.js +0 -215
  139. package/dist/data/table-signing.test.d.ts +0 -1
  140. package/dist/data/table-signing.test.js +0 -118
  141. package/dist/data/tool-access.test.d.ts +0 -1
  142. package/dist/data/tool-access.test.js +0 -166
  143. package/dist/data/user-permissions.test.d.ts +0 -1
  144. package/dist/data/user-permissions.test.js +0 -305
  145. package/dist/data/user-trust-levels.test.d.ts +0 -1
  146. package/dist/data/user-trust-levels.test.js +0 -37
  147. package/dist/define/also-implements.test.d.ts +0 -1
  148. package/dist/define/also-implements.test.js +0 -123
  149. package/dist/define/declare-contract.test.d.ts +0 -1
  150. package/dist/define/declare-contract.test.js +0 -573
  151. package/dist/define/define.test.d.ts +0 -1
  152. package/dist/define/define.test.js +0 -1010
  153. package/dist/define/empty-fields.test.d.ts +0 -1
  154. package/dist/define/empty-fields.test.js +0 -55
  155. package/dist/define/events-observables.test.d.ts +0 -1
  156. package/dist/define/events-observables.test.js +0 -1013
  157. package/dist/define/serve-members.test.d.ts +0 -1
  158. package/dist/define/serve-members.test.js +0 -630
  159. package/dist/define/vars.test.d.ts +0 -1
  160. package/dist/define/vars.test.js +0 -39
  161. package/dist/device/binary-peer-connection-v2.test.d.ts +0 -1
  162. package/dist/device/binary-peer-connection-v2.test.js +0 -730
  163. package/dist/device/binary-peer-connection.test.d.ts +0 -1
  164. package/dist/device/binary-peer-connection.test.js +0 -184
  165. package/dist/device/connection.test.d.ts +0 -1
  166. package/dist/device/connection.test.js +0 -613
  167. package/dist/device/device-election.test.d.ts +0 -1
  168. package/dist/device/device-election.test.js +0 -516
  169. package/dist/device/device.test.d.ts +0 -1
  170. package/dist/device/device.test.js +0 -243
  171. package/dist/device/frame-cipher.test.d.ts +0 -1
  172. package/dist/device/frame-cipher.test.js +0 -219
  173. package/dist/device/get-trust-level-fn.test.d.ts +0 -1
  174. package/dist/device/get-trust-level-fn.test.js +0 -685
  175. package/dist/device/streamed-socket.test.d.ts +0 -1
  176. package/dist/device/streamed-socket.test.js +0 -216
  177. package/dist/device/tx-encoding.test.d.ts +0 -1
  178. package/dist/device/tx-encoding.test.js +0 -295
  179. package/dist/device-pairing/cpace.test.d.ts +0 -1
  180. package/dist/device-pairing/cpace.test.js +0 -116
  181. package/dist/device-pairing/device-pairing.test.d.ts +0 -1
  182. package/dist/device-pairing/device-pairing.test.js +0 -211
  183. package/dist/hlc.test.d.ts +0 -1
  184. package/dist/hlc.test.js +0 -45
  185. package/dist/host-credentials.test.d.ts +0 -1
  186. package/dist/host-credentials.test.js +0 -88
  187. package/dist/identity/account-email.test.d.ts +0 -1
  188. package/dist/identity/account-email.test.js +0 -59
  189. package/dist/identity/anchor-document.test.d.ts +0 -1
  190. package/dist/identity/anchor-document.test.js +0 -16
  191. package/dist/identity/anchor-resolution.test.d.ts +0 -1
  192. package/dist/identity/anchor-resolution.test.js +0 -130
  193. package/dist/identity/key-change.test.d.ts +0 -1
  194. package/dist/identity/key-change.test.js +0 -811
  195. package/dist/invites/group-approval.test.d.ts +0 -1
  196. package/dist/invites/group-approval.test.js +0 -223
  197. package/dist/invites/invite-token.test.d.ts +0 -1
  198. package/dist/invites/invite-token.test.js +0 -123
  199. package/dist/keys.test.d.ts +0 -1
  200. package/dist/keys.test.js +0 -336
  201. package/dist/logging/console-logger.test.d.ts +0 -1
  202. package/dist/logging/console-logger.test.js +0 -300
  203. package/dist/logging/console-logs.table.test.d.ts +0 -1
  204. package/dist/logging/console-logs.table.test.js +0 -156
  205. package/dist/observable.test.d.ts +0 -1
  206. package/dist/observable.test.js +0 -309
  207. package/dist/package-build/build-package.test.d.ts +0 -1
  208. package/dist/package-build/build-package.test.js +0 -204
  209. package/dist/package-build/contract-add.test.d.ts +0 -1
  210. package/dist/package-build/contract-add.test.js +0 -467
  211. package/dist/package-build/freeze-contracts.test.d.ts +0 -1
  212. package/dist/package-build/freeze-contracts.test.js +0 -355
  213. package/dist/package-installer/package-author-signing.test.d.ts +0 -1
  214. package/dist/package-installer/package-author-signing.test.js +0 -200
  215. package/dist/package-installer/package-cloner.test.d.ts +0 -1
  216. package/dist/package-installer/package-cloner.test.js +0 -278
  217. package/dist/package-installer/package-creator.test.d.ts +0 -1
  218. package/dist/package-installer/package-creator.test.js +0 -908
  219. package/dist/package-installer/package-dev-tools.test.d.ts +0 -1
  220. package/dist/package-installer/package-dev-tools.test.js +0 -64
  221. package/dist/package-installer/package-installer.test.d.ts +0 -1
  222. package/dist/package-installer/package-installer.test.js +0 -882
  223. package/dist/package-installer/package-propagation.test.d.ts +0 -1
  224. package/dist/package-installer/package-propagation.test.js +0 -1269
  225. package/dist/package-installer/package-publisher.test.d.ts +0 -1
  226. package/dist/package-installer/package-publisher.test.js +0 -153
  227. package/dist/package-installer/package-remote-checker.test.d.ts +0 -1
  228. package/dist/package-installer/package-remote-checker.test.js +0 -334
  229. package/dist/package-installer/package-seed-installer.test.d.ts +0 -1
  230. package/dist/package-installer/package-seed-installer.test.js +0 -123
  231. package/dist/package-installer/package-tarball.test.d.ts +0 -1
  232. package/dist/package-installer/package-tarball.test.js +0 -75
  233. package/dist/package-installer/peers-core-installer.test.d.ts +0 -1
  234. package/dist/package-installer/peers-core-installer.test.js +0 -49
  235. package/dist/package-installer/peers-core-trust-anchor.test.d.ts +0 -1
  236. package/dist/package-installer/peers-core-trust-anchor.test.js +0 -155
  237. package/dist/package-loader/contract-package-loader.test.d.ts +0 -1
  238. package/dist/package-loader/contract-package-loader.test.js +0 -274
  239. package/dist/package-loader/host-authority-loading.test.d.ts +0 -1
  240. package/dist/package-loader/host-authority-loading.test.js +0 -104
  241. package/dist/package-loader/host-dispatch-policy.test.d.ts +0 -1
  242. package/dist/package-loader/host-dispatch-policy.test.js +0 -56
  243. package/dist/package-loader/isolated-artifact.test.d.ts +0 -1
  244. package/dist/package-loader/isolated-artifact.test.js +0 -688
  245. package/dist/package-loader/isolated-persistent-var-gateway.test.d.ts +0 -1
  246. package/dist/package-loader/isolated-persistent-var-gateway.test.js +0 -56
  247. package/dist/package-loader/isolated-table-gateway.test.d.ts +0 -1
  248. package/dist/package-loader/isolated-table-gateway.test.js +0 -633
  249. package/dist/package-loader/package-loader.test.d.ts +0 -1
  250. package/dist/package-loader/package-loader.test.js +0 -1764
  251. package/dist/package-loader/resolve-consumed-contracts.test.d.ts +0 -1
  252. package/dist/package-loader/resolve-consumed-contracts.test.js +0 -166
  253. package/dist/serial-json.compat.test.d.ts +0 -1
  254. package/dist/serial-json.compat.test.js +0 -369
  255. package/dist/serial-json.test.d.ts +0 -1
  256. package/dist/serial-json.test.js +0 -401
  257. package/dist/testing/mock-host-contract.test.d.ts +0 -1
  258. package/dist/testing/mock-host-contract.test.js +0 -782
  259. package/dist/utils.test.d.ts +0 -1
  260. package/dist/utils.test.js +0 -232
@@ -1,1199 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- require("../../index");
4
- const nacl = require("tweetnacl");
5
- const tools_1 = require("../../data/tools");
6
- const field_type_1 = require("../../types/field-type");
7
- const zod_types_1 = require("../../types/zod-types");
8
- const utils_1 = require("../../utils");
9
- const registry_1 = require("../registry");
10
- const tool_catalog_contract_1 = require("../system/tool-catalog-contract");
11
- const systemContracts = require("../system-contracts");
12
- const tool_catalog_1 = require("../tool-catalog");
13
- const tool_catalog_identity_1 = require("../tool-catalog-identity");
14
- const tool_catalog_migrations_1 = require("../tool-catalog-migrations");
15
- function required(value) {
16
- if (value === undefined)
17
- throw new Error("Missing fixture value");
18
- return value;
19
- }
20
- const contractId = "00mp287gdl5atltgs5bh0vm9z";
21
- function definition(names = ["read"], version = 1) {
22
- return {
23
- contractId,
24
- version,
25
- name: "Test",
26
- description: "",
27
- devTag: "dev",
28
- tables: [],
29
- observables: [],
30
- tools: names.map((name) => ({
31
- name,
32
- usageDescription: name,
33
- inputFields: [],
34
- outputFields: [],
35
- })),
36
- };
37
- }
38
- function legacy(toolId = (0, utils_1.newid)()) {
39
- return {
40
- toolId,
41
- name: "legacy",
42
- usageDescription: "preserve",
43
- code: "existing",
44
- inputSchema: { type: tools_1.IOSchemaType.none, fields: [] },
45
- outputSchema: { type: tools_1.IOSchemaType.none, fields: [] },
46
- accessLevel: 70,
47
- };
48
- }
49
- function fixture(rows = new Map()) {
50
- const listeners = new Set();
51
- const table = {
52
- list: async () => [...rows.values()],
53
- get: async (id) => rows.get(id),
54
- update: jest.fn(async (patch) => {
55
- const row = tools_1.toolSchema.parse({ ...rows.get(patch.toolId), ...patch });
56
- rows.set(row.toolId, row);
57
- for (const listener of listeners)
58
- listener({ dataObject: row, op: "update", source: "local" });
59
- return row;
60
- }),
61
- save: jest.fn(async (row, options) => {
62
- if (!options?.insertOnly || !rows.has(row.toolId))
63
- rows.set(row.toolId, tools_1.toolSchema.parse(row));
64
- for (const listener of listeners)
65
- listener({ dataObject: rows.get(row.toolId), op: "insert", source: "local" });
66
- return rows.get(row.toolId);
67
- }),
68
- dataChanged: {
69
- subscribe: (fn) => {
70
- listeners.add(fn);
71
- return { unsubscribe: () => listeners.delete(fn) };
72
- },
73
- },
74
- };
75
- const context = {
76
- dataContextId: (0, utils_1.newid)(),
77
- tableContainer: { getTable: () => table },
78
- };
79
- const registry = new registry_1.ContractRegistry();
80
- const catalog = new tool_catalog_1.ToolCatalog(context, registry);
81
- const publish = async (def = definition(), provider = (0, utils_1.newid)(), prepare = async () => { }) => catalog.publish(provider, [{ definition: def }], prepare, () => registry.replaceBatch(provider, [{ definition: def }]));
82
- return { rows, table, registry, catalog, publish, listeners };
83
- }
84
- test.each([
85
- "save",
86
- "snapshot",
87
- ])("system readiness rejects quarantine during a later %s before publishing any provider", async (boundary) => {
88
- const first = definition();
89
- const second = { ...definition(), contractId: (0, utils_1.newid)() };
90
- const inventory = jest.spyOn(systemContracts, "listSystemContracts").mockReturnValue([
91
- { providerPackageId: (0, utils_1.newid)(), getDefinition: () => first },
92
- { providerPackageId: (0, utils_1.newid)(), getDefinition: () => second },
93
- ]);
94
- const f = fixture();
95
- const publish = jest.spyOn(f.registry, "replaceBatch");
96
- const firstId = (0, tool_catalog_migrations_1.toolCatalogIds)({ contractId: first.contractId, version: 1, toolName: "read" })[0];
97
- const secondId = (0, tool_catalog_migrations_1.toolCatalogIds)({
98
- contractId: second.contractId,
99
- version: 1,
100
- toolName: "read",
101
- })[0];
102
- const quarantine = () => f.table.update({ toolId: firstId, catalogState: "quarantined" });
103
- if (boundary === "save") {
104
- const save = f.table.save.getMockImplementation();
105
- if (!save)
106
- throw new Error("Missing fixture save");
107
- f.table.save.mockImplementation(async (row, options) => {
108
- const saved = await save(row, options);
109
- if (row.toolId === secondId)
110
- await quarantine();
111
- return saved;
112
- });
113
- }
114
- else {
115
- const get = f.table.get;
116
- jest.spyOn(f.table, "get").mockImplementation(async (id) => {
117
- if (id === secondId)
118
- await quarantine();
119
- return get(id);
120
- });
121
- }
122
- try {
123
- await expect(f.catalog.ready()).rejects.toThrow(/quarantined/i);
124
- expect(publish).not.toHaveBeenCalled();
125
- expect(f.registry.getProviderPackageId(first.contractId, 1)).toBeUndefined();
126
- expect(f.registry.getProviderPackageId(second.contractId, 1)).toBeUndefined();
127
- expect(f.rows.get(firstId)?.catalogState).toBe("quarantined");
128
- expect(f.rows.has(secondId)).toBe(true);
129
- expect(f.listeners.size).toBe(0);
130
- }
131
- finally {
132
- await f.catalog.dispose();
133
- inventory.mockRestore();
134
- }
135
- });
136
- describe("permanent Tools identity", () => {
137
- test("delete validation protects reserved identities before rows exist", () => {
138
- for (const id of [
139
- tool_catalog_migrations_1.toolCatalogMigrations[0].toolId,
140
- (0, tool_catalog_identity_1.deriveToolCatalogId)({ contractId, version: 1, toolName: "read" }),
141
- ]) {
142
- expect(() => (0, tool_catalog_1.validateToolCatalogWrite)(undefined, undefined, id)).toThrow(/permanent/);
143
- }
144
- expect(() => (0, tool_catalog_1.validateToolCatalogWrite)(undefined, undefined, (0, utils_1.newid)())).not.toThrow();
145
- const target = { contractId, contractVersion: 1, toolName: "read" };
146
- for (const row of [
147
- { ...legacy(), ...target },
148
- { ...legacy(), catalogState: "quarantined" },
149
- ])
150
- expect(() => (0, tool_catalog_1.validateToolCatalogWrite)(row, undefined)).toThrow(/permanent/);
151
- });
152
- test("unresolved legacy rows are ordinary rows that may be deleted", () => {
153
- expect(() => (0, tool_catalog_1.validateToolCatalogWrite)(legacy(), undefined)).not.toThrow();
154
- expect(() => (0, tool_catalog_1.validateToolCatalogWrite)({ ...legacy(), catalogState: "unresolved" }, undefined)).not.toThrow();
155
- });
156
- test.each([
157
- [contractId, 1, "read", "000tool0m6n9qqflttd2vhqt0"],
158
- [contractId, 2, "read", "000tool0hz4s7a3bzw0hx7h6e"],
159
- [contractId, 1, "Read", "000tool0tkbtut20wcn4p2n9c"],
160
- [contractId, 1, "é🛠", "000tool0w8lfnvvspj7cl3u9x"],
161
- [contractId, 1, "é", "000tool0hcab9lwucf1sls3uk"],
162
- [contractId, 1, "e\u0301", "000tool0m71grpdum3oh5ak0e"],
163
- [contractId, 1, " read ", "000tool03ja6984o18ycrskkz"],
164
- [contractId, 1, 'quote"\n\\', "000tool007ny5nm141nkk5puv"],
165
- ["00MP287GDL5ATLTGS5BH0VM9Z", 1, "read", "000tool0xd9tnb6xvxcvtrojg"],
166
- [contractId, Number.MAX_SAFE_INTEGER, "read", "000tool0yk5ipn4g8azf6c6yt"],
167
- ])("golden %s v%s %s", (id, version, toolName, expected) => {
168
- const target = {
169
- contractId: id,
170
- version: version,
171
- toolName: toolName,
172
- };
173
- expect(Buffer.from((0, tool_catalog_identity_1.encodeToolCatalogTarget)(target)).toString()).toBe(JSON.stringify(["peers.tools-catalog.v1", id, version, toolName]));
174
- expect((0, tool_catalog_identity_1.deriveToolCatalogId)(target)).toBe(expected);
175
- expect((0, utils_1.isid)(expected)).toBe(true);
176
- expect(zod_types_1.zodPeerId.safeParse(expected).success).toBe(true);
177
- });
178
- test("rejects malformed Unicode, unsafe versions and invalid time prefixes", () => {
179
- for (const toolName of ["\ud800", "x\udfff", "\ud800x", ""])
180
- expect(() => (0, tool_catalog_identity_1.validateToolCatalogTarget)({ contractId, version: 1, toolName })).toThrow();
181
- for (const version of [0, -1, 1.1, Number.MAX_SAFE_INTEGER + 1])
182
- expect(() => (0, tool_catalog_identity_1.validateToolCatalogTarget)({ contractId, version, toolName: "ok" })).toThrow();
183
- expect(() => (0, tool_catalog_identity_1.validateToolCatalogTarget)({ contractId: "z".repeat(25), version: 1, toolName: "ok" })).toThrow();
184
- expect(() => (0, tool_catalog_identity_1.validateToolCatalogTarget)({
185
- contractId: { toString: () => contractId },
186
- version: 1,
187
- toolName: "ok",
188
- })).toThrow();
189
- expect((0, tool_catalog_identity_1.deriveToolCatalogId)({ contractId, version: 1, toolName: " read " })).not.toBe((0, tool_catalog_identity_1.deriveToolCatalogId)({ contractId, version: 1, toolName: "read" }));
190
- });
191
- test("canonical bytes preserve UTF-8 and JSON escaping exactly", () => {
192
- expect(Buffer.from((0, tool_catalog_identity_1.encodeToolCatalogTarget)({ contractId, version: 1, toolName: "é🛠" })).toString("hex")).toBe("5b2270656572732e746f6f6c732d636174616c6f672e7631222c2230306d7032383767646c3561746c74677335626830766d397a222c312c22c3a9f09f9ba0225d");
193
- expect(Buffer.from((0, tool_catalog_identity_1.encodeToolCatalogTarget)({ contractId, version: 1, toolName: 'quote"\n\\' })).toString("hex")).toBe("5b2270656572732e746f6f6c732d636174616c6f672e7631222c2230306d7032383767646c3561746c74677335626830766d397a222c312c2271756f74655c225c6e5c5c225d");
194
- });
195
- test("reserves literal inventory and preserves narrow shipped exceptions", () => {
196
- for (const entry of tool_catalog_migrations_1.toolCatalogMigrations) {
197
- expect((0, tool_catalog_migrations_1.resolveLegacyToolId)(entry.toolId)).toEqual({
198
- contractId: entry.contractId,
199
- version: entry.version,
200
- toolName: entry.toolName,
201
- });
202
- expect(tools_1.toolSchema.safeParse(legacy(entry.toolId)).success).toBe(true);
203
- (0, tool_catalog_identity_1.validateToolCatalogTarget)(entry);
204
- }
205
- expect(zod_types_1.zodPeerId.safeParse("000peers0toolaccessctl0001").success).toBe(false);
206
- expect(zod_types_1.zodPeerId.safeParse("00muxgugn661g0qmpuwafrmsi4").success).toBe(false);
207
- expect((0, tool_catalog_migrations_1.resolveLegacyToolId)("000peers0tool00querydb001")?.toolName).toBe("query-database");
208
- });
209
- test("many-to-one aliases have no winner; ambiguity fails closed", () => {
210
- const target = { contractId: "00muxgugn6xvs8f2r80ru205t", version: 1, toolName: "invoke" };
211
- expect((0, tool_catalog_migrations_1.toolCatalogIds)(target)).toEqual([
212
- "00muxoa8xeet2lpi8bdq55q03",
213
- "00muxob3hbo3f6g49mipy1hyt",
214
- ]);
215
- const entry = tool_catalog_migrations_1.toolCatalogMigrations[0];
216
- expect(() => (0, tool_catalog_migrations_1.resolveLegacyToolId)(entry.toolId, [entry, { ...entry, toolName: "other" }])).toThrow(/Ambiguous/);
217
- });
218
- });
219
- describe("per-context catalog lifecycle", () => {
220
- test("system metadata changes only supplemental fields, preserving policy and history", async () => {
221
- const f = fixture();
222
- await f.catalog.ready();
223
- const toolId = "000peers0tool00000runner1";
224
- const stored = {
225
- ...required(f.rows.get(toolId)),
226
- code: "user source",
227
- packageId: (0, utils_1.newid)(),
228
- accessLevel: 90,
229
- remoteAccessLevel: 80,
230
- authorityPropagation: "cap",
231
- };
232
- f.rows.set(toolId, stored);
233
- const metadata = {
234
- ...stored,
235
- code: "replacement",
236
- packageId: (0, utils_1.newid)(),
237
- name: "replacement",
238
- accessLevel: 0,
239
- remoteAccessLevel: 0,
240
- authorityPropagation: "preserve",
241
- isAssistantRunner: true,
242
- configSchema: { type: tools_1.IOSchemaType.simple, fields: [] },
243
- detailedDescription: "runner details",
244
- };
245
- await f.catalog.saveToolMetadata([metadata]);
246
- expect(f.rows.get(toolId)).toEqual({
247
- ...stored,
248
- isAssistantRunner: true,
249
- configSchema: metadata.configSchema,
250
- detailedDescription: metadata.detailedDescription,
251
- });
252
- const count = f.table.update.mock.calls.length;
253
- await f.catalog.saveToolMetadata([metadata]);
254
- expect(f.table.update).toHaveBeenCalledTimes(count);
255
- await f.catalog.dispose();
256
- });
257
- test("system metadata rejects unresolved, quarantined, missing and non-system targets", async () => {
258
- const f = fixture();
259
- await f.publish();
260
- const id = "000peers0tool00000runner1";
261
- const stored = required(f.rows.get(id));
262
- for (const catalogState of ["unresolved", "quarantined"]) {
263
- f.rows.set(id, { ...stored, catalogState });
264
- await expect(f.catalog.saveToolMetadata([{ toolId: id }])).rejects.toThrow(/resolved system member/);
265
- }
266
- f.rows.delete(id);
267
- for (const toolId of [
268
- id,
269
- (0, utils_1.newid)(),
270
- (0, tool_catalog_identity_1.deriveToolCatalogId)({ contractId, version: 1, toolName: "read" }),
271
- ])
272
- await expect(f.catalog.saveToolMetadata([{ toolId }])).rejects.toThrow(/resolved system member/);
273
- await f.catalog.dispose();
274
- });
275
- test("publication refreshes runner metadata only after successful preparation", async () => {
276
- const f = fixture();
277
- const def = definition();
278
- const provider = (0, utils_1.newid)();
279
- const metadata = { contractId, version: 1, toolName: "read", isAssistantRunner: true };
280
- const id = (0, tool_catalog_migrations_1.toolCatalogIds)(metadata)[0];
281
- await f.publish(def, provider);
282
- const before = f.rows.get(id);
283
- await expect(f.catalog.publish(provider, [{ definition: def }], async () => {
284
- throw new Error("prepare failed");
285
- }, () => { }, [metadata])).rejects.toThrow("prepare failed");
286
- expect(f.rows.get(id)).toEqual(before);
287
- await f.catalog.publish(provider, [{ definition: def }], async () => { }, () => { }, [metadata]);
288
- expect(f.rows.get(id)?.isAssistantRunner).toBe(true);
289
- await f.catalog.dispose();
290
- });
291
- test("contract upgrades and dev members refresh unclaimed descriptions only after preparation", async () => {
292
- const f = fixture();
293
- const provider = (0, utils_1.newid)();
294
- const toolId = (0, tool_catalog_identity_1.deriveToolCatalogId)({ contractId, version: 1, toolName: "read" });
295
- await f.publish(definition(), provider);
296
- f.rows.set(toolId, { ...f.rows.get(toolId), accessLevel: 40 });
297
- const changed = definition();
298
- changed.tools[0] = {
299
- ...changed.tools[0],
300
- usageDescription: "changed",
301
- inputFields: [{ name: "path", type: field_type_1.FieldType.string }],
302
- };
303
- const before = f.rows.get(toolId);
304
- await expect(f.publish(changed, provider, async () => {
305
- throw new Error("prepare failed");
306
- })).rejects.toThrow("prepare failed");
307
- expect(f.rows.get(toolId)).toEqual(before);
308
- await f.publish(changed, provider);
309
- expect(f.rows.get(toolId)).toMatchObject({
310
- usageDescription: "changed",
311
- inputSchema: { type: tools_1.IOSchemaType.simple, fields: changed.tools[0].inputFields },
312
- accessLevel: 40,
313
- catalogIdentity: "deterministic",
314
- });
315
- const updates = f.table.update.mock.calls.length;
316
- await f.publish(changed, (0, utils_1.newid)());
317
- expect(f.table.update).toHaveBeenCalledTimes(updates);
318
- await f.catalog.dispose();
319
- });
320
- test("historical provenance limits description refresh to its provider", async () => {
321
- const owner = (0, utils_1.newid)();
322
- const old = { ...legacy("00muxoa8xeet2lpi8bdq55q03"), packageId: owner };
323
- const f = fixture(new Map([[old.toolId, old]]));
324
- const def = { ...definition(["invoke"]), contractId: "00muxgugn6xvs8f2r80ru205t" };
325
- await f.publish(def, (0, utils_1.newid)());
326
- expect(f.rows.get(old.toolId)?.usageDescription).toBe("preserve");
327
- await f.publish(def, owner);
328
- expect(f.rows.get(old.toolId)?.usageDescription).toBe("invoke");
329
- await f.catalog.dispose();
330
- });
331
- test.each([
332
- 0, 1,
333
- ])("disposal during initialization neither publishes %s system providers nor subscribes", async (count) => {
334
- const system = definition();
335
- const inventory = jest
336
- .spyOn(systemContracts, "listSystemContracts")
337
- .mockReturnValue(count ? [{ providerPackageId: (0, utils_1.newid)(), getDefinition: () => system }] : []);
338
- const f = fixture();
339
- let release;
340
- let entered;
341
- const held = new Promise((resolve) => {
342
- release = resolve;
343
- });
344
- const entering = new Promise((resolve) => {
345
- entered = resolve;
346
- });
347
- const list = f.table.list;
348
- jest.spyOn(f.table, "list").mockImplementationOnce(async () => {
349
- entered();
350
- await held;
351
- return list();
352
- });
353
- try {
354
- const ready = f.catalog.ready();
355
- await entering;
356
- const disposing = f.catalog.dispose();
357
- release();
358
- await expect(ready).rejects.toThrow(/disposed/);
359
- await disposing;
360
- expect(f.registry.resolve(contractId, 1)).toBeUndefined();
361
- expect(f.listeners.size).toBe(0);
362
- }
363
- finally {
364
- inventory.mockRestore();
365
- }
366
- });
367
- test("identical targets across providers and contexts; reversed install order", async () => {
368
- const a = fixture();
369
- const b = fixture();
370
- const providers = [(0, utils_1.newid)(), (0, utils_1.newid)()];
371
- for (const provider of providers)
372
- await a.publish(definition(), provider);
373
- for (const provider of [...providers].reverse())
374
- await b.publish(definition(), provider);
375
- expect([...a.rows.keys()].sort()).toEqual([...b.rows.keys()].sort());
376
- await a.catalog.dispose();
377
- await b.catalog.dispose();
378
- });
379
- test.each([false, true])("both aliases on fresh/upgraded database: %s", async (upgraded) => {
380
- const old = legacy("00muxob3hbo3f6g49mipy1hyt");
381
- const f = fixture(new Map(upgraded ? [[old.toolId, old]] : []));
382
- await f.publish({ ...definition(["invoke"]), contractId: "00muxgugn6xvs8f2r80ru205t" });
383
- expect(f.rows.has("00muxoa8xeet2lpi8bdq55q03")).toBe(true);
384
- expect(f.rows.get(old.toolId)?.catalogIdentity).toBe("legacy-alias");
385
- // No package claims the upgraded row, so the definition describes it; the
386
- // primary key, legacy code and policy are preserved.
387
- if (upgraded)
388
- expect(f.rows.get(old.toolId)).toMatchObject({
389
- ...old,
390
- name: "invoke",
391
- usageDescription: "invoke",
392
- inputSchema: { type: tools_1.IOSchemaType.simple, fields: [] },
393
- outputSchema: { type: tools_1.IOSchemaType.simple, fields: [] },
394
- });
395
- expect([...f.rows.values()].filter((r) => r.toolName === "invoke")).toHaveLength(2);
396
- await f.catalog.dispose();
397
- });
398
- test("backfills unknown legacy rows without inventing targets and quarantines unknown mappings", async () => {
399
- const old = legacy();
400
- const bad = { ...legacy(), contractId, contractVersion: 1, toolName: "read" };
401
- const f = fixture(new Map([
402
- [old.toolId, old],
403
- [bad.toolId, bad],
404
- ]));
405
- await f.catalog.ready();
406
- expect(f.rows.get(old.toolId)).toMatchObject({ catalogState: "unresolved", code: "existing" });
407
- expect(f.rows.get(bad.toolId)?.catalogState).toBe("quarantined");
408
- await f.catalog.dispose();
409
- });
410
- test("persistence failures retry; replacement failure leaves active metadata untouched", async () => {
411
- const f = fixture();
412
- const provider = (0, utils_1.newid)();
413
- await f.catalog.ready();
414
- f.table.save.mockRejectedValueOnce(new Error("disk full"));
415
- await expect(f.publish(definition(), provider)).rejects.toThrow("disk full");
416
- expect(f.registry.resolve(contractId, 1)).toBeUndefined();
417
- await f.publish(definition(), provider);
418
- const before = [...f.rows.values()];
419
- await expect(f.publish(definition(["read", "new"]), provider, async () => {
420
- throw new Error("prepare failed");
421
- })).rejects.toThrow("prepare failed");
422
- expect(f.registry.resolve(contractId, 1)?.tools.map((t) => t.name)).toEqual(["read"]);
423
- for (const row of before)
424
- expect(f.rows.get(row.toolId)).toEqual(row);
425
- expect([...f.rows.values()].some((r) => r.toolName === "new")).toBe(true);
426
- await f.catalog.dispose();
427
- const restarted = fixture(f.rows);
428
- await restarted.publish();
429
- await restarted.catalog.dispose();
430
- });
431
- test("concurrent providers cannot activate before serialized preparation finishes", async () => {
432
- const f = fixture();
433
- const order = [];
434
- let release;
435
- const held = new Promise((resolve) => {
436
- release = resolve;
437
- });
438
- const first = f.publish(definition(), (0, utils_1.newid)(), async () => {
439
- order.push("first");
440
- await held;
441
- });
442
- const second = f.publish(definition(), (0, utils_1.newid)(), async () => {
443
- order.push("second");
444
- });
445
- await new Promise((resolve) => setTimeout(resolve, 10));
446
- expect(order).toEqual(["first"]);
447
- expect(f.registry.resolve(contractId, 1)).toBeUndefined();
448
- release();
449
- await Promise.all([first, second]);
450
- expect(order).toEqual(["first", "second"]);
451
- await f.catalog.dispose();
452
- });
453
- test("forced hash collision and insert-only loser fail closed", async () => {
454
- const f = fixture();
455
- await f.catalog.ready();
456
- const hash = jest.spyOn(nacl, "hash").mockReturnValue(new Uint8Array(64));
457
- try {
458
- await expect(f.publish(definition(["one", "two"]))).rejects.toThrow(/collision/);
459
- }
460
- finally {
461
- hash.mockRestore();
462
- }
463
- expect(f.registry.resolve(contractId, 1)).toBeUndefined();
464
- f.table.save.mockImplementationOnce(async (row) => ({ ...row, toolName: "wrong" }));
465
- await expect(f.publish()).rejects.toThrow(/identity cannot change/);
466
- await f.catalog.dispose();
467
- });
468
- test.each([
469
- false,
470
- true,
471
- ])("alsoImplements indexes only older public members, and removal retains history (carried=%s)", async (carried) => {
472
- const f = fixture();
473
- const provider = (0, utils_1.newid)();
474
- const older = definition(["old"]);
475
- const newer = definition(["old", "new"], 2);
476
- if (!carried)
477
- f.registry.register((0, utils_1.newid)(), older);
478
- const regs = [
479
- {
480
- definition: newer,
481
- alsoImplements: [{ contractId, version: 1, ...(carried ? { definition: older } : {}) }],
482
- },
483
- ];
484
- await f.catalog.publish(provider, regs, async () => { }, () => f.registry.replaceBatch(provider, regs));
485
- expect([...f.rows.values()]
486
- .filter((r) => r.contractId === contractId)
487
- .map((r) => [r.contractVersion, r.toolName])
488
- .sort()).toEqual([
489
- [1, "old"],
490
- [2, "new"],
491
- [2, "old"],
492
- ]);
493
- await f.publish(definition([], 2), provider);
494
- f.registry.unregister(provider);
495
- const count = f.rows.size;
496
- await f.catalog.dispose();
497
- expect(f.rows.size).toBe(count);
498
- expect(f.listeners.size).toBe(0);
499
- });
500
- test("shared write guard rejects retargeting, stripping, deletion, and unverified aliases", () => {
501
- const target = { contractId, version: 1, toolName: "read" };
502
- const row = {
503
- ...legacy((0, tool_catalog_identity_1.deriveToolCatalogId)(target)),
504
- contractId,
505
- contractVersion: 1,
506
- toolName: "read",
507
- catalogState: "resolved",
508
- };
509
- for (const candidate of [
510
- undefined,
511
- legacy(row.toolId),
512
- { ...row, toolName: "other" },
513
- { ...row, toolId: (0, utils_1.newid)() },
514
- ])
515
- expect(() => (0, tool_catalog_1.validateToolCatalogWrite)(row, candidate)).toThrow();
516
- expect(() => (0, tool_catalog_1.validateToolCatalogWrite)(row, { ...row, usageDescription: "updated" })).not.toThrow();
517
- });
518
- test("pre-catalog full rows keep established identity; partial stripping still fails", () => {
519
- const target = { contractId, version: 1, toolName: "read" };
520
- const row = {
521
- ...legacy((0, tool_catalog_identity_1.deriveToolCatalogId)(target)),
522
- contractId,
523
- contractVersion: 1,
524
- toolName: "read",
525
- catalogIdentity: "deterministic",
526
- catalogState: "quarantined",
527
- catalogReason: "conflict",
528
- };
529
- const older = { ...legacy(row.toolId), usageDescription: "from an older peer" };
530
- const retained = (0, tool_catalog_1.retainToolCatalogFields)(row, older);
531
- expect(retained).toEqual({ ...row, usageDescription: "from an older peer" });
532
- expect(() => (0, tool_catalog_1.validateToolCatalogWrite)(row, retained)).not.toThrow();
533
- const stripped = { ...row, contractId: undefined };
534
- expect((0, tool_catalog_1.retainToolCatalogFields)(row, stripped)).toBe(stripped);
535
- expect(() => (0, tool_catalog_1.validateToolCatalogWrite)(row, stripped)).toThrow();
536
- expect((0, tool_catalog_1.retainToolCatalogFields)(undefined, older)).toBe(older);
537
- });
538
- });
539
- test("Tools dataChanged reconciles new legacy evidence without recursive writes or provider activation", async () => {
540
- const f = fixture();
541
- await f.catalog.ready();
542
- const row = legacy();
543
- const count = f.table.save.mock.calls.length;
544
- await f.table.save(row);
545
- await new Promise((resolve) => setTimeout(resolve, 20));
546
- expect(f.rows.get(row.toolId)?.catalogState).toBe("unresolved");
547
- expect(f.table.save.mock.calls.length - count).toBe(1);
548
- expect(f.table.update).toHaveBeenCalledTimes(1);
549
- expect(f.registry.resolve(contractId, 1)).toBeUndefined();
550
- await f.catalog.dispose();
551
- });
552
- test("reconciliation does not overwrite metadata changed after its snapshot", async () => {
553
- const old = legacy();
554
- const f = fixture(new Map([[old.toolId, old]]));
555
- f.table.update.mockImplementationOnce(async (patch) => {
556
- expect(Object.keys(patch).sort()).toEqual(["catalogState", "toolId"]);
557
- const current = { ...old, usageDescription: "synced while reconciling", accessLevel: 90 };
558
- const saved = { ...current, ...patch };
559
- f.rows.set(old.toolId, saved);
560
- return saved;
561
- });
562
- await f.catalog.ready();
563
- expect(f.rows.get(old.toolId)).toMatchObject({
564
- catalogState: "unresolved",
565
- usageDescription: "synced while reconciling",
566
- accessLevel: 90,
567
- });
568
- await f.catalog.dispose();
569
- });
570
- test("untargeted deterministic IDs and quarantined rows cannot be claimed by a provider", async () => {
571
- const target = { contractId, version: 1, toolName: "read" };
572
- const old = legacy((0, tool_catalog_identity_1.deriveToolCatalogId)(target));
573
- const f = fixture(new Map([[old.toolId, old]]));
574
- await f.catalog.ready();
575
- expect(f.rows.get(old.toolId)?.catalogState).toBe("quarantined");
576
- await expect(f.publish()).rejects.toThrow(/Quarantined/);
577
- expect(f.registry.resolve(contractId, 1)).toBeUndefined();
578
- const row = { ...old, contractId, contractVersion: 1, toolName: "read" };
579
- expect(() => (0, tool_catalog_1.validateToolCatalogWrite)(old, row)).toThrow(/cannot be claimed/);
580
- expect(() => (0, tool_catalog_1.validateToolCatalogWrite)({ ...row, catalogState: "quarantined" }, { ...row, catalogState: "resolved" })).toThrow(/Quarantined/);
581
- await f.catalog.dispose();
582
- });
583
- describe("bare deterministic row repair", () => {
584
- const target = { contractId, version: 1, toolName: "read" };
585
- const toolId = (0, tool_catalog_identity_1.deriveToolCatalogId)(target);
586
- const bare = {
587
- toolId,
588
- name: "read",
589
- usageDescription: "stale",
590
- code: "",
591
- inputSchema: { type: tools_1.IOSchemaType.none, fields: [] },
592
- outputSchema: { type: tools_1.IOSchemaType.none, fields: [] },
593
- catalogState: "quarantined",
594
- catalogReason: `Error: Missing tool catalog target: ${toolId}`,
595
- };
596
- const restored = {
597
- ...bare,
598
- contractId,
599
- contractVersion: 1,
600
- toolName: "read",
601
- catalogIdentity: "deterministic",
602
- catalogState: "resolved",
603
- catalogReason: undefined,
604
- };
605
- test.each([undefined, "deterministic"].flatMap((catalogIdentity) => [undefined, "unresolved", "quarantined"].map((catalogState) => ({
606
- catalogIdentity,
607
- catalogState,
608
- }))))("publication restores identity=$catalogIdentity state=$catalogState and preserves current policy", async (state) => {
609
- const old = {
610
- ...bare,
611
- ...state,
612
- packageId: (0, utils_1.newid)(),
613
- codeRepository: "historical repository",
614
- accessLevel: 70,
615
- remoteAccessLevel: 60,
616
- authorityPropagation: "cap",
617
- isAssistantRunner: true,
618
- detailedDescription: "saved details",
619
- configSchema: { type: tools_1.IOSchemaType.none, fields: [] },
620
- };
621
- expect(() => (0, tool_catalog_1.validateToolCatalogWrite)(old, restored, toolId, true)).not.toThrow();
622
- const f = fixture(new Map([[toolId, old]]));
623
- const update = required(f.table.update.getMockImplementation());
624
- f.table.update.mockImplementation(async (patch) => {
625
- if (patch.toolId === toolId && patch.catalogState === "resolved") {
626
- expect((0, tools_1.isToolCatalogPatch)(patch)).toBe(true);
627
- expect(patch).toEqual({
628
- toolId,
629
- contractId,
630
- contractVersion: 1,
631
- toolName: "read",
632
- catalogIdentity: "deterministic",
633
- catalogState: "resolved",
634
- catalogReason: undefined,
635
- });
636
- f.rows.set(toolId, {
637
- ...required(f.rows.get(toolId)),
638
- accessLevel: 90,
639
- remoteAccessLevel: 80,
640
- authorityPropagation: "preserve",
641
- });
642
- }
643
- return update(patch);
644
- });
645
- try {
646
- await f.publish();
647
- expect(f.rows.get(toolId)).toEqual({
648
- ...old,
649
- ...restored,
650
- accessLevel: 90,
651
- remoteAccessLevel: 80,
652
- authorityPropagation: "preserve",
653
- });
654
- expect(f.registry.resolve(contractId, 1)?.name).toBe("Test");
655
- }
656
- finally {
657
- await f.catalog.dispose();
658
- }
659
- });
660
- test.each([
661
- ["executable source", { code: "existing" }],
662
- ["whitespace source", { code: " " }],
663
- ["missing source", { code: undefined }],
664
- ["another identity", { catalogIdentity: "legacy-alias" }],
665
- ["invalid empty identity", { catalogIdentity: "" }],
666
- ["different name", { name: "write" }],
667
- ["different name case", { name: "Read" }],
668
- ["conflicting stored target", { contractId, contractVersion: 1, toolName: "write" }],
669
- ["partial contract ID", { contractId }],
670
- ["partial version", { contractVersion: 1 }],
671
- ["partial member name", { toolName: "read" }],
672
- ])("rejects %s even for an explicit catalog patch", (_name, fields) => {
673
- expect(() => (0, tool_catalog_1.validateToolCatalogWrite)({ ...bare, ...fields }, restored, toolId, true)).toThrow(/Quarantined/);
674
- // Exercise the cannot-be-claimed branch without a quarantine early-out too.
675
- expect(() => (0, tool_catalog_1.validateToolCatalogWrite)({ ...bare, ...fields, catalogState: undefined }, restored, toolId, true)).toThrow();
676
- });
677
- test.each([
678
- ["missing target", { contractId: undefined, contractVersion: undefined, toolName: undefined }],
679
- ["partial target", { contractVersion: undefined }],
680
- ["invalid target", { contractVersion: 0 }],
681
- ["nonmatching deterministic ID", { contractVersion: 2 }],
682
- ["different member", { toolName: "write" }],
683
- // A repair must resolve in the same write; it cannot retarget quarantine evidence.
684
- ["quarantined state", { catalogState: "quarantined" }],
685
- ["unset state", { catalogState: undefined }],
686
- ["unset identity kind", { catalogIdentity: undefined }],
687
- ])("rejects a candidate with %s", (_name, fields) => {
688
- expect(() => (0, tool_catalog_1.validateToolCatalogWrite)(bare, { ...restored, ...fields }, toolId, true)).toThrow();
689
- });
690
- test("ordinary saves and replay clear a missing-target quarantine only with the canonical target", () => {
691
- expect(() => (0, tool_catalog_1.validateToolCatalogWrite)(bare, restored)).not.toThrow();
692
- for (const candidate of [
693
- { ...bare, catalogState: "resolved" },
694
- { ...bare, catalogState: undefined, usageDescription: "metadata" },
695
- ])
696
- expect(() => (0, tool_catalog_1.validateToolCatalogWrite)(bare, candidate)).toThrow(/Quarantined/);
697
- const metadata = (0, tool_catalog_1.retainToolCatalogFields)(bare, {
698
- ...legacy(toolId),
699
- code: "",
700
- name: "read",
701
- usageDescription: "older peer metadata",
702
- });
703
- expect(() => (0, tool_catalog_1.validateToolCatalogWrite)(bare, metadata)).not.toThrow();
704
- expect(metadata.catalogState).toBe("quarantined");
705
- });
706
- test("repair requires exactly one selected ID even when the first ID matches", async () => {
707
- const ids = jest
708
- .spyOn(await Promise.resolve().then(() => require("../tool-catalog-migrations")), "toolCatalogIds")
709
- .mockReturnValue([toolId, (0, utils_1.newid)()]);
710
- try {
711
- expect(() => (0, tool_catalog_1.validateToolCatalogWrite)(bare, restored, toolId, true)).toThrow(/Quarantined/);
712
- }
713
- finally {
714
- ids.mockRestore();
715
- }
716
- });
717
- test.each([
718
- { contractId, version: 1, toolName: "new-task" },
719
- { contractId: "00muxgugn6xvs8f2r80ru205t", version: 1, toolName: "invoke" },
720
- ])("aliases suppress deterministic repair for $toolName", async (aliasedTarget) => {
721
- const deterministicId = (0, tool_catalog_identity_1.deriveToolCatalogId)(aliasedTarget);
722
- const aliases = (0, tool_catalog_migrations_1.toolCatalogIds)(aliasedTarget);
723
- const old = { ...bare, toolId: deterministicId, name: aliasedTarget.toolName };
724
- const candidate = {
725
- ...restored,
726
- toolId: deterministicId,
727
- contractId: aliasedTarget.contractId,
728
- toolName: aliasedTarget.toolName,
729
- };
730
- expect(() => (0, tool_catalog_1.validateToolCatalogWrite)(old, candidate, deterministicId, true)).toThrow();
731
- for (const alias of aliases)
732
- expect(() => (0, tool_catalog_1.validateToolCatalogWrite)({ ...old, toolId: alias }, { ...candidate, toolId: alias, catalogIdentity: "legacy-alias" }, alias, true)).toThrow(/Quarantined/);
733
- const f = fixture(new Map([[deterministicId, old]]));
734
- try {
735
- await f.publish({
736
- ...definition([aliasedTarget.toolName]),
737
- contractId: aliasedTarget.contractId,
738
- });
739
- expect(f.rows.get(deterministicId)).toEqual(old);
740
- for (const alias of aliases)
741
- expect(f.rows.get(alias)?.catalogIdentity).toBe("legacy-alias");
742
- expect(f.table.save.mock.calls.some(([row]) => row.toolId === deterministicId)).toBe(false);
743
- }
744
- finally {
745
- await f.catalog.dispose();
746
- }
747
- });
748
- test.each([
749
- "collision",
750
- "quarantine",
751
- ])("a later %s prevents all repair writes in the batch", async (failure) => {
752
- const f = fixture();
753
- await f.catalog.ready();
754
- const hash = failure === "collision"
755
- ? jest.spyOn(nacl, "hash").mockReturnValue(new Uint8Array(64))
756
- : undefined;
757
- const firstId = (0, tool_catalog_identity_1.deriveToolCatalogId)(target);
758
- const old = { ...bare, toolId: firstId };
759
- f.rows.set(firstId, old);
760
- if (failure === "quarantine") {
761
- const secondId = (0, tool_catalog_identity_1.deriveToolCatalogId)({ ...target, toolName: "write" });
762
- f.rows.set(secondId, {
763
- ...restored,
764
- toolId: secondId,
765
- toolName: "write",
766
- catalogState: "quarantined",
767
- });
768
- }
769
- f.table.update.mockClear();
770
- f.table.save.mockClear();
771
- try {
772
- await expect(f.publish(definition(["read", "write"]))).rejects.toThrow(/collision|Quarantined/);
773
- expect(f.rows.get(firstId)).toEqual(old);
774
- expect(f.table.update).not.toHaveBeenCalled();
775
- expect(f.table.save).not.toHaveBeenCalled();
776
- expect(f.registry.resolve(contractId, 1)).toBeUndefined();
777
- }
778
- finally {
779
- hash?.mockRestore();
780
- await f.catalog.dispose();
781
- }
782
- });
783
- });
784
- test.each([
785
- false,
786
- true,
787
- ])("system readiness restores the startup listTools row (quarantined=%s)", async (quarantined) => {
788
- const definition = (0, tool_catalog_contract_1.getToolCatalogContractDefinition)();
789
- const toolId = (0, tool_catalog_identity_1.deriveToolCatalogId)({
790
- contractId: definition.contractId,
791
- version: definition.version,
792
- toolName: "listTools",
793
- });
794
- expect(toolId).toBe("000tool0ggg5rilyp6kbxevip");
795
- const bare = {
796
- ...legacy(toolId),
797
- name: "listTools",
798
- code: "",
799
- catalogState: quarantined ? "quarantined" : undefined,
800
- catalogReason: quarantined ? `Error: Missing tool catalog target: ${toolId}` : undefined,
801
- };
802
- const f = fixture(new Map([[toolId, bare]]));
803
- try {
804
- await f.catalog.ready();
805
- expect(f.rows.get(toolId)).toMatchObject({
806
- contractId: definition.contractId,
807
- contractVersion: definition.version,
808
- toolName: "listTools",
809
- catalogIdentity: "deterministic",
810
- catalogState: "resolved",
811
- code: "",
812
- accessLevel: 70,
813
- });
814
- expect(f.rows.get(toolId)?.catalogReason).toBeUndefined();
815
- expect(f.registry.getProviderPackageId(definition.contractId, definition.version)).toBeDefined();
816
- const writes = f.table.update.mock.calls.length;
817
- await f.catalog.ready();
818
- expect(f.table.update).toHaveBeenCalledTimes(writes);
819
- }
820
- finally {
821
- await f.catalog.dispose();
822
- }
823
- });
824
- test.each([
825
- ["executable source", { code: "existing" }],
826
- ["a different name", { name: "list-tools" }],
827
- ["another identity", { catalogIdentity: "legacy-alias" }],
828
- ])("system readiness still aborts on a real listTools quarantine with %s", async (_name, fields) => {
829
- const definition = (0, tool_catalog_contract_1.getToolCatalogContractDefinition)();
830
- const toolId = (0, tool_catalog_identity_1.deriveToolCatalogId)({
831
- contractId: definition.contractId,
832
- version: definition.version,
833
- toolName: "listTools",
834
- });
835
- const quarantined = {
836
- ...legacy(toolId),
837
- name: "listTools",
838
- code: "",
839
- ...fields,
840
- catalogState: "quarantined",
841
- catalogReason: `Error: Missing tool catalog target: ${toolId}`,
842
- };
843
- const f = fixture(new Map([[toolId, quarantined]]));
844
- const publish = jest.spyOn(f.registry, "replaceBatch");
845
- try {
846
- await expect(f.catalog.ready()).rejects.toThrow(`Quarantined tool identity: ${toolId}`);
847
- expect(f.rows.get(toolId)).toEqual(quarantined);
848
- expect(publish).not.toHaveBeenCalled();
849
- }
850
- finally {
851
- await f.catalog.dispose();
852
- }
853
- });
854
- test("host removal waits for publication and disposal cancels queued activation", async () => {
855
- const f = fixture();
856
- const provider = (0, utils_1.newid)();
857
- let release;
858
- let prepared;
859
- const preparing = new Promise((resolve) => {
860
- prepared = resolve;
861
- });
862
- const held = new Promise((resolve) => {
863
- release = resolve;
864
- });
865
- const installing = f.publish(definition(), provider, async () => {
866
- prepared();
867
- await held;
868
- });
869
- await preparing;
870
- const deactivate = jest.fn();
871
- const removing = f.catalog.removeProvider(provider, deactivate);
872
- expect(deactivate).not.toHaveBeenCalled();
873
- release();
874
- await installing;
875
- await removing;
876
- expect(deactivate).toHaveBeenCalledTimes(1);
877
- expect(f.registry.resolve(contractId, 1)).toBeUndefined();
878
- expect(f.rows.has((0, tool_catalog_identity_1.deriveToolCatalogId)({ contractId, version: 1, toolName: "read" }))).toBe(true);
879
- await f.catalog.dispose();
880
- await expect(f.publish()).rejects.toThrow(/disposed/);
881
- });
882
- test("activation guard rejects quarantine after asynchronous activation work", async () => {
883
- const f = fixture();
884
- const toolId = (0, tool_catalog_identity_1.deriveToolCatalogId)({ contractId, version: 1, toolName: "read" });
885
- const commit = jest.fn();
886
- try {
887
- await expect(f.catalog.publish((0, utils_1.newid)(), [{ definition: definition() }], async () => { }, async (assertCurrent) => {
888
- await f.table.update({ toolId, catalogState: "quarantined" });
889
- assertCurrent();
890
- commit();
891
- })).rejects.toThrow(/quarantined/);
892
- expect(commit).not.toHaveBeenCalled();
893
- }
894
- finally {
895
- await f.catalog.dispose();
896
- expect(f.listeners.size).toBe(0);
897
- }
898
- });
899
- test("removal requested during cold initialization follows the earlier publication", async () => {
900
- const f = fixture();
901
- const provider = (0, utils_1.newid)();
902
- const installing = f.publish(definition(), provider);
903
- const removing = f.catalog.removeProvider(provider, () => { });
904
- await Promise.all([installing, removing]);
905
- expect(f.registry.getProviderPackageId(contractId, 1)).toBeUndefined();
906
- expect([...f.rows.values()].some((row) => row.contractId === contractId)).toBe(true);
907
- await f.catalog.dispose();
908
- });
909
- test("ready upgrades unresolved host aliases, keeps policy, refreshes descriptions and restores missing rows", async () => {
910
- const old = {
911
- ...legacy("000peers0tool0creategroup"),
912
- accessLevel: 40,
913
- catalogState: "unresolved",
914
- };
915
- const f = fixture(new Map([[old.toolId, old]]));
916
- await f.catalog.ready();
917
- expect(f.rows.get(old.toolId)).toMatchObject({
918
- catalogState: "resolved",
919
- catalogIdentity: "legacy-alias",
920
- name: "create-group",
921
- accessLevel: 40,
922
- });
923
- expect(f.rows.get(old.toolId)?.usageDescription).not.toBe(old.usageDescription);
924
- f.rows.delete(old.toolId);
925
- await f.catalog.dispose();
926
- const restarted = fixture(f.rows);
927
- await restarted.catalog.ready();
928
- expect(restarted.rows.get(old.toolId)?.catalogState).toBe("resolved");
929
- await restarted.catalog.dispose();
930
- });
931
- describe("missing-target quarantine resolution", () => {
932
- const target = { contractId, version: 1, toolName: "read" };
933
- const toolId = (0, tool_catalog_identity_1.deriveToolCatalogId)(target);
934
- const missingTarget = `Error: Missing tool catalog target: ${toolId}`;
935
- // The shape found on Mark's Family Hub (2026-10-26): a bare deterministic row that reconcile()
936
- // quarantined, while peers hold the published root plus the synced quarantine patches.
937
- const bare = {
938
- toolId,
939
- name: "read",
940
- usageDescription: "stale",
941
- code: "",
942
- inputSchema: { type: tools_1.IOSchemaType.none, fields: [] },
943
- outputSchema: { type: tools_1.IOSchemaType.none, fields: [] },
944
- catalogState: "quarantined",
945
- catalogReason: missingTarget,
946
- };
947
- const published = {
948
- ...bare,
949
- contractId,
950
- contractVersion: 1,
951
- toolName: "read",
952
- catalogIdentity: "deterministic",
953
- catalogState: "resolved",
954
- catalogReason: undefined,
955
- };
956
- const replayed = {
957
- ...published,
958
- catalogState: "quarantined",
959
- catalogReason: missingTarget,
960
- };
961
- test("sync replay may give a bare row its own target, still quarantined, and reconcile resolves it", async () => {
962
- expect(() => (0, tool_catalog_1.validateToolCatalogWrite)(bare, replayed)).not.toThrow();
963
- expect(() => (0, tool_catalog_1.validateToolCatalogWrite)(bare, published)).not.toThrow();
964
- const f = fixture(new Map([[toolId, replayed]]));
965
- try {
966
- await f.catalog.ready();
967
- expect(f.rows.get(toolId)).toMatchObject({
968
- contractId,
969
- contractVersion: 1,
970
- toolName: "read",
971
- catalogIdentity: "deterministic",
972
- catalogState: "resolved",
973
- });
974
- expect(f.rows.get(toolId)?.catalogReason).toBeUndefined();
975
- }
976
- finally {
977
- await f.catalog.dispose();
978
- }
979
- });
980
- test("publication resolves a row left half-restored with its target", async () => {
981
- const f = fixture();
982
- try {
983
- await f.catalog.ready();
984
- f.rows.set(toolId, replayed);
985
- await f.publish();
986
- expect(f.rows.get(toolId)?.catalogState).toBe("resolved");
987
- expect(f.registry.resolve(contractId, 1)?.name).toBe("Test");
988
- }
989
- finally {
990
- await f.catalog.dispose();
991
- }
992
- });
993
- test.each([
994
- ["another quarantine reason", { catalogReason: "Error: Tool catalog identity cannot change" }],
995
- ["no quarantine reason", { catalogReason: undefined }],
996
- ["a reason mentioning missing targets", { catalogReason: `Conflict; ${missingTarget}` }],
997
- ["a reason with a suffix", { catalogReason: `${missingTarget}; identity collision` }],
998
- [
999
- "another row's missing target",
1000
- { catalogReason: `Error: Missing tool catalog target: ${(0, utils_1.newid)()}` },
1001
- ],
1002
- ["executable source", { code: "existing" }],
1003
- ["a different name", { name: "write" }],
1004
- ["another identity kind", { catalogIdentity: "legacy-alias" }],
1005
- ["a partial target", { contractVersion: undefined }],
1006
- ["a target its ID doesn't derive from", { contractVersion: 2 }],
1007
- ])("a quarantined row with %s stays quarantined", async (_name, fields) => {
1008
- const stuck = { ...replayed, ...fields };
1009
- expect(() => (0, tool_catalog_1.validateToolCatalogWrite)(stuck, published)).toThrow(/Quarantined/);
1010
- const f = fixture(new Map([[toolId, stuck]]));
1011
- try {
1012
- await f.catalog.ready();
1013
- expect(f.rows.get(toolId)?.catalogState).toBe("quarantined");
1014
- }
1015
- finally {
1016
- await f.catalog.dispose();
1017
- }
1018
- });
1019
- test.each([
1020
- ["another quarantine reason", { catalogReason: "Error: Tool catalog identity cannot change" }],
1021
- ["no quarantine reason", { catalogReason: undefined }],
1022
- ["a reason mentioning missing targets", { catalogReason: `Conflict; ${missingTarget}` }],
1023
- [
1024
- "another row's missing target",
1025
- { catalogReason: `Error: Missing tool catalog target: ${(0, utils_1.newid)()}` },
1026
- ],
1027
- ["unset identity kind", { catalogIdentity: undefined }],
1028
- ["a target its ID doesn't derive from", { contractVersion: 2 }],
1029
- ["executable source", { code: "existing" }],
1030
- ])("a replayed candidate with %s cannot claim a bare row", (_name, fields) => {
1031
- expect(() => (0, tool_catalog_1.validateToolCatalogWrite)(bare, { ...replayed, ...fields })).toThrow();
1032
- });
1033
- test("reviewed aliases suppress resolution of an aliased member's quarantined rows", async () => {
1034
- const aliased = { contractId, version: 1, toolName: "new-task" };
1035
- const rows = [(0, tool_catalog_identity_1.deriveToolCatalogId)(aliased), ...(0, tool_catalog_migrations_1.toolCatalogIds)(aliased)].map((id) => ({
1036
- ...replayed,
1037
- toolId: id,
1038
- name: "new-task",
1039
- toolName: "new-task",
1040
- catalogReason: `Error: Missing tool catalog target: ${id}`,
1041
- }));
1042
- for (const row of rows)
1043
- expect(() => (0, tool_catalog_1.validateToolCatalogWrite)(row, {
1044
- ...row,
1045
- catalogState: "resolved",
1046
- catalogReason: undefined,
1047
- })).toThrow(/Quarantined/);
1048
- const f = fixture(new Map(rows.map((row) => [row.toolId, row])));
1049
- try {
1050
- await f.catalog.ready();
1051
- for (const row of rows)
1052
- expect(f.rows.get(row.toolId)).toEqual(row);
1053
- }
1054
- finally {
1055
- await f.catalog.dispose();
1056
- }
1057
- });
1058
- test.each([
1059
- bare,
1060
- replayed,
1061
- ])("sync cannot relabel another quarantine as a missing target (%#)", (row) => {
1062
- const stuck = { ...row, catalogReason: "Error: Tool catalog identity collision" };
1063
- expect(() => (0, tool_catalog_1.validateToolCatalogWrite)(stuck, { ...stuck, catalogReason: missingTarget })).toThrow(/Quarantined/);
1064
- expect(() => (0, tool_catalog_1.validateToolCatalogWrite)(stuck, { ...stuck, usageDescription: "metadata" })).not.toThrow();
1065
- });
1066
- test.each([
1067
- undefined,
1068
- "deterministic",
1069
- ])("sync cannot replace a quarantined identity kind with %s to enable repair", (catalogIdentity) => {
1070
- const stuck = { ...replayed, catalogIdentity: "legacy-alias" };
1071
- expect(() => (0, tool_catalog_1.validateToolCatalogWrite)(stuck, { ...stuck, catalogIdentity })).toThrow(/Quarantined/);
1072
- expect(() => (0, tool_catalog_1.validateToolCatalogWrite)(stuck, { ...stuck, usageDescription: "metadata" })).not.toThrow();
1073
- });
1074
- test.each([
1075
- undefined,
1076
- "unresolved",
1077
- ])("sync can claim a bare row in state %s", (catalogState) => {
1078
- expect(() => (0, tool_catalog_1.validateToolCatalogWrite)({ ...bare, catalogState }, published)).not.toThrow();
1079
- expect(() => (0, tool_catalog_1.validateToolCatalogWrite)({ ...bare, catalogState }, replayed)).not.toThrow();
1080
- });
1081
- test("reconciliation patches only identity and state, preserving concurrent policy and history", async () => {
1082
- const stored = {
1083
- ...replayed,
1084
- packageId: (0, utils_1.newid)(),
1085
- codeRepository: "historical repository",
1086
- accessLevel: 70,
1087
- detailedDescription: "saved details",
1088
- };
1089
- const f = fixture(new Map([[toolId, stored]]));
1090
- const update = required(f.table.update.getMockImplementation());
1091
- f.table.update.mockImplementationOnce(async (patch) => {
1092
- expect((0, tools_1.isToolCatalogPatch)(patch)).toBe(true);
1093
- expect(patch).toEqual({
1094
- toolId,
1095
- contractId,
1096
- contractVersion: 1,
1097
- toolName: "read",
1098
- catalogIdentity: "deterministic",
1099
- catalogState: "resolved",
1100
- catalogReason: undefined,
1101
- });
1102
- const current = {
1103
- ...stored,
1104
- accessLevel: 90,
1105
- remoteAccessLevel: 80,
1106
- authorityPropagation: "preserve",
1107
- };
1108
- f.rows.set(toolId, current);
1109
- (0, tool_catalog_1.validateToolCatalogWrite)(current, { ...current, ...patch }, toolId, true);
1110
- return update(patch);
1111
- });
1112
- try {
1113
- await f.catalog.ready();
1114
- expect(f.rows.get(toolId)).toEqual({
1115
- ...stored,
1116
- catalogState: "resolved",
1117
- catalogReason: undefined,
1118
- accessLevel: 90,
1119
- remoteAccessLevel: 80,
1120
- authorityPropagation: "preserve",
1121
- });
1122
- }
1123
- finally {
1124
- await f.catalog.dispose();
1125
- }
1126
- });
1127
- test("a concurrent conflicting quarantine rejects a stale reconciliation patch", async () => {
1128
- const f = fixture(new Map([[toolId, replayed]]));
1129
- const conflict = { ...replayed, catalogReason: "Error: Tool catalog identity collision" };
1130
- f.table.update.mockImplementationOnce(async (patch) => {
1131
- f.rows.set(toolId, conflict);
1132
- (0, tool_catalog_1.validateToolCatalogWrite)(conflict, { ...conflict, ...patch }, toolId, true);
1133
- throw new Error("Conflict was not rejected");
1134
- });
1135
- try {
1136
- await expect(f.catalog.ready()).rejects.toThrow(/Quarantined/);
1137
- expect(f.rows.get(toolId)).toEqual(conflict);
1138
- expect(f.registry.resolve(contractId, 1)).toBeUndefined();
1139
- }
1140
- finally {
1141
- await f.catalog.dispose();
1142
- }
1143
- });
1144
- test("resolved and bare peers converge after replay without recursive repair writes", async () => {
1145
- const peers = [fixture(new Map([[toolId, published]])), fixture(new Map([[toolId, bare]]))];
1146
- try {
1147
- for (const f of peers) {
1148
- await f.catalog.ready();
1149
- f.table.update.mockClear();
1150
- (0, tool_catalog_1.validateToolCatalogWrite)(f.rows.get(toolId), replayed);
1151
- await f.table.save(replayed);
1152
- }
1153
- await new Promise((resolve) => setImmediate(resolve));
1154
- for (const f of peers) {
1155
- expect(f.rows.get(toolId)).toEqual(published);
1156
- expect(f.table.update).toHaveBeenCalledTimes(1);
1157
- (0, tool_catalog_1.validateToolCatalogWrite)(f.rows.get(toolId), published);
1158
- await f.table.save(published);
1159
- }
1160
- await new Promise((resolve) => setImmediate(resolve));
1161
- for (const f of peers) {
1162
- expect(f.table.update).toHaveBeenCalledTimes(1);
1163
- expect(f.registry.resolve(contractId, 1)).toBeUndefined();
1164
- }
1165
- }
1166
- finally {
1167
- await Promise.all(peers.map((f) => f.catalog.dispose()));
1168
- }
1169
- });
1170
- test("a hash collision cannot retarget retained missing-target quarantine evidence", () => {
1171
- const hash = jest.spyOn(nacl, "hash").mockReturnValue(new Uint8Array(64));
1172
- try {
1173
- const collisionId = (0, tool_catalog_identity_1.deriveToolCatalogId)(target);
1174
- expect((0, tool_catalog_identity_1.deriveToolCatalogId)({ ...target, version: 2 })).toBe(collisionId);
1175
- const stored = {
1176
- ...replayed,
1177
- toolId: collisionId,
1178
- catalogReason: `Error: Missing tool catalog target: ${collisionId}`,
1179
- };
1180
- for (const catalogState of ["resolved", "quarantined"])
1181
- expect(() => (0, tool_catalog_1.validateToolCatalogWrite)(stored, { ...stored, contractVersion: 2, catalogState })).toThrow();
1182
- }
1183
- finally {
1184
- hash.mockRestore();
1185
- }
1186
- });
1187
- test("an established mapping is not re-patched on each publish", async () => {
1188
- const f = fixture(new Map([[toolId, { ...published, catalogReason: undefined }]]));
1189
- try {
1190
- await f.publish();
1191
- const updates = f.table.update.mock.calls.length;
1192
- await f.publish(definition(), (0, utils_1.newid)());
1193
- expect(f.table.update).toHaveBeenCalledTimes(updates);
1194
- }
1195
- finally {
1196
- await f.catalog.dispose();
1197
- }
1198
- });
1199
- });