/ tests / pipeline / pipeline-service-stages.test.js
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  });