@0xsequence/catapult 1.3.17 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (124) hide show
  1. package/README.md +249 -0
  2. package/dist/cli.d.ts.map +1 -1
  3. package/dist/cli.js +1 -0
  4. package/dist/cli.js.map +1 -1
  5. package/dist/commands/index.d.ts +1 -0
  6. package/dist/commands/index.d.ts.map +1 -1
  7. package/dist/commands/index.js +1 -0
  8. package/dist/commands/index.js.map +1 -1
  9. package/dist/commands/list.d.ts.map +1 -1
  10. package/dist/commands/list.js +12 -0
  11. package/dist/commands/list.js.map +1 -1
  12. package/dist/commands/provenance.d.ts +3 -0
  13. package/dist/commands/provenance.d.ts.map +1 -0
  14. package/dist/commands/provenance.js +138 -0
  15. package/dist/commands/provenance.js.map +1 -0
  16. package/dist/lib/__tests__/deployer.spec.js +118 -1
  17. package/dist/lib/__tests__/deployer.spec.js.map +1 -1
  18. package/dist/lib/__tests__/provenance.spec.d.ts +2 -0
  19. package/dist/lib/__tests__/provenance.spec.d.ts.map +1 -0
  20. package/dist/lib/__tests__/provenance.spec.js +205 -0
  21. package/dist/lib/__tests__/provenance.spec.js.map +1 -0
  22. package/dist/lib/contracts/__tests__/repository.spec.js +243 -0
  23. package/dist/lib/contracts/__tests__/repository.spec.js.map +1 -1
  24. package/dist/lib/contracts/repository.d.ts +9 -1
  25. package/dist/lib/contracts/repository.d.ts.map +1 -1
  26. package/dist/lib/contracts/repository.js +93 -7
  27. package/dist/lib/contracts/repository.js.map +1 -1
  28. package/dist/lib/core/__tests__/assert-action.spec.d.ts +2 -0
  29. package/dist/lib/core/__tests__/assert-action.spec.d.ts.map +1 -0
  30. package/dist/lib/core/__tests__/assert-action.spec.js +377 -0
  31. package/dist/lib/core/__tests__/assert-action.spec.js.map +1 -0
  32. package/dist/lib/core/__tests__/engine.spec.js +80 -0
  33. package/dist/lib/core/__tests__/engine.spec.js.map +1 -1
  34. package/dist/lib/core/__tests__/loader.spec.js +29 -0
  35. package/dist/lib/core/__tests__/loader.spec.js.map +1 -1
  36. package/dist/lib/core/__tests__/resolver.spec.js +383 -0
  37. package/dist/lib/core/__tests__/resolver.spec.js.map +1 -1
  38. package/dist/lib/core/engine.d.ts.map +1 -1
  39. package/dist/lib/core/engine.js +33 -0
  40. package/dist/lib/core/engine.js.map +1 -1
  41. package/dist/lib/core/loader.d.ts +1 -0
  42. package/dist/lib/core/loader.d.ts.map +1 -1
  43. package/dist/lib/core/loader.js +6 -1
  44. package/dist/lib/core/loader.js.map +1 -1
  45. package/dist/lib/core/resolver.d.ts +2 -0
  46. package/dist/lib/core/resolver.d.ts.map +1 -1
  47. package/dist/lib/core/resolver.js +89 -0
  48. package/dist/lib/core/resolver.js.map +1 -1
  49. package/dist/lib/deployer.d.ts.map +1 -1
  50. package/dist/lib/deployer.js +21 -4
  51. package/dist/lib/deployer.js.map +1 -1
  52. package/dist/lib/index.d.ts +1 -0
  53. package/dist/lib/index.d.ts.map +1 -1
  54. package/dist/lib/index.js +1 -0
  55. package/dist/lib/index.js.map +1 -1
  56. package/dist/lib/parsers/__tests__/job.spec.js +77 -0
  57. package/dist/lib/parsers/__tests__/job.spec.js.map +1 -1
  58. package/dist/lib/parsers/__tests__/source.spec.d.ts +2 -0
  59. package/dist/lib/parsers/__tests__/source.spec.d.ts.map +1 -0
  60. package/dist/lib/parsers/__tests__/source.spec.js +121 -0
  61. package/dist/lib/parsers/__tests__/source.spec.js.map +1 -0
  62. package/dist/lib/parsers/index.d.ts +1 -0
  63. package/dist/lib/parsers/index.d.ts.map +1 -1
  64. package/dist/lib/parsers/index.js +1 -0
  65. package/dist/lib/parsers/index.js.map +1 -1
  66. package/dist/lib/parsers/job.d.ts.map +1 -1
  67. package/dist/lib/parsers/job.js +11 -0
  68. package/dist/lib/parsers/job.js.map +1 -1
  69. package/dist/lib/parsers/source.d.ts +4 -0
  70. package/dist/lib/parsers/source.d.ts.map +1 -0
  71. package/dist/lib/parsers/source.js +107 -0
  72. package/dist/lib/parsers/source.js.map +1 -0
  73. package/dist/lib/provenance.d.ts +34 -0
  74. package/dist/lib/provenance.d.ts.map +1 -0
  75. package/dist/lib/provenance.js +645 -0
  76. package/dist/lib/provenance.js.map +1 -0
  77. package/dist/lib/types/actions.d.ts +18 -2
  78. package/dist/lib/types/actions.d.ts.map +1 -1
  79. package/dist/lib/types/actions.js +1 -0
  80. package/dist/lib/types/actions.js.map +1 -1
  81. package/dist/lib/types/contracts.d.ts +3 -0
  82. package/dist/lib/types/contracts.d.ts.map +1 -1
  83. package/dist/lib/types/definitions.d.ts +1 -0
  84. package/dist/lib/types/definitions.d.ts.map +1 -1
  85. package/dist/lib/types/index.d.ts +1 -0
  86. package/dist/lib/types/index.d.ts.map +1 -1
  87. package/dist/lib/types/index.js +1 -0
  88. package/dist/lib/types/index.js.map +1 -1
  89. package/dist/lib/types/source.d.ts +24 -0
  90. package/dist/lib/types/source.d.ts.map +1 -0
  91. package/dist/lib/types/source.js +3 -0
  92. package/dist/lib/types/source.js.map +1 -0
  93. package/dist/lib/types/values.d.ts +33 -1
  94. package/dist/lib/types/values.d.ts.map +1 -1
  95. package/package.json +1 -1
  96. package/src/cli.ts +3 -2
  97. package/src/commands/index.ts +2 -1
  98. package/src/commands/list.ts +14 -1
  99. package/src/commands/provenance.ts +120 -0
  100. package/src/lib/__tests__/deployer.spec.ts +177 -1
  101. package/src/lib/__tests__/provenance.spec.ts +208 -0
  102. package/src/lib/contracts/__tests__/repository.spec.ts +270 -2
  103. package/src/lib/contracts/repository.ts +112 -14
  104. package/src/lib/core/__tests__/assert-action.spec.ts +474 -0
  105. package/src/lib/core/__tests__/engine.spec.ts +116 -0
  106. package/src/lib/core/__tests__/loader.spec.ts +34 -1
  107. package/src/lib/core/__tests__/resolver.spec.ts +444 -1
  108. package/src/lib/core/engine.ts +52 -0
  109. package/src/lib/core/loader.ts +8 -2
  110. package/src/lib/core/resolver.ts +116 -0
  111. package/src/lib/deployer.ts +28 -4
  112. package/src/lib/index.ts +4 -1
  113. package/src/lib/parsers/__tests__/job.spec.ts +81 -0
  114. package/src/lib/parsers/__tests__/source.spec.ts +134 -0
  115. package/src/lib/parsers/index.ts +1 -0
  116. package/src/lib/parsers/job.ts +14 -2
  117. package/src/lib/parsers/source.ts +129 -0
  118. package/src/lib/provenance.ts +785 -0
  119. package/src/lib/types/actions.ts +22 -1
  120. package/src/lib/types/contracts.ts +4 -1
  121. package/src/lib/types/definitions.ts +7 -0
  122. package/src/lib/types/index.ts +1 -0
  123. package/src/lib/types/source.ts +26 -0
  124. package/src/lib/types/values.ts +71 -0
