// @vitest-environment node import { readdirSync, readFileSync } from 'node:fs' import { join } from 'node:path' import { fileURLToPath } from 'node:url' import { describe, expect, it } from 'vitest' import { checkTestLocations } from '../../scripts/check-test-locations.mjs' import { E2E_AFFECTED_MANIFEST } from '../../scripts/lib/e2e-affected-manifest.mjs' import { CANONICAL_E2E_SPEC_SUFFIX, CANONICAL_E2E_TEST_DIR, CANONICAL_INT_INCLUDE, CANONICAL_UNIT_INCLUDE, E2E_MANIFEST_DOMAIN_EXEMPT, findMisplacedSpecPaths, findUncoveredE2eDomainPrefixes, HIGH_RISK_EXACT, isBuildPath, isCanonicalSpecPath, isCodePath, isHighRisk, isMisplacedSpecPath, isSrcPath, isTestPath, } from '../../scripts/lib/test-affected-core.mjs' const repoRoot = join(fileURLToPath(new URL('.', import.meta.url)), '../..') describe('ciSkipInvariants', () => { it('exports shared path predicates used by classifiers and the lint guard', () => { expect(isSrcPath('src/lib/x.ts')).toBe(true) expect(isTestPath('tests/unit/x.unit.spec.ts')).toBe(true) expect(isCodePath('tsconfig.json')).toBe(true) expect(isBuildPath('public/favicon.ico')).toBe(true) expect(isHighRisk('src/migrations/x.ts')).toBe(true) expect(E2E_MANIFEST_DOMAIN_EXEMPT.has('shared')).toBe(true) }) it('rejects misplaced *.spec|*.test files outside the three canonical trees', () => { expect(isCanonicalSpecPath('tests/unit/foo.unit.spec.ts')).toBe(true) expect(isCanonicalSpecPath('tests/unit/foo.unit.spec.tsx')).toBe(true) expect(isCanonicalSpecPath('tests/int/foo.int.spec.ts')).toBe(true) expect(isCanonicalSpecPath('tests/e2e/foo.e2e.spec.ts')).toBe(true) for (const path of [ 'src/utilities/foo.spec.ts', 'scripts/bar.test.ts', 'tests/unit/wrong.spec.ts', 'tests/int/wrong.spec.ts', 'tests/e2e/nested/foo.e2e.spec.ts', 'foo.test.tsx', ]) { expect(isMisplacedSpecPath(path), path).toBe(true) } expect(findMisplacedSpecPaths(['tests/unit/ok.unit.spec.ts', 'src/x.spec.ts'])).toEqual([ 'src/x.spec.ts', ]) }) it('repo walk finds no misplaced specs (same check as the lint job)', () => { expect(checkTestLocations(repoRoot)).toEqual([]) }) it('vitest/playwright harness globs match the canonical constants', () => { const unitConfig = readFileSync(join(repoRoot, 'vitest.unit.config.mts'), 'utf8') const intConfig = readFileSync(join(repoRoot, 'vitest.config.mts'), 'utf8') const playwrightConfig = readFileSync(join(repoRoot, 'playwright.config.ts'), 'utf8') expect(unitConfig).toContain(`'${CANONICAL_UNIT_INCLUDE}'`) expect(intConfig).toContain(`'${CANONICAL_INT_INCLUDE}'`) expect(playwrightConfig).toContain(`'./${CANONICAL_E2E_TEST_DIR}'`) expect(CANONICAL_E2E_SPEC_SUFFIX).toBe('.e2e.spec.ts') }) it('keeps local e2e build artifacts outside the development dist directory', () => { for (const file of [ '.forgejo/workflows/ci.yml', '.forgejo/workflows/ci-pr.yml', '.gitignore', '.prettierignore', 'eslint.config.mjs', 'playwright.config.ts', 'scripts/run-e2e-affected.mjs', 'tsconfig.json', ]) { const source = readFileSync(join(repoRoot, file), 'utf8') expect(source, file).not.toContain('.next/e2e') expect(source, file).toContain('.next-e2e') } }) it('skip classifiers and invariant specs are high-risk (force full suite)', () => { for (const path of [ 'scripts/lib/test-affected-core.mjs', 'scripts/ci-scope.mjs', 'scripts/ci-classify-production.mjs', 'scripts/lib/dockerignore.mjs', 'scripts/check-test-locations.mjs', 'scripts/check-plans-only-pr-closes.mjs', 'scripts/lib/plansOnlyClosesGuard.mjs', 'scripts/test-affected.mjs', 'scripts/e2e-affected.mjs', 'scripts/run-e2e-affected.mjs', 'tests/unit/ciSkipInvariants.unit.spec.ts', 'tests/unit/testAffected.unit.spec.ts', ]) { expect(HIGH_RISK_EXACT.has(path), path).toBe(true) } }) it('main CI runs in a 30-minute window with a production gate (OPS65)', () => { const ci = readFileSync(join(repoRoot, '.forgejo/workflows/ci.yml'), 'utf8') expect(ci).toContain('schedule:') expect(ci).toContain("cron: '*/30 * * * *'") expect(ci).not.toMatch(/push:\s*\n\s*branches:\s*\[main\]/) expect(ci).toContain('workflow_dispatch:') // The cheap gate (no pnpm) classifies production change and every // expensive job consumes it, fail-open on gate errors. The gate is // main-only: a branch dispatch must not run branch code on the host. expect(ci).toContain('gate production change') expect(ci).toContain("if: github.ref == 'refs/heads/main'") expect(ci).toContain('fetch-depth: 0') expect(ci).toContain('scripts/ci-classify-production.mjs') expect(ci).toContain( "needs.gate.result != 'success' || needs.gate.outputs.production == 'true'", ) // Deploy is double-gated: green rollup AND production change (never // deploy blind, never deploy with a red suite). expect(ci).toContain('needs: [checks, gate]') expect(ci).toContain( "needs.checks.result == 'success' && needs.gate.outputs.production == 'true'", ) }) it('every campaign domain dir is covered by the e2e affected manifest', () => { const componentDirs = readdirSync(join(repoRoot, 'src/components/campaign'), { withFileTypes: true, }) .filter((entry) => entry.isDirectory()) .map((entry) => entry.name) const appRouteDirs = readdirSync(join(repoRoot, 'src/app/(campaign)/campanha/(app)'), { withFileTypes: true, }) .filter((entry) => entry.isDirectory()) .map((entry) => entry.name) const { missing } = findUncoveredE2eDomainPrefixes( componentDirs, appRouteDirs, E2E_AFFECTED_MANIFEST, ) expect(missing, `unmapped domains:\n${missing.join('\n')}`).toEqual([]) }) })