task-decomposition-validator-gate.test.js
7.8 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
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
/**
* 43 号文档 Phase 3:coder-validator 判自己 success 之前,子任务必须全部 completed
* 才认;以及 settleSubtaskResult 这个落库口本身的条件更新/幂等/校验。
*/
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import fs from 'fs';
import os from 'os';
import path from 'path';
const TEST_DB_PATH = '/tmp/test-task-decomp-validator-gate.sqlite';
const DB_FILES = [TEST_DB_PATH, `${TEST_DB_PATH}-shm`, `${TEST_DB_PATH}-wal`];
const MODULES = [
'../../services/graph-db',
'../../services/graph-service',
'../../services/org-graph-service',
'../../services/work-graph-db',
'../../services/graph-run-sse',
'../../services/graph-engine',
];
describe('43 号文档 Phase 3', () => {
let wdb;
let engine;
let runsDir;
beforeAll(() => {
for (const f of DB_FILES) { try { fs.unlinkSync(f); } catch {} }
process.env.GRAPH_DB_PATH = TEST_DB_PATH;
runsDir = fs.mkdtempSync(path.join(os.tmpdir(), 'task-decomp-gate-runs-'));
process.env.GRAPH_RUNS_DIR = runsDir;
for (const m of MODULES) delete require.cache[require.resolve(m)];
wdb = require('../../services/work-graph-db');
engine = require('../../services/graph-engine');
});
afterAll(() => {
for (const f of DB_FILES) { try { fs.unlinkSync(f); } catch {} }
delete process.env.GRAPH_DB_PATH;
try { fs.rmSync(runsDir, { recursive: true, force: true }); } catch {}
delete process.env.GRAPH_RUNS_DIR;
for (const m of MODULES) delete require.cache[require.resolve(m)];
});
function ensureAgent(id) {
const db = wdb.getDb();
const now = new Date().toISOString();
db.prepare(`INSERT OR IGNORE INTO system_agents
(id, name, scope, owner_company_id, legacy_path, status, created_at, updated_at)
VALUES (?, ?, 'public', NULL, ?, 'active', ?, ?)`)
.run(id, id, `system/agents/${id}`, now, now);
}
// ── settleSubtaskResult 本身 ─────────────────────────────────────
it('settleSubtaskResult:非法 status 直接拒', () => {
expect(() => wdb.settleSubtaskResult('T-whatever', 'running')).toThrow(/status 只能是/);
});
it('settleSubtaskResult:不是子任务(parent_task_id 为空)不生效,changes=0', () => {
const goal = wdb.createGoal({ companyId: 'co-gate-1', title: 'g', description: 'd' });
const top = wdb.createTask({ goalId: goal.id, title: 't', description: 'd' });
const changed = wdb.settleSubtaskResult(top.id, 'completed');
expect(changed).toBe(0);
expect(wdb.getTask(top.id).status).toBe('draft'); // 没被动过
});
it('settleSubtaskResult:正常上报 + 终态幂等(第二次不同结果上报不生效)', () => {
const goal = wdb.createGoal({ companyId: 'co-gate-2', title: 'g', description: 'd' });
const parent = wdb.createTask({ goalId: goal.id, title: 'p', description: 'd', acceptanceMode: 'itemized' });
const child = wdb.createTask({
goalId: goal.id, title: 'c', description: 'd', parentTaskId: parent.id, subtaskKind: 'acceptance_item',
});
expect(wdb.settleSubtaskResult(child.id, 'completed', '通过')).toBe(1);
const after1 = wdb.getTask(child.id);
expect(after1.status).toBe('completed');
expect(after1.summary).toBe('通过');
expect(after1.finished_at).toBeTruthy();
// 已经是终态,再上报 failed 不该覆盖——防的是 coder-validator 因 retry 重复上报
expect(wdb.settleSubtaskResult(child.id, 'failed', '改判')).toBe(0);
expect(wdb.getTask(child.id).status).toBe('completed');
});
// ── 引擎侧的闸门:coder-validator 判 success 前核子任务 ─────────────
async function runValidator(companyId, taskId) {
ensureAgent('coder-validator');
const g = wdb.createWorkGraph({
companyId,
taskId,
nodes: [{ nodeName: 'v', systemAgentId: 'coder-validator' }],
edges: [],
});
expect(g.errors || []).toEqual([]);
const run = wdb.createRun({ graphKind: 'work', graphId: g.id, companyId, taskId });
engine.markRunReady(run.id);
engine.startRun(run.id, { mode: 'manual' });
engine.setAgentExecutor(async () => ({ status: 'success' }));
let out;
try {
out = await engine.advanceOne(run.id);
} finally {
engine.setAgentExecutor(null);
}
return out;
}
it('itemized 任务 + 子任务还没全部 completed → coder-validator 判自己 success 但被引擎打回 fail', async () => {
const companyId = 'co-gate-3';
const goal = wdb.createGoal({ companyId, title: 'g', description: 'd' });
const parent = wdb.createTask({ goalId: goal.id, title: 'p', description: 'd', acceptanceMode: 'itemized' });
const c1 = wdb.createTask({
goalId: goal.id, title: 'c1', description: 'd', parentTaskId: parent.id, subtaskKind: 'acceptance_item',
});
wdb.createTask({
goalId: goal.id, title: 'c2', description: 'd', parentTaskId: parent.id, subtaskKind: 'acceptance_item',
});
wdb.settleSubtaskResult(c1.id, 'completed'); // 只完成一半
const out = await runValidator(companyId, parent.id);
// 单节点图没有兜底边可退,fail 之后引擎会继续把它收敛成 blocked(等人)——
// 这里要验的是「没被判 success」+「归因是 subtasks_incomplete」,不是具体停在哪个终态
expect(out.node.status).not.toBe('success');
expect(out.node.failReason).toBe('subtasks_incomplete');
});
it('itemized 任务 + 子任务全部 completed → coder-validator 正常判 success', async () => {
const companyId = 'co-gate-4';
const goal = wdb.createGoal({ companyId, title: 'g', description: 'd' });
const parent = wdb.createTask({ goalId: goal.id, title: 'p', description: 'd', acceptanceMode: 'itemized' });
const c1 = wdb.createTask({
goalId: goal.id, title: 'c1', description: 'd', parentTaskId: parent.id, subtaskKind: 'acceptance_item',
});
const c2 = wdb.createTask({
goalId: goal.id, title: 'c2', description: 'd', parentTaskId: parent.id, subtaskKind: 'acceptance_item',
});
wdb.settleSubtaskResult(c1.id, 'completed');
wdb.settleSubtaskResult(c2.id, 'completed');
const out = await runValidator(companyId, parent.id);
expect(out.node.status).toBe('success');
});
it('46 号文档 Phase 3:同一个 task 下还挂着 decomposed_work 子任务(未 completed)时,'
+ '不影响 acceptance_item 的验收判定——两套子任务完成判定互不干扰', async () => {
const companyId = 'co-gate-6';
const goal = wdb.createGoal({ companyId, title: 'g', description: 'd' });
const parent = wdb.createTask({ goalId: goal.id, title: 'p', description: 'd', acceptanceMode: 'itemized' });
const c1 = wdb.createTask({
goalId: goal.id, title: 'c1', description: 'd', parentTaskId: parent.id, subtaskKind: 'acceptance_item',
});
wdb.settleSubtaskResult(c1.id, 'completed'); // 验收条目全部完成
// decomposed_work 子任务还停在 draft,还没被 CEO enqueue、更不会 completed——
// 如果闸门没收窄,这条会被当成"未完成的子任务"误伤,把上面本该通过的验收打成失败
wdb.createTask({
goalId: goal.id, title: 'todo-step', description: 'd', parentTaskId: parent.id,
subtaskKind: 'decomposed_work',
});
const out = await runValidator(companyId, parent.id);
expect(out.node.status).toBe('success');
});
it('holistic 任务(没有子任务):闸门不生效,success 照常通过', async () => {
const companyId = 'co-gate-5';
const goal = wdb.createGoal({ companyId, title: 'g', description: 'd' });
const task = wdb.createTask({ goalId: goal.id, title: 't', description: 'd' }); // 缺省 holistic
const out = await runValidator(companyId, task.id);
expect(out.node.status).toBe('success');
});
});