Commit ad045ddf9c6112e228d5e3d6b2772c028b86cc71

Authored by yibo
1 parent fdeafcdb

fix(tests): 修掉此前被误判为"环境噪音"的 3 个真单测失败

之前几轮全量跑一直有 3 个失败,当时判断是环境耦合/pre-existing、跟本次改动无关就没细查,
这次逐个真查了一遍,三个都是真 bug:

- system/agents/sdd-planning/SKILL.md:Evolve 训练产出从 train/evolve/SKILL.md 提升到
  生产位置时,"训练依据"引用的相对路径没跟着调整(少了 train/ 前缀),指向了不存在的
  system/agents/hunt/、system/agents/digest/;真实文件其实在同一个 agent 自己的
  train/hunt/、train/digest/ 下,补前缀即可。

- graduation-criteria-reality.test.js:断言死等 code-planner 的状态回到 'active',但
  该本体的 config.json 早已从曾经踩坑的 "training" 改成了真正毕业态的 "enabled"——
  seedAgents() 把 DB 同步成 'enabled' 是设计如此(同为毕业态,不是降级),不该被判失败。
  改成用 isUngraduatedStatus() 判断"没被打回未毕业域",不再钉死具体状态值;顺手把
  graph-seed-service.js 里那句会随源码变化过期的注释也改成不点名具体值。

- graph-engine-extend-budget.test.js:一颗时间炸弹——用硬编码绝对时间戳
  '2026-08-23T05:23:52.886Z' 模拟"卡住很久了",但被测代码真的调 Date.now() 算经过时长,
  写这条用例时"经过时长"确实超过 2h 上限、又低于追加后的 6h 上限;现在真实时间已经过去
  178 小时,连追加后的上限都超了,测试注定要红,跟被测代码毫无关系。改成
  Date.now() - 3小时 相对计算,不再随时间推移漂移。

