← back to The Ai Factory

src/stages/smoke_test.js

36 lines

// Stage 5 — smoke_test. Cheap structural validation on the final scaffold file.
// Pure-local, deterministic — no LLM, no Claude CLI. Catches the most common
// failure modes (missing frontmatter, name drift, empty body) without burning
// quota or wall-clock.

const fs = require('node:fs/promises');

function escapeRe(s) { return String(s).replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); }

async function runSmokeTest({ outPath, spec }) {
  const content = await fs.readFile(outPath, 'utf8');
  const checks = [
    { name: 'has_frontmatter_open',  pass: /^---\s*\n/.test(content) },
    { name: 'has_frontmatter_close', pass: /\n---\s*\n/.test(content) },
    { name: 'name_matches_spec',     pass: new RegExp(`^name:\\s*${escapeRe(spec.artifact_name)}\\s*$`, 'm').test(content) },
    { name: 'has_description',       pass: /^description:\s*\S/m.test(content) },
    { name: 'body_nonempty',         pass: content.length > 200 },
    { name: 'no_unrendered_template', pass: !/<<<|>>>|TODO_FILL|XXX_REPLACE/.test(content) }
  ];
  if (spec.artifact_type === 'subagent') {
    checks.push({ name: 'has_tools',  pass: /^tools:\s*\S/m.test(content) });
    checks.push({ name: 'has_model',  pass: /^model:\s*(sonnet|opus|haiku)\s*$/m.test(content) });
  }

  const passed = checks.filter(c => c.pass).length;
  return {
    checks,
    passed,
    total: checks.length,
    passing: passed === checks.length,
    failing_checks: checks.filter(c => !c.pass).map(c => c.name)
  };
}

module.exports = { runSmokeTest };