@principles/pd-cli 1.81.0 → 1.83.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/dist/commands/diagnose.d.ts.map +1 -1
- package/dist/commands/diagnose.js +5 -0
- package/dist/commands/diagnose.js.map +1 -1
- package/dist/commands/pain-retry.d.ts.map +1 -1
- package/dist/commands/pain-retry.js +5 -0
- package/dist/commands/pain-retry.js.map +1 -1
- package/dist/commands/runtime-internalization-run-once.d.ts.map +1 -1
- package/dist/commands/runtime-internalization-run-once.js +59 -9
- package/dist/commands/runtime-internalization-run-once.js.map +1 -1
- package/dist/config-reader.d.ts +25 -0
- package/dist/config-reader.d.ts.map +1 -0
- package/dist/config-reader.js +109 -0
- package/dist/config-reader.js.map +1 -0
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/commands/diagnose.ts +8 -1
- package/src/commands/pain-retry.ts +8 -1
- package/src/commands/runtime-internalization-run-once.ts +66 -11
- package/src/config-reader.ts +122 -0
- package/src/index.ts +1 -1
- package/tests/commands/candidate-audit-repair.test.ts +1 -0
- package/tests/commands/candidate-intake.test.ts +1 -0
- package/tests/commands/candidate-internalization-backfill.test.ts +1 -0
- package/tests/commands/candidate-internalize.test.ts +1 -0
- package/tests/commands/candidate-route.test.ts +1 -0
- package/tests/commands/diagnose.test.ts +5 -0
- package/tests/commands/health.test.ts +1 -0
- package/tests/commands/pain-record.test.ts +1 -0
- package/tests/commands/pain-retry.test.ts +5 -0
- package/tests/commands/runtime-activation.test.ts +1 -0
- package/tests/commands/runtime-canary.test.ts +1 -0
- package/tests/commands/runtime-diagnostics-export.test.ts +1 -0
- package/tests/commands/runtime-health-snapshot.test.ts +1 -0
- package/tests/commands/runtime-internalization-enqueue-successors.test.ts +1 -0
- package/tests/commands/runtime-internalization-integrity-repair.test.ts +1 -0
- package/tests/commands/runtime-internalization-integrity.test.ts +1 -0
- package/tests/commands/runtime-internalization-queue.test.ts +1 -0
- package/tests/commands/runtime-internalization-run-once.test.ts +292 -15
- package/tests/commands/runtime-internalization-wake-once.test.ts +1 -0
- package/tests/commands/runtime-pruning.test.ts +1 -0
- package/tests/commands/runtime-recovery.test.ts +1 -0
- package/tests/commands/runtime.test.ts +1 -0
- package/tests/commands/trace.test.ts +1 -0
- package/tests/config-reader.test.ts +142 -0
|
@@ -122,6 +122,11 @@ vi.mock('@principles/core/runtime-v2', () => ({
|
|
|
122
122
|
}),
|
|
123
123
|
isRuntimeConfigError: vi.fn().mockReturnValue(false),
|
|
124
124
|
validateRuntimeConfig: vi.fn(),
|
|
125
|
+
resolveOutputLanguage: vi.fn().mockReturnValue({ outputLanguage: 'zh-CN' }),
|
|
126
|
+
}));
|
|
127
|
+
|
|
128
|
+
vi.mock('../../src/config-reader.js', () => ({
|
|
129
|
+
readOutputLanguageFromWorkspace: vi.fn().mockReturnValue({ outputLanguage: 'zh-CN' }),
|
|
125
130
|
}));
|
|
126
131
|
|
|
127
132
|
import { handleRuntimeInternalizationRunOnce } from '../../src/commands/runtime-internalization-run-once.js';
|
|
@@ -535,7 +540,7 @@ describe('handleRuntimeInternalizationRunOnce', () => {
|
|
|
535
540
|
expect(text).toContain('resultRef: philosopher://run-phil-002');
|
|
536
541
|
});
|
|
537
542
|
|
|
538
|
-
it('successful dreamer
|
|
543
|
+
it('auto-enqueue: successful dreamer returns successor info by default', async () => {
|
|
539
544
|
mockWakeOnce.mockResolvedValue({
|
|
540
545
|
decision: 'would_lease',
|
|
541
546
|
taskId: 'task-dreamer-enq-001',
|
|
@@ -564,11 +569,11 @@ describe('handleRuntimeInternalizationRunOnce', () => {
|
|
|
564
569
|
|
|
565
570
|
const output = JSON.parse(consoleLogSpy.mock.calls[0][0]);
|
|
566
571
|
expect(output.enqueueDecision).toBe('successor_created');
|
|
567
|
-
expect(output.
|
|
572
|
+
expect(output.successorTaskIds![0]).toBe('task-phil-enq-001');
|
|
568
573
|
expect(output.successorKind).toBe('philosopher');
|
|
569
574
|
});
|
|
570
575
|
|
|
571
|
-
it('repeated
|
|
576
|
+
it('auto-enqueue: repeated run returns existing successorTaskId', async () => {
|
|
572
577
|
mockWakeOnce.mockResolvedValue({
|
|
573
578
|
decision: 'would_lease',
|
|
574
579
|
taskId: 'task-dreamer-enq-002',
|
|
@@ -597,11 +602,11 @@ describe('handleRuntimeInternalizationRunOnce', () => {
|
|
|
597
602
|
|
|
598
603
|
const output = JSON.parse(consoleLogSpy.mock.calls[0][0]);
|
|
599
604
|
expect(output.enqueueDecision).toBe('successor_exists');
|
|
600
|
-
expect(output.
|
|
605
|
+
expect(output.successorTaskIds![0]).toBe('task-phil-enq-002');
|
|
601
606
|
expect(output.successorKind).toBe('philosopher');
|
|
602
607
|
});
|
|
603
608
|
|
|
604
|
-
it('
|
|
609
|
+
it('auto-enqueue: no_successor does not set successor info', async () => {
|
|
605
610
|
mockWakeOnce.mockResolvedValue({
|
|
606
611
|
decision: 'would_lease',
|
|
607
612
|
taskId: 'task-dreamer-enq-003',
|
|
@@ -629,10 +634,10 @@ describe('handleRuntimeInternalizationRunOnce', () => {
|
|
|
629
634
|
|
|
630
635
|
const output = JSON.parse(consoleLogSpy.mock.calls[0][0]);
|
|
631
636
|
expect(output.enqueueDecision).toBe('no_successor');
|
|
632
|
-
expect(output.
|
|
637
|
+
expect(output.successorTaskIds).toEqual([]);
|
|
633
638
|
});
|
|
634
639
|
|
|
635
|
-
it('
|
|
640
|
+
it('auto-enqueue: failed run does not call commitNextTaskProposal', async () => {
|
|
636
641
|
mockWakeOnce.mockResolvedValue({
|
|
637
642
|
decision: 'would_lease',
|
|
638
643
|
taskId: 'task-dreamer-enq-004',
|
|
@@ -652,7 +657,7 @@ describe('handleRuntimeInternalizationRunOnce', () => {
|
|
|
652
657
|
expect(mockCommitNextTaskProposal).not.toHaveBeenCalled();
|
|
653
658
|
});
|
|
654
659
|
|
|
655
|
-
it('
|
|
660
|
+
it('auto-enqueue without --allow-test-double still blocked', async () => {
|
|
656
661
|
await handleRuntimeInternalizationRunOnce({ workspace: WS, runner: 'dreamer', runtime: 'test-double', enqueueNext: true });
|
|
657
662
|
|
|
658
663
|
expect(process.exitCode).toBe(1);
|
|
@@ -880,7 +885,7 @@ describe('handleRuntimeInternalizationRunOnce', () => {
|
|
|
880
885
|
expect(output.runnerResult.status).toBe('succeeded');
|
|
881
886
|
});
|
|
882
887
|
|
|
883
|
-
it('--runner scribe
|
|
888
|
+
it('auto-enqueue: --runner scribe creates artificer successor', async () => {
|
|
884
889
|
mockWakeOnce.mockResolvedValue({
|
|
885
890
|
decision: 'would_lease',
|
|
886
891
|
taskId: 'task-scribe-enq-001',
|
|
@@ -916,7 +921,7 @@ describe('handleRuntimeInternalizationRunOnce', () => {
|
|
|
916
921
|
|
|
917
922
|
const output = JSON.parse(consoleLogSpy.mock.calls[0][0]);
|
|
918
923
|
expect(output.enqueueDecision).toBe('successor_created');
|
|
919
|
-
expect(output.
|
|
924
|
+
expect(output.successorTaskIds![0]).toBe('task-artificer-enq-001');
|
|
920
925
|
expect(output.successorKind).toBe('artificer');
|
|
921
926
|
});
|
|
922
927
|
|
|
@@ -979,7 +984,7 @@ describe('handleRuntimeInternalizationRunOnce', () => {
|
|
|
979
984
|
expect(output.runnerResult.status).toBe('succeeded');
|
|
980
985
|
});
|
|
981
986
|
|
|
982
|
-
it('--runner artificer
|
|
987
|
+
it('auto-enqueue: --runner artificer returns successor decision', async () => {
|
|
983
988
|
mockWakeOnce.mockResolvedValue({
|
|
984
989
|
decision: 'would_lease',
|
|
985
990
|
taskId: 'task-artificer-enq-001',
|
|
@@ -1022,7 +1027,7 @@ describe('handleRuntimeInternalizationRunOnce', () => {
|
|
|
1022
1027
|
|
|
1023
1028
|
const output = JSON.parse(consoleLogSpy.mock.calls[0][0]);
|
|
1024
1029
|
expect(output.enqueueDecision).toBe('successor_created');
|
|
1025
|
-
expect(output.
|
|
1030
|
+
expect(output.successorTaskIds![0]).toBe('task-evaluator-enq-001');
|
|
1026
1031
|
expect(output.successorKind).toBe('evaluator');
|
|
1027
1032
|
});
|
|
1028
1033
|
|
|
@@ -1112,7 +1117,7 @@ describe('handleRuntimeInternalizationRunOnce', () => {
|
|
|
1112
1117
|
expect(output.runnerResult.status).toBe('succeeded');
|
|
1113
1118
|
});
|
|
1114
1119
|
|
|
1115
|
-
it('--runner evaluator
|
|
1120
|
+
it('auto-enqueue: --runner evaluator returns successor decision', async () => {
|
|
1116
1121
|
mockWakeOnce.mockResolvedValue({
|
|
1117
1122
|
decision: 'would_lease',
|
|
1118
1123
|
taskId: 'task-evaluator-enq-001',
|
|
@@ -1155,7 +1160,7 @@ describe('handleRuntimeInternalizationRunOnce', () => {
|
|
|
1155
1160
|
|
|
1156
1161
|
const output = JSON.parse(consoleLogSpy.mock.calls[0][0]);
|
|
1157
1162
|
expect(output.enqueueDecision).toBe('successor_created');
|
|
1158
|
-
expect(output.
|
|
1163
|
+
expect(output.successorTaskIds![0]).toBe('task-rollout-reviewer-enq-001');
|
|
1159
1164
|
expect(output.successorKind).toBe('rollout_reviewer');
|
|
1160
1165
|
});
|
|
1161
1166
|
|
|
@@ -1208,7 +1213,7 @@ describe('handleRuntimeInternalizationRunOnce', () => {
|
|
|
1208
1213
|
expect(output.runnerResult.status).toBe('succeeded');
|
|
1209
1214
|
});
|
|
1210
1215
|
|
|
1211
|
-
it('--runner rollout_reviewer
|
|
1216
|
+
it('auto-enqueue: --runner rollout_reviewer returns no_successor for prompt channel', async () => {
|
|
1212
1217
|
mockWakeOnce.mockResolvedValue({
|
|
1213
1218
|
decision: 'would_lease',
|
|
1214
1219
|
taskId: 'task-rollout-reviewer-enq-001',
|
|
@@ -1315,4 +1320,276 @@ describe('handleRuntimeInternalizationRunOnce', () => {
|
|
|
1315
1320
|
expect(output.nextAction).toBeTruthy();
|
|
1316
1321
|
expect(process.exitCode).toBe(1);
|
|
1317
1322
|
});
|
|
1323
|
+
|
|
1324
|
+
// === Default enqueue successor tests ===
|
|
1325
|
+
|
|
1326
|
+
it('default behavior (no --no-enqueue-next) auto-enqueues successor on runner success', async () => {
|
|
1327
|
+
mockWakeOnce.mockResolvedValue({
|
|
1328
|
+
decision: 'would_lease',
|
|
1329
|
+
taskId: 'task-dreamer-auto-001',
|
|
1330
|
+
taskKind: 'dreamer',
|
|
1331
|
+
});
|
|
1332
|
+
|
|
1333
|
+
mockRun.mockResolvedValue({
|
|
1334
|
+
status: 'succeeded',
|
|
1335
|
+
taskId: 'task-dreamer-auto-001',
|
|
1336
|
+
runId: 'run-auto-001',
|
|
1337
|
+
artifactId: 'pi-art-auto-001',
|
|
1338
|
+
resultRef: 'dreamer://run-auto-001',
|
|
1339
|
+
contextHash: 'ctx-auto',
|
|
1340
|
+
output: { valid: true, taskId: 'task-dreamer-auto-001', candidates: VALID_DREAMER_CANDIDATES, contextRefs: [], generatedAt: new Date().toISOString() },
|
|
1341
|
+
attemptCount: 1,
|
|
1342
|
+
});
|
|
1343
|
+
|
|
1344
|
+
mockCommitNextTaskProposal.mockResolvedValue({
|
|
1345
|
+
decision: 'successor_created',
|
|
1346
|
+
sourceTaskId: 'task-dreamer-auto-001',
|
|
1347
|
+
successorTaskId: 'task-phil-auto-001',
|
|
1348
|
+
successorKind: 'philosopher',
|
|
1349
|
+
});
|
|
1350
|
+
|
|
1351
|
+
// No enqueueNext specified — default should auto-enqueue
|
|
1352
|
+
await handleRuntimeInternalizationRunOnce({ workspace: WS, runner: 'dreamer', runtime: 'test-double', allowTestDouble: true, json: true });
|
|
1353
|
+
|
|
1354
|
+
const output = JSON.parse(consoleLogSpy.mock.calls[0][0]);
|
|
1355
|
+
expect(output.successorEnqueueAttempted).toBe(true);
|
|
1356
|
+
expect(output.successorTasksCreated).toBe(1);
|
|
1357
|
+
expect(output.successorTaskIds).toContain('task-phil-auto-001');
|
|
1358
|
+
expect(output.enqueueDecision).toBe('successor_created');
|
|
1359
|
+
expect(output.successorKind).toBe('philosopher');
|
|
1360
|
+
expect(mockCommitNextTaskProposal).toHaveBeenCalledWith('task-dreamer-auto-001');
|
|
1361
|
+
});
|
|
1362
|
+
|
|
1363
|
+
it('--no-enqueue-next (enqueueNext: false) skips successor enqueue', async () => {
|
|
1364
|
+
mockWakeOnce.mockResolvedValue({
|
|
1365
|
+
decision: 'would_lease',
|
|
1366
|
+
taskId: 'task-dreamer-skip-001',
|
|
1367
|
+
taskKind: 'dreamer',
|
|
1368
|
+
});
|
|
1369
|
+
|
|
1370
|
+
mockRun.mockResolvedValue({
|
|
1371
|
+
status: 'succeeded',
|
|
1372
|
+
taskId: 'task-dreamer-skip-001',
|
|
1373
|
+
runId: 'run-skip-001',
|
|
1374
|
+
artifactId: 'pi-art-skip-001',
|
|
1375
|
+
resultRef: 'dreamer://run-skip-001',
|
|
1376
|
+
contextHash: 'ctx-skip',
|
|
1377
|
+
output: { valid: true, taskId: 'task-dreamer-skip-001', candidates: VALID_DREAMER_CANDIDATES, contextRefs: [], generatedAt: new Date().toISOString() },
|
|
1378
|
+
attemptCount: 1,
|
|
1379
|
+
});
|
|
1380
|
+
|
|
1381
|
+
await handleRuntimeInternalizationRunOnce({ workspace: WS, runner: 'dreamer', runtime: 'test-double', allowTestDouble: true, enqueueNext: false, json: true });
|
|
1382
|
+
|
|
1383
|
+
const output = JSON.parse(consoleLogSpy.mock.calls[0][0]);
|
|
1384
|
+
expect(output.successorEnqueueAttempted).toBe(false);
|
|
1385
|
+
expect(output.nextAction).toContain('--no-enqueue-next');
|
|
1386
|
+
expect(mockCommitNextTaskProposal).not.toHaveBeenCalled();
|
|
1387
|
+
});
|
|
1388
|
+
|
|
1389
|
+
it('successor enqueue failure outputs partial_success with reason and nextAction', async () => {
|
|
1390
|
+
mockWakeOnce.mockResolvedValue({
|
|
1391
|
+
decision: 'would_lease',
|
|
1392
|
+
taskId: 'task-dreamer-fail-001',
|
|
1393
|
+
taskKind: 'dreamer',
|
|
1394
|
+
});
|
|
1395
|
+
|
|
1396
|
+
mockRun.mockResolvedValue({
|
|
1397
|
+
status: 'succeeded',
|
|
1398
|
+
taskId: 'task-dreamer-fail-001',
|
|
1399
|
+
runId: 'run-fail-001',
|
|
1400
|
+
artifactId: 'pi-art-fail-001',
|
|
1401
|
+
resultRef: 'dreamer://run-fail-001',
|
|
1402
|
+
contextHash: 'ctx-fail',
|
|
1403
|
+
output: { valid: true, taskId: 'task-dreamer-fail-001', candidates: VALID_DREAMER_CANDIDATES, contextRefs: [], generatedAt: new Date().toISOString() },
|
|
1404
|
+
attemptCount: 1,
|
|
1405
|
+
});
|
|
1406
|
+
|
|
1407
|
+
mockCommitNextTaskProposal.mockRejectedValue(new Error('database locked'));
|
|
1408
|
+
|
|
1409
|
+
await handleRuntimeInternalizationRunOnce({ workspace: WS, runner: 'dreamer', runtime: 'test-double', allowTestDouble: true, json: true });
|
|
1410
|
+
|
|
1411
|
+
const output = JSON.parse(consoleLogSpy.mock.calls[0][0]);
|
|
1412
|
+
expect(output.decision).toBe('partial_success');
|
|
1413
|
+
expect(output.successorEnqueueAttempted).toBe(true);
|
|
1414
|
+
expect(output.enqueueDecision).toBe('enqueue_failed');
|
|
1415
|
+
expect(output.enqueueReason).toContain('database locked');
|
|
1416
|
+
expect(output.nextAction).toContain('enqueue-successors');
|
|
1417
|
+
expect(output.successorTasksCreated).toBe(0);
|
|
1418
|
+
expect(output.successorTaskIds).toEqual([]);
|
|
1419
|
+
});
|
|
1420
|
+
|
|
1421
|
+
it('runner failure never enqueues successor (default behavior)', async () => {
|
|
1422
|
+
mockWakeOnce.mockResolvedValue({
|
|
1423
|
+
decision: 'would_lease',
|
|
1424
|
+
taskId: 'task-dreamer-nofail-001',
|
|
1425
|
+
taskKind: 'dreamer',
|
|
1426
|
+
});
|
|
1427
|
+
|
|
1428
|
+
mockRun.mockResolvedValue({
|
|
1429
|
+
status: 'failed',
|
|
1430
|
+
taskId: 'task-dreamer-nofail-001',
|
|
1431
|
+
errorCategory: 'execution_failed',
|
|
1432
|
+
failureReason: 'Runtime unavailable',
|
|
1433
|
+
attemptCount: 1,
|
|
1434
|
+
});
|
|
1435
|
+
|
|
1436
|
+
// Default behavior (no --no-enqueue-next)
|
|
1437
|
+
await handleRuntimeInternalizationRunOnce({ workspace: WS, runner: 'dreamer', runtime: 'test-double', allowTestDouble: true, json: true });
|
|
1438
|
+
|
|
1439
|
+
expect(mockCommitNextTaskProposal).not.toHaveBeenCalled();
|
|
1440
|
+
const output = JSON.parse(consoleLogSpy.mock.calls[0][0]);
|
|
1441
|
+
expect(output.successorEnqueueAttempted).toBeUndefined();
|
|
1442
|
+
});
|
|
1443
|
+
|
|
1444
|
+
it('JSON output is single parseable JSON with successor fields', async () => {
|
|
1445
|
+
mockWakeOnce.mockResolvedValue({
|
|
1446
|
+
decision: 'would_lease',
|
|
1447
|
+
taskId: 'task-dreamer-json-001',
|
|
1448
|
+
taskKind: 'dreamer',
|
|
1449
|
+
});
|
|
1450
|
+
|
|
1451
|
+
mockRun.mockResolvedValue({
|
|
1452
|
+
status: 'succeeded',
|
|
1453
|
+
taskId: 'task-dreamer-json-001',
|
|
1454
|
+
runId: 'run-json-001',
|
|
1455
|
+
artifactId: 'pi-art-json-001',
|
|
1456
|
+
resultRef: 'dreamer://run-json-001',
|
|
1457
|
+
contextHash: 'ctx-json',
|
|
1458
|
+
output: { valid: true, taskId: 'task-dreamer-json-001', candidates: VALID_DREAMER_CANDIDATES, contextRefs: [], generatedAt: new Date().toISOString() },
|
|
1459
|
+
attemptCount: 1,
|
|
1460
|
+
});
|
|
1461
|
+
|
|
1462
|
+
mockCommitNextTaskProposal.mockResolvedValue({
|
|
1463
|
+
decision: 'successor_created',
|
|
1464
|
+
sourceTaskId: 'task-dreamer-json-001',
|
|
1465
|
+
successorTaskId: 'task-phil-json-001',
|
|
1466
|
+
successorKind: 'philosopher',
|
|
1467
|
+
});
|
|
1468
|
+
|
|
1469
|
+
await handleRuntimeInternalizationRunOnce({ workspace: WS, runner: 'dreamer', runtime: 'test-double', allowTestDouble: true, json: true });
|
|
1470
|
+
|
|
1471
|
+
const rawOutput = consoleLogSpy.mock.calls[0][0];
|
|
1472
|
+
// Must be single parseable JSON
|
|
1473
|
+
const output = JSON.parse(rawOutput);
|
|
1474
|
+
expect(output).toHaveProperty('successorEnqueueAttempted');
|
|
1475
|
+
expect(output).toHaveProperty('successorTasksCreated');
|
|
1476
|
+
expect(output).toHaveProperty('successorTaskIds');
|
|
1477
|
+
// nextAction may be undefined when successor is successfully created
|
|
1478
|
+
// but must be present on partial_success / no_successor / skipped
|
|
1479
|
+
});
|
|
1480
|
+
|
|
1481
|
+
it('text output for auto-enqueue shows successor info', async () => {
|
|
1482
|
+
mockWakeOnce.mockResolvedValue({
|
|
1483
|
+
decision: 'would_lease',
|
|
1484
|
+
taskId: 'task-dreamer-text-001',
|
|
1485
|
+
taskKind: 'dreamer',
|
|
1486
|
+
});
|
|
1487
|
+
|
|
1488
|
+
mockRun.mockResolvedValue({
|
|
1489
|
+
status: 'succeeded',
|
|
1490
|
+
taskId: 'task-dreamer-text-001',
|
|
1491
|
+
runId: 'run-text-001',
|
|
1492
|
+
artifactId: 'pi-art-text-001',
|
|
1493
|
+
resultRef: 'dreamer://run-text-001',
|
|
1494
|
+
contextHash: 'ctx-text',
|
|
1495
|
+
output: { valid: true, taskId: 'task-dreamer-text-001', candidates: VALID_DREAMER_CANDIDATES, contextRefs: [], generatedAt: new Date().toISOString() },
|
|
1496
|
+
attemptCount: 1,
|
|
1497
|
+
});
|
|
1498
|
+
|
|
1499
|
+
mockCommitNextTaskProposal.mockResolvedValue({
|
|
1500
|
+
decision: 'successor_created',
|
|
1501
|
+
sourceTaskId: 'task-dreamer-text-001',
|
|
1502
|
+
successorTaskId: 'task-phil-text-001',
|
|
1503
|
+
successorKind: 'philosopher',
|
|
1504
|
+
});
|
|
1505
|
+
|
|
1506
|
+
await handleRuntimeInternalizationRunOnce({ workspace: WS, runner: 'dreamer', runtime: 'test-double', allowTestDouble: true, json: false });
|
|
1507
|
+
|
|
1508
|
+
const text = consoleLogSpy.mock.calls.map((c: string[]) => c[0]).join('\n');
|
|
1509
|
+
expect(text).toContain('successor: task-phil-text-001');
|
|
1510
|
+
expect(text).toContain('enqueue_attempted: true');
|
|
1511
|
+
expect(text).toContain('successors_created: 1');
|
|
1512
|
+
});
|
|
1513
|
+
});
|
|
1514
|
+
|
|
1515
|
+
// === Commander parser-level tests for --no-enqueue-next ===
|
|
1516
|
+
// These tests verify that Commander correctly maps --no-enqueue-next to opts.enqueueNext.
|
|
1517
|
+
// They exercise the real Commander parsing path, NOT the handler directly.
|
|
1518
|
+
// See ERR-063: previous code used opts.noEnqueueNext (always undefined) instead of opts.enqueueNext.
|
|
1519
|
+
|
|
1520
|
+
describe('Commander --no-enqueue-next parser wiring', () => {
|
|
1521
|
+
function buildRunOnceCommand(capturedOpts: Record<string, unknown>) {
|
|
1522
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
1523
|
+
const { Command } = require('commander') as typeof import('commander');
|
|
1524
|
+
const program = new Command();
|
|
1525
|
+
program.exitOverride(); // prevent process.exit during tests
|
|
1526
|
+
|
|
1527
|
+
const internalizationCmd = program.command('internalization');
|
|
1528
|
+
internalizationCmd
|
|
1529
|
+
.command('run-once')
|
|
1530
|
+
.description('Wake-and-run: lease the next PI task and execute it')
|
|
1531
|
+
.option('-w, --workspace <path>', 'Workspace directory')
|
|
1532
|
+
.option('--runner <kind>', 'Runner kind', 'dreamer')
|
|
1533
|
+
.option('--runtime <kind>', 'Runtime adapter kind', 'config')
|
|
1534
|
+
.option('--allow-test-double', 'Acknowledge test-double runtime')
|
|
1535
|
+
.option('--no-enqueue-next', 'Skip successor enqueue after successful runner')
|
|
1536
|
+
.option('--timeout-ms <ms>', 'Runner timeout', parseInt)
|
|
1537
|
+
.option('--json', 'Output raw JSON')
|
|
1538
|
+
.action(async (opts) => {
|
|
1539
|
+
Object.assign(capturedOpts, opts);
|
|
1540
|
+
});
|
|
1541
|
+
|
|
1542
|
+
return program;
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1545
|
+
it('with --no-enqueue-next, Commander sets enqueueNext=false', async () => {
|
|
1546
|
+
const captured: Record<string, unknown> = {};
|
|
1547
|
+
const program = buildRunOnceCommand(captured);
|
|
1548
|
+
|
|
1549
|
+
await program.parseAsync([
|
|
1550
|
+
'node', 'pd', 'internalization', 'run-once',
|
|
1551
|
+
'--no-enqueue-next',
|
|
1552
|
+
'--workspace', '/tmp/test',
|
|
1553
|
+
'--runtime', 'test-double',
|
|
1554
|
+
'--allow-test-double',
|
|
1555
|
+
'--json',
|
|
1556
|
+
]);
|
|
1557
|
+
|
|
1558
|
+
expect(captured).toHaveProperty('enqueueNext', false);
|
|
1559
|
+
expect(captured).not.toHaveProperty('noEnqueueNext');
|
|
1560
|
+
});
|
|
1561
|
+
|
|
1562
|
+
it('without --no-enqueue-next, Commander sets enqueueNext=true (default)', async () => {
|
|
1563
|
+
const captured: Record<string, unknown> = {};
|
|
1564
|
+
const program = buildRunOnceCommand(captured);
|
|
1565
|
+
|
|
1566
|
+
await program.parseAsync([
|
|
1567
|
+
'node', 'pd', 'internalization', 'run-once',
|
|
1568
|
+
'--workspace', '/tmp/test',
|
|
1569
|
+
'--runtime', 'test-double',
|
|
1570
|
+
'--allow-test-double',
|
|
1571
|
+
'--json',
|
|
1572
|
+
]);
|
|
1573
|
+
|
|
1574
|
+
expect(captured).toHaveProperty('enqueueNext', true);
|
|
1575
|
+
});
|
|
1576
|
+
|
|
1577
|
+
it('opts has no noEnqueueNext property regardless of flag presence', async () => {
|
|
1578
|
+
const capturedWith: Record<string, unknown> = {};
|
|
1579
|
+
const programWith = buildRunOnceCommand(capturedWith);
|
|
1580
|
+
await programWith.parseAsync([
|
|
1581
|
+
'node', 'pd', 'internalization', 'run-once',
|
|
1582
|
+
'--no-enqueue-next', '--workspace', '/tmp/test',
|
|
1583
|
+
]);
|
|
1584
|
+
|
|
1585
|
+
const capturedWithout: Record<string, unknown> = {};
|
|
1586
|
+
const programWithout = buildRunOnceCommand(capturedWithout);
|
|
1587
|
+
await programWithout.parseAsync([
|
|
1588
|
+
'node', 'pd', 'internalization', 'run-once',
|
|
1589
|
+
'--workspace', '/tmp/test',
|
|
1590
|
+
]);
|
|
1591
|
+
|
|
1592
|
+
expect(capturedWith).not.toHaveProperty('noEnqueueNext');
|
|
1593
|
+
expect(capturedWithout).not.toHaveProperty('noEnqueueNext');
|
|
1594
|
+
});
|
|
1318
1595
|
});
|
|
@@ -21,6 +21,7 @@ vi.mock('@principles/core/runtime-v2', () => ({
|
|
|
21
21
|
InternalizationOrchestrator: vi.fn().mockImplementation(function () {
|
|
22
22
|
return { wakeOnce: mockWakeOnce };
|
|
23
23
|
}),
|
|
24
|
+
resolveOutputLanguage: vi.fn().mockReturnValue({ outputLanguage: 'zh-CN' }),
|
|
24
25
|
}));
|
|
25
26
|
|
|
26
27
|
import { handleRuntimeInternalizationWakeOnce } from '../../src/commands/runtime-internalization-wake-once.js';
|
|
@@ -87,6 +87,7 @@ vi.mock('@principles/core/runtime-v2', () => ({
|
|
|
87
87
|
listPruningReviews: mockListPruningReviews,
|
|
88
88
|
buildMaskedPrincipleSet: mockBuildMaskedPrincipleSet,
|
|
89
89
|
removeOrphanReferencesFromLedger: mockRemoveOrphanReferencesFromLedger,
|
|
90
|
+
resolveOutputLanguage: vi.fn().mockReturnValue({ outputLanguage: 'zh-CN' }),
|
|
90
91
|
}));
|
|
91
92
|
|
|
92
93
|
import { handlePruningReport, handlePruningExplain, handlePruningReview, handlePruningRollback, handlePruningOrphans } from '../../src/commands/runtime-pruning.js';
|
|
@@ -28,6 +28,7 @@ vi.mock('@principles/core/runtime-v2', () => ({
|
|
|
28
28
|
warnings: input.warnings ?? [],
|
|
29
29
|
})),
|
|
30
30
|
remediationAction: vi.fn((input) => input),
|
|
31
|
+
resolveOutputLanguage: vi.fn().mockReturnValue({ outputLanguage: 'zh-CN' }),
|
|
31
32
|
}));
|
|
32
33
|
|
|
33
34
|
import { handleRuntimeRecoverySweep } from '../../src/commands/runtime-recovery.js';
|
|
@@ -32,6 +32,7 @@ vi.mock('@principles/core/runtime-v2', () => ({
|
|
|
32
32
|
agentId: 'main',
|
|
33
33
|
}),
|
|
34
34
|
isRuntimeConfigError: vi.fn().mockReturnValue(false),
|
|
35
|
+
resolveOutputLanguage: vi.fn().mockReturnValue({ outputLanguage: 'zh-CN' }),
|
|
35
36
|
PDRuntimeError: class PDRuntimeError extends Error {
|
|
36
37
|
constructor(public category: string, message: string) {
|
|
37
38
|
super(message);
|
|
@@ -14,6 +14,7 @@ vi.mock('@principles/core/runtime-v2', () => ({
|
|
|
14
14
|
PainChainReadModel: vi.fn().mockImplementation(function () {
|
|
15
15
|
return { traceByPainId: mockTraceByPainId, close: mockPainChainClose };
|
|
16
16
|
}),
|
|
17
|
+
resolveOutputLanguage: vi.fn().mockReturnValue({ outputLanguage: 'zh-CN' }),
|
|
17
18
|
}));
|
|
18
19
|
|
|
19
20
|
vi.mock('../../src/resolve-workspace.js', () => ({
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for config-reader — readOutputLanguageFromWorkspace (PRI-336).
|
|
3
|
+
*
|
|
4
|
+
* Validates:
|
|
5
|
+
* - No config file → legitimate default, no warning
|
|
6
|
+
* - No principles section → legitimate default, no warning
|
|
7
|
+
* - No outputLanguage key → legitimate default, no warning
|
|
8
|
+
* - Read error → degraded with read_error reason + nextAction (ERR-002/009)
|
|
9
|
+
* - YAML parse error → degraded with yaml_parse_error reason + nextAction
|
|
10
|
+
* - Invalid config root → degraded with invalid_config_root reason + nextAction
|
|
11
|
+
* - Invalid principles structure → degraded with invalid_principles_structure reason + nextAction
|
|
12
|
+
* - Valid outputLanguage → resolved value
|
|
13
|
+
* - Invalid outputLanguage value → degraded with invalid value warning
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
17
|
+
import { readOutputLanguageFromWorkspace } from '../../src/config-reader.js';
|
|
18
|
+
|
|
19
|
+
// Mock fs and yaml
|
|
20
|
+
vi.mock('fs', () => ({
|
|
21
|
+
existsSync: vi.fn(),
|
|
22
|
+
readFileSync: vi.fn(),
|
|
23
|
+
}));
|
|
24
|
+
|
|
25
|
+
vi.mock('js-yaml', () => ({
|
|
26
|
+
default: {
|
|
27
|
+
load: vi.fn(),
|
|
28
|
+
JSON_SCHEMA: 'JSON_SCHEMA',
|
|
29
|
+
},
|
|
30
|
+
}));
|
|
31
|
+
|
|
32
|
+
import * as fs from 'fs';
|
|
33
|
+
import yaml from 'js-yaml';
|
|
34
|
+
|
|
35
|
+
const mockExistsSync = vi.mocked(fs.existsSync);
|
|
36
|
+
const mockReadFileSync = vi.mocked(fs.readFileSync);
|
|
37
|
+
const mockYamlLoad = vi.mocked(yaml.load);
|
|
38
|
+
|
|
39
|
+
beforeEach(() => {
|
|
40
|
+
vi.clearAllMocks();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
afterEach(() => {
|
|
44
|
+
vi.restoreAllMocks();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe('readOutputLanguageFromWorkspace', () => {
|
|
48
|
+
it('returns default with no warning when config file does not exist', () => {
|
|
49
|
+
mockExistsSync.mockReturnValue(false);
|
|
50
|
+
const result = readOutputLanguageFromWorkspace('/workspace');
|
|
51
|
+
expect(result.outputLanguage).toBe('zh-CN');
|
|
52
|
+
expect(result.degradationWarning).toBeUndefined();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('returns default with no warning when principles section is missing', () => {
|
|
56
|
+
mockExistsSync.mockReturnValue(true);
|
|
57
|
+
mockReadFileSync.mockReturnValue('version: 1\n');
|
|
58
|
+
mockYamlLoad.mockReturnValue({ version: 1 });
|
|
59
|
+
const result = readOutputLanguageFromWorkspace('/workspace');
|
|
60
|
+
expect(result.outputLanguage).toBe('zh-CN');
|
|
61
|
+
expect(result.degradationWarning).toBeUndefined();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('returns default with no warning when outputLanguage key is missing', () => {
|
|
65
|
+
mockExistsSync.mockReturnValue(true);
|
|
66
|
+
mockReadFileSync.mockReturnValue('version: 1\nprinciples: {}\n');
|
|
67
|
+
mockYamlLoad.mockReturnValue({ version: 1, principles: {} });
|
|
68
|
+
const result = readOutputLanguageFromWorkspace('/workspace');
|
|
69
|
+
expect(result.outputLanguage).toBe('zh-CN');
|
|
70
|
+
expect(result.degradationWarning).toBeUndefined();
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('returns valid outputLanguage when configured', () => {
|
|
74
|
+
mockExistsSync.mockReturnValue(true);
|
|
75
|
+
mockReadFileSync.mockReturnValue('version: 1\nprinciples:\n outputLanguage: en\n');
|
|
76
|
+
mockYamlLoad.mockReturnValue({ version: 1, principles: { outputLanguage: 'en' } });
|
|
77
|
+
const result = readOutputLanguageFromWorkspace('/workspace');
|
|
78
|
+
expect(result.outputLanguage).toBe('en');
|
|
79
|
+
expect(result.degradationWarning).toBeUndefined();
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('returns degraded with read_error when readFileSync throws', () => {
|
|
83
|
+
mockExistsSync.mockReturnValue(true);
|
|
84
|
+
mockReadFileSync.mockImplementation(() => { throw new Error('EACCES: permission denied'); });
|
|
85
|
+
const result = readOutputLanguageFromWorkspace('/workspace');
|
|
86
|
+
expect(result.outputLanguage).toBe('zh-CN');
|
|
87
|
+
expect(result.degradationWarning).toContain('Failed to read .pd/config.yaml');
|
|
88
|
+
expect(result.degradationWarning).toContain('nextAction');
|
|
89
|
+
expect(result.degradationWarning).toContain('Check file permissions');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('returns degraded with yaml_parse_error when yaml.load throws', () => {
|
|
93
|
+
mockExistsSync.mockReturnValue(true);
|
|
94
|
+
mockReadFileSync.mockReturnValue('invalid: yaml: [broken');
|
|
95
|
+
mockYamlLoad.mockImplementation(() => { throw new Error('YAML syntax error'); });
|
|
96
|
+
const result = readOutputLanguageFromWorkspace('/workspace');
|
|
97
|
+
expect(result.outputLanguage).toBe('zh-CN');
|
|
98
|
+
expect(result.degradationWarning).toContain('Failed to parse .pd/config.yaml');
|
|
99
|
+
expect(result.degradationWarning).toContain('nextAction');
|
|
100
|
+
expect(result.degradationWarning).toContain('Fix YAML syntax');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('returns degraded with invalid_config_root when parsed YAML is not an object', () => {
|
|
104
|
+
mockExistsSync.mockReturnValue(true);
|
|
105
|
+
mockReadFileSync.mockReturnValue('"just a string"');
|
|
106
|
+
mockYamlLoad.mockReturnValue('just a string');
|
|
107
|
+
const result = readOutputLanguageFromWorkspace('/workspace');
|
|
108
|
+
expect(result.outputLanguage).toBe('zh-CN');
|
|
109
|
+
expect(result.degradationWarning).toContain('root is not an object');
|
|
110
|
+
expect(result.degradationWarning).toContain('nextAction');
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('returns degraded with invalid_config_root when parsed YAML is an array', () => {
|
|
114
|
+
mockExistsSync.mockReturnValue(true);
|
|
115
|
+
mockReadFileSync.mockReturnValue('- item1\n- item2\n');
|
|
116
|
+
mockYamlLoad.mockReturnValue(['item1', 'item2']);
|
|
117
|
+
const result = readOutputLanguageFromWorkspace('/workspace');
|
|
118
|
+
expect(result.outputLanguage).toBe('zh-CN');
|
|
119
|
+
expect(result.degradationWarning).toContain('root is not an object');
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('returns degraded with invalid_principles_structure when principles is not an object', () => {
|
|
123
|
+
mockExistsSync.mockReturnValue(true);
|
|
124
|
+
mockReadFileSync.mockReturnValue('version: 1\nprinciples: "not an object"\n');
|
|
125
|
+
mockYamlLoad.mockReturnValue({ version: 1, principles: 'not an object' });
|
|
126
|
+
const result = readOutputLanguageFromWorkspace('/workspace');
|
|
127
|
+
expect(result.outputLanguage).toBe('zh-CN');
|
|
128
|
+
expect(result.degradationWarning).toContain('principles field is not an object');
|
|
129
|
+
expect(result.degradationWarning).toContain('nextAction');
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('returns degraded with invalid value warning when outputLanguage is invalid', () => {
|
|
133
|
+
mockExistsSync.mockReturnValue(true);
|
|
134
|
+
mockReadFileSync.mockReturnValue('version: 1\nprinciples:\n outputLanguage: fr\n');
|
|
135
|
+
mockYamlLoad.mockReturnValue({ version: 1, principles: { outputLanguage: 'fr' } });
|
|
136
|
+
const result = readOutputLanguageFromWorkspace('/workspace');
|
|
137
|
+
expect(result.outputLanguage).toBe('zh-CN');
|
|
138
|
+
expect(result.degradationWarning).toContain('invalid');
|
|
139
|
+
expect(result.degradationWarning).toContain('fr');
|
|
140
|
+
expect(result.degradationWarning).toContain('nextAction');
|
|
141
|
+
});
|
|
142
|
+
});
|