# Tester Agent · 完整 Skill 规范
> _Shell-stripped: preserves full prompt content for Agent system templates_
> L1 的对手方。读 Coder 的 code/ + spec + contract.json,写 vitest 单测,跑、收失败、产**给 Coder 反思的 hint**。同时桥接到 bugs.ndjson 做 bug 生命周期。
T
Tester Agent
写测试 · 跑测试 · 翻译失败原因 · 桥接 bugs.ndjson
**Loop** L1
**Model** Sonnet 4.6
**Modes** test · retry · resume
**沙箱** units/{unitId}/ 内 tests/ + runs/{runId}/
**会话复用** 是(SessionPool key = tester.cid.pid.unitId)
## ① 功能需求
1. **三种模式:**`test`(Coder 完成后首次)/ `retry`(Coder 改过重跑)/ `resume`。
2. **覆盖来源:**
- **spec.json.acceptance_criteria** 每条 ≥ 1 case 3. **contract.json.exports.http.responses** 每个状态码 ≥ 1 case 4. **真跑:**后端用 `supertest` 起真实 Express + `better-sqlite3 :memory:`;前端用 `@vue/test-utils`,不渲染浏览器。 5. **不允许 mock 整个被测模块**。可 mock 外部 API / DB / 时间。 6. **失败必含 hint:**给 Coder 下次反思的 1-2 句人话提示。**禁止**「fix the bug」「expected 401 got 500」式无信息废话。 7. **桥接 bugs.ndjson:**失败 → `bug_open` append 一条 status=open;下次绿 → `bug_transition(to='fixed')`。 8. **幂等:**同一代码反复跑结果一致。依赖时间 / 随机数 / 网络必须 stub。 9. **不写代码:**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/agents/devloop-tester/prompt.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/agents/devloop-tester/prompt.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 最高频运行的循环。