@principles/pd-cli 1.147.10 → 1.147.12

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.
@@ -8,15 +8,23 @@ vi.mock('../../src/resolve-workspace.js', () => ({
8
8
  resolveWorkspaceDir: vi.fn().mockReturnValue('/fake/workspace'),
9
9
  }));
10
10
 
11
- vi.mock('@principles/core/runtime-v2', () => ({
12
- createRecoverySweepService: vi.fn().mockResolvedValue({
13
- service: {
14
- detectFailedTasks: mockDetectFailedTasks,
15
- recoverFailedTask: mockRecoverFailedTask,
16
- },
17
- close: mockServiceClose,
18
- }),
19
- }));
11
+ vi.mock('@principles/core/runtime-v2', async (importOriginal) => {
12
+ // Keep the real kind guards (isPeerRunnerKind / isDiagnosticianStageKind):
13
+ // the per-kind execution guidance under test must exercise the actual
14
+ // classification, not a test-local reimplementation. Only the I/O-bearing
15
+ // service factory is replaced.
16
+ const actual = await importOriginal<typeof import('@principles/core/runtime-v2')>();
17
+ return {
18
+ ...actual,
19
+ createRecoverySweepService: vi.fn().mockResolvedValue({
20
+ service: {
21
+ detectFailedTasks: mockDetectFailedTasks,
22
+ recoverFailedTask: mockRecoverFailedTask,
23
+ },
24
+ close: mockServiceClose,
25
+ }),
26
+ };
27
+ });
20
28
 
21
29
  import { handleRuntimeRecoveryFailedTasks } from '../../src/commands/runtime-recovery-failed-tasks.js';
22
30
 
@@ -179,6 +187,7 @@ describe('pd runtime recovery failed-tasks command contract', () => {
179
187
  maxAttempts: 3,
180
188
  isExhausted: false,
181
189
  status: 'failed',
190
+ inputRef: null,
182
191
  },
183
192
  ]);
184
193
  mockRecoverFailedTask.mockResolvedValue(null);
@@ -198,4 +207,122 @@ describe('pd runtime recovery failed-tasks command contract', () => {
198
207
  reason: expect.stringContaining('no longer failed or concurrently modified'),
199
208
  });
200
209
  });
