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,757 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Local Worker Routing Policy — Tests
|
|
3
|
-
* ====================================
|
|
4
|
-
*
|
|
5
|
-
* Tests for task classification and routing decision logic.
|
|
6
|
-
*
|
|
7
|
-
* Test organization:
|
|
8
|
-
* - Without deployment: classification-only tests (reader_eligible, editor_eligible, high_entropy, risk, ambiguous)
|
|
9
|
-
* - With deployment enabled: full route_local decision
|
|
10
|
-
* - With deployment disabled: stay_main with deployment_unavailable
|
|
11
|
-
* - Helper functions: canRouteToProfile, isAnyLocalRoutingEnabled, listEnabledProfiles
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
15
|
-
import * as fs from 'fs';
|
|
16
|
-
import * as path from 'path';
|
|
17
|
-
import * as os from 'os';
|
|
18
|
-
import {
|
|
19
|
-
classifyTask,
|
|
20
|
-
canRouteToProfile,
|
|
21
|
-
isAnyLocalRoutingEnabled,
|
|
22
|
-
listEnabledProfiles,
|
|
23
|
-
type RoutingInput,
|
|
24
|
-
type RoutingDecision,
|
|
25
|
-
} from '../../src/core/local-worker-routing.js';
|
|
26
|
-
import {
|
|
27
|
-
registerTrainingRun,
|
|
28
|
-
startTrainingRun,
|
|
29
|
-
completeTrainingRun,
|
|
30
|
-
registerCheckpoint,
|
|
31
|
-
attachEvalSummary,
|
|
32
|
-
markCheckpointDeployable,
|
|
33
|
-
} from '../../src/core/model-training-registry.js';
|
|
34
|
-
import {
|
|
35
|
-
advancePromotion,
|
|
36
|
-
DEFAULT_BASELINE_METRICS,
|
|
37
|
-
} from '../../src/core/promotion-gate.js';
|
|
38
|
-
import {
|
|
39
|
-
bindCheckpointToWorkerProfile,
|
|
40
|
-
enableRoutingForProfile,
|
|
41
|
-
disableRoutingForProfile,
|
|
42
|
-
} from '../../src/core/model-deployment-registry.js';
|
|
43
|
-
|
|
44
|
-
// ---------------------------------------------------------------------------
|
|
45
|
-
// Test Fixtures
|
|
46
|
-
// ---------------------------------------------------------------------------
|
|
47
|
-
|
|
48
|
-
function makeTmpDir(): string {
|
|
49
|
-
return fs.mkdtempSync(path.join(os.tmpdir(), 'pd-routing-test-'));
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function rmdir(dir: string): void {
|
|
53
|
-
try {
|
|
54
|
-
if (fs.existsSync(dir)) {
|
|
55
|
-
fs.rmSync(dir, { recursive: true, force: true });
|
|
56
|
-
}
|
|
57
|
-
} catch {
|
|
58
|
-
// Ignore
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/** Set up a fully deployable reader-family checkpoint and bind to local-reader */
|
|
63
|
-
function setupReaderDeployment(tmpDir: string, routingEnabled = false): string {
|
|
64
|
-
const run = registerTrainingRun(tmpDir, {
|
|
65
|
-
targetModelFamily: 'claude-reader-latest',
|
|
66
|
-
datasetFingerprint: 'sha256-rdr',
|
|
67
|
-
exportId: 'export-rdr',
|
|
68
|
-
sampleCount: 10,
|
|
69
|
-
configFingerprint: 'cfg-v1',
|
|
70
|
-
});
|
|
71
|
-
const ck = registerCheckpoint(tmpDir, {
|
|
72
|
-
trainRunId: run.trainRunId,
|
|
73
|
-
targetModelFamily: 'claude-reader-latest',
|
|
74
|
-
artifactPath: '/ck/reader.safetensors',
|
|
75
|
-
});
|
|
76
|
-
attachEvalSummary(tmpDir, ck.checkpointId, {
|
|
77
|
-
evalId: 'eval-rdr',
|
|
78
|
-
checkpointId: ck.checkpointId,
|
|
79
|
-
targetModelFamily: 'claude-reader-latest',
|
|
80
|
-
benchmarkId: 'bench',
|
|
81
|
-
mode: 'reduced_prompt',
|
|
82
|
-
baselineScore: 0.5,
|
|
83
|
-
candidateScore: 0.65,
|
|
84
|
-
delta: 0.15,
|
|
85
|
-
verdict: 'pass',
|
|
86
|
-
});
|
|
87
|
-
startTrainingRun(tmpDir, run.trainRunId);
|
|
88
|
-
completeTrainingRun(tmpDir, run.trainRunId);
|
|
89
|
-
markCheckpointDeployable(tmpDir, ck.checkpointId, true);
|
|
90
|
-
advancePromotion(tmpDir, {
|
|
91
|
-
checkpointId: ck.checkpointId,
|
|
92
|
-
targetProfile: 'local-reader',
|
|
93
|
-
baselineMetrics: DEFAULT_BASELINE_METRICS,
|
|
94
|
-
orchestratorReviewPassed: true,
|
|
95
|
-
reviewNote: 'Test approval',
|
|
96
|
-
});
|
|
97
|
-
bindCheckpointToWorkerProfile(tmpDir, 'local-reader', ck.checkpointId, 'reader deployment');
|
|
98
|
-
if (routingEnabled) {
|
|
99
|
-
enableRoutingForProfile(tmpDir, 'local-reader');
|
|
100
|
-
}
|
|
101
|
-
return ck.checkpointId;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/** Set up a fully deployable editor-family checkpoint and bind to local-editor */
|
|
105
|
-
function setupEditorDeployment(tmpDir: string, routingEnabled = false): string {
|
|
106
|
-
const run = registerTrainingRun(tmpDir, {
|
|
107
|
-
targetModelFamily: 'gpt-editor-v4',
|
|
108
|
-
datasetFingerprint: 'sha256-edt',
|
|
109
|
-
exportId: 'export-edt',
|
|
110
|
-
sampleCount: 10,
|
|
111
|
-
configFingerprint: 'cfg-v1',
|
|
112
|
-
});
|
|
113
|
-
const ck = registerCheckpoint(tmpDir, {
|
|
114
|
-
trainRunId: run.trainRunId,
|
|
115
|
-
targetModelFamily: 'gpt-editor-v4',
|
|
116
|
-
artifactPath: '/ck/editor.safetensors',
|
|
117
|
-
});
|
|
118
|
-
attachEvalSummary(tmpDir, ck.checkpointId, {
|
|
119
|
-
evalId: 'eval-edt',
|
|
120
|
-
checkpointId: ck.checkpointId,
|
|
121
|
-
targetModelFamily: 'gpt-editor-v4',
|
|
122
|
-
benchmarkId: 'bench',
|
|
123
|
-
mode: 'reduced_prompt',
|
|
124
|
-
baselineScore: 0.5,
|
|
125
|
-
candidateScore: 0.7,
|
|
126
|
-
delta: 0.2,
|
|
127
|
-
verdict: 'pass',
|
|
128
|
-
});
|
|
129
|
-
startTrainingRun(tmpDir, run.trainRunId);
|
|
130
|
-
completeTrainingRun(tmpDir, run.trainRunId);
|
|
131
|
-
markCheckpointDeployable(tmpDir, ck.checkpointId, true);
|
|
132
|
-
advancePromotion(tmpDir, {
|
|
133
|
-
checkpointId: ck.checkpointId,
|
|
134
|
-
targetProfile: 'local-editor',
|
|
135
|
-
baselineMetrics: DEFAULT_BASELINE_METRICS,
|
|
136
|
-
orchestratorReviewPassed: true,
|
|
137
|
-
reviewNote: 'Test approval',
|
|
138
|
-
});
|
|
139
|
-
bindCheckpointToWorkerProfile(tmpDir, 'local-editor', ck.checkpointId, 'editor deployment');
|
|
140
|
-
if (routingEnabled) {
|
|
141
|
-
enableRoutingForProfile(tmpDir, 'local-editor');
|
|
142
|
-
}
|
|
143
|
-
return ck.checkpointId;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
describe('LocalWorkerRouting reader_eligible classification', () => {
|
|
147
|
-
let tmpDir: string;
|
|
148
|
-
beforeEach(() => { tmpDir = makeTmpDir(); });
|
|
149
|
-
afterEach(() => { rmdir(tmpDir); });
|
|
150
|
-
|
|
151
|
-
it('classifies "read_file" taskIntent as reader_eligible', () => {
|
|
152
|
-
const decision = classifyTask({ taskIntent: 'read_file', taskDescription: 'Read the config file' }, tmpDir);
|
|
153
|
-
expect(decision.classification).toBe('deployment_unavailable'); // eligible but no deployment
|
|
154
|
-
expect(decision.decision).toBe('stay_main'); // No deployment
|
|
155
|
-
expect(decision.deploymentCheck.performed).toBe(true);
|
|
156
|
-
expect(decision.deploymentCheck.routingEnabled).toBe(false);
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
it('classifies "grep" taskIntent as reader_eligible', () => {
|
|
160
|
-
const decision = classifyTask({ taskIntent: 'grep', taskDescription: 'Find all occurrences of foo in src/' }, tmpDir);
|
|
161
|
-
expect(decision.classification).toBe('deployment_unavailable');
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
it('classifies "summarize" taskIntent as reader_eligible', () => {
|
|
165
|
-
const decision = classifyTask({ taskIntent: 'summarize', taskDescription: 'Summarize the changelog' }, tmpDir);
|
|
166
|
-
expect(decision.classification).toBe('deployment_unavailable');
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
it('classifies "inspect" keyword in description as reader_eligible', () => {
|
|
170
|
-
const decision = classifyTask({ taskIntent: 'read', taskDescription: 'inspect the package.json for dependencies' }, tmpDir);
|
|
171
|
-
expect(decision.classification).toBe('deployment_unavailable');
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
it('classifies with only taskIntent (no description) as reader_eligible', () => {
|
|
175
|
-
const decision = classifyTask({ taskIntent: 'grep' }, tmpDir);
|
|
176
|
-
expect(decision.classification).toBe('deployment_unavailable');
|
|
177
|
-
});
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
// ---------------------------------------------------------------------------
|
|
181
|
-
// Tests: Editor-Eligible Classification (no deployment)
|
|
182
|
-
// ---------------------------------------------------------------------------
|
|
183
|
-
|
|
184
|
-
describe('LocalWorkerRouting editor_eligible classification', () => {
|
|
185
|
-
let tmpDir: string;
|
|
186
|
-
beforeEach(() => { tmpDir = makeTmpDir(); });
|
|
187
|
-
afterEach(() => { rmdir(tmpDir); });
|
|
188
|
-
|
|
189
|
-
it('classifies "edit" taskIntent as editor_eligible', () => {
|
|
190
|
-
const decision = classifyTask({ taskIntent: 'edit_file', taskDescription: 'Edit the config to add new key' }, tmpDir);
|
|
191
|
-
expect(decision.classification).toBe('deployment_unavailable'); // eligible but no deployment
|
|
192
|
-
expect(decision.decision).toBe('stay_main'); // No deployment
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
it('classifies "fix" taskIntent as editor_eligible', () => {
|
|
196
|
-
const decision = classifyTask({ taskIntent: 'fix', taskDescription: 'Fix the typo in README.md' }, tmpDir);
|
|
197
|
-
expect(decision.classification).toBe('deployment_unavailable');
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
it('classifies "replace" keyword in description as editor_eligible', () => {
|
|
201
|
-
const decision = classifyTask({ taskIntent: 'replace', taskDescription: 'Replace all old API calls with new ones' }, tmpDir);
|
|
202
|
-
expect(decision.classification).toBe('deployment_unavailable');
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
it('classifies "add" keyword as editor_eligible', () => {
|
|
206
|
-
const decision = classifyTask({ taskIntent: 'add', taskDescription: 'add logging to the function' }, tmpDir);
|
|
207
|
-
expect(decision.classification).toBe('deployment_unavailable');
|
|
208
|
-
});
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
// ---------------------------------------------------------------------------
|
|
212
|
-
// Tests: High-Entropy Rejection
|
|
213
|
-
// ---------------------------------------------------------------------------
|
|
214
|
-
|
|
215
|
-
describe('LocalWorkerRouting high_entropy_disallowed', () => {
|
|
216
|
-
let tmpDir: string;
|
|
217
|
-
beforeEach(() => { tmpDir = makeTmpDir(); });
|
|
218
|
-
afterEach(() => { rmdir(tmpDir); });
|
|
219
|
-
|
|
220
|
-
it('rejects "design" taskIntent as high_entropy', () => {
|
|
221
|
-
const decision = classifyTask({ taskIntent: 'design_system', taskDescription: 'Design the new architecture' }, tmpDir);
|
|
222
|
-
expect(decision.classification).toBe('high_entropy_disallowed');
|
|
223
|
-
expect(decision.decision).toBe('stay_main');
|
|
224
|
-
expect(decision.blockers.length).toBeGreaterThan(0);
|
|
225
|
-
});
|
|
226
|
-
|
|
227
|
-
it('rejects "plan" keyword as high_entropy', () => {
|
|
228
|
-
const decision = classifyTask({ taskIntent: 'plan', taskDescription: 'Plan the refactoring approach' }, tmpDir);
|
|
229
|
-
expect(decision.classification).toBe('high_entropy_disallowed');
|
|
230
|
-
});
|
|
231
|
-
|
|
232
|
-
it('rejects "architect" keyword as high_entropy', () => {
|
|
233
|
-
const decision = classifyTask({ taskIntent: 'architect', taskDescription: 'Architect the microservices layout' }, tmpDir);
|
|
234
|
-
expect(decision.classification).toBe('high_entropy_disallowed');
|
|
235
|
-
});
|
|
236
|
-
|
|
237
|
-
it('rejects "research" keyword as high_entropy', () => {
|
|
238
|
-
const decision = classifyTask({ taskIntent: 'research', taskDescription: 'Research the best approach for this problem' }, tmpDir);
|
|
239
|
-
expect(decision.classification).toBe('high_entropy_disallowed');
|
|
240
|
-
});
|
|
241
|
-
|
|
242
|
-
it('rejects "investigate" keyword as high_entropy', () => {
|
|
243
|
-
const decision = classifyTask({ taskIntent: 'investigate', taskDescription: 'Investigate the memory leak' }, tmpDir);
|
|
244
|
-
// Note: "investigate" is high entropy but "fix" is editor-eligible
|
|
245
|
-
expect(decision.classification).toBe('high_entropy_disallowed');
|
|
246
|
-
});
|
|
247
|
-
|
|
248
|
-
it('rejects complexity hint "multi_step" as high_entropy', () => {
|
|
249
|
-
const decision = classifyTask({
|
|
250
|
-
taskIntent: 'fix',
|
|
251
|
-
taskDescription: 'Fix the bug',
|
|
252
|
-
complexityHints: ['multi_step', 'cross_file'],
|
|
253
|
-
}, tmpDir);
|
|
254
|
-
expect(decision.classification).toBe('high_entropy_disallowed');
|
|
255
|
-
});
|
|
256
|
-
|
|
257
|
-
it('rejects "ambiguous" complexity hint as high_entropy', () => {
|
|
258
|
-
const decision = classifyTask({
|
|
259
|
-
taskIntent: 'fix',
|
|
260
|
-
taskDescription: 'Improve the code',
|
|
261
|
-
complexityHints: ['ambiguous'],
|
|
262
|
-
}, tmpDir);
|
|
263
|
-
expect(decision.classification).toBe('high_entropy_disallowed');
|
|
264
|
-
});
|
|
265
|
-
|
|
266
|
-
it('high_entropy blocks even with editor-eligible keywords', () => {
|
|
267
|
-
// "design" + "edit" → high_entropy wins
|
|
268
|
-
const decision = classifyTask({
|
|
269
|
-
taskIntent: 'edit',
|
|
270
|
-
taskDescription: 'design and edit the new module',
|
|
271
|
-
}, tmpDir);
|
|
272
|
-
expect(decision.classification).toBe('high_entropy_disallowed');
|
|
273
|
-
});
|
|
274
|
-
|
|
275
|
-
it('rejects large-scale multi-file editing (4+ files) as high_entropy', () => {
|
|
276
|
-
// Bounded scope: 1-3 files → editor_eligible
|
|
277
|
-
// Too broad: 4+ files → high_entropy_disallowed (requires main agent coordination)
|
|
278
|
-
const decision = classifyTask({
|
|
279
|
-
taskIntent: 'edit',
|
|
280
|
-
taskDescription: 'Fix the bug across multiple modules',
|
|
281
|
-
requestedFiles: [
|
|
282
|
-
'src/auth/login.ts',
|
|
283
|
-
'src/auth/session.ts',
|
|
284
|
-
'src/auth/middleware.ts',
|
|
285
|
-
'src/auth/guards.ts',
|
|
286
|
-
],
|
|
287
|
-
}, tmpDir);
|
|
288
|
-
expect(decision.classification).toBe('high_entropy_disallowed');
|
|
289
|
-
expect(decision.blockers[0]).toContain('large-scale multi-file edit');
|
|
290
|
-
expect(decision.decision).toBe('stay_main');
|
|
291
|
-
});
|
|
292
|
-
|
|
293
|
-
it('allows bounded multi-file editing (1-3 files) as editor_eligible', () => {
|
|
294
|
-
const decision = classifyTask({
|
|
295
|
-
taskIntent: 'edit',
|
|
296
|
-
taskDescription: 'Fix the bug in auth files',
|
|
297
|
-
requestedFiles: [
|
|
298
|
-
'src/auth/login.ts',
|
|
299
|
-
'src/auth/session.ts',
|
|
300
|
-
],
|
|
301
|
-
}, tmpDir);
|
|
302
|
-
// Raw classification is editor_eligible; no deployment → final is deployment_unavailable
|
|
303
|
-
expect(decision.classification).toBe('deployment_unavailable');
|
|
304
|
-
expect(decision.decision).toBe('stay_main'); // no deployment
|
|
305
|
-
});
|
|
306
|
-
});
|
|
307
|
-
|
|
308
|
-
// ---------------------------------------------------------------------------
|
|
309
|
-
// Tests: Risk Disallowed
|
|
310
|
-
// ---------------------------------------------------------------------------
|
|
311
|
-
|
|
312
|
-
describe('LocalWorkerRouting risk_disallowed', () => {
|
|
313
|
-
let tmpDir: string;
|
|
314
|
-
beforeEach(() => { tmpDir = makeTmpDir(); });
|
|
315
|
-
afterEach(() => { rmdir(tmpDir); });
|
|
316
|
-
|
|
317
|
-
it('rejects bash tool as risk', () => {
|
|
318
|
-
const decision = classifyTask({
|
|
319
|
-
taskIntent: 'run',
|
|
320
|
-
taskDescription: 'Execute a bash command',
|
|
321
|
-
requestedTools: ['bash'],
|
|
322
|
-
}, tmpDir);
|
|
323
|
-
expect(decision.classification).toBe('risk_disallowed');
|
|
324
|
-
expect(decision.decision).toBe('stay_main');
|
|
325
|
-
expect(decision.blockers).toContain('risk tool requested (bash/exec/sudo/DROP/DELETE)');
|
|
326
|
-
});
|
|
327
|
-
|
|
328
|
-
it('rejects rm/destroy tools as risk', () => {
|
|
329
|
-
const decision = classifyTask({
|
|
330
|
-
taskIntent: 'process',
|
|
331
|
-
requestedTools: ['rm', 'delete'],
|
|
332
|
-
riskSignals: ['destructive'],
|
|
333
|
-
}, tmpDir);
|
|
334
|
-
expect(decision.classification).toBe('risk_disallowed');
|
|
335
|
-
});
|
|
336
|
-
|
|
337
|
-
it('rejects production file as risk', () => {
|
|
338
|
-
const decision = classifyTask({
|
|
339
|
-
taskIntent: 'edit',
|
|
340
|
-
requestedFiles: ['production-config.yaml', '.env'],
|
|
341
|
-
}, tmpDir);
|
|
342
|
-
expect(decision.classification).toBe('risk_disallowed');
|
|
343
|
-
expect(decision.blockers).toContain('risk file pattern detected (production/secrets/.git/node_modules)');
|
|
344
|
-
});
|
|
345
|
-
|
|
346
|
-
it('rejects .git/config as risk file', () => {
|
|
347
|
-
const decision = classifyTask({
|
|
348
|
-
taskIntent: 'edit',
|
|
349
|
-
requestedFiles: ['.git/config'],
|
|
350
|
-
}, tmpDir);
|
|
351
|
-
expect(decision.classification).toBe('risk_disallowed');
|
|
352
|
-
});
|
|
353
|
-
|
|
354
|
-
it('rejects explicit riskSignals as risk', () => {
|
|
355
|
-
const decision = classifyTask({
|
|
356
|
-
taskIntent: 'edit',
|
|
357
|
-
riskSignals: ['destructive', 'irreversible'],
|
|
358
|
-
}, tmpDir);
|
|
359
|
-
expect(decision.classification).toBe('risk_disallowed');
|
|
360
|
-
expect(decision.blockers).toContain('risk tool requested (bash/exec/sudo/DROP/DELETE)');
|
|
361
|
-
});
|
|
362
|
-
|
|
363
|
-
it('risk blocks even reader-eligible tasks', () => {
|
|
364
|
-
// bash + read → risk wins
|
|
365
|
-
const decision = classifyTask({
|
|
366
|
-
taskIntent: 'grep',
|
|
367
|
-
taskDescription: 'Search for pattern in files',
|
|
368
|
-
requestedTools: ['bash'],
|
|
369
|
-
}, tmpDir);
|
|
370
|
-
expect(decision.classification).toBe('risk_disallowed');
|
|
371
|
-
});
|
|
372
|
-
|
|
373
|
-
it('rejects node_modules as risk file', () => {
|
|
374
|
-
const decision = classifyTask({
|
|
375
|
-
taskIntent: 'edit',
|
|
376
|
-
requestedFiles: ['node_modules/some/package.json'],
|
|
377
|
-
}, tmpDir);
|
|
378
|
-
expect(decision.classification).toBe('risk_disallowed');
|
|
379
|
-
});
|
|
380
|
-
});
|
|
381
|
-
|
|
382
|
-
// ---------------------------------------------------------------------------
|
|
383
|
-
// Tests: Ambiguous Scope
|
|
384
|
-
// ---------------------------------------------------------------------------
|
|
385
|
-
|
|
386
|
-
describe('LocalWorkerRouting ambiguous_scope', () => {
|
|
387
|
-
let tmpDir: string;
|
|
388
|
-
beforeEach(() => { tmpDir = makeTmpDir(); });
|
|
389
|
-
afterEach(() => { rmdir(tmpDir); });
|
|
390
|
-
|
|
391
|
-
it('rejects very short generic taskDescription as ambiguous', () => {
|
|
392
|
-
const decision = classifyTask({ taskIntent: 'process', taskDescription: 'fix it' }, tmpDir);
|
|
393
|
-
expect(decision.classification).toBe('ambiguous_scope');
|
|
394
|
-
});
|
|
395
|
-
|
|
396
|
-
it('rejects "todo" as ambiguous', () => {
|
|
397
|
-
const decision = classifyTask({ taskIntent: 'todo', taskDescription: 'todo' }, tmpDir);
|
|
398
|
-
expect(decision.classification).toBe('ambiguous_scope');
|
|
399
|
-
});
|
|
400
|
-
|
|
401
|
-
it('rejects "improve" as ambiguous', () => {
|
|
402
|
-
const decision = classifyTask({ taskIntent: 'improve', taskDescription: 'improve' }, tmpDir);
|
|
403
|
-
expect(decision.classification).toBe('ambiguous_scope');
|
|
404
|
-
});
|
|
405
|
-
|
|
406
|
-
it('rejects open-ended question words as ambiguous', () => {
|
|
407
|
-
const decision = classifyTask({
|
|
408
|
-
taskIntent: 'analyze',
|
|
409
|
-
taskDescription: 'Should we refactor this or rewrite it?',
|
|
410
|
-
}, tmpDir);
|
|
411
|
-
expect(decision.classification).toBe('ambiguous_scope');
|
|
412
|
-
expect(decision.blockers).toContain('open-ended question words detected');
|
|
413
|
-
});
|
|
414
|
-
|
|
415
|
-
it('rejects when no intent and no description', () => {
|
|
416
|
-
const decision = classifyTask({}, tmpDir);
|
|
417
|
-
expect(decision.classification).toBe('ambiguous_scope');
|
|
418
|
-
});
|
|
419
|
-
|
|
420
|
-
it('does NOT classify a detailed description as ambiguous', () => {
|
|
421
|
-
const decision = classifyTask({
|
|
422
|
-
taskIntent: 'fix',
|
|
423
|
-
taskDescription: 'Fix the null pointer exception thrown when parsing the config file in parseConfig()',
|
|
424
|
-
}, tmpDir);
|
|
425
|
-
// Task is editor_eligible (fix keyword in intent and description)
|
|
426
|
-
// No deployment exists, so final classification is deployment_unavailable
|
|
427
|
-
expect(decision.classification).toBe('deployment_unavailable');
|
|
428
|
-
expect(decision.decision).toBe('stay_main'); // no deployment exists
|
|
429
|
-
});
|
|
430
|
-
|
|
431
|
-
it('DEBUG: detailed fix description classification', () => {
|
|
432
|
-
const decision = classifyTask({
|
|
433
|
-
taskIntent: 'fix',
|
|
434
|
-
taskDescription: 'Fix the null pointer exception thrown when parsing the config file in parseConfig()',
|
|
435
|
-
}, tmpDir);
|
|
436
|
-
// Same as above — editor_eligible but no deployment → deployment_unavailable
|
|
437
|
-
expect(decision.classification).toBe('deployment_unavailable');
|
|
438
|
-
});
|
|
439
|
-
});
|
|
440
|
-
|
|
441
|
-
// ---------------------------------------------------------------------------
|
|
442
|
-
// Tests: Deployment Availability (no deployment at all)
|
|
443
|
-
// ---------------------------------------------------------------------------
|
|
444
|
-
|
|
445
|
-
describe('LocalWorkerRouting deployment_unavailable', () => {
|
|
446
|
-
let tmpDir: string;
|
|
447
|
-
beforeEach(() => { tmpDir = makeTmpDir(); });
|
|
448
|
-
afterEach(() => { rmdir(tmpDir); });
|
|
449
|
-
|
|
450
|
-
it('returns deployment_unavailable when no deployment exists', () => {
|
|
451
|
-
const decision = classifyTask({ taskIntent: 'read_file', taskDescription: 'read the config' }, tmpDir);
|
|
452
|
-
expect(decision.classification).toBe('deployment_unavailable');
|
|
453
|
-
expect(decision.decision).toBe('stay_main');
|
|
454
|
-
expect(decision.deploymentCheck.performed).toBe(true);
|
|
455
|
-
expect(decision.deploymentCheck.routingEnabled).toBe(false);
|
|
456
|
-
});
|
|
457
|
-
});
|
|
458
|
-
|
|
459
|
-
// ---------------------------------------------------------------------------
|
|
460
|
-
// Tests: Routing with Enabled Deployment
|
|
461
|
-
// ---------------------------------------------------------------------------
|
|
462
|
-
|
|
463
|
-
describe('LocalWorkerRouting with enabled deployment', () => {
|
|
464
|
-
let tmpDir: string;
|
|
465
|
-
beforeEach(() => { tmpDir = makeTmpDir(); });
|
|
466
|
-
afterEach(() => { rmdir(tmpDir); });
|
|
467
|
-
|
|
468
|
-
it('reader task routes to local-reader when deployment is enabled', () => {
|
|
469
|
-
setupReaderDeployment(tmpDir, true);
|
|
470
|
-
|
|
471
|
-
const decision = classifyTask({ taskIntent: 'read_file', taskDescription: 'read the config' }, tmpDir);
|
|
472
|
-
|
|
473
|
-
expect(decision.decision).toBe('route_local');
|
|
474
|
-
expect(decision.targetProfile).toBe('local-reader');
|
|
475
|
-
expect(decision.classification).toBe('reader_eligible');
|
|
476
|
-
expect(decision.deploymentCheck.routingEnabled).toBe(true);
|
|
477
|
-
expect(decision.blockers).toHaveLength(0);
|
|
478
|
-
});
|
|
479
|
-
|
|
480
|
-
it('editor task routes to local-editor when deployment is enabled', () => {
|
|
481
|
-
setupEditorDeployment(tmpDir, true);
|
|
482
|
-
|
|
483
|
-
const decision = classifyTask({ taskIntent: 'edit', taskDescription: 'edit the config' }, tmpDir);
|
|
484
|
-
|
|
485
|
-
expect(decision.decision).toBe('route_local');
|
|
486
|
-
expect(decision.targetProfile).toBe('local-editor');
|
|
487
|
-
expect(decision.classification).toBe('editor_eligible');
|
|
488
|
-
expect(decision.blockers).toHaveLength(0);
|
|
489
|
-
});
|
|
490
|
-
|
|
491
|
-
it('reader task still blocked as high_entropy even with enabled deployment', () => {
|
|
492
|
-
setupReaderDeployment(tmpDir, true);
|
|
493
|
-
|
|
494
|
-
const decision = classifyTask({
|
|
495
|
-
taskIntent: 'design',
|
|
496
|
-
taskDescription: 'Design the new system architecture',
|
|
497
|
-
}, tmpDir);
|
|
498
|
-
|
|
499
|
-
expect(decision.decision).toBe('stay_main');
|
|
500
|
-
expect(decision.classification).toBe('high_entropy_disallowed');
|
|
501
|
-
});
|
|
502
|
-
|
|
503
|
-
it('reader task blocked as risk even with enabled deployment', () => {
|
|
504
|
-
setupReaderDeployment(tmpDir, true);
|
|
505
|
-
|
|
506
|
-
const decision = classifyTask({
|
|
507
|
-
taskIntent: 'grep',
|
|
508
|
-
requestedTools: ['bash'],
|
|
509
|
-
}, tmpDir);
|
|
510
|
-
|
|
511
|
-
expect(decision.decision).toBe('stay_main');
|
|
512
|
-
expect(decision.classification).toBe('risk_disallowed');
|
|
513
|
-
});
|
|
514
|
-
});
|
|
515
|
-
|
|
516
|
-
// ---------------------------------------------------------------------------
|
|
517
|
-
// Tests: Routing with Disabled Deployment
|
|
518
|
-
// ---------------------------------------------------------------------------
|
|
519
|
-
|
|
520
|
-
describe('LocalWorkerRouting with disabled deployment (routing=false)', () => {
|
|
521
|
-
let tmpDir: string;
|
|
522
|
-
beforeEach(() => { tmpDir = makeTmpDir(); });
|
|
523
|
-
afterEach(() => { rmdir(tmpDir); });
|
|
524
|
-
|
|
525
|
-
it('reader-eligible task stays_main when routing is disabled', () => {
|
|
526
|
-
setupReaderDeployment(tmpDir, false); // routingEnabled = false
|
|
527
|
-
|
|
528
|
-
const decision = classifyTask({ taskIntent: 'read_file', taskDescription: 'read the config' }, tmpDir);
|
|
529
|
-
|
|
530
|
-
expect(decision.decision).toBe('stay_main');
|
|
531
|
-
expect(decision.classification).toBe('deployment_unavailable');
|
|
532
|
-
expect(decision.deploymentCheck.routingEnabled).toBe(false);
|
|
533
|
-
expect(decision.reason).toContain('routing is not enabled');
|
|
534
|
-
});
|
|
535
|
-
|
|
536
|
-
it('editor-eligible task stays_main when routing is disabled', () => {
|
|
537
|
-
setupEditorDeployment(tmpDir, false);
|
|
538
|
-
|
|
539
|
-
const decision = classifyTask({ taskIntent: 'edit', taskDescription: 'edit the file' }, tmpDir);
|
|
540
|
-
|
|
541
|
-
expect(decision.decision).toBe('stay_main');
|
|
542
|
-
expect(decision.classification).toBe('deployment_unavailable');
|
|
543
|
-
});
|
|
544
|
-
|
|
545
|
-
it('re-enabling routing allows route_local again', () => {
|
|
546
|
-
setupReaderDeployment(tmpDir, false);
|
|
547
|
-
enableRoutingForProfile(tmpDir, 'local-reader');
|
|
548
|
-
|
|
549
|
-
const decision = classifyTask({ taskIntent: 'read_file' }, tmpDir);
|
|
550
|
-
expect(decision.decision).toBe('route_local');
|
|
551
|
-
expect(decision.targetProfile).toBe('local-reader');
|
|
552
|
-
});
|
|
553
|
-
|
|
554
|
-
it('stays_main when active checkpoint has been revoked (no longer deployable)', () => {
|
|
555
|
-
// Set up deployment with routing enabled
|
|
556
|
-
const ckId = setupReaderDeployment(tmpDir, true);
|
|
557
|
-
|
|
558
|
-
// Verify it routes successfully first
|
|
559
|
-
const before = classifyTask({ taskIntent: 'read_file' }, tmpDir);
|
|
560
|
-
expect(before.decision).toBe('route_local');
|
|
561
|
-
|
|
562
|
-
// Revoke the checkpoint — it no longer passes evaluation
|
|
563
|
-
markCheckpointDeployable(tmpDir, ckId, false);
|
|
564
|
-
|
|
565
|
-
// Routing must now be blocked — governance: revoked checkpoints must not be used
|
|
566
|
-
const after = classifyTask({ taskIntent: 'read_file' }, tmpDir);
|
|
567
|
-
expect(after.decision).toBe('stay_main');
|
|
568
|
-
expect(after.classification).toBe('deployment_unavailable');
|
|
569
|
-
expect(after.deploymentCheck.checkpointDeployable).toBe(false);
|
|
570
|
-
expect(after.blockers.some((b: string) => b.includes('no longer deployable'))).toBe(true);
|
|
571
|
-
});
|
|
572
|
-
});
|
|
573
|
-
|
|
574
|
-
// ---------------------------------------------------------------------------
|
|
575
|
-
// Tests: canRouteToProfile helper
|
|
576
|
-
// ---------------------------------------------------------------------------
|
|
577
|
-
|
|
578
|
-
describe('LocalWorkerRouting canRouteToProfile', () => {
|
|
579
|
-
let tmpDir: string;
|
|
580
|
-
beforeEach(() => { tmpDir = makeTmpDir(); });
|
|
581
|
-
afterEach(() => { rmdir(tmpDir); });
|
|
582
|
-
|
|
583
|
-
it('returns true when profile has enabled deployment and task is eligible', () => {
|
|
584
|
-
setupReaderDeployment(tmpDir, true);
|
|
585
|
-
|
|
586
|
-
const result = canRouteToProfile({ taskIntent: 'read_file', taskDescription: 'read config' }, tmpDir, 'local-reader');
|
|
587
|
-
expect(result).toBe(true);
|
|
588
|
-
});
|
|
589
|
-
|
|
590
|
-
it('returns false when no deployment exists', () => {
|
|
591
|
-
const result = canRouteToProfile({ taskIntent: 'read_file' }, tmpDir, 'local-reader');
|
|
592
|
-
expect(result).toBe(false);
|
|
593
|
-
});
|
|
594
|
-
|
|
595
|
-
it('returns false when task is high-entropy', () => {
|
|
596
|
-
setupReaderDeployment(tmpDir, true);
|
|
597
|
-
|
|
598
|
-
const result = canRouteToProfile({ taskIntent: 'design', taskDescription: 'Design the system' }, tmpDir, 'local-reader');
|
|
599
|
-
expect(result).toBe(false);
|
|
600
|
-
});
|
|
601
|
-
|
|
602
|
-
it('returns false when routing is disabled', () => {
|
|
603
|
-
setupReaderDeployment(tmpDir, false);
|
|
604
|
-
|
|
605
|
-
const result = canRouteToProfile({ taskIntent: 'read_file' }, tmpDir, 'local-reader');
|
|
606
|
-
expect(result).toBe(false);
|
|
607
|
-
});
|
|
608
|
-
|
|
609
|
-
it('returns false for editor profile on reader task', () => {
|
|
610
|
-
setupEditorDeployment(tmpDir, true);
|
|
611
|
-
|
|
612
|
-
const result = canRouteToProfile({ taskIntent: 'read_file', taskDescription: 'read config' }, tmpDir, 'local-editor');
|
|
613
|
-
expect(result).toBe(false);
|
|
614
|
-
});
|
|
615
|
-
});
|
|
616
|
-
|
|
617
|
-
// ---------------------------------------------------------------------------
|
|
618
|
-
// Tests: isAnyLocalRoutingEnabled / listEnabledProfiles
|
|
619
|
-
// ---------------------------------------------------------------------------
|
|
620
|
-
|
|
621
|
-
describe('LocalWorkerRouting isAnyLocalRoutingEnabled / listEnabledProfiles', () => {
|
|
622
|
-
let tmpDir: string;
|
|
623
|
-
beforeEach(() => { tmpDir = makeTmpDir(); });
|
|
624
|
-
afterEach(() => { rmdir(tmpDir); });
|
|
625
|
-
|
|
626
|
-
it('returns false when no deployments exist', () => {
|
|
627
|
-
expect(isAnyLocalRoutingEnabled(tmpDir)).toBe(false);
|
|
628
|
-
expect(listEnabledProfiles(tmpDir)).toEqual([]);
|
|
629
|
-
});
|
|
630
|
-
|
|
631
|
-
it('returns false when deployments exist but routing is disabled', () => {
|
|
632
|
-
setupReaderDeployment(tmpDir, false);
|
|
633
|
-
setupEditorDeployment(tmpDir, false);
|
|
634
|
-
|
|
635
|
-
expect(isAnyLocalRoutingEnabled(tmpDir)).toBe(false);
|
|
636
|
-
expect(listEnabledProfiles(tmpDir)).toEqual([]);
|
|
637
|
-
});
|
|
638
|
-
|
|
639
|
-
it('returns true and lists profile when routing is enabled', () => {
|
|
640
|
-
setupReaderDeployment(tmpDir, true);
|
|
641
|
-
|
|
642
|
-
expect(isAnyLocalRoutingEnabled(tmpDir)).toBe(true);
|
|
643
|
-
expect(listEnabledProfiles(tmpDir)).toEqual(['local-reader']);
|
|
644
|
-
});
|
|
645
|
-
|
|
646
|
-
it('lists multiple enabled profiles', () => {
|
|
647
|
-
setupReaderDeployment(tmpDir, true);
|
|
648
|
-
setupEditorDeployment(tmpDir, true);
|
|
649
|
-
|
|
650
|
-
const enabled = listEnabledProfiles(tmpDir);
|
|
651
|
-
expect(enabled).toContain('local-reader');
|
|
652
|
-
expect(enabled).toContain('local-editor');
|
|
653
|
-
expect(enabled).toHaveLength(2);
|
|
654
|
-
});
|
|
655
|
-
|
|
656
|
-
it('only lists profiles with routing enabled (not just bound)', () => {
|
|
657
|
-
setupReaderDeployment(tmpDir, true); // enabled
|
|
658
|
-
setupEditorDeployment(tmpDir, false); // bound but disabled
|
|
659
|
-
|
|
660
|
-
const enabled = listEnabledProfiles(tmpDir);
|
|
661
|
-
expect(enabled).toEqual(['local-reader']);
|
|
662
|
-
});
|
|
663
|
-
});
|
|
664
|
-
|
|
665
|
-
// ---------------------------------------------------------------------------
|
|
666
|
-
// Tests: targetProfile override
|
|
667
|
-
// ---------------------------------------------------------------------------
|
|
668
|
-
|
|
669
|
-
describe('LocalWorkerRouting targetProfile override', () => {
|
|
670
|
-
let tmpDir: string;
|
|
671
|
-
beforeEach(() => { tmpDir = makeTmpDir(); });
|
|
672
|
-
afterEach(() => { rmdir(tmpDir); });
|
|
673
|
-
|
|
674
|
-
it('uses targetProfile from input when specified', () => {
|
|
675
|
-
setupEditorDeployment(tmpDir, true); // Only editor is enabled
|
|
676
|
-
|
|
677
|
-
// Reader deployment doesn't exist but we explicitly target reader
|
|
678
|
-
const decision = classifyTask({
|
|
679
|
-
taskIntent: 'read_file',
|
|
680
|
-
taskDescription: 'read the config',
|
|
681
|
-
targetProfile: 'local-reader',
|
|
682
|
-
}, tmpDir);
|
|
683
|
-
|
|
684
|
-
// Reader deployment doesn't exist → deployment_unavailable
|
|
685
|
-
expect(decision.decision).toBe('stay_main');
|
|
686
|
-
expect(decision.classification).toBe('deployment_unavailable');
|
|
687
|
-
});
|
|
688
|
-
|
|
689
|
-
it('rejects editor task targeting local-reader (profile-task mismatch)', () => {
|
|
690
|
-
// Both deployments enabled
|
|
691
|
-
setupReaderDeployment(tmpDir, true);
|
|
692
|
-
setupEditorDeployment(tmpDir, true);
|
|
693
|
-
|
|
694
|
-
// Editor-eligible task but explicitly targeting local-reader
|
|
695
|
-
const decision = classifyTask({
|
|
696
|
-
taskIntent: 'edit',
|
|
697
|
-
taskDescription: 'Edit the file',
|
|
698
|
-
targetProfile: 'local-reader',
|
|
699
|
-
}, tmpDir);
|
|
700
|
-
|
|
701
|
-
// MUST reject: editor task cannot route to reader profile
|
|
702
|
-
expect(decision.decision).toBe('stay_main');
|
|
703
|
-
expect(decision.classification).toBe('profile_mismatch');
|
|
704
|
-
expect(decision.blockers).toContainEqual(expect.stringContaining('profile mismatch'));
|
|
705
|
-
});
|
|
706
|
-
|
|
707
|
-
it('accepts editor task targeting local-editor (correct profile)', () => {
|
|
708
|
-
setupEditorDeployment(tmpDir, true);
|
|
709
|
-
|
|
710
|
-
const decision = classifyTask({
|
|
711
|
-
taskIntent: 'edit',
|
|
712
|
-
taskDescription: 'Edit the file',
|
|
713
|
-
targetProfile: 'local-editor',
|
|
714
|
-
}, tmpDir);
|
|
715
|
-
|
|
716
|
-
expect(decision.decision).toBe('route_local');
|
|
717
|
-
expect(decision.targetProfile).toBe('local-editor');
|
|
718
|
-
});
|
|
719
|
-
});
|
|
720
|
-
|
|
721
|
-
// ---------------------------------------------------------------------------
|
|
722
|
-
// Tests: Decision Explainability
|
|
723
|
-
// ---------------------------------------------------------------------------
|
|
724
|
-
|
|
725
|
-
describe('LocalWorkerRouting explainability', () => {
|
|
726
|
-
let tmpDir: string;
|
|
727
|
-
beforeEach(() => { tmpDir = makeTmpDir(); });
|
|
728
|
-
afterEach(() => { rmdir(tmpDir); });
|
|
729
|
-
|
|
730
|
-
it('always provides a reason string', () => {
|
|
731
|
-
setupReaderDeployment(tmpDir, true);
|
|
732
|
-
|
|
733
|
-
const decision = classifyTask({ taskIntent: 'read_file' }, tmpDir);
|
|
734
|
-
expect(typeof decision.reason).toBe('string');
|
|
735
|
-
expect(decision.reason.length).toBeGreaterThan(0);
|
|
736
|
-
});
|
|
737
|
-
|
|
738
|
-
it('blockers is empty when route_local', () => {
|
|
739
|
-
setupReaderDeployment(tmpDir, true);
|
|
740
|
-
|
|
741
|
-
const decision = classifyTask({ taskIntent: 'read_file' }, tmpDir);
|
|
742
|
-
expect(decision.blockers).toEqual([]);
|
|
743
|
-
});
|
|
744
|
-
|
|
745
|
-
it('blockers is non-empty when stay_main', () => {
|
|
746
|
-
const decision = classifyTask({ taskIntent: 'design', taskDescription: 'Design the system' }, tmpDir);
|
|
747
|
-
expect(decision.blockers.length).toBeGreaterThan(0);
|
|
748
|
-
});
|
|
749
|
-
|
|
750
|
-
it('provides deployment check details', () => {
|
|
751
|
-
const decision = classifyTask({ taskIntent: 'read_file' }, tmpDir);
|
|
752
|
-
expect(decision.deploymentCheck).toBeDefined();
|
|
753
|
-
expect('performed' in decision.deploymentCheck).toBe(true);
|
|
754
|
-
expect('profileAvailable' in decision.deploymentCheck).toBe(true);
|
|
755
|
-
expect('routingEnabled' in decision.deploymentCheck).toBe(true);
|
|
756
|
-
});
|
|
757
|
-
});
|