test-id-stability.test.js
5.58 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
/**
* 元测试 —— 测试里不许用「随机 / 时间戳」拼 agent id 或公司 id(28 号 E2E 后的遗留项)
*
* ── 为什么这条要单独立一道防线 ────────────────────────────────────────────
* 28 号定下的产品决策:**测试在真实 `system/agents/` 下建出来的本体是正式注册表条目,
* 不删、不 ignore、进版本库**(复用得越多,agent 攒下的历史与经验越多)。
* 这条决策一旦成立,「id 每跑一次换一个」就从「无害的隔离手法」变成了**注册表泄漏**:
* `tmp-draft-role-1787029777338-8idwe` → 跑 10 次 = 10 个再也没人认领的僵尸本体,
* 而它们又是 `system/agents/` 全量遍历型用例(graduation-criteria-reality /
* template-verdicts / graph-seed-service)的输入,噪声会一路涨进断言里。
* 公司 id 同理:E2E 基线里就躺着 `tmp-w7-blank-1787029777338-8idwe` 这类 `data/companies/` 垃圾。
*
* 反过来,id 固定之后第二次跑正好走「本体已存在 → 复用」那条分支 ——
* 「第二家公司雇同一个角色」这条真实路径顺带被测到了,与决策本身同向。
*
* ── 判据为什么窄 ──────────────────────────────────────────────────────────
* 只认**「字面量前缀 + `-` + 随机源」这一种拼 id 的形状**(`'c-test-' + Math.random()`、
* `` `tmp-draft-role-${RUN_ID}` ``),随机源可以隔一层局部变量(RUN_ID 这种)。
* 时间戳本身不是罪:`Date.now() - 60_000` 造一个「一分钟前」的时刻、
* `/tmp/xxx-${Date.now()}.sqlite` 造一次性库文件、任务文本里加个去重 nonce,
* 都与注册表增长无关,判据必须放它们过去(含 `/` 的字面量整条跳过 = 路径不是 id)。
* 宽一档就会把这些一起判红,防线于是被人整条注释掉 —— 那比没有更糟。
*/
import { describe, it, expect } from 'vitest';
import fs from 'fs';
import path from 'path';
const TESTS_ROOT = path.resolve(__dirname, '..');
/** 随机源:这两个是本仓唯一用过的 */
const RAND = String.raw`(?:Date\.now\(\)|Math\.random\(\))`;
/**
* 拼 id 的形状。两种写法各一条分支,都要求 `-` 紧挨着随机源 —— 那个连字符正是
* 「前缀 + 唯一后缀」的签名:
* ① `'c-test-' + Math.random()...` / `'-' + Math.random()`(字符串加法)
* ② `` `tmp-draft-role-${RUN_ID}` ``(模板字面量)
* 字面量里出现 `/` 一律跳过:那是路径(`/tmp/test-benchmark-${Date.now()}.sqlite`),不是 id。
*/
const idConcatRe = (sources) => new RegExp(
String.raw`(?:'[^'\n/]*-'\s*\+\s*|"[^"\n/]*-"\s*\+\s*|\`[^\`\n/]*-\$\{\s*)(?:${sources})`,
'g',
);
/** 值里带随机源的局部变量:`const RUN_ID = \`${Date.now()}-${Math.random()...}\`` */
function taintedNames(src) {
const names = new Set();
const re = new RegExp(String.raw`(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*[^;\n]*${RAND}`, 'g');
let m;
while ((m = re.exec(src))) names.add(m[1]);
return [...names];
}
/** 一份源码里所有「随机拼 id」的行号 + 原文 */
function violationsIn(src, file) {
const sources = [RAND, ...taintedNames(src).map((n) => String.raw`${n}\b`)].join('|');
const re = idConcatRe(sources);
const out = [];
src.split('\n').forEach((line, i) => {
re.lastIndex = 0;
if (re.test(line)) out.push(`${file}:${i + 1} → ${line.trim()}`);
});
return out;
}
function testFiles(dir, out = []) {
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
const p = path.join(dir, e.name);
if (e.isDirectory()) testFiles(p, out);
// 本文件自己除外:它的注释与「判据自身有效」那条用例**必须**逐字带着反例,
// 否则判据就没法证明自己还抓得住东西 —— 唯一合法的例外,也只有这一份。
else if (e.name.endsWith('.test.js') && p !== __filename) out.push(p);
}
return out;
}
const FILES = testFiles(TESTS_ROOT);
describe('测试用的 agent / 公司 id 必须是固定的', () => {
it('扫到了测试文件(改目录结构后别静默变成空跑)', () => {
expect(FILES.length).toBeGreaterThan(50);
});
it('判据自身有效:这三种写法必须被抓出来,这四种必须放过', () => {
// 抓:不管随机源是直接写的还是隔了一层变量
const caught = [
'const RUN_ID = `${Date.now()}-x`;\nconst ROLE = `tmp-draft-role-${RUN_ID}`;',
"cid = 'c-test-' + Math.random().toString(36).slice(2, 7);",
"const cid = 'c-p3-' + name + '-' + Math.random().toString(36).slice(2, 7);",
];
for (const src of caught) {
expect(violationsIn(src, 'x'), src).not.toEqual([]);
}
// 放过:时间戳的正当用法 —— 相对时刻、一次性库文件路径、去重 nonce、纯序号
const passed = [
'createdAt: new Date(Date.now() - 60_000).toISOString(),',
'const TEST_DB_PATH = `/tmp/test-benchmark-${Date.now()}.sqlite`;',
'const RUN = `r${Date.now().toString(36)}`;\nawait dispatch(ALPHA, `写季度战略 ${RUN}`);',
"const cid = 'c-test-' + (++seq);",
];
for (const src of passed) {
expect(violationsIn(src, 'x'), src).toEqual([]);
}
});
it('server/tests 下没有一处用随机 / 时间戳拼 id', () => {
const bad = FILES.flatMap((f) =>
violationsIn(fs.readFileSync(f, 'utf-8'), path.relative(TESTS_ROOT, f)),
);
expect(bad).toEqual([]);
});
});