@principles/pd-cli 1.111.0 → 1.112.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@principles/pd-cli",
3
- "version": "1.111.0",
3
+ "version": "1.112.0",
4
4
  "description": "PD CLI — Pain recording, sample management, and evolution tasks",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,4 +1,4 @@
1
- import { describe, it, expect, vi, beforeEach } from 'vitest';
1
+ import { describe, it, expect, vi, beforeEach, afterEach, beforeAll } from 'vitest';
2
2
  import { Command } from 'commander';
3
3
 
4
4
  const { MockRuntimeStateManager, mockGetCandidatesByTaskId, mockUpdateCandidateStatus } = vi.hoisted(() => {
@@ -20,6 +20,11 @@ const { MockRuntimeStateManager, mockGetCandidatesByTaskId, mockUpdateCandidateS
20
20
  connection = {} as Record<string, unknown>;
21
21
  taskStore = {};
22
22
  runStore = {};
23
+ piArtifactStore = {};
24
+ getRetryPolicy = vi.fn().mockReturnValue({
25
+ shouldRetry: () => true,
26
+ calculateBackoff: () => 10,
27
+ });
23
28
  }
24
29
  return { MockRuntimeStateManager, mockGetCandidatesByTaskId, mockUpdateCandidateStatus };
25
30
  }, { validateType: true });
@@ -161,6 +166,16 @@ const SUCCEEDED_RESULT = {
161
166
  attemptCount: 1,
162
167
  };
163
168
 
169
+ // ERR-067: reset the mocked run() to a clean successful default after each test
170
+ // so later tests don't see leftover mockResolvedValueOnce() chains or call counts.
171
+ const DEFAULT_SUCCEEDED_RUN_RESULT = {
172
+ status: 'succeeded' as const,
173
+ taskId: 'test-task-1',
174
+ output: SUCCEEDED_RESULT.output,
175
+ };
176
+
177
+
178
+
164
179
  describe('pd diagnose run --runtime routing', () => {
165
180
  let mockResolveRuntimeConfig: ReturnType<typeof vi.fn>;
166
181
  let mockIsRuntimeConfigError: ReturnType<typeof vi.fn>;
@@ -928,3 +943,165 @@ describe('pd status stalled-threshold validation', () => {
928
943
  consoleSpy.mockRestore();
929
944
  });
930
945
  });
