import { afterEach, describe, expect, it } from 'vitest' import { spawnSync } from 'node:child_process' import { writeFileSync, rmSync, mkdirSync, existsSync } from 'node:fs' import { join, resolve } from 'node:path' // AZ-485 / F4 — verifies the STC-ARCH-01 static gate (scripts/check-arch-imports.mjs): // - AC-5 : passes on the migrated codebase as-is // - AC-4 : fails when a synthetic cross-component deep import is added // - AC-4 : deep import into class-colors is NOT exempt (regression guard for // AZ-511 — the F3 carry-over exemption was removed when classColors // moved to src/class-colors/ with its own barrel; any consumer that // bypasses the barrel must now fail STC-ARCH-01 like every other // component) // - AC-4 : ignores deep imports written inside // line comments // // AZ-486 / F7 — verifies the STC-ARCH-02 static gate (same script, // --mode=api-literals): // - AC-4 : passes on the migrated codebase as-is // - AC-3 : fails when a synthetic `/api//...` literal is added // (covers single-quoted, double-quoted, and template-literal forms) // - AC-3 : *.test.ts colocated under src/ are exempt (the test file IS the // contract per module-layout.md "code-derived documentation") // - AC-3 : literals inside // line comments do not trip the gate // // Both gates are exercised via subprocess so a regression in the script // (regex drift, exemption logic, exit code) trips the test even if the bash // glue in run-tests.sh keeps reporting PASS. // // All offending substrings are built via concatenation at runtime so the // scanner itself does not flag this test file when it walks `tests/**` or // `src/**` (api-literals mode); the api-literals scanner does walk `src/**` // but exempts `*.test.tsx?` so colocated test files are safe. const REPO_ROOT = resolve(__dirname, '..') const SCRIPT = join(REPO_ROOT, 'scripts', 'check-arch-imports.mjs') const ARCH_FIXTURE_DIR = join(REPO_ROOT, 'tests', '_arch_fixtures') const API_FIXTURE_DIR = join(REPO_ROOT, 'src', '_arch_fixtures') const FROM = 'fr' + 'om' const UP2 = '..' + '/..' const DEEP_API = `${UP2}/src/api/cl` + 'ient' const DEEP_CLASSCOLORS_NEW = `${UP2}/src/class-colors/classCo` + 'lors' // Build synthetic API path strings by concatenation so this test file itself // never matches the api-literal regex when scanned. Quote characters are // added per fixture body below. const API_ADMIN_USERS = '/api/' + 'admin/' + 'users/me' const API_FLIGHTS = '/api/' + 'flights/' + 'liveGps' function runCheck(mode: 'arch-imports' | 'api-literals'): { status: number; stderr: string } { const res = spawnSync( 'node', [SCRIPT, `--root=${REPO_ROOT}`, `--mode=${mode}`], { cwd: REPO_ROOT, encoding: 'utf8' }, ) return { status: res.status ?? -1, stderr: res.stderr ?? '' } } function writeFixture(dir: string, filename: string, content: string): string { mkdirSync(dir, { recursive: true }) const path = join(dir, filename) writeFileSync(path, content, 'utf8') return path } describe('AZ-485 STC-ARCH-01 — no cross-component deep imports', () => { afterEach(() => { if (existsSync(ARCH_FIXTURE_DIR)) rmSync(ARCH_FIXTURE_DIR, { recursive: true, force: true }) }) it('AC-5: passes on the migrated codebase (no fixtures)', () => { // Assert const { status, stderr } = runCheck('arch-imports') expect(stderr, stderr).toBe('') expect(status).toBe(0) }) it('AC-4: FAILS when a deep import into another component is introduced', () => { // Arrange const body = `import { api } ${FROM} '${DEEP_API}'\nexport const _force = api\n` writeFixture(ARCH_FIXTURE_DIR, 'synthetic_deep_import.ts', body) // Act const { status, stderr } = runCheck('arch-imports') // Assert expect(status).not.toBe(0) expect(stderr).toMatch(/STC-ARCH-01/) expect(stderr).toMatch(/synthetic_deep_import\.ts/) expect(stderr).toMatch(/src\/api\/client/) }) it('AC-4: FAILS when a deep import bypasses the class-colors barrel (AZ-511 regression guard)', () => { // Arrange — F3 was closed by AZ-511; class-colors now has a proper barrel // at src/class-colors/index.ts, so reaching past it into the file directly // must trip STC-ARCH-01 like every other component. The previous fixture // asserted the exemption WORKED; this replacement asserts no exemption // remains for class-colors at all. const body = `import { FALLBACK_CLASS_NAMES } ${FROM} '${DEEP_CLASSCOLORS_NEW}'\n` + `export const _force = FALLBACK_CLASS_NAMES\n` writeFixture(ARCH_FIXTURE_DIR, 'synthetic_classcolors_deep_import.ts', body) // Act const { status, stderr } = runCheck('arch-imports') // Assert expect(status).not.toBe(0) expect(stderr).toMatch(/STC-ARCH-01/) expect(stderr).toMatch(/synthetic_classcolors_deep_import\.ts/) expect(stderr).toMatch(/src\/class-colors\/classColors/) }) it('AC-4: deep imports inside line comments do not trip the gate', () => { // Arrange const body = `// import { api } ${FROM} '${DEEP_API}'\nexport const _x = 1\n` writeFixture(ARCH_FIXTURE_DIR, 'commented_out_deep_import.ts', body) // Act const { status } = runCheck('arch-imports') // Assert expect(status).toBe(0) }) }) describe('AZ-486 STC-ARCH-02 — no hardcoded /api// literals', () => { afterEach(() => { if (existsSync(API_FIXTURE_DIR)) rmSync(API_FIXTURE_DIR, { recursive: true, force: true }) }) it('AC-4: passes on the migrated codebase (no fixtures)', () => { // Assert const { status, stderr } = runCheck('api-literals') expect(stderr, stderr).toBe('') expect(status).toBe(0) }) it('AC-3: FAILS when a single-quoted /api// literal is added in src/', () => { // Arrange const body = `export const url = '${API_ADMIN_USERS}'\n` writeFixture(API_FIXTURE_DIR, 'synthetic_api_literal_single.ts', body) // Act const { status, stderr } = runCheck('api-literals') // Assert expect(status).not.toBe(0) expect(stderr).toMatch(/STC-ARCH-02/) expect(stderr).toMatch(/synthetic_api_literal_single\.ts/) }) it('AC-3: FAILS when a double-quoted /api// literal is added in src/', () => { // Arrange const body = `export const url = "${API_ADMIN_USERS}"\n` writeFixture(API_FIXTURE_DIR, 'synthetic_api_literal_double.ts', body) // Act const { status, stderr } = runCheck('api-literals') // Assert expect(status).not.toBe(0) expect(stderr).toMatch(/STC-ARCH-02/) expect(stderr).toMatch(/synthetic_api_literal_double\.ts/) }) it('AC-3: FAILS when a template-literal /api// form is added in src/', () => { // Arrange — backticks built via String.fromCharCode(96) to keep this test // file itself a non-matching artifact under api-literals scanning of src/ // (the test file lives under tests/, but defense in depth is cheap). const bt = String.fromCharCode(96) const body = `export const url = (id: string) => ${bt}${API_FLIGHTS}/\${id}${bt}\n` writeFixture(API_FIXTURE_DIR, 'synthetic_api_literal_template.ts', body) // Act const { status, stderr } = runCheck('api-literals') // Assert expect(status).not.toBe(0) expect(stderr).toMatch(/STC-ARCH-02/) expect(stderr).toMatch(/synthetic_api_literal_template\.ts/) }) it('AC-3: still PASSES when an offending literal lives in a *.test.ts file under src/', () => { // Arrange — test files are exempt: the test file IS the contract const body = `export const url = '${API_ADMIN_USERS}'\n` writeFixture(API_FIXTURE_DIR, 'synthetic_api_literal_exempt.test.ts', body) // Act const { status, stderr } = runCheck('api-literals') // Assert expect(stderr, stderr).toBe('') expect(status).toBe(0) }) it('AC-3: literals inside // line comments do not trip the gate', () => { // Arrange const body = `// example: '${API_ADMIN_USERS}'\nexport const _x = 1\n` writeFixture(API_FIXTURE_DIR, 'synthetic_api_literal_commented.ts', body) // Act const { status } = runCheck('api-literals') // Assert expect(status).toBe(0) }) })