@pikku/inspector 0.11.2 → 0.12.1

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 (211) hide show
  1. package/CHANGELOG.md +36 -1
  2. package/OPTIMIZATION-PLAN.md +195 -0
  3. package/dist/add/add-ai-agent.d.ts +2 -0
  4. package/dist/add/add-ai-agent.js +314 -0
  5. package/dist/add/add-channel.js +81 -61
  6. package/dist/add/add-cli.d.ts +1 -1
  7. package/dist/add/add-cli.js +42 -19
  8. package/dist/add/add-file-extends-core-type.d.ts +1 -1
  9. package/dist/add/add-file-with-config.d.ts +1 -1
  10. package/dist/add/add-file-with-factory.d.ts +1 -1
  11. package/dist/add/add-file-with-factory.js +2 -0
  12. package/dist/add/add-functions.d.ts +1 -1
  13. package/dist/add/add-functions.js +256 -82
  14. package/dist/add/add-http-route.d.ts +20 -10
  15. package/dist/add/add-http-route.js +156 -66
  16. package/dist/add/add-http-routes.d.ts +5 -0
  17. package/dist/add/add-http-routes.js +160 -0
  18. package/dist/add/add-keyed-wiring.d.ts +12 -0
  19. package/dist/add/add-keyed-wiring.js +97 -0
  20. package/dist/add/add-mcp-prompt.d.ts +1 -1
  21. package/dist/add/add-mcp-prompt.js +14 -9
  22. package/dist/add/add-mcp-resource.d.ts +1 -1
  23. package/dist/add/add-mcp-resource.js +14 -9
  24. package/dist/add/add-middleware.d.ts +1 -4
  25. package/dist/add/add-middleware.js +364 -79
  26. package/dist/add/add-permission.d.ts +1 -1
  27. package/dist/add/add-permission.js +152 -40
  28. package/dist/add/add-queue-worker.d.ts +1 -1
  29. package/dist/add/add-queue-worker.js +18 -12
  30. package/dist/add/add-rpc-invocations.d.ts +3 -3
  31. package/dist/add/add-rpc-invocations.js +24 -10
  32. package/dist/add/add-schedule.d.ts +1 -1
  33. package/dist/add/add-schedule.js +11 -5
  34. package/dist/add/add-secret.d.ts +3 -0
  35. package/dist/add/add-secret.js +82 -0
  36. package/dist/add/add-trigger.d.ts +2 -0
  37. package/dist/add/add-trigger.js +87 -0
  38. package/dist/add/add-variable.d.ts +1 -0
  39. package/dist/add/add-variable.js +8 -0
  40. package/dist/add/add-wire-addon.d.ts +7 -0
  41. package/dist/add/add-wire-addon.js +70 -0
  42. package/dist/add/add-workflow-graph.d.ts +3 -2
  43. package/dist/add/add-workflow-graph.js +143 -406
  44. package/dist/add/add-workflow.d.ts +1 -1
  45. package/dist/add/add-workflow.js +6 -4
  46. package/dist/error-codes.d.ts +15 -1
  47. package/dist/error-codes.js +20 -1
  48. package/dist/index.d.ts +9 -8
  49. package/dist/index.js +5 -4
  50. package/dist/inspector.d.ts +2 -2
  51. package/dist/inspector.js +95 -15
  52. package/dist/schema-generator.d.ts +1 -0
  53. package/dist/schema-generator.js +1 -0
  54. package/dist/types-map.js +10 -1
  55. package/dist/types.d.ts +180 -50
  56. package/dist/utils/compute-required-schemas.d.ts +4 -0
  57. package/dist/utils/compute-required-schemas.js +40 -0
  58. package/dist/utils/contract-hashes.d.ts +52 -0
  59. package/dist/utils/contract-hashes.js +269 -0
  60. package/dist/utils/custom-types-generator.d.ts +9 -0
  61. package/dist/utils/custom-types-generator.js +71 -0
  62. package/dist/utils/detect-schema-vendor.d.ts +22 -0
  63. package/dist/utils/detect-schema-vendor.js +76 -0
  64. package/dist/utils/does-type-extend-core-type.d.ts +1 -1
  65. package/dist/utils/ensure-function-metadata.d.ts +6 -3
  66. package/dist/utils/ensure-function-metadata.js +220 -6
  67. package/dist/utils/extract-function-name.d.ts +5 -16
  68. package/dist/utils/extract-function-name.js +86 -291
  69. package/dist/utils/extract-services.d.ts +2 -1
  70. package/dist/utils/extract-services.js +25 -1
  71. package/dist/utils/filter-inspector-state.d.ts +1 -1
  72. package/dist/utils/filter-inspector-state.js +107 -23
  73. package/dist/utils/filter-utils.d.ts +2 -2
  74. package/dist/utils/get-files-and-methods.d.ts +1 -1
  75. package/dist/utils/get-property-value.d.ts +6 -1
  76. package/dist/utils/get-property-value.js +28 -3
  77. package/dist/utils/hash.d.ts +2 -0
  78. package/dist/utils/hash.js +23 -0
  79. package/dist/utils/middleware.d.ts +9 -32
  80. package/dist/utils/middleware.js +80 -66
  81. package/dist/utils/permissions.d.ts +4 -4
  82. package/dist/utils/permissions.js +10 -10
  83. package/dist/utils/post-process.d.ts +11 -11
  84. package/dist/utils/post-process.js +247 -24
  85. package/dist/utils/resolve-addon-package.d.ts +16 -0
  86. package/dist/utils/resolve-addon-package.js +34 -0
  87. package/dist/utils/resolve-function-types.d.ts +6 -0
  88. package/dist/utils/resolve-function-types.js +29 -0
  89. package/dist/utils/resolve-identifier.d.ts +10 -0
  90. package/dist/utils/resolve-identifier.js +36 -0
  91. package/dist/utils/resolve-versions.d.ts +2 -0
  92. package/dist/utils/resolve-versions.js +78 -0
  93. package/dist/utils/schema-generator.d.ts +9 -0
  94. package/dist/utils/schema-generator.js +209 -0
  95. package/dist/utils/serialize-inspector-state.d.ts +70 -23
  96. package/dist/utils/serialize-inspector-state.js +98 -22
  97. package/dist/utils/serialize-mcp-json.d.ts +2 -0
  98. package/dist/utils/serialize-mcp-json.js +99 -0
  99. package/dist/utils/serialize-middleware-groups-meta.d.ts +12 -0
  100. package/dist/utils/serialize-middleware-groups-meta.js +28 -0
  101. package/dist/utils/serialize-openapi-json.d.ts +85 -0
  102. package/dist/utils/serialize-openapi-json.js +151 -0
  103. package/dist/utils/serialize-permissions-groups-meta.d.ts +6 -0
  104. package/dist/utils/serialize-permissions-groups-meta.js +31 -0
  105. package/dist/utils/validate-auth-sessionless.d.ts +3 -0
  106. package/dist/utils/validate-auth-sessionless.js +14 -0
  107. package/dist/utils/workflow/dsl/deserialize-dsl-workflow.js +34 -102
  108. package/dist/utils/workflow/dsl/extract-dsl-workflow.d.ts +1 -1
  109. package/dist/utils/workflow/dsl/extract-dsl-workflow.js +23 -4
  110. package/dist/utils/workflow/graph/convert-dsl-to-graph.js +12 -10
  111. package/dist/utils/workflow/graph/finalize-workflow-wires.d.ts +3 -0
  112. package/dist/utils/workflow/graph/finalize-workflow-wires.js +276 -0
  113. package/dist/utils/workflow/graph/finalize-workflows.d.ts +2 -0
  114. package/dist/utils/workflow/graph/finalize-workflows.js +75 -0
  115. package/dist/utils/workflow/graph/index.d.ts +2 -0
  116. package/dist/utils/workflow/graph/index.js +2 -0
  117. package/dist/utils/workflow/graph/serialize-workflow-graph.d.ts +0 -8
  118. package/dist/utils/workflow/graph/serialize-workflow-graph.js +1 -3
  119. package/dist/utils/workflow/graph/workflow-graph.types.d.ts +53 -79
  120. package/dist/utils/workflow/graph/workflow-graph.types.js +1 -1
  121. package/dist/visit.d.ts +1 -1
  122. package/dist/visit.js +13 -6
  123. package/package.json +14 -4
  124. package/src/add/add-ai-agent.ts +468 -0
  125. package/src/add/add-channel.ts +103 -79
  126. package/src/add/add-cli.ts +68 -24
  127. package/src/add/add-file-extends-core-type.ts +1 -1
  128. package/src/add/add-file-with-config.ts +1 -1
  129. package/src/add/add-file-with-factory.ts +3 -1
  130. package/src/add/add-functions.ts +349 -103
  131. package/src/add/add-http-route.ts +263 -89
  132. package/src/add/add-http-routes.ts +229 -0
  133. package/src/add/add-keyed-wiring.ts +151 -0
  134. package/src/add/add-mcp-prompt.ts +27 -16
  135. package/src/add/add-mcp-resource.ts +28 -16
  136. package/src/add/add-middleware.ts +482 -80
  137. package/src/add/add-permission.ts +199 -40
  138. package/src/add/add-queue-worker.ts +25 -20
  139. package/src/add/add-rpc-invocations.ts +28 -11
  140. package/src/add/add-schedule.ts +17 -12
  141. package/src/add/add-secret.ts +140 -0
  142. package/src/add/add-trigger.ts +154 -0
  143. package/src/add/add-variable.ts +9 -0
  144. package/src/add/add-wire-addon.ts +80 -0
  145. package/src/add/add-workflow-graph.ts +180 -522
  146. package/src/add/add-workflow.ts +7 -6
  147. package/src/error-codes.ts +25 -1
  148. package/src/index.ts +23 -13
  149. package/src/inspector.ts +139 -19
  150. package/src/schema-generator.ts +1 -0
  151. package/src/types-map.ts +12 -1
  152. package/src/types.ts +199 -69
  153. package/src/utils/compute-required-schemas.ts +48 -0
  154. package/src/utils/contract-hashes.test.ts +553 -0
  155. package/src/utils/contract-hashes.ts +386 -0
  156. package/src/utils/custom-types-generator.ts +88 -0
  157. package/src/utils/detect-schema-vendor.ts +90 -0
  158. package/src/utils/does-type-extend-core-type.ts +1 -1
  159. package/src/utils/ensure-function-metadata.ts +325 -8
  160. package/src/utils/extract-function-name.ts +101 -351
  161. package/src/utils/extract-services.ts +35 -2
  162. package/src/utils/filter-inspector-state.test.ts +37 -25
  163. package/src/utils/filter-inspector-state.ts +146 -32
  164. package/src/utils/filter-utils.test.ts +1 -1
  165. package/src/utils/filter-utils.ts +2 -2
  166. package/src/utils/get-files-and-methods.ts +1 -1
  167. package/src/utils/get-property-value.ts +42 -4
  168. package/src/utils/hash.ts +26 -0
  169. package/src/utils/middleware.test.ts +204 -0
  170. package/src/utils/middleware.ts +131 -69
  171. package/src/utils/permissions.test.ts +35 -12
  172. package/src/utils/permissions.ts +12 -12
  173. package/src/utils/post-process.ts +306 -44
  174. package/src/utils/resolve-addon-package.ts +49 -0
  175. package/src/utils/resolve-function-types.ts +42 -0
  176. package/src/utils/resolve-identifier.ts +46 -0
  177. package/src/utils/resolve-versions.test.ts +249 -0
  178. package/src/utils/resolve-versions.ts +105 -0
  179. package/src/utils/schema-generator.ts +329 -0
  180. package/src/utils/serialize-inspector-state.ts +184 -43
  181. package/src/utils/serialize-mcp-json.ts +145 -0
  182. package/src/utils/serialize-middleware-groups-meta.ts +33 -0
  183. package/src/utils/serialize-openapi-json.ts +277 -0
  184. package/src/utils/serialize-permissions-groups-meta.ts +35 -0
  185. package/src/utils/test-data/inspector-state.json +69 -66
  186. package/src/utils/validate-auth-sessionless.ts +29 -0
  187. package/src/utils/workflow/dsl/deserialize-dsl-workflow.ts +43 -119
  188. package/src/utils/workflow/dsl/extract-dsl-workflow.ts +26 -6
  189. package/src/utils/workflow/graph/convert-dsl-to-graph.ts +17 -10
  190. package/src/utils/workflow/graph/finalize-workflow-wires.ts +310 -0
  191. package/src/utils/workflow/graph/finalize-workflows.ts +100 -0
  192. package/src/utils/workflow/graph/index.ts +5 -0
  193. package/src/utils/workflow/graph/serialize-workflow-graph.ts +1 -8
  194. package/src/utils/workflow/graph/workflow-graph.types.ts +29 -78
  195. package/src/visit.ts +19 -7
  196. package/tsconfig.tsbuildinfo +1 -1
  197. package/dist/add/add-forge-credential.d.ts +0 -8
  198. package/dist/add/add-forge-credential.js +0 -77
  199. package/dist/add/add-forge-node.d.ts +0 -7
  200. package/dist/add/add-forge-node.js +0 -77
  201. package/dist/add/add-mcp-tool.d.ts +0 -2
  202. package/dist/add/add-mcp-tool.js +0 -81
  203. package/dist/utils/extract-service-metadata.d.ts +0 -19
  204. package/dist/utils/extract-service-metadata.js +0 -244
  205. package/dist/utils/write-service-metadata.d.ts +0 -13
  206. package/dist/utils/write-service-metadata.js +0 -37
  207. package/src/add/add-forge-credential.ts +0 -119
  208. package/src/add/add-forge-node.ts +0 -132
  209. package/src/add/add-mcp-tool.ts +0 -141
  210. package/src/utils/extract-service-metadata.ts +0 -353
  211. package/src/utils/write-service-metadata.ts +0 -51
