|
| 1 | +/** |
| 2 | + * Sentinel Node Tester — Failure error_code classification regression tests |
| 3 | + * |
| 4 | + * Root cause this guards (2026-06): buildFailResult() set `error` (the message) |
| 5 | + * but never set `errorCode`, so batch_results.error_code landed NULL and |
| 6 | + * error_logs.error_code fell back to 'UNKNOWN' for every real failure — the |
| 7 | + * per-row failure-copy block (a product MUST) showed "Error code: UNKNOWN". |
| 8 | + * Real prod failures looked like: |
| 9 | + * connect EHOSTUNREACH 198.51.100.1:21045 -> must map to HOST_UNREACH |
| 10 | + * Node test timed out -> must map to TIMEOUT |
| 11 | + * … TKD handshake HTTP 500: {…} -> must map to HTTP_ERROR |
| 12 | + * |
| 13 | + * These tests extract the pure classifyProbeError() from pipeline.js by source |
| 14 | + * (no native imports, no DB, no server) and assert the mappings, plus a static |
| 15 | + * check that buildFailResult wires classifyProbeError into errorCode. |
| 16 | + * |
| 17 | + * Run: node test/error-code-classification.test.js |
| 18 | + */ |
| 19 | + |
| 20 | +import { readFileSync } from 'fs'; |
| 21 | +import { fileURLToPath } from 'url'; |
| 22 | +import path from 'path'; |
| 23 | + |
| 24 | +const __dirname = path.dirname(fileURLToPath(import.meta.url)); |
| 25 | +const ROOT = path.join(__dirname, '..'); |
| 26 | +const src = readFileSync(path.join(ROOT, 'audit/pipeline.js'), 'utf8'); |
| 27 | + |
| 28 | +const results = { pass: 0, fail: 0 }; |
| 29 | +function ok(cond, name) { |
| 30 | + if (cond) { results.pass++; console.log(` PASS ${name}`); } |
| 31 | + else { results.fail++; console.error(` FAIL ${name}`); } |
| 32 | +} |
| 33 | + |
| 34 | +// ─── Extract the pure classifyProbeError(err) function from source ─────────── |
| 35 | +// It references only locals (err/msg/code) so it can be evaluated standalone. |
| 36 | +function extractFn(name) { |
| 37 | + const start = src.indexOf(`function ${name}(`); |
| 38 | + if (start === -1) throw new Error(`could not find function ${name}`); |
| 39 | + // Find the body's opening brace by first balancing the parameter-list parens, |
| 40 | + // so a default param like `diag = {}` in the signature is NOT mistaken for the |
| 41 | + // function body's `{` (that bug silently truncated buildFailResult). |
| 42 | + const parenOpen = src.indexOf('(', start); |
| 43 | + let pd = 0, bodyOpen = -1; |
| 44 | + for (let i = parenOpen; i < src.length; i++) { |
| 45 | + if (src[i] === '(') pd++; |
| 46 | + else if (src[i] === ')') { pd--; if (pd === 0) { bodyOpen = src.indexOf('{', i); break; } } |
| 47 | + } |
| 48 | + if (bodyOpen === -1) throw new Error(`could not find body open for ${name}`); |
| 49 | + let depth = 0; |
| 50 | + for (let i = bodyOpen; i < src.length; i++) { |
| 51 | + const c = src[i]; |
| 52 | + if (c === '{') depth++; |
| 53 | + else if (c === '}') { |
| 54 | + depth--; |
| 55 | + if (depth === 0) { |
| 56 | + const body = src.slice(start, i + 1); |
| 57 | + if (!body.startsWith(`function ${name}(`)) throw new Error('mis-extracted ' + name); |
| 58 | + return body; |
| 59 | + } |
| 60 | + } |
| 61 | + } |
| 62 | + throw new Error(`unbalanced braces extracting ${name}`); |
| 63 | +} |
| 64 | + |
| 65 | +const classifyBody = extractFn('classifyProbeError'); |
| 66 | +const classifyProbeError = new Function(`${classifyBody}; return classifyProbeError;`)(); |
| 67 | + |
| 68 | +console.log('Failure error_code classification — regression tests\n'); |
| 69 | + |
| 70 | +// ─── 1. The three real prod failure shapes map to stable codes ─────────────── |
| 71 | +console.log('1. observed prod failure messages map to real codes'); |
| 72 | +ok(classifyProbeError({ message: 'connect EHOSTUNREACH 198.51.100.1:21045' }) === 'HOST_UNREACH', |
| 73 | + "'connect EHOSTUNREACH …' -> HOST_UNREACH"); |
| 74 | +ok(classifyProbeError({ message: 'Node test timed out' }) === 'TIMEOUT', |
| 75 | + "'Node test timed out' -> TIMEOUT (the 'timed out' wording fix)"); |
| 76 | +ok(classifyProbeError({ message: '409 persistent even after fresh session: TKD handshake HTTP 500: {"suc' }) === 'HTTP_ERROR', |
| 77 | + "'… HTTP 500 …' -> HTTP_ERROR"); |
| 78 | + |
| 79 | +// ─── 2. Code-based + other branches still classify ─────────────────────────── |
| 80 | +console.log('\n2. code-based branches + non-null fallback'); |
| 81 | +ok(classifyProbeError({ message: 'whatever', code: 'ETIMEDOUT' }) === 'TIMEOUT', "code ETIMEDOUT -> TIMEOUT"); |
| 82 | +ok(classifyProbeError({ message: 'connect ECONNREFUSED 1.2.3.4:5' }) === 'TCP_REFUSED', "ECONNREFUSED -> TCP_REFUSED"); |
| 83 | +ok(classifyProbeError({ message: 'getaddrinfo ENOTFOUND host' }) === 'DNS_FAIL', "ENOTFOUND -> DNS_FAIL"); |
| 84 | +ok(classifyProbeError({ message: 'some unrecognized failure' }) === 'OTHER', "unknown -> OTHER (never null)"); |
| 85 | +ok(typeof classifyProbeError({ message: '' }) === 'string' && classifyProbeError({ message: '' }).length > 0, |
| 86 | + "empty message still yields a non-empty code"); |
| 87 | + |
| 88 | +// ─── 3. buildFailResult wires classifyProbeError into errorCode ────────────── |
| 89 | +console.log('\n3. buildFailResult sets errorCode (static wiring check)'); |
| 90 | +// Extract the exact function bodies (robust to layout/length changes) rather |
| 91 | +// than slicing fixed char windows — see code-quality review of 9fedfbe. |
| 92 | +const bfrBody = extractFn('buildFailResult'); |
| 93 | +ok(/errorCode:\s*classifyProbeError\(/.test(bfrBody), |
| 94 | + 'buildFailResult assigns errorCode: classifyProbeError(...)'); |
| 95 | +ok(/timed out/.test(classifyBody), |
| 96 | + "classifyProbeError timeout branch also matches 'timed out'"); |
| 97 | + |
| 98 | +// ─── Summary ───────────────────────────────────────────────────────────────── |
| 99 | +console.log(`\n============================================================`); |
| 100 | +console.log(`RESULTS: ${results.pass} passed, ${results.fail} failed (${results.pass + results.fail} total)`); |
| 101 | +console.log(`============================================================`); |
| 102 | +process.exit(results.fail === 0 ? 0 : 1); |
0 commit comments