From b06d285922a8677172cb2df33ab0644c22bd890c Mon Sep 17 00:00:00 2001 From: Martin Porwoll Date: Sun, 15 Feb 2026 22:54:45 +0000 Subject: [PATCH] test: implement project-wide dynamic test growth guard --- .github/workflows/ci.yml | 3 + .gitignore | 1 + CLAUDE.md | 3 + README.md | 1 + docs/TEST_SYSTEM.md | 49 ++++++ package.json | 4 +- scripts/test-system/audit.mjs | 301 ++++++++++++++++++++++++++++++++ tests/test-system-baseline.json | 263 ++++++++++++++++++++++++++++ 8 files changed, 624 insertions(+), 1 deletion(-) create mode 100644 docs/TEST_SYSTEM.md create mode 100644 scripts/test-system/audit.mjs create mode 100644 tests/test-system-baseline.json diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0816464..9a95b52 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -53,6 +53,9 @@ jobs: - name: Install dependencies run: pnpm install --frozen-lockfile + - name: Test system growth guard + run: pnpm test:system + - name: Run ESLint run: pnpm lint # Warnings are allowed, only errors fail the build diff --git a/.gitignore b/.gitignore index 9bdc413..6e60d63 100644 --- a/.gitignore +++ b/.gitignore @@ -50,3 +50,4 @@ node_modules/ /playwright/.cache/ *.sql .playwright-mcp/ +docs/reports/test-system-status.md diff --git a/CLAUDE.md b/CLAUDE.md index a9039c7..0304cda 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -104,10 +104,13 @@ pm2 logs queue-worker | pm2 restart queue-worker # Tests pnpm test # Alle Tests +pnpm test:system # Dynamischer Test-Growth-Guard (projektweit) pnpm test:security # Security Tests pnpm test:access-control # Access Control Tests pnpm test:coverage # Mit Coverage-Report +Testsystem-Doku: `docs/TEST_SYSTEM.md` + # Lint & Format pnpm lint # ESLint pnpm typecheck # TypeScript Check (4GB heap) diff --git a/README.md b/README.md index 28b4538..34f1c4f 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,7 @@ pnpm build # Production build pnpm payload migrate # Run migrations pnpm payload migrate:create # Create migration pnpm test # Run tests +pnpm test:system # Dynamic test-growth guard pnpm lint # ESLint pnpm typecheck # TypeScript check ``` diff --git a/docs/TEST_SYSTEM.md b/docs/TEST_SYSTEM.md new file mode 100644 index 0000000..1554ed9 --- /dev/null +++ b/docs/TEST_SYSTEM.md @@ -0,0 +1,49 @@ +# Projektweites Dynamisches Testsystem + +## Ziel + +Dieses System stellt sicher, dass die Testabdeckung mit dem gesamten Projekt mitwaechst. +Bestehende Altlasten werden als Baseline dokumentiert, aber neue ungetestete Dateien +werden sofort als Fehler erkannt. + +## Komponenten + +- Guard-Script: `scripts/test-system/audit.mjs` +- Baseline: `tests/test-system-baseline.json` +- Statusreport: `docs/reports/test-system-status.md` + +## Funktionsweise + +1. Das Script scannt alle `src/**/*.{ts,tsx}` Dateien (mit Exclusions, z. B. Migrationen). +2. Es sucht Test-Evidenz in `tests/unit`, `tests/int`, `tests/e2e`: +- direkte Modulimporte (z. B. `@/lib/...`) +- API-Endpoint-Nutzung fuer `route.ts` Dateien (z. B. `/api/news`) +- konservative Dateinamen-Heuristik +3. Es vergleicht die aktuelle Liste ungetesteter Dateien mit der Baseline. +4. Ergebnis: +- **Pass**: keine neuen Luecken +- **Fail**: neue ungetestete Dateien gefunden + +## Befehle + +```bash +# Guard ausfuehren (CI-geeignet) +pnpm test:system + +# Baseline absichtlich aktualisieren +pnpm test:system:update +``` + +## Team-Regeln fuer laufende Weiterentwicklung + +- Bei jeder neuen Datei unter `src/` direkt passende Tests anlegen. +- Wenn technisch begruendet keine Tests moeglich sind, Datei bewusst in der Baseline belassen. +- Baseline nur nach Review aktualisieren (kein automatisches "wegklicken" von Luecken). +- Report unter `docs/reports/test-system-status.md` bei Auffaelligkeiten pruefen. + +## CI-Integration + +Der Guard laeuft im CI-Workflow `ci.yml` im Lint-Job als Schritt: +- `pnpm test:system` + +Damit greifen die Regeln sowohl auf Pull Requests als auch auf Pushes nach `develop`. diff --git a/package.json b/package.json index 1ba426a..229f3de 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,9 @@ "format": "prettier --write \"src/**/*.{ts,tsx,js,jsx}\" --ignore-unknown", "payload": "cross-env NODE_OPTIONS=--no-deprecation payload", "start": "cross-env NODE_OPTIONS=--no-deprecation next start", - "test": "pnpm run test:unit && pnpm run test:int && pnpm run test:e2e", + "test": "pnpm run test:system && pnpm run test:unit && pnpm run test:int && pnpm run test:e2e", + "test:system": "node ./scripts/test-system/audit.mjs", + "test:system:update": "node ./scripts/test-system/audit.mjs --update-baseline", "test:unit": "cross-env NODE_OPTIONS=--no-deprecation vitest run --config ./vitest.config.mts tests/unit", "test:int": "cross-env NODE_OPTIONS=--no-deprecation vitest run --config ./vitest.config.mts tests/int", "test:security": "cross-env NODE_OPTIONS=--no-deprecation vitest run --config ./vitest.config.mts tests/unit/security tests/int/security-api.int.spec.ts", diff --git a/scripts/test-system/audit.mjs b/scripts/test-system/audit.mjs new file mode 100644 index 0000000..cb9ea4c --- /dev/null +++ b/scripts/test-system/audit.mjs @@ -0,0 +1,301 @@ +#!/usr/bin/env node +import fs from 'node:fs' +import path from 'node:path' + +const ROOT = process.cwd() +const SRC_DIR = path.join(ROOT, 'src') +const TEST_DIR = path.join(ROOT, 'tests') +const BASELINE_FILE = path.join(ROOT, 'tests', 'test-system-baseline.json') +const REPORT_FILE = path.join(ROOT, 'docs', 'reports', 'test-system-status.md') + +const args = new Set(process.argv.slice(2)) +const updateBaseline = args.has('--update-baseline') +const reportOnly = args.has('--report-only') + +const SOURCE_EXTENSIONS = new Set(['.ts', '.tsx']) +const TEST_EXTENSIONS = new Set(['.ts', '.tsx']) + +const EXCLUDE_SOURCE_PATTERNS = [ + /(^|\/)migrations\//, + /(^|\/)payload-types\.ts$/, + /(^|\/)payload-generated-schema\.ts$/, + /\.d\.ts$/, +] + +function toPosix(p) { + return p.split(path.sep).join('/') +} + +function fileExists(p) { + try { + fs.accessSync(p, fs.constants.F_OK) + return true + } catch { + return false + } +} + +function walkFiles(dir) { + if (!fileExists(dir)) return [] + const entries = fs.readdirSync(dir, { withFileTypes: true }) + const files = [] + + for (const entry of entries) { + const abs = path.join(dir, entry.name) + if (entry.isDirectory()) { + files.push(...walkFiles(abs)) + } else if (entry.isFile()) { + files.push(abs) + } + } + + return files +} + +function isExcludedSource(relPath) { + return EXCLUDE_SOURCE_PATTERNS.some((pattern) => pattern.test(relPath)) +} + +function stripExtension(relPath) { + return relPath.replace(/\.[^.]+$/, '') +} + +function isRouteFile(relPath) { + return /\/api\/.+\/route\.ts$/.test(relPath) +} + +function routeEndpointFromSource(relPath) { + const noGroups = relPath.replace(/\([^)]*\)\//g, '') + const apiIndex = noGroups.indexOf('/api/') + if (apiIndex === -1) return null + + let endpoint = noGroups.slice(apiIndex).replace(/\/route\.ts$/, '') + endpoint = endpoint.replace(/\[\.\.\.[^/]+\]/g, '') + endpoint = endpoint.replace(/\[[^/]+\]/g, '') + endpoint = endpoint.replace(/\/+/g, '/') + if (!endpoint.startsWith('/api/')) return null + return endpoint.endsWith('/') && endpoint.length > 1 ? endpoint.slice(0, -1) : endpoint +} + +function buildSourceRecords() { + const files = walkFiles(SRC_DIR) + .map((abs) => toPosix(path.relative(ROOT, abs))) + .filter((rel) => SOURCE_EXTENSIONS.has(path.extname(rel))) + .filter((rel) => !isExcludedSource(rel)) + .sort() + + return files.map((rel) => { + const noExt = stripExtension(rel) + const base = path.basename(noExt) + const dir = path.dirname(noExt) + + const importNeedles = new Set([ + `@/${noExt.replace(/^src\//, '')}`, + noExt, + ]) + + if (base === 'index') { + importNeedles.add(`@/${dir.replace(/^src\//, '')}`) + importNeedles.add(dir) + } + + return { + source: rel, + noExt, + base, + endpoint: isRouteFile(rel) ? routeEndpointFromSource(rel) : null, + importNeedles: [...importNeedles], + } + }) +} + +function buildTestRecords() { + const files = walkFiles(TEST_DIR) + .map((abs) => toPosix(path.relative(ROOT, abs))) + .filter((rel) => TEST_EXTENSIONS.has(path.extname(rel))) + .sort() + + return files.map((rel) => { + const content = fs.readFileSync(path.join(ROOT, rel), 'utf8') + return { testFile: rel, content } + }) +} + +function hasImportEvidence(test, sourceRecord) { + for (const needle of sourceRecord.importNeedles) { + if (!needle) continue + if (test.content.includes(needle)) return true + } + return false +} + +function hasEndpointEvidence(test, sourceRecord) { + if (!sourceRecord.endpoint) return false + return test.content.includes(sourceRecord.endpoint) +} + +function hasFilenameHeuristic(test, sourceRecord) { + if (sourceRecord.base.length < 5) return false + const testName = path.basename(test.testFile).toLowerCase() + return testName.includes(sourceRecord.base.toLowerCase()) +} + +function findCoverageForSource(sourceRecord, tests) { + const matched = [] + + for (const test of tests) { + if (hasImportEvidence(test, sourceRecord)) { + matched.push({ testFile: test.testFile, reason: 'import' }) + continue + } + + if (hasEndpointEvidence(test, sourceRecord)) { + matched.push({ testFile: test.testFile, reason: 'endpoint' }) + continue + } + + if (hasFilenameHeuristic(test, sourceRecord)) { + matched.push({ testFile: test.testFile, reason: 'filename' }) + } + } + + return matched +} + +function ensureDirFor(filePath) { + fs.mkdirSync(path.dirname(filePath), { recursive: true }) +} + +function loadBaseline() { + if (!fileExists(BASELINE_FILE)) return null + try { + const parsed = JSON.parse(fs.readFileSync(BASELINE_FILE, 'utf8')) + const untested = Array.isArray(parsed?.untestedSources) + ? parsed.untestedSources.filter((v) => typeof v === 'string') + : [] + return { ...parsed, untestedSources: untested } + } catch { + return null + } +} + +function writeBaseline(payload) { + ensureDirFor(BASELINE_FILE) + fs.writeFileSync(BASELINE_FILE, `${JSON.stringify(payload, null, 2)}\n`, 'utf8') +} + +function writeReport(payload) { + const lines = [] + lines.push('# Test System Status') + lines.push('') + lines.push(`- Generated: ${payload.generatedAt}`) + lines.push(`- Total source files (monitored): ${payload.totalSources}`) + lines.push(`- Covered source files: ${payload.coveredSources}`) + lines.push(`- Untested source files: ${payload.untestedSources.length}`) + lines.push(`- Baseline size: ${payload.baselineSize}`) + lines.push(`- New gaps vs baseline: ${payload.newGaps.length}`) + lines.push(`- Fixed since baseline: ${payload.fixedSinceBaseline.length}`) + lines.push('') + + lines.push('## New Gaps (Failing)') + lines.push('') + if (payload.newGaps.length === 0) { + lines.push('- None') + } else { + for (const item of payload.newGaps) lines.push(`- \`${item}\``) + } + lines.push('') + + lines.push('## Fixed Since Baseline') + lines.push('') + if (payload.fixedSinceBaseline.length === 0) { + lines.push('- None') + } else { + for (const item of payload.fixedSinceBaseline) lines.push(`- \`${item}\``) + } + lines.push('') + + lines.push('## Current Untested Inventory') + lines.push('') + for (const item of payload.untestedSources) lines.push(`- \`${item}\``) + + ensureDirFor(REPORT_FILE) + fs.writeFileSync(REPORT_FILE, `${lines.join('\n')}\n`, 'utf8') +} + +function main() { + const sourceRecords = buildSourceRecords() + const tests = buildTestRecords() + + const coverageEntries = sourceRecords.map((source) => ({ + source: source.source, + matches: findCoverageForSource(source, tests), + })) + + const coveredSources = coverageEntries.filter((e) => e.matches.length > 0).map((e) => e.source) + const untestedSources = coverageEntries.filter((e) => e.matches.length === 0).map((e) => e.source) + + const baseline = loadBaseline() + const baselineUntested = new Set(baseline?.untestedSources || []) + const currentUntested = new Set(untestedSources) + + const newGaps = [...currentUntested].filter((s) => !baselineUntested.has(s)).sort() + const fixedSinceBaseline = [...baselineUntested].filter((s) => !currentUntested.has(s)).sort() + + const reportPayload = { + generatedAt: new Date().toISOString(), + totalSources: sourceRecords.length, + coveredSources: coveredSources.length, + untestedSources: [...currentUntested].sort(), + baselineSize: baselineUntested.size, + newGaps, + fixedSinceBaseline, + } + + writeReport(reportPayload) + + if (updateBaseline) { + const baselinePayload = { + version: 1, + generatedAt: reportPayload.generatedAt, + totalSources: reportPayload.totalSources, + coveredSources: reportPayload.coveredSources, + untestedSources: reportPayload.untestedSources, + notes: [ + 'Baseline blocks only NEW untested files.', + 'Update baseline intentionally after planned test-debt cleanup.', + ], + } + + writeBaseline(baselinePayload) + console.log(`Baseline updated: ${toPosix(path.relative(ROOT, BASELINE_FILE))}`) + console.log(`Report generated: ${toPosix(path.relative(ROOT, REPORT_FILE))}`) + return + } + + if (!baseline) { + console.error('Missing baseline file.') + console.error('Run: pnpm test:system:update') + process.exit(reportOnly ? 0 : 1) + } + + console.log(`Report generated: ${toPosix(path.relative(ROOT, REPORT_FILE))}`) + console.log(`Monitored sources: ${reportPayload.totalSources}`) + console.log(`Covered sources: ${reportPayload.coveredSources}`) + console.log(`Untested sources: ${reportPayload.untestedSources.length}`) + console.log(`New gaps vs baseline: ${newGaps.length}`) + + if (fixedSinceBaseline.length > 0) { + console.log(`Fixed since baseline: ${fixedSinceBaseline.length} (run pnpm test:system:update to shrink baseline)`) + } + + if (!reportOnly && newGaps.length > 0) { + console.error('New untested source files detected:') + for (const gap of newGaps) { + console.error(`- ${gap}`) + } + process.exit(1) + } +} + +main() diff --git a/tests/test-system-baseline.json b/tests/test-system-baseline.json new file mode 100644 index 0000000..ca908aa --- /dev/null +++ b/tests/test-system-baseline.json @@ -0,0 +1,263 @@ +{ + "version": 1, + "generatedAt": "2026-02-15T20:51:48.854Z", + "totalSources": 301, + "coveredSources": 50, + "untestedSources": [ + "src/app/(frontend)/[locale]/layout.tsx", + "src/app/(frontend)/[locale]/page.tsx", + "src/app/(frontend)/api/team/[slug]/vcard/route.ts", + "src/app/(frontend)/api/team/route.ts", + "src/app/(frontend)/api/workflows/route.ts", + "src/app/(frontend)/layout.tsx", + "src/app/(frontend)/page.tsx", + "src/app/(payload)/admin/[[...segments]]/not-found.tsx", + "src/app/(payload)/admin/[[...segments]]/page.tsx", + "src/app/(payload)/admin/views/community/analytics/AnalyticsDashboard.tsx", + "src/app/(payload)/admin/views/community/analytics/components/ChannelComparison.tsx", + "src/app/(payload)/admin/views/community/analytics/components/KPICards.tsx", + "src/app/(payload)/admin/views/community/analytics/components/ResponseMetrics.tsx", + "src/app/(payload)/admin/views/community/analytics/components/SentimentTrendChart.tsx", + "src/app/(payload)/admin/views/community/analytics/components/TopContent.tsx", + "src/app/(payload)/admin/views/community/analytics/components/TopicCloud.tsx", + "src/app/(payload)/admin/views/community/analytics/page.tsx", + "src/app/(payload)/admin/views/community/inbox/CommunityInbox.tsx", + "src/app/(payload)/admin/views/community/inbox/page.tsx", + "src/app/(payload)/api/admin/queues/[queueName]/jobs/route.ts", + "src/app/(payload)/api/admin/queues/route.ts", + "src/app/(payload)/api/auth/meta/callback/route.ts", + "src/app/(payload)/api/auth/meta/route.ts", + "src/app/(payload)/api/community/analytics/channel-comparison/route.ts", + "src/app/(payload)/api/community/analytics/overview/route.ts", + "src/app/(payload)/api/community/analytics/response-metrics/route.ts", + "src/app/(payload)/api/community/analytics/sentiment-trend/route.ts", + "src/app/(payload)/api/community/analytics/top-content/route.ts", + "src/app/(payload)/api/community/analytics/topic-cloud/route.ts", + "src/app/(payload)/api/community/export/route.ts", + "src/app/(payload)/api/community/generate-reply/route.ts", + "src/app/(payload)/api/community/reply/route.ts", + "src/app/(payload)/api/community/stats/route.ts", + "src/app/(payload)/api/community/stream/route.ts", + "src/app/(payload)/api/community/sync-comments/route.ts", + "src/app/(payload)/api/community/sync-status/route.ts", + "src/app/(payload)/api/community/sync/route.ts", + "src/app/(payload)/api/cron/community-sync/route.ts", + "src/app/(payload)/api/cron/send-reports/route.ts", + "src/app/(payload)/api/cron/token-refresh/route.ts", + "src/app/(payload)/api/cron/youtube-channel-sync/route.ts", + "src/app/(payload)/api/cron/youtube-metrics-sync/route.ts", + "src/app/(payload)/api/cron/youtube-sync/route.ts", + "src/app/(payload)/api/email-logs/export/route.ts", + "src/app/(payload)/api/email-logs/stats/route.ts", + "src/app/(payload)/api/generate-pdf/route.ts", + "src/app/(payload)/api/graphql/route.ts", + "src/app/(payload)/api/monitoring/alerts/acknowledge/route.ts", + "src/app/(payload)/api/monitoring/alerts/route.ts", + "src/app/(payload)/api/monitoring/health/route.ts", + "src/app/(payload)/api/monitoring/logs/route.ts", + "src/app/(payload)/api/monitoring/performance/route.ts", + "src/app/(payload)/api/monitoring/services/route.ts", + "src/app/(payload)/api/monitoring/snapshots/route.ts", + "src/app/(payload)/api/monitoring/stream/route.ts", + "src/app/(payload)/api/retention/route.ts", + "src/app/(payload)/api/youtube/analytics/route.ts", + "src/app/(payload)/api/youtube/auth/route.ts", + "src/app/(payload)/api/youtube/calendar/route.ts", + "src/app/(payload)/api/youtube/capacity/route.ts", + "src/app/(payload)/api/youtube/complete-task/[id]/route.ts", + "src/app/(payload)/api/youtube/dashboard/route.ts", + "src/app/(payload)/api/youtube/my-tasks/route.ts", + "src/app/(payload)/api/youtube/refresh-token/route.ts", + "src/app/(payload)/api/youtube/thumbnails/bulk/route.ts", + "src/app/(payload)/api/youtube/upload/route.ts", + "src/app/(payload)/layout.tsx", + "src/app/robots.ts", + "src/app/sitemap.ts", + "src/blocks/AccordionBlock.ts", + "src/blocks/AuthorBioBlock.ts", + "src/blocks/BeforeAfterBlock.ts", + "src/blocks/CTABlock.ts", + "src/blocks/CardGridBlock.ts", + "src/blocks/ComparisonBlock.ts", + "src/blocks/ContactFormBlock.ts", + "src/blocks/DividerBlock.ts", + "src/blocks/DownloadsBlock.ts", + "src/blocks/EventsBlock.ts", + "src/blocks/FAQBlock.ts", + "src/blocks/FavoritesBlock.ts", + "src/blocks/FeaturedContentBlock.ts", + "src/blocks/HeroBlock.ts", + "src/blocks/HeroSliderBlock.ts", + "src/blocks/ImageSliderBlock.ts", + "src/blocks/ImageTextBlock.ts", + "src/blocks/JobsBlock.ts", + "src/blocks/LocationsBlock.ts", + "src/blocks/LogoGridBlock.ts", + "src/blocks/MapBlock.ts", + "src/blocks/NewsletterBlock.ts", + "src/blocks/OrgChartBlock.ts", + "src/blocks/PostsListBlock.ts", + "src/blocks/PricingBlock.ts", + "src/blocks/ProcessStepsBlock.ts", + "src/blocks/QuoteBlock.ts", + "src/blocks/RelatedPostsBlock.ts", + "src/blocks/ScriptSectionBlock.ts", + "src/blocks/SeriesBlock.ts", + "src/blocks/SeriesDetailBlock.ts", + "src/blocks/ServicesBlock.ts", + "src/blocks/ShareButtonsBlock.ts", + "src/blocks/StatsBlock.ts", + "src/blocks/TableOfContentsBlock.ts", + "src/blocks/TabsBlock.ts", + "src/blocks/TeamBlock.ts", + "src/blocks/TeamFilterBlock.ts", + "src/blocks/TestimonialsBlock.ts", + "src/blocks/TextBlock.ts", + "src/blocks/TimelineBlock.ts", + "src/blocks/VideoBlock.ts", + "src/blocks/VideoEmbedBlock.ts", + "src/blocks/index.ts", + "src/collections/AuditLogs.ts", + "src/collections/Authors.ts", + "src/collections/Bookings.ts", + "src/collections/Categories.ts", + "src/collections/Certifications.ts", + "src/collections/CommunityInteractions.ts", + "src/collections/CommunityRules.ts", + "src/collections/CommunityTemplates.ts", + "src/collections/ConsentLogs.ts", + "src/collections/CookieConfigurations.ts", + "src/collections/CookieInventory.ts", + "src/collections/Downloads.ts", + "src/collections/EmailLogs.ts", + "src/collections/Events.ts", + "src/collections/FAQs.ts", + "src/collections/Favorites.ts", + "src/collections/FormSubmissionsOverrides.ts", + "src/collections/Jobs.ts", + "src/collections/Locations.ts", + "src/collections/MonitoringAlertHistory.ts", + "src/collections/MonitoringAlertRules.ts", + "src/collections/MonitoringLogs.ts", + "src/collections/MonitoringSnapshots.ts", + "src/collections/Navigations.ts", + "src/collections/NewsletterSubscribers.ts", + "src/collections/Pages.ts", + "src/collections/Partners.ts", + "src/collections/PortfolioCategories.ts", + "src/collections/Portfolios.ts", + "src/collections/Posts.ts", + "src/collections/PrivacyPolicySettings.ts", + "src/collections/ProductCategories.ts", + "src/collections/Products.ts", + "src/collections/Projects.ts", + "src/collections/ReportSchedules.ts", + "src/collections/Series.ts", + "src/collections/ServiceCategories.ts", + "src/collections/Services.ts", + "src/collections/SiteSettings.ts", + "src/collections/SocialAccounts.ts", + "src/collections/SocialLinks.ts", + "src/collections/SocialPlatforms.ts", + "src/collections/Tags.ts", + "src/collections/Team.ts", + "src/collections/Tenants.ts", + "src/collections/Testimonials.ts", + "src/collections/Timelines.ts", + "src/collections/Users.ts", + "src/collections/VideoCategories.ts", + "src/collections/Workflows.ts", + "src/collections/YouTubeChannels.ts", + "src/collections/YouTubeContent.ts", + "src/collections/YtBatches.ts", + "src/collections/YtChecklistTemplates.ts", + "src/collections/YtMonthlyGoals.ts", + "src/collections/YtNotifications.ts", + "src/collections/YtScriptTemplates.ts", + "src/collections/YtSeries.ts", + "src/collections/YtTasks.ts", + "src/components/admin/CommunityNavLinks.tsx", + "src/components/admin/ContentCalendar.tsx", + "src/components/admin/ContentCalendarView.tsx", + "src/components/admin/DashboardNavLink.tsx", + "src/components/admin/EmailDeliverabilityInfo.tsx", + "src/components/admin/MonitoringDashboard.tsx", + "src/components/admin/MonitoringDashboardView.tsx", + "src/components/admin/MonitoringNavLinks.tsx", + "src/components/admin/TenantBreadcrumb.tsx", + "src/components/admin/TenantDashboard.tsx", + "src/components/admin/TenantDashboardView.tsx", + "src/components/admin/TestEmailButton.tsx", + "src/components/admin/YouTubeAnalyticsDashboard.tsx", + "src/components/admin/YouTubeAnalyticsDashboardView.tsx", + "src/components/admin/YouTubeAnalyticsNavLinks.tsx", + "src/globals/SEOSettings.ts", + "src/hooks/auditAuthEvents.ts", + "src/hooks/auditTenantChanges.ts", + "src/hooks/auditUserChanges.ts", + "src/hooks/emailFailureAlertHook.ts", + "src/hooks/formSubmissionHooks.ts", + "src/hooks/invalidateCache.ts", + "src/hooks/invalidateEmailCache.ts", + "src/hooks/processFeaturedVideo.ts", + "src/hooks/sendFormNotification.ts", + "src/hooks/sendNewsletterConfirmation.ts", + "src/hooks/useRealtimeUpdates.ts", + "src/hooks/youtubeChannels/downloadChannelImage.ts", + "src/hooks/youtubeContent/createTasksOnStatusChange.ts", + "src/hooks/youtubeContent/downloadThumbnail.ts", + "src/hooks/ytTasks/notifyOnAssignment.ts", + "src/instrumentation.ts", + "src/jobs/consentRetentionJob.ts", + "src/jobs/scheduler.ts", + "src/jobs/syncAllComments.ts", + "src/lib/alerting/alert-service.ts", + "src/lib/audit/audit-service.ts", + "src/lib/communityAccess.ts", + "src/lib/email/newsletter-service.ts", + "src/lib/email/newsletter-templates.ts", + "src/lib/email/payload-email-adapter.ts", + "src/lib/envValidation.ts", + "src/lib/integrations/claude/ClaudeAnalysisService.ts", + "src/lib/integrations/claude/ClaudeReplyService.ts", + "src/lib/integrations/meta/FacebookClient.ts", + "src/lib/integrations/meta/FacebookSyncService.ts", + "src/lib/integrations/meta/InstagramClient.ts", + "src/lib/integrations/meta/InstagramSyncService.ts", + "src/lib/integrations/meta/MetaBaseClient.ts", + "src/lib/integrations/meta/index.ts", + "src/lib/integrations/youtube/CommentsSyncService.ts", + "src/lib/jobs/JobLogger.ts", + "src/lib/jobs/NotificationService.ts", + "src/lib/jobs/TokenRefreshService.ts", + "src/lib/jobs/UnifiedSyncService.ts", + "src/lib/jobs/syncAllComments.ts", + "src/lib/monitoring/snapshot-collector.ts", + "src/lib/pdf/pdf-service.ts", + "src/lib/queue/index.ts", + "src/lib/queue/jobs/email-job.ts", + "src/lib/queue/jobs/pdf-job.ts", + "src/lib/queue/jobs/retention-job.ts", + "src/lib/queue/jobs/youtube-upload-job.ts", + "src/lib/queue/queue-service.ts", + "src/lib/queue/workers/email-worker.ts", + "src/lib/queue/workers/pdf-worker.ts", + "src/lib/queue/workers/retention-worker.ts", + "src/lib/queue/workers/youtube-upload-worker.ts", + "src/lib/retention/cleanup-service.ts", + "src/lib/retention/index.ts", + "src/lib/retention/retention-config.ts", + "src/lib/services/ReportGeneratorService.ts", + "src/lib/services/RulesEngine.ts", + "src/lib/structuredData.ts", + "src/lib/validation/index.ts", + "src/lib/validation/slug-validation.ts", + "src/lib/youtubeAccess.ts", + "src/middleware.ts", + "src/types/meta.ts" + ], + "notes": [ + "Baseline blocks only NEW untested files.", + "Update baseline intentionally after planned test-debt cleanup." + ] +}