diff --git a/core/testing/__tests__/fixtures.contract.test.ts b/core/testing/__tests__/fixtures.contract.test.ts new file mode 100644 index 0000000..6d454e2 --- /dev/null +++ b/core/testing/__tests__/fixtures.contract.test.ts @@ -0,0 +1,35 @@ +import { describe, expect, it } from "vitest"; +import { discoverFixtureFiles } from "@/core/testing/fixtureRegistry"; + +/** + * SDK-upgrade fixture contract (issue #26). + * + * Fixtures are intentionally decentralized — one `fixtures/` directory per + * feature slice, owned by that slice's contributor — so there is no single + * fixtures root to hand-audit after a `@stellar/stellar-sdk` bump. Most + * fixtures call real SDK constructors (`Keypair`, `TransactionBuilder`, + * `xdr.*`) at module-evaluation time rather than hand-typing values, which + * means a renamed export, changed constructor signature, or removed enum + * member throws the moment the fixture module loads — this test just needs + * to load every one of them and surface exactly which file broke. + * + * Deliberately-invalid fixture *values* (e.g. `notAnEnvelopeXdr`, + * `notBase64`) are untouched by this: they're plain strings a slice's own + * tests decode and expect to fail, not something this contract should flag. + */ +describe("fixture SDK-upgrade contract", () => { + const fixtures = discoverFixtureFiles(); + + it("discovers at least one fixture per feature slice with a fixtures directory", () => { + expect(fixtures.length).toBeGreaterThan(0); + }); + + it.each(fixtures.map((f) => [f.relativePath, f] as const))( + "%s imports cleanly against the installed SDK", + async (_label, fixture) => { + await expect( + import(/* @vite-ignore */ fixture.absolutePath) + ).resolves.toBeDefined(); + } + ); +}); diff --git a/core/testing/fixtureRegistry.ts b/core/testing/fixtureRegistry.ts new file mode 100644 index 0000000..7236340 --- /dev/null +++ b/core/testing/fixtureRegistry.ts @@ -0,0 +1,56 @@ +import { readdirSync, statSync } from "node:fs"; +import path from "node:path"; + +const FEATURES_DIR = path.resolve(process.cwd(), "features"); +const FIXTURE_DIR_NAME = "fixtures"; +const FIXTURE_FILE_RE = /\.fixture\.tsx?$/; + +export interface FixtureFileEntry { + /** Feature slice this fixture belongs to, e.g. "fee-stats". */ + slice: string; + /** Absolute path to the fixture file. */ + absolutePath: string; + /** Path relative to the repo root, used for reporting. */ + relativePath: string; +} + +/** + * Walks `features/*/fixtures/*.fixture.ts(x)` and returns every fixture + * file found, grouped by the slice that owns it. + * + * Fixtures are intentionally decentralized — one directory per slice, owned + * by that slice's contributor — so this discovers them by walking every + * feature directory rather than assuming a single shared fixtures root. + */ +export function discoverFixtureFiles(): FixtureFileEntry[] { + const entries: FixtureFileEntry[] = []; + let slices: string[]; + try { + slices = readdirSync(FEATURES_DIR).filter((name) => + statSync(path.join(FEATURES_DIR, name)).isDirectory() + ); + } catch { + return entries; + } + + for (const slice of slices) { + const fixturesDir = path.join(FEATURES_DIR, slice, FIXTURE_DIR_NAME); + let files: string[]; + try { + files = readdirSync(fixturesDir); + } catch { + continue; + } + for (const file of files) { + if (!FIXTURE_FILE_RE.test(file)) continue; + const absolutePath = path.join(fixturesDir, file); + entries.push({ + slice, + absolutePath, + relativePath: path.relative(process.cwd(), absolutePath) + }); + } + } + + return entries.sort((a, b) => a.relativePath.localeCompare(b.relativePath)); +} diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index a943d1b..6e5ff21 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -99,13 +99,26 @@ module would test the mock instead of the code. ## Quality gates ```bash -npm run check # registry → lint → test → verify:features → build +npm run check # registry → lint → test → verify:features → verify:issues → verify:fixtures → build ``` CI runs the same steps on every pull request, including `npm run verify:features`, which fails a slice that does not meet the contract, -and `npm run verify:issues`, which preserves a backlog of at least 40 -independent specifications with a stable 20-issue advanced wave. +`npm run verify:issues`, which preserves a backlog of at least 40 +independent specifications with a stable 20-issue advanced wave, and +`npm run verify:fixtures`, which imports every `features/*/fixtures/*.fixture.ts` +file and fails with the specific file if a `@stellar/stellar-sdk` upgrade +broke it. + +### SDK upgrades + +Fixtures are decentralized — one `fixtures/` directory per slice, owned by +that slice's contributor — so there is no single place to eyeball after +bumping `@stellar/stellar-sdk`. Most fixtures build their values with real SDK +calls (`Keypair`, `TransactionBuilder`, `xdr.*`) rather than hand-typing them, +so a renamed export or changed constructor throws the moment the fixture +module loads. Run `npm run verify:fixtures` as part of every SDK-upgrade PR — +it reports exactly which fixture file failed to import and why. See [ISSUE_PUBLISHING.md](./ISSUE_PUBLISHING.md) for the five-at-a-time GrantFox publication flow. diff --git a/package.json b/package.json index f09cf55..1511c47 100644 --- a/package.json +++ b/package.json @@ -18,9 +18,10 @@ "test:coverage": "vitest run --coverage", "verify:features": "node scripts/verify-features.mjs", "verify:issues": "node scripts/validate-issue-catalog.mjs", + "verify:fixtures": "vitest run core/testing/__tests__/fixtures.contract.test.ts --reporter=verbose", "new:feature": "node scripts/new-feature.mjs", "postinstall": "node scripts/generate-registry.mjs", - "check": "npm run lint && npm run test && npm run verify:features && npm run verify:issues && npm run build", + "check": "npm run lint && npm run test && npm run verify:features && npm run verify:issues && npm run verify:fixtures && npm run build", "issues": "node scripts/create-issues.mjs", "issues:drips": "node scripts/create-issues.mjs --target drips" },