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,362 +0,0 @@
|
|
|
1
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
-
import * as fs from 'fs';
|
|
3
|
-
import * as os from 'os';
|
|
4
|
-
import * as path from 'path';
|
|
5
|
-
import { WorkspaceContext } from '../../src/core/workspace-context.js';
|
|
6
|
-
import { createImplementationAssetDir, getImplementationAssetRoot } from '../../src/core/code-implementation-storage.js';
|
|
7
|
-
import {
|
|
8
|
-
loadLedger,
|
|
9
|
-
saveLedger,
|
|
10
|
-
type LedgerPrinciple,
|
|
11
|
-
type LedgerRule,
|
|
12
|
-
} from '../../src/core/principle-tree-ledger.js';
|
|
13
|
-
import { handlePromoteImplCommand } from '../../src/commands/promote-impl.js';
|
|
14
|
-
import { handleDisableImplCommand } from '../../src/commands/disable-impl.js';
|
|
15
|
-
import { handleArchiveImplCommand } from '../../src/commands/archive-impl.js';
|
|
16
|
-
import { handleRollbackImplCommand } from '../../src/commands/rollback-impl.js';
|
|
17
|
-
import type { PluginCommandContext } from '../../src/openclaw-sdk.js';
|
|
18
|
-
import type { Implementation } from '../../src/types/principle-tree-schema.js';
|
|
19
|
-
import { TrajectoryDatabase, TrajectoryRegistry } from '../../src/core/trajectory.js';
|
|
20
|
-
import { registerSample } from '../../src/core/nocturnal-dataset.js';
|
|
21
|
-
import { safeRmDir } from '../test-utils.js';
|
|
22
|
-
|
|
23
|
-
function makePrinciple(): LedgerPrinciple {
|
|
24
|
-
return {
|
|
25
|
-
id: 'P-1',
|
|
26
|
-
version: 1,
|
|
27
|
-
text: 'Prefer explicit principle-backed implementations',
|
|
28
|
-
triggerPattern: 'principle',
|
|
29
|
-
action: 'use implementations safely',
|
|
30
|
-
status: 'active',
|
|
31
|
-
priority: 'P1',
|
|
32
|
-
scope: 'general',
|
|
33
|
-
evaluability: 'deterministic',
|
|
34
|
-
valueScore: 0,
|
|
35
|
-
adherenceRate: 0,
|
|
36
|
-
painPreventedCount: 0,
|
|
37
|
-
derivedFromPainIds: [],
|
|
38
|
-
ruleIds: ['R-1'],
|
|
39
|
-
conflictsWithPrincipleIds: [],
|
|
40
|
-
createdAt: '2026-04-08T00:00:00.000Z',
|
|
41
|
-
updatedAt: '2026-04-08T00:00:00.000Z',
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function makeRule(implementationIds: string[]): LedgerRule {
|
|
46
|
-
return {
|
|
47
|
-
id: 'R-1',
|
|
48
|
-
version: 1,
|
|
49
|
-
name: 'Guard implementation lifecycle',
|
|
50
|
-
description: 'Tracks implementation state transitions',
|
|
51
|
-
type: 'gate',
|
|
52
|
-
triggerCondition: 'lifecycle',
|
|
53
|
-
enforcement: 'warn',
|
|
54
|
-
action: 'record state safely',
|
|
55
|
-
principleId: 'P-1',
|
|
56
|
-
status: 'implemented',
|
|
57
|
-
coverageRate: 0,
|
|
58
|
-
falsePositiveRate: 0,
|
|
59
|
-
implementationPath: undefined,
|
|
60
|
-
testPath: undefined,
|
|
61
|
-
createdAt: '2026-04-08T00:00:00.000Z',
|
|
62
|
-
updatedAt: '2026-04-08T00:00:00.000Z',
|
|
63
|
-
implementationIds,
|
|
64
|
-
};
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function makeImplementation(
|
|
68
|
-
id: string,
|
|
69
|
-
lifecycleState: Implementation['lifecycleState'],
|
|
70
|
-
overrides: Partial<Implementation> = {}
|
|
71
|
-
): Implementation {
|
|
72
|
-
return {
|
|
73
|
-
id,
|
|
74
|
-
ruleId: 'R-1',
|
|
75
|
-
type: 'code',
|
|
76
|
-
path: path.join('virtual', `${id}.js`),
|
|
77
|
-
version: '1.0.0',
|
|
78
|
-
coversCondition: 'lifecycle',
|
|
79
|
-
coveragePercentage: 100,
|
|
80
|
-
lifecycleState,
|
|
81
|
-
createdAt: '2026-04-08T00:00:00.000Z',
|
|
82
|
-
updatedAt: '2026-04-08T00:00:00.000Z',
|
|
83
|
-
...overrides,
|
|
84
|
-
};
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function makeCommandContext(workspaceDir: string, args: string): PluginCommandContext {
|
|
88
|
-
return {
|
|
89
|
-
channel: 'test',
|
|
90
|
-
isAuthorizedSender: true,
|
|
91
|
-
commandBody: args,
|
|
92
|
-
args,
|
|
93
|
-
config: { workspaceDir, language: 'en' },
|
|
94
|
-
workspaceDir,
|
|
95
|
-
sessionId: 'session-1',
|
|
96
|
-
};
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
function writePassingReplayReport(stateDir: string, implementationId: string): void {
|
|
100
|
-
const assetRoot = getImplementationAssetRoot(stateDir, implementationId);
|
|
101
|
-
const reportPath = path.join(assetRoot, 'replays', '2026-04-08T00-00-00.000Z.json');
|
|
102
|
-
fs.writeFileSync(
|
|
103
|
-
reportPath,
|
|
104
|
-
JSON.stringify(
|
|
105
|
-
{
|
|
106
|
-
overallDecision: 'pass',
|
|
107
|
-
replayResults: {
|
|
108
|
-
painNegative: { total: 1, passed: 1, failed: 0, details: [] },
|
|
109
|
-
successPositive: { total: 1, passed: 1, failed: 0, details: [] },
|
|
110
|
-
principleAnchor: { total: 1, passed: 1, failed: 0, details: [] },
|
|
111
|
-
},
|
|
112
|
-
blockers: [],
|
|
113
|
-
generatedAt: '2026-04-08T00:00:00.000Z',
|
|
114
|
-
implementationId,
|
|
115
|
-
sampleFingerprints: ['fp-1'],
|
|
116
|
-
},
|
|
117
|
-
null,
|
|
118
|
-
2
|
|
119
|
-
),
|
|
120
|
-
'utf-8'
|
|
121
|
-
);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
describe('implementation lifecycle commands', () => {
|
|
125
|
-
let tempDir: string;
|
|
126
|
-
let workspaceDir: string;
|
|
127
|
-
let stateDir: string;
|
|
128
|
-
let trajectory: TrajectoryDatabase;
|
|
129
|
-
|
|
130
|
-
beforeEach(() => {
|
|
131
|
-
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pd-impl-cmd-'));
|
|
132
|
-
workspaceDir = path.join(tempDir, 'workspace');
|
|
133
|
-
stateDir = path.join(tempDir, '.state');
|
|
134
|
-
fs.mkdirSync(workspaceDir, { recursive: true });
|
|
135
|
-
fs.mkdirSync(stateDir, { recursive: true });
|
|
136
|
-
fs.writeFileSync(
|
|
137
|
-
path.join(workspaceDir, 'PROFILE.json'),
|
|
138
|
-
JSON.stringify({ risk_paths: ['src/**'] }, null, 2),
|
|
139
|
-
'utf-8',
|
|
140
|
-
);
|
|
141
|
-
trajectory = new TrajectoryDatabase({ workspaceDir });
|
|
142
|
-
|
|
143
|
-
WorkspaceContext.clearCache();
|
|
144
|
-
const workspaceContext = WorkspaceContext.fromHookContext({ workspaceDir, stateDir });
|
|
145
|
-
vi.spyOn(WorkspaceContext, 'fromHookContext').mockReturnValue(workspaceContext);
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
afterEach(() => {
|
|
149
|
-
try {
|
|
150
|
-
trajectory.dispose();
|
|
151
|
-
} catch {
|
|
152
|
-
// Best effort cleanup.
|
|
153
|
-
}
|
|
154
|
-
try {
|
|
155
|
-
TrajectoryRegistry.dispose(workspaceDir);
|
|
156
|
-
} catch {
|
|
157
|
-
// Best effort cleanup.
|
|
158
|
-
}
|
|
159
|
-
vi.restoreAllMocks();
|
|
160
|
-
WorkspaceContext.clearCache();
|
|
161
|
-
safeRmDir(tempDir);
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
function seedLedger(implementations: Implementation[]): void {
|
|
165
|
-
saveLedger(stateDir, {
|
|
166
|
-
trainingStore: {
|
|
167
|
-
'P-1': {
|
|
168
|
-
principleId: 'P-1',
|
|
169
|
-
evaluability: 'deterministic',
|
|
170
|
-
applicableOpportunityCount: 1,
|
|
171
|
-
observedViolationCount: 0,
|
|
172
|
-
complianceRate: 1,
|
|
173
|
-
violationTrend: 0,
|
|
174
|
-
generatedSampleCount: 0,
|
|
175
|
-
approvedSampleCount: 0,
|
|
176
|
-
includedTrainRunIds: [],
|
|
177
|
-
deployedCheckpointIds: [],
|
|
178
|
-
internalizationStatus: 'internalized',
|
|
179
|
-
},
|
|
180
|
-
},
|
|
181
|
-
tree: {
|
|
182
|
-
principles: {
|
|
183
|
-
'P-1': makePrinciple(),
|
|
184
|
-
},
|
|
185
|
-
rules: {
|
|
186
|
-
'R-1': makeRule(implementations.map((impl) => impl.id)),
|
|
187
|
-
},
|
|
188
|
-
implementations: Object.fromEntries(implementations.map((impl) => [impl.id, impl])),
|
|
189
|
-
metrics: {},
|
|
190
|
-
lastUpdated: '2026-04-08T00:00:00.000Z',
|
|
191
|
-
},
|
|
192
|
-
});
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
function seedReplaySample(sessionId: string, artifactId: string): void {
|
|
196
|
-
trajectory.recordSession({
|
|
197
|
-
sessionId,
|
|
198
|
-
startedAt: '2026-04-08T00:00:00.000Z',
|
|
199
|
-
});
|
|
200
|
-
trajectory.recordToolCall({
|
|
201
|
-
sessionId,
|
|
202
|
-
toolName: 'write',
|
|
203
|
-
outcome: 'blocked',
|
|
204
|
-
errorMessage: 'risky write requires approval',
|
|
205
|
-
paramsJson: { filePath: 'src/app.ts' },
|
|
206
|
-
createdAt: '2026-04-08T00:01:00.000Z',
|
|
207
|
-
});
|
|
208
|
-
trajectory.recordPainEvent({
|
|
209
|
-
sessionId,
|
|
210
|
-
source: 'gate',
|
|
211
|
-
score: 75,
|
|
212
|
-
reason: 'risky write requires approval',
|
|
213
|
-
createdAt: '2026-04-08T00:01:01.000Z',
|
|
214
|
-
});
|
|
215
|
-
trajectory.recordGateBlock({
|
|
216
|
-
sessionId,
|
|
217
|
-
toolName: 'write',
|
|
218
|
-
filePath: 'src/app.ts',
|
|
219
|
-
reason: 'risky write requires approval',
|
|
220
|
-
planStatus: 'NONE',
|
|
221
|
-
createdAt: '2026-04-08T00:01:02.000Z',
|
|
222
|
-
});
|
|
223
|
-
|
|
224
|
-
const artifactPath = path.join(stateDir, 'nocturnal', 'samples', `${artifactId}.json`);
|
|
225
|
-
fs.mkdirSync(path.dirname(artifactPath), { recursive: true });
|
|
226
|
-
fs.writeFileSync(
|
|
227
|
-
artifactPath,
|
|
228
|
-
JSON.stringify(
|
|
229
|
-
{
|
|
230
|
-
artifactId,
|
|
231
|
-
sessionId,
|
|
232
|
-
principleId: 'P-1',
|
|
233
|
-
sourceSnapshotRef: `snapshot-${sessionId}`,
|
|
234
|
-
badDecision: 'Retried the write without checking approval requirements',
|
|
235
|
-
betterDecision: 'Check the approval requirements before writing',
|
|
236
|
-
rationale: 'Riskiest paths should not be written without a plan',
|
|
237
|
-
createdAt: '2026-04-08T00:02:00.000Z',
|
|
238
|
-
},
|
|
239
|
-
null,
|
|
240
|
-
2,
|
|
241
|
-
),
|
|
242
|
-
'utf-8',
|
|
243
|
-
);
|
|
244
|
-
|
|
245
|
-
registerSample(
|
|
246
|
-
workspaceDir,
|
|
247
|
-
{
|
|
248
|
-
artifactId,
|
|
249
|
-
sessionId,
|
|
250
|
-
principleId: 'P-1',
|
|
251
|
-
sourceSnapshotRef: `snapshot-${sessionId}`,
|
|
252
|
-
badDecision: 'Retried the write without checking approval requirements',
|
|
253
|
-
betterDecision: 'Check the approval requirements before writing',
|
|
254
|
-
rationale: 'Riskiest paths should not be written without a plan',
|
|
255
|
-
createdAt: '2026-04-08T00:02:00.000Z',
|
|
256
|
-
},
|
|
257
|
-
artifactPath,
|
|
258
|
-
null,
|
|
259
|
-
'pain-negative',
|
|
260
|
-
);
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
it('promote preserves the legacy training store while activating the candidate', () => {
|
|
264
|
-
seedLedger([makeImplementation('IMPL-CAND', 'candidate')]);
|
|
265
|
-
createImplementationAssetDir(stateDir, 'IMPL-CAND', '1.0.0');
|
|
266
|
-
writePassingReplayReport(stateDir, 'IMPL-CAND');
|
|
267
|
-
|
|
268
|
-
const result = handlePromoteImplCommand(makeCommandContext(workspaceDir, 'IMPL-CAND'));
|
|
269
|
-
|
|
270
|
-
expect(result.text).toContain('Implementation promoted');
|
|
271
|
-
expect(loadLedger(stateDir).tree.implementations['IMPL-CAND'].lifecycleState).toBe('active');
|
|
272
|
-
|
|
273
|
-
const raw = JSON.parse(
|
|
274
|
-
fs.readFileSync(path.join(stateDir, 'principle_training_state.json'), 'utf-8')
|
|
275
|
-
) as Record<string, unknown>;
|
|
276
|
-
expect(raw['P-1']).toBeDefined();
|
|
277
|
-
expect(raw._tree).toBeDefined();
|
|
278
|
-
});
|
|
279
|
-
|
|
280
|
-
it('eval generates a replay report for a candidate implementation before promotion', () => {
|
|
281
|
-
seedLedger([makeImplementation('IMPL-CAND', 'candidate')]);
|
|
282
|
-
createImplementationAssetDir(stateDir, 'IMPL-CAND', '1.0.0', {
|
|
283
|
-
entrySource: [
|
|
284
|
-
'export const meta = {',
|
|
285
|
-
' name: "risky-write-guard",',
|
|
286
|
-
' version: "1.0.0",',
|
|
287
|
-
' ruleId: "R-1",',
|
|
288
|
-
' coversCondition: "risky write",',
|
|
289
|
-
'};',
|
|
290
|
-
'export function evaluate(input, helpers) {',
|
|
291
|
-
" if (helpers.isRiskPath() && helpers.getToolName() === 'write' && helpers.getPlanStatus() !== 'READY') {",
|
|
292
|
-
" return { decision: 'requireApproval', matched: true, reason: 'plan required' };",
|
|
293
|
-
' }',
|
|
294
|
-
" return { decision: 'allow', matched: false, reason: 'not-applicable' };",
|
|
295
|
-
'}',
|
|
296
|
-
].join('\n'),
|
|
297
|
-
});
|
|
298
|
-
seedReplaySample('session-eval', 'artifact-eval');
|
|
299
|
-
|
|
300
|
-
const result = handlePromoteImplCommand(makeCommandContext(workspaceDir, 'eval IMPL-CAND'));
|
|
301
|
-
|
|
302
|
-
expect(result.text).toContain('Replay Evaluation Report');
|
|
303
|
-
expect(result.text).toContain('Overall Decision: [PASS]');
|
|
304
|
-
expect(
|
|
305
|
-
fs.readdirSync(path.join(getImplementationAssetRoot(stateDir, 'IMPL-CAND'), 'replays')).length,
|
|
306
|
-
).toBeGreaterThan(0);
|
|
307
|
-
});
|
|
308
|
-
|
|
309
|
-
it('disable preserves the legacy training store while recording disable metadata', () => {
|
|
310
|
-
seedLedger([makeImplementation('IMPL-ACTIVE', 'active')]);
|
|
311
|
-
|
|
312
|
-
const result = handleDisableImplCommand(
|
|
313
|
-
makeCommandContext(workspaceDir, 'IMPL-ACTIVE --reason "manual disable"')
|
|
314
|
-
);
|
|
315
|
-
|
|
316
|
-
expect(result.text).toContain('Implementation disabled');
|
|
317
|
-
const updated = loadLedger(stateDir).tree.implementations['IMPL-ACTIVE'];
|
|
318
|
-
expect(updated.lifecycleState).toBe('disabled');
|
|
319
|
-
expect(updated.disabledReason).toBe('manual disable');
|
|
320
|
-
|
|
321
|
-
const raw = JSON.parse(
|
|
322
|
-
fs.readFileSync(path.join(stateDir, 'principle_training_state.json'), 'utf-8')
|
|
323
|
-
) as Record<string, unknown>;
|
|
324
|
-
expect(raw['P-1']).toBeDefined();
|
|
325
|
-
});
|
|
326
|
-
|
|
327
|
-
it('archive preserves the legacy training store while archiving a candidate', () => {
|
|
328
|
-
seedLedger([makeImplementation('IMPL-CAND', 'candidate')]);
|
|
329
|
-
|
|
330
|
-
const result = handleArchiveImplCommand(makeCommandContext(workspaceDir, 'IMPL-CAND'));
|
|
331
|
-
|
|
332
|
-
expect(result.text).toContain('Implementation archived');
|
|
333
|
-
expect(loadLedger(stateDir).tree.implementations['IMPL-CAND'].lifecycleState).toBe('archived');
|
|
334
|
-
|
|
335
|
-
const raw = JSON.parse(
|
|
336
|
-
fs.readFileSync(path.join(stateDir, 'principle_training_state.json'), 'utf-8')
|
|
337
|
-
) as Record<string, unknown>;
|
|
338
|
-
expect(raw['P-1']).toBeDefined();
|
|
339
|
-
});
|
|
340
|
-
|
|
341
|
-
it('rollback preserves the legacy training store while restoring the previous active implementation', () => {
|
|
342
|
-
seedLedger([
|
|
343
|
-
makeImplementation('IMPL-CURRENT', 'active', { previousActive: 'IMPL-PREV' }),
|
|
344
|
-
makeImplementation('IMPL-PREV', 'disabled'),
|
|
345
|
-
]);
|
|
346
|
-
|
|
347
|
-
const result = handleRollbackImplCommand(
|
|
348
|
-
makeCommandContext(workspaceDir, 'IMPL-CURRENT --reason "rollback"')
|
|
349
|
-
);
|
|
350
|
-
|
|
351
|
-
expect(result.text).toContain('Rollback complete');
|
|
352
|
-
|
|
353
|
-
const ledger = loadLedger(stateDir);
|
|
354
|
-
expect(ledger.tree.implementations['IMPL-CURRENT'].lifecycleState).toBe('disabled');
|
|
355
|
-
expect(ledger.tree.implementations['IMPL-PREV'].lifecycleState).toBe('active');
|
|
356
|
-
|
|
357
|
-
const raw = JSON.parse(
|
|
358
|
-
fs.readFileSync(path.join(stateDir, 'principle_training_state.json'), 'utf-8')
|
|
359
|
-
) as Record<string, unknown>;
|
|
360
|
-
expect(raw['P-1']).toBeDefined();
|
|
361
|
-
});
|
|
362
|
-
});
|
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
-
import { DetectionFunnel } from '../../src/core/detection-funnel.js';
|
|
3
|
-
|
|
4
|
-
describe('DetectionFunnel', () => {
|
|
5
|
-
const mockDictionary = {
|
|
6
|
-
match: vi.fn(),
|
|
7
|
-
};
|
|
8
|
-
|
|
9
|
-
beforeEach(() => {
|
|
10
|
-
vi.clearAllMocks();
|
|
11
|
-
});
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
it('should skip protocol text before dictionary and queue checks', () => {
|
|
15
|
-
const funnel = new DetectionFunnel(mockDictionary as any);
|
|
16
|
-
const enqueueSpy = vi.spyOn(funnel as any, 'enqueueAsync');
|
|
17
|
-
|
|
18
|
-
const result = funnel.detect('[EVOLUTION_ACK]');
|
|
19
|
-
|
|
20
|
-
expect(result).toEqual({ detected: false, source: 'l1_exact' });
|
|
21
|
-
expect(mockDictionary.match).not.toHaveBeenCalled();
|
|
22
|
-
expect(enqueueSpy).not.toHaveBeenCalled();
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
it('L1: should return true immediately if dictionary matches', async () => {
|
|
26
|
-
mockDictionary.match.mockReturnValue({ ruleId: 'P_CONFUSION', severity: 35 });
|
|
27
|
-
const funnel = new DetectionFunnel(mockDictionary as any);
|
|
28
|
-
|
|
29
|
-
const result = funnel.detect('I am confused');
|
|
30
|
-
|
|
31
|
-
expect(result.detected).toBe(true);
|
|
32
|
-
expect(result.source).toBe('l1_exact');
|
|
33
|
-
expect(mockDictionary.match).toHaveBeenCalledWith('I am confused');
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
it('L2: should return true if found in cache after initial mismatch', async () => {
|
|
37
|
-
mockDictionary.match.mockReturnValue(undefined);
|
|
38
|
-
const funnel = new DetectionFunnel(mockDictionary as any);
|
|
39
|
-
|
|
40
|
-
// Manually prime the cache for testing (or simulate a previous L3 hit)
|
|
41
|
-
(funnel as any).cache.set('hash_123', { detected: true, severity: 40 });
|
|
42
|
-
|
|
43
|
-
// We need a stable hash for the test
|
|
44
|
-
vi.spyOn(funnel as any, 'computeHash').mockReturnValue('hash_123');
|
|
45
|
-
|
|
46
|
-
const result = funnel.detect('Some repetitive text');
|
|
47
|
-
|
|
48
|
-
expect(result.detected).toBe(true);
|
|
49
|
-
expect(result.source).toBe('l2_cache');
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
it('L3: should enqueue for async detection if mismatch and not in cache', () => {
|
|
53
|
-
mockDictionary.match.mockReturnValue(undefined);
|
|
54
|
-
const funnel = new DetectionFunnel(mockDictionary as any);
|
|
55
|
-
const enqueueSpy = vi.spyOn(funnel as any, 'enqueueAsync');
|
|
56
|
-
|
|
57
|
-
const result = funnel.detect('New unknown expression');
|
|
58
|
-
|
|
59
|
-
expect(result.detected).toBe(false);
|
|
60
|
-
expect(result.source).toBe('l3_async_queued');
|
|
61
|
-
expect(enqueueSpy).toHaveBeenCalledWith('New unknown expression');
|
|
62
|
-
});
|
|
63
|
-
});
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
import * as fs from 'fs';
|
|
2
|
-
import * as os from 'os';
|
|
3
|
-
import * as path from 'path';
|
|
4
|
-
import { afterEach, describe, expect, it } from 'vitest';
|
|
5
|
-
import { EvolutionReducerImpl } from '../../src/core/evolution-reducer.js';
|
|
6
|
-
import { handleEvolutionStatusCommand } from '../../src/commands/evolution-status.js';
|
|
7
|
-
|
|
8
|
-
const tempDirs: string[] = [];
|
|
9
|
-
function makeTempDir(): string {
|
|
10
|
-
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'pd-evolution-e2e-'));
|
|
11
|
-
tempDirs.push(dir);
|
|
12
|
-
return dir;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
afterEach(() => {
|
|
16
|
-
for (const dir of tempDirs.splice(0)) {
|
|
17
|
-
fs.rmSync(dir, { recursive: true, force: true });
|
|
18
|
-
}
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
describe('evolution loop e2e', () => {
|
|
22
|
-
it('runs pain -> create principle -> status query -> rollback flow', () => {
|
|
23
|
-
const workspace = makeTempDir();
|
|
24
|
-
const reducer = new EvolutionReducerImpl({ workspaceDir: workspace });
|
|
25
|
-
|
|
26
|
-
// Emit pain (no longer creates principle automatically)
|
|
27
|
-
reducer.emitSync({
|
|
28
|
-
ts: new Date().toISOString(),
|
|
29
|
-
type: 'pain_detected',
|
|
30
|
-
data: {
|
|
31
|
-
painId: 'pain-e2e-1',
|
|
32
|
-
painType: 'tool_failure',
|
|
33
|
-
source: 'write',
|
|
34
|
-
reason: 'write failed on risky file',
|
|
35
|
-
},
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
// Create principle from diagnosis
|
|
39
|
-
const principleId = reducer.createPrincipleFromDiagnosis({
|
|
40
|
-
painId: 'pain-e2e-1',
|
|
41
|
-
painType: 'tool_failure',
|
|
42
|
-
triggerPattern: 'file write operation fails on risky files',
|
|
43
|
-
action: 'verify file permissions before writing',
|
|
44
|
-
source: 'write',
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
const p = reducer.getProbationPrinciples()[0];
|
|
48
|
-
expect(p).toBeDefined();
|
|
49
|
-
|
|
50
|
-
const statusBefore = handleEvolutionStatusCommand({ config: { workspaceDir: workspace, language: 'en' } } as any);
|
|
51
|
-
expect(statusBefore.text).toContain('probation principles: 1');
|
|
52
|
-
|
|
53
|
-
reducer.rollbackPrinciple(p.id, 'manual validation failed');
|
|
54
|
-
|
|
55
|
-
const statusAfter = handleEvolutionStatusCommand({ config: { workspaceDir: workspace, language: 'en' } } as any);
|
|
56
|
-
expect(statusAfter.text).toContain('deprecated principles: 1');
|
|
57
|
-
});
|
|
58
|
-
});
|