real-pm-smoke.js
4.72 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
#!/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
// 加载根 .env (含 CLAUDE_CODE_OAUTH_TOKEN); dotenv 装在 server/node_modules
const path = require('path');
const dotenv = require(require.resolve('dotenv', { paths: [path.resolve(__dirname, '../server')] }));
dotenv.config({ path: path.join(__dirname, '../.env') });
const fs = require('fs');
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/agents/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);
});