Skip to content

Commit 1d84e85

Browse files
authored
feat(e2e): support running tests against legacy noir contract artifacts (#22410)
1 parent a07850d commit 1d84e85

4 files changed

Lines changed: 164 additions & 0 deletions

File tree

yarn-project/end-to-end/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ ultrahonk-bench-inputs
77
web/main.js*
88
consensys_web3signer_*
99
scripts/ha/postgres_data/
10+
.legacy-contracts/

yarn-project/end-to-end/README.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,30 @@ which will spawn the two processes.
2424
You can also run this by `docker-compose up` which will spawn 2 different containers for Anvil and the test runner.
2525

2626
You can run a single test by running `yarn test:compose <test_name>`.
27+
28+
## Running tests against legacy contract artifacts
29+
30+
To verify that contracts deployed from a previous release still work against the current stack, set
31+
`CONTRACT_ARTIFACTS_VERSION` to a published version of `@aztec/noir-contracts.js` / `@aztec/noir-test-contracts.js`:
32+
33+
```
34+
CONTRACT_ARTIFACTS_VERSION=4.1.3 yarn test:e2e src/e2e_amm.test.ts
35+
```
36+
37+
Only the JSON artifact files (`.../artifacts/*.json`) are redirected. The TypeScript wrapper classes
38+
(e.g. `TokenContract`) continue to load from the current workspace and use the current `@aztec/aztec.js` — so this
39+
exercises whether a deployed contract's ABI / bytecode / notes still work through the *new* client, not whether the
40+
legacy wrapper code still imports cleanly.
41+
42+
The first run downloads the pinned packages into `.legacy-contracts/<version>/node_modules/` (cached across runs). A
43+
startup banner and a per-redirect line are printed to stderr so you can confirm the legacy artifacts were actually
44+
loaded:
45+
46+
```
47+
[legacy-contracts][jest] CONTRACT_ARTIFACTS_VERSION=4.1.3
48+
[legacy-contracts][jest] redirecting @aztec/noir-contracts.js/artifacts/*.json -> .legacy-contracts/4.1.3/...
49+
[legacy-contracts][jest] redirected token_contract-Token.json -> /abs/.../.legacy-contracts/4.1.3/.../token_contract-Token.json
50+
```
51+
52+
When `CONTRACT_ARTIFACTS_VERSION` is unset the test run is byte-identical to the default behaviour. The cache is
53+
populated automatically on first use.

yarn-project/end-to-end/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,7 @@
159159
"moduleNameMapper": {
160160
"^(\\.{1,2}/.*)\\.[cm]?js$": "$1"
161161
},
162+
"resolver": "<rootDir>/legacy-jest-resolver.cjs",
162163
"testRegex": "./src/.*\\.test\\.(js|mjs|ts)$",
163164
"rootDir": "./src",
164165
"testTimeout": 120000,
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
// Custom Jest resolver. When CONTRACT_ARTIFACTS_VERSION is set, redirects *only* JSON artifact files under
2+
// @aztec/noir-contracts.js/artifacts/ and @aztec/noir-test-contracts.js/artifacts/ to a local cache of the pinned
3+
// legacy versions. TypeScript wrapper classes (e.g. Token.ts) continue to load from the current workspace and use the
4+
// current @aztec/aztec.js — only the artifact JSON (the deployed-contract ABI / bytecode / notes surface) is swapped.
5+
//
6+
// Why JSON-only: the JSON artifact is the actual interchange surface a "deployed contract" exposes. The TS wrapper is
7+
// generated client-side ergonomics that's tightly coupled to the current @aztec/aztec.js API. Redirecting the wrapper
8+
// would couple this test to a moving aztec.js surface and break at import time on unrelated breaking changes; we want
9+
// to fail only on actual artifact-compat regressions.
10+
//
11+
// The cache is populated on demand by running `npm install` into .legacy-contracts/<version>/.
12+
//
13+
// Activated by env var; passthrough otherwise.
14+
/* eslint-disable @typescript-eslint/no-require-imports */
15+
16+
const path = require('path');
17+
const fs = require('fs');
18+
const { execSync } = require('child_process');
19+
20+
const version = process.env.CONTRACT_ARTIFACTS_VERSION;
21+
const REDIRECTED = ['@aztec/noir-contracts.js', '@aztec/noir-test-contracts.js'];
22+
23+
// Jest sets rootDir to <e2e>/src; this file lives there too.
24+
const e2eRoot = path.resolve(__dirname, '..');
25+
const cacheRoot = version ? path.join(e2eRoot, '.legacy-contracts', version) : null;
26+
27+
function pkgJsonPath(name) {
28+
return path.join(cacheRoot, 'node_modules', name, 'package.json');
29+
}
30+
31+
function ensureCache() {
32+
const missing = REDIRECTED.some(p => !fs.existsSync(pkgJsonPath(p)));
33+
if (!missing) {
34+
return;
35+
}
36+
fs.mkdirSync(cacheRoot, { recursive: true });
37+
// Seed a standalone package.json so `npm install --prefix` treats cacheRoot as its own project. Without this, npm
38+
// walks up and finds the yarn-project workspace root, which breaks on `workspace:` protocol deps and risks
39+
// clobbering the monorepo's node_modules.
40+
const seed = path.join(cacheRoot, 'package.json');
41+
if (!fs.existsSync(seed)) {
42+
fs.writeFileSync(seed, JSON.stringify({ name: 'legacy-contracts-cache', private: true }));
43+
}
44+
45+
const specs = REDIRECTED.map(p => `${p}@${version}`).join(' ');
46+
process.stderr.write(`[legacy-contracts] installing ${specs} into ${cacheRoot}\n`);
47+
// --prefix: install into cacheRoot instead of cwd, so the cache is isolated from the monorepo.
48+
// --no-save: don't write the installed packages back to the seeded package.json.
49+
// --ignore-scripts: skip lifecycle scripts (preinstall/postinstall) of the legacy packages and their transitive
50+
// deps; we only want the files on disk, not to run any build steps.
51+
// --legacy-peer-deps: tolerate peer-dependency mismatches between the pinned legacy @aztec/* graph and whatever
52+
// current versions npm would otherwise try to reconcile.
53+
execSync(`npm install --prefix "${cacheRoot}" --no-save --ignore-scripts --legacy-peer-deps ${specs}`, {
54+
stdio: 'inherit',
55+
});
56+
57+
// Verify versions on disk match the requested version.
58+
for (const p of REDIRECTED) {
59+
const onDisk = JSON.parse(fs.readFileSync(pkgJsonPath(p), 'utf8')).version;
60+
if (onDisk !== version) {
61+
throw new Error(`[legacy-contracts] ${p} on disk is ${onDisk}, expected ${version}`);
62+
}
63+
}
64+
}
65+
66+
if (version) {
67+
ensureCache();
68+
}
69+
70+
let bannerPrinted = false;
71+
const seen = new Set();
72+
73+
function printBannerOnce() {
74+
if (bannerPrinted || !version) {
75+
return;
76+
}
77+
bannerPrinted = true;
78+
const lines = ['='.repeat(60), `[legacy-contracts][jest] CONTRACT_ARTIFACTS_VERSION=${version}`];
79+
for (const p of REDIRECTED) {
80+
const v = JSON.parse(fs.readFileSync(pkgJsonPath(p), 'utf8')).version;
81+
if (v !== version) {
82+
throw new Error(`[legacy-contracts] ${p} on disk is ${v}, expected ${version}`);
83+
}
84+
lines.push(`[legacy-contracts][jest] redirecting ${p}/artifacts/*.json -> .legacy-contracts/${version}/...`);
85+
}
86+
lines.push('='.repeat(60));
87+
process.stderr.write(lines.join('\n') + '\n');
88+
}
89+
90+
// Match a resolved absolute path against the workspace artifacts dirs and return the legacy cache equivalent, or null
91+
// if it's not an artifact path we should redirect.
92+
function legacyArtifactPath(resolved) {
93+
if (!resolved.endsWith('.json')) {
94+
return null;
95+
}
96+
for (const pkg of REDIRECTED) {
97+
// pkg = '@aztec/noir-contracts.js' -> match '/noir-contracts.js/artifacts/'
98+
const dirName = pkg.split('/')[1];
99+
const marker = `/${dirName}/artifacts/`;
100+
const idx = resolved.indexOf(marker);
101+
if (idx === -1) {
102+
continue;
103+
}
104+
const basename = resolved.slice(idx + marker.length);
105+
return path.join(cacheRoot, 'node_modules', pkg, 'artifacts', basename);
106+
}
107+
return null;
108+
}
109+
110+
module.exports = function legacyResolver(request, options) {
111+
// Always run the default resolver first. We only inspect (and possibly rewrite) the *result*; this catches both
112+
// bare-specifier imports of `@aztec/noir-contracts.js/artifacts/foo.json` and the relative `../artifacts/foo.json`
113+
// imports inside the workspace TS wrapper classes — both resolve to the same workspace artifact path that we then
114+
// redirect.
115+
const resolved = options.defaultResolver(request, options);
116+
if (!version) {
117+
return resolved;
118+
}
119+
printBannerOnce();
120+
const legacy = legacyArtifactPath(resolved);
121+
if (!legacy) {
122+
return resolved;
123+
}
124+
if (!fs.existsSync(legacy)) {
125+
throw new Error(
126+
`[legacy-contracts] artifact ${path.basename(legacy)} not present in legacy cache @${version}; ` +
127+
`the contract may have been added after that release. Pin a newer CONTRACT_ARTIFACTS_VERSION or skip this test.`,
128+
);
129+
}
130+
if (!seen.has(resolved)) {
131+
seen.add(resolved);
132+
process.stderr.write(`[legacy-contracts][jest] redirected ${path.basename(legacy)} -> ${legacy}\n`);
133+
}
134+
return legacy;
135+
};

0 commit comments

Comments
 (0)