test: implement project-wide dynamic test growth guard

This commit is contained in:
Martin Porwoll 2026-02-15 22:54:45 +00:00
parent 153e4a2d5f
commit b06d285922
8 changed files with 624 additions and 1 deletions

View file

@ -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

1
.gitignore vendored
View file

@ -50,3 +50,4 @@ node_modules/
/playwright/.cache/
*.sql
.playwright-mcp/
docs/reports/test-system-status.md

View file

@ -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)

View file

@ -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
```

49
docs/TEST_SYSTEM.md Normal file
View file

@ -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`.

View file

@ -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",

View file

@ -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()

View file

@ -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."
]
}