real-pm-smoke.js 4.49 KB
#!/usr/bin/env node
/**
 * scripts/real-pm-smoke.js
 *
 * 最小验证: 真 claude CLI 跑一次 PM Agent,看 ClaudeCliProtocol +
 * --append-system-prompt + 工作树 mtime diff 这条 real 链路是否
 * 端到端能用。预算 $0.50 cap。
 *
 * 期望:
 *   ① claude 进程 spawn 成功
 *   ② Agent 输出非空 (result_text 有内容)
 *   ③ files_written 至少 ≥ 1 (Agent 调用 Write 工具写了文件)
 *   ④ git 有新 commit
 *
 * NOTE: 真 LLM 输出不可控;PM 可能不完全照 prompt.md 跑全套 11 章节。
 *      只要它写了任意 prd/* 文件就算 wire-up 通。
 */

delete process.env.LEMONBOSS_CLAUDE_MODE; // 显式关闭 mock

const fs = require('fs');
const path = require('path');

const lc = require('../server/src/core/devloop/loop-controller');
const pr = require('../server/services/devloop/path-resolver');

// --max-budget-usd 仅在 --print 模式可用,与我们的 stream-json 长连接互斥
// → 用进程超时 (5min) + 一次性单 agent 调用兜底
const HARD_TIMEOUT_MS = 5 * 60 * 1000;
const cleanup = [];

async function main() {
  console.log('\n┌─ Real Claude CLI · PM Smoke ────────────────────');
  console.log(`│ Hard timeout: 5min (no maxBudgetUsd — only --print mode)`);
  console.log(`│ Model: claude default`);
  console.log('└──────────────────────────────────────────────────\n');

  const cid = 'c-real-pm-' + Date.now().toString(36);
  cleanup.push(cid);
  const proj = await lc.createProject({
    company_id: cid,
    mode: 'sandbox',
    idea: 'real PM smoke: minimal TODO API + Vue UI',
  });
  console.log(`pid = ${proj.pid}`);
  console.log(`root = ${pr.resolveProjectRoot(proj.pid)}\n`);

  const userPrompt = [
    'Mode: init',
    'Requirement: A minimal TODO list — POST /todos (add) + GET /todos (list).',
    '',
    '约束 (please obey for this smoke test):',
    '  • cwd 已经是项目根,直接相对路径写文件',
    '  • 至少写 prd/index.html 一个最小 HTML stub (含 <article data-doc="prd" data-rev="1"><section data-rev="1"><h3>...</h3><p>...</p></section></article>)',
    '  • 不要尝试调 hash_contract / bootstrap_prd_site 等 LemonBoss 内部工具 — 它们在本 session 不可用',
    '  • 不要执行 npm install / git / curl 等 Bash 命令',
    '  • 完成后直接 stop, 不需要总结',
    '',
    '回 "DONE" 一词即可结束。',
  ].join('\n');

  console.log('▸ calling devloop-pm via real claude CLI...');
  console.log(`  (system prompt: system/templates/devloop-pm/prompt.md, ~3KB)\n`);

  const startMs = Date.now();
  let chunkCount = 0;
  const r = await Promise.race([
    lc.callAgent(proj.pid, 'devloop-pm', userPrompt, {
      mock: false,
      onChunk: () => { chunkCount++; },
    }),
    new Promise((_, rej) =>
      setTimeout(() => rej(new Error('hard timeout 5min reached')), HARD_TIMEOUT_MS),
    ),
  ]);
  const elapsedSec = Math.round((Date.now() - startMs) / 1000);

  console.log('\n▸ result:');
  console.log(`  elapsed:       ${elapsedSec}s`);
  console.log(`  stream chunks: ${chunkCount}`);
  console.log(`  result text:   ${r.result_text.slice(0, 120).replace(/\n/g, ' ')}${r.result_text.length > 120 ? '…' : ''}`);
  console.log(`  files_written: ${r.files_written.length}`);
  for (const f of r.files_written.slice(0, 10)) console.log(`     · ${f}`);
  if (r.files_written.length > 10) console.log(`     · (+${r.files_written.length - 10} more)`);
  console.log(`  commit:        ${r.commit?.sha?.slice(0, 7) || '(none)'}`);

  console.log('\n┌─ Wire-up status ────────────────────────────────');
  const checks = [
    ['process spawn',  true],
    ['stream chunks',  chunkCount > 0],
    ['result text',    r.result_text.length > 0],
    ['files written',  r.files_written.length > 0],
    ['git commit',     !!r.commit?.sha],
  ];
  let pass = 0;
  for (const [k, ok] of checks) {
    console.log(`│ ${ok ? '✓' : '✗'} ${k}`);
    if (ok) pass++;
  }
  console.log(`└─ ${pass}/${checks.length} checks passed\n`);

  console.log('Artifacts retained for inspection:');
  console.log(`  ${pr.resolveProjectRoot(proj.pid)}`);
  console.log(`  cleanup: rm -rf data/companies/${cid}\n`);

  process.exit(pass === checks.length ? 0 : 1);
}

main().catch(e => {
  console.error('\n[real-pm-smoke] FATAL:', e?.message || e);
  console.error(e?.stack);
  process.exit(1);
});