全量 vitest 131/131 文件、1089/1089 用例通过。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
server/services/graph-seed-service.js
@@ -458,8 +458,10 @@ function seedAgents() { @@ -458,8 +458,10 @@ function seedAgents() {
458 // **只写不降级**:config.json 里的 status 是本体的**出生默认值**,而毕业与否是运行时 458 // **只写不降级**:config.json 里的 status 是本体的**出生默认值**,而毕业与否是运行时
459 // 从磁盘定义学到的事实(promoteDraftIfComplete / reconcile 的 E 分支)。原样 upsert 的话, 459 // 从磁盘定义学到的事实(promoteDraftIfComplete / reconcile 的 E 分支)。原样 upsert 的话,
460 // 每次进程重启 seedAgents 都会把已经转正的行按 config.json 打回 draft/training —— 460 // 每次进程重启 seedAgents 都会把已经转正的行按 config.json 打回 draft/training ——
461 - // 闸门抬起来一次、下次启动又落下去,而且一处报错都没有(code-planner 的 config.json  
462 - // 至今写着 "training")。所以:库里已经脱离未毕业域的行,不许再被推回去;其余照写。 461 + // 闸门抬起来一次、下次启动又落下去,而且一处报错都没有(真实撞过的样本是 code-planner,
  462 + // 它的 config.json 当时写着 "training",见 `graduation-criteria-reality.test.js`;
  463 + // 后来 config.json 已经改成了毕业态的 "enabled",这条注释只留问题形状,不再钉死具体值,
  464 + // 避免又变成一处会随源码变化过期的死引用)。所以:库里已经脱离未毕业域的行,不许再被推回去;其余照写。
463 const status = (existing && isUngraduatedStatus(configStatus) && !isUngraduatedStatus(existing.status)) 465 const status = (existing && isUngraduatedStatus(configStatus) && !isUngraduatedStatus(existing.status))
464 ? existing.status 466 ? existing.status
465 : configStatus; 467 : configStatus;
server/tests/unit/graduation-criteria-reality.test.js
@@ -196,10 +196,14 @@ describe.skipIf(!fs.existsSync(CODE_PLANNER_DIR))(&#39;code-planner 能被新公å¸é @@ -196,10 +196,14 @@ describe.skipIf(!fs.existsSync(CODE_PLANNER_DIR))(&#39;code-planner 能被新公å¸é
196 expect(r.graduated).toContain('code-planner'); 196 expect(r.graduated).toContain('code-planner');
197 expect(statusOf()).toBe('active'); 197 expect(statusOf()).toBe('active');
198 198
199 - // seedAgents 会把 config.json çš„ status 原样 upsert —— å®ƒå†™ç€ "training"。  
200 - // ä¸åŠ ã€Œåªå†™ä¸é™çº§ã€çš„è¯ï¼Œè¿™é‡Œä¼šæŠŠåˆšè½¬æ­£çš„è¡Œæ‰“å›žåŽ»ï¼Œæ¯æ¬¡è¿›ç¨‹é‡å¯éƒ½é‡æ¼”一é。 199 + // seedAgents 会把 config.json çš„ status 原样 upsert。「åªå†™ä¸é™çº§ã€å®ˆçš„æ˜¯ã€Œä¸è®¸è¢«
  200 + // config.json 推回 draft/trainingã€ï¼Œ**ä¸**ä¿è¯è½¬æ­£åŽçš„值原样ä¸å˜â€”—config.json 当å‰
  201 + // 写的是什么已毕业状æ€ï¼ˆ"enabled" 也好ã€åˆ«çš„也好,åªè¦ä¸æ˜¯ draft/training),upsert 就会
  202 + // ç”¨é‚£ä¸ªå€¼è¦†ç›–ï¼Œè¿™æ˜¯è®¾è®¡å¦‚æ­¤ï¼Œä¸æ˜¯æœ¬ç”¨ä¾‹è¦æµ‹çš„东西(2026-08-30 夿 ¸ï¼šconfig.json å·²ç»ä»Ž
  203 + // 写死的 "training" 改æˆäº† "enabled",ä¸å†ç¡¬ç¼–ç å…·ä½“值,é¿å…è¿™æ¡æµ‹è¯•冿¬¡å› ä¸º config.json
  204 + // 内容å˜åŒ–而å‡çº¢ï¼‰ã€‚
201 seed.seedAgents(); 205 seed.seedAgents();
202 - expect(statusOf()).toBe('active'); 206 + expect(graphService.isUngraduatedStatus(statusOf())).toBe(false);
203 207
204 // å¹‚ç­‰ï¼šç¬¬äºŒéæ²¡æœ‰å¯è½¬æ­£çš„行 208 // å¹‚ç­‰ï¼šç¬¬äºŒéæ²¡æœ‰å¯è½¬æ­£çš„行
205 expect(seed.reconcileDanglingRows().graduated).toEqual([]); 209 expect(seed.reconcileDanglingRows().graduated).toEqual([]);
server/tests/unit/graph-engine-extend-budget.test.js
@@ -36,7 +36,12 @@ describe(&#39;extendBudget · 追加预算并结清卡死的墙钟锚点&#39;, () =&gt; { @@ -36,7 +36,12 @@ describe(&#39;extendBudget · 追加预算并结清卡死的墙钟锚点&#39;, () =&gt; {
36 beforeEach(() => wdb.getDb().prepare('DELETE FROM graph_runs').run()); 36 beforeEach(() => wdb.getDb().prepare('DELETE FROM graph_runs').run());
37 37
38 it('run_b962ea87f58f 的真实形态:追加后立即可用,且锚点被断开', () => { 38 it('run_b962ea87f58f 的真实形态:追加后立即可用,且锚点被断开', () => {
39 - const stuckStartedAt = '2026-08-23T05:23:52.886Z'; // 距 now() 已远超 2h 39 + // ⚠️ 曾经硬编码成绝对时间戳 '2026-08-23T05:23:52.886Z'(真实事故发生时的时刻)——
  40 + // 这是一颗时间炸弹:随真实时间流逝,这个绝对值与 Date.now() 的差值只会越来越大,
  41 + // 迟早会超过下面追加后的 6h 上限,让断言从「追加后立即可用」变成「怎么追加都不够」,
  42 + // 而这条红跟被测代码毫无关系。改成相对 Date.now() 回退 3 小时——同时满足
  43 + // 「超过追加前 2h 上限」与「低于追加后 6h 上限」,且不会随时间推移而漂移。
  44 + const stuckStartedAt = new Date(Date.now() - 3 * HOUR).toISOString();
40 const run = seedRun('r_stuck', { 45 const run = seedRun('r_stuck', {
41 total_agent_calls: 100, total_tokens: 5_000_000, wallclock_ms: 2 * HOUR, 46 total_agent_calls: 100, total_tokens: 5_000_000, wallclock_ms: 2 * HOUR,
42 spent: { agent_calls: 35, tokens: 249_324, active_ms: 0, started_at: stuckStartedAt }, 47 spent: { agent_calls: 35, tokens: 249_324, active_ms: 0, started_at: stuckStartedAt },
system/agents/sdd-planning/SKILL.md
@@ -3,7 +3,7 @@ @@ -3,7 +3,7 @@
3 > 本版本为 Evolve 训练产出(v1,首次训练)。基线为生产版 `system/agents/sdd-planning/agent.md`, 3 > 本版本为 Evolve 训练产出(v1,首次训练)。基线为生产版 `system/agents/sdd-planning/agent.md`,
4 > 仅强化「接口契约」「数据形态」两类的执行深度指导,其余框架(五类东西结构、 4 > 仅强化「接口契约」「数据形态」两类的执行深度指导,其余框架(五类东西结构、
5 > `contract.json`/`attempt-{n}.json` 模板、越权判断)与基线一致,未重写。 5 > `contract.json`/`attempt-{n}.json` 模板、越权判断)与基线一致,未重写。
6 -> 训练依据:`../hunt/knowhow_v1.md` + `../digest/benchmark_v1.md`(D2 判据可独立复现性,权重30+封顶)。 6 +> 训练依据:`train/hunt/knowhow_v1.md` + `train/digest/benchmark_v1.md`(D2 判据可独立复现性,权重30+封顶)。
7 7
8 ## 身份定位 8 ## 身份定位
9 9