Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions .github/workflows/fixtures-regression.yml
Original file line number Diff line number Diff line change
@@ -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
44 changes: 44 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/<blueprint-name>/fixtures.json`.
2. Add one or more fixture objects with the following shape:

{
"name": "short-descriptive-name",
"contractId": "<CONTRACT_ID>",
"raw": { /* raw event payload captured from network */ },
"expected": { "description": "Exact expected string", "status": "translated" },
"blueprintFile": "lib/translator/blueprints/<file>.ts",
"fingerprint": "<sha256-of-blueprint-file>"
}

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:
Expand Down
112 changes: 112 additions & 0 deletions lib/translator/fixtures/sac-transfer/fixtures.json
Original file line number Diff line number Diff line change
@@ -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"
}
]
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
73 changes: 73 additions & 0 deletions tests/fixtures-regression.spec.ts
Original file line number Diff line number Diff line change
@@ -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);
});
}
});
Loading