rebuild source discovery workflows
CI / compose (push) Failing after 5m46s

This commit is contained in:
Marco0300
2026-09-04 20:52:37 +02:00
parent 3a9b553440
commit 6aefbdc1f3
18 changed files with 853 additions and 106 deletions
+12 -9
View File
@@ -57,7 +57,7 @@ const expectedIds = [
'loginScreen', 'loginForm', 'loginEmail', 'loginPassword', 'dashboardShell', 'logoutBtn', 'apiStatus',
'explorer', 'detailPanel', 'userGreetingName', 'reviewQueueCount', 'reviewQueueState', 'savedFilterForm', 'savedFilterSelect',
'deleteSavedFilterBtn', 'bulkVerifyBtn', 'bulkRejectBtn', 'nextPageBtn', 'jobs', 'jobsList', 'jobDetailPanel',
'sources', 'sourcesList', 'sourceRecordsList', 'discoveryForm', 'discoveryWorkspace', 'directDiscoveryForm', 'directDiscoveryRunsState', 'directDiscoveryResultState', 'directDiscoverySources', 'directDiscoveryDailyLimit', 'directDiscoverySchedule', 'directDiscoveryDryRun', 'sourceFilter', 'geographyFilter', 'categoryFilter', 'contactStatusFilter', 'crmPipeline', 'pipelineBoard', 'crmActivity',
'sources', 'sourcesList', 'sourceRecordsList', 'discoveryWorkspace', 'directDiscoveryForm', 'directDiscoveryRunsState', 'directDiscoveryResultState', 'directDiscoverySources', 'directDiscoveryDailyLimit', 'directDiscoveryDryRun', 'sourceFilter', 'geographyFilter', 'categoryFilter', 'contactStatusFilter', 'crmPipeline', 'pipelineBoard', 'crmActivity',
'interactionState', 'interactionForm', 'crmReports', 'pipelineReport', 'outcomesReport', 'activityReport',
'suppressionCenter', 'suppressionForm', 'suppressionState', 'outreachSettings', 'providerPolicyPanel', 'aiProviderSettings', 'aiProviderForm', 'aiProviderStatus', 'aiProviderRefreshBtn', 'testAiProviderBtn', 'saveAiProviderBtn',
'scoreRules', 'scoreRulesPanel', 'scoreDistributionPanel', 'mergeDialog', 'confirmMergeBtn'
@@ -73,7 +73,7 @@ check('dom.dynamic-markers', 'detail safety panels define dynamic smoke markers'
const routeContracts = [
'/api/v1/auth/me', '/api/v1/auth/login', '/api/v1/auth/logout', '/api/v1/dashboard/summary', '/api/v1/businesses',
'/api/v1/review-queue', '/api/v1/saved-filters', '/api/v1/businesses/bulk-review', '/api/v1/jobs', '/api/v1/sources',
'/api/v1/source-records', '/api/v1/discovery', '/api/v1/discovery-runs', '/api/v1/discovery-queries', '/api/v1/merge-history', '/api/v1/scoring/summary', '/api/v1/score-rules',
'/api/v1/source-records', '/api/v1/discovery', '/api/v1/discovery-runs', '/api/v1/merge-history', '/api/v1/scoring/summary', '/api/v1/score-rules',
'/api/v1/pipeline-entries', '/api/v1/interactions', '/api/v1/reports/pipeline', '/api/v1/reports/outcomes', '/api/v1/reports/activity',
'/api/v1/suppressions', '/api/v1/ai-runs', '/api/v1/outreach/drafts', '/api/v1/outreach/provider-config', '/api/v1/ai/provider-config', '/api/v1/admin/ai-provider-config/test',
'/matches', '/domains/check', '/domain-candidates', '/websites/scan', '/contacts/extract', '/score/recalculate', '/pipeline', '/verify'
@@ -81,13 +81,16 @@ const routeContracts = [
check('routes.contracts', 'all critical API route contracts are referenced by the client', all(routeContracts, route => js.includes(route)), listMissing(routeContracts, route => js.includes(route)).join(', '));
check('routes.authenticated', 'protected API requests include cookie credentials', js.includes("credentials:'include'") && js.includes('jsonRequest'));
check('auth.display-name-greeting', 'authenticated display name drives the greeting and identity', js.includes('currentUser.display_name') && js.includes("$('userGreetingName').textContent") && !html.includes('Good morning, Alex'));
check('sources.registry-status', 'source registry exposes governed configuration and operational status fields', all(['source_code', 'display_name', 'configured', 'available', 'enabled', 'API credential', 'Terms', 'Owner', 'Rate limit', 'Daily quota', 'Last health', 'Success / error', 'Circuit', 'data-source-action="review"', 'data-source-action="test"'], token => `${html}\n${js}\n${css}`.includes(token)));
check('sources.optional-gated', 'optional adapters are rendered unavailable until configured', all(['/api/v1/sources/adapters', 'configuration-gated', 'Optional adapter', 'source.optional', 'available:Boolean(adapter.available)'], token => `${html}\n${js}\n${css}`.includes(token)));
check('sources.registered-actions', 'registered sources keep Review/Test actions actionable while previews provide a working setup path', js.includes("data-source-action=\"${source.id?'toggle':'setup'}\"") && js.includes('Set up source') && js.includes('registerAdapterPreview') && js.includes("if (!source || !source.id)"));
check('sources.toggle-guard', 'source toggle handler ignores unknown rows before reading source fields', js.includes("const source = findRegisteredSource(actionButton.dataset.sourceId);\n if (!source) return;\n const needsPromptConfig"));
check('sources.setup-affordances', 'manual and CSV setup affordances remain explicit and disabled by default', all(['Manual records', 'CSV import', 'CSV content is required', 'enabled:false', 'remains disabled'], token => `${html}\n${js}\n${css}`.includes(token)));
check('discovery.operator-controls', 'discovery builder and run controls are represented', all(['data-run-action="pause"', 'data-run-action="resume"', 'data-run-action="cancel"', 'live-log', 'source-health', 'daily_limit', 'source_ids', 'schedule'], token => `${html}\n${js}\n${css}`.includes(token)));
check('sources.registry-only', 'sources contain governed registry configuration, not discovery criteria or prompt/query controls', all(['sourceOwner', 'sourceTermsUrl', 'sourceTermsStatus', 'sourceRateLimit', 'sourceDailyQuota', 'sourceProviderSettings', 'sourceSaveBtn', 'sourceTestBtn', 'sourceEnableBtn', 'sourceStatus'], id => htmlIds.has(id)) && !html.includes('id="discoveryForm"') && !html.includes('name="query"') && !js.includes('function runDiscovery('));
check('sources.registry-payload', 'source save sends only registry/configuration fields and explicitly starts disabled', all(['terms_status:fields.terms_status', 'daily_quota:Number(fields.daily_quota)', 'provider_settings:fields.provider_settings', 'enabled:false'], token => js.includes(token)) && !/criteria\s*:|query\s*:|prompt\s*:/i.test(js.match(/async function saveSource[\s\S]*?(?=\n function findRegisteredSource)/)?.[0] || ''));
check('sources.lifecycle-states', 'source save, test, and enable controls expose loading, success, error, and disabled explanations', all(['Saving source configuration…', 'Source configuration saved.', 'Testing source', 'Enable is unavailable until terms, owner, limits, and a successful test are recorded.', 'Unable to save source configuration.'], token => `${html}\n${js}`.includes(token)));
check('sources.optional-gated', 'source registry reads optional adapter readiness without treating it as an enabled source', all(['/api/v1/sources/adapters', 'optional:true', 'available:Boolean'], token => `${html}\n${js}`.includes(token)));
check('discovery.canonical-builder', 'discovery exposes canonical criteria, source selection, bounded limits, website priorities, and dry-run controls', all(['directDiscoveryCategory', 'directDiscoveryKeywords', 'directDiscoveryCity', 'directDiscoveryProvince', 'directDiscoveryCountry', 'directDiscoveryLanguage', 'directDiscoverySources', 'directDiscoveryMaxResults', 'directDiscoveryMaxPages', 'directDiscoveryDailyLimit', 'directDiscoveryDryRun', 'directDiscoveryPriorities', 'directDiscoveryIncludeWebsites', 'directDiscoveryExcludeWebsites'], id => htmlIds.has(id)));
check('discovery.payload-contract', 'discovery submits canonical criteria and does not send source configuration fields', all(['category:fields.category.trim()', 'language:fields.language.trim()', 'website_analysis:', 'include_websites:', 'exclude_websites:', 'max_results:Number(fields.max_results)', 'source_ids:selectedSourceIds()'], token => js.includes(token)) && !/terms_status|provider_settings|owner/.test(js.match(/async function submitDirectDiscovery[\s\S]*?(?=\n\n function parseCsv)/)?.[0] || ''));
check('discovery.run-controls', 'run history, events, provenance, retry, and only route-backed cancel controls are represented', all(['data-run-action="cancel"', 'data-run-action="retry"', 'live-log', 'source-health', 'provenance', '/api/v1/discovery-runs/${encodeURIComponent(run.id)}/cancel', '/api/v1/jobs/${encodeURIComponent(run.job_id)}/retry'], token => `${html}\n${js}\n${css}`.includes(token)) && !`${html}\n${js}`.includes('data-run-action="pause"') && !`${html}\n${js}`.includes('data-run-action="resume"'));
check('discovery.control-states', 'discovery controls communicate loading, success, error, and disabled explanations', all(['Starting bounded discovery job…', 'Discovery accepted. Tracking job status and partial results below.', 'Select at least one enabled source before starting a discovery run.', 'No enabled sources are available. Configure, test, and enable a source first.', 'Unable to load discovery runs'], token => `${html}\n${js}`.includes(token)));
check('prospects.filter-contract', 'prospect explorer exposes source, geography, category, and contact filters', all(['sourceFilter', 'geographyFilter', 'categoryFilter', 'contactStatusFilter', 'contact_status'], token => `${html}\n${js}\n${css}`.includes(token)));
check('ui.no-demo-labels', 'static frontend contains no demo-labelled job or failure controls', !/\bdemo\b|DEMO_FAILURE/i.test(`${html}\n${js}`));
check('routes.no-arbitrary-target-fetch', 'browser does not fetch arbitrary target URLs directly', !/fetch\s*\(\s*(?:url|target|website|href)\b/i.test(js));
const canonicalStages = ['new', 'contacted', 'qualified', 'proposal', 'negotiation', 'won', 'lost'];
@@ -125,7 +128,7 @@ const approvalTokens = ['window.confirm', 'human_approval:true', 'send:false', '
check('safety.approval-gated', 'approval actions require explicit human confirmation and remain non-delivering', all(approvalTokens, token => js.includes(token)), listMissing(approvalTokens, token => js.includes(token)).join(', '));
const suppressionTokens = ['statusOf', "st==='suppressed'", "renderAiState('suppressed')", "renderOutreachState('suppressed')", 'suppression', 'Do not contact'];
check('safety.suppression-precedence', 'suppression disables AI/outreach and preserves do-not-contact state', all(suppressionTokens, token => combined.includes(token)), listMissing(suppressionTokens, token => combined.includes(token)).join(', '));
check('safety.source-disabled', 'discovery remains disabled until an approved source is enabled', all(['Discovery is disabled by default.', 'enabled:false', 'This source is disabled. Enable it only after review.'], token => combined.includes(token)));
check('safety.source-disabled', 'discovery remains disabled until an approved source is enabled', all(['enabled:false', 'No enabled source. Configure, test, and enable a source before discovery.', 'No enabled sources are available. Configure, test, and enable a source first.'], token => combined.includes(token)));
check('safety.no-external-network', 'client has no direct provider or target network origins', !/(?:fetch|XMLHttpRequest|WebSocket)\s*\(\s*[`'\"]https?:\/\//i.test(js));
const secretPatterns = [
/-----BEGIN(?: RSA| EC| OPENSSH)? PRIVATE KEY-----/i,