/** * routes/devloop.js 集成测试 * * 不启动完整 server,只挂 devloop router 到独立 express app + fetch。 */ process.env.LEMONBOSS_CLAUDE_MODE = 'mock'; const fs = require('fs'); const path = require('path'); const os = require('os'); const express = require('express'); const TEST_HOME = fs.mkdtempSync(path.join(os.tmpdir(), 'devloop-rt-')); process.env.HOME = TEST_HOME; const pathResolver = require('../../services/devloop/path-resolver'); const devloopRouter = require('../../routes/devloop'); function startApp() { const app = express(); app.use(express.json()); app.use('/api/devloop', devloopRouter); return new Promise((resolve) => { const srv = app.listen(0, '127.0.0.1', () => { const port = srv.address().port; resolve({ srv, base: `http://127.0.0.1:${port}` }); }); }); } describe('routes/devloop', () => { let app, base; const created = []; beforeAll(async () => { pathResolver._resetForTesting(); ({ srv: app, base } = await startApp()); }); afterAll(() => { app.close(); for (const c of created) { try { fs.rmSync(path.join(pathResolver.DATA_DIR, 'companies', c), { recursive: true, force: true }); } catch { /* ignore */ } } }); it('POST /api/devloop/projects requires company_id', async () => { const r = await fetch(`${base}/api/devloop/projects`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ requirement: 'x' }), }); expect(r.status).toBe(400); const j = await r.json(); expect(j.error).toMatch(/company_id/); }); it('POST /api/devloop/projects requires requirement', async () => { const r = await fetch(`${base}/api/devloop/projects`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ company_id: 'c-x' }), }); expect(r.status).toBe(400); const j = await r.json(); expect(j.error).toMatch(/requirement/); }); it('POST /api/devloop/projects → creates project and runs PM + Architect', async () => { const cid = 'c-route-test-' + Math.random().toString(36).slice(2, 7); created.push(cid); const r = await fetch(`${base}/api/devloop/projects`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ company_id: cid, name: 'todo-demo', requirement: '做一个 TODO 列表:能新增、列表、Vue 前端', }), }); expect(r.status).toBe(200); const j = await r.json(); expect(j.ok).toBe(true); expect(j.pid).toMatch(/^p-/); expect(j.pm.files_written).toBeGreaterThanOrEqual(10); expect(j.architect.files_written).toBeGreaterThanOrEqual(10); expect(j.pm.commit).toBeTruthy(); expect(j.architect.commit).toBeTruthy(); // 验证文件确实落盘 const root = pathResolver.resolveProjectRoot(j.pid); expect(fs.existsSync(path.join(root, 'prd', 'index.html'))).toBe(true); expect(fs.existsSync(path.join(root, 'sdd', 'mvcp', 'model.html'))).toBe(true); expect(fs.existsSync(path.join(root, 'units.json'))).toBe(true); // GET /state const sR = await fetch(`${base}/api/devloop/projects/${j.pid}/state`); expect(sR.status).toBe(200); const s = await sR.json(); expect(s.pid).toBe(j.pid); expect(s.prd_site).toBe(true); expect(s.sdd_site).toBe(true); expect(s.units).toHaveLength(3); }); it('GET /api/devloop/projects/:pid/state on unknown pid returns 404', async () => { const r = await fetch(`${base}/api/devloop/projects/p-doesnotexist/state`); expect(r.status).toBe(404); const j = await r.json(); expect(j.error).toMatch(/unknown/); }); it('GET /api/devloop/projects lists projects', async () => { const r = await fetch(`${base}/api/devloop/projects`); expect(r.status).toBe(200); const j = await r.json(); expect(Array.isArray(j.projects)).toBe(true); }); });