@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,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
|
});
|