@@ -0,0 +1,553 @@
1
+ import { strict as assert } from 'assert'
2
+ import { describe, test } from 'node:test'
3
+ import { canonicalJSON } from './hash.js'
4
+ import {
5
+ computeContractHash,
6
+ createEmptyManifest,
7
+ serializeManifest,
8
+ validateContracts,
9
+ updateManifest,
10
+ extractContractsFromMeta,
11
+ } from './contract-hashes.js'
12
+ import type { ContractEntry, VersionManifest } from './contract-hashes.js'
13
+ import type { FunctionsMeta } from '@pikku/core'
14
+ import { ErrorCode } from '../error-codes.js'
15
+
16
+ function makeContracts(
17
+ entries: Record<
18
+ string,
19
+ {
20
+ functionKey: string
21
+ version: number
22
+ contractHash: string
23
+ inputHash?: string
24
+ outputHash?: string
25
+ }
26
+ >
27
+ ): Map<string, ContractEntry> {
28
+ const map = new Map<string, ContractEntry>()
29
+ for (const [id, entry] of Object.entries(entries)) {
30
+ map.set(id, {
31
+ ...entry,
32
+ inputHash: entry.inputHash ?? entry.contractHash.slice(0, 8),
33
+ outputHash: entry.outputHash ?? entry.contractHash.slice(0, 8),
34
+ })
35
+ }
36
+ return map
37
+ }
38
+
39
+ describe('canonicalJSON', () => {
40
+ test('sorts object keys alphabetically', () => {
41
+ assert.strictEqual(canonicalJSON({ b: 2, a: 1 }), '{"a":1,"b":2}')
42
+ })
43
+
44
+ test('sorts nested objects recursively', () => {
45
+ const input = { z: { b: 2, a: 1 }, a: { d: 4, c: 3 } }
46
+ const expected = '{"a":{"c":3,"d":4},"z":{"a":1,"b":2}}'
47
+ assert.strictEqual(canonicalJSON(input), expected)
48
+ })
49
+
50
+ test('preserves array order', () => {
51
+ assert.strictEqual(canonicalJSON([3, 1, 2]), '[3,1,2]')
52
+ assert.strictEqual(
53
+ canonicalJSON([
54
+ { b: 1, a: 2 },
55
+ { d: 3, c: 4 },
56
+ ]),
57
+ '[{"a":2,"b":1},{"c":4,"d":3}]'
58
+ )
59
+ })
60
+
61
+ test('handles null/undefined/primitives', () => {
62
+ assert.strictEqual(canonicalJSON(null), 'null')
63
+ assert.strictEqual(canonicalJSON(undefined), undefined)
64
+ assert.strictEqual(canonicalJSON(42), '42')
65
+ assert.strictEqual(canonicalJSON('hello'), '"hello"')
66
+ assert.strictEqual(canonicalJSON(true), 'true')
67
+ })
68
+
69
+ test('deterministic regardless of insertion order', () => {
70
+ const a: Record<string, number> = {}
71
+ a['x'] = 1
72
+ a['a'] = 2
73
+ a['m'] = 3
74
+
75
+ const b: Record<string, number> = {}
76
+ b['m'] = 3
77
+ b['a'] = 2
78
+ b['x'] = 1
79
+
80
+ assert.strictEqual(canonicalJSON(a), canonicalJSON(b))
81
+ })
82
+ })
83
+
84
+ describe('computeContractHash', () => {
85
+ test('returns 16-char hex string', () => {
86
+ const hash = computeContractHash({
87
+ functionKey: 'createUser',
88
+ inputSchema: { type: 'object' },
89
+ outputSchema: { type: 'object' },
90
+ })
91
+ assert.match(hash, /^[0-9a-f]{16}$/)
92
+ })
93
+
94
+ test('stable across calls', () => {
95
+ const data = {
96
+ functionKey: 'getUser',
97
+ inputSchema: { id: 'string' },
98
+ outputSchema: { name: 'string' },
99
+ }
100
+ assert.strictEqual(computeContractHash(data), computeContractHash(data))
101
+ })
102
+
103
+ test('different functionKey produces different hash', () => {
104
+ const schema = { type: 'object' }
105
+ const hash1 = computeContractHash({
106
+ functionKey: 'createUser',
107
+ inputSchema: schema,
108
+ outputSchema: schema,
109
+ })
110
+ const hash2 = computeContractHash({
111
+ functionKey: 'deleteUser',
112
+ inputSchema: schema,
113
+ outputSchema: schema,
114
+ })
115
+ assert.notStrictEqual(hash1, hash2)
116
+ })
117
+
118
+ test('different schema produces different hash', () => {
119
+ const hash1 = computeContractHash({
120
+ functionKey: 'createUser',
121
+ inputSchema: { type: 'string' },
122
+ outputSchema: null,
123
+ })
124
+ const hash2 = computeContractHash({
125
+ functionKey: 'createUser',
126
+ inputSchema: { type: 'number' },
127
+ outputSchema: null,
128
+ })
129
+ assert.notStrictEqual(hash1, hash2)
130
+ })
131
+ })
132
+
133
+ describe('createEmptyManifest', () => {
134
+ test('returns empty manifest with version 1', () => {
135
+ assert.deepStrictEqual(createEmptyManifest(), {
136
+ manifestVersion: 1,
137
+ contracts: {},
138
+ })
139
+ })
140
+ })
141
+
142
+ describe('serializeManifest', () => {
143
+ test('sorts contract keys alphabetically', () => {
144
+ const manifest: VersionManifest = {
145
+ manifestVersion: 1,
146
+ contracts: {
147
+ zeta: { latest: 1, versions: { '1': 'abc' } },
148
+ alpha: { latest: 1, versions: { '1': 'def' } },
149
+ },
150
+ }
151
+ const result = serializeManifest(manifest)
152
+ const keys = Object.keys(JSON.parse(result).contracts)
153
+ assert.deepStrictEqual(keys, ['alpha', 'zeta'])
154
+ })
155
+
156
+ test('sorts version keys numerically', () => {
157
+ const manifest: VersionManifest = {
158
+ manifestVersion: 1,
159
+ contracts: {
160
+ func: { latest: 10, versions: { '10': 'c', '2': 'b', '1': 'a' } },
161
+ },
162
+ }
163
+ const result = serializeManifest(manifest)
164
+ const versionKeys = Object.keys(JSON.parse(result).contracts.func.versions)
165
+ assert.deepStrictEqual(versionKeys, ['1', '2', '10'])
166
+ })
167
+
168
+ test('ends with trailing newline', () => {
169
+ const manifest: VersionManifest = {
170
+ manifestVersion: 1,
171
+ contracts: {},
172
+ }
173
+ const result = serializeManifest(manifest)
174
+ assert.ok(result.endsWith('\n'))
175
+ })
176
+
177
+ test('idempotent', () => {
178
+ const manifest: VersionManifest = {
179
+ manifestVersion: 1,
180
+ contracts: {
181
+ beta: { latest: 2, versions: { '2': 'xyz', '1': 'abc' } },
182
+ alpha: { latest: 1, versions: { '1': 'def' } },
183
+ },
184
+ }
185
+ const first = serializeManifest(manifest)
186
+ const second = serializeManifest(JSON.parse(first))
187
+ assert.strictEqual(first, second)
188
+ })
189
+ })
190
+
191
+ describe('validateContracts', () => {
192
+ describe('PKU861 — FUNCTION_VERSION_MODIFIED', () => {
193
+ test('hash changed for existing version produces error', () => {
194
+ const manifest: VersionManifest = {
195
+ manifestVersion: 1,
196
+ contracts: {
197
+ createUser: { latest: 1, versions: { '1': 'oldhash1234567890' } },
198
+ },
199
+ }
200
+ const contracts = makeContracts({
201
+ createUser: {
202
+ functionKey: 'createUser',
203
+ version: 1,
204
+ contractHash: 'newhash1234567890',
205
+ },
206
+ })
207
+ const result = validateContracts(manifest, contracts)
208
+ assert.strictEqual(result.valid, false)
209
+ assert.strictEqual(result.errors.length, 1)
210
+ assert.strictEqual(
211
+ result.errors[0].code,
212
+ ErrorCode.FUNCTION_VERSION_MODIFIED
213
+ )
214
+ })
215
+
216
+ test('hash unchanged for existing version produces no error', () => {
217
+ const manifest: VersionManifest = {
218
+ manifestVersion: 1,
219
+ contracts: {
220
+ createUser: { latest: 1, versions: { '1': 'samehash123456789' } },
221
+ },
222
+ }
223
+ const contracts = makeContracts({
224
+ createUser: {
225
+ functionKey: 'createUser',
226
+ version: 1,
227
+ contractHash: 'samehash123456789',
228
+ },
229
+ })
230
+ const result = validateContracts(manifest, contracts)
231
+ assert.strictEqual(result.valid, true)
232
+ assert.strictEqual(result.errors.length, 0)
233
+ })
234
+ })
235
+
236
+ describe('PKU862 — CONTRACT_CHANGED_REQUIRES_BUMP', () => {
237
+ test('latest hash changed without recorded version produces bump error', () => {
238
+ const manifest: VersionManifest = {
239
+ manifestVersion: 1,
240
+ contracts: {
241
+ getUser: { latest: 1, versions: {} },
242
+ },
243
+ }
244
+ const contracts = makeContracts({
245
+ getUser: {
246
+ functionKey: 'getUser',
247
+ version: 1,
248
+ contractHash: 'newhash1234567890',
249
+ },
250
+ })
251
+ const result = validateContracts(manifest, contracts)
252
+ const bump = result.errors.find(
253
+ (e) => e.code === ErrorCode.CONTRACT_CHANGED_REQUIRES_BUMP
254
+ )
255
+ assert.ok(bump)
256
+ })
257
+
258
+ test('function removed from code but still in manifest produces no error', () => {
259
+ const manifest: VersionManifest = {
260
+ manifestVersion: 1,
261
+ contracts: {
262
+ deletedFunc: { latest: 1, versions: { '1': 'abc1234567890abc' } },
263
+ },
264
+ }
265
+ const contracts = makeContracts({})
266
+ const result = validateContracts(manifest, contracts)
267
+ assert.strictEqual(result.valid, true)
268
+ assert.strictEqual(result.errors.length, 0)
269
+ })
270
+ })
271
+
272
+ describe('PKU863 — VERSION_REGRESSION_OR_CONFLICT', () => {
273
+ test('new version <= latest but not recorded produces error', () => {
274
+ const manifest: VersionManifest = {
275
+ manifestVersion: 1,
276
+ contracts: {
277
+ createUser: {
278
+ latest: 3,
279
+ versions: { '1': 'aaa', '2': 'bbb', '3': 'ccc' },
280
+ },
281
+ },
282
+ }
283
+ const contracts = makeContracts({
284
+ 'createUser@v2': {
285
+ functionKey: 'createUser',
286
+ version: 2,
287
+ contractHash: 'bbb',
288
+ },
289
+ 'createUser@v3': {
290
+ functionKey: 'createUser',
291
+ version: 3,
292
+ contractHash: 'ccc',
293
+ },
294
+ 'createUser@v1': {
295
+ functionKey: 'createUser',
296
+ version: 1,
297
+ contractHash: 'different_hash!',
298
+ },
299
+ })
300
+ const result = validateContracts(manifest, contracts)
301
+ const regression = result.errors.find(
302
+ (e) => e.code === ErrorCode.VERSION_REGRESSION_OR_CONFLICT
303
+ )
304
+ assert.strictEqual(regression, undefined)
305
+
306
+ const manifest2: VersionManifest = {
307
+ manifestVersion: 1,
308
+ contracts: {
309
+ myFunc: { latest: 3, versions: { '1': 'aaa', '3': 'ccc' } },
310
+ },
311
+ }
312
+ const contracts2 = makeContracts({
313
+ 'myFunc@v2': { functionKey: 'myFunc', version: 2, contractHash: 'xxx' },
314
+ 'myFunc@v3': { functionKey: 'myFunc', version: 3, contractHash: 'ccc' },
315
+ })
316
+ const result2 = validateContracts(manifest2, contracts2)
317
+ const regression2 = result2.errors.find(
318
+ (e) => e.code === ErrorCode.VERSION_REGRESSION_OR_CONFLICT
319
+ )
320
+ assert.ok(regression2)
321
+ })
322
+ })
323
+
324
+ describe('PKU864 — VERSION_GAP_NOT_ALLOWED', () => {
325
+ test('new version > latest + 1 produces error', () => {
326
+ const manifest: VersionManifest = {
327
+ manifestVersion: 1,
328
+ contracts: {
329
+ createUser: { latest: 1, versions: { '1': 'aaa' } },
330
+ },
331
+ }
332
+ const contracts = makeContracts({
333
+ 'createUser@v3': {
334
+ functionKey: 'createUser',
335
+ version: 3,
336
+ contractHash: 'xxx',
337
+ },
338
+ })
339
+ const result = validateContracts(manifest, contracts)
340
+ const gap = result.errors.find(
341
+ (e) => e.code === ErrorCode.VERSION_GAP_NOT_ALLOWED
342
+ )
343
+ assert.ok(gap)
344
+ })
345
+ })
346
+
347
+ describe('PKU865 — MANIFEST_INTEGRITY_ERROR', () => {
348
+ test('latest field not matching max version key produces error', () => {
349
+ const manifest: VersionManifest = {
350
+ manifestVersion: 1,
351
+ contracts: {
352
+ createUser: { latest: 1, versions: { '1': 'aaa', '2': 'bbb' } },
353
+ },
354
+ }
355
+ const contracts = makeContracts({})
356
+ const result = validateContracts(manifest, contracts)
357
+ const integrity = result.errors.find(
358
+ (e) => e.code === ErrorCode.MANIFEST_INTEGRITY_ERROR
359
+ )
360
+ assert.ok(integrity)
361
+ })
362
+ })
363
+
364
+ describe('happy paths', () => {
365
+ test('new function not in manifest is valid', () => {
366
+ const manifest: VersionManifest = {
367
+ manifestVersion: 1,
368
+ contracts: {},
369
+ }
370
+ const contracts = makeContracts({
371
+ createUser: {
372
+ functionKey: 'createUser',
373
+ version: 1,
374
+ contractHash: 'abc123',
375
+ },
376
+ })
377
+ const result = validateContracts(manifest, contracts)
378
+ assert.strictEqual(result.valid, true)
379
+ })
380
+
381
+ test('correct version bump is valid', () => {
382
+ const manifest: VersionManifest = {
383
+ manifestVersion: 1,
384
+ contracts: {
385
+ createUser: { latest: 1, versions: { '1': 'aaa' } },
386
+ },
387
+ }
388
+ const contracts = makeContracts({
389
+ 'createUser@v1': {
390
+ functionKey: 'createUser',
391
+ version: 1,
392
+ contractHash: 'aaa',
393
+ },
394
+ 'createUser@v2': {
395
+ functionKey: 'createUser',
396
+ version: 2,
397
+ contractHash: 'bbb',
398
+ },
399
+ })
400
+ const result = validateContracts(manifest, contracts)
401
+ assert.strictEqual(result.valid, true)
402
+ })
403
+
404
+ test('empty manifest with new contracts is valid', () => {
405
+ const manifest: VersionManifest = {
406
+ manifestVersion: 1,
407
+ contracts: {},
408
+ }
409
+ const contracts = makeContracts({
410
+ funcA: { functionKey: 'funcA', version: 1, contractHash: 'aaa' },
411
+ funcB: { functionKey: 'funcB', version: 1, contractHash: 'bbb' },
412
+ })
413
+ const result = validateContracts(manifest, contracts)
414
+ assert.strictEqual(result.valid, true)
415
+ })
416
+ })
417
+ })
418
+
419
+ describe('updateManifest', () => {
420
+ test('adds new function to empty manifest', () => {
421
+ const manifest: VersionManifest = { manifestVersion: 1, contracts: {} }
422
+ const contracts = makeContracts({
423
+ createUser: {
424
+ functionKey: 'createUser',
425
+ version: 1,
426
+ contractHash: 'abc',
427
+ inputHash: 'in123456',
428
+ outputHash: 'out1234',
429
+ },
430
+ })
431
+ const result = updateManifest(manifest, contracts)
432
+ assert.strictEqual(result.contracts.createUser.latest, 1)
433
+ assert.deepStrictEqual(result.contracts.createUser.versions['1'], {
434
+ inputHash: 'in123456',
435
+ outputHash: 'out1234',
436
+ })
437
+ })
438
+
439
+ test('adds new version to existing function and updates latest', () => {
440
+ const manifest: VersionManifest = {
441
+ manifestVersion: 1,
442
+ contracts: {
443
+ createUser: { latest: 1, versions: { '1': 'aaa' } },
444
+ },
445
+ }
446
+ const contracts = makeContracts({
447
+ 'createUser@v1': {
448
+ functionKey: 'createUser',
449
+ version: 1,
450
+ contractHash: 'aaa',
451
+ inputHash: 'inaaa123',
452
+ outputHash: 'outaaa12',
453
+ },
454
+ 'createUser@v2': {
455
+ functionKey: 'createUser',
456
+ version: 2,
457
+ contractHash: 'bbb',
458
+ inputHash: 'inbbb123',
459
+ outputHash: 'outbbb12',
460
+ },
461
+ })
462
+ const result = updateManifest(manifest, contracts)
463
+ assert.strictEqual(result.contracts.createUser.latest, 2)
464
+ assert.deepStrictEqual(result.contracts.createUser.versions['2'], {
465
+ inputHash: 'inbbb123',
466
+ outputHash: 'outbbb12',
467
+ })
468
+ assert.deepStrictEqual(result.contracts.createUser.versions['1'], {
469
+ inputHash: 'inaaa123',
470
+ outputHash: 'outaaa12',
471
+ })
472
+ })
473
+
474
+ test('preserves deleted functions', () => {
475
+ const manifest: VersionManifest = {
476
+ manifestVersion: 1,
477
+ contracts: {
478
+ removedFunc: { latest: 1, versions: { '1': 'old' } },
479
+ },
480
+ }
481
+ const contracts = makeContracts({
482
+ newFunc: { functionKey: 'newFunc', version: 1, contractHash: 'new' },
483
+ })
484
+ const result = updateManifest(manifest, contracts)
485
+ assert.ok(result.contracts.removedFunc)
486
+ assert.strictEqual(result.contracts.removedFunc.latest, 1)
487
+ assert.ok(result.contracts.newFunc)
488
+ })
489
+
490
+ test('does not mutate input manifest', () => {
491
+ const manifest: VersionManifest = {
492
+ manifestVersion: 1,
493
+ contracts: {
494
+ createUser: { latest: 1, versions: { '1': 'aaa' } },
495
+ },
496
+ }
497
+ const before = JSON.stringify(manifest)
498
+ const contracts = makeContracts({
499
+ 'createUser@v2': {
500
+ functionKey: 'createUser',
501
+ version: 2,
502
+ contractHash: 'bbb',
503
+ },
504
+ })
505
+ updateManifest(manifest, contracts)
506
+ assert.strictEqual(JSON.stringify(manifest), before)
507
+ })
508
+ })
509
+
510
+ describe('extractContractsFromMeta', () => {
511
+ test('extracts entries where contractHash is set', () => {
512
+ const meta: FunctionsMeta = {
513
+ createUser: {
514
+ pikkuFuncId: 'createUser',
515
+ inputSchemaName: 'CreateUserInput',
516
+ outputSchemaName: 'CreateUserOutput',
517
+ contractHash: 'abc123',
518
+ },
519
+ }
520
+ const result = extractContractsFromMeta(meta)
521
+ assert.strictEqual(result.size, 1)
522
+ assert.ok(result.has('createUser'))
523
+ assert.strictEqual(result.get('createUser')!.contractHash, 'abc123')
524
+ assert.strictEqual(result.get('createUser')!.functionKey, 'createUser')
525
+ assert.strictEqual(result.get('createUser')!.version, 1)
526
+ })
527
+
528
+ test('skips remote functions', () => {
529
+ const meta: FunctionsMeta = {
530
+ remoteFunc: {
531
+ pikkuFuncId: 'remoteFunc',
532
+ inputSchemaName: null,
533
+ outputSchemaName: null,
534
+ remote: true,
535
+ contractHash: 'shouldskip',
536
+ },
537
+ }
538
+ const result = extractContractsFromMeta(meta)
539
+ assert.strictEqual(result.size, 0)
540
+ })
541
+
542
+ test('skips functions without contractHash', () => {
543
+ const meta: FunctionsMeta = {
544
+ noHash: {
545
+ pikkuFuncId: 'noHash',
546
+ inputSchemaName: null,
547
+ outputSchemaName: null,
548
+ },
549
+ }
550
+ const result = extractContractsFromMeta(meta)
551
+ assert.strictEqual(result.size, 0)
552
+ })
553
+ })