946
+
947
+ // ERR-067: 1 initial run + maxRetryLoops(10) before the safety limit converts retried to failed.
948
+ const EXPECTED_MAX_RETRY_CALLS = 11;
949
+
950
+ describe('ERR-067: pd diagnose run retry loop for retried status', () => {
951
+ beforeEach(async () => {
952
+ vi.clearAllMocks();
953
+ const { run } = await import('@principles/core/runtime-v2');
954
+ vi.mocked(run).mockResolvedValue(DEFAULT_SUCCEEDED_RUN_RESULT);
955
+ });
956
+
957
+ afterEach(async () => {
958
+ const { run } = await import('@principles/core/runtime-v2');
959
+ vi.mocked(run).mockResolvedValue(DEFAULT_SUCCEEDED_RUN_RESULT);
960
+ });
961
+
962
+ it('ERR-067-01: retried status triggers retry loop, succeeds on second attempt', async () => {
963
+ const { run } = await import('@principles/core/runtime-v2');
964
+ const runMock = vi.mocked(run);
965
+ runMock
966
+ .mockResolvedValueOnce({
967
+ status: 'retried' as const,
968
+ taskId: 'test-task-1',
969
+ attemptCount: 1,
970
+ })
971
+ .mockResolvedValueOnce(SUCCEEDED_RESULT);
972
+
973
+ const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
974
+ const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
975
+
976
+ await handleDiagnoseRun({
977
+ taskId: 'test-task-1',
978
+ workspace: '/tmp/fake-workspace',
979
+ runtime: 'test-double',
980
+ json: false,
981
+ } as DiagnoseRunOptions);
982
+
983
+ expect(runMock).toHaveBeenCalledTimes(2);
984
+ expect(exitSpy).not.toHaveBeenCalledWith(1);
985
+
986
+ consoleSpy.mockRestore();
987
+ exitSpy.mockRestore();
988
+ runMock.mockResolvedValue(DEFAULT_SUCCEEDED_RUN_RESULT);
989
+ });
990
+
991
+ it('ERR-067-02: retried status loops until maxRetryLoops, then converts to failed (P0-1 fix)', async () => {
992
+ const { run } = await import('@principles/core/runtime-v2');
993
+ const runMock = vi.mocked(run);
994
+ runMock.mockResolvedValue({
995
+ status: 'retried' as const,
996
+ taskId: 'test-task-1',
997
+ attemptCount: 1,
998
+ });
999
+
1000
+ const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
1001
+ const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
1002
+
1003
+ await handleDiagnoseRun({
1004
+ taskId: 'test-task-1',
1005
+ workspace: '/tmp/fake-workspace',
1006
+ runtime: 'test-double',
1007
+ json: false,
1008
+ } as DiagnoseRunOptions);
1009
+
1010
+ expect(runMock).toHaveBeenCalledTimes(EXPECTED_MAX_RETRY_CALLS);
1011
+ expect(exitSpy).toHaveBeenCalledWith(1);
1012
+
1013
+ consoleSpy.mockRestore();
1014
+ exitSpy.mockRestore();
1015
+ runMock.mockResolvedValue(DEFAULT_SUCCEEDED_RUN_RESULT);
1016
+ });
1017
+
1018
+ it('ERR-067-03: retried with failureReason propagates to failed status (P0-1 fix)', async () => {
1019
+ const { run } = await import('@principles/core/runtime-v2');
1020
+ const runMock = vi.mocked(run);
1021
+ runMock.mockResolvedValue({
1022
+ status: 'retried' as const,
1023
+ taskId: 'test-task-1',
1024
+ attemptCount: 1,
1025
+ failureReason: 'Schema validation failed',
1026
+ });
1027
+
1028
+ const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
1029
+ const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
1030
+
1031
+ await handleDiagnoseRun({
1032
+ taskId: 'test-task-1',
1033
+ workspace: '/tmp/fake-workspace',
1034
+ runtime: 'test-double',
1035
+ json: true,
1036
+ } as DiagnoseRunOptions);
1037
+
1038
+ expect(runMock).toHaveBeenCalledTimes(EXPECTED_MAX_RETRY_CALLS);
1039
+
1040
+ const jsonOutput = consoleSpy.mock.calls.find(call => {
1041
+ try {
1042
+ const parsed = JSON.parse(call[0] as string);
1043
+ return parsed.status === 'failed';
1044
+ } catch { return false; }
1045
+ });
1046
+ expect(jsonOutput).toBeDefined();
1047
+ const parsed = JSON.parse((jsonOutput as [string])[0]);
1048
+ expect(parsed.status).toBe('failed');
1049
+ expect(parsed.failureReason).toContain('Max retry loops');
1050
+ expect(parsed.failureReason).toContain('Schema validation failed');
1051
+
1052
+ consoleSpy.mockRestore();
1053
+ exitSpy.mockRestore();
1054
+ runMock.mockResolvedValue(DEFAULT_SUCCEEDED_RUN_RESULT);
1055
+ });
1056
+
1057
+ it('ERR-067-04: succeeded status does NOT trigger retry loop', async () => {
1058
+ const { run } = await import('@principles/core/runtime-v2');
1059
+ const runMock = vi.mocked(run);
1060
+ runMock.mockResolvedValue(SUCCEEDED_RESULT);
1061
+
1062
+ const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
1063
+ const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
1064
+
1065
+ await handleDiagnoseRun({
1066
+ taskId: 'test-task-1',
1067
+ workspace: '/tmp/fake-workspace',
1068
+ runtime: 'test-double',
1069
+ json: false,
1070
+ } as DiagnoseRunOptions);
1071
+
1072
+ expect(runMock).toHaveBeenCalledTimes(1);
1073
+ expect(exitSpy).not.toHaveBeenCalledWith(1);
1074
+
1075
+ consoleSpy.mockRestore();
1076
+ exitSpy.mockRestore();
1077
+ runMock.mockResolvedValue(DEFAULT_SUCCEEDED_RUN_RESULT);
1078
+ });
1079
+
1080
+ it('ERR-067-05: failed status does NOT trigger retry loop', async () => {
1081
+ const { run } = await import('@principles/core/runtime-v2');
1082
+ const runMock = vi.mocked(run);
1083
+ runMock.mockResolvedValue({
1084
+ status: 'failed' as const,
1085
+ taskId: 'test-task-1',
1086
+ attemptCount: 3,
1087
+ failureReason: 'Max attempts exceeded',
1088
+ });
1089
+
1090
+ const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
1091
+ const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
1092
+
1093
+ await handleDiagnoseRun({
1094
+ taskId: 'test-task-1',
1095
+ workspace: '/tmp/fake-workspace',
1096
+ runtime: 'test-double',
1097
+ json: false,
1098
+ } as DiagnoseRunOptions);
1099
+
1100
+ expect(runMock).toHaveBeenCalledTimes(1);
1101
+ expect(exitSpy).toHaveBeenCalledWith(1);
1102
+
1103
+ consoleSpy.mockRestore();
1104
+ exitSpy.mockRestore();
1105
+ runMock.mockResolvedValue(DEFAULT_SUCCEEDED_RUN_RESULT);
1106
+ });
1107
+ });
@@ -421,4 +421,63 @@ describe('resolveRuntimeWithOverrides', () => {
421
421
  expect(result.runtimeProfileLabel).toBe('pi-ai: anthropic/claude-3-5-sonnet');
422
422
  } finally { rmTmpDir(tmp); }
423
423
  });
