@principles/pd-cli 1.101.0 → 1.103.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.js +27 -27
- package/dist/commands/diagnose.js.map +1 -1
- package/dist/commands/pain-retry.d.ts.map +1 -1
- package/dist/commands/pain-retry.js +22 -27
- package/dist/commands/pain-retry.js.map +1 -1
- package/dist/commands/runtime-internalization-integrity.d.ts.map +1 -1
- package/dist/commands/runtime-internalization-integrity.js +40 -1
- package/dist/commands/runtime-internalization-integrity.js.map +1 -1
- package/dist/commands/runtime-internalization-run-once.d.ts.map +1 -1
- package/dist/commands/runtime-internalization-run-once.js +11 -9
- package/dist/commands/runtime-internalization-run-once.js.map +1 -1
- package/dist/commands/runtime.d.ts +1 -1
- package/dist/commands/runtime.d.ts.map +1 -1
- package/dist/commands/runtime.js +92 -25
- package/dist/commands/runtime.js.map +1 -1
- package/dist/services/mainline-snapshot-assembler.d.ts +35 -0
- package/dist/services/mainline-snapshot-assembler.d.ts.map +1 -0
- package/dist/services/mainline-snapshot-assembler.js +399 -0
- package/dist/services/mainline-snapshot-assembler.js.map +1 -0
- package/dist/services/resolve-runtime-from-pd-config.d.ts +59 -0
- package/dist/services/resolve-runtime-from-pd-config.d.ts.map +1 -0
- package/dist/services/resolve-runtime-from-pd-config.js +96 -0
- package/dist/services/resolve-runtime-from-pd-config.js.map +1 -0
- package/package.json +1 -1
- package/src/commands/diagnose.ts +26 -26
- package/src/commands/pain-retry.ts +21 -25
- package/src/commands/runtime-internalization-integrity.ts +40 -1
- package/src/commands/runtime-internalization-run-once.ts +10 -9
- package/src/commands/runtime.ts +96 -24
- package/src/services/mainline-snapshot-assembler.ts +544 -0
- package/src/services/resolve-runtime-from-pd-config.ts +142 -0
- package/tests/commands/console-launcher-edge-cases.test.ts +14 -47
- package/tests/commands/diagnose.test.ts +91 -39
- package/tests/commands/pain-retry.test.ts +130 -15
- package/tests/commands/pri-393-runtime-config-unification.test.ts +284 -0
- package/tests/commands/runtime-internalization-integrity.test.ts +37 -0
- package/tests/commands/runtime-internalization-run-once.test.ts +59 -53
- package/tests/commands/runtime.test.ts +124 -1
- package/tests/services/mainline-snapshot-assembler.test.ts +425 -0
|
@@ -55,65 +55,32 @@ describe('Port competition scenarios', () => {
|
|
|
55
55
|
});
|
|
56
56
|
|
|
57
57
|
it('findAvailablePort skips occupied ports in sequence', async () => {
|
|
58
|
-
//
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
probeServer.listen(0, '127.0.0.1', () => {
|
|
64
|
-
const addr = probeServer.address();
|
|
65
|
-
if (typeof addr === 'object' && addr) resolve(addr.port);
|
|
66
|
-
});
|
|
67
|
-
});
|
|
68
|
-
await new Promise<void>((resolve) => probeServer.close(() => resolve()));
|
|
69
|
-
|
|
70
|
-
for (let i = 0; i < 3; i++) {
|
|
71
|
-
const s = net.createServer();
|
|
72
|
-
await new Promise<void>((resolve) => {
|
|
73
|
-
s.listen(basePort + i, '127.0.0.1', () => resolve());
|
|
74
|
-
});
|
|
75
|
-
servers.push(s);
|
|
76
|
-
}
|
|
77
|
-
|
|
58
|
+
// Use mock to simulate 3 consecutive occupied ports — avoids flaky real-network I/O
|
|
59
|
+
const basePort = 49200;
|
|
60
|
+
(globalThis as any).__mockIsPortInUse = async (_host: string, port: number) => {
|
|
61
|
+
return port >= basePort && port <= basePort + 2;
|
|
62
|
+
};
|
|
78
63
|
try {
|
|
79
|
-
// Should skip all 3 and return the next free one
|
|
80
64
|
const port = await findAvailablePort('127.0.0.1', basePort, 5);
|
|
65
|
+
// Should skip basePort, basePort+1, basePort+2 and return basePort+3
|
|
81
66
|
expect(port).toBe(basePort + 3);
|
|
82
67
|
} finally {
|
|
83
|
-
|
|
84
|
-
await new Promise<void>((resolve) => s.close(() => resolve()));
|
|
85
|
-
}
|
|
68
|
+
delete (globalThis as any).__mockIsPortInUse;
|
|
86
69
|
}
|
|
87
70
|
});
|
|
88
71
|
|
|
89
72
|
it('returns null when all fallback ports are exhausted', async () => {
|
|
90
|
-
//
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
const addr = probeServer.address();
|
|
96
|
-
if (typeof addr === 'object' && addr) resolve(addr.port);
|
|
97
|
-
});
|
|
98
|
-
});
|
|
99
|
-
await new Promise<void>((resolve) => probeServer.close(() => resolve()));
|
|
100
|
-
|
|
101
|
-
for (let i = 0; i < 10; i++) {
|
|
102
|
-
const s = net.createServer();
|
|
103
|
-
await new Promise<void>((resolve) => {
|
|
104
|
-
s.listen(basePort + i, '127.0.0.1', () => resolve());
|
|
105
|
-
});
|
|
106
|
-
servers.push(s);
|
|
107
|
-
}
|
|
108
|
-
|
|
73
|
+
// Use mock to simulate all ports occupied — avoids flaky real-network I/O
|
|
74
|
+
const basePort = 49300;
|
|
75
|
+
(globalThis as any).__mockIsPortInUse = async (_host: string, port: number) => {
|
|
76
|
+
return port >= basePort && port <= basePort + 9;
|
|
77
|
+
};
|
|
109
78
|
try {
|
|
110
|
-
// With limit=5,
|
|
79
|
+
// With limit=5, all 5 candidates are occupied → null
|
|
111
80
|
const port = await findAvailablePort('127.0.0.1', basePort, 5);
|
|
112
81
|
expect(port).toBeNull();
|
|
113
82
|
} finally {
|
|
114
|
-
|
|
115
|
-
await new Promise<void>((resolve) => s.close(() => resolve()));
|
|
116
|
-
}
|
|
83
|
+
delete (globalThis as any).__mockIsPortInUse;
|
|
117
84
|
}
|
|
118
85
|
});
|
|
119
86
|
});
|
|
@@ -41,6 +41,23 @@ const { MockPrincipleTreeLedgerAdapter } = vi.hoisted(() => {
|
|
|
41
41
|
return { MockPrincipleTreeLedgerAdapter };
|
|
42
42
|
});
|
|
43
43
|
|
|
44
|
+
const { mockResolveRuntimeFromPdConfig } = vi.hoisted(() => {
|
|
45
|
+
const mockResolveRuntimeFromPdConfig = vi.fn().mockReturnValue({
|
|
46
|
+
result: {
|
|
47
|
+
runtimeKind: 'pi-ai',
|
|
48
|
+
provider: 'test-provider',
|
|
49
|
+
model: 'test-model',
|
|
50
|
+
apiKeyEnv: 'TEST_KEY',
|
|
51
|
+
timeoutMs: 300000,
|
|
52
|
+
agentId: 'main',
|
|
53
|
+
},
|
|
54
|
+
legacyWarnings: [],
|
|
55
|
+
configSource: '.pd/config.yaml',
|
|
56
|
+
configLoadResult: { ok: true, effective: {}, defaults: {}, legacyFilesDetected: [] },
|
|
57
|
+
});
|
|
58
|
+
return { mockResolveRuntimeFromPdConfig };
|
|
59
|
+
});
|
|
60
|
+
|
|
44
61
|
vi.mock('../../src/resolve-workspace.js', () => ({
|
|
45
62
|
resolveWorkspaceDir: vi.fn().mockReturnValue('/tmp/fake-workspace'),
|
|
46
63
|
}));
|
|
@@ -122,6 +139,10 @@ vi.mock('../../src/services/pd-config-loader.js', () => ({
|
|
|
122
139
|
computeFlagsFromLoadResult: vi.fn().mockReturnValue({}),
|
|
123
140
|
}));
|
|
124
141
|
|
|
142
|
+
vi.mock('../../src/services/resolve-runtime-from-pd-config.js', () => ({
|
|
143
|
+
resolveRuntimeFromPdConfig: mockResolveRuntimeFromPdConfig,
|
|
144
|
+
}));
|
|
145
|
+
|
|
125
146
|
import { handleDiagnoseRun, handleDiagnoseStatus, type DiagnoseRunOptions } from '../../src/commands/diagnose.js';
|
|
126
147
|
|
|
127
148
|
const SUCCEEDED_RESULT = {
|
|
@@ -177,14 +198,18 @@ describe('pd diagnose run --runtime routing', () => {
|
|
|
177
198
|
exitSpy.mockRestore();
|
|
178
199
|
});
|
|
179
200
|
|
|
180
|
-
it('HG-03: --runtime openclaw-cli without mode (no file config) fails via
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
201
|
+
it('HG-03: --runtime openclaw-cli without mode (no file config) fails via resolveRuntimeFromPdConfig', async () => {
|
|
202
|
+
mockResolveRuntimeFromPdConfig.mockReturnValueOnce({
|
|
203
|
+
result: {
|
|
204
|
+
ok: false,
|
|
205
|
+
reason: 'missing_openclaw_mode',
|
|
206
|
+
message: 'runtimeKind is openclaw-cli but no mode specified',
|
|
207
|
+
nextAction: 'Provide exactly one mode',
|
|
208
|
+
},
|
|
209
|
+
legacyWarnings: [],
|
|
210
|
+
configSource: '.pd/config.yaml',
|
|
211
|
+
configLoadResult: { ok: true, effective: {}, defaults: {}, legacyFilesDetected: [] },
|
|
186
212
|
});
|
|
187
|
-
mockIsRuntimeConfigError.mockReturnValueOnce(true);
|
|
188
213
|
|
|
189
214
|
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
190
215
|
const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
|
|
@@ -196,7 +221,7 @@ describe('pd diagnose run --runtime routing', () => {
|
|
|
196
221
|
json: false,
|
|
197
222
|
} as DiagnoseRunOptions);
|
|
198
223
|
|
|
199
|
-
expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining('no mode
|
|
224
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining('no mode resolved'));
|
|
200
225
|
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
201
226
|
|
|
202
227
|
consoleErrorSpy.mockRestore();
|
|
@@ -226,13 +251,17 @@ describe('pd diagnose run --runtime routing', () => {
|
|
|
226
251
|
});
|
|
227
252
|
|
|
228
253
|
it('DPB-09: openclaw-cli with file config openclawMode succeeds without CLI flag', async () => {
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
254
|
+
mockResolveRuntimeFromPdConfig.mockReturnValueOnce({
|
|
255
|
+
result: {
|
|
256
|
+
runtimeKind: 'openclaw-cli',
|
|
257
|
+
openclawMode: 'local',
|
|
258
|
+
timeoutMs: 300000,
|
|
259
|
+
agentId: 'main',
|
|
260
|
+
},
|
|
261
|
+
legacyWarnings: [],
|
|
262
|
+
configSource: '.pd/config.yaml',
|
|
263
|
+
configLoadResult: { ok: true, effective: {}, defaults: {}, legacyFilesDetected: [] },
|
|
234
264
|
});
|
|
235
|
-
mockIsRuntimeConfigError.mockReturnValueOnce(false);
|
|
236
265
|
|
|
237
266
|
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
238
267
|
const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
|
|
@@ -250,14 +279,18 @@ describe('pd diagnose run --runtime routing', () => {
|
|
|
250
279
|
exitSpy.mockRestore();
|
|
251
280
|
});
|
|
252
281
|
|
|
253
|
-
it('DPB-09: openclaw-cli flag overrides file config mode', async () => {
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
282
|
+
it('DPB-09: openclaw-cli flag overrides file config mode (config=gateway, flag=local → runtimeMode=local)', async () => {
|
|
283
|
+
mockResolveRuntimeFromPdConfig.mockReturnValueOnce({
|
|
284
|
+
result: {
|
|
285
|
+
runtimeKind: 'openclaw-cli',
|
|
286
|
+
openclawMode: 'gateway',
|
|
287
|
+
timeoutMs: 300000,
|
|
288
|
+
agentId: 'main',
|
|
289
|
+
},
|
|
290
|
+
legacyWarnings: [],
|
|
291
|
+
configSource: '.pd/config.yaml',
|
|
292
|
+
configLoadResult: { ok: true, effective: {}, defaults: {}, legacyFilesDetected: [] },
|
|
259
293
|
});
|
|
260
|
-
mockIsRuntimeConfigError.mockReturnValueOnce(false);
|
|
261
294
|
|
|
262
295
|
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
263
296
|
const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
|
|
@@ -270,6 +303,13 @@ describe('pd diagnose run --runtime routing', () => {
|
|
|
270
303
|
json: false,
|
|
271
304
|
} as DiagnoseRunOptions);
|
|
272
305
|
|
|
306
|
+
// Flag override: config says gateway, flag says local → adapter gets local
|
|
307
|
+
const OpenClawCliMock = vi.mocked(
|
|
308
|
+
await import('@principles/core/runtime-v2').then(m => m.OpenClawCliRuntimeAdapter),
|
|
309
|
+
);
|
|
310
|
+
expect(OpenClawCliMock).toHaveBeenCalledWith(
|
|
311
|
+
expect.objectContaining({ runtimeMode: 'local' }),
|
|
312
|
+
);
|
|
273
313
|
expect(exitSpy).not.toHaveBeenCalledWith(1);
|
|
274
314
|
|
|
275
315
|
consoleSpy.mockRestore();
|
|
@@ -277,13 +317,17 @@ describe('pd diagnose run --runtime routing', () => {
|
|
|
277
317
|
});
|
|
278
318
|
|
|
279
319
|
it('DPB-09: openclaw-cli missing mode (--json) outputs JSON error', async () => {
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
320
|
+
mockResolveRuntimeFromPdConfig.mockReturnValueOnce({
|
|
321
|
+
result: {
|
|
322
|
+
runtimeKind: 'openclaw-cli',
|
|
323
|
+
openclawMode: undefined,
|
|
324
|
+
timeoutMs: 300000,
|
|
325
|
+
agentId: 'main',
|
|
326
|
+
},
|
|
327
|
+
legacyWarnings: [],
|
|
328
|
+
configSource: '.pd/config.yaml',
|
|
329
|
+
configLoadResult: { ok: true, effective: {}, defaults: {}, legacyFilesDetected: [] },
|
|
285
330
|
});
|
|
286
|
-
mockIsRuntimeConfigError.mockReturnValueOnce(true);
|
|
287
331
|
|
|
288
332
|
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
289
333
|
const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
|
|
@@ -323,13 +367,17 @@ describe('pd diagnose run --runtime routing', () => {
|
|
|
323
367
|
});
|
|
324
368
|
|
|
325
369
|
it('DPB-09: openclaw-cli --openclaw-gateway constructs adapter with runtimeMode=gateway', async () => {
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
370
|
+
mockResolveRuntimeFromPdConfig.mockReturnValueOnce({
|
|
371
|
+
result: {
|
|
372
|
+
runtimeKind: 'openclaw-cli',
|
|
373
|
+
openclawMode: 'gateway',
|
|
374
|
+
timeoutMs: 300000,
|
|
375
|
+
agentId: 'main',
|
|
376
|
+
},
|
|
377
|
+
legacyWarnings: [],
|
|
378
|
+
configSource: '.pd/config.yaml',
|
|
379
|
+
configLoadResult: { ok: true, effective: {}, defaults: {}, legacyFilesDetected: [] },
|
|
331
380
|
});
|
|
332
|
-
mockIsRuntimeConfigError.mockReturnValueOnce(false);
|
|
333
381
|
|
|
334
382
|
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
335
383
|
const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
|
|
@@ -355,13 +403,17 @@ describe('pd diagnose run --runtime routing', () => {
|
|
|
355
403
|
});
|
|
356
404
|
|
|
357
405
|
it('DPB-09: openclaw-cli --openclaw-local constructs adapter with runtimeMode=local', async () => {
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
406
|
+
mockResolveRuntimeFromPdConfig.mockReturnValueOnce({
|
|
407
|
+
result: {
|
|
408
|
+
runtimeKind: 'openclaw-cli',
|
|
409
|
+
openclawMode: 'local',
|
|
410
|
+
timeoutMs: 300000,
|
|
411
|
+
agentId: 'main',
|
|
412
|
+
},
|
|
413
|
+
legacyWarnings: [],
|
|
414
|
+
configSource: '.pd/config.yaml',
|
|
415
|
+
configLoadResult: { ok: true, effective: {}, defaults: {}, legacyFilesDetected: [] },
|
|
363
416
|
});
|
|
364
|
-
mockIsRuntimeConfigError.mockReturnValueOnce(false);
|
|
365
417
|
|
|
366
418
|
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
367
419
|
const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
|
|
@@ -55,7 +55,7 @@ const { MockPrincipleTreeLedgerAdapter } = vi.hoisted(() => {
|
|
|
55
55
|
return { MockPrincipleTreeLedgerAdapter };
|
|
56
56
|
});
|
|
57
57
|
|
|
58
|
-
const { mockRun, mockResolveRuntimeConfig } = vi.hoisted(() => {
|
|
58
|
+
const { mockRun, mockResolveRuntimeConfig, mockResolveRuntimeFromPdConfig } = vi.hoisted(() => {
|
|
59
59
|
const mockRun = vi.fn().mockResolvedValue({
|
|
60
60
|
status: 'succeeded',
|
|
61
61
|
taskId: 'diagnosis_test-pain-1',
|
|
@@ -70,7 +70,20 @@ const { mockRun, mockResolveRuntimeConfig } = vi.hoisted(() => {
|
|
|
70
70
|
timeoutMs: 300000,
|
|
71
71
|
agentId: 'main',
|
|
72
72
|
});
|
|
73
|
-
|
|
73
|
+
const mockResolveRuntimeFromPdConfig = vi.fn().mockReturnValue({
|
|
74
|
+
result: {
|
|
75
|
+
runtimeKind: 'pi-ai',
|
|
76
|
+
provider: 'test-provider',
|
|
77
|
+
model: 'test-model',
|
|
78
|
+
apiKeyEnv: 'TEST_KEY',
|
|
79
|
+
timeoutMs: 300000,
|
|
80
|
+
agentId: 'main',
|
|
81
|
+
},
|
|
82
|
+
legacyWarnings: [],
|
|
83
|
+
configSource: '.pd/config.yaml',
|
|
84
|
+
configLoadResult: { ok: true, effective: {}, defaults: {}, legacyFilesDetected: [] },
|
|
85
|
+
});
|
|
86
|
+
return { mockRun, mockResolveRuntimeConfig, mockResolveRuntimeFromPdConfig };
|
|
74
87
|
});
|
|
75
88
|
|
|
76
89
|
vi.mock('../../src/resolve-workspace.js', () => ({
|
|
@@ -128,6 +141,10 @@ vi.mock('../../src/config-reader.js', () => ({
|
|
|
128
141
|
readOutputLanguageFromWorkspace: vi.fn().mockReturnValue({ outputLanguage: 'zh-CN' }),
|
|
129
142
|
}));
|
|
130
143
|
|
|
144
|
+
vi.mock('../../src/services/resolve-runtime-from-pd-config.js', () => ({
|
|
145
|
+
resolveRuntimeFromPdConfig: mockResolveRuntimeFromPdConfig,
|
|
146
|
+
}));
|
|
147
|
+
|
|
131
148
|
import { handlePainRetry } from '../../src/commands/pain-retry.js';
|
|
132
149
|
|
|
133
150
|
// ── Test Data ──────────────────────────────────────────────────────────────────
|
|
@@ -201,6 +218,19 @@ describe('pd pain retry — validation and error paths', () => {
|
|
|
201
218
|
timeoutMs: 300000,
|
|
202
219
|
agentId: 'main',
|
|
203
220
|
});
|
|
221
|
+
mockResolveRuntimeFromPdConfig.mockReturnValue({
|
|
222
|
+
result: {
|
|
223
|
+
runtimeKind: 'pi-ai',
|
|
224
|
+
provider: 'test-provider',
|
|
225
|
+
model: 'test-model',
|
|
226
|
+
apiKeyEnv: 'TEST_KEY',
|
|
227
|
+
timeoutMs: 300000,
|
|
228
|
+
agentId: 'main',
|
|
229
|
+
},
|
|
230
|
+
legacyWarnings: [],
|
|
231
|
+
configSource: '.pd/config.yaml',
|
|
232
|
+
configLoadResult: { ok: true, effective: {}, defaults: {}, legacyFilesDetected: [] },
|
|
233
|
+
});
|
|
204
234
|
mockGetTask.mockResolvedValue(null);
|
|
205
235
|
mockGetCandidatesByTaskId.mockResolvedValue([]);
|
|
206
236
|
mockUpdateCandidateStatus.mockResolvedValue(undefined);
|
|
@@ -342,11 +372,16 @@ describe('pd pain retry — validation and error paths', () => {
|
|
|
342
372
|
|
|
343
373
|
it('RETRY-05a: missing --runtime and no config — refused with reason + nextAction (JSON)', async () => {
|
|
344
374
|
mockGetTask.mockResolvedValue(RETRY_WAIT_TASK);
|
|
345
|
-
//
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
375
|
+
// PRI-393: resolveRuntimeFromPdConfig returns error → no runtime resolved
|
|
376
|
+
mockResolveRuntimeFromPdConfig.mockReturnValueOnce({
|
|
377
|
+
result: {
|
|
378
|
+
reason: 'config_not_found',
|
|
379
|
+
message: 'No .pd/config.yaml found',
|
|
380
|
+
nextAction: 'Create .pd/config.yaml or pass --runtime',
|
|
381
|
+
},
|
|
382
|
+
legacyWarnings: [],
|
|
383
|
+
configSource: '.pd/config.yaml',
|
|
384
|
+
configLoadResult: { ok: false, effective: {}, defaults: {}, legacyFilesDetected: [] },
|
|
350
385
|
});
|
|
351
386
|
|
|
352
387
|
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
@@ -404,14 +439,19 @@ describe('pd pain retry — validation and error paths', () => {
|
|
|
404
439
|
|
|
405
440
|
it('RETRY-05c: blank provider/model/apiKeyEnv — refused with missing_required_config', async () => {
|
|
406
441
|
mockGetTask.mockResolvedValue(RETRY_WAIT_TASK);
|
|
407
|
-
//
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
442
|
+
// PRI-393: resolveRuntimeFromPdConfig returns config with blank strings
|
|
443
|
+
mockResolveRuntimeFromPdConfig.mockReturnValueOnce({
|
|
444
|
+
result: {
|
|
445
|
+
runtimeKind: 'pi-ai',
|
|
446
|
+
provider: '',
|
|
447
|
+
model: ' ',
|
|
448
|
+
apiKeyEnv: '',
|
|
449
|
+
timeoutMs: 300000,
|
|
450
|
+
agentId: 'main',
|
|
451
|
+
},
|
|
452
|
+
legacyWarnings: [],
|
|
453
|
+
configSource: '.pd/config.yaml',
|
|
454
|
+
configLoadResult: { ok: true, effective: {}, defaults: {}, legacyFilesDetected: [] },
|
|
415
455
|
});
|
|
416
456
|
|
|
417
457
|
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
@@ -440,6 +480,81 @@ describe('pd pain retry — validation and error paths', () => {
|
|
|
440
480
|
logSpy.mockRestore();
|
|
441
481
|
exitSpy.mockRestore();
|
|
442
482
|
});
|
|
483
|
+
|
|
484
|
+
it('DPB-09: openclaw-cli flag overrides file config mode (config=gateway, flag=local → runtimeMode=local)', async () => {
|
|
485
|
+
mockGetTask.mockResolvedValue(RETRY_WAIT_TASK);
|
|
486
|
+
mockResolveRuntimeFromPdConfig.mockReturnValueOnce({
|
|
487
|
+
result: {
|
|
488
|
+
runtimeKind: 'openclaw-cli',
|
|
489
|
+
openclawMode: 'gateway',
|
|
490
|
+
timeoutMs: 300000,
|
|
491
|
+
agentId: 'main',
|
|
492
|
+
},
|
|
493
|
+
legacyWarnings: [],
|
|
494
|
+
configSource: '.pd/config.yaml',
|
|
495
|
+
configLoadResult: { ok: true, effective: {}, defaults: {}, legacyFilesDetected: [] },
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
499
|
+
const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
|
|
500
|
+
|
|
501
|
+
await handlePainRetry({
|
|
502
|
+
painId: 'test-pain-1',
|
|
503
|
+
workspace: '/tmp/fake-workspace',
|
|
504
|
+
runtime: 'openclaw-cli',
|
|
505
|
+
openclawLocal: true,
|
|
506
|
+
json: true,
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
// Flag override: config says gateway, flag says local → adapter gets local
|
|
510
|
+
const OpenClawCliMock = vi.mocked(
|
|
511
|
+
await import('@principles/core/runtime-v2').then(m => m.OpenClawCliRuntimeAdapter),
|
|
512
|
+
);
|
|
513
|
+
expect(OpenClawCliMock).toHaveBeenCalledWith(
|
|
514
|
+
expect.objectContaining({ runtimeMode: 'local' }),
|
|
515
|
+
);
|
|
516
|
+
expect(exitSpy).not.toHaveBeenCalledWith(1);
|
|
517
|
+
|
|
518
|
+
logSpy.mockRestore();
|
|
519
|
+
exitSpy.mockRestore();
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
it('DPB-09: openclaw-cli flag overrides file config mode (config=local, flag=gateway → runtimeMode=gateway)', async () => {
|
|
523
|
+
mockGetTask.mockResolvedValue(RETRY_WAIT_TASK);
|
|
524
|
+
mockResolveRuntimeFromPdConfig.mockReturnValueOnce({
|
|
525
|
+
result: {
|
|
526
|
+
runtimeKind: 'openclaw-cli',
|
|
527
|
+
openclawMode: 'local',
|
|
528
|
+
timeoutMs: 300000,
|
|
529
|
+
agentId: 'main',
|
|
530
|
+
},
|
|
531
|
+
legacyWarnings: [],
|
|
532
|
+
configSource: '.pd/config.yaml',
|
|
533
|
+
configLoadResult: { ok: true, effective: {}, defaults: {}, legacyFilesDetected: [] },
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
537
|
+
const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
|
|
538
|
+
|
|
539
|
+
await handlePainRetry({
|
|
540
|
+
painId: 'test-pain-1',
|
|
541
|
+
workspace: '/tmp/fake-workspace',
|
|
542
|
+
runtime: 'openclaw-cli',
|
|
543
|
+
openclawGateway: true,
|
|
544
|
+
json: true,
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
const OpenClawCliMock = vi.mocked(
|
|
548
|
+
await import('@principles/core/runtime-v2').then(m => m.OpenClawCliRuntimeAdapter),
|
|
549
|
+
);
|
|
550
|
+
expect(OpenClawCliMock).toHaveBeenCalledWith(
|
|
551
|
+
expect.objectContaining({ runtimeMode: 'gateway' }),
|
|
552
|
+
);
|
|
553
|
+
expect(exitSpy).not.toHaveBeenCalledWith(1);
|
|
554
|
+
|
|
555
|
+
logSpy.mockRestore();
|
|
556
|
+
exitSpy.mockRestore();
|
|
557
|
+
});
|
|
443
558
|
});
|
|
444
559
|
|
|
445
560
|
describe('pd pain retry — success paths', () => {
|