mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-13 15:13:07 +00:00
A ferramenta de abaixo-assinado já existia, mas a petição do PL não tinha caminho reproduzível fora do dump legado: produção só carregava as três petições antigas editadas no admin. O conteúdo agora é versionado em scripts/lib/petitionPl5594Content.mjs e provisionado por pnpm seed:petition-pl-5594 — dry-run por padrão, create-if-missing por id, Consent whatsapp-inscricao resolvido por chave (fail-closed) e guards de escrita (PETITION_SEED_CONFIRM + TEQO_ENV casando o banco exato), sem sobrescrever edição do admin. O corpo traz os fatos do inteiro teor (autorização no registro, 5 exercícios de IR, sigilos bancário/fiscal/telefônico/de conexão, exclusão do conteúdo das comunicações, auditoria, LGPD e divulgação pela Justiça Eleitoral), a motivação de 18-19/10 sem acusar crime e o pedido sem prometer aprovação. A copy hardcoded do bloco do formulário deixa de falar em 'campanha'/'luta dos trabalhadores' e vira genérica. Prova: unit full 475 arquivos/5238 testes verdes, lint/typecheck/format limpos, seed aplicado e reaplicado em DB temporário (criação + no-op) e smoke real no navegador (página 200, consent, assinatura nº 1). Sem migration, collection, Consent novo ou mudança no fluxo de assinatura.
507 lines
18 KiB
JavaScript
507 lines
18 KiB
JavaScript
/**
|
|
* Pure selection logic for affected-test CI jobs (OPS5). No git, no fs, no
|
|
* process — the CLI wrappers feed it the changed file list and it decides.
|
|
* Unit-pinned in tests/unit/testAffected.unit.spec.ts,
|
|
* tests/unit/ciSkipInvariants.unit.spec.ts, and
|
|
* tests/unit/e2eAffectedManifest.unit.spec.ts.
|
|
*
|
|
* OPS86: e2e selection is never silently zero when code changed — high-risk
|
|
* diffs run the curated cross-section (`curated`), risk-area files without a
|
|
* manifest entry fail closed (`unmapped-risk`), and any other unmapped src/
|
|
* file wakes the home smoke.
|
|
*/
|
|
|
|
import {
|
|
E2E_CURATED_SPECS,
|
|
E2E_RISK_PREFIXES,
|
|
E2E_SMOKE_FALLBACK_SPEC,
|
|
} from './e2e-affected-manifest.mjs'
|
|
|
|
/**
|
|
* `scripts/**` modules reachable from a spec under `tests/` (transitive over
|
|
* relative imports). Each is pinned by the suite a lib-only diff would
|
|
* otherwise skip: without the entry `classifyTestScope` returns `none` and the
|
|
* covering spec never runs (S6, OPS119++). The list is literal because this
|
|
* module is pure (no fs); the invariant in
|
|
* tests/unit/ciSkipInvariants.unit.spec.ts recomputes this closure and requires
|
|
* exact equality, so a new spec-pinned module (missing) or a rename (stale)
|
|
* fails the suite until the map tracks it. Overlap with the explicit entries in
|
|
* HIGH_RISK_EXACT below is intentional — this group is the exact map.
|
|
*/
|
|
export const SCRIPTS_SPEC_PINNED = [
|
|
'scripts/build-chart-from-data.mjs',
|
|
'scripts/check-test-locations.mjs',
|
|
'scripts/cityReportSnapshot.mjs',
|
|
'scripts/institutionSnapshot.mjs',
|
|
'scripts/publish-archive-photos.mjs',
|
|
'scripts/themeSnapshot.mjs',
|
|
'scripts/lib/agent-forgejo.mjs',
|
|
'scripts/lib/agent-plan-lifecycle.mjs',
|
|
'scripts/lib/agent-pool-cursor.mjs',
|
|
'scripts/lib/agent-pool-eligibility.mjs',
|
|
'scripts/lib/agent-pool-models.mjs',
|
|
'scripts/lib/agent-pool-prompt.mjs',
|
|
'scripts/lib/agent-pool-state.mjs',
|
|
'scripts/lib/agent-session.mjs',
|
|
'scripts/lib/ansi.mjs',
|
|
'scripts/lib/archiveCatalogPlan.mjs',
|
|
'scripts/lib/archiveIntegrityPlan.mjs',
|
|
'scripts/lib/archiveIntegrityRepair.mjs',
|
|
'scripts/lib/archivePublishPlan.mjs',
|
|
'scripts/lib/archiveThumbnailPlan.mjs',
|
|
'scripts/lib/archiveVisionApi.mjs',
|
|
'scripts/lib/auto-unblock.mjs',
|
|
'scripts/lib/briefingContent.mjs',
|
|
'scripts/lib/briefingRender.mjs',
|
|
'scripts/lib/buildPdf.mjs',
|
|
'scripts/lib/camaraFetch.mjs',
|
|
'scripts/lib/camaraSpeeches.mjs',
|
|
'scripts/lib/campaignKitAssets.mjs',
|
|
'scripts/lib/changelog.mjs',
|
|
'scripts/lib/chartData.mjs',
|
|
'scripts/lib/chartMotion.mjs',
|
|
'scripts/lib/chartMotionRender.mjs',
|
|
'scripts/lib/chartPrimitives.mjs',
|
|
'scripts/lib/cityReportBlocks.mjs',
|
|
'scripts/lib/cityReportDatabase.mjs',
|
|
'scripts/lib/cityReportFormat.mjs',
|
|
'scripts/lib/cityReportRender.mjs',
|
|
'scripts/lib/cityReportResearch.mjs',
|
|
'scripts/lib/cli.mjs',
|
|
'scripts/lib/colinhaReel.mjs',
|
|
'scripts/lib/colinhaReelTemplate.mjs',
|
|
'scripts/lib/conflictMarkers.mjs',
|
|
'scripts/lib/db-start.mjs',
|
|
'scripts/lib/deploy-trigger.mjs',
|
|
'scripts/lib/dossieBlocks.mjs',
|
|
'scripts/lib/dossieBulletin.mjs',
|
|
'scripts/lib/dossieBulletinRender.mjs',
|
|
'scripts/lib/dossieCamara.mjs',
|
|
'scripts/lib/dossieCareer.mjs',
|
|
'scripts/lib/dossieHealthData.mjs',
|
|
'scripts/lib/dossiePack.mjs',
|
|
'scripts/lib/dossieRender.mjs',
|
|
'scripts/lib/dossieResearch.mjs',
|
|
'scripts/lib/dossieUnit.mjs',
|
|
'scripts/lib/e2e-affected-manifest.mjs',
|
|
'scripts/lib/faceConsentTexts.mjs',
|
|
'scripts/lib/faceFigureCatalogSeed.mjs',
|
|
'scripts/lib/faceFigurePlan.mjs',
|
|
'scripts/lib/faceIndexPlan.mjs',
|
|
'scripts/lib/flickrApi.mjs',
|
|
'scripts/lib/flickrPlan.mjs',
|
|
'scripts/lib/github-api.mjs',
|
|
'scripts/lib/github-branch-protection.mjs',
|
|
'scripts/lib/github-pr-flow.mjs',
|
|
'scripts/lib/graficosInstagramRender.mjs',
|
|
'scripts/lib/imageFill.mjs',
|
|
'scripts/lib/imageResize.mjs',
|
|
'scripts/lib/instagramContentPlan.mjs',
|
|
'scripts/lib/issues-panel.mjs',
|
|
'scripts/lib/markdown-ansi.mjs',
|
|
'scripts/lib/mediaBinaries.mjs',
|
|
'scripts/lib/mediaEndpoint.mjs',
|
|
'scripts/lib/opsGlobal.mjs',
|
|
'scripts/lib/petitionPl5594Content.mjs',
|
|
'scripts/lib/plansOnlyClosesGuard.mjs',
|
|
'scripts/lib/playwright-e2e-args.mjs',
|
|
'scripts/lib/portalTransparenciaEmendas.mjs',
|
|
'scripts/lib/readOnlyExtract.mjs',
|
|
'scripts/lib/reel-ingest.mjs',
|
|
'scripts/lib/reelFfmpeg.mjs',
|
|
'scripts/lib/reelPackage.mjs',
|
|
'scripts/lib/reelScript.mjs',
|
|
'scripts/lib/reelShotList.mjs',
|
|
'scripts/lib/reelTimeline.mjs',
|
|
'scripts/lib/reelTts.mjs',
|
|
'scripts/lib/reportText.mjs',
|
|
'scripts/lib/seed-minimal-manifest.mjs',
|
|
'scripts/lib/sollaCeuciSalvadorMetrics.mjs',
|
|
'scripts/lib/speechIndex.mjs',
|
|
'scripts/lib/staging-test-account.mjs',
|
|
'scripts/lib/test-affected-core.mjs',
|
|
'scripts/lib/testing-audit-metrics-core.mjs',
|
|
'scripts/lib/webSpeeches.mjs',
|
|
'scripts/lib/worktree-env.mjs',
|
|
'scripts/lib/worktree.mjs',
|
|
'scripts/lib/wpArticles.mjs',
|
|
]
|
|
|
|
/**
|
|
* Paths whose blast radius is the whole app: schema, test harness, lockfile,
|
|
* skip classifiers. Any diff touching them runs the FULL suites — selection
|
|
* is unsafe there.
|
|
*/
|
|
export const HIGH_RISK_EXACT = new Set([
|
|
'package.json',
|
|
'pnpm-lock.yaml',
|
|
'playwright.config.ts',
|
|
'vitest.config.mts',
|
|
'vitest.unit.config.mts',
|
|
'vitest.setup.ts',
|
|
'src/payload-types.ts',
|
|
'src/payload.config.ts',
|
|
'scripts/seed-minimal.mjs',
|
|
'scripts/lib/seed-minimal-manifest.mjs',
|
|
'scripts/worktree.mjs',
|
|
'scripts/agent-session.mjs',
|
|
'scripts/lib/agent-session.mjs',
|
|
'scripts/lib/worktree.mjs',
|
|
'scripts/lib/test-affected-core.mjs',
|
|
'scripts/lib/e2e-affected-manifest.mjs',
|
|
'scripts/test-affected.mjs',
|
|
'scripts/e2e-affected.mjs',
|
|
'scripts/run-e2e-affected.mjs',
|
|
'scripts/vitest-changed-or-full.mjs',
|
|
'scripts/ci-scope.mjs',
|
|
'scripts/db-reset.mjs',
|
|
'scripts/lib/cli.mjs',
|
|
'scripts/check-test-locations.mjs',
|
|
'scripts/check-plans-only-pr-closes.mjs',
|
|
'scripts/lib/plansOnlyClosesGuard.mjs',
|
|
'scripts/gate-ci.mjs',
|
|
'tests/unit/ciSkipInvariants.unit.spec.ts',
|
|
'tests/unit/testAffected.unit.spec.ts',
|
|
'.env.test',
|
|
// OPS119++: spec-pinned scripts/** modules (some already explicit above; the
|
|
// Set dedupes — SCRIPTS_SPEC_PINNED is the exact map the invariant checks).
|
|
...SCRIPTS_SPEC_PINNED,
|
|
])
|
|
|
|
export const HIGH_RISK_PREFIXES = [
|
|
'src/migrations/',
|
|
'src/collections/',
|
|
'src/globals/',
|
|
'tests/helpers/',
|
|
'tests/e2e/fixtures/',
|
|
]
|
|
|
|
/** Canonical vitest/playwright globs — keep in sync with config files. */
|
|
export const CANONICAL_UNIT_INCLUDE = 'tests/unit/**/*.unit.spec.{ts,tsx}'
|
|
export const CANONICAL_INT_INCLUDE = 'tests/int/**/*.int.spec.ts'
|
|
export const CANONICAL_E2E_TEST_DIR = 'tests/e2e'
|
|
export const CANONICAL_E2E_SPEC_SUFFIX = '.e2e.spec.ts'
|
|
|
|
const CODE_CONFIG_EXACT = new Set([
|
|
'package.json',
|
|
'pnpm-lock.yaml',
|
|
'tsconfig.json',
|
|
'tsconfig.tsbuildinfo',
|
|
'eslint.config.mjs',
|
|
'eslint.config.js',
|
|
'knip.json',
|
|
'knip.ts',
|
|
'knip.config.ts',
|
|
'.eslintrc.cjs',
|
|
'.eslintrc.js',
|
|
])
|
|
|
|
const CODE_CONFIG_PREFIXES = ['tsconfig']
|
|
|
|
const BUILD_SURFACE_EXACT = new Set([
|
|
'package.json',
|
|
'pnpm-lock.yaml',
|
|
'next.config.js',
|
|
'next.config.mjs',
|
|
'next.config.ts',
|
|
'src/payload.config.ts',
|
|
'src/payload-types.ts',
|
|
'tsconfig.json',
|
|
// OPS88: a change to the reset script must exercise it — build_mode='build'
|
|
// arms the Reset step in ci-pr.yml (and the mirror in gate-ci.mjs).
|
|
'scripts/db-reset.mjs',
|
|
])
|
|
|
|
const BUILD_SURFACE_PREFIXES = [
|
|
'src/',
|
|
'public/',
|
|
'src/migrations/',
|
|
'src/collections/',
|
|
'src/globals/',
|
|
'tsconfig',
|
|
]
|
|
|
|
const SPEC_FILE_RE = /\.(?:spec|test)\.(?:ts|tsx)$/
|
|
const UNIT_SPEC_RE = /^tests\/unit\/.+\.unit\.spec\.(?:ts|tsx)$/
|
|
const INT_SPEC_RE = /^tests\/int\/.+\.int\.spec\.ts$/
|
|
const E2E_SPEC_RE = /^tests\/e2e\/[^/]+\.e2e\.spec\.ts$/
|
|
|
|
/** Cross-cutting dirs under campaign components (no dedicated e2e family). */
|
|
export const E2E_MANIFEST_DOMAIN_EXEMPT = new Set(['shared'])
|
|
|
|
export function isHighRisk(path) {
|
|
return HIGH_RISK_EXACT.has(path) || HIGH_RISK_PREFIXES.some((prefix) => path.startsWith(prefix))
|
|
}
|
|
|
|
export function isTestPath(path) {
|
|
return path.startsWith('tests/')
|
|
}
|
|
|
|
export function isSrcPath(path) {
|
|
return path.startsWith('src/')
|
|
}
|
|
|
|
/**
|
|
* Paths that affect typecheck / knip / madge (and therefore those PR jobs).
|
|
*/
|
|
export function isCodePath(path) {
|
|
if (isSrcPath(path) || isTestPath(path)) return true
|
|
if (path.startsWith('scripts/')) return true
|
|
if (CODE_CONFIG_EXACT.has(path)) return true
|
|
if (CODE_CONFIG_PREFIXES.some((prefix) => path === prefix || path.startsWith(`${prefix}.`))) {
|
|
return true
|
|
}
|
|
if (path.startsWith('eslint') || path.includes('eslint.config')) return true
|
|
if (path.startsWith('knip')) return true
|
|
return false
|
|
}
|
|
|
|
/**
|
|
* Paths that affect `pnpm build` (Next + Payload surface).
|
|
*/
|
|
export function isBuildPath(path) {
|
|
if (BUILD_SURFACE_EXACT.has(path)) return true
|
|
if (isSrcPath(path) || path.startsWith('public/')) return true
|
|
if (path.startsWith('next.config')) return true
|
|
if (BUILD_SURFACE_PREFIXES.some((prefix) => path.startsWith(prefix))) return true
|
|
return false
|
|
}
|
|
|
|
export function isCanonicalSpecPath(path) {
|
|
return UNIT_SPEC_RE.test(path) || INT_SPEC_RE.test(path) || E2E_SPEC_RE.test(path)
|
|
}
|
|
|
|
export function isMisplacedSpecPath(path) {
|
|
return SPEC_FILE_RE.test(path) && !isCanonicalSpecPath(path)
|
|
}
|
|
|
|
/**
|
|
* Decide vitest scope for a PR diff.
|
|
* @param {{ path: string, status: string }[]} files git diff --name-status rows.
|
|
* @returns {{ mode: 'full' | 'changed' | 'none', reason: string }}
|
|
*/
|
|
export function classifyTestScope(files) {
|
|
if (files.some(({ path }) => isHighRisk(path))) {
|
|
return { mode: 'full', reason: 'diff touches a high-risk path (schema/lockfile/test harness)' }
|
|
}
|
|
if (files.some(({ path, status }) => status === 'A' && path.startsWith('src/'))) {
|
|
return { mode: 'full', reason: 'new src/ file — vitest --changed cannot trace tests to it yet' }
|
|
}
|
|
if (files.some(({ path }) => path.startsWith('src/') || path.startsWith('tests/'))) {
|
|
return { mode: 'changed', reason: 'src/tests diff without high-risk paths' }
|
|
}
|
|
return { mode: 'none', reason: 'no src/ or tests/ changes' }
|
|
}
|
|
|
|
/**
|
|
* Decide whether typecheck / knip / cycles should run.
|
|
* @returns {{ mode: 'code' | 'none', reason: string }}
|
|
*/
|
|
export function classifyStaticScope(files) {
|
|
if (files.some(({ path }) => isCodePath(path))) {
|
|
return { mode: 'code', reason: 'diff touches src/tests/scripts or type/graph config' }
|
|
}
|
|
return { mode: 'none', reason: 'no code/type/graph surface changes' }
|
|
}
|
|
|
|
/**
|
|
* Decide whether the PR build job should run.
|
|
* @returns {{ mode: 'build' | 'none', reason: string }}
|
|
*/
|
|
export function classifyBuildScope(files) {
|
|
if (files.some(({ path }) => isHighRisk(path) && isBuildPath(path))) {
|
|
return { mode: 'build', reason: 'diff touches a high-risk build surface path' }
|
|
}
|
|
if (files.some(({ path }) => isBuildPath(path))) {
|
|
return { mode: 'build', reason: 'diff touches build surface' }
|
|
}
|
|
return { mode: 'none', reason: 'no build surface changes' }
|
|
}
|
|
|
|
const E2E_SPEC_PATTERN = /^tests\/e2e\/[^/]+\.e2e\.spec\.ts$/
|
|
|
|
/**
|
|
* Walk files and collect src/ paths that have no manifest match.
|
|
* This is the single source of truth for unmapped paths in selectE2eSpecs — the
|
|
* loop below consumes this list (via Set) instead of re-collecting.
|
|
* Mirrors loop semantics: skip deleted e2e spec files, only src/ paths, startsWith.
|
|
* @param {{ path: string, status: string }[]} files
|
|
* @param {{ prefixes: string[], specs: string[] }[]} manifest
|
|
* @returns {string[]}
|
|
*/
|
|
function collectUnmappedSrc(files, manifest) {
|
|
const unmapped = []
|
|
for (const { path, status } of files) {
|
|
// A deleted spec file cannot run — selecting it would make CI fail with
|
|
// "No tests found" (the stale-manifest variant of a spec rename).
|
|
if (status === 'D' && E2E_SPEC_PATTERN.test(path)) continue
|
|
if (E2E_SPEC_PATTERN.test(path)) continue
|
|
if (!path.startsWith('src/')) continue
|
|
if (manifest.some((entry) => entry.prefixes.some((prefix) => path.startsWith(prefix)))) {
|
|
continue
|
|
}
|
|
unmapped.push(path)
|
|
}
|
|
return unmapped
|
|
}
|
|
|
|
/**
|
|
* Decide the Playwright scope for a PR diff.
|
|
* @param {{ path: string, status: string }[]} files
|
|
* @param {{ prefixes: string[], specs: string[] }[]} manifest prefix → spec names.
|
|
* @returns {{ mode: 'curated' | 'selected' | 'unmapped-risk' | 'none',
|
|
* specs: string[], reason: string, unmapped: string[] }}
|
|
*
|
|
* Modes (OPS86):
|
|
* - `curated` — high-risk diff: run `E2E_CURATED_SPECS`, never zero (the
|
|
* full suite stays in the deploy verify). If risk-area files without a
|
|
* manifest entry exist, they are included in `unmapped` and appended to
|
|
* `reason` (OPS86+: diagnostic visibility even when curated wins).
|
|
* - `unmapped-risk` — a risk-area file (`E2E_RISK_PREFIXES`) matched no
|
|
* manifest entry: the CI fails closed listing the files instead of going
|
|
* green with zero e2e.
|
|
* - `selected` — manifest-selected specs; an unmapped non-risk src/ file
|
|
* still wakes the home smoke so code never ships on zero e2e.
|
|
* - `none` — no e2e-relevant changes (docs-only).
|
|
*/
|
|
export function selectE2eSpecs(files, manifest) {
|
|
// OPS86+: compute unmapped src/ files before any early-return so risk-area
|
|
// files without a manifest entry surface in the curated result (mode stays
|
|
// curated — the curated cross-section covers risk surfaces).
|
|
const unmappedSrc = collectUnmappedSrc(files, manifest)
|
|
const unmappedRisk = unmappedSrc.filter((path) =>
|
|
E2E_RISK_PREFIXES.some((prefix) => path.startsWith(prefix)),
|
|
)
|
|
if (files.some(({ path }) => isHighRisk(path))) {
|
|
return {
|
|
mode: 'curated',
|
|
specs: [...E2E_CURATED_SPECS],
|
|
reason:
|
|
'diff touches a high-risk path (schema/lockfile/test harness) — curated e2e cross-section; full suite stays in the deploy verify' +
|
|
(unmappedRisk.length > 0
|
|
? ` — curated + risk files without mapping: ${unmappedRisk.join(', ')}`
|
|
: ''),
|
|
unmapped: unmappedRisk,
|
|
}
|
|
}
|
|
// Main loop: collect specs. The hoisted unmappedSrc list is the single source
|
|
// of truth for unmapped; unmapped files are skipped via `continue`.
|
|
const specs = new Set()
|
|
for (const { path, status } of files) {
|
|
// A deleted spec file cannot run — selecting it would make CI fail with
|
|
// "No tests found" (the stale-manifest variant of a spec rename).
|
|
if (status === 'D' && E2E_SPEC_PATTERN.test(path)) continue
|
|
if (E2E_SPEC_PATTERN.test(path)) {
|
|
specs.add(path.slice('tests/e2e/'.length, -'.e2e.spec.ts'.length))
|
|
continue
|
|
}
|
|
if (!path.startsWith('src/')) continue
|
|
const matches = manifest.filter((entry) =>
|
|
entry.prefixes.some((prefix) => path.startsWith(prefix)),
|
|
)
|
|
if (matches.length === 0) continue // already in unmappedSrc
|
|
for (const match of matches) for (const spec of match.specs) specs.add(spec)
|
|
}
|
|
// unmappedRisk is already computed above; reuse for the fail-closed branch.
|
|
if (unmappedRisk.length > 0) {
|
|
return {
|
|
mode: 'unmapped-risk',
|
|
specs: [],
|
|
reason: 'risk-area files have no e2e manifest mapping — add an entry (fail-closed)',
|
|
unmapped: unmappedSrc,
|
|
}
|
|
}
|
|
if (specs.size === 0) {
|
|
if (unmappedSrc.length > 0) {
|
|
return {
|
|
mode: 'selected',
|
|
specs: [E2E_SMOKE_FALLBACK_SPEC],
|
|
reason:
|
|
'src/ changes with no e2e manifest mapping — running the home smoke (never zero e2e)',
|
|
unmapped: unmappedSrc,
|
|
}
|
|
}
|
|
return {
|
|
mode: 'none',
|
|
specs: [],
|
|
reason: 'no e2e-relevant changes',
|
|
unmapped: [],
|
|
}
|
|
}
|
|
// The `setup` spec is dev-mode-only: the setup project is dropped under
|
|
// CI/prod (playwright.config.ts), so a selection containing ONLY it would
|
|
// run `playwright test -- tests/e2e/setup.e2e.spec.ts` against zero projects
|
|
// and fail with "No tests found" (OPS39 — first setup-only PR hit it). In a
|
|
// mixed set the spec is harmless (it just matches no project), so only the
|
|
// setup-only case drops out — and an unmapped src/ file still wakes the
|
|
// smoke instead (OPS86: never zero e2e on touched code).
|
|
if (specs.size === 1 && specs.has('setup')) {
|
|
if (unmappedSrc.length > 0) {
|
|
return {
|
|
mode: 'selected',
|
|
specs: [E2E_SMOKE_FALLBACK_SPEC],
|
|
reason: 'setup-only spec plus unmapped src/ changes — running the home smoke',
|
|
unmapped: unmappedSrc,
|
|
}
|
|
}
|
|
return {
|
|
mode: 'none',
|
|
specs: [],
|
|
reason: 'setup spec is dev-mode-only; prod-mode e2e cannot run it',
|
|
unmapped: unmappedSrc,
|
|
}
|
|
}
|
|
return {
|
|
mode: 'selected',
|
|
specs: [...specs].sort(),
|
|
reason: `${specs.size} spec(s) selected via manifest and changed specs`,
|
|
unmapped: unmappedSrc,
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Walk a list of repo-relative paths and return misplaced spec/test files.
|
|
* @param {string[]} paths
|
|
* @returns {string[]}
|
|
*/
|
|
export function findMisplacedSpecPaths(paths) {
|
|
return paths.filter(isMisplacedSpecPath).sort()
|
|
}
|
|
|
|
/**
|
|
* Domain folders that must appear in the e2e affected manifest (or be exempt).
|
|
* @param {string[]} componentDirs immediate children of src/components/campaign
|
|
* @param {string[]} appRouteDirs immediate children of src/app/(campaign)/campanha/(app)
|
|
* @param {{ prefixes: string[] }[]} manifest
|
|
* @param {Set<string>} [exempt]
|
|
* @returns {{ missing: string[], covered: string[] }}
|
|
*/
|
|
export function findUncoveredE2eDomainPrefixes(
|
|
componentDirs,
|
|
appRouteDirs,
|
|
manifest,
|
|
exempt = E2E_MANIFEST_DOMAIN_EXEMPT,
|
|
) {
|
|
const prefixes = manifest.flatMap((entry) => entry.prefixes)
|
|
const required = []
|
|
|
|
for (const dir of componentDirs) {
|
|
if (exempt.has(dir)) continue
|
|
required.push(`src/components/campaign/${dir}`)
|
|
}
|
|
for (const dir of appRouteDirs) {
|
|
if (dir.includes('.')) continue // files like page.tsx / error.tsx
|
|
required.push(`src/app/(campaign)/campanha/(app)/${dir}`)
|
|
}
|
|
|
|
const missing = []
|
|
const covered = []
|
|
for (const path of required) {
|
|
const hit = prefixes.some(
|
|
(prefix) => prefix === path || prefix.startsWith(`${path}/`) || path.startsWith(prefix),
|
|
)
|
|
if (hit) covered.push(path)
|
|
else missing.push(path)
|
|
}
|
|
return { missing: missing.sort(), covered: covered.sort() }
|
|
}
|