210
+
211
+ // ── PRI-674 review P2: per-kind execution guidance ────────────────────────
212
+ // Guidance must name commands that actually support the task kind:
213
+ // internalization → run-once; diagnostician parent / diag_* stage → diagnose run.
214
+
215
+ describe('per-kind execution nextAction (review P2)', () => {
216
+ it('diagnostician parent task points at pd diagnose run', async () => {
217
+ mockDetectFailedTasks.mockResolvedValue([
218
+ { taskId: 'diagnosis_pain-001', taskKind: 'diagnostician', attemptCount: 1, maxAttempts: 3, isExhausted: false, status: 'failed', inputRef: 'pain-001' },
219
+ ]);
220
+
221
+ await handleRuntimeRecoveryFailedTasks({ workspace: '/fake/workspace', confirm: true, json: true });
222
+
223
+ const output = JSON.parse(consoleLogSpy.mock.calls[0][0] as string);
224
+ const nextAction: string = output.tasks[0].nextAction;
225
+ expect(nextAction).toContain('pd diagnose run --task-id diagnosis_pain-001');
226
+ expect(nextAction).not.toContain('internalization run-once');
227
+ // Summary must not claim a single internalization command executes everything
228
+ expect(output.nextAction).toContain('pd diagnose run');
229
+ expect(output.nextAction).not.toContain('run-once to execute recovered tasks');
230
+ });
231
+
232
+ it('diag_* stage task resolves parent from inputRef linkage', async () => {
233
+ mockDetectFailedTasks.mockResolvedValue([
234
+ { taskId: 'diag_rootcause-diagnosis_pain-001', taskKind: 'diag_rootcause', attemptCount: 1, maxAttempts: 3, isExhausted: false, status: 'failed', inputRef: 'diagnosis_pain-001' },
235
+ ]);
236
+
237
+ await handleRuntimeRecoveryFailedTasks({ workspace: '/fake/workspace', confirm: true, json: true });
238
+
239
+ const output = JSON.parse(consoleLogSpy.mock.calls[0][0] as string);
240
+ const nextAction: string = output.tasks[0].nextAction;
241
+ expect(nextAction).toContain('diagnosis_pain-001');
242
+ expect(nextAction).toContain('pd diagnose run --task-id diagnosis_pain-001');
243
+ expect(nextAction).not.toContain('run-once');
244
+ });
245
+
246
+ it('diag_* stage task falls back to diag_<stage>-<parent> ID convention when inputRef is null', async () => {
247
+ mockDetectFailedTasks.mockResolvedValue([
248
+ { taskId: 'diag_router-diagnosis_pain-007', taskKind: 'diag_router', attemptCount: 1, maxAttempts: 3, isExhausted: false, status: 'failed', inputRef: null },
249
+ ]);
250
+
251
+ await handleRuntimeRecoveryFailedTasks({ workspace: '/fake/workspace', confirm: true, json: true });
252
+
253
+ const output = JSON.parse(consoleLogSpy.mock.calls[0][0] as string);
254
+ const nextAction: string = output.tasks[0].nextAction;
255
+ expect(nextAction).toContain('diagnosis_pain-007');
256
+ expect(nextAction).toContain('pd diagnose run --task-id diagnosis_pain-007');
257
+ });
258
+
259
+ it('diag_* stage task with no inputRef and non-conventional ID reports unresolved parent instead of a guessed command', async () => {
260
+ mockDetectFailedTasks.mockResolvedValue([
261
+ { taskId: 'weird-stage-id', taskKind: 'diag_distiller', attemptCount: 1, maxAttempts: 3, isExhausted: false, status: 'failed', inputRef: null },
262
+ ]);
263
+
264
+ await handleRuntimeRecoveryFailedTasks({ workspace: '/fake/workspace', confirm: true, json: true });
265
+
266
+ const output = JSON.parse(consoleLogSpy.mock.calls[0][0] as string);
267
+ const nextAction: string = output.tasks[0].nextAction;
268
+ expect(nextAction).toContain('could not be resolved');
269
+ expect(nextAction).toContain('pd diagnose status --task-id weird-stage-id');
270
+ expect(nextAction).not.toContain('pd diagnose run --task-id');
271
+ expect(nextAction).not.toContain('run-once');
272
+ });
273
+
274
+ it('internalization task points at run-once with its runner kind', async () => {
275
+ mockDetectFailedTasks.mockResolvedValue([
276
+ { taskId: 'pi-task-1', taskKind: 'scribe', attemptCount: 1, maxAttempts: 3, isExhausted: false, status: 'failed', inputRef: null },
277
+ ]);
278
+
279
+ await handleRuntimeRecoveryFailedTasks({ workspace: '/fake/workspace', confirm: true, json: true });
280
+
281
+ const output = JSON.parse(consoleLogSpy.mock.calls[0][0] as string);
282
+ const nextAction: string = output.tasks[0].nextAction;
283
+ expect(nextAction).toContain('pd runtime internalization run-once --runner scribe');
284
+ expect(nextAction).not.toContain('pd diagnose run');
285
+ });
286
+
287
+ it('mixed recovery summary splits guidance per executor instead of one command for all', async () => {
288
+ mockDetectFailedTasks.mockResolvedValue([
289
+ { taskId: 'diagnosis_pain-001', taskKind: 'diagnostician', attemptCount: 1, maxAttempts: 3, isExhausted: false, status: 'failed', inputRef: 'pain-001' },
290
+ { taskId: 'diag_rootcause-diagnosis_pain-001', taskKind: 'diag_rootcause', attemptCount: 1, maxAttempts: 3, isExhausted: false, status: 'failed', inputRef: 'diagnosis_pain-001' },
291
+ { taskId: 'pi-task-1', taskKind: 'dreamer', attemptCount: 1, maxAttempts: 3, isExhausted: false, status: 'failed', inputRef: null },
292
+ { taskId: 'pi-task-2', taskKind: 'rollout_reviewer', attemptCount: 1, maxAttempts: 3, isExhausted: false, status: 'failed', inputRef: null },
293
+ ]);
294
+
295
+ await handleRuntimeRecoveryFailedTasks({ workspace: '/fake/workspace', confirm: true, json: true });
296
+
297
+ const output = JSON.parse(consoleLogSpy.mock.calls[0][0] as string);
298
+ expect(output.recoveredCount).toBe(4);
299
+ const summary: string = output.nextAction;
300
+ expect(summary).toContain('diagnostician parent task(s) (diagnosis_pain-001)');
301
+ expect(summary).toContain('pd diagnose run --task-id <taskId>');
302
+ expect(summary).toContain('diag_* stage task(s)');
303
+ expect(summary).toContain('internalization task(s) (pi-task-1, pi-task-2)');
304
+ expect(summary).toContain('run-once --runner <kind>');
305
+ // No stale single-command claim
306
+ expect(summary).not.toContain('Run pd runtime internalization run-once to execute recovered tasks');
307
+ });
308
+
309
+ it('guidance matches real command registrations (help wiring contract)', async () => {
310
+ // cli-7-test-wiring: the commands we recommend must actually be
311
+ // registered with the flags we name. Check the built CLI's help.
312
+ const { execFileSync } = await import('node:child_process');
313
+ const { getBuiltPdCliPath } = await import('../helpers/pd-cli-path.js');
314
+ const cliPath = getBuiltPdCliPath();
315
+
316
+ const diagnoseHelp = execFileSync('node', [cliPath, 'diagnose', 'run', '--help'], { encoding: 'utf8' });
317
+ expect(diagnoseHelp).toContain('--task-id');
318
+ expect(diagnoseHelp).toContain('--workspace');
319
+
320
+ const runOnceHelp = execFileSync('node', [cliPath, 'runtime', 'internalization', 'run-once', '--help'], { encoding: 'utf8' });
321
+ expect(runOnceHelp).toContain('--runner');
322
+ // run-once must NOT advertise diagnostician support: the supported list
323
+ // comes from SUPPORTED_RUNNERS (peer runners only)
324
+ expect(runOnceHelp).toMatch(/dreamer/);
325
+ expect(runOnceHelp).not.toMatch(/diag_|diagnostician/);
326
+ });
327
+ });
201
328
  });