task-state-cleanup.test.js
4.81 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
/**
* 任务态清理(28 §5.6)—— **只清任务态,永不碰公司态**。
*
* ── 为什么这条测试比一般的清理逻辑更要紧 ────────────────────────────
* 删错不可逆。而且被删错的那一半(公司态 `memory/` / `conversations/`)
* 是 dream 的**唯一原料**:删掉损失的不是缓存,是这个 agent 全部的经验历史,
* 且**不可再生**(28 §5.6b)。
*
* ── 四道判据 ──────────────────────────────────────────────────────────
* ① 只在 `company_agents` 名册里的公司下面清(`train/` `workspace/` 和测试残留天然被排除);
* ② **白名单**:只删认得出来的任务态目录名,其余一概不动;
* ③ 年龄早于 N 天;
* ④ `run_*` 目录对应的 run 还没到终态就不删。
*
* ── 第 ② 条是踩出来的 ─────────────────────────────────────────────────
* 第一版写成黑名单(「不在保护名单里的都算任务态」),dry-run 直接选中了
* `trend-scout-global/idearun/venv` 和 `youtube-crawler/idearun/data` ——
* 一个 Python 虚拟环境、一个数据目录,都在**真公司**底下,都不是任务态。
* 判据必须是「认得出才删」,不是「不认识就删」。
*/
import { describe, it, expect } from 'vitest';
import fs from 'fs';
import os from 'os';
import path from 'path';
// 隔离库:不设的话 graph-db 的闸门会拦下测试进程,realCompanies() 走 catch 分支
// 返回空集合 —— 那时**一家公司都不认**,整套逻辑什么都不清。
// 那个 fail-closed 行为本身是对的(见下面单独一条),但会让其它用例全部空跑。
const TMP = fs.mkdtempSync(path.join(os.tmpdir(), 'lemonboss-cleanup-'));
process.env.GRAPH_DB_PATH = path.join(TMP, 'graph.sqlite');
const cleanup = require('../../services/task-state-cleanup');
describe('任务态清理 · 判据', () => {
it('② 白名单认得出真实存在的那几种任务态目录名', () => {
// 这些形状都是实查 system/agents 得到的,不是想出来的
for (const name of [
'run_9168bd24f46c', 'heartbeat-msy9yfdd', 'callback-T-msyu6u9b-939c',
'task-T-abc123', 'T-msyu6u9b-939c', 'conv-abc', 'tpl-train-x', '_no-task',
]) {
expect(cleanup.looksLikeTaskDir(name)).toBe(true);
}
});
it('② 白名单**不**认这些 —— 第一版正是在这里判错的', () => {
// venv / data 是真公司目录下的非任务态目录,实测被第一版黑名单判据选中过
for (const name of ['venv', 'data', 'memory', 'conversations', '.claude',
'workspace', 'node_modules', 'artifacts', 'projects']) {
expect(cleanup.looksLikeTaskDir(name)).toBe(false);
}
});
it('公司态永远在保护名单里(这一条红了 = dream 的原料要被删)', () => {
expect(cleanup.PROTECTED.has('memory')).toBe(true);
expect(cleanup.PROTECTED.has('conversations')).toBe(true);
});
it('读不到名册时一家公司都不认(fail-closed:宁可不清,也不赌)', () => {
const orig = process.env.GRAPH_DB_PATH;
delete process.env.GRAPH_DB_PATH; // 触发闸门 → listCompanyAgents 抛
try {
const r = cleanup.sweep({ maxAgeDays: 0 });
expect(r.candidates).toEqual([]);
expect(r.removed).toEqual([]);
} finally { process.env.GRAPH_DB_PATH = orig; }
});
it('真扫一遍:默认 14 天窗口下不删任何东西,且跳过原因说得清', () => {
const r = cleanup.sweep({ maxAgeDays: 14 });
expect(r.removed).toEqual([]); // dry-run 不删
expect(r.skipped.length).toBeGreaterThan(50); // 样本非空:真的扫到了东西
// 每条跳过都要有能读的原因 —— 「为什么没清」和「清了什么」一样值得看得见
for (const s of r.skipped.slice(0, 50)) expect(String(s.reason).length).toBeGreaterThan(4);
// 公司态一定出现在跳过里,且理由点名 §5.6b
const mem = r.skipped.find((s) => s.path.endsWith('/memory'));
expect(mem).toBeTruthy();
expect(mem.reason).toContain('公司态');
});
it('把窗口放到 0 天时确实能选出任务态目录,且**一条公司态都没混进来**', () => {
const r = cleanup.sweep({ maxAgeDays: 0 });
expect(r.candidates.length).toBeGreaterThan(0); // 样本非空,否则下面的负向断言是空跑
for (const c of r.candidates) {
const last = c.split('/').pop();
expect(cleanup.PROTECTED.has(last)).toBe(false);
expect(cleanup.looksLikeTaskDir(last)).toBe(true);
}
expect(r.removed).toEqual([]); // 仍然是 dry-run
});
});