mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-13 15:13:07 +00:00
Adiciona a CLI pnpm archive:integrity: varredura read-only de todas as archivePhoto (download + decode pelo mesmo caminho da rota pública e do índice facial; sai 1 enquanto houver mídia quebrada approved) e --apply que re-ingere do Flickr pelo flickrId, substitui o objeto na mesma chave via payload.update filePath (sem tocar curadoria/status) e rebaixa a draft as irrecuperáveis (removed intocado). Dono da leitura ganha inspectPrivateMediaObject e isPrivateMediaMissingError (S3 NoSuchKey/404 + ENOENT local; checksum nunca é missing); archivePhotoIngest ganha repairArchivePhotoObject e withdrawArchivePhotoFromPublic; decisão do reparo em scripts/lib/archiveIntegrityRepair.mjs com deps injetadas e puro de CLI/recibo em scripts/lib/archiveIntegrityPlan.mjs. Sem migration, sem UI. Guardas: ARCHIVE_INTEGRITY_CONFIRM + TEQO_ENV exato + 4 S3_* (em qualquer modo no alvo não-local) + FLICKR_API_KEY. Runbook C246, changelog e unit/int/e2e afetado.
503 lines
18 KiB
JavaScript
503 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/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/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/cityReportTerritory.mjs',
|
|
'scripts/lib/cli.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/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() }
|
|
}
|