principles-disciple 1.17.0 → 1.18.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 +1 -1
- package/package.json +1 -1
- package/src/core/nocturnal-compliance.ts +1 -0
- package/src/core/nocturnal-trinity.ts +463 -140
- package/src/service/evolution-worker.ts +13 -6
- package/src/service/nocturnal-target-selector.ts +9 -2
- package/templates/langs/zh/skills/ai-sprint-orchestration/references/specs/nocturnal-trinity-quality-enhancement.json +111 -0
- package/templates/langs/zh/skills/ai-sprint-orchestration/scripts/lib/task-specs.mjs +1 -1
- package/templates/langs/zh/skills/ai-sprint-orchestration/scripts/run.mjs +1 -1
- package/tests/service/evolution-worker.nocturnal.test.ts +0 -547
|
@@ -71,225 +71,6 @@ describe('EvolutionWorkerService nocturnal hardening', () => {
|
|
|
71
71
|
EvolutionWorkerService.api = null;
|
|
72
72
|
});
|
|
73
73
|
|
|
74
|
-
it('does not start a nocturnal workflow when only an empty fallback snapshot is available', async () => {
|
|
75
|
-
const workspaceDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pd-nocturnal-empty-'));
|
|
76
|
-
const stateDir = path.join(workspaceDir, '.state');
|
|
77
|
-
fs.mkdirSync(path.join(stateDir, 'sessions'), { recursive: true });
|
|
78
|
-
fs.mkdirSync(path.join(stateDir, 'logs'), { recursive: true });
|
|
79
|
-
|
|
80
|
-
mockGetNocturnalSessionSnapshot.mockReturnValue(null);
|
|
81
|
-
|
|
82
|
-
fs.writeFileSync(
|
|
83
|
-
path.join(stateDir, 'evolution_queue.json'),
|
|
84
|
-
JSON.stringify([
|
|
85
|
-
{
|
|
86
|
-
id: 'sleep-empty',
|
|
87
|
-
taskKind: 'sleep_reflection',
|
|
88
|
-
priority: 'medium',
|
|
89
|
-
score: 50,
|
|
90
|
-
source: 'nocturnal',
|
|
91
|
-
reason: 'Sleep reflection',
|
|
92
|
-
timestamp: '2026-04-10T00:00:00.000Z',
|
|
93
|
-
enqueued_at: '2026-04-10T00:00:00.000Z',
|
|
94
|
-
status: 'pending',
|
|
95
|
-
retryCount: 0,
|
|
96
|
-
maxRetries: 1,
|
|
97
|
-
recentPainContext: {
|
|
98
|
-
mostRecent: null,
|
|
99
|
-
recentPainCount: 0,
|
|
100
|
-
recentMaxPainScore: 0,
|
|
101
|
-
},
|
|
102
|
-
},
|
|
103
|
-
], null, 2),
|
|
104
|
-
'utf8'
|
|
105
|
-
);
|
|
106
|
-
|
|
107
|
-
try {
|
|
108
|
-
EvolutionWorkerService.start({
|
|
109
|
-
workspaceDir,
|
|
110
|
-
stateDir,
|
|
111
|
-
logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() },
|
|
112
|
-
config: {},
|
|
113
|
-
} as any);
|
|
114
|
-
|
|
115
|
-
await vi.advanceTimersByTimeAsync(6000);
|
|
116
|
-
|
|
117
|
-
const queue = readQueue(stateDir);
|
|
118
|
-
expect(queue[0].status).toBe('failed');
|
|
119
|
-
expect(queue[0].lastError).toContain('invalid_snapshot_ingress');
|
|
120
|
-
expect(queue[0].lastError).toContain('fallback snapshot must contain at least one pain signal');
|
|
121
|
-
expect(queue[0].resultRef).toBeFalsy();
|
|
122
|
-
expect(mockStartWorkflow).not.toHaveBeenCalled();
|
|
123
|
-
} finally {
|
|
124
|
-
EvolutionWorkerService.stop({ workspaceDir, stateDir, logger: console } as any);
|
|
125
|
-
safeRmDir(workspaceDir);
|
|
126
|
-
}
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
it('uses stub_fallback for expected gateway-only background unavailability', async () => {
|
|
130
|
-
const workspaceDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pd-nocturnal-gateway-'));
|
|
131
|
-
const stateDir = path.join(workspaceDir, '.state');
|
|
132
|
-
fs.mkdirSync(path.join(stateDir, 'sessions'), { recursive: true });
|
|
133
|
-
fs.mkdirSync(path.join(stateDir, 'logs'), { recursive: true });
|
|
134
|
-
|
|
135
|
-
mockGetNocturnalSessionSnapshot.mockReturnValue({
|
|
136
|
-
sessionId: 'sleep-gateway',
|
|
137
|
-
startedAt: '2026-04-10T00:00:00.000Z',
|
|
138
|
-
updatedAt: '2026-04-10T00:01:00.000Z',
|
|
139
|
-
assistantTurns: [],
|
|
140
|
-
userTurns: [],
|
|
141
|
-
toolCalls: [],
|
|
142
|
-
painEvents: [],
|
|
143
|
-
gateBlocks: [],
|
|
144
|
-
stats: {
|
|
145
|
-
totalAssistantTurns: 1,
|
|
146
|
-
totalToolCalls: 1,
|
|
147
|
-
totalPainEvents: 0,
|
|
148
|
-
totalGateBlocks: 0,
|
|
149
|
-
failureCount: 0,
|
|
150
|
-
},
|
|
151
|
-
});
|
|
152
|
-
mockStartWorkflow.mockResolvedValue({ workflowId: 'wf-1', childSessionKey: 'child-1', state: 'active' });
|
|
153
|
-
mockGetWorkflowDebugSummary.mockResolvedValue({
|
|
154
|
-
state: 'terminal_error',
|
|
155
|
-
metadata: {},
|
|
156
|
-
recentEvents: [
|
|
157
|
-
{
|
|
158
|
-
reason: 'Error: Plugin runtime subagent methods are only available during a gateway request.',
|
|
159
|
-
payload: {},
|
|
160
|
-
},
|
|
161
|
-
],
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
EvolutionWorkerService.api = {
|
|
165
|
-
logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() },
|
|
166
|
-
runtime: {},
|
|
167
|
-
} as any;
|
|
168
|
-
|
|
169
|
-
fs.writeFileSync(
|
|
170
|
-
path.join(stateDir, 'evolution_queue.json'),
|
|
171
|
-
JSON.stringify([
|
|
172
|
-
{
|
|
173
|
-
id: 'sleep-gateway',
|
|
174
|
-
taskKind: 'sleep_reflection',
|
|
175
|
-
priority: 'medium',
|
|
176
|
-
score: 50,
|
|
177
|
-
source: 'nocturnal',
|
|
178
|
-
reason: 'Sleep reflection',
|
|
179
|
-
timestamp: '2026-04-10T00:00:00.000Z',
|
|
180
|
-
enqueued_at: '2026-04-10T00:00:00.000Z',
|
|
181
|
-
status: 'pending',
|
|
182
|
-
retryCount: 0,
|
|
183
|
-
maxRetries: 1,
|
|
184
|
-
recentPainContext: {
|
|
185
|
-
mostRecent: { score: 0.5, source: 'pain', reason: 'x', timestamp: '2026-04-10T00:00:00.000Z' },
|
|
186
|
-
recentPainCount: 1,
|
|
187
|
-
recentMaxPainScore: 0.5,
|
|
188
|
-
},
|
|
189
|
-
},
|
|
190
|
-
], null, 2),
|
|
191
|
-
'utf8'
|
|
192
|
-
);
|
|
193
|
-
|
|
194
|
-
try {
|
|
195
|
-
EvolutionWorkerService.start({
|
|
196
|
-
workspaceDir,
|
|
197
|
-
stateDir,
|
|
198
|
-
logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() },
|
|
199
|
-
config: {},
|
|
200
|
-
} as any);
|
|
201
|
-
|
|
202
|
-
await vi.advanceTimersByTimeAsync(6000);
|
|
203
|
-
|
|
204
|
-
const queue = readQueue(stateDir);
|
|
205
|
-
// #237: Expected gateway unavailability → stub_fallback (completed), not failed
|
|
206
|
-
// This is an environment limitation (daemon mode, cron job, etc.), not a real failure
|
|
207
|
-
expect(queue[0].status).toBe('completed');
|
|
208
|
-
expect(queue[0].resolution).toBe('stub_fallback');
|
|
209
|
-
expect(queue[0].lastError).toContain('gateway request');
|
|
210
|
-
} finally {
|
|
211
|
-
EvolutionWorkerService.stop({ workspaceDir, stateDir, logger: console } as any);
|
|
212
|
-
safeRmDir(workspaceDir);
|
|
213
|
-
}
|
|
214
|
-
});
|
|
215
|
-
|
|
216
|
-
it('uses stub_fallback for expected subagent runtime unavailability', async () => {
|
|
217
|
-
const workspaceDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pd-nocturnal-runtime-unavailable-'));
|
|
218
|
-
const stateDir = path.join(workspaceDir, '.state');
|
|
219
|
-
fs.mkdirSync(path.join(stateDir, 'sessions'), { recursive: true });
|
|
220
|
-
fs.mkdirSync(path.join(stateDir, 'logs'), { recursive: true });
|
|
221
|
-
|
|
222
|
-
mockGetNocturnalSessionSnapshot.mockReturnValue({
|
|
223
|
-
sessionId: 'sleep-runtime',
|
|
224
|
-
startedAt: '2026-04-10T00:00:00.000Z',
|
|
225
|
-
updatedAt: '2026-04-10T00:01:00.000Z',
|
|
226
|
-
assistantTurns: [],
|
|
227
|
-
userTurns: [],
|
|
228
|
-
toolCalls: [],
|
|
229
|
-
painEvents: [],
|
|
230
|
-
gateBlocks: [],
|
|
231
|
-
stats: {
|
|
232
|
-
totalAssistantTurns: 1,
|
|
233
|
-
totalToolCalls: 1,
|
|
234
|
-
totalPainEvents: 0,
|
|
235
|
-
totalGateBlocks: 0,
|
|
236
|
-
failureCount: 0,
|
|
237
|
-
},
|
|
238
|
-
});
|
|
239
|
-
mockStartWorkflow.mockRejectedValue(new Error('NocturnalWorkflowManager: subagent runtime unavailable'));
|
|
240
|
-
|
|
241
|
-
EvolutionWorkerService.api = {
|
|
242
|
-
logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() },
|
|
243
|
-
runtime: {},
|
|
244
|
-
} as any;
|
|
245
|
-
|
|
246
|
-
fs.writeFileSync(
|
|
247
|
-
path.join(stateDir, 'evolution_queue.json'),
|
|
248
|
-
JSON.stringify([
|
|
249
|
-
{
|
|
250
|
-
id: 'sleep-runtime',
|
|
251
|
-
taskKind: 'sleep_reflection',
|
|
252
|
-
priority: 'medium',
|
|
253
|
-
score: 50,
|
|
254
|
-
source: 'nocturnal',
|
|
255
|
-
reason: 'Sleep reflection',
|
|
256
|
-
timestamp: '2026-04-10T00:00:00.000Z',
|
|
257
|
-
enqueued_at: '2026-04-10T00:00:00.000Z',
|
|
258
|
-
status: 'pending',
|
|
259
|
-
retryCount: 0,
|
|
260
|
-
maxRetries: 1,
|
|
261
|
-
recentPainContext: {
|
|
262
|
-
mostRecent: { score: 0.5, source: 'pain', reason: 'x', timestamp: '2026-04-10T00:00:00.000Z', sessionId: 'sleep-runtime' },
|
|
263
|
-
recentPainCount: 1,
|
|
264
|
-
recentMaxPainScore: 0.5,
|
|
265
|
-
},
|
|
266
|
-
},
|
|
267
|
-
], null, 2),
|
|
268
|
-
'utf8'
|
|
269
|
-
);
|
|
270
|
-
|
|
271
|
-
try {
|
|
272
|
-
EvolutionWorkerService.start({
|
|
273
|
-
workspaceDir,
|
|
274
|
-
stateDir,
|
|
275
|
-
logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() },
|
|
276
|
-
config: {},
|
|
277
|
-
} as any);
|
|
278
|
-
|
|
279
|
-
await vi.advanceTimersByTimeAsync(6000);
|
|
280
|
-
|
|
281
|
-
const queue = readQueue(stateDir);
|
|
282
|
-
// #237: Expected subagent unavailability → stub_fallback (completed), not failed
|
|
283
|
-
// This is an environment limitation (daemon mode, process isolation, etc.), not a real failure
|
|
284
|
-
expect(queue[0].status).toBe('completed');
|
|
285
|
-
expect(queue[0].resolution).toBe('stub_fallback');
|
|
286
|
-
expect(queue[0].lastError).toContain('subagent runtime unavailable');
|
|
287
|
-
} finally {
|
|
288
|
-
EvolutionWorkerService.stop({ workspaceDir, stateDir, logger: console } as any);
|
|
289
|
-
safeRmDir(workspaceDir);
|
|
290
|
-
}
|
|
291
|
-
});
|
|
292
|
-
|
|
293
74
|
it('extracts session_id from .pain_flag file correctly', async () => {
|
|
294
75
|
const workspaceDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pd-pain-session-'));
|
|
295
76
|
const stateDir = path.join(workspaceDir, '.state');
|
|
@@ -346,213 +127,6 @@ score: 80`,
|
|
|
346
127
|
}
|
|
347
128
|
});
|
|
348
129
|
|
|
349
|
-
it('prioritizes pain signal session ID for snapshot extraction', async () => {
|
|
350
|
-
const workspaceDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pd-pain-priority-'));
|
|
351
|
-
const stateDir = path.join(workspaceDir, '.state');
|
|
352
|
-
fs.mkdirSync(stateDir, { recursive: true });
|
|
353
|
-
fs.mkdirSync(path.join(stateDir, 'sessions'), { recursive: true });
|
|
354
|
-
fs.mkdirSync(path.join(stateDir, 'logs'), { recursive: true });
|
|
355
|
-
|
|
356
|
-
// Mock extractor to succeed ONLY for the pain session ID
|
|
357
|
-
mockGetNocturnalSessionSnapshot.mockImplementation((sessionId: string) => {
|
|
358
|
-
if (sessionId === 'pain-session-id') {
|
|
359
|
-
return {
|
|
360
|
-
sessionId: 'pain-session-id',
|
|
361
|
-
startedAt: '2026-04-10T00:00:00.000Z',
|
|
362
|
-
updatedAt: '2026-04-10T00:01:00.000Z',
|
|
363
|
-
assistantTurns: [],
|
|
364
|
-
userTurns: [],
|
|
365
|
-
toolCalls: [],
|
|
366
|
-
painEvents: [],
|
|
367
|
-
gateBlocks: [],
|
|
368
|
-
stats: { totalToolCalls: 10, totalAssistantTurns: 5, failureCount: 2 },
|
|
369
|
-
stats: {
|
|
370
|
-
totalAssistantTurns: 5,
|
|
371
|
-
totalToolCalls: 10,
|
|
372
|
-
totalPainEvents: 0,
|
|
373
|
-
totalGateBlocks: 0,
|
|
374
|
-
failureCount: 2,
|
|
375
|
-
},
|
|
376
|
-
};
|
|
377
|
-
}
|
|
378
|
-
return null;
|
|
379
|
-
});
|
|
380
|
-
|
|
381
|
-
mockStartWorkflow.mockResolvedValue({ workflowId: 'wf-1', childSessionKey: 'child-1', state: 'active' });
|
|
382
|
-
mockGetWorkflowDebugSummary.mockResolvedValue({ state: 'active', metadata: {} });
|
|
383
|
-
|
|
384
|
-
EvolutionWorkerService.api = {
|
|
385
|
-
logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() },
|
|
386
|
-
runtime: {},
|
|
387
|
-
} as any;
|
|
388
|
-
|
|
389
|
-
// Create a queue with a task that HAS a pain session ID
|
|
390
|
-
const taskWithPainSession = {
|
|
391
|
-
id: 'task-with-pain',
|
|
392
|
-
taskKind: 'sleep_reflection',
|
|
393
|
-
priority: 'medium',
|
|
394
|
-
score: 50,
|
|
395
|
-
source: 'nocturnal',
|
|
396
|
-
reason: 'Sleep reflection',
|
|
397
|
-
timestamp: '2026-04-10T00:00:00.000Z',
|
|
398
|
-
enqueued_at: '2026-04-10T00:00:00.000Z',
|
|
399
|
-
status: 'pending',
|
|
400
|
-
retryCount: 0,
|
|
401
|
-
maxRetries: 1,
|
|
402
|
-
recentPainContext: {
|
|
403
|
-
mostRecent: { sessionId: 'pain-session-id', score: 80, source: 'test', reason: 'r', timestamp: 't' },
|
|
404
|
-
recentPainCount: 1,
|
|
405
|
-
recentMaxPainScore: 80,
|
|
406
|
-
},
|
|
407
|
-
};
|
|
408
|
-
|
|
409
|
-
fs.writeFileSync(
|
|
410
|
-
path.join(stateDir, 'evolution_queue.json'),
|
|
411
|
-
JSON.stringify([taskWithPainSession]),
|
|
412
|
-
'utf8'
|
|
413
|
-
);
|
|
414
|
-
|
|
415
|
-
try {
|
|
416
|
-
EvolutionWorkerService.start({
|
|
417
|
-
workspaceDir,
|
|
418
|
-
stateDir,
|
|
419
|
-
logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() },
|
|
420
|
-
config: { get: () => 15000 },
|
|
421
|
-
} as any);
|
|
422
|
-
|
|
423
|
-
// Advance time to process the pending task
|
|
424
|
-
await vi.advanceTimersByTimeAsync(6000);
|
|
425
|
-
|
|
426
|
-
// Verify the extractor was called with the pain session ID first
|
|
427
|
-
expect(mockGetNocturnalSessionSnapshot).toHaveBeenCalledWith('pain-session-id');
|
|
428
|
-
|
|
429
|
-
// Verify workflow started (meaning snapshot was found via pain session ID)
|
|
430
|
-
expect(mockStartWorkflow).toHaveBeenCalled();
|
|
431
|
-
const workflowStartInput = mockStartWorkflow.mock.calls[0][1];
|
|
432
|
-
expect(workflowStartInput.metadata.snapshot.startedAt).toBe('2026-04-10T00:00:00.000Z');
|
|
433
|
-
expect(Array.isArray(workflowStartInput.metadata.snapshot.assistantTurns)).toBe(true);
|
|
434
|
-
expect(Array.isArray(workflowStartInput.metadata.snapshot.toolCalls)).toBe(true);
|
|
435
|
-
|
|
436
|
-
// Verify task status updated
|
|
437
|
-
const queue = readQueue(stateDir);
|
|
438
|
-
expect(queue[0].status).toBe('in_progress');
|
|
439
|
-
expect(queue[0].resultRef).toBe('wf-1');
|
|
440
|
-
|
|
441
|
-
} finally {
|
|
442
|
-
EvolutionWorkerService.stop({ workspaceDir, stateDir, logger: console } as any);
|
|
443
|
-
safeRmDir(workspaceDir);
|
|
444
|
-
}
|
|
445
|
-
});
|
|
446
|
-
|
|
447
|
-
it('does not select fallback sessions newer than the triggering task timestamp', async () => {
|
|
448
|
-
const workspaceDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pd-nocturnal-bounded-'));
|
|
449
|
-
const stateDir = path.join(workspaceDir, '.state');
|
|
450
|
-
fs.mkdirSync(stateDir, { recursive: true });
|
|
451
|
-
fs.mkdirSync(path.join(stateDir, 'sessions'), { recursive: true });
|
|
452
|
-
fs.mkdirSync(path.join(stateDir, 'logs'), { recursive: true });
|
|
453
|
-
|
|
454
|
-
mockGetNocturnalSessionSnapshot.mockImplementation((sessionId: string) => {
|
|
455
|
-
if (sessionId === 'older-session') {
|
|
456
|
-
return {
|
|
457
|
-
sessionId: 'older-session',
|
|
458
|
-
startedAt: '2026-04-09T23:00:00.000Z',
|
|
459
|
-
updatedAt: '2026-04-09T23:10:00.000Z',
|
|
460
|
-
assistantTurns: [],
|
|
461
|
-
userTurns: [],
|
|
462
|
-
toolCalls: [],
|
|
463
|
-
painEvents: [],
|
|
464
|
-
gateBlocks: [],
|
|
465
|
-
stats: {
|
|
466
|
-
totalAssistantTurns: 1,
|
|
467
|
-
totalToolCalls: 1,
|
|
468
|
-
totalPainEvents: 0,
|
|
469
|
-
totalGateBlocks: 0,
|
|
470
|
-
failureCount: 1,
|
|
471
|
-
},
|
|
472
|
-
};
|
|
473
|
-
}
|
|
474
|
-
return null;
|
|
475
|
-
});
|
|
476
|
-
mockListRecentNocturnalCandidateSessions.mockReturnValue([
|
|
477
|
-
{
|
|
478
|
-
sessionId: 'newer-session',
|
|
479
|
-
startedAt: '2026-04-10T01:00:00.000Z',
|
|
480
|
-
updatedAt: '2026-04-10T01:10:00.000Z',
|
|
481
|
-
assistantTurnCount: 1,
|
|
482
|
-
toolCallCount: 2,
|
|
483
|
-
painEventCount: 1,
|
|
484
|
-
gateBlockCount: 0,
|
|
485
|
-
failureCount: 1,
|
|
486
|
-
},
|
|
487
|
-
{
|
|
488
|
-
sessionId: 'older-session',
|
|
489
|
-
startedAt: '2026-04-09T23:00:00.000Z',
|
|
490
|
-
updatedAt: '2026-04-09T23:10:00.000Z',
|
|
491
|
-
assistantTurnCount: 1,
|
|
492
|
-
toolCallCount: 2,
|
|
493
|
-
painEventCount: 1,
|
|
494
|
-
gateBlockCount: 0,
|
|
495
|
-
failureCount: 1,
|
|
496
|
-
},
|
|
497
|
-
]);
|
|
498
|
-
mockStartWorkflow.mockResolvedValue({ workflowId: 'wf-bounded', childSessionKey: 'child-bounded', state: 'active' });
|
|
499
|
-
mockGetWorkflowDebugSummary.mockResolvedValue({ state: 'active', metadata: {} });
|
|
500
|
-
|
|
501
|
-
EvolutionWorkerService.api = {
|
|
502
|
-
logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() },
|
|
503
|
-
runtime: {},
|
|
504
|
-
} as any;
|
|
505
|
-
|
|
506
|
-
fs.writeFileSync(
|
|
507
|
-
path.join(stateDir, 'evolution_queue.json'),
|
|
508
|
-
JSON.stringify([
|
|
509
|
-
{
|
|
510
|
-
id: 'sleep-bounded',
|
|
511
|
-
taskKind: 'sleep_reflection',
|
|
512
|
-
priority: 'medium',
|
|
513
|
-
score: 50,
|
|
514
|
-
source: 'nocturnal',
|
|
515
|
-
reason: 'Sleep reflection',
|
|
516
|
-
timestamp: '2026-04-10T00:00:00.000Z',
|
|
517
|
-
enqueued_at: '2026-04-10T00:00:00.000Z',
|
|
518
|
-
status: 'pending',
|
|
519
|
-
retryCount: 0,
|
|
520
|
-
maxRetries: 1,
|
|
521
|
-
recentPainContext: {
|
|
522
|
-
mostRecent: null,
|
|
523
|
-
recentPainCount: 0,
|
|
524
|
-
recentMaxPainScore: 0,
|
|
525
|
-
},
|
|
526
|
-
},
|
|
527
|
-
], null, 2),
|
|
528
|
-
'utf8'
|
|
529
|
-
);
|
|
530
|
-
|
|
531
|
-
try {
|
|
532
|
-
EvolutionWorkerService.start({
|
|
533
|
-
workspaceDir,
|
|
534
|
-
stateDir,
|
|
535
|
-
logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() },
|
|
536
|
-
config: { get: () => 15000 },
|
|
537
|
-
} as any);
|
|
538
|
-
|
|
539
|
-
await vi.advanceTimersByTimeAsync(6000);
|
|
540
|
-
|
|
541
|
-
expect(mockListRecentNocturnalCandidateSessions).toHaveBeenCalledWith(
|
|
542
|
-
expect.objectContaining({
|
|
543
|
-
limit: 20,
|
|
544
|
-
minToolCalls: 1,
|
|
545
|
-
dateTo: '2026-04-10T00:00:00.000Z',
|
|
546
|
-
})
|
|
547
|
-
);
|
|
548
|
-
expect(mockGetNocturnalSessionSnapshot).not.toHaveBeenCalledWith('newer-session');
|
|
549
|
-
expect(mockGetNocturnalSessionSnapshot).toHaveBeenCalledWith('older-session');
|
|
550
|
-
} finally {
|
|
551
|
-
EvolutionWorkerService.stop({ workspaceDir, stateDir, logger: console } as any);
|
|
552
|
-
safeRmDir(workspaceDir);
|
|
553
|
-
}
|
|
554
|
-
});
|
|
555
|
-
|
|
556
130
|
// === End-to-End Contract Tests ===
|
|
557
131
|
|
|
558
132
|
it('e2e: pain flag → worker enqueue → session_id is correctly attached to queued task', async () => {
|
|
@@ -640,125 +214,4 @@ session_id: pain-session-abc
|
|
|
640
214
|
safeRmDir(workspaceDir);
|
|
641
215
|
}
|
|
642
216
|
});
|
|
643
|
-
|
|
644
|
-
it('e2e: bounded session selection — never picks a session newer than the triggering task', async () => {
|
|
645
|
-
const workspaceDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pd-e2e-bounded-'));
|
|
646
|
-
const stateDir = path.join(workspaceDir, '.state');
|
|
647
|
-
fs.mkdirSync(stateDir, { recursive: true });
|
|
648
|
-
fs.mkdirSync(path.join(stateDir, 'sessions'), { recursive: true });
|
|
649
|
-
fs.mkdirSync(path.join(stateDir, 'logs'), { recursive: true });
|
|
650
|
-
|
|
651
|
-
// Mock: only the older session snapshot is available
|
|
652
|
-
mockGetNocturnalSessionSnapshot.mockImplementation((sessionId: string) => {
|
|
653
|
-
if (sessionId === 'older-session-bounded') {
|
|
654
|
-
return {
|
|
655
|
-
sessionId: 'older-session-bounded',
|
|
656
|
-
startedAt: '2026-04-09T23:00:00.000Z',
|
|
657
|
-
updatedAt: '2026-04-09T23:10:00.000Z',
|
|
658
|
-
assistantTurns: [],
|
|
659
|
-
userTurns: [],
|
|
660
|
-
toolCalls: [],
|
|
661
|
-
painEvents: [{ source: 'tool_failure', score: 60 }],
|
|
662
|
-
gateBlocks: [],
|
|
663
|
-
stats: {
|
|
664
|
-
totalAssistantTurns: 3,
|
|
665
|
-
totalToolCalls: 5,
|
|
666
|
-
totalPainEvents: 1,
|
|
667
|
-
totalGateBlocks: 0,
|
|
668
|
-
failureCount: 1,
|
|
669
|
-
},
|
|
670
|
-
};
|
|
671
|
-
}
|
|
672
|
-
return null;
|
|
673
|
-
});
|
|
674
|
-
|
|
675
|
-
// Candidate sessions — one newer, one older than the task trigger time
|
|
676
|
-
mockListRecentNocturnalCandidateSessions.mockReturnValue([
|
|
677
|
-
{
|
|
678
|
-
sessionId: 'newer-unrelated',
|
|
679
|
-
startedAt: '2026-04-10T01:00:00.000Z',
|
|
680
|
-
updatedAt: '2026-04-10T01:10:00.000Z',
|
|
681
|
-
assistantTurnCount: 2,
|
|
682
|
-
toolCallCount: 5,
|
|
683
|
-
painEventCount: 2,
|
|
684
|
-
gateBlockCount: 0,
|
|
685
|
-
failureCount: 1,
|
|
686
|
-
},
|
|
687
|
-
{
|
|
688
|
-
sessionId: 'older-session-bounded',
|
|
689
|
-
startedAt: '2026-04-09T23:00:00.000Z',
|
|
690
|
-
updatedAt: '2026-04-09T23:10:00.000Z',
|
|
691
|
-
assistantTurnCount: 3,
|
|
692
|
-
toolCallCount: 5,
|
|
693
|
-
painEventCount: 1,
|
|
694
|
-
gateBlockCount: 0,
|
|
695
|
-
failureCount: 1,
|
|
696
|
-
},
|
|
697
|
-
]);
|
|
698
|
-
|
|
699
|
-
mockStartWorkflow.mockResolvedValue({ workflowId: 'wf-bounded', childSessionKey: 'child-bounded', state: 'active' });
|
|
700
|
-
mockGetWorkflowDebugSummary.mockResolvedValue({ state: 'active', metadata: {} });
|
|
701
|
-
|
|
702
|
-
EvolutionWorkerService.api = {
|
|
703
|
-
logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() },
|
|
704
|
-
runtime: {},
|
|
705
|
-
} as any;
|
|
706
|
-
|
|
707
|
-
// Task triggered at 2026-04-10T00:00:00.000Z — no pain session ID, so it falls back to candidate selection
|
|
708
|
-
fs.writeFileSync(
|
|
709
|
-
path.join(stateDir, 'evolution_queue.json'),
|
|
710
|
-
JSON.stringify([
|
|
711
|
-
{
|
|
712
|
-
id: 'sleep-bounded',
|
|
713
|
-
taskKind: 'sleep_reflection',
|
|
714
|
-
priority: 'medium',
|
|
715
|
-
score: 50,
|
|
716
|
-
source: 'nocturnal',
|
|
717
|
-
reason: 'Sleep reflection',
|
|
718
|
-
timestamp: '2026-04-10T00:00:00.000Z',
|
|
719
|
-
enqueued_at: '2026-04-10T00:00:00.000Z',
|
|
720
|
-
status: 'pending',
|
|
721
|
-
retryCount: 0,
|
|
722
|
-
maxRetries: 1,
|
|
723
|
-
recentPainContext: {
|
|
724
|
-
mostRecent: null,
|
|
725
|
-
recentPainCount: 0,
|
|
726
|
-
recentMaxPainScore: 0,
|
|
727
|
-
},
|
|
728
|
-
},
|
|
729
|
-
], null, 2),
|
|
730
|
-
'utf8'
|
|
731
|
-
);
|
|
732
|
-
|
|
733
|
-
try {
|
|
734
|
-
EvolutionWorkerService.start({
|
|
735
|
-
workspaceDir,
|
|
736
|
-
stateDir,
|
|
737
|
-
logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() },
|
|
738
|
-
config: { get: () => 15000 },
|
|
739
|
-
} as any);
|
|
740
|
-
|
|
741
|
-
await vi.advanceTimersByTimeAsync(6000);
|
|
742
|
-
|
|
743
|
-
// Verify dateTo boundary was passed
|
|
744
|
-
expect(mockListRecentNocturnalCandidateSessions).toHaveBeenCalledWith(
|
|
745
|
-
expect.objectContaining({
|
|
746
|
-
limit: 20,
|
|
747
|
-
minToolCalls: 1,
|
|
748
|
-
dateTo: '2026-04-10T00:00:00.000Z',
|
|
749
|
-
})
|
|
750
|
-
);
|
|
751
|
-
|
|
752
|
-
// Should NOT query the newer session
|
|
753
|
-
expect(mockGetNocturnalSessionSnapshot).not.toHaveBeenCalledWith('newer-unrelated');
|
|
754
|
-
// Should use the older session that passes the time boundary
|
|
755
|
-
expect(mockGetNocturnalSessionSnapshot).toHaveBeenCalledWith('older-session-bounded');
|
|
756
|
-
|
|
757
|
-
// Workflow should have started
|
|
758
|
-
expect(mockStartWorkflow).toHaveBeenCalled();
|
|
759
|
-
} finally {
|
|
760
|
-
EvolutionWorkerService.stop({ workspaceDir, stateDir, logger: console } as any);
|
|
761
|
-
safeRmDir(workspaceDir);
|
|
762
|
-
}
|
|
763
|
-
});
|
|
764
217
|
});
|