@@ -2,23 +2,26 @@ import * as fs from 'fs/promises'
2
2
  import * as path from 'path'
3
3
  import { createHash } from 'crypto'
4
4
  import { Contract } from '../types/contracts'
5
+ import { BuildInfoSourceProvenance, SourceProvenance } from '../types/source'
5
6
  import { parseArtifact } from '../parsers/artifact'
6
7
  import { parseBuildInfo, isBuildInfoFile } from '../parsers/buildinfo'
8
+ import { mergeSourceProvenance, parseSourceDocument } from '../parsers/source'
7
9
 
8
10
  export class ContractRepository {
9
11
  private contracts: Map<string, Contract> = new Map()
10
12
  private referenceMap: Map<string, string[]> = new Map()
11
13
  private ambiguousReferences: Set<string> = new Set()
14
+ private sourceProvenanceByBuildInfoPath: Map<string, BuildInfoSourceProvenance> = new Map()
12
15
 
13
16
  /**
14
17
  * Main entry point that orchestrates the discovery and hydration process
15
18
  */
16
19
  public async loadFrom(projectRoot: string): Promise<void> {
17
- // Step 1: Discover all .json files
18
- const files = await this.findContractFiles(projectRoot)
20
+ const files = await this.findProjectFiles(projectRoot)
19
21
 
20
- // Step 2: Parse and hydrate contracts from all discovered files
21
- for (const filePath of files) {
22
+ await this.loadSourceProvenanceFiles(files.sourceFiles)
23
+
24
+ for (const filePath of files.contractFiles) {
22
25
  try {
23
26
  const content = await fs.readFile(filePath, 'utf-8')
24
27
  await this.parseAndHydrateFromFile(content, filePath)
@@ -27,7 +30,6 @@ export class ContractRepository {
27
30
  }
28
31
  }
29
32
 
30
- // Step 3: Build reference maps and identify ambiguous references
31
33
  this.disambiguateReferences()
32
34
  }
33
35
 
@@ -40,6 +42,7 @@ export class ContractRepository {
40
42
  const extractedContracts = parseBuildInfo(content, filePath)
41
43
  if (extractedContracts) {
42
44
  for (const extracted of extractedContracts) {
45
+ const sourceProvenance = this.getSourceProvenance(filePath, extracted.fullyQualifiedName)
43
46
  this.hydrateContract({
44
47
  creationCode: extracted.bytecode,
45
48
  runtimeBytecode: extracted.deployedBytecode,
@@ -49,6 +52,7 @@ export class ContractRepository {
49
52
  source: extracted.source,
50
53
  compiler: extracted.compiler,
51
54
  buildInfoId: extracted.buildInfoId,
55
+ sourceProvenance,
52
56
  }, filePath)
53
57
  }
54
58
  return
@@ -82,6 +86,7 @@ export class ContractRepository {
82
86
  source?: string
83
87
  compiler?: any
84
88
  buildInfoId?: string
89
+ sourceProvenance?: SourceProvenance
85
90
  }, sourceFilePath: string): void {
86
91
  // Validate that we have creation code for hashing (but allow empty string)
87
92
  if (data.creationCode === null || data.creationCode === undefined) {
@@ -97,7 +102,8 @@ export class ContractRepository {
97
102
  contract = {
98
103
  uniqueHash,
99
104
  creationCode: data.creationCode,
100
- _sources: new Set<string>()
105
+ _sources: new Set<string>(),
106
+ _sourceProvenance: new Map<string, SourceProvenance>()
101
107
  }
102
108
  this.contracts.set(uniqueHash, contract)
103
109
  }
@@ -130,6 +136,28 @@ export class ContractRepository {
130
136
  if (data.buildInfoId && !contract.buildInfoId) {
131
137
  contract.buildInfoId = data.buildInfoId
132
138
  }
139
+ if (data.sourceProvenance) {
140
+ if (!contract._sourceProvenance) {
141
+ contract._sourceProvenance = new Map<string, SourceProvenance>()
142
+ }
143
+ contract._sourceProvenance.set(sourceFilePath, data.sourceProvenance)
144
+
145
+ contract.sourceProvenance = this.selectPreferredSourceProvenance(contract._sourceProvenance)
146
+ }
147
+ }
148
+
149
+ private getSourceProvenance(buildInfoPath: string, fullyQualifiedName: string): SourceProvenance | undefined {
150
+ const provenance = this.sourceProvenanceByBuildInfoPath.get(buildInfoPath)
151
+ if (!provenance) {
152
+ return undefined
153
+ }
154
+
155
+ return mergeSourceProvenance(provenance, provenance.contracts?.[fullyQualifiedName])
156
+ }
157
+
158
+ private selectPreferredSourceProvenance(sourceProvenance: Map<string, SourceProvenance>): SourceProvenance | undefined {
159
+ const firstEntry = Array.from(sourceProvenance.entries()).sort(([a], [b]) => a.localeCompare(b))[0]
160
+ return firstEntry?.[1]
133
161
  }
134
162
 
135
163
  /**
@@ -269,6 +297,7 @@ export class ContractRepository {
269
297
  source?: string
270
298
  compiler?: any
271
299
  buildInfoId?: string
300
+ sourceProvenance?: SourceProvenance
272
301
  _path: string
273
302
  _hash: string
274
303
  }): void {
@@ -281,17 +310,21 @@ export class ContractRepository {
281
310
  source: contractData.source,
282
311
  compiler: contractData.compiler,
283
312
  buildInfoId: contractData.buildInfoId,
313
+ sourceProvenance: contractData.sourceProvenance,
284
314
  }, contractData._path)
285
315
 
286
316
  // For testing, immediately disambiguate references after adding
287
317
  this.disambiguateReferences()
288
318
  }
289
319
 
290
- /**
291
- * Recursively finds all files that might contain contracts (e.g., .json files)
292
- */
293
- private async findContractFiles(dir: string, ignoreDirs: Set<string> = new Set(['node_modules', 'dist', '.git', '.idea', '.vscode'])): Promise<string[]> {
294
- let results: string[] = []
320
+ private async findProjectFiles(
321
+ dir: string,
322
+ ignoreDirs: Set<string> = new Set(['node_modules', 'dist', '.git', '.idea', '.vscode'])
323
+ ): Promise<{ contractFiles: string[]; sourceFiles: string[] }> {
324
+ const results: { contractFiles: string[]; sourceFiles: string[] } = {
325
+ contractFiles: [],
326
+ sourceFiles: []
327
+ }
295
328
  try {
296
329
  const list = await fs.readdir(dir, { withFileTypes: true })
297
330
 
@@ -299,10 +332,14 @@ export class ContractRepository {
299
332
  const fullPath = path.resolve(dir, dirent.name)
300
333
  if (dirent.isDirectory()) {
301
334
  if (!ignoreDirs.has(dirent.name)) {
302
- results = results.concat(await this.findContractFiles(fullPath, ignoreDirs))
335
+ const childResults = await this.findProjectFiles(fullPath, ignoreDirs)
336
+ results.contractFiles.push(...childResults.contractFiles)
337
+ results.sourceFiles.push(...childResults.sourceFiles)
303
338
  }
304
339
  } else if (dirent.isFile() && dirent.name.endsWith('.json')) {
305
- results.push(fullPath)
340
+ results.contractFiles.push(fullPath)
341
+ } else if (dirent.isFile() && (dirent.name === 'source.yaml' || dirent.name === 'source.yml')) {
342
+ results.sourceFiles.push(fullPath)
306
343
  }
307
344
  }
308
345
  } catch (err) {
@@ -310,4 +347,65 @@ export class ContractRepository {
310
347
  }
311
348
  return results
312
349
  }
313
- }
350
+
351
+ private async loadSourceProvenanceFiles(sourceFiles: string[]): Promise<void> {
352
+ this.sourceProvenanceByBuildInfoPath.clear()
353
+
354
+ for (const sourceFilePath of sourceFiles) {
355
+ let sourceDocument
356
+ try {
357
+ const content = await fs.readFile(sourceFilePath, 'utf-8')
358
+ sourceDocument = parseSourceDocument(content)
359
+ } catch (error) {
360
+ this.warnSourceProvenance(`Skipping source provenance file ${sourceFilePath}: ${error instanceof Error ? error.message : String(error)}`)
361
+ continue
362
+ }
363
+
364
+ if (!sourceDocument) {
365
+ continue
366
+ }
367
+
368
+ for (const warning of sourceDocument.warnings || []) {
369
+ this.warnSourceProvenance(`Skipping source provenance entry ${sourceFilePath}: ${warning}`)
370
+ }
371
+
372
+ const sourceDir = path.dirname(sourceFilePath)
373
+ for (const [buildInfoRef, provenance] of Object.entries(sourceDocument.build_info)) {
374
+ const buildInfoPath = path.resolve(sourceDir, buildInfoRef)
375
+ if (!isBuildInfoFile(buildInfoPath)) {
376
+ this.warnSourceProvenance(`Skipping source provenance entry ${sourceFilePath}: "${buildInfoRef}" does not point to a build-info JSON file.`)
377
+ continue
378
+ }
379
+
380
+ if (!await this.pathExists(buildInfoPath)) {
381
+ this.warnSourceProvenance(`Skipping source provenance entry ${sourceFilePath}: build-info file "${buildInfoRef}" does not exist.`)
382
+ continue
383
+ }
384
+
385
+ if (this.sourceProvenanceByBuildInfoPath.has(buildInfoPath)) {
386
+ this.warnSourceProvenance(`Skipping source provenance entry ${sourceFilePath}: duplicate provenance for build-info file "${buildInfoRef}".`)
387
+ continue
388
+ }
389
+
390
+ this.sourceProvenanceByBuildInfoPath.set(buildInfoPath, {
391
+ ...provenance,
392
+ sourceDocumentPath: sourceFilePath,
393
+ buildInfoPath
394
+ })
395
+ }
396
+ }
397
+ }
398
+
399
+ private async pathExists(filePath: string): Promise<boolean> {
400
+ try {
401
+ await fs.access(filePath)
402
+ return true
403
+ } catch {
404
+ return false
405
+ }
406
+ }
407
+
408
+ private warnSourceProvenance(message: string): void {
409
+ console.warn(message)
410
+ }
411
+ }
@@ -0,0 +1,474 @@
1
+ import { ExecutionEngine } from '../engine'
2
+ import { ExecutionContext } from '../context'
3
+ import { ContractRepository } from '../../contracts/repository'
4
+ import { Action, Network } from '../../types'
5
+ import { VerificationPlatformRegistry } from '../../verification/etherscan'
6
+
7
+ describe('Assert Action', () => {
8
+ let engine: ExecutionEngine
9
+ let context: ExecutionContext
10
+ let mockNetwork: Network
11
+ let mockRegistry: ContractRepository
12
+ let templates: Map<string, any>
13
+
14
+ beforeEach(() => {
15
+ mockNetwork = { name: 'testnet', chainId: 999, rpcUrl: 'http://localhost:8545' }
16
+ mockRegistry = new ContractRepository()
17
+
18
+ // Create a mock context that doesn't require a real connection
19
+ context = {
20
+ getNetwork: () => mockNetwork,
21
+ setOutput: jest.fn(),
22
+ getOutput: jest.fn(),
23
+ setContextPath: jest.fn(),
24
+ getContextPath: jest.fn(),
25
+ dispose: jest.fn()
26
+ } as any
27
+
28
+ templates = new Map()
29
+ const verificationRegistry = new VerificationPlatformRegistry()
30
+ engine = new ExecutionEngine(templates, { verificationRegistry })
31
+ })
32
+
33
+ describe('assert primitive action', () => {
34
+ it('should pass when eq comparison is true', async () => {
35
+ const action: Action = {
36
+ type: 'assert',
37
+ name: 'test-assert-eq',
38
+ arguments: {
39
+ actual: '42',
40
+ eq: '42'
41
+ }
42
+ }
43
+
44
+ await (engine as any).executePrimitive(action, context, new Map())
45
+
46
+ expect(context.setOutput).toHaveBeenCalledWith('test-assert-eq.actual', '42')
47
+ })
48
+
49
+ it('should fail when eq comparison is false', async () => {
50
+ const action: Action = {
51
+ type: 'assert',
52
+ name: 'test-assert-fail',
53
+ arguments: {
54
+ actual: '42',
55
+ eq: '99'
56
+ }
57
+ }
58
+
59
+ await expect(
60
+ (engine as any).executePrimitive(action, context, new Map())
61
+ ).rejects.toThrow(/assert failed.*actual=42.*expected=99.*op=eq/)
62
+ })
63
+
64
+ it('should include custom message on failure', async () => {
65
+ const action: Action = {
66
+ type: 'assert',
67
+ name: 'test-assert-msg',
68
+ arguments: {
69
+ actual: '10',
70
+ eq: '20',
71
+ message: 'balance mismatch'
72
+ }
73
+ }
74
+
75
+ await expect(
76
+ (engine as any).executePrimitive(action, context, new Map())
77
+ ).rejects.toThrow(/assert failed: balance mismatch.*actual=10.*expected=20.*op=eq/)
78
+ })
79
+
80
+ it('should pass neq comparison', async () => {
81
+ const action: Action = {
82
+ type: 'assert',
83
+ name: 'test-assert-neq',
84
+ arguments: {
85
+ actual: '10',
86
+ neq: '20'
87
+ }
88
+ }
89
+
90
+ await (engine as any).executePrimitive(action, context, new Map())
91
+ expect(context.setOutput).toHaveBeenCalledWith('test-assert-neq.actual', '10')
92
+ })
93
+
94
+ it('should fail neq comparison when values are equal', async () => {
95
+ const action: Action = {
96
+ type: 'assert',
97
+ name: 'test-assert-neq-fail',
98
+ arguments: {
99
+ actual: '10',
100
+ neq: '10'
101
+ }
102
+ }
103
+
104
+ await expect(
105
+ (engine as any).executePrimitive(action, context, new Map())
106
+ ).rejects.toThrow(/assert failed.*op=neq/)
107
+ })
108
+
109
+ it('should pass gte comparison', async () => {
110
+ const action: Action = {
111
+ type: 'assert',
112
+ name: 'test-assert-gte',
113
+ arguments: {
114
+ actual: '100',
115
+ gte: '50'
116
+ }
117
+ }
118
+
119
+ await (engine as any).executePrimitive(action, context, new Map())
120
+ expect(context.setOutput).toHaveBeenCalledWith('test-assert-gte.actual', '100')
121
+ })
122
+
123
+ it('should pass gte when equal', async () => {
124
+ const action: Action = {
125
+ type: 'assert',
126
+ name: 'test-assert-gte-equal',
127
+ arguments: {
128
+ actual: '50',
129
+ gte: '50'
130
+ }
131
+ }
132
+
133
+ await (engine as any).executePrimitive(action, context, new Map())
134
+ })
135
+
136
+ it('should fail gte when less', async () => {
137
+ const action: Action = {
138
+ type: 'assert',
139
+ name: 'test-assert-gte-fail',
140
+ arguments: {
141
+ actual: '10',
142
+ gte: '100'
143
+ }
144
+ }
145
+
146
+ await expect(
147
+ (engine as any).executePrimitive(action, context, new Map())
148
+ ).rejects.toThrow(/assert failed.*op=gte/)
149
+ })
150
+
151
+ it('should pass lt comparison', async () => {
152
+ const action: Action = {
153
+ type: 'assert',
154
+ name: 'test-assert-lt',
155
+ arguments: {
156
+ actual: '10',
157
+ lt: '100'
158
+ }
159
+ }
160
+
161
+ await (engine as any).executePrimitive(action, context, new Map())
162
+ })
163
+
164
+ it('should fail lt when greater', async () => {
165
+ const action: Action = {
166
+ type: 'assert',
167
+ name: 'test-assert-lt-fail',
168
+ arguments: {
169
+ actual: '100',
170
+ lt: '10'
171
+ }
172
+ }
173
+
174
+ await expect(
175
+ (engine as any).executePrimitive(action, context, new Map())
176
+ ).rejects.toThrow(/assert failed.*op=lt/)
177
+ })
178
+
179
+ it('should pass lte comparison', async () => {
180
+ const action: Action = {
181
+ type: 'assert',
182
+ name: 'test-assert-lte',
183
+ arguments: {
184
+ actual: '10',
185
+ lte: '100'
186
+ }
187
+ }
188
+
189
+ await (engine as any).executePrimitive(action, context, new Map())
190
+ })
191
+
192
+ it('should pass lte when equal', async () => {
193
+ const action: Action = {
194
+ type: 'assert',
195
+ name: 'test-assert-lte-equal',
196
+ arguments: {
197
+ actual: '100',
198
+ lte: '100'
199
+ }
200
+ }
201
+
202
+ await (engine as any).executePrimitive(action, context, new Map())
203
+ })
204
+
205
+ it('should fail lte when greater', async () => {
206
+ const action: Action = {
207
+ type: 'assert',
208
+ name: 'test-assert-lte-fail',
209
+ arguments: {
210
+ actual: '100',
211
+ lte: '10'
212
+ }
213
+ }
214
+
215
+ await expect(
216
+ (engine as any).executePrimitive(action, context, new Map())
217
+ ).rejects.toThrow(/assert failed.*op=lte/)
218
+ })
219
+
220
+ it('should pass gt comparison', async () => {
221
+ const action: Action = {
222
+ type: 'assert',
223
+ name: 'test-assert-gt',
224
+ arguments: {
225
+ actual: '100',
226
+ gt: '10'
227
+ }
228
+ }
229
+
230
+ await (engine as any).executePrimitive(action, context, new Map())
231
+ })
232
+
233
+ it('should fail gt when less', async () => {
234
+ const action: Action = {
235
+ type: 'assert',
236
+ name: 'test-assert-gt-fail',
237
+ arguments: {
238
+ actual: '10',
239
+ gt: '100'
240
+ }
241
+ }
242
+
243
+ await expect(
244
+ (engine as any).executePrimitive(action, context, new Map())
245
+ ).rejects.toThrow(/assert failed.*op=gt/)
246
+ })
247
+
248
+ it('should use `to` + `signature` form (call resolver)', async () => {
249
+ // Mock the resolver to simulate a call returning a value
250
+ const mockResolver = {
251
+ resolve: jest.fn(async (value: any, ctx: any, scope: any) => {
252
+ if (value.type === 'call') {
253
+ return '0xDepositManagerAddress'
254
+ }
255
+ if (value.type === 'basic-arithmetic') {
256
+ const [a, b] = value.arguments.values
257
+ if (value.arguments.operation === 'eq') {
258
+ return a === b
259
+ }
260
+ }
261
+ return value
262
+ })
263
+ }
264
+ ;(engine as any).resolver = mockResolver
265
+
266
+ const action: Action = {
267
+ type: 'assert',
268
+ name: 'test-assert-call',
269
+ arguments: {
270
+ to: '0xSomeProxyAddress',
271
+ signature: 'depositManager() returns (address)',
272
+ eq: '0xDepositManagerAddress'
273
+ }
274
+ }
275
+
276
+ await (engine as any).executePrimitive(action, context, new Map())
277
+
278
+ expect(mockResolver.resolve).toHaveBeenCalledWith(
279
+ { type: 'call', arguments: { to: '0xSomeProxyAddress', signature: 'depositManager() returns (address)', values: [] } },
280
+ context,
281
+ new Map()
282
+ )
283
+ expect(context.setOutput).toHaveBeenCalledWith('test-assert-call.actual', '0xDepositManagerAddress')
284
+ })
285
+
286
+ it('should use `actual` form with read-balance resolver', async () => {
287
+ // Mock the resolver to simulate a read-balance returning a value
288
+ const mockResolver = {
289
+ resolve: jest.fn(async (value: any, ctx: any, scope: any) => {
290
+ if (value.type === 'read-balance') {
291
+ return '1000000000000000000' // 1 ETH
292
+ }
293
+ if (value.type === 'basic-arithmetic') {
294
+ const [a, b] = value.arguments.values
295
+ if (value.arguments.operation === 'gte') {
296
+ return BigInt(a) >= BigInt(b)
297
+ }
298
+ }
299
+ return value
300
+ })
301
+ }
302
+ ;(engine as any).resolver = mockResolver
303
+
304
+ const action: Action = {
305
+ type: 'assert',
306
+ name: 'test-assert-read-balance',
307
+ arguments: {
308
+ actual: { type: 'read-balance', arguments: { address: '0xDeployer' } },
309
+ gte: '1000000000000000000',
310
+ message: 'deployer underfunded'
311
+ }
312
+ }
313
+
314
+ await (engine as any).executePrimitive(action, context, new Map())
315
+
316
+ expect(mockResolver.resolve).toHaveBeenCalledWith(
317
+ { type: 'read-balance', arguments: { address: '0xDeployer' } },
318
+ context,
319
+ new Map()
320
+ )
321
+ expect(context.setOutput).toHaveBeenCalledWith('test-assert-read-balance.actual', '1000000000000000000')
322
+ })
323
+
324
+ it('should not store outputs when action has no name', async () => {
325
+ const action: Action = {
326
+ type: 'assert',
327
+ arguments: {
328
+ actual: '42',
329
+ eq: '42'
330
+ }
331
+ }
332
+
333
+ await (engine as any).executePrimitive(action, context, new Map())
334
+
335
+ expect(context.setOutput).not.toHaveBeenCalled()
336
+ })
337
+
338
+ it('should not store outputs when action has custom output', async () => {
339
+ const action: Action = {
340
+ type: 'assert',
341
+ name: 'test-assert-custom',
342
+ arguments: {
343
+ actual: '42',
344
+ eq: '42'
345
+ }
346
+ }
347
+
348
+ await (engine as any).executePrimitive(action, context, new Map(), true)
349
+
350
+ // With hasCustomOutput=true, the .actual output should not be stored by the assert case
351
+ // (the custom output handling is done elsewhere in executeAction)
352
+ const setOutputCalls = (context.setOutput as jest.Mock).mock.calls
353
+ const actualOutputs = setOutputCalls.filter((c: any[]) => c[0].includes('.actual'))
354
+ expect(actualOutputs.length).toBe(0)
355
+ })
356
+
357
+ it('should fail when no comparator key is provided', async () => {
358
+ const action: Action = {
359
+ type: 'assert',
360
+ name: 'test-assert-no-comparator',
361
+ arguments: {
362
+ actual: '42'
363
+ }
364
+ }
365
+
366
+ await expect(
367
+ (engine as any).executePrimitive(action, context, new Map())
368
+ ).rejects.toThrow(/assert must have exactly one of/)
369
+ })
370
+
371
+ it('should fail when more than one comparator key is provided', async () => {
372
+ const action: Action = {
373
+ type: 'assert',
374
+ name: 'test-assert-multi-comparator',
375
+ arguments: {
376
+ actual: '42',
377
+ eq: '42',
378
+ gte: '1'
379
+ }
380
+ }
381
+
382
+ await expect(
383
+ (engine as any).executePrimitive(action, context, new Map())
384
+ ).rejects.toThrow(/assert must have exactly one comparator, but got: eq, gte/)
385
+ })
386
+
387
+ it('should resolve values from context variables', async () => {
388
+ // Mock the resolver to simulate template variable resolution
389
+ // returning the same value for both actual and expected
390
+ const mockResolver = {
391
+ resolve: jest.fn(async (value: any, ctx: any, scope: any) => {
392
+ if (typeof value === 'string' && value.startsWith('{{') && value.endsWith('}}')) {
393
+ return 'resolved-value'
394
+ }
395
+ if (value.type === 'basic-arithmetic') {
396
+ const [a, b] = value.arguments.values
397
+ if (value.arguments.operation === 'eq') {
398
+ return a === b
399
+ }
400
+ }
401
+ return value
402
+ })
403
+ }
404
+ ;(engine as any).resolver = mockResolver
405
+
406
+ const action: Action = {
407
+ type: 'assert',
408
+ name: 'test-assert-context',
409
+ arguments: {
410
+ actual: '{{myValue}}',
411
+ eq: '{{myExpected}}'
412
+ }
413
+ }
414
+
415
+ await (engine as any).executePrimitive(action, context, new Map())
416
+
417
+ // Both {{myValue}} and {{myExpected}} resolve to 'resolved-value', so eq returns true
418
+ expect(context.setOutput).toHaveBeenCalledWith('test-assert-context.actual', 'resolved-value')
419
+ })
420
+
421
+ it('should handle boolean values in eq comparison', async () => {
422
+ const action: Action = {
423
+ type: 'assert',
424
+ name: 'test-assert-bool',
425
+ arguments: {
426
+ actual: true,
427
+ eq: true
428
+ }
429
+ }
430
+
431
+ await (engine as any).executePrimitive(action, context, new Map())
432
+ expect(context.setOutput).toHaveBeenCalledWith('test-assert-bool.actual', true)
433
+ })
434
+
435
+ it('should handle large number comparisons', async () => {
436
+ const action: Action = {
437
+ type: 'assert',
438
+ name: 'test-assert-large',
439
+ arguments: {
440
+ actual: '115792089237316195423570985008687907853269984665640564039457584007913129639935',
441
+ gte: '10000000000000000000000000000000000000000000000000000000000000000000000000000'
442
+ }
443
+ }
444
+
445
+ await (engine as any).executePrimitive(action, context, new Map())
446
+ expect(context.setOutput).toHaveBeenCalledWith('test-assert-large.actual', '115792089237316195423570985008687907853269984665640564039457584007913129639935')
447
+ })
448
+
449
+ it('should describe call form as signature in error message', async () => {
450
+ const mockResolver = {
451
+ resolve: jest.fn(async (value: any, ctx: any, scope: any) => {
452
+ if (value.type === 'call') return 'wrong-address'
453
+ if (value.type === 'basic-arithmetic') return false
454
+ return value
455
+ })
456
+ }
457
+ ;(engine as any).resolver = mockResolver
458
+
459
+ const action: Action = {
460
+ type: 'assert',
461
+ name: 'test-assert-call-desc',
462
+ arguments: {
463
+ to: '0xProxy',
464
+ signature: 'getOwner() returns (address)',
465
+ eq: '0xCorrectOwner'
466
+ }
467
+ }
468
+
469
+ await expect(
470
+ (engine as any).executePrimitive(action, context, new Map())
471
+ ).rejects.toThrow(/assert failed.*getOwner\(\) returns \(address\)/)
472
+ })
473
+ })
474
+ })