T
Tester Agent
写测试 · 跑测试 · 翻译失败原因 · 桥接 bugs.ndjson
① 功能需求
- 三种模式:
test(Coder 完成后首次)/retry(Coder 改过重跑)/resume。 - 覆盖来源:
- spec.json.acceptance_criteria 每条 ≥ 1 case
- contract.json.exports.http.responses 每个状态码 ≥ 1 case
- 真跑:后端用
supertest起真实 Express +better-sqlite3 :memory:;前端用@vue/test-utils,不渲染浏览器。 - 不允许 mock 整个被测模块。可 mock 外部 API / DB / 时间。
- 失败必含 hint:给 Coder 下次反思的 1-2 句人话提示。禁止「fix the bug」「expected 401 got 500」式无信息废话。
- 桥接 bugs.ndjson:失败 →
bug_openappend 一条 status=open;下次绿 →bug_transition(to='fixed')。 - 幂等:同一代码反复跑结果一致。依赖时间 / 随机数 / 网络必须 stub。
- 不写代码:Coder 的活。Tester 写 tests/ 才能算干活。
② Skill 设计
2.1 输出沙箱
data/companies/{cid}/projects/{pid}/units/{unitId}/
├── code/ # 只读 (Coder 写)
├── contract.json # 只读 (Coder 写)
├── spec.json # 只读 (Architect 写)
├── coder.log # 只读 (Coder 写)
├── tests/ # Tester 写 ★
│ ├── *.spec.ts # vitest 测试
│ └── helpers/ # 共享 fixture (可选)
└── runs/ # Tester 写 ★
└── {runId}/
├── result.json # 结构化结果
├── stdout.txt # vitest 原始输出
└── stderr.txt # crash 时
# 还要写 (跨项目):
../../bugs.ndjson # bug_open / bug_transition 工具 append
2.2 完整 System Prompt(可拷贝直用)
落 system/templates/devloop-tester.md。LoopController 注入时头部补 ## Memory + ## Unit Code + Contract(节选 code/ 的 export 接口 + contract.json)。
# ROLE 你是 LemonBoss DevLoop 的 Tester Agent。本次任务:测试 Unit {unitId}。 读 Coder 的 code/ + Architect 的 spec.json,写 vitest 单测,跑测试, 把失败翻译成给 Coder 下次反思的 hint。 # MODES - "test" Coder 首次完成,Tester 首次跑 - "retry" Coder 改了 code,重跑 (如有旧 tests 复用) - "resume" 从 resume_token 继续 # SANDBOX 你只能写: ./tests/** ./runs/{runId}/** 禁止写: ./code/** (Coder 的领地) ./contract.json (Coder 的领地) ./coder.log (Coder 的领地) ../{别的 unit}/** (其他 Unit) 你可读: ./code/** (你测的代码) ./contract.json (推导期望响应) ./spec.json (acceptance_criteria) ../{depends_on}/contract.json (跨 Unit 不读其 code) ./tests/** (复用旧 spec) # MANDATORY WORKFLOW ## step 0 · 读上下文 - 读 spec.json acceptance_criteria 全条 - 读 contract.json exports.http (每个 status code 必测) - 读 code/ 入口文件 (找 export 接口) - retry: 读 ./tests/* 看哪些已存在 (可保留 + 重跑) ## step 1 · write_todolist ### test 典型 (5 项): step-001 写 tests/{unit}.spec.ts - 后端: supertest + 内存 sqlite - 前端: @vue/test-utils mount step-002 覆盖 acceptance 全条 + contract 全 status code step-003 跑 vitest --reporter=json > runs/{runId}/raw.json step-004 parse_vitest → runs/{runId}/result.json + 生成 hint step-005 bug_open / bug_transition (按结果) ### retry 典型 (3 项): step-001 跑现有 tests (代码改了,测试可能不需要改) step-002 parse_vitest → result.json step-003 bug_transition (如之前 open 现绿,转 fixed) # RULES (硬约束) 1. 后端测试模板: import request from 'supertest'; import { describe, it, expect, beforeEach } from 'vitest'; import app from '../code/server'; // 用 :memory: sqlite 起干净状态 2. 前端测试模板: import { mount } from '@vue/test-utils'; import { describe, it, expect } from 'vitest'; import Component from '../code/Component.vue'; 3. 禁止 mock 整个被测模块。可以 mock: - 外部 API (fetch / axios) - 第三方服务 (S3 / 邮件) - 时间 (vi.useFakeTimers) - 随机 (vi.spyOn(Math, 'random')) 4. 每个 acceptance_criterion → 至少 1 个 it() block。 不允许把多条揉进一个 it()。 5. 每个 contract.responses 状态码 → 至少 1 个 negative + positive case。 401 → 测一个返 401 的输入 500 → 不主动测 (500 是 bug 不是 feature) 6. 失败 hint 必须是给 Coder 的 1-2 句人话提示: ✓ "user 不存在时不要 bcrypt.compare(null,...);先查再比" ✓ "title 字段缺失 req.body.title 是 undefined,先做存在性判断" ✗ "fix the bug" ✗ "expected 401 got 500" (没有信息量) 由 hint_builder 工具帮你从 stack + expected/got 生成 (但你要审一遍)。 7. bugs.ndjson 同步: 失败:调 bug_open({source:'unit_test', unit_id, run_id, test_name, ...}) 如该 unit 之前有 status=open 的同 test bug → 不重复 open (用 dedupKey) 绿:之前有 status=open/fixing → bug_transition(to='fixed') 8. 任何 Bash 带 JSON body → /tmp/*.json + -d @file (CLAUDE.md L91-99)。 9. 单次 vitest 跑总时长上限 60s。超时 → kill + 标 result.crashed=true。 10. 不写 ./code/, 不调 call_sub_agent。 # SELF-CHECK 1. tests/ 至少有 acceptance_criteria 数量 + contract status code 数量个 it() block 2. result.json 通过 typebox 校验 3. result.json.failures 每条有非空 hint 4. bugs.ndjson 新增/转换条目都在 5. todolist 全 done 6. 没写出沙箱
2.3 工具集
| 工具 | 签名 | 用途 | 访问边界 |
|---|---|---|---|
Read | (path) | 读 code / contract / spec / 上游 contract / 历史 tests | 沙箱白名单 |
Write | (path, content) | 写 tests/ + runs/{runId}/ | tests/** + runs/** |
Bash | (cmd) | 跑 vitest run --reporter=json; npm i (限定包) | cwd 锁 units/{unitId}/, jsonGuard 强制, 60s 超时 |
parse_vitest NEW | (rawJsonPath) → RunResult | vitest JSON reporter → 结构化 result.json + 自动生成 hint | — |
hint_builder NEW | (failure) → string | 从 stack/expected/got 启发式生成 hint(Tester 可再编辑) | — |
bug_open NEW | (pid, bug) → bugId | append bugs.ndjson · dedupKey 防重 | bugs.ndjson |
bug_transition NEW | (pid, bugId, to, opts) | 状态机转换 | bugs.ndjson |
bug_list NEW | (pid, filter) | 查当前 open/fixing 的 bug(dedup 时用) | — |
write_todolist | (items) | 同上 | agent-state/ |
update_todo_item | (id, status) | 同上 | — |
update_runtime_state | (step, total) | 同上 | — |
timeline_append | (kind, ref, summary) | 同上 · kind: unit_tested · test_failed · test_passed | — |
record_memory | (topic, content, relevance) | memory/devloop/tester/ | memory/devloop/tester/ |
2.4 禁用
- 不允许 Edit / Write code/ — Coder 领地
- 不允许 mock 整个被测模块 — 否则测试永远绿
- 不允许 inline JSON Bash — jsonGuard 强制
- 不允许 call_sub_agent
- 不允许跳过 hint 字段 — failure 必含可执行提示
③ 持久化集成
同 Coder 模式 + 加 bugs.ndjson 桥接。失败时 bug_open;绿时 bug_transition;中间状态由 LoopController 转 fixing。
Tester memory 典型话题(很少):
- "vitest 启动慢 (Vite cold start),固定用 --pool=threads 加速"
- "better-sqlite3 :memory: 跨 it() 状态隔离需 beforeEach 重建"
- memory 在 Tester 这层很少有可复用洞见,注意不要乱写
④ I/O 契约
const TesterInput = Type.Object({ pid: Type.String(), unit_id: Type.String(), mode: Type.Union([Type.Literal('test'), Type.Literal('retry'), Type.Literal('resume')]), resume_token: Type.Optional(Type.String()), call_id: Type.String(), company_id: Type.String(), }); const TesterOutput = Type.Object({ ok: Type.Boolean(), // 总判:全绿才 true mode: Type.String(), unit_id: Type.String(), run_id: Type.String(), result_ref: Type.String(), // path to result.json summary: Type.Object({ total: Type.Number(), passed: Type.Number(), failed: Type.Number(), skipped: Type.Number(), }), failures: Type.Array(Type.Object({ test: Type.String(), expected: Type.Any(), got: Type.Any(), stack: Type.String(), hint: Type.String(), // 非空,给 Coder bug_id: Type.String(), // 关联 bugs.ndjson })), bugs_opened: Type.Array(Type.String()), // 新开 bug id 列表 bugs_transitioned: Type.Array(Type.Object({ id, from, to })), acceptance_coverage: Type.Number({ minimum:0, maximum:1 }), contract_coverage: Type.Number({ minimum:0, maximum:1 }), crashed: Type.Boolean(), // vitest 自身崩 duration_ms: Type.Number(), tokens_used: Type.Number(), });
⑤ 上下游传递
⬆ 上游
- Coder · 通过 code/ + contract.json
- Architect · 通过 spec.json acceptance + sdd tests 列表
- LoopController · 注入 input + memory
⬇ 下游
- Coder(retry 时)· 读 result.json.failures.hint 反思
- LoopController · 读 result.ok 决定 pass/retry/block
- UnitRegistry · 接 status: testing → passed | failed
- BugTracker / bugs.ndjson · 接 bug_open / bug_transition
- Integrator(passed 后)· 间接:tests/ 可作 e2e 回归基础
- Dashboard · SSE stream 看 stdout
⑥ 完整执行样例
# ─────────── INPUT ─────────── { "pid": "p-test001", "unit_id": "todo-be", "mode": "test", "call_id": "call-t-001", ... } # INJECTED Unit Code Excerpt (LoopController 拼) ## Unit Code: todo-be exports from code/server.js: app (Express) exports from code/service.js: create(title), list() ## Contract (contract.json) POST /api/todos { title } → 201 | 400 GET /api/todos → 200 { items } ## Acceptance Criteria (spec.json) - 创建后能列出 - 空 title 返 400 - 200+ 字符 title 返 400 # step 1 · write_todolist (5 项 test 模板) # step 2 · 写 tests/todo-be.spec.ts import request from 'supertest'; import { describe, it, expect, beforeEach } from 'vitest'; // 注意:Coder 实现里 db 是 module 级 :memory:,跨 it() 状态会污染 // 这里 fresh app/db 每个 it() (后面 hint 会指出 Coder 该改) describe('todo-be POST /api/todos', () => { let app; beforeEach(() => { delete require.cache[require.resolve('../code/server')]; app = require('../code/server'); }); it('有效 title → 201 + 返 id', async () => { const r = await request(app).post('/api/todos').send({ title: 'buy milk' }); expect(r.status).toBe(201); expect(r.body.id).toBeTypeOf('number'); }); it('空 title → 400 title_required', async () => { const r = await request(app).post('/api/todos').send({ title: '' }); expect(r.status).toBe(400); expect(r.body.error).toBe('title_required'); }); it('缺 body title → 400', async () => { const r = await request(app).post('/api/todos').send({}); expect(r.status).toBe(400); }); it('200+ title → 400 title_too_long', async () => { const r = await request(app).post('/api/todos') .send({ title: 'x'.repeat(201) }); expect(r.status).toBe(400); }); }); describe('todo-be GET /api/todos', () => { it('创建后能列出', async () => { const app = require('../code/server'); await request(app).post('/api/todos').send({ title: 'a' }); const r = await request(app).get('/api/todos'); expect(r.status).toBe(200); expect(r.body.items.some(t => t.title === 'a')).toBe(true); }); }); # step 3 · Bash: vitest run --reporter=json > runs/run-20260513-1031/raw.json # step 4 · parse_vitest parse_vitest("runs/run-20260513-1031/raw.json") // 结果: 5 个 test, 4 passed, 1 failed // 失败: "缺 body title → 400" 实际收 500 TypeError # hint_builder 看 stack: TypeError on req.body.title.length hint_builder({ expected: 400, got: 500, stack: "TypeError: cannot read properties of undefined (reading 'length') at create..." }) // → "缺 body 字段时 req.body.title 是 undefined,先做存在性判断再 .length" # step 5 · bug_open bug_open({ source: 'unit_test', unit_id: 'todo-be', run_id: 'run-20260513-1031', test_name: '缺 body title → 400', category: 'unknown', severity: 'high', title: '缺 body title 时 500 而非 400', detail: 'TypeError: ...', affected_units: ['todo-be'] }) // → bug-20260513-1031-c9f2 (新开) # 写 result.json { "run_id": "run-20260513-1031", "loop": "L1", "unit_id": "todo-be", "agent": "tester", "attempt": 1, "ok": false, "summary": {"total":5,"passed":4,"failed":1}, "failures": [{ "test": "缺 body title → 400", "expected": 400, "got": 500, "stack": "TypeError: ...", "hint": "缺 body 字段时 req.body.title 是 undefined,先做存在性判断再 .length", "bug_id": "bug-20260513-1031-c9f2" }], "next_action": "retry_coder", "retry_budget_left": 4, "duration_ms": 1_245 } # OUTPUT { "ok": false, "mode": "test", "unit_id": "todo-be", "run_id": "run-20260513-1031", "summary": {"total":5,"passed":4,"failed":1,"skipped":0}, "failures": [{ "hint": "...", "bug_id": "bug-..." }], "bugs_opened": ["bug-20260513-1031-c9f2"], "bugs_transitioned": [], "acceptance_coverage": 1.0, // 3/3 "contract_coverage": 1.0, "crashed": false, "duration_ms": 12_410, "tokens_used": 4_120 } # LoopController 接 ok=false → 触发 Coder retry (带 history)
# ─────────── INPUT (retry mode) ─────────── { "pid": "p-test001", "unit_id": "todo-be", "mode": "retry", "call_id": "call-t-002", ... } # 上下文: Coder 已改 code/server.js (按上次 hint) # tests/ 不需要改,直接复用 # step 1 · todolist 3 项 (retry 模板) # step 2 · Bash vitest 重跑 # step 3 · parse_vitest // 5 passed, 0 failed # step 4 · bug_list(unit_id=todo-be, status=[open,fixing]) → 1 条:bug-..-c9f2 # 之前 LoopController 让 Coder 干活时已转 open → fixing # 现在 ok=true → 转 fixing → fixed bug_transition(pid, "bug-20260513-1031-c9f2", { to: "fixed", by: "tester", fix_refs: ["run-20260513-1034"] }) # OUTPUT { "ok": true, "mode": "retry", "unit_id": "todo-be", "run_id": "run-20260513-1034", "summary": {"total":5,"passed":5,"failed":0}, "failures": [], "bugs_opened": [], "bugs_transitioned": [{ "id":"bug-...c9f2", "from":"fixing", "to":"fixed" }], ... } # LoopController 接 ok=true → Unit 转 passed → 等 L2 闸门
# ─────────── INPUT (vitest 自身崩) ─────────── { "pid": "p-test001", "unit_id": "profile-be", "mode": "test", ... } # step 3 · Bash vitest run → 退出码 1, 但不是测试失败 # vitest 启动时 require code/server.js 直接抛 SyntaxError # step 4 · parse_vitest 检测 raw.json 不存在或字段缺 # → result.crashed = true # → next_action = "retry_tester" 而非 "retry_coder" # 写 result.json { "run_id": "run-...", "unit_id": "profile-be", "ok": false, "crashed": true, "crash_reason": "vitest could not load ../code/server.js: SyntaxError", "stdout_tail": "...完整 stderr...", "next_action": "retry_tester" // 注意:不是 retry_coder } # bug_open 也开,但标 source='unit_test_infra' 区分 # LoopController 接 crashed=true → retry Tester (而非 Coder) # → retry 仍 crash → 升级 Coder retry
⑦ 助手工具源码
7.1 parse-vitest-json.js · vitest JSON reporter 解析
// server/services/agent-tools/parse-vitest-json.js · ~80 行 const fs = require('fs'); const path = require('path'); const { buildHint } = require('./hint-builder'); function parseVitest(rawJsonPath, opts = {}) { if (!fs.existsSync(rawJsonPath)) { return { ok: false, crashed: true, crash_reason: 'vitest raw json missing - process likely crashed early', summary: { total:0, passed:0, failed:0, skipped:0 }, failures: [], }; } let raw; try { raw = JSON.parse(fs.readFileSync(rawJsonPath, 'utf8')); } catch (e) { return { ok:false, crashed:true, crash_reason: 'invalid vitest json', summary:{}, failures:[] }; } const summary = { total:0, passed:0, failed:0, skipped:0 }; const failures = []; // vitest 1.x JSON reporter 结构: // { testResults: [{ assertionResults: [{ status, fullName, failureMessages, ... }] }] } for (const file of (raw.testResults || [])) { for (const a of (file.assertionResults || [])) { summary.total++; if (a.status === 'passed') summary.passed++; else if (a.status === 'failed') { summary.failed++; const stack = (a.failureMessages || []).join('\n'); const { expected, got } = extractAssertion(stack); failures.push({ test: a.fullName, expected, got, stack, hint: buildHint({ testName: a.fullName, expected, got, stack }), }); } else summary.skipped++; } } return { ok: summary.failed === 0 && summary.total > 0, crashed: false, summary, failures, duration_ms: raw.testResults?.[0]?.perfStats?.runtime || 0, }; } function extractAssertion(stack) { const exp = stack.match(/Expected: ([^\n]+)/); const got = stack.match(/Received: ([^\n]+)/); return { expected: exp ? exp[1].trim() : null, got: got ? got[1].trim() : null, }; } module.exports = { parseVitest };
7.2 hint-builder.js · 失败 → 人话 hint
// server/services/agent-tools/hint-builder.js · ~60 行 const PATTERNS = [ { match: (s) => /cannot read.*?of undefined/i.test(s), hint: (ctx) => '某个对象/字段是 undefined,先做存在性判断再访问', }, { match: (s) => /expected (\d+).*received (\d+)/i.test(s), hint: (ctx) => { const m = ctx.stack.match(/expected (\d+).*received (\d+)/i); const exp = +m[1], got = +m[2]; if (exp === 401 && got === 500) return '凭据错误应返 401 不是 500;检查认证路径是否 try/catch 兜底'; if (exp === 400 && got === 500) return '参数校验错应返 400 不是 500;在 route 层检查后再传 service'; if (exp === 404 && got === 200) return '不存在的资源不应返 200;返回前判断查询结果'; return `期望 ${exp} 收 ${got},检查对应分支处理`; }, }, { match: (s) => /TypeError.*compare/i.test(s), hint: (ctx) => '调用 bcrypt.compare 前先确认 user 存在;否则 null 参数会抛 TypeError', }, { match: (s) => /SQLITE_CONSTRAINT/i.test(s), hint: (ctx) => '数据库约束冲突(unique/foreign key),业务层应该先查再写', }, { match: (s) => /timeout/i.test(s), hint: (ctx) => '某个异步操作超时;检查是否漏 await 或外部依赖未 mock', }, ]; function buildHint(ctx) { const stack = ctx.stack || ''; for (const p of PATTERNS) { if (p.match(stack)) return p.hint(ctx); } // fallback: Tester 在 prompt 里被要求人工审一遍补充 return `${ctx.testName}: 期望 ${ctx.expected} 收 ${ctx.got},检查 stack 推断原因 (Tester 应补充)`; } module.exports = { buildHint, PATTERNS };
7.3 bug-bridge.js · Tester ↔ bugs.ndjson
// server/services/agent-tools/bug-bridge.js · ~70 行 const fs = require('fs'); const path = require('path'); const { bugTracker } = require('../bug-tracker'); /** * Tester 收到 vitest failures 后,自动桥接到 bugs.ndjson * - dedup: 同 unit 同 test 已有 open/fixing → 复用 id * - 新 fail → bug_open * 调用方:Tester Agent 的工具调用 (而非直接 fs) */ function bridgeFailuresToBugs(pid, unitId, runId, failures) { const bugs_opened = []; const existing = bugTracker.list(pid, { unit_id: unitId, status: ['open', 'fixing'], }); for (const f of failures) { const dedupKey = `${unitId}:${f.test}`; const dup = existing.find(b => bugTracker.dedupKey(b) === dedupKey); if (dup) { f.bug_id = dup.id; // 复用,不新开 } else { const bug = bugTracker.open(pid, { source: 'unit_test', unit_id: unitId, run_id: runId, test_name: f.test, category: 'unknown', // BugTriager 在 L2 才精修 severity: guessSeverity(f), title: `${f.test}: 期望 ${f.expected} 收 ${f.got}`, detail: f.stack, affected_units: [unitId], }); f.bug_id = bug.id; bugs_opened.push(bug.id); } } return { bugs_opened }; } function bridgePassToBugs(pid, unitId, runId) { // 单测绿了 → 该 unit 所有 open/fixing 转 fixed const existing = bugTracker.list(pid, { unit_id: unitId, status: ['open', 'fixing'], }); const transitioned = []; for (const b of existing) { bugTracker.transition(pid, b.id, { to: 'fixed', by: 'tester', fix_refs: [runId], }); transitioned.push({ id: b.id, from: b.status, to: 'fixed' }); } return { bugs_transitioned: transitioned }; } function guessSeverity(f) { if (/5\d\d/.test(String(f.got))) return 'high'; if (f.test.includes('security') || f.test.includes('auth')) return 'critical'; return 'medium'; } module.exports = { bridgeFailuresToBugs, bridgePassToBugs };
⑧ 自检 Checklist
| # | 检查项 | 命令 / 工具 | 通过判定 |
|---|---|---|---|
| 1 | tests/ 至少含 N 个 it()(N = acceptance + contract responses) | cheerio-like scan | true |
| 2 | result.json 通过 typebox | schema check (M-7) | 合规 |
| 3 | 每个 failure 有非空 hint | jsonpath | 无空 hint |
| 4 | bugs.ndjson 同步 | 对照 bug_open/transition 输出 | bugs_opened/transitioned 全部有 entry |
| 5 | 沙箱无越界 | scanSandbox | violations=[] |
| 6 | 没 mock 整个被测模块 | spec 文件 grep vi.mock('../code/' | 无 |
| 7 | todolist 全 done | read agent-state/tester-todo.json | true |
| 8 | timeline 含 unit_tested / test_failed / test_passed | tail | 命中 |
| 9 | vitest 没超时(60s) | duration_ms < 60000 | true |
| 10 | acceptance_coverage = 1.0 | compare spec.json acceptance vs tests it() 名称 | 1.0 |
⑨ 失败模式与降级
| 场景 | 检测 | 降级 |
|---|---|---|
| vitest 自身崩 | parse_vitest crashed=true | result.crashed=true · next_action='retry_tester' |
| Tester 写 trivial test(永远绿) | spec 强制至少 1 negative case | Architect 在 spec.json 加 negative criterion |
| Tester 越权改 code/ | 沙箱拒绝 | — |
| vitest 超时(>60s) | Bash 工具超时 | kill + result.crashed=true · 排查死循环测试 |
| hint 为空(只 fallback) | self-check 4 | Tester 必须人工审 + 补充 |
| npm install 慢 | 同 Coder | 缓存 node_modules 共享 |
| 测试运行依赖网络(fetch real API) | vitest spec 扫描 | 禁止 · 必须 mock fetch |
| vitest 版本与 LemonBoss 不一致 | package.json 校验 | 用 LemonBoss 既有 vitest 版本 |
⑩ 与其他 Agent 握手摘要
| 对方 | Tester 给的 | Tester 收的 |
|---|---|---|
| Coder | tests/* + result.json.failures (含 hint) | code/** + contract.json |
| Architect | (无) | spec.json acceptance + sdd tests 列表 |
| Integrator | tests/ 可复用作回归基础 | (无) |
| BugTracker | bug_open / bug_transition | — |
| LoopController | TesterOutput JSON | TesterInput |
| UnitRegistry | — | —(LoopController 中介) |
| memory/devloop/tester/ | 很少写 | 启动注入 |
⑪ 实战验证命令
# 前置: Coder 已跑通,units/todo-be/code/* + contract.json 就绪 # 1. 注册模板 # system/templates/devloop-tester.md ← 本页 §2.2 # 2. 准备输入 echo '{"pid":"p-test001","unit_id":"todo-be","mode":"test","call_id":"call-t-1","company_id":"c-test"}' > /tmp/tester-test.json # 3. 调 Tester curl -X POST http://localhost:3010/api/devloop/_test/run-agent/tester -d @/tmp/tester-test.json # 4. 期望:30-60 秒内 # units/todo-be/tests/*.spec.ts 生成 # units/todo-be/runs/{runId}/result.json 含 summary + failures (如有) # 5. 验证 ls data/companies/c-test/projects/p-test001/units/todo-be/tests/ # 期望: 至少 1 个 .spec.ts cat data/companies/c-test/projects/p-test001/units/todo-be/runs/*/result.json | jq .summary # 期望: { total: ≥4, passed/failed 看 Coder 代码质量 } cat data/companies/c-test/projects/p-test001/bugs.ndjson | head -5 # 期望: 若有 failure,每条 failure 对应 1 行 bug 记录 (status=open) # 6. 模拟 Coder 修后 retry echo '{"pid":"p-test001","unit_id":"todo-be","mode":"retry","call_id":"call-t-2","company_id":"c-test"}' > /tmp/tester-retry.json curl -X POST http://localhost:3010/api/devloop/_test/run-agent/tester -d @/tmp/tester-retry.json # 期望: # result.json ok=true (Coder 已根据 hint 改了) # bugs.ndjson 末尾新增一行 (同 id, status=fixed) node -e " const {bugTracker} = require('./server/services/bug-tracker'); console.log(bugTracker.list('p-test001')); " # 期望: 全部 bug 状态 = fixed
Coder + Tester 这两 §⑪ 都通过 = L1 完整闭环验证完成。Coder/Tester 反复跑直到 unit passed,这是整个 DevLoop 最高频运行的循环。