principles-disciple 1.32.0 → 1.34.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/openclaw.plugin.json +4 -4
- package/package.json +1 -1
- package/src/core/correction-cue-learner.ts +203 -0
- package/src/core/correction-types.ts +88 -0
- package/src/core/evolution-logger.ts +3 -3
- package/src/core/init.ts +67 -0
- package/src/service/correction-observer-types.ts +58 -0
- package/src/service/correction-observer-workflow-manager.ts +218 -0
- package/src/service/evolution-worker.ts +172 -146
- package/src/service/nocturnal-service.ts +4 -1
- package/src/service/subagent-workflow/index.ts +14 -0
- package/src/service/subagent-workflow/nocturnal-workflow-manager.ts +3 -1
- package/tests/service/evolution-worker.nocturnal.test.ts +14 -1
- package/tests/service/evolution-worker.timeout.test.ts +350 -0
- package/tests/commands/implementation-lifecycle.test.ts +0 -362
- package/tests/core/detection-funnel.test.ts +0 -63
- package/tests/core/evolution-e2e.test.ts +0 -58
- package/tests/core/evolution-engine-gate-integration.test.ts +0 -543
- package/tests/core/evolution-engine.test.ts +0 -562
- package/tests/core/evolution-reducer.test.ts +0 -180
- package/tests/core/evolution-user-stories.e2e.test.ts +0 -249
- package/tests/core/local-worker-routing.test.ts +0 -757
- package/tests/core/rule-host.test.ts +0 -389
- package/tests/core/trajectory-correction-pain.test.ts +0 -180
- package/tests/hooks/gate-edit-verification.test.ts +0 -435
- package/tests/hooks/llm.test.ts +0 -308
- package/tests/hooks/progressive-trust-gate.test.ts +0 -277
- package/tests/hooks/prompt.test.ts +0 -1473
- package/tests/index.integration.test.ts +0 -179
- package/tests/index.shadow-routing.integration.test.ts +0 -140
- package/tests/service/evolution-worker.test.ts +0 -462
- package/tests/service/nocturnal-service.test.ts +0 -577
- package/tests/service/nocturnal-workflow-manager.test.ts +0 -441
- package/tests/tools/critique-prompt.test.ts +0 -260
- package/tests/tools/deep-reflect.test.ts +0 -232
- package/tests/tools/model-index.test.ts +0 -246
- package/tests/ui/app.test.tsx +0 -114
|
@@ -1,562 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Evolution Engine V2.0 - 单元测试
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { describe, it, test, expect, beforeEach, afterEach } from 'vitest';
|
|
6
|
-
import * as fs from 'fs';
|
|
7
|
-
import * as path from 'path';
|
|
8
|
-
import * as os from 'os';
|
|
9
|
-
import {
|
|
10
|
-
EvolutionEngine,
|
|
11
|
-
disposeAllEvolutionEngines,
|
|
12
|
-
disposeEvolutionEngine,
|
|
13
|
-
getEvolutionEngine,
|
|
14
|
-
} from '../../src/core/evolution-engine.js';
|
|
15
|
-
import {
|
|
16
|
-
EvolutionTier,
|
|
17
|
-
TIER_DEFINITIONS,
|
|
18
|
-
TASK_DIFFICULTY_CONFIG,
|
|
19
|
-
getTierByPoints,
|
|
20
|
-
EvolutionEvent,
|
|
21
|
-
EvolutionScorecard,
|
|
22
|
-
TaskDifficulty,
|
|
23
|
-
} from '../../src/core/evolution-types.js';
|
|
24
|
-
|
|
25
|
-
// ===== 测试工具 =====
|
|
26
|
-
|
|
27
|
-
function createTempWorkspace(): string {
|
|
28
|
-
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ep-test-'));
|
|
29
|
-
// EvolutionEngine 使用 .state 目录(STATE_DIR)
|
|
30
|
-
const stateDir = path.join(tmpDir, '.state');
|
|
31
|
-
fs.mkdirSync(stateDir, { recursive: true });
|
|
32
|
-
return tmpDir;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function cleanupWorkspace(dir: string): void {
|
|
36
|
-
try {
|
|
37
|
-
fs.rmSync(dir, { recursive: true, force: true });
|
|
38
|
-
} catch {}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// ===== 测试套件 =====
|
|
42
|
-
|
|
43
|
-
describe('EvolutionEngine', () => {
|
|
44
|
-
let workspace: string;
|
|
45
|
-
let engine: EvolutionEngine;
|
|
46
|
-
|
|
47
|
-
beforeEach(() => {
|
|
48
|
-
workspace = createTempWorkspace();
|
|
49
|
-
engine = new EvolutionEngine(workspace);
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
afterEach(() => {
|
|
53
|
-
disposeAllEvolutionEngines();
|
|
54
|
-
cleanupWorkspace(workspace);
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
// ===== 等级系统测试 =====
|
|
58
|
-
|
|
59
|
-
describe('Tier System', () => {
|
|
60
|
-
test('should start at Seed tier with 0 points', () => {
|
|
61
|
-
expect(engine.getTier()).toBe(EvolutionTier.Seed);
|
|
62
|
-
expect(engine.getPoints()).toBe(0);
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
test('should promote to Sprout at 50 points', () => {
|
|
66
|
-
// 50 points / 3 base = ~17 normal successes
|
|
67
|
-
for (let i = 0; i < 17; i++) {
|
|
68
|
-
engine.recordSuccess('write', { difficulty: 'normal' });
|
|
69
|
-
}
|
|
70
|
-
expect(engine.getTier()).toBeGreaterThanOrEqual(EvolutionTier.Sprout);
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
test('should promote to Sapling at 200 points', () => {
|
|
74
|
-
// 200 points / 8 base = 25 hard successes
|
|
75
|
-
for (let i = 0; i < 26; i++) {
|
|
76
|
-
engine.recordSuccess('write', { difficulty: 'hard' });
|
|
77
|
-
}
|
|
78
|
-
expect(engine.getTier()).toBeGreaterThanOrEqual(EvolutionTier.Sapling);
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
test('getTierByPoints returns correct tier', () => {
|
|
82
|
-
expect(getTierByPoints(0)).toBe(EvolutionTier.Seed);
|
|
83
|
-
expect(getTierByPoints(49)).toBe(EvolutionTier.Seed);
|
|
84
|
-
expect(getTierByPoints(50)).toBe(EvolutionTier.Sprout);
|
|
85
|
-
expect(getTierByPoints(199)).toBe(EvolutionTier.Sprout);
|
|
86
|
-
expect(getTierByPoints(200)).toBe(EvolutionTier.Sapling);
|
|
87
|
-
expect(getTierByPoints(499)).toBe(EvolutionTier.Sapling);
|
|
88
|
-
expect(getTierByPoints(500)).toBe(EvolutionTier.Tree);
|
|
89
|
-
expect(getTierByPoints(999)).toBe(EvolutionTier.Tree);
|
|
90
|
-
expect(getTierByPoints(1000)).toBe(EvolutionTier.Forest);
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
test('TIER_DEFINITIONS has 5 tiers', () => {
|
|
94
|
-
expect(TIER_DEFINITIONS).toHaveLength(5);
|
|
95
|
-
});
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
// ===== 积分计算测试 =====
|
|
99
|
-
|
|
100
|
-
describe('Points Calculation', () => {
|
|
101
|
-
test('should award base points for normal success', () => {
|
|
102
|
-
const result = engine.recordSuccess('write', { difficulty: 'normal' });
|
|
103
|
-
expect(result.pointsAwarded).toBe(TASK_DIFFICULTY_CONFIG.normal.basePoints);
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
test('should award more points for hard tasks', () => {
|
|
107
|
-
const normalResult = engine.recordSuccess('write', { difficulty: 'normal' });
|
|
108
|
-
|
|
109
|
-
// Reset for hard test
|
|
110
|
-
workspace = createTempWorkspace();
|
|
111
|
-
engine = new EvolutionEngine(workspace);
|
|
112
|
-
const hardResult = engine.recordSuccess('write', { difficulty: 'hard' });
|
|
113
|
-
|
|
114
|
-
expect(hardResult.pointsAwarded).toBeGreaterThan(normalResult.pointsAwarded);
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
test('should not award points for exploratory tools', () => {
|
|
118
|
-
const result = engine.recordSuccess('read');
|
|
119
|
-
expect(result.pointsAwarded).toBe(0);
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
test('failure should award 0 points', () => {
|
|
123
|
-
const result = engine.recordFailure('write');
|
|
124
|
-
expect(result.pointsAwarded).toBe(0);
|
|
125
|
-
});
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
// ===== 双倍奖励测试 =====
|
|
129
|
-
|
|
130
|
-
describe('Double Reward', () => {
|
|
131
|
-
test('should give double reward after failure then success', () => {
|
|
132
|
-
// 先失败
|
|
133
|
-
engine.recordFailure('write', { filePath: 'test.ts' });
|
|
134
|
-
|
|
135
|
-
// 再成功(同类任务)
|
|
136
|
-
const result = engine.recordSuccess('write', { filePath: 'test.ts', difficulty: 'normal' });
|
|
137
|
-
|
|
138
|
-
expect(result.isDoubleReward).toBe(true);
|
|
139
|
-
expect(result.pointsAwarded).toBe(TASK_DIFFICULTY_CONFIG.normal.basePoints * 2);
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
test('should not give double reward without prior failure', () => {
|
|
143
|
-
const result = engine.recordSuccess('write', { difficulty: 'normal' });
|
|
144
|
-
expect(result.isDoubleReward).toBe(false);
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
test('should respect cooldown for double reward', () => {
|
|
148
|
-
// 第一次失败→成功(触发双倍)
|
|
149
|
-
engine.recordFailure('write', { filePath: 'test1.ts' });
|
|
150
|
-
const first = engine.recordSuccess('write', { filePath: 'test1.ts', difficulty: 'normal' });
|
|
151
|
-
expect(first.isDoubleReward).toBe(true);
|
|
152
|
-
|
|
153
|
-
// 第二次失败→成功(冷却中,不双倍)
|
|
154
|
-
engine.recordFailure('write', { filePath: 'test2.ts' });
|
|
155
|
-
const second = engine.recordSuccess('write', { filePath: 'test2.ts', difficulty: 'normal' });
|
|
156
|
-
expect(second.isDoubleReward).toBe(false);
|
|
157
|
-
});
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
// ===== 难度衰减测试 =====
|
|
161
|
-
|
|
162
|
-
describe('Difficulty Penalty', () => {
|
|
163
|
-
test('should apply penalty for trivial tasks at high tier', () => {
|
|
164
|
-
// 直接设置高积分来模拟高等级
|
|
165
|
-
for (let i = 0; i < 63; i++) {
|
|
166
|
-
engine.recordSuccess('write', { difficulty: 'hard' });
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
// 此时应该是 Tree 级别
|
|
170
|
-
const tier = engine.getTier();
|
|
171
|
-
if (tier >= EvolutionTier.Tree) {
|
|
172
|
-
const result = engine.recordSuccess('write', { difficulty: 'trivial' });
|
|
173
|
-
// trivial 基础分1分,Tree级衰减后应该是 0.1 → 最少1分
|
|
174
|
-
expect(result.pointsAwarded).toBeLessThanOrEqual(TASK_DIFFICULTY_CONFIG.trivial.basePoints);
|
|
175
|
-
}
|
|
176
|
-
});
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
// ===== Gate 检查测试 =====
|
|
180
|
-
|
|
181
|
-
describe('Gate Integration', () => {
|
|
182
|
-
test('Seed tier should limit to 150 lines', () => {
|
|
183
|
-
const decision = engine.beforeToolCall({
|
|
184
|
-
toolName: 'write',
|
|
185
|
-
content: Array(151).fill('line').join('\n'),
|
|
186
|
-
});
|
|
187
|
-
expect(decision.allowed).toBe(false);
|
|
188
|
-
expect(decision.reason).toContain('150');
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
test('Seed tier should allow within limit', () => {
|
|
192
|
-
const decision = engine.beforeToolCall({
|
|
193
|
-
toolName: 'write',
|
|
194
|
-
content: Array(100).fill('line').join('\n'),
|
|
195
|
-
});
|
|
196
|
-
expect(decision.allowed).toBe(true);
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
test('Seed tier should block risk path', () => {
|
|
200
|
-
const decision = engine.beforeToolCall({
|
|
201
|
-
toolName: 'write',
|
|
202
|
-
filePath: 'src/core/trust-engine.ts',
|
|
203
|
-
isRiskPath: true,
|
|
204
|
-
lineCount: 10,
|
|
205
|
-
});
|
|
206
|
-
expect(decision.allowed).toBe(false);
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
test('Seed tier should allow subagent spawn', () => {
|
|
210
|
-
const decision = engine.beforeToolCall({
|
|
211
|
-
toolName: 'sessions_spawn',
|
|
212
|
-
});
|
|
213
|
-
expect(decision.allowed).toBe(true);
|
|
214
|
-
});
|
|
215
|
-
|
|
216
|
-
test('Forest tier should allow everything', () => {
|
|
217
|
-
// 直接设置为 Forest
|
|
218
|
-
for (let i = 0; i < 125; i++) {
|
|
219
|
-
engine.recordSuccess('write', { difficulty: 'hard' });
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
expect(engine.getTier()).toBe(EvolutionTier.Forest);
|
|
223
|
-
|
|
224
|
-
const decision = engine.beforeToolCall({
|
|
225
|
-
toolName: 'write',
|
|
226
|
-
content: Array(10000).fill('line').join('\n'),
|
|
227
|
-
isRiskPath: true,
|
|
228
|
-
});
|
|
229
|
-
expect(decision.allowed).toBe(true);
|
|
230
|
-
});
|
|
231
|
-
});
|
|
232
|
-
|
|
233
|
-
// ===== 持久化测试 =====
|
|
234
|
-
|
|
235
|
-
describe('Persistence', () => {
|
|
236
|
-
test('should save and load scorecard', () => {
|
|
237
|
-
engine.recordSuccess('write', { difficulty: 'hard' });
|
|
238
|
-
engine.recordSuccess('write', { difficulty: 'hard' });
|
|
239
|
-
|
|
240
|
-
const pointsBefore = engine.getPoints();
|
|
241
|
-
|
|
242
|
-
// 创建新实例(模拟重启)
|
|
243
|
-
const engine2 = new EvolutionEngine(workspace);
|
|
244
|
-
expect(engine2.getPoints()).toBe(pointsBefore);
|
|
245
|
-
});
|
|
246
|
-
|
|
247
|
-
test('should persist failure hashes across restart', () => {
|
|
248
|
-
engine.recordFailure('write', { filePath: 'test.ts' });
|
|
249
|
-
|
|
250
|
-
// 重启
|
|
251
|
-
const engine2 = new EvolutionEngine(workspace);
|
|
252
|
-
const result = engine2.recordSuccess('write', { filePath: 'test.ts', difficulty: 'normal' });
|
|
253
|
-
|
|
254
|
-
expect(result.isDoubleReward).toBe(true);
|
|
255
|
-
});
|
|
256
|
-
});
|
|
257
|
-
|
|
258
|
-
// ===== 统计测试 =====
|
|
259
|
-
|
|
260
|
-
describe('Stats', () => {
|
|
261
|
-
test('should track success/failure counts', () => {
|
|
262
|
-
engine.recordSuccess('write', { difficulty: 'normal' });
|
|
263
|
-
engine.recordSuccess('write', { difficulty: 'normal' });
|
|
264
|
-
engine.recordFailure('write');
|
|
265
|
-
|
|
266
|
-
const stats = engine.getStats();
|
|
267
|
-
expect(stats.totalSuccesses).toBe(2);
|
|
268
|
-
expect(stats.totalFailures).toBe(1);
|
|
269
|
-
});
|
|
270
|
-
|
|
271
|
-
test('should track consecutive streaks', () => {
|
|
272
|
-
engine.recordSuccess('write');
|
|
273
|
-
engine.recordSuccess('write');
|
|
274
|
-
engine.recordSuccess('write');
|
|
275
|
-
|
|
276
|
-
const stats = engine.getStats();
|
|
277
|
-
expect(stats.consecutiveSuccesses).toBe(3);
|
|
278
|
-
expect(stats.consecutiveFailures).toBe(0);
|
|
279
|
-
});
|
|
280
|
-
|
|
281
|
-
test('should break streak on failure', () => {
|
|
282
|
-
engine.recordSuccess('write');
|
|
283
|
-
engine.recordSuccess('write');
|
|
284
|
-
engine.recordFailure('write');
|
|
285
|
-
|
|
286
|
-
const stats = engine.getStats();
|
|
287
|
-
expect(stats.consecutiveSuccesses).toBe(0);
|
|
288
|
-
expect(stats.consecutiveFailures).toBe(1);
|
|
289
|
-
});
|
|
290
|
-
});
|
|
291
|
-
|
|
292
|
-
// ===== 状态摘要测试 =====
|
|
293
|
-
|
|
294
|
-
describe('Status Summary', () => {
|
|
295
|
-
test('should return correct status summary', () => {
|
|
296
|
-
const summary = engine.getStatusSummary();
|
|
297
|
-
expect(summary.tier).toBe(EvolutionTier.Seed);
|
|
298
|
-
expect(summary.tierName).toBe('Seed');
|
|
299
|
-
expect(summary.totalPoints).toBe(0);
|
|
300
|
-
expect(summary.nextTier).toBeDefined();
|
|
301
|
-
expect(summary.nextTier!.name).toBe('Sprout');
|
|
302
|
-
});
|
|
303
|
-
|
|
304
|
-
test('Forest should have no next tier', () => {
|
|
305
|
-
for (let i = 0; i < 125; i++) {
|
|
306
|
-
engine.recordSuccess('write', { difficulty: 'hard' });
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
const summary = engine.getStatusSummary();
|
|
310
|
-
expect(summary.nextTier).toBeNull();
|
|
311
|
-
});
|
|
312
|
-
});
|
|
313
|
-
|
|
314
|
-
// ===== P0 修复验证:多 Workspace 隔离 =====
|
|
315
|
-
|
|
316
|
-
describe('Multi-Workspace Isolation (P0 fix)', () => {
|
|
317
|
-
test('getEvolutionEngine should return different instances for different workspaces', () => {
|
|
318
|
-
const workspace1 = createTempWorkspace();
|
|
319
|
-
const workspace2 = createTempWorkspace();
|
|
320
|
-
|
|
321
|
-
const engine1 = getEvolutionEngine(workspace1);
|
|
322
|
-
const engine2 = getEvolutionEngine(workspace2);
|
|
323
|
-
|
|
324
|
-
// 不同 workspace 应该有不同的引擎实例
|
|
325
|
-
expect(engine1).not.toBe(engine2);
|
|
326
|
-
|
|
327
|
-
// 各自独立计分
|
|
328
|
-
engine1.recordSuccess('write', { difficulty: 'hard' });
|
|
329
|
-
expect(engine1.getPoints()).toBe(TASK_DIFFICULTY_CONFIG.hard.basePoints);
|
|
330
|
-
expect(engine2.getPoints()).toBe(0);
|
|
331
|
-
|
|
332
|
-
cleanupWorkspace(workspace1);
|
|
333
|
-
cleanupWorkspace(workspace2);
|
|
334
|
-
});
|
|
335
|
-
|
|
336
|
-
test('getEvolutionEngine should return same instance for same workspace', () => {
|
|
337
|
-
const ws = createTempWorkspace();
|
|
338
|
-
const e1 = getEvolutionEngine(ws);
|
|
339
|
-
const e2 = getEvolutionEngine(ws);
|
|
340
|
-
expect(e1).toBe(e2);
|
|
341
|
-
disposeEvolutionEngine(ws);
|
|
342
|
-
cleanupWorkspace(ws);
|
|
343
|
-
});
|
|
344
|
-
|
|
345
|
-
test('disposeEvolutionEngine should remove cached instance', () => {
|
|
346
|
-
const ws = createTempWorkspace();
|
|
347
|
-
const e1 = getEvolutionEngine(ws);
|
|
348
|
-
disposeEvolutionEngine(ws);
|
|
349
|
-
const e2 = getEvolutionEngine(ws);
|
|
350
|
-
expect(e2).not.toBe(e1);
|
|
351
|
-
disposeEvolutionEngine(ws);
|
|
352
|
-
cleanupWorkspace(ws);
|
|
353
|
-
});
|
|
354
|
-
});
|
|
355
|
-
|
|
356
|
-
// ===== P0 修复验证:并发写入 =====
|
|
357
|
-
|
|
358
|
-
describe('Concurrent Write Safety (P0 fix)', () => {
|
|
359
|
-
test('should handle rapid sequential writes without corruption', () => {
|
|
360
|
-
const iterations = 50;
|
|
361
|
-
for (let i = 0; i < iterations; i++) {
|
|
362
|
-
engine.recordSuccess('write', { difficulty: 'normal' });
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
// 验证数据完整性
|
|
366
|
-
expect(engine.getPoints()).toBe(iterations * TASK_DIFFICULTY_CONFIG.normal.basePoints);
|
|
367
|
-
expect(engine.getStats().totalSuccesses).toBe(iterations);
|
|
368
|
-
});
|
|
369
|
-
|
|
370
|
-
test('should persist correctly after rapid writes', () => {
|
|
371
|
-
const iterations = 30;
|
|
372
|
-
for (let i = 0; i < iterations; i++) {
|
|
373
|
-
engine.recordSuccess('write', { difficulty: 'hard' });
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
const expectedPoints = engine.getPoints();
|
|
377
|
-
|
|
378
|
-
// 重新加载验证持久化
|
|
379
|
-
const engine2 = new EvolutionEngine(workspace);
|
|
380
|
-
expect(engine2.getPoints()).toBe(expectedPoints);
|
|
381
|
-
});
|
|
382
|
-
});
|
|
383
|
-
|
|
384
|
-
// ===== P0 修复验证:锁竞态条件 =====
|
|
385
|
-
|
|
386
|
-
describe('Lock Race Condition Fixes', () => {
|
|
387
|
-
test('should handle concurrent engine instances safely', () => {
|
|
388
|
-
// 测试多个引擎实例(共享同一文件)的安全性
|
|
389
|
-
// 每个实例有独立的内存状态,但文件是共享的
|
|
390
|
-
|
|
391
|
-
const engine1 = new EvolutionEngine(workspace);
|
|
392
|
-
const engine2 = new EvolutionEngine(workspace);
|
|
393
|
-
|
|
394
|
-
// 两个实例交替写入
|
|
395
|
-
engine1.recordSuccess('write', { difficulty: 'normal' });
|
|
396
|
-
engine2.recordSuccess('write', { difficulty: 'normal' });
|
|
397
|
-
engine1.recordSuccess('write', { difficulty: 'normal' });
|
|
398
|
-
engine2.recordSuccess('write', { difficulty: 'normal' });
|
|
399
|
-
|
|
400
|
-
// 重新加载验证最终状态
|
|
401
|
-
const engine3 = new EvolutionEngine(workspace);
|
|
402
|
-
// 由于内存状态独立,最终积分取决于最后一次成功保存的实例
|
|
403
|
-
// 这里主要验证没有数据损坏
|
|
404
|
-
expect(() => engine3.getPoints()).not.toThrow();
|
|
405
|
-
});
|
|
406
|
-
|
|
407
|
-
test('should not corrupt data under high contention', () => {
|
|
408
|
-
// 快速连续写入,测试文件锁的保护
|
|
409
|
-
const iterations = 50;
|
|
410
|
-
for (let i = 0; i < iterations; i++) {
|
|
411
|
-
engine.recordSuccess('write', { difficulty: 'normal' });
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
// 验证数据完整性(没有损坏)
|
|
415
|
-
const stateDir = path.join(workspace, '.state');
|
|
416
|
-
const storagePath = path.join(stateDir, 'evolution-scorecard.json');
|
|
417
|
-
const content = fs.readFileSync(storagePath, 'utf8');
|
|
418
|
-
const data = JSON.parse(content); // 不应该抛出异常
|
|
419
|
-
|
|
420
|
-
expect(data.totalPoints).toBe(iterations * TASK_DIFFICULTY_CONFIG.normal.basePoints);
|
|
421
|
-
});
|
|
422
|
-
|
|
423
|
-
test('should handle lock timeout gracefully', () => {
|
|
424
|
-
// 创建一个持有锁的"假进程"
|
|
425
|
-
const stateDir = path.join(workspace, '.state');
|
|
426
|
-
const storagePath = path.join(stateDir, 'evolution-scorecard.json');
|
|
427
|
-
const lockPath = `${storagePath}.lock`;
|
|
428
|
-
|
|
429
|
-
// 模拟锁被死进程持有
|
|
430
|
-
const deadPid = 99999999;
|
|
431
|
-
fs.writeFileSync(lockPath, String(deadPid), 'utf8');
|
|
432
|
-
|
|
433
|
-
// 尝试写入应该成功(死进程锁会被清理)
|
|
434
|
-
const result = engine.recordSuccess('write', { difficulty: 'normal' });
|
|
435
|
-
expect(result.pointsAwarded).toBeGreaterThan(0);
|
|
436
|
-
});
|
|
437
|
-
});
|
|
438
|
-
|
|
439
|
-
// ===== P0 修复验证:数据不丢失 =====
|
|
440
|
-
|
|
441
|
-
describe('No Data Loss on Lock Failure', () => {
|
|
442
|
-
test('should not silently drop data when lock fails', () => {
|
|
443
|
-
// 记录初始状态
|
|
444
|
-
const initialPoints = engine.getPoints();
|
|
445
|
-
|
|
446
|
-
// 连续快速操作
|
|
447
|
-
const operations = 100;
|
|
448
|
-
for (let i = 0; i < operations; i++) {
|
|
449
|
-
engine.recordSuccess('write', { difficulty: 'trivial' });
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
// 所有操作都应该被记录(无丢失)
|
|
453
|
-
const expectedPoints = initialPoints + operations * TASK_DIFFICULTY_CONFIG.trivial.basePoints;
|
|
454
|
-
expect(engine.getPoints()).toBe(expectedPoints);
|
|
455
|
-
});
|
|
456
|
-
|
|
457
|
-
test('should preserve all failure records for double reward', () => {
|
|
458
|
-
// 记录多次失败
|
|
459
|
-
for (let i = 0; i < 5; i++) {
|
|
460
|
-
engine.recordFailure('write', { filePath: `test-${i}.ts` });
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
// 重新加载
|
|
464
|
-
const engine2 = new EvolutionEngine(workspace);
|
|
465
|
-
|
|
466
|
-
// 验证失败记录被保留
|
|
467
|
-
const result = engine2.recordSuccess('write', {
|
|
468
|
-
filePath: 'test-0.ts',
|
|
469
|
-
difficulty: 'normal'
|
|
470
|
-
});
|
|
471
|
-
|
|
472
|
-
expect(result.isDoubleReward).toBe(true);
|
|
473
|
-
});
|
|
474
|
-
|
|
475
|
-
test('retry queue should eventually save data', async () => {
|
|
476
|
-
// 这个测试验证正常情况下数据能被持久化
|
|
477
|
-
// 重试队列仅在锁获取失败时触发
|
|
478
|
-
|
|
479
|
-
// 记录一些数据
|
|
480
|
-
engine.recordSuccess('write', { difficulty: 'hard' });
|
|
481
|
-
const pointsBefore = engine.getPoints();
|
|
482
|
-
|
|
483
|
-
// 等待可能的异步操作完成
|
|
484
|
-
await new Promise(resolve => setTimeout(resolve, 100));
|
|
485
|
-
|
|
486
|
-
// 重新加载验证
|
|
487
|
-
const engine2 = new EvolutionEngine(workspace);
|
|
488
|
-
expect(engine2.getPoints()).toBe(pointsBefore);
|
|
489
|
-
});
|
|
490
|
-
});
|
|
491
|
-
|
|
492
|
-
// ===== 原子写入验证 =====
|
|
493
|
-
|
|
494
|
-
describe('Atomic Write Operations', () => {
|
|
495
|
-
test('should write to temp file first, then rename', () => {
|
|
496
|
-
const stateDir = path.join(workspace, '.state');
|
|
497
|
-
const tempFiles = fs.readdirSync(stateDir).filter(f => f.includes('.tmp.'));
|
|
498
|
-
|
|
499
|
-
// 不应该有残留的临时文件
|
|
500
|
-
expect(tempFiles.length).toBe(0);
|
|
501
|
-
|
|
502
|
-
// 执行写入
|
|
503
|
-
engine.recordSuccess('write', { difficulty: 'normal' });
|
|
504
|
-
|
|
505
|
-
// 仍然不应该有残留的临时文件
|
|
506
|
-
const tempFilesAfter = fs.readdirSync(stateDir).filter(f => f.includes('.tmp.'));
|
|
507
|
-
expect(tempFilesAfter.length).toBe(0);
|
|
508
|
-
});
|
|
509
|
-
|
|
510
|
-
test('should have valid JSON after concurrent writes', () => {
|
|
511
|
-
// 快速写入
|
|
512
|
-
for (let i = 0; i < 50; i++) {
|
|
513
|
-
engine.recordSuccess('write', { difficulty: 'normal' });
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
// 直接读取文件验证 JSON 有效性
|
|
517
|
-
const stateDir = path.join(workspace, '.state');
|
|
518
|
-
const storagePath = path.join(stateDir, 'evolution-scorecard.json');
|
|
519
|
-
|
|
520
|
-
const content = fs.readFileSync(storagePath, 'utf8');
|
|
521
|
-
const data = JSON.parse(content); // 不应该抛出异常
|
|
522
|
-
|
|
523
|
-
expect(data.totalPoints).toBe(50 * TASK_DIFFICULTY_CONFIG.normal.basePoints);
|
|
524
|
-
});
|
|
525
|
-
});
|
|
526
|
-
|
|
527
|
-
// ===== PID 存活检测测试 =====
|
|
528
|
-
|
|
529
|
-
describe('PID Liveness Detection', () => {
|
|
530
|
-
test('should detect current process as alive', () => {
|
|
531
|
-
// 当前进程应该总是存活的
|
|
532
|
-
expect(() => process.kill(process.pid, 0)).not.toThrow();
|
|
533
|
-
});
|
|
534
|
-
|
|
535
|
-
test('should clean up dead process lock and save data', () => {
|
|
536
|
-
const stateDir = path.join(workspace, '.state');
|
|
537
|
-
const storagePath = path.join(stateDir, 'evolution-scorecard.json');
|
|
538
|
-
const lockPath = `${storagePath}.lock`;
|
|
539
|
-
|
|
540
|
-
// 创建一个"死进程"的锁
|
|
541
|
-
// 使用一个非常大的 PID 号(如 99999999),这个进程几乎肯定不存在
|
|
542
|
-
const deadPid = 99999999;
|
|
543
|
-
fs.writeFileSync(lockPath, String(deadPid), 'utf8');
|
|
544
|
-
|
|
545
|
-
// 验证锁文件存在
|
|
546
|
-
expect(fs.existsSync(lockPath)).toBe(true);
|
|
547
|
-
|
|
548
|
-
// 下一次写入应该能获取锁(因为持有者进程已死亡)并成功保存
|
|
549
|
-
const result = engine.recordSuccess('write', { difficulty: 'normal' });
|
|
550
|
-
expect(result.pointsAwarded).toBeGreaterThan(0);
|
|
551
|
-
|
|
552
|
-
// 验证内存中的积分正确
|
|
553
|
-
expect(engine.getPoints()).toBe(result.pointsAwarded);
|
|
554
|
-
|
|
555
|
-
// 验证锁被释放(文件不存在)
|
|
556
|
-
expect(fs.existsSync(lockPath)).toBe(false);
|
|
557
|
-
|
|
558
|
-
// 验证数据文件存在
|
|
559
|
-
expect(fs.existsSync(storagePath)).toBe(true);
|
|
560
|
-
});
|
|
561
|
-
});
|
|
562
|
-
});
|