424
+
425
+ describe('empty-string normalization (PRI-402)', () => {
426
+ it('normalizes empty-string provider override to undefined', () => {
427
+ const tmp = mkTmpDir();
428
+ writeConfig(tmp, makeValidPiAiConfigYaml(tmp));
429
+ try {
430
+ const result = resolveRuntimeWithOverrides(tmp, { provider: '' }, mockEnvWithKeys);
431
+ expect(result.mergedConfig).not.toBeNull();
432
+ if (!result.mergedConfig) throw new Error('Expected mergedConfig');
433
+ expect(result.mergedConfig.provider).toBe(undefined);
434
+ } finally { rmTmpDir(tmp); }
435
+ });
436
+
437
+ it('normalizes empty-string model override to undefined', () => {
438
+ const tmp = mkTmpDir();
439
+ writeConfig(tmp, makeValidPiAiConfigYaml(tmp));
440
+ try {
441
+ const result = resolveRuntimeWithOverrides(tmp, { model: '' }, mockEnvWithKeys);
442
+ expect(result.mergedConfig).not.toBeNull();
443
+ if (!result.mergedConfig) throw new Error('Expected mergedConfig');
444
+ expect(result.mergedConfig.model).toBe(undefined);
445
+ } finally { rmTmpDir(tmp); }
446
+ });
447
+
448
+ it('normalizes empty-string apiKeyEnv override to undefined', () => {
449
+ const tmp = mkTmpDir();
450
+ writeConfig(tmp, makeValidPiAiConfigYaml(tmp));
451
+ try {
452
+ const result = resolveRuntimeWithOverrides(tmp, { apiKeyEnv: '' }, mockEnvWithKeys);
453
+ expect(result.mergedConfig).not.toBeNull();
454
+ if (!result.mergedConfig) throw new Error('Expected mergedConfig');
455
+ expect(result.mergedConfig.apiKeyEnv).toBe(undefined);
456
+ } finally { rmTmpDir(tmp); }
457
+ });
458
+
459
+ it('normalizes empty-string baseUrl override to undefined', () => {
460
+ const tmp = mkTmpDir();
461
+ writeConfig(tmp, makeValidPiAiConfigYaml(tmp));
462
+ try {
463
+ const result = resolveRuntimeWithOverrides(tmp, { baseUrl: '' }, mockEnvWithKeys);
464
+ expect(result.mergedConfig).not.toBeNull();
465
+ if (!result.mergedConfig) throw new Error('Expected mergedConfig');
466
+ expect(result.mergedConfig.baseUrl).toBe(undefined);
467
+ } finally { rmTmpDir(tmp); }
468
+ });
469
+
470
+ it('preserves non-empty string values', () => {
471
+ const tmp = mkTmpDir();
472
+ writeConfig(tmp, makeValidPiAiConfigYaml(tmp));
473
+ try {
474
+ const result = resolveRuntimeWithOverrides(tmp, {}, mockEnvWithKeys);
475
+ expect(result.mergedConfig).not.toBeNull();
476
+ if (!result.mergedConfig) throw new Error('Expected mergedConfig');
477
+ expect(result.mergedConfig.provider).toBe('anthropic');
478
+ expect(result.mergedConfig.model).toBe('claude-3-5-sonnet');
479
+ expect(result.mergedConfig.apiKeyEnv).toBe('ANTHROPIC_API_KEY');
480
+ } finally { rmTmpDir(tmp); }
481
+ });
482
+ });
424
483
  });