From 0af02283c263c828206acea4ffd55d49408cf1b6 Mon Sep 17 00:00:00 2001 From: gbena-afk Date: Mon, 29 Jun 2026 12:26:09 +0100 Subject: [PATCH] feat(fixtures): add fixture-based regression tests + CI + docs --- .github/workflows/fixtures-regression.yml | 28 +++++ CONTRIBUTING.md | 44 +++++++ .../fixtures/sac-transfer/fixtures.json | 112 ++++++++++++++++++ package.json | 1 + tests/fixtures-regression.spec.ts | 73 ++++++++++++ 5 files changed, 258 insertions(+) create mode 100644 .github/workflows/fixtures-regression.yml create mode 100644 lib/translator/fixtures/sac-transfer/fixtures.json create mode 100644 tests/fixtures-regression.spec.ts diff --git a/.github/workflows/fixtures-regression.yml b/.github/workflows/fixtures-regression.yml new file mode 100644 index 0000000..3a66b3d --- /dev/null +++ b/.github/workflows/fixtures-regression.yml @@ -0,0 +1,28 @@ +name: Fixture Regression Tests + +on: + pull_request: + paths: + - 'lib/translator/**' + - 'tests/**' + - '.github/workflows/**' + push: + paths: + - 'lib/translator/**' + - 'tests/**' + +jobs: + fixtures-regression: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Use Node.js 20 + uses: actions/setup-node@v4 + with: + node-version: '20' + - name: Install dependencies + run: | + npm ci + - name: Run fixture regression tests + run: | + npm run test:fixtures diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9ddf49a..23de1b9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -409,6 +409,50 @@ Here's a clean, copy-pasteable JSON configuration you can use with the "Upload A } ``` +## Adding translation fixtures (for blueprint authors) + +We use committed, real-world fixtures to ensure blueprints continue to produce +exact, human-readable translations over time. Follow these steps to add fixtures +for a blueprint you maintain: + +1. Create a fixtures file under `lib/translator/fixtures//fixtures.json`. +2. Add one or more fixture objects with the following shape: + + { + "name": "short-descriptive-name", + "contractId": "", + "raw": { /* raw event payload captured from network */ }, + "expected": { "description": "Exact expected string", "status": "translated" }, + "blueprintFile": "lib/translator/blueprints/.ts", + "fingerprint": "" + } + +3. Compute the `fingerprint` by running `sha256sum` on the blueprint source + file and committing the hex digest. + + Example: + + ```bash + sha256sum lib/translator/blueprints/sac-transfer.ts | awk '{print $1}' + ``` + +4. Run the fixture regression test locally to verify: + + ```bash + npm ci + npm run test:fixtures + ``` + +5. Open a PR with the fixture and a short rationale describing the event source + (timestamp, network, tx hash if public, and why the example is representative). + +Notes +- If a fixture starts failing after a contract upgrade, the test will report + `NEEDS_REVIEW` when the blueprint source fingerprint differs from the + committed fingerprint — this indicates the contract's code may have changed + and the fixture should be revalidated before updating the expected string. + + #### Step 6: Run the Test Suite Before pushing a PR, always run: diff --git a/lib/translator/fixtures/sac-transfer/fixtures.json b/lib/translator/fixtures/sac-transfer/fixtures.json new file mode 100644 index 0000000..d273a91 --- /dev/null +++ b/lib/translator/fixtures/sac-transfer/fixtures.json @@ -0,0 +1,112 @@ +[ + { + "name": "sac-transfer-usdc-1", + "contractId": "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC", + "raw": { + "contractId": "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC", + "ledger": 1, + "topics": [ + "0x0000000000000000000000000000000000000000000000000000000074726e73", + "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + ], + "data": "0x000000003B9ACA00", + "txHash": "0xdeadbeef", + "id": 1 + }, + "expected": { + "description": "Public Key [GAAA...AAAA] transferred 100.00 USDC to [GBBB...BBBB]", + "status": "translated" + }, + "blueprintFile": "lib/translator/blueprints/sac-transfer.ts", + "fingerprint": "a14f7e07609f8512fb834f3b7c8dd66b95307921051262e17e2a61995ae08e4e" + }, + { + "name": "sac-transfer-xlm-1", + "contractId": "CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA", + "raw": { + "contractId": "CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA", + "ledger": 1, + "topics": [ + "0x0000000000000000000000000000000000000000000000000000000074726e73", + "0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "0xdddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd" + ], + "data": "0x000000003B9ACA00", + "txHash": "0xdeadbeef", + "id": 2 + }, + "expected": { + "description": "Public Key [GCCC...CCCC] transferred 100.00 XLM to [GDDD...DDDD]", + "status": "translated" + }, + "blueprintFile": "lib/translator/blueprints/sac-transfer.ts", + "fingerprint": "a14f7e07609f8512fb834f3b7c8dd66b95307921051262e17e2a61995ae08e4e" + }, + { + "name": "sac-transfer-eurc-1", + "contractId": "CAZAQB3D7KSLSNOSQKYD2V4JP5V2Y3B4RDJZRLBFCCIXDCTE3WHSY3UE", + "raw": { + "contractId": "CAZAQB3D7KSLSNOSQKYD2V4JP5V2Y3B4RDJZRLBFCCIXDCTE3WHSY3UE", + "ledger": 1, + "topics": [ + "0x0000000000000000000000000000000000000000000000000000000074726e73", + "0x1111111111111111111111111111111111111111111111111111111111111111", + "0x2222222222222222222222222222222222222222222222222222222222222222" + ], + "data": "0x000000003B9ACA00", + "txHash": "0xdeadbeef", + "id": 3 + }, + "expected": { + "description": "Public Key [G111...1111] transferred 100.00 EURC to [G222...2222]", + "status": "translated" + }, + "blueprintFile": "lib/translator/blueprints/sac-transfer.ts", + "fingerprint": "a14f7e07609f8512fb834f3b7c8dd66b95307921051262e17e2a61995ae08e4e" + }, + { + "name": "sac-transfer-usdc-2", + "contractId": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "raw": { + "contractId": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "ledger": 1, + "topics": [ + "0x0000000000000000000000000000000000000000000000000000000074726e73", + "0x3333333333333333333333333333333333333333333333333333333333333333", + "0x4444444444444444444444444444444444444444444444444444444444444444" + ], + "data": "0x000000003B9ACA00", + "txHash": "0xdeadbeef", + "id": 4 + }, + "expected": { + "description": "Public Key [G333...3333] transferred 100.00 USDC to [G444...4444]", + "status": "translated" + }, + "blueprintFile": "lib/translator/blueprints/sac-transfer.ts", + "fingerprint": "a14f7e07609f8512fb834f3b7c8dd66b95307921051262e17e2a61995ae08e4e" + }, + { + "name": "sac-transfer-xlm-2", + "contractId": "CBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB", + "raw": { + "contractId": "CBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB", + "ledger": 1, + "topics": [ + "0x0000000000000000000000000000000000000000000000000000000074726e73", + "0x5555555555555555555555555555555555555555555555555555555555555555", + "0x6666666666666666666666666666666666666666666666666666666666666666" + ], + "data": "0x000000003B9ACA00", + "txHash": "0xdeadbeef", + "id": 5 + }, + "expected": { + "description": "Public Key [G555...5555] transferred 100.00 XLM to [G666...6666]", + "status": "translated" + }, + "blueprintFile": "lib/translator/blueprints/sac-transfer.ts", + "fingerprint": "a14f7e07609f8512fb834f3b7c8dd66b95307921051262e17e2a61995ae08e4e" + } +] diff --git a/package.json b/package.json index 742600d..9666ec5 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "test:wasm:watch": "vitest lib/wasm-sandbox", "test:wasm:manual": "node scripts/test-wasm-sandbox.js", "test:wasm:benchmark": "node scripts/test-wasm-sandbox.js benchmark", + "test:fixtures": "vitest run tests/fixtures-regression.spec.ts", "wasm:build-examples": "cd lib/wasm-sandbox/examples/rust && ./build-all.sh", "build:cli": "tsc cli/open-audit-cli.ts --outDir dist --module commonjs --target es2020 --moduleResolution node --esModuleInterop --resolveJsonModule --skipLibCheck", "cli": "node dist/cli/open-audit-cli.js", diff --git a/tests/fixtures-regression.spec.ts b/tests/fixtures-regression.spec.ts new file mode 100644 index 0000000..4ff14fd --- /dev/null +++ b/tests/fixtures-regression.spec.ts @@ -0,0 +1,73 @@ +import { describe, it, expect } from "vitest"; +import { readFileSync, readdirSync } from "fs"; +import { join, relative } from "path"; +import crypto from "crypto"; +import { translateEvent } from "../lib/translator/registry"; + +type Fixture = { + name: string; + contractId: string; + raw: any; + expected: { description: string; status: string }; + blueprintFile: string; + fingerprint: string; +}; + +function sha256Hex(contents: string | Buffer) { + return crypto.createHash("sha256").update(contents).digest("hex"); +} + +function loadAllFixtures(): Fixture[] { + const fixturesRoot = join(process.cwd(), "lib", "translator", "fixtures"); + const fixtures: Fixture[] = []; + + const entries = readdirSync(fixturesRoot, { withFileTypes: true }); + for (const e of entries) { + if (!e.isDirectory()) continue; + const p = join(fixturesRoot, e.name); + const files = readdirSync(p).filter((f) => f.endsWith(".json")); + for (const f of files) { + const content = readFileSync(join(p, f), "utf8"); + const parsed = JSON.parse(content) as Fixture[]; + fixtures.push(...parsed); + } + } + + return fixtures; +} + +const fixtures = loadAllFixtures(); + +describe("Fixture-based blueprint regression tests", () => { + for (const fx of fixtures) { + it(`${fx.name} — ${fx.contractId}`, () => { + const translated = translateEvent(fx.raw); + + // Exact string match for description + if ((translated.description ?? null) !== fx.expected.description) { + // Compute current fingerprint of the blueprint source referenced by the fixture + const blueprintPath = join(process.cwd(), fx.blueprintFile); + let currentFp = ""; + try { + const src = readFileSync(blueprintPath); + currentFp = sha256Hex(src); + } catch (e) { + throw new Error(`ERROR: Could not read blueprint file at ${fx.blueprintFile}: ${(e as Error).message}`); + } + + if (currentFp !== fx.fingerprint) { + throw new Error( + `NEEDS_REVIEW: Fixture '${fx.name}' output changed and blueprint fingerprint differs.\nRecorded fingerprint: ${fx.fingerprint}\nCurrent fingerprint: ${currentFp}\nExpected: ${fx.expected.description}\nActual: ${translated.description}` + ); + } + + throw new Error( + `REGRESSION: Fixture '${fx.name}' produced different translation.\nExpected: ${fx.expected.description}\nActual: ${translated.description}` + ); + } + + expect(translated.status).toBe(fx.expected.status); + expect(translated.description).toBe(fx.expected.description); + }); + } +});