pipeline-service-stages.test.js
1 /** 2 * Tests for pipeline-service.js stage configuration 3 * 4 * Validates that scoring/rescoring stages have been removed from the pipeline 5 * and moved to the orchestrator (score_sites/score_semantic batches via Claude Max). 6 * 7 * Uses Node.js 22+ built-in test runner (same pattern as other tests in this project). 8 */ 9 10 import { test, describe, mock } from 'node:test'; 11 import assert from 'node:assert'; 12 import { readFileSync } from 'node:fs'; 13 import { resolve, dirname } from 'node:path'; 14 import { fileURLToPath } from 'node:url'; 15 16 const __dirname = dirname(fileURLToPath(import.meta.url)); 17 const pipelineServicePath = resolve(__dirname, '../../src/pipeline-service.js'); 18 19 // Read the source file directly to validate the static configuration. 20 // We don't import the module because it calls main() on load (connects to DB, starts loops). 21 const source = readFileSync(pipelineServicePath, 'utf8'); 22 23 describe('pipeline-service.js — stage configuration', () => { 24 describe('API_STAGES', () => { 25 test('API_STAGES only contains SERPs', () => { 26 // Extract the API_STAGES array definition 27 const apiStagesMatch = source.match(/const API_STAGES\s*=\s*\[([\s\S]*?)\];/); 28 assert.ok(apiStagesMatch, 'API_STAGES should be defined in pipeline-service.js'); 29 30 const apiStagesBlock = apiStagesMatch[1]; 31 32 // Should contain SERPs 33 assert.ok( 34 apiStagesBlock.includes("'SERPs'") || apiStagesBlock.includes('"SERPs"'), 35 'API_STAGES should contain SERPs' 36 ); 37 38 // Should NOT contain Scoring or Rescoring 39 assert.ok( 40 !apiStagesBlock.includes("'Scoring'") && !apiStagesBlock.includes('"Scoring"'), 41 'API_STAGES should not contain Scoring (moved to orchestrator)' 42 ); 43 assert.ok( 44 !apiStagesBlock.includes("'Rescoring'") && !apiStagesBlock.includes('"Rescoring"'), 45 'API_STAGES should not contain Rescoring (moved to orchestrator)' 46 ); 47 }); 48 49 test('API_STAGES has SERPs and 2Step entries only (no Scoring/Rescoring)', () => { 50 const apiStagesMatch = source.match(/const API_STAGES\s*=\s*\[([\s\S]*?)\];/); 51 const apiStagesBlock = apiStagesMatch[1]; 52 // All entries should be SERPs or 2Step — no Scoring/Rescoring 53 const nameMatches = [...apiStagesBlock.matchAll(/name:\s*['"]([^'"]+)['"]/g)].map(m => m[1]); 54 assert.ok(nameMatches.length >= 1, 'API_STAGES should have at least one entry'); 55 for (const name of nameMatches) { 56 assert.ok( 57 name === 'SERPs' || name.startsWith('2Step-'), 58 `API_STAGES entry '${name}' should be SERPs or 2Step-*` 59 ); 60 } 61 }); 62 }); 63 64 describe('STAGE_BATCH_WEIGHTS', () => { 65 test('does not contain scoring or rescoring', () => { 66 const weightsMatch = source.match(/const STAGE_BATCH_WEIGHTS\s*=\s*\{([\s\S]*?)\};/); 67 assert.ok(weightsMatch, 'STAGE_BATCH_WEIGHTS should be defined'); 68 69 const weightsBlock = weightsMatch[1]; 70 assert.ok( 71 !weightsBlock.includes('scoring'), 72 'STAGE_BATCH_WEIGHTS should not contain scoring' 73 ); 74 assert.ok( 75 !weightsBlock.includes('rescoring'), 76 'STAGE_BATCH_WEIGHTS should not contain rescoring' 77 ); 78 }); 79 80 test('contains expected pipeline stages', () => { 81 const weightsMatch = source.match(/const STAGE_BATCH_WEIGHTS\s*=\s*\{([\s\S]*?)\};/); 82 const weightsBlock = weightsMatch[1]; 83 84 for (const stage of ['outreach', 'replies', 'enrich', 'assets', 'serps']) { 85 assert.ok(weightsBlock.includes(stage), `STAGE_BATCH_WEIGHTS should contain '${stage}'`); 86 } 87 }); 88 }); 89 90 describe('STAGE_BACKLOG_SQL', () => { 91 test('does not contain scoring or rescoring', () => { 92 const sqlMatch = source.match(/const STAGE_BACKLOG_SQL\s*=\s*\{([\s\S]*?)\};/); 93 assert.ok(sqlMatch, 'STAGE_BACKLOG_SQL should be defined'); 94 95 const sqlBlock = sqlMatch[1]; 96 assert.ok( 97 !sqlBlock.match(/^\s*scoring\s*:/m), 98 'STAGE_BACKLOG_SQL should not contain scoring key' 99 ); 100 assert.ok( 101 !sqlBlock.match(/^\s*rescoring\s*:/m), 102 'STAGE_BACKLOG_SQL should not contain rescoring key' 103 ); 104 }); 105 106 test('contains expected pipeline stages', () => { 107 const sqlMatch = source.match(/const STAGE_BACKLOG_SQL\s*=\s*\{([\s\S]*?)\};/); 108 const sqlBlock = sqlMatch[1]; 109 110 for (const stage of ['serps', 'assets', 'enrich', 'outreach', 'replies']) { 111 assert.ok(sqlBlock.includes(stage), `STAGE_BACKLOG_SQL should contain '${stage}'`); 112 } 113 }); 114 }); 115 116 describe('STAGE_OUTPUT_STATUS', () => { 117 test('does not contain scoring or rescoring', () => { 118 const outputMatch = source.match(/const STAGE_OUTPUT_STATUS\s*=\s*\{([\s\S]*?)\};/); 119 assert.ok(outputMatch, 'STAGE_OUTPUT_STATUS should be defined'); 120 121 const outputBlock = outputMatch[1]; 122 assert.ok( 123 !outputBlock.match(/^\s*scoring\s*:/m), 124 'STAGE_OUTPUT_STATUS should not contain scoring key' 125 ); 126 assert.ok( 127 !outputBlock.match(/^\s*rescoring\s*:/m), 128 'STAGE_OUTPUT_STATUS should not contain rescoring key' 129 ); 130 }); 131 132 test('contains expected pipeline stages', () => { 133 const outputMatch = source.match(/const STAGE_OUTPUT_STATUS\s*=\s*\{([\s\S]*?)\};/); 134 const outputBlock = outputMatch[1]; 135 136 for (const stage of ['serps', 'assets', 'enrich']) { 137 assert.ok(outputBlock.includes(stage), `STAGE_OUTPUT_STATUS should contain '${stage}'`); 138 } 139 }); 140 }); 141 142 describe('import statements', () => { 143 test('scoring imports are commented out', () => { 144 // Ensure there are no active imports of scoring/rescoring modules 145 const lines = source.split('\n'); 146 const activeImports = lines.filter( 147 l => l.match(/^\s*import\s/) && (l.includes('score') || l.includes('rescore')) 148 ); 149 assert.strictEqual( 150 activeImports.length, 151 0, 152 `No active scoring/rescoring imports should exist. Found: ${activeImports.join(', ')}` 153 ); 154 }); 155 }); 156 157 describe('API loop log message', () => { 158 test('log message indicates SERPs only', () => { 159 assert.ok( 160 source.includes('SERPs only') || 161 source.includes('Scoring/Rescoring handled by orchestrator'), 162 'API loop should log that it runs SERPs only' 163 ); 164 }); 165 }); 166 });