graph-engine-extend-budget.test.js
3.54 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
/**
* Unit Test — engine.extendBudget:给 blocked run 追加预算 + 结清卡死的墙钟锚点
*
* 背景:run_b962ea87f58f 因 wallclock_ms 耗尽被 block,`spent.started_at` 卡在 block
* 之前的旧时刻没有断开(该 run 被 block 那一刻树里还有别的 run 在跑,settleWallclock
* 判定「不结算」,见 budget-wallclock-active.test.js 那条同名用例)。
* 只加 wallclock_ms 上限不够:`startRunSync` 一启动就走 settleWallclock('running'),
* 见到 started_at 非空会拒绝重锚,新加的额度立刻被「挂起等待的时长」吃光。
* 这个测试钉住 extendBudget 必须同时把这个锚点结清。
*/
import { describe, it, expect, afterAll, beforeEach } from 'vitest';
import fs from 'fs';
import os from 'os';
import path from 'path';
const TMP = fs.mkdtempSync(path.join(os.tmpdir(), 'lemonboss-extend-budget-'));
process.env.GRAPH_DB_PATH = path.join(TMP, 'graph.sqlite');
const engine = require('../../services/graph-engine');
const wdb = require('../../services/work-graph-db');
const HOUR = 3600_000;
function seedRun(id, budgetObj, status = 'blocked') {
const db = wdb.getDb();
const t = '2026-08-23T05:23:52.886Z';
db.prepare(`INSERT INTO graph_runs
(id, graph_kind, graph_id, company_id, root_run_id, execution_mode, run_status, budget_json, created_at, updated_at)
VALUES (?, 'org', 'g1', 'co1', ?, 'manual', ?, ?, ?, ?)`)
.run(id, id, status, JSON.stringify(budgetObj), t, t);
return wdb.getRun(id);
}
describe('extendBudget · 追加预算并结清卡死的墙钟锚点', () => {
afterAll(() => fs.rmSync(TMP, { recursive: true, force: true }));
beforeEach(() => wdb.getDb().prepare('DELETE FROM graph_runs').run());
it('run_b962ea87f58f 的真实形态:追加后立即可用,且锚点被断开', () => {
const stuckStartedAt = '2026-08-23T05:23:52.886Z'; // 距 now() 已远超 2h
const run = seedRun('r_stuck', {
total_agent_calls: 100, total_tokens: 5_000_000, wallclock_ms: 2 * HOUR,
spent: { agent_calls: 35, tokens: 249_324, active_ms: 0, started_at: stuckStartedAt },
});
// 追加前:卡死的锚点让墙钟早就判耗尽
expect(engine.checkBudget(run).exhausted).toContain('wallclock_ms');
const out = engine.extendBudget(run.id, { wallclock_ms: 4 * HOUR });
expect(out.ok).toBe(true);
expect(out.budget.wallclock_ms).toBe(6 * HOUR);
expect(out.budget.spent.started_at).toBeNull(); // 锚点必须断开
expect(out.budget.spent.active_ms).toBeGreaterThan(0); // 挂起时长已并入 active_ms
const fresh = wdb.getRun('r_stuck');
expect(engine.checkBudget(fresh).ok).toBe(true);
// 「启动」重锚不会被旧锚点拒绝——settleWallclock('running') 应该重新开一段计时
const settled = engine.settleWallclock('r_stuck', 'running');
expect(settled.started_at).toBeTruthy();
});
it('目标维度本来就不限额时忽略该维度;一维都没追加成就报错', () => {
seedRun('r_partial', { total_agent_calls: 100, spent: { agent_calls: 5 } });
// wallclock_ms 本来就没配额度,追加它是 no-op;total_agent_calls 有配额,真正生效
const out = engine.extendBudget('r_partial', { wallclock_ms: HOUR, total_agent_calls: 50 });
expect(out.applied).toEqual({ total_agent_calls: 50 });
expect(out.budget.total_agent_calls).toBe(150);
expect(out.budget.wallclock_ms).toBeUndefined();
seedRun('r_none', { total_agent_calls: 100, spent: {} });
expect(() => engine.extendBudget('r_none', { wallclock_ms: HOUR })).toThrow();
});
});