progmune-runtime 2.0.2 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.mcp.json +11 -0
- package/.progmune_allowlist +50 -0
- package/.test_report/test_report.md +87 -0
- package/Dockerfile +2 -10
- package/FAQ.md +167 -0
- package/demo-project/auth.ts +55 -0
- package/demo-project/tsconfig.json +8 -0
- package/dist/ab-stats.js +11 -0
- package/dist/acl-breakdown.js +13 -0
- package/dist/all-sessions.js +11 -0
- package/dist/antibody-stats.js +11 -0
- package/dist/audit.js +218 -0
- package/dist/benchmark-count.js +7 -0
- package/dist/benchmark-full.js +17 -0
- package/dist/benchmark-pass-rate.js +54 -0
- package/dist/benchmark-report.js +67 -0
- package/dist/benchmark-save.js +62 -0
- package/dist/benchmark-status.js +15 -0
- package/dist/branch-ledger.js +365 -0
- package/dist/branch-tree-count.js +14 -0
- package/dist/check.js +506 -0
- package/dist/common-fixpath.js +12 -0
- package/dist/constraint-types.js +12 -0
- package/dist/deterministic-replay.js +277 -0
- package/dist/emitter.js +112 -12
- package/dist/exec-metrics.js +11 -0
- package/dist/execute.js +242 -0
- package/dist/extract-ir.js +550 -5
- package/dist/failure-collector.js +143 -0
- package/dist/failure-corpus.js +481 -35
- package/dist/failure-report.js +11 -0
- package/dist/failures.js +11 -0
- package/dist/fast-path-hits.js +13 -0
- package/dist/feedback.js +6 -3
- package/dist/file-lock.js +82 -0
- package/dist/find-session.js +10 -0
- package/dist/fingerprint-list.js +15 -0
- package/dist/gen-history-log.js +13 -0
- package/dist/generate.js +2 -1
- package/dist/generate_500.js +4 -2
- package/dist/genome.js +11 -0
- package/dist/heatmap-data.js +11 -0
- package/dist/heatmap.js +11 -0
- package/dist/immune-reporter.js +137 -0
- package/dist/learned.js +11 -0
- package/dist/ledger-registry.js +241 -0
- package/dist/llm.js +43 -2
- package/dist/load-benchmarks.js +47 -0
- package/dist/main.js +2 -1
- package/dist/mcp-server.mjs +446 -34
- package/dist/memory-layer.js +51 -13
- package/dist/metrics.js +11 -0
- package/dist/obs-web.js +561 -0
- package/dist/p0_ssg_demo.js +255 -40
- package/dist/planner.js +933 -65
- package/dist/protocol-registry.js +105 -0
- package/dist/recent-session.js +12 -0
- package/dist/repair-proposal.js +353 -0
- package/dist/report.js +32 -0
- package/dist/runtime-invariants.js +161 -0
- package/dist/runtime-types.js +117 -0
- package/dist/search-planner.js +38 -9
- package/dist/semantic-snapshot.js +155 -0
- package/dist/semantic-trace.js +1497 -0
- package/dist/semantic-validator.js +3 -2
- package/dist/semantic_guard_test.js +2 -1
- package/dist/sessions.js +11 -0
- package/dist/ssg-validator.js +658 -20
- package/dist/svl-distribution.js +11 -0
- package/dist/terminal-status.js +11 -0
- package/dist/test_failure_corpus.js +3 -1
- package/dist/token-savings.js +11 -0
- package/dist/total-repairs.js +12 -0
- package/dist/unresolved-count.js +12 -0
- package/dist/valid-fingerprints.js +13 -0
- package/dist/validator.js +116 -60
- package/dist/verify-fps.js +11 -0
- package/dist/verify-ledgers.js +11 -0
- package/docs/whitepaper-style.css +77 -0
- package/docs/whitepaper-v2.1.md +609 -0
- package/docs/whitepaper-v2.2.md +1064 -0
- package/docs/whitepaper-v2.2.pdf +0 -0
- package/fly.toml +1 -1
- package/package.json +13 -4
- package/protocols.json +136 -0
- package/public/dashboard.html +119 -0
- package/readme.md +829 -0
- package/server/hub.js +84 -12
- package/test/replay-golden/sess_1780063202050_mgeld.json +9 -0
- package/test/replay-golden/sess_1780064032560_gocld.json +354 -0
- package/test/replay-golden/sess_1780064413331_s2709.json +606 -0
- package/test/replay-golden/sess_1780064792710_y3avo.json +614 -0
- package/test/replay-golden.ts +84 -0
- package/test_benchmark.js +165 -0
- package/test_comprehensive.mjs +638 -0
- package/test_concurrency.js +129 -0
- package/test_ir_robustness.js +85 -0
- package/test_semantic_contracts.js +269 -0
- package/test_ssg_stress.js +156 -0
- package/test_svl3.js +58 -0
- package/tsconfig.json +1 -1
- package/.env.example +0 -3
- package/.progmune_memory/fingerprints.json +0 -7
- package/.progmune_memory/opt_in.json +0 -4
- package/README.md +0 -118
- package/dist/mcp-server.js +0 -55
- package/immune_hub_data/2026-05-14.json +0 -50
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 并发安全测试
|
|
3
|
+
* ============
|
|
4
|
+
* 测试 feedback.json 和 Failure Corpus 在并发写入时的数据完整性
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
|
|
10
|
+
const PROJECT_ROOT = '/Users/shenlian/progmune-runtime';
|
|
11
|
+
const results = { pass: 0, fail: 0, total: 0, details: [] };
|
|
12
|
+
|
|
13
|
+
function assert(condition, name, detail) {
|
|
14
|
+
results.total++;
|
|
15
|
+
if (condition) { results.pass++; results.details.push({ name, status: '✅', detail }); }
|
|
16
|
+
else { results.fail++; results.details.push({ name, status: '❌', detail }); }
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async function main() {
|
|
20
|
+
console.log('🧪 并发安全测试\n');
|
|
21
|
+
|
|
22
|
+
// ============================================================
|
|
23
|
+
// 1. feedback.json 并发写入
|
|
24
|
+
// ============================================================
|
|
25
|
+
console.log('--- feedback.json 并发写入 ---');
|
|
26
|
+
|
|
27
|
+
const feedbackPath = path.join(PROJECT_ROOT, 'feedback.json');
|
|
28
|
+
const backup = fs.existsSync(feedbackPath) ? fs.readFileSync(feedbackPath, 'utf-8') : null;
|
|
29
|
+
fs.writeFileSync(feedbackPath, '[]');
|
|
30
|
+
|
|
31
|
+
const { recordRun } = require('./dist/feedback.js');
|
|
32
|
+
|
|
33
|
+
const concurrency = 20;
|
|
34
|
+
const promises = [];
|
|
35
|
+
for (let i = 0; i < concurrency; i++) {
|
|
36
|
+
promises.push(new Promise(resolve => {
|
|
37
|
+
setImmediate(() => {
|
|
38
|
+
try { recordRun(`test_concurrent_${i}`, [{ kind: 'call', function: `func_${i}`, args: [] }], true); } catch(e) {}
|
|
39
|
+
resolve();
|
|
40
|
+
});
|
|
41
|
+
}));
|
|
42
|
+
}
|
|
43
|
+
await Promise.all(promises);
|
|
44
|
+
await new Promise(r => setTimeout(r, 500));
|
|
45
|
+
|
|
46
|
+
const feedbackData = JSON.parse(fs.readFileSync(feedbackPath, 'utf-8'));
|
|
47
|
+
assert(feedbackData.length === concurrency, 'feedback.json: 并发 20 次写入不丢数据', `期望 ${concurrency} 条,实际 ${feedbackData.length} 条`);
|
|
48
|
+
if (backup) fs.writeFileSync(feedbackPath, backup);
|
|
49
|
+
|
|
50
|
+
// ============================================================
|
|
51
|
+
// 2. Failure Corpus 并发写入
|
|
52
|
+
// ============================================================
|
|
53
|
+
console.log('\n--- Failure Corpus 并发写入 ---');
|
|
54
|
+
|
|
55
|
+
const { recordFailure, getAllFailures } = require('./dist/failure-corpus.js');
|
|
56
|
+
|
|
57
|
+
const beforeCount = getAllFailures().length;
|
|
58
|
+
const concurrency2 = 30;
|
|
59
|
+
const promises2 = [];
|
|
60
|
+
|
|
61
|
+
for (let i = 0; i < concurrency2; i++) {
|
|
62
|
+
promises2.push(new Promise(resolve => {
|
|
63
|
+
setImmediate(() => {
|
|
64
|
+
try {
|
|
65
|
+
recordFailure({
|
|
66
|
+
intent: `concurrent_test_${i}`,
|
|
67
|
+
projectFunctions: ['f'],
|
|
68
|
+
violatedSVL: 'SVL-1',
|
|
69
|
+
constraintType: 'symbol_existence',
|
|
70
|
+
actionSequence: [],
|
|
71
|
+
errorDetail: 'concurrent test'
|
|
72
|
+
});
|
|
73
|
+
} catch(e) {}
|
|
74
|
+
resolve();
|
|
75
|
+
});
|
|
76
|
+
}));
|
|
77
|
+
}
|
|
78
|
+
await Promise.all(promises2);
|
|
79
|
+
await new Promise(r => setTimeout(r, 500));
|
|
80
|
+
|
|
81
|
+
const afterCount = getAllFailures().length;
|
|
82
|
+
assert(afterCount - beforeCount === concurrency2, 'Failure Corpus: 并发 30 次写入不丢数据', `期望新增 ${concurrency2} 条,实际新增 ${afterCount - beforeCount} 条`);
|
|
83
|
+
|
|
84
|
+
// ============================================================
|
|
85
|
+
// 3. 记忆系统并发写入
|
|
86
|
+
// ============================================================
|
|
87
|
+
console.log('\n--- 记忆系统并发写入 ---');
|
|
88
|
+
|
|
89
|
+
const { recordEpisode, getRecentEpisodes } = require('./dist/memory-layer.js');
|
|
90
|
+
|
|
91
|
+
const beforeEp = getRecentEpisodes(100).length;
|
|
92
|
+
const concurrency3 = 20;
|
|
93
|
+
const promises3 = [];
|
|
94
|
+
|
|
95
|
+
for (let i = 0; i < concurrency3; i++) {
|
|
96
|
+
promises3.push(new Promise(resolve => {
|
|
97
|
+
setImmediate(() => {
|
|
98
|
+
try {
|
|
99
|
+
recordEpisode({
|
|
100
|
+
intent: `concurrent_mem_${i}`,
|
|
101
|
+
actions: [{ kind: 'assign', target: 'x', value: '1' }],
|
|
102
|
+
success: true
|
|
103
|
+
});
|
|
104
|
+
} catch(e) {}
|
|
105
|
+
resolve();
|
|
106
|
+
});
|
|
107
|
+
}));
|
|
108
|
+
}
|
|
109
|
+
await Promise.all(promises3);
|
|
110
|
+
await new Promise(r => setTimeout(r, 500));
|
|
111
|
+
|
|
112
|
+
const afterEp = getRecentEpisodes(100).length;
|
|
113
|
+
assert(afterEp - beforeEp === concurrency3, '记忆系统: 并发 20 次写入不丢数据', `期望新增 ${concurrency3} 条,实际新增 ${afterEp - beforeEp} 条`);
|
|
114
|
+
|
|
115
|
+
// ============================================================
|
|
116
|
+
// 报告
|
|
117
|
+
// ============================================================
|
|
118
|
+
console.log('\n========================================');
|
|
119
|
+
console.log(`结果: ${results.pass}/${results.total} 通过`);
|
|
120
|
+
console.log('========================================\n');
|
|
121
|
+
|
|
122
|
+
results.details.forEach(d => console.log(` ${d.status} ${d.name}${d.detail ? ' (' + d.detail + ')' : ''}`));
|
|
123
|
+
|
|
124
|
+
const passRate = (results.pass / results.total * 100).toFixed(0);
|
|
125
|
+
console.log(`\n通过率: ${passRate}%`);
|
|
126
|
+
process.exit(results.fail > 0 ? 1 : 0);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
main().catch(console.error);
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const PROJECT_ROOT = '/Users/shenlian/progmune-runtime';
|
|
4
|
+
const results = { pass: 0, fail: 0, total: 0, details: [] };
|
|
5
|
+
function assert(c, n, d) { results.total++; if (c) { results.pass++; results.details.push({ n, s: '✅', d }); } else { results.fail++; results.details.push({ n, s: '❌', d }); } }
|
|
6
|
+
|
|
7
|
+
async function main() {
|
|
8
|
+
console.log('🧪 IR 提取鲁棒性测试\n');
|
|
9
|
+
|
|
10
|
+
// 1. IR 合并与协议注入
|
|
11
|
+
console.log('--- IR 合并与协议注入 ---');
|
|
12
|
+
function mergeProtocols(irArray) {
|
|
13
|
+
const pf = path.resolve(PROJECT_ROOT, 'protocols.json');
|
|
14
|
+
if (!fs.existsSync(pf)) return irArray;
|
|
15
|
+
const p = JSON.parse(fs.readFileSync(pf, 'utf-8'));
|
|
16
|
+
return irArray.map(fn => p[fn.name] ? { ...fn, protocol: p[fn.name] } : fn);
|
|
17
|
+
}
|
|
18
|
+
assert(Array.isArray(mergeProtocols([])) && mergeProtocols([]).length === 0, '空 IR: 返回空数组', '');
|
|
19
|
+
const tp = { test_func: { pre_states: ['READY'], post_states: ['DONE'] } };
|
|
20
|
+
fs.writeFileSync(path.join(PROJECT_ROOT, 'protocols.json'), JSON.stringify(tp));
|
|
21
|
+
const r1b = mergeProtocols([{ name: 'test_func', params: [] }]);
|
|
22
|
+
assert(r1b[0].protocol && r1b[0].protocol.pre_states[0] === 'READY', '协议注入: 匹配函数注入协议', '');
|
|
23
|
+
const r1c = mergeProtocols([{ name: 'no_match', params: [] }]);
|
|
24
|
+
assert(!r1c[0].protocol, '协议注入: 不匹配函数无协议', '');
|
|
25
|
+
try { fs.unlinkSync(path.join(PROJECT_ROOT, 'protocols.json')); } catch {}
|
|
26
|
+
|
|
27
|
+
// 2. IR 空值处理
|
|
28
|
+
console.log('\n--- IR 空值处理 ---');
|
|
29
|
+
[{ desc: '目录不存在', ir: [] }, { desc: '无 .py 文件', ir: [] }].forEach(s => assert(Array.isArray(s.ir) && s.ir.length === 0, `IR 空值: ${s.desc}`, ''));
|
|
30
|
+
|
|
31
|
+
// 3. 大 IR 性能
|
|
32
|
+
console.log('\n--- 大 IR 性能 ---');
|
|
33
|
+
const largeIr = [];
|
|
34
|
+
for (let i = 0; i < 1000; i++) largeIr.push({ name: `func_${i}`, params: [{ n: 'a', t: 'str' }], returnType: 'bool', file: 'm.py' });
|
|
35
|
+
fs.writeFileSync(path.join(PROJECT_ROOT, 'ir.json'), JSON.stringify(largeIr));
|
|
36
|
+
Object.keys(require.cache).filter(k => k.includes('validator')).forEach(k => delete require.cache[k]);
|
|
37
|
+
const { validateActionSequence: v } = require('./dist/validator.js');
|
|
38
|
+
const start = Date.now();
|
|
39
|
+
assert(v([{ kind: 'call', function: 'func_500', args: [{ name: 'a', type: 'str', value: 'x' }] }]).valid, '大 IR: 1000 函数中查找通过', '');
|
|
40
|
+
assert(!v([{ kind: 'call', function: 'nonexistent', args: [] }]).valid, '大 IR: 拦截不存在函数', '');
|
|
41
|
+
assert(Date.now() - start < 500, '大 IR: 查找耗时 < 500ms', `${Date.now() - start}ms`);
|
|
42
|
+
|
|
43
|
+
// 4. Planner 降级回退
|
|
44
|
+
console.log('\n--- Planner 降级回退 ---');
|
|
45
|
+
const origKey = process.env.LLM_API_KEY;
|
|
46
|
+
process.env.LLM_API_KEY = '';
|
|
47
|
+
// 清空记忆文件确保不命中语义模板
|
|
48
|
+
try { fs.writeFileSync(path.join(PROJECT_ROOT, '.progmune_memory/episodic.json'), '[]'); } catch {}
|
|
49
|
+
try { fs.writeFileSync(path.join(PROJECT_ROOT, '.progmune_memory/semantic.json'), '[]'); } catch {}
|
|
50
|
+
fs.writeFileSync(path.join(PROJECT_ROOT, 'ir.json'), JSON.stringify([]));
|
|
51
|
+
Object.keys(require.cache).filter(k => k.includes('planner') || k.includes('memory')).forEach(k => delete require.cache[k]);
|
|
52
|
+
const { plan: planEmpty } = require('./dist/planner.js');
|
|
53
|
+
try { const r = await planEmpty('test'); assert(Array.isArray(r) && r.length === 0, '降级: 空 IR 返回空数组', ''); } catch(e) { assert(false, '降级: 空 IR 不抛异常', e.message); }
|
|
54
|
+
fs.writeFileSync(path.join(PROJECT_ROOT, 'ir.json'), JSON.stringify([{ name: 'login', params: [{ name: 'u', type: 'str' }], returnType: 'bool' }]));
|
|
55
|
+
Object.keys(require.cache).filter(k => k.includes('planner')).forEach(k => delete require.cache[k]);
|
|
56
|
+
const { plan: planWith } = require('./dist/planner.js');
|
|
57
|
+
try { const r = await planWith('implement login'); assert(Array.isArray(r) && r.length > 0, '降级: 匹配意图生成动作', `生成了 ${r.length} 个`); assert(r[0].kind === 'call', '降级: 动作为 call 类型', r[0].kind); } catch(e) { assert(false, '降级: 不抛异常', e.message); }
|
|
58
|
+
process.env.LLM_API_KEY = origKey;
|
|
59
|
+
|
|
60
|
+
// 5. executeActionCode 边界
|
|
61
|
+
console.log('\n--- executeActionCode 边界 ---');
|
|
62
|
+
const { executeActionCode } = require('./dist/action-runtime.js');
|
|
63
|
+
assert(Array.isArray(executeActionCode('')), '空代码返回数组', '');
|
|
64
|
+
assert(Array.isArray(executeActionCode('assign("x","1")')) && executeActionCode('assign("x","1")').length === 1, '简单赋值', '');
|
|
65
|
+
assert(executeActionCode('{{{') === null, '无效语法返回 null', '');
|
|
66
|
+
const cmp = executeActionCode(['assign("u","i")','callAssign("get","p","u")','ifElse("p",()=>{output("p")},()=>{output("nf")})'].join('\n'));
|
|
67
|
+
assert(cmp !== null && cmp.length >= 3, '复杂嵌套', `${cmp ? cmp.length : 0} 个动作`);
|
|
68
|
+
const dp = executeActionCode(['assign("a","1")','assign("b","2")','assign("c","3")','assign("d","4")','assign("e","5")','ifElse("a",()=>{ifElse("b",()=>{ifElse("c",()=>{ifElse("d",()=>{ifElse("e",()=>{output("deep")},()=>{})},()=>{})},()=>{})},()=>{})},()=>{})'].join('\n'));
|
|
69
|
+
assert(dp !== null, '5 层嵌套不抛异常', '');
|
|
70
|
+
|
|
71
|
+
// 6. 语义模板
|
|
72
|
+
console.log('\n--- 语义模板快速通道 ---');
|
|
73
|
+
const { findSemanticTemplate, consolidateSemantic, recordEpisode } = require('./dist/memory-layer.js');
|
|
74
|
+
assert(findSemanticTemplate('brand new intent') === undefined, '无匹配返回 undefined', '');
|
|
75
|
+
for (let i = 0; i < 5; i++) recordEpisode({ intent: 'repeated pattern', actions: [{ kind: 'assign', target: 'x', value: '1' }], success: true });
|
|
76
|
+
consolidateSemantic(3);
|
|
77
|
+
const t = findSemanticTemplate('repeated pattern with extra');
|
|
78
|
+
assert(t !== undefined, '重复模式被巩固', t ? `id: ${t.id}` : '');
|
|
79
|
+
|
|
80
|
+
console.log(`\n结果: ${results.pass}/${results.total} 通过`);
|
|
81
|
+
results.details.forEach(d => console.log(` ${d.s} ${d.n}${d.d ? ' (' + d.d + ')' : ''}`));
|
|
82
|
+
console.log(`通过率: ${(results.pass/results.total*100).toFixed(0)}%`);
|
|
83
|
+
process.exit(results.fail > 0 ? 1 : 0);
|
|
84
|
+
}
|
|
85
|
+
main().catch(console.error);
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 语义合约专项测试
|
|
3
|
+
* ==================
|
|
4
|
+
* 覆盖四种合约类型的全部边界情况
|
|
5
|
+
*
|
|
6
|
+
* checkSemantic 返回语义:
|
|
7
|
+
* valid = true → 语义检查通过(无合约违规)
|
|
8
|
+
* valid = false → 存在合约违规
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const path = require('path');
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
|
|
14
|
+
const PROJECT_ROOT = '/Users/shenlian/progmune-runtime';
|
|
15
|
+
const results = { pass: 0, fail: 0, total: 0, details: [] };
|
|
16
|
+
|
|
17
|
+
function assert(condition, name, detail) {
|
|
18
|
+
results.total++;
|
|
19
|
+
if (condition) { results.pass++; results.details.push({ name, status: '✅', detail }); }
|
|
20
|
+
else { results.fail++; results.details.push({ name, status: '❌', detail }); }
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function writeIr(ir) {
|
|
24
|
+
fs.writeFileSync(path.join(PROJECT_ROOT, 'ir.json'), JSON.stringify(ir, null, 2));
|
|
25
|
+
delete require.cache[require.resolve('./dist/semantic-validator.js')];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function reloadChecker() {
|
|
29
|
+
delete require.cache[require.resolve('./dist/semantic-validator.js')];
|
|
30
|
+
return require('./dist/semantic-validator.js').checkSemantic;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
console.log('🧪 语义合约专项测试\n');
|
|
34
|
+
|
|
35
|
+
// ============================================================
|
|
36
|
+
// 1. must_be_checked
|
|
37
|
+
// ============================================================
|
|
38
|
+
console.log('--- must_be_checked ---');
|
|
39
|
+
|
|
40
|
+
let checkSemantic = reloadChecker();
|
|
41
|
+
|
|
42
|
+
writeIr([{
|
|
43
|
+
name: 'verify_password',
|
|
44
|
+
contracts: [{ type: 'must_be_checked', description: '密码验证结果必须用于条件判断' }]
|
|
45
|
+
}]);
|
|
46
|
+
|
|
47
|
+
// 1a. 返回值用于 if → valid true(合约满足)
|
|
48
|
+
const r1a = checkSemantic('login', [
|
|
49
|
+
{ kind: 'call', function: 'verify_password', args: [], assignTo: 'ok' },
|
|
50
|
+
{ kind: 'if', condition: 'ok', thenActions: [] }
|
|
51
|
+
]);
|
|
52
|
+
assert(r1a.valid === true, 'must_be_checked: 返回值用于 if 通过', '');
|
|
53
|
+
|
|
54
|
+
// 1b. 返回值未用于 if → valid false(合约违规)
|
|
55
|
+
const r1b = checkSemantic('login', [
|
|
56
|
+
{ kind: 'call', function: 'verify_password', args: [], assignTo: 'ok' }
|
|
57
|
+
]);
|
|
58
|
+
assert(r1b.valid === false && r1b.errors.some(e => e.includes('verify_password')), 'must_be_checked: 返回值未用于 if 拦截', r1b.errors.join('; '));
|
|
59
|
+
|
|
60
|
+
// 1c. 未使用 assignTo 保存返回值 → valid false
|
|
61
|
+
const r1c = checkSemantic('login', [
|
|
62
|
+
{ kind: 'call', function: 'verify_password', args: [] }
|
|
63
|
+
]);
|
|
64
|
+
assert(r1c.valid === false && r1c.errors.some(e => e.includes('未使用assignTo')), 'must_be_checked: 未使用 assignTo 拦截', r1c.errors.join('; '));
|
|
65
|
+
|
|
66
|
+
// ============================================================
|
|
67
|
+
// 2. sequence_after
|
|
68
|
+
// ============================================================
|
|
69
|
+
console.log('\n--- sequence_after ---');
|
|
70
|
+
|
|
71
|
+
checkSemantic = reloadChecker();
|
|
72
|
+
|
|
73
|
+
writeIr([{
|
|
74
|
+
name: 'generate_jwt',
|
|
75
|
+
contracts: [{ type: 'sequence_after', function: 'verify_password', description: '签发令牌前必须先验证密码' }]
|
|
76
|
+
}]);
|
|
77
|
+
|
|
78
|
+
// 2a. verify_password 在 generate_jwt 之前 → valid true
|
|
79
|
+
const r2a = checkSemantic('login', [
|
|
80
|
+
{ kind: 'call', function: 'verify_password', args: [], assignTo: 'ok' },
|
|
81
|
+
{ kind: 'if', condition: 'ok', thenActions: [
|
|
82
|
+
{ kind: 'call', function: 'generate_jwt', args: [] }
|
|
83
|
+
]}
|
|
84
|
+
]);
|
|
85
|
+
assert(r2a.valid === true, 'sequence_after: 先验证后签发通过', '');
|
|
86
|
+
|
|
87
|
+
// 2b. generate_jwt 在 verify_password 之前 → valid false
|
|
88
|
+
const r2b = checkSemantic('login', [
|
|
89
|
+
{ kind: 'call', function: 'generate_jwt', args: [] },
|
|
90
|
+
{ kind: 'call', function: 'verify_password', args: [], assignTo: 'ok' }
|
|
91
|
+
]);
|
|
92
|
+
assert(r2b.valid === false && r2b.errors.some(e => e.includes('generate_jwt')), 'sequence_after: 跳过验证直接签发拦截', r2b.errors.join('; '));
|
|
93
|
+
|
|
94
|
+
// 2c. verify_password 从未被调用 → valid false
|
|
95
|
+
const r2c = checkSemantic('login', [
|
|
96
|
+
{ kind: 'call', function: 'generate_jwt', args: [] }
|
|
97
|
+
]);
|
|
98
|
+
assert(r2c.valid === false && r2c.errors.some(e => e.includes('generate_jwt')), 'sequence_after: 缺少前置函数拦截', r2c.errors.join('; '));
|
|
99
|
+
|
|
100
|
+
// 2d. 链式依赖 A→B→C(C 需要在 B 之后,但 B 在 C 之后出现)
|
|
101
|
+
checkSemantic = reloadChecker();
|
|
102
|
+
writeIr([{
|
|
103
|
+
name: 'C',
|
|
104
|
+
contracts: [{ type: 'sequence_after', function: 'B', description: 'C 必须在 B 之后' }]
|
|
105
|
+
}]);
|
|
106
|
+
const r2d = checkSemantic('test', [
|
|
107
|
+
{ kind: 'call', function: 'A', args: [] },
|
|
108
|
+
{ kind: 'call', function: 'C', args: [] },
|
|
109
|
+
{ kind: 'call', function: 'B', args: [] }
|
|
110
|
+
]);
|
|
111
|
+
assert(r2d.valid === false && r2d.errors.some(e => e.includes('C')), 'sequence_after: 链式依赖 A→B→C 拦截', r2d.errors.join('; '));
|
|
112
|
+
|
|
113
|
+
// ============================================================
|
|
114
|
+
// 3. param_from
|
|
115
|
+
// ============================================================
|
|
116
|
+
console.log('\n--- param_from ---');
|
|
117
|
+
|
|
118
|
+
checkSemantic = reloadChecker();
|
|
119
|
+
|
|
120
|
+
writeIr([{
|
|
121
|
+
name: 'create_session',
|
|
122
|
+
params: [{ name: 'token', type: 'str' }],
|
|
123
|
+
contracts: [{ type: 'param_from', param: 'token', function: 'generate_jwt', description: 'session 的 token 必须来自 generate_jwt' }]
|
|
124
|
+
}]);
|
|
125
|
+
|
|
126
|
+
// 3a. token 来自 generate_jwt 的输出 → valid true
|
|
127
|
+
const r3a = checkSemantic('login', [
|
|
128
|
+
{ kind: 'call', function: 'generate_jwt', args: [], assignTo: 'jwt' },
|
|
129
|
+
{ kind: 'call', function: 'create_session', args: [{ name: 'token', type: 'str', value: 'jwt' }] }
|
|
130
|
+
]);
|
|
131
|
+
assert(r3a.valid === true, 'param_from: token 来自 generate_jwt 通过', '');
|
|
132
|
+
|
|
133
|
+
// 3b. token 不来自 generate_jwt → valid false
|
|
134
|
+
const r3b = checkSemantic('login', [
|
|
135
|
+
{ kind: 'call', function: 'generate_jwt', args: [], assignTo: 'other' },
|
|
136
|
+
{ kind: 'call', function: 'create_session', args: [{ name: 'token', type: 'str', value: 'manual_token' }] }
|
|
137
|
+
]);
|
|
138
|
+
assert(r3b.valid === false && r3b.errors.some(e => e.includes('create_session')), 'param_from: token 不来自 generate_jwt 拦截', r3b.errors.join('; '));
|
|
139
|
+
|
|
140
|
+
// 3c. generate_jwt 从未被调用 → valid false
|
|
141
|
+
const r3c = checkSemantic('login', [
|
|
142
|
+
{ kind: 'call', function: 'create_session', args: [{ name: 'token', type: 'str', value: 'jwt' }] }
|
|
143
|
+
]);
|
|
144
|
+
assert(r3c.valid === false && r3c.errors.some(e => e.includes('create_session')), 'param_from: generate_jwt 未调用拦截', r3c.errors.join('; '));
|
|
145
|
+
|
|
146
|
+
// 3d. 多级传递 A→B→C(C 的 input 必须来自 B,但实际来自 A)
|
|
147
|
+
checkSemantic = reloadChecker();
|
|
148
|
+
writeIr([{
|
|
149
|
+
name: 'C',
|
|
150
|
+
params: [{ name: 'input', type: 'str' }],
|
|
151
|
+
contracts: [{ type: 'param_from', param: 'input', function: 'B', description: 'C 的 input 必须来自 B' }]
|
|
152
|
+
}]);
|
|
153
|
+
const r3d = checkSemantic('test', [
|
|
154
|
+
{ kind: 'call', function: 'A', args: [], assignTo: 'a_val' },
|
|
155
|
+
{ kind: 'call', function: 'C', args: [{ name: 'input', type: 'str', value: 'a_val' }] }
|
|
156
|
+
]);
|
|
157
|
+
assert(r3d.valid === false && r3d.errors.some(e => e.includes('C')), 'param_from: 多级传递 A→C 跳过 B 拦截', r3d.errors.join('; '));
|
|
158
|
+
|
|
159
|
+
// ============================================================
|
|
160
|
+
// 4. when_intent
|
|
161
|
+
// ============================================================
|
|
162
|
+
console.log('\n--- when_intent ---');
|
|
163
|
+
|
|
164
|
+
checkSemantic = reloadChecker();
|
|
165
|
+
|
|
166
|
+
writeIr([{
|
|
167
|
+
name: 'send_otp',
|
|
168
|
+
contracts: [{ type: 'must_be_checked', when_intent: ['login', 'signin'], description: '登录场景下 OTP 结果必须检查' }]
|
|
169
|
+
}]);
|
|
170
|
+
|
|
171
|
+
// 4a. 意图匹配时合约生效 → valid false
|
|
172
|
+
const r4a = checkSemantic('user login flow', [
|
|
173
|
+
{ kind: 'call', function: 'send_otp', args: [], assignTo: 'code' }
|
|
174
|
+
]);
|
|
175
|
+
assert(r4a.valid === false && r4a.errors.some(e => e.includes('send_otp')), 'when_intent: 意图匹配时合约生效', r4a.errors.join('; '));
|
|
176
|
+
|
|
177
|
+
// 4b. 意图不匹配时合约静默跳过 → valid true
|
|
178
|
+
const r4b = checkSemantic('query report data', [
|
|
179
|
+
{ kind: 'call', function: 'send_otp', args: [], assignTo: 'code' }
|
|
180
|
+
]);
|
|
181
|
+
assert(r4b.valid === true, 'when_intent: 意图不匹配时合约跳过', '');
|
|
182
|
+
|
|
183
|
+
// 4c. when_intent 未设置时始终生效
|
|
184
|
+
checkSemantic = reloadChecker();
|
|
185
|
+
writeIr([{
|
|
186
|
+
name: 'check_access',
|
|
187
|
+
contracts: [{ type: 'must_be_checked', description: '访问检查结果必须用于条件判断' }]
|
|
188
|
+
}]);
|
|
189
|
+
const r4c = checkSemantic('any intent here', [
|
|
190
|
+
{ kind: 'call', function: 'check_access', args: [], assignTo: 'result' }
|
|
191
|
+
]);
|
|
192
|
+
assert(r4c.valid === false && r4c.errors.some(e => e.includes('check_access')), 'when_intent: 未设置时始终生效', r4c.errors.join('; '));
|
|
193
|
+
|
|
194
|
+
// ============================================================
|
|
195
|
+
// 5. require_param
|
|
196
|
+
// ============================================================
|
|
197
|
+
console.log('\n--- require_param ---');
|
|
198
|
+
|
|
199
|
+
checkSemantic = reloadChecker();
|
|
200
|
+
|
|
201
|
+
writeIr([{
|
|
202
|
+
name: 'send_email',
|
|
203
|
+
params: [{ name: 'to', type: 'str' }],
|
|
204
|
+
contracts: [{ type: 'require_param', param: 'to', not_empty: true, description: '收件人地址不能为空' }]
|
|
205
|
+
}]);
|
|
206
|
+
|
|
207
|
+
// 5a. 参数为空模板 → valid false
|
|
208
|
+
const r5a = checkSemantic('send', [
|
|
209
|
+
{ kind: 'call', function: 'send_email', args: [{ name: 'to', type: 'str', value: '{}' }] }
|
|
210
|
+
]);
|
|
211
|
+
assert(r5a.valid === false && r5a.errors.some(e => e.includes('send_email')), 'require_param: 空模板参数拦截', r5a.errors.join('; '));
|
|
212
|
+
|
|
213
|
+
// 5b. 参数为空字符串 → valid false
|
|
214
|
+
const r5b = checkSemantic('send', [
|
|
215
|
+
{ kind: 'call', function: 'send_email', args: [{ name: 'to', type: 'str', value: '' }] }
|
|
216
|
+
]);
|
|
217
|
+
assert(r5b.valid === false && r5b.errors.some(e => e.includes('send_email')), 'require_param: 空字符串拦截', r5b.errors.join('; '));
|
|
218
|
+
|
|
219
|
+
// 5c. 参数正常 → valid true
|
|
220
|
+
const r5c = checkSemantic('send', [
|
|
221
|
+
{ kind: 'call', function: 'send_email', args: [{ name: 'to', type: 'str', value: 'user@example.com' }] }
|
|
222
|
+
]);
|
|
223
|
+
assert(r5c.valid === true, 'require_param: 正常参数通过', '');
|
|
224
|
+
|
|
225
|
+
// ============================================================
|
|
226
|
+
// 6. 组合合约
|
|
227
|
+
// ============================================================
|
|
228
|
+
console.log('\n--- 组合合约 ---');
|
|
229
|
+
|
|
230
|
+
checkSemantic = reloadChecker();
|
|
231
|
+
|
|
232
|
+
writeIr([{
|
|
233
|
+
name: 'process_payment',
|
|
234
|
+
params: [{ name: 'amount', type: 'int' }],
|
|
235
|
+
contracts: [
|
|
236
|
+
{ type: 'sequence_after', function: 'validate_user', description: '支付前必须验证用户' },
|
|
237
|
+
{ type: 'must_be_checked', description: '支付结果必须检查' }
|
|
238
|
+
]
|
|
239
|
+
}]);
|
|
240
|
+
|
|
241
|
+
// 6a. 两个合约都违规 → valid false
|
|
242
|
+
const r6a = checkSemantic('pay', [
|
|
243
|
+
{ kind: 'call', function: 'process_payment', args: [{ name: 'amount', type: 'int', value: 100 }], assignTo: 'pay_result' }
|
|
244
|
+
]);
|
|
245
|
+
assert(r6a.valid === false && r6a.errors.length >= 1, '组合合约: 两个都违规拦截', r6a.errors.join('; '));
|
|
246
|
+
|
|
247
|
+
// 6b. 两个合约都满足 → valid true
|
|
248
|
+
const r6b = checkSemantic('pay', [
|
|
249
|
+
{ kind: 'call', function: 'validate_user', args: [] },
|
|
250
|
+
{ kind: 'call', function: 'process_payment', args: [{ name: 'amount', type: 'int', value: 100 }], assignTo: 'pay_result' },
|
|
251
|
+
{ kind: 'if', condition: 'pay_result', thenActions: [] }
|
|
252
|
+
]);
|
|
253
|
+
assert(r6b.valid === true, '组合合约: 都满足时通过', '');
|
|
254
|
+
|
|
255
|
+
// ============================================================
|
|
256
|
+
// 报告
|
|
257
|
+
// ============================================================
|
|
258
|
+
console.log('\n========================================');
|
|
259
|
+
console.log(`结果: ${results.pass}/${results.total} 通过`);
|
|
260
|
+
console.log('========================================\n');
|
|
261
|
+
|
|
262
|
+
results.details.forEach(d => {
|
|
263
|
+
console.log(` ${d.status} ${d.name}`);
|
|
264
|
+
if (d.detail) console.log(` ${d.detail}`);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
const passRate = (results.pass / results.total * 100).toFixed(0);
|
|
268
|
+
console.log(`\n通过率: ${passRate}%`);
|
|
269
|
+
process.exit(results.fail > 0 ? 1 : 0);
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SSG 状态机压力测试
|
|
3
|
+
* ==================
|
|
4
|
+
* 测试大规模状态图下的性能
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const { StateMachineValidator } = require('./dist/ssg-validator.js');
|
|
8
|
+
|
|
9
|
+
const results = { pass: 0, fail: 0, total: 0, details: [] };
|
|
10
|
+
|
|
11
|
+
function assert(condition, name, detail) {
|
|
12
|
+
results.total++;
|
|
13
|
+
if (condition) { results.pass++; results.details.push({ name, status: '✅', detail }); }
|
|
14
|
+
else { results.fail++; results.details.push({ name, status: '❌', detail }); }
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
console.log('🧪 SSG 状态机压力测试\n');
|
|
18
|
+
|
|
19
|
+
// ============================================================
|
|
20
|
+
// 1. 大规模状态图(20 个状态,30 条跃迁)
|
|
21
|
+
// ============================================================
|
|
22
|
+
console.log('--- 大规模状态图 (20 states, 30 transitions) ---');
|
|
23
|
+
|
|
24
|
+
const states = [];
|
|
25
|
+
for (let i = 0; i < 20; i++) states.push(`S${i}`);
|
|
26
|
+
|
|
27
|
+
const protocols = [];
|
|
28
|
+
for (let i = 0; i < 19; i++) {
|
|
29
|
+
protocols.push({
|
|
30
|
+
function: `step_${i}`,
|
|
31
|
+
protocol: { pre_states: [states[i]], post_states: [states[i+1]], invalidate: [states[i]] }
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
// 添加一些跨层跃迁
|
|
35
|
+
protocols.push({
|
|
36
|
+
function: 'jump',
|
|
37
|
+
protocol: { pre_states: ['S0'], post_states: ['S10'], invalidate: ['S0'] }
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const start = Date.now();
|
|
41
|
+
const ssv = new StateMachineValidator(protocols, 'S0');
|
|
42
|
+
for (let i = 0; i < 19; i++) {
|
|
43
|
+
const r = ssv.apply(`step_${i}`);
|
|
44
|
+
if (!r.valid) { console.log(` 失败于 step_${i}: ${r.error}`); break; }
|
|
45
|
+
}
|
|
46
|
+
const linearTime = Date.now() - start;
|
|
47
|
+
assert(linearTime < 100, '大规模状态图: 线性跃迁耗时 < 100ms', `${linearTime}ms`);
|
|
48
|
+
|
|
49
|
+
// ============================================================
|
|
50
|
+
// 2. 非法跃迁检测性能(1000 次调用)
|
|
51
|
+
// ============================================================
|
|
52
|
+
console.log('\n--- 非法跃迁检测 (1000次) ---');
|
|
53
|
+
|
|
54
|
+
const ssv2 = new StateMachineValidator(protocols, 'S0');
|
|
55
|
+
const start2 = Date.now();
|
|
56
|
+
let illegalCount = 0;
|
|
57
|
+
for (let i = 0; i < 1000; i++) {
|
|
58
|
+
const r = ssv2.apply('jump');
|
|
59
|
+
if (!r.valid) illegalCount++;
|
|
60
|
+
}
|
|
61
|
+
const illegalTime = Date.now() - start2;
|
|
62
|
+
assert(illegalTime < 500, '非法跃迁检测: 1000 次耗时 < 500ms', `${illegalTime}ms, 拦截 ${illegalCount} 次`);
|
|
63
|
+
|
|
64
|
+
// ============================================================
|
|
65
|
+
// 3. findMissingSteps 性能(复杂图)
|
|
66
|
+
// ============================================================
|
|
67
|
+
console.log('\n--- findMissingSteps 性能 ---');
|
|
68
|
+
|
|
69
|
+
// 构建分支状态图:S0 → S1/S2 → S3/S4 → S5
|
|
70
|
+
const branchProtocols = [
|
|
71
|
+
{ function: 'to_S1', protocol: { pre_states: ['S0'], post_states: ['S1'], invalidate: ['S0'] } },
|
|
72
|
+
{ function: 'to_S2', protocol: { pre_states: ['S0'], post_states: ['S2'], invalidate: ['S0'] } },
|
|
73
|
+
{ function: 'to_S3', protocol: { pre_states: ['S1'], post_states: ['S3'], invalidate: ['S1'] } },
|
|
74
|
+
{ function: 'to_S4', protocol: { pre_states: ['S2'], post_states: ['S4'], invalidate: ['S2'] } },
|
|
75
|
+
{ function: 'to_S5', protocol: { pre_states: ['S3', 'S4'], post_states: ['S5'] } },
|
|
76
|
+
// 非法跃迁(用于触发 findMissingSteps)
|
|
77
|
+
{ function: 'illegal_final', protocol: { pre_states: ['S5'], post_states: ['DONE'] } },
|
|
78
|
+
];
|
|
79
|
+
|
|
80
|
+
const ssv3 = new StateMachineValidator(branchProtocols, 'S0');
|
|
81
|
+
const start3 = Date.now();
|
|
82
|
+
const r3 = ssv3.apply('illegal_final');
|
|
83
|
+
const missingTime = Date.now() - start3;
|
|
84
|
+
assert(r3.valid === false && missingTime < 50, 'findMissingSteps: 分支图耗时 < 50ms', `${missingTime}ms`);
|
|
85
|
+
|
|
86
|
+
// ============================================================
|
|
87
|
+
// 4. 空规则处理
|
|
88
|
+
// ============================================================
|
|
89
|
+
console.log('\n--- 空规则处理 ---');
|
|
90
|
+
|
|
91
|
+
const ssv4 = new StateMachineValidator([], 'INIT');
|
|
92
|
+
const r4 = ssv4.apply('any_function');
|
|
93
|
+
assert(r4.valid === true, '空规则: 任意函数通过', '');
|
|
94
|
+
|
|
95
|
+
// ============================================================
|
|
96
|
+
// 5. 超大规则集(100 条规则)
|
|
97
|
+
// ============================================================
|
|
98
|
+
console.log('\n--- 超大规则集 (100 rules) ---');
|
|
99
|
+
|
|
100
|
+
const manyRules = [];
|
|
101
|
+
for (let i = 0; i < 100; i++) {
|
|
102
|
+
manyRules.push({
|
|
103
|
+
function: `func_${i}`,
|
|
104
|
+
protocol: { pre_states: [`PRE_${i}`], post_states: [`POST_${i}`] }
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
const start5 = Date.now();
|
|
108
|
+
const ssv5 = new StateMachineValidator(manyRules, 'PRE_0');
|
|
109
|
+
const r5a = ssv5.apply('func_0');
|
|
110
|
+
const r5b = ssv5.apply('func_99');
|
|
111
|
+
const manyTime = Date.now() - start5;
|
|
112
|
+
assert(r5a.valid === true && r5b.valid === false, '超大规则集: 100 规则初始化+2次调用', `${manyTime}ms`);
|
|
113
|
+
|
|
114
|
+
// ============================================================
|
|
115
|
+
// 6. 状态膨胀(同时处于多个状态)
|
|
116
|
+
// ============================================================
|
|
117
|
+
console.log('\n--- 多状态并发 ---');
|
|
118
|
+
|
|
119
|
+
// 注意:grant_role_a/b/c 不移除 INIT,因为需要允许多角色同时存在
|
|
120
|
+
const multiStateProtocols = [
|
|
121
|
+
{ function: 'grant_role_a', protocol: { pre_states: ['INIT'], post_states: ['ROLE_A'] } },
|
|
122
|
+
{ function: 'grant_role_b', protocol: { pre_states: ['INIT'], post_states: ['ROLE_B'] } },
|
|
123
|
+
{ function: 'grant_role_c', protocol: { pre_states: ['INIT'], post_states: ['ROLE_C'] } },
|
|
124
|
+
{ function: 'access_admin', protocol: { pre_states: ['ROLE_A', 'ROLE_B'], post_states: ['ADMIN'] } },
|
|
125
|
+
];
|
|
126
|
+
const ssv6 = new StateMachineValidator(multiStateProtocols, 'INIT');
|
|
127
|
+
ssv6.apply('grant_role_a');
|
|
128
|
+
ssv6.apply('grant_role_b');
|
|
129
|
+
const r6a = ssv6.apply('access_admin');
|
|
130
|
+
assert(r6a.valid === true, '多状态并发: 同时拥有 ROLE_A+ROLE_B 可访问 admin', 'states: ' + ssv6.getCurrentStates().join(','));
|
|
131
|
+
|
|
132
|
+
const ssv6b = new StateMachineValidator(multiStateProtocols, 'INIT');
|
|
133
|
+
ssv6b.apply('grant_role_a');
|
|
134
|
+
const r6b = ssv6b.apply('access_admin');
|
|
135
|
+
assert(r6b.valid === false, '多状态并发: 只有 ROLE_A 无法访问 admin', '需要 [ROLE_A,ROLE_B],当前状态: ' + ssv6b.getCurrentStates().join(','));
|
|
136
|
+
|
|
137
|
+
// 验证所有三个角色都授予后才能访问
|
|
138
|
+
const ssv6c = new StateMachineValidator(multiStateProtocols, 'INIT');
|
|
139
|
+
ssv6c.apply('grant_role_a');
|
|
140
|
+
ssv6c.apply('grant_role_b');
|
|
141
|
+
ssv6c.apply('grant_role_c');
|
|
142
|
+
const r6c = ssv6c.apply('access_admin');
|
|
143
|
+
assert(r6c.valid === true, '多状态并发: ROLE_A+ROLE_B+ROLE_C 可访问 admin', 'states: ' + ssv6c.getCurrentStates().join(','));
|
|
144
|
+
|
|
145
|
+
// ============================================================
|
|
146
|
+
// 报告
|
|
147
|
+
// ============================================================
|
|
148
|
+
console.log('\n========================================');
|
|
149
|
+
console.log(`结果: ${results.pass}/${results.total} 通过`);
|
|
150
|
+
console.log('========================================\n');
|
|
151
|
+
|
|
152
|
+
results.details.forEach(d => console.log(` ${d.status} ${d.name}${d.detail ? ' (' + d.detail + ')' : ''}`));
|
|
153
|
+
|
|
154
|
+
const passRate = (results.pass / results.total * 100).toFixed(0);
|
|
155
|
+
console.log(`\n通过率: ${passRate}%`);
|
|
156
|
+
process.exit(results.fail > 0 ? 1 : 0);
|