@peers-app/peers-sdk 0.27.3 → 0.27.5

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 (272) 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/contract-transport.d.ts +2 -1
  4. package/dist/contracts/contract-transport.js +24 -2
  5. package/dist/contracts/create-local-consumer.js +16 -1
  6. package/dist/contracts/index.d.ts +2 -0
  7. package/dist/contracts/index.js +2 -0
  8. package/dist/contracts/system/index.d.ts +2 -0
  9. package/dist/contracts/system/index.js +2 -0
  10. package/dist/contracts/system/invites-contract.js +2 -1
  11. package/dist/contracts/system/sync-check-contract.d.ts +97 -0
  12. package/dist/contracts/system/sync-check-contract.js +115 -0
  13. package/dist/contracts/tool-arguments.d.ts +15 -0
  14. package/dist/contracts/tool-arguments.js +41 -0
  15. package/dist/contracts/tool-catalog-service.js +2 -12
  16. package/dist/contracts/tool-catalog.js +6 -2
  17. package/dist/data/change-tracking.d.ts +40 -0
  18. package/dist/data/change-tracking.js +89 -0
  19. package/dist/data/orm/types.d.ts +24 -7
  20. package/dist/data/orm/types.js +97 -24
  21. package/dist/define/manifest.js +6 -34
  22. package/dist/device/binary-peer-connection-v2.d.ts +7 -15
  23. package/dist/device/binary-peer-connection-v2.js +14 -30
  24. package/dist/device/connection.d.ts +34 -43
  25. package/dist/device/connection.js +188 -186
  26. package/dist/device/device.d.ts +1 -8
  27. package/dist/device/device.js +1 -30
  28. package/dist/device/socket.type.d.ts +6 -12
  29. package/dist/package-loader/package-loader.js +27 -22
  30. package/dist/system-ids.d.ts +5 -0
  31. package/dist/system-ids.js +6 -1
  32. package/package.json +4 -2
  33. package/dist/context/user-context-identity.test.d.ts +0 -1
  34. package/dist/context/user-context-identity.test.js +0 -220
  35. package/dist/contracts/__tests__/action-dispatch.test.d.ts +0 -1
  36. package/dist/contracts/__tests__/action-dispatch.test.js +0 -319
  37. package/dist/contracts/__tests__/also-implements.test.d.ts +0 -1
  38. package/dist/contracts/__tests__/also-implements.test.js +0 -110
  39. package/dist/contracts/__tests__/builder.test.d.ts +0 -1
  40. package/dist/contracts/__tests__/builder.test.js +0 -669
  41. package/dist/contracts/__tests__/contract-events.test.d.ts +0 -1
  42. package/dist/contracts/__tests__/contract-events.test.js +0 -199
  43. package/dist/contracts/__tests__/contract-observables.test.d.ts +0 -1
  44. package/dist/contracts/__tests__/contract-observables.test.js +0 -290
  45. package/dist/contracts/__tests__/contract-provider-router.test.d.ts +0 -1
  46. package/dist/contracts/__tests__/contract-provider-router.test.js +0 -315
  47. package/dist/contracts/__tests__/contract-proxy.test.d.ts +0 -1
  48. package/dist/contracts/__tests__/contract-proxy.test.js +0 -657
  49. package/dist/contracts/__tests__/contract-resolvers.test.d.ts +0 -1
  50. package/dist/contracts/__tests__/contract-resolvers.test.js +0 -758
  51. package/dist/contracts/__tests__/contract-subscription-lifecycle.test.d.ts +0 -1
  52. package/dist/contracts/__tests__/contract-subscription-lifecycle.test.js +0 -353
  53. package/dist/contracts/__tests__/contract-table-events.test.d.ts +0 -1
  54. package/dist/contracts/__tests__/contract-table-events.test.js +0 -209
  55. package/dist/contracts/__tests__/contract-transport.test.d.ts +0 -1
  56. package/dist/contracts/__tests__/contract-transport.test.js +0 -163
  57. package/dist/contracts/__tests__/extract.test.d.ts +0 -1
  58. package/dist/contracts/__tests__/extract.test.js +0 -145
  59. package/dist/contracts/__tests__/frozen-contracts.test.d.ts +0 -1
  60. package/dist/contracts/__tests__/frozen-contracts.test.js +0 -191
  61. package/dist/contracts/__tests__/generic-tool-dispatch.test.d.ts +0 -1
  62. package/dist/contracts/__tests__/generic-tool-dispatch.test.js +0 -130
  63. package/dist/contracts/__tests__/host-contract-client.test.d.ts +0 -1
  64. package/dist/contracts/__tests__/host-contract-client.test.js +0 -793
  65. package/dist/contracts/__tests__/host-contract-consumer.test.d.ts +0 -1
  66. package/dist/contracts/__tests__/host-contract-consumer.test.js +0 -163
  67. package/dist/contracts/__tests__/host-contract-transport.test.d.ts +0 -1
  68. package/dist/contracts/__tests__/host-contract-transport.test.js +0 -66
  69. package/dist/contracts/__tests__/integration.test.d.ts +0 -1
  70. package/dist/contracts/__tests__/integration.test.js +0 -348
  71. package/dist/contracts/__tests__/legacy-removal.test.d.ts +0 -1
  72. package/dist/contracts/__tests__/legacy-removal.test.js +0 -128
  73. package/dist/contracts/__tests__/persistent-registry.test.d.ts +0 -1
  74. package/dist/contracts/__tests__/persistent-registry.test.js +0 -109
  75. package/dist/contracts/__tests__/playground.test.d.ts +0 -1
  76. package/dist/contracts/__tests__/playground.test.js +0 -216
  77. package/dist/contracts/__tests__/registry.test.d.ts +0 -1
  78. package/dist/contracts/__tests__/registry.test.js +0 -238
  79. package/dist/contracts/__tests__/remote-contract-transport.test.d.ts +0 -1
  80. package/dist/contracts/__tests__/remote-contract-transport.test.js +0 -119
  81. package/dist/contracts/__tests__/system-contracts.test.d.ts +0 -1
  82. package/dist/contracts/__tests__/system-contracts.test.js +0 -247
  83. package/dist/contracts/__tests__/target-dispatch.d.ts +0 -11
  84. package/dist/contracts/__tests__/target-dispatch.js +0 -24
  85. package/dist/contracts/__tests__/tool-catalog-inventory.test.d.ts +0 -1
  86. package/dist/contracts/__tests__/tool-catalog-inventory.test.js +0 -44
  87. package/dist/contracts/__tests__/tool-catalog-service.test.d.ts +0 -1
  88. package/dist/contracts/__tests__/tool-catalog-service.test.js +0 -482
  89. package/dist/contracts/__tests__/tool-catalog.test.d.ts +0 -1
  90. package/dist/contracts/__tests__/tool-catalog.test.js +0 -1199
  91. package/dist/contracts/__tests__/validate.test.d.ts +0 -1
  92. package/dist/contracts/__tests__/validate.test.js +0 -991
  93. package/dist/contracts/__tests__/workflow-context.test.d.ts +0 -1
  94. package/dist/contracts/__tests__/workflow-context.test.js +0 -50
  95. package/dist/data/access-level.test.d.ts +0 -1
  96. package/dist/data/access-level.test.js +0 -53
  97. package/dist/data/change-tracking.test.d.ts +0 -1
  98. package/dist/data/change-tracking.test.js +0 -62
  99. package/dist/data/files/files.test.d.ts +0 -1
  100. package/dist/data/files/files.test.js +0 -770
  101. package/dist/data/group-members.test.d.ts +0 -1
  102. package/dist/data/group-members.test.js +0 -336
  103. package/dist/data/group-share.test.d.ts +0 -1
  104. package/dist/data/group-share.test.js +0 -110
  105. package/dist/data/groups.test.d.ts +0 -1
  106. package/dist/data/groups.test.js +0 -153
  107. package/dist/data/orm/cursor.test.d.ts +0 -1
  108. package/dist/data/orm/cursor.test.js +0 -314
  109. package/dist/data/orm/data-query.mongo.test.d.ts +0 -1
  110. package/dist/data/orm/data-query.mongo.test.js +0 -367
  111. package/dist/data/orm/data-query.sqlite.test.d.ts +0 -1
  112. package/dist/data/orm/data-query.sqlite.test.js +0 -365
  113. package/dist/data/orm/data-query.test.d.ts +0 -1
  114. package/dist/data/orm/data-query.test.js +0 -545
  115. package/dist/data/orm/dependency-injection.test.d.ts +0 -1
  116. package/dist/data/orm/dependency-injection.test.js +0 -171
  117. package/dist/data/orm/event-registry.test.d.ts +0 -1
  118. package/dist/data/orm/event-registry.test.js +0 -44
  119. package/dist/data/orm/multi-cursors.test.d.ts +0 -1
  120. package/dist/data/orm/multi-cursors.test.js +0 -463
  121. package/dist/data/orm/sql.data-source.test.d.ts +0 -1
  122. package/dist/data/orm/sql.data-source.test.js +0 -731
  123. package/dist/data/orm/table-container-events.test.d.ts +0 -1
  124. package/dist/data/orm/table-container-events.test.js +0 -101
  125. package/dist/data/orm/table-container.test.d.ts +0 -1
  126. package/dist/data/orm/table-container.test.js +0 -273
  127. package/dist/data/orm/table.event-source.test.d.ts +0 -1
  128. package/dist/data/orm/table.event-source.test.js +0 -331
  129. package/dist/data/orm/types.test.d.ts +0 -1
  130. package/dist/data/orm/types.test.js +0 -409
  131. package/dist/data/package-table-identity.test.d.ts +0 -1
  132. package/dist/data/package-table-identity.test.js +0 -46
  133. package/dist/data/package-version-permissions.test.d.ts +0 -1
  134. package/dist/data/package-version-permissions.test.js +0 -107
  135. package/dist/data/package-version-resolver.test.d.ts +0 -4
  136. package/dist/data/package-version-resolver.test.js +0 -543
  137. package/dist/data/package-versions.test.d.ts +0 -1
  138. package/dist/data/package-versions.test.js +0 -227
  139. package/dist/data/persistent-vars.conflict.test.d.ts +0 -1
  140. package/dist/data/persistent-vars.conflict.test.js +0 -126
  141. package/dist/data/persistent-vars.migration.test.d.ts +0 -1
  142. package/dist/data/persistent-vars.migration.test.js +0 -133
  143. package/dist/data/persistent-vars.package-scope.test.d.ts +0 -1
  144. package/dist/data/persistent-vars.package-scope.test.js +0 -27
  145. package/dist/data/sync-recovery-queue.test.d.ts +0 -1
  146. package/dist/data/sync-recovery-queue.test.js +0 -215
  147. package/dist/data/table-signing.test.d.ts +0 -1
  148. package/dist/data/table-signing.test.js +0 -118
  149. package/dist/data/tool-access.test.d.ts +0 -1
  150. package/dist/data/tool-access.test.js +0 -166
  151. package/dist/data/user-permissions.test.d.ts +0 -1
  152. package/dist/data/user-permissions.test.js +0 -305
  153. package/dist/data/user-trust-levels.test.d.ts +0 -1
  154. package/dist/data/user-trust-levels.test.js +0 -37
  155. package/dist/define/also-implements.test.d.ts +0 -1
  156. package/dist/define/also-implements.test.js +0 -123
  157. package/dist/define/declare-contract.test.d.ts +0 -1
  158. package/dist/define/declare-contract.test.js +0 -573
  159. package/dist/define/define.test.d.ts +0 -1
  160. package/dist/define/define.test.js +0 -1010
  161. package/dist/define/empty-fields.test.d.ts +0 -1
  162. package/dist/define/empty-fields.test.js +0 -55
  163. package/dist/define/events-observables.test.d.ts +0 -1
  164. package/dist/define/events-observables.test.js +0 -1013
  165. package/dist/define/serve-members.test.d.ts +0 -1
  166. package/dist/define/serve-members.test.js +0 -630
  167. package/dist/define/vars.test.d.ts +0 -1
  168. package/dist/define/vars.test.js +0 -39
  169. package/dist/device/binary-peer-connection-v2.test.d.ts +0 -1
  170. package/dist/device/binary-peer-connection-v2.test.js +0 -730
  171. package/dist/device/binary-peer-connection.d.ts +0 -58
  172. package/dist/device/binary-peer-connection.js +0 -516
  173. package/dist/device/binary-peer-connection.test.d.ts +0 -1
  174. package/dist/device/binary-peer-connection.test.js +0 -184
  175. package/dist/device/connection.test.d.ts +0 -1
  176. package/dist/device/connection.test.js +0 -613
  177. package/dist/device/device-election.test.d.ts +0 -1
  178. package/dist/device/device-election.test.js +0 -516
  179. package/dist/device/device.test.d.ts +0 -1
  180. package/dist/device/device.test.js +0 -243
  181. package/dist/device/frame-cipher.test.d.ts +0 -1
  182. package/dist/device/frame-cipher.test.js +0 -219
  183. package/dist/device/get-trust-level-fn.test.d.ts +0 -1
  184. package/dist/device/get-trust-level-fn.test.js +0 -685
  185. package/dist/device/streamed-socket.d.ts +0 -23
  186. package/dist/device/streamed-socket.js +0 -255
  187. package/dist/device/streamed-socket.test.d.ts +0 -1
  188. package/dist/device/streamed-socket.test.js +0 -216
  189. package/dist/device/tx-encoding.test.d.ts +0 -1
  190. package/dist/device/tx-encoding.test.js +0 -295
  191. package/dist/device-pairing/cpace.test.d.ts +0 -1
  192. package/dist/device-pairing/cpace.test.js +0 -116
  193. package/dist/device-pairing/device-pairing.test.d.ts +0 -1
  194. package/dist/device-pairing/device-pairing.test.js +0 -211
  195. package/dist/hlc.test.d.ts +0 -1
  196. package/dist/hlc.test.js +0 -45
  197. package/dist/host-credentials.test.d.ts +0 -1
  198. package/dist/host-credentials.test.js +0 -88
  199. package/dist/identity/account-email.test.d.ts +0 -1
  200. package/dist/identity/account-email.test.js +0 -59
  201. package/dist/identity/anchor-document.test.d.ts +0 -1
  202. package/dist/identity/anchor-document.test.js +0 -16
  203. package/dist/identity/anchor-resolution.test.d.ts +0 -1
  204. package/dist/identity/anchor-resolution.test.js +0 -130
  205. package/dist/identity/key-change.test.d.ts +0 -1
  206. package/dist/identity/key-change.test.js +0 -811
  207. package/dist/invites/group-approval.test.d.ts +0 -1
  208. package/dist/invites/group-approval.test.js +0 -223
  209. package/dist/invites/invite-token.test.d.ts +0 -1
  210. package/dist/invites/invite-token.test.js +0 -123
  211. package/dist/keys.test.d.ts +0 -1
  212. package/dist/keys.test.js +0 -336
  213. package/dist/logging/console-logger.test.d.ts +0 -1
  214. package/dist/logging/console-logger.test.js +0 -300
  215. package/dist/logging/console-logs.table.test.d.ts +0 -1
  216. package/dist/logging/console-logs.table.test.js +0 -156
  217. package/dist/observable.test.d.ts +0 -1
  218. package/dist/observable.test.js +0 -309
  219. package/dist/package-build/build-package.test.d.ts +0 -1
  220. package/dist/package-build/build-package.test.js +0 -204
  221. package/dist/package-build/contract-add.test.d.ts +0 -1
  222. package/dist/package-build/contract-add.test.js +0 -467
  223. package/dist/package-build/freeze-contracts.test.d.ts +0 -1
  224. package/dist/package-build/freeze-contracts.test.js +0 -355
  225. package/dist/package-installer/package-author-signing.test.d.ts +0 -1
  226. package/dist/package-installer/package-author-signing.test.js +0 -200
  227. package/dist/package-installer/package-cloner.test.d.ts +0 -1
  228. package/dist/package-installer/package-cloner.test.js +0 -278
  229. package/dist/package-installer/package-creator.test.d.ts +0 -1
  230. package/dist/package-installer/package-creator.test.js +0 -908
  231. package/dist/package-installer/package-dev-tools.test.d.ts +0 -1
  232. package/dist/package-installer/package-dev-tools.test.js +0 -64
  233. package/dist/package-installer/package-installer.test.d.ts +0 -1
  234. package/dist/package-installer/package-installer.test.js +0 -882
  235. package/dist/package-installer/package-propagation.test.d.ts +0 -1
  236. package/dist/package-installer/package-propagation.test.js +0 -1269
  237. package/dist/package-installer/package-publisher.test.d.ts +0 -1
  238. package/dist/package-installer/package-publisher.test.js +0 -153
  239. package/dist/package-installer/package-remote-checker.test.d.ts +0 -1
  240. package/dist/package-installer/package-remote-checker.test.js +0 -334
  241. package/dist/package-installer/package-seed-installer.test.d.ts +0 -1
  242. package/dist/package-installer/package-seed-installer.test.js +0 -123
  243. package/dist/package-installer/package-tarball.test.d.ts +0 -1
  244. package/dist/package-installer/package-tarball.test.js +0 -75
  245. package/dist/package-installer/peers-core-installer.test.d.ts +0 -1
  246. package/dist/package-installer/peers-core-installer.test.js +0 -49
  247. package/dist/package-installer/peers-core-trust-anchor.test.d.ts +0 -1
  248. package/dist/package-installer/peers-core-trust-anchor.test.js +0 -155
  249. package/dist/package-loader/contract-package-loader.test.d.ts +0 -1
  250. package/dist/package-loader/contract-package-loader.test.js +0 -274
  251. package/dist/package-loader/host-authority-loading.test.d.ts +0 -1
  252. package/dist/package-loader/host-authority-loading.test.js +0 -104
  253. package/dist/package-loader/host-dispatch-policy.test.d.ts +0 -1
  254. package/dist/package-loader/host-dispatch-policy.test.js +0 -56
  255. package/dist/package-loader/isolated-artifact.test.d.ts +0 -1
  256. package/dist/package-loader/isolated-artifact.test.js +0 -688
  257. package/dist/package-loader/isolated-persistent-var-gateway.test.d.ts +0 -1
  258. package/dist/package-loader/isolated-persistent-var-gateway.test.js +0 -56
  259. package/dist/package-loader/isolated-table-gateway.test.d.ts +0 -1
  260. package/dist/package-loader/isolated-table-gateway.test.js +0 -633
  261. package/dist/package-loader/package-loader.test.d.ts +0 -1
  262. package/dist/package-loader/package-loader.test.js +0 -1764
  263. package/dist/package-loader/resolve-consumed-contracts.test.d.ts +0 -1
  264. package/dist/package-loader/resolve-consumed-contracts.test.js +0 -166
  265. package/dist/serial-json.compat.test.d.ts +0 -1
  266. package/dist/serial-json.compat.test.js +0 -369
  267. package/dist/serial-json.test.d.ts +0 -1
  268. package/dist/serial-json.test.js +0 -401
  269. package/dist/testing/mock-host-contract.test.d.ts +0 -1
  270. package/dist/testing/mock-host-contract.test.js +0 -782
  271. package/dist/utils.test.d.ts +0 -1
  272. 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
- });