@principles/pd-cli 1.101.0 → 1.102.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-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/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-run-once.ts +10 -9
- package/src/commands/runtime.ts +96 -24
- 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-run-once.test.ts +59 -53
- package/tests/commands/runtime.test.ts +124 -1
|
@@ -20,13 +20,13 @@ import {
|
|
|
20
20
|
TestDoubleRuntimeAdapter,
|
|
21
21
|
PiAiRuntimeAdapter,
|
|
22
22
|
OpenClawCliRuntimeAdapter,
|
|
23
|
-
resolveRuntimeConfig,
|
|
24
23
|
isRuntimeConfigError,
|
|
25
24
|
validateRuntimeConfig,
|
|
26
25
|
} from '@principles/core/runtime-v2';
|
|
27
26
|
import type { WakeOnceResult, DreamerRunnerResult, PhilosopherRunnerResult, ScribeRunnerResult, ArtificerRunnerResult, EvaluatorRunnerResult, RolloutReviewerRunnerResult, TrainerRunnerResult, PDRuntimeAdapter, PeerRunnerKind, OutputLanguage } from '@principles/core/runtime-v2';
|
|
28
27
|
import { resolveWorkspaceDir } from '../resolve-workspace.js';
|
|
29
28
|
import { readOutputLanguageFromWorkspace } from '../config-reader.js';
|
|
29
|
+
import { resolveRuntimeFromPdConfig } from '../services/resolve-runtime-from-pd-config.js';
|
|
30
30
|
|
|
31
31
|
interface RunOnceOptions {
|
|
32
32
|
workspace?: string;
|
|
@@ -472,19 +472,20 @@ function resolveRuntimeAdapter(opts: ResolveAdapterOptions): PDRuntimeAdapter {
|
|
|
472
472
|
});
|
|
473
473
|
}
|
|
474
474
|
|
|
475
|
-
|
|
476
|
-
const
|
|
475
|
+
// PRI-393: resolve runtime from .pd/config.yaml (not .state/workflows.yaml)
|
|
476
|
+
const resolved = resolveRuntimeFromPdConfig(opts.workspaceDir);
|
|
477
|
+
const configResult = resolved.result;
|
|
477
478
|
|
|
478
479
|
if (isRuntimeConfigError(configResult)) {
|
|
479
480
|
throw new ConfigResolutionError(
|
|
480
|
-
`Config resolution failed: ${configResult.reason}. ` +
|
|
481
|
+
`Config resolution from .pd/config.yaml failed: ${configResult.reason}. ` +
|
|
481
482
|
`${configResult.message}. nextAction: ${configResult.nextAction}`,
|
|
482
483
|
);
|
|
483
484
|
}
|
|
484
485
|
|
|
485
486
|
if (opts.runtimeKind === 'pi-ai' || (opts.runtimeKind === 'config' && configResult.runtimeKind === 'pi-ai')) {
|
|
486
487
|
validateRuntimeConfig(configResult);
|
|
487
|
-
// CLI --timeout-ms overrides
|
|
488
|
+
// CLI --timeout-ms overrides config timeoutMs
|
|
488
489
|
const adapterTimeoutMs = opts.timeoutMs ?? configResult.timeoutMs;
|
|
489
490
|
return new PiAiRuntimeAdapter({
|
|
490
491
|
provider: String(configResult.provider),
|
|
@@ -502,8 +503,8 @@ function resolveRuntimeAdapter(opts: ResolveAdapterOptions): PDRuntimeAdapter {
|
|
|
502
503
|
if (!openclawMode) {
|
|
503
504
|
throw new ConfigResolutionError(
|
|
504
505
|
`runtimeKind 'openclaw-cli' requires openclawMode. ` +
|
|
505
|
-
`Provide --openclaw-local or --openclaw-gateway, or set openclawMode in
|
|
506
|
-
`nextAction: Add openclawMode: local|gateway to your
|
|
506
|
+
`Provide --openclaw-local or --openclaw-gateway, or set openclawMode in .pd/config.yaml. ` +
|
|
507
|
+
`nextAction: Add openclawMode: local|gateway to your .pd/config.yaml runtime profile or use CLI flags.`,
|
|
507
508
|
);
|
|
508
509
|
}
|
|
509
510
|
return new OpenClawCliRuntimeAdapter({
|
|
@@ -533,7 +534,7 @@ export async function handleRuntimeInternalizationRunOnce(opts: RunOnceOptions):
|
|
|
533
534
|
if (runtimeKind === 'test-double' && !opts.allowTestDouble) {
|
|
534
535
|
console.error('Error: test-double runtime mutates real queue state (leases tasks, marks them succeeded with empty output).');
|
|
535
536
|
console.error('Use --runtime test-double --allow-test-double to acknowledge this risk.');
|
|
536
|
-
console.error('For production use, use --runtime config (reads from
|
|
537
|
+
console.error('For production use, use --runtime config (reads from .pd/config.yaml) or --runtime pi-ai / openclaw-cli.');
|
|
537
538
|
process.exitCode = 1;
|
|
538
539
|
return;
|
|
539
540
|
}
|
|
@@ -716,7 +717,7 @@ export async function handleRuntimeInternalizationRunOnce(opts: RunOnceOptions):
|
|
|
716
717
|
decision: isConfigError ? 'config_error' : 'runtime_error',
|
|
717
718
|
reason: message,
|
|
718
719
|
nextAction: isConfigError
|
|
719
|
-
? 'Fix the
|
|
720
|
+
? 'Fix the .pd/config.yaml runtime profile, or use --runtime pi-ai / openclaw-cli with explicit flags'
|
|
720
721
|
: 'Check runner logs and workspace state; re-run with --runtime test-double to isolate',
|
|
721
722
|
}, null, 2));
|
|
722
723
|
} else {
|
package/src/commands/runtime.ts
CHANGED
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import * as path from 'path';
|
|
11
11
|
import { probeRuntime } from '@principles/core/runtime-v2';
|
|
12
|
-
import { PDRuntimeError } from '@principles/core/runtime-v2';
|
|
12
|
+
import { PDRuntimeError, isRuntimeConfigError } from '@principles/core/runtime-v2';
|
|
13
|
+
import { resolveRuntimeFromPdConfig, resolveRuntimeWithOverrides } from '../services/resolve-runtime-from-pd-config.js';
|
|
13
14
|
|
|
14
15
|
interface RuntimeProbeOptions {
|
|
15
16
|
runtime: string;
|
|
@@ -139,48 +140,55 @@ async function handlePiAiProbe(opts: RuntimeProbeOptions): Promise<void> {
|
|
|
139
140
|
let model = opts.model ?? '';
|
|
140
141
|
let apiKeyEnv = opts.apiKeyEnv ?? '';
|
|
141
142
|
let baseUrl = opts.baseUrl ?? '';
|
|
142
|
-
let {timeoutMs} = opts;
|
|
143
|
+
let { timeoutMs, maxRetries } = opts;
|
|
143
144
|
|
|
144
|
-
//
|
|
145
|
+
// PRI-393: always load workspace policy from .pd/config.yaml (not .state/workflows.yaml)
|
|
145
146
|
if (workspaceDir) {
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
147
|
+
const resolved = resolveRuntimeWithOverrides(workspaceDir, {
|
|
148
|
+
provider: opts.provider,
|
|
149
|
+
model: opts.model,
|
|
150
|
+
apiKeyEnv: opts.apiKeyEnv,
|
|
151
|
+
baseUrl: opts.baseUrl,
|
|
152
|
+
maxRetries: opts.maxRetries,
|
|
153
|
+
timeoutMs: opts.timeoutMs,
|
|
154
|
+
});
|
|
155
|
+
for (const w of resolved.legacyWarnings) console.warn(`Warning: ${w}`);
|
|
156
|
+
if (resolved.mergedConfig) {
|
|
157
|
+
provider = provider || resolved.mergedConfig.provider || '';
|
|
158
|
+
model = model || resolved.mergedConfig.model || '';
|
|
159
|
+
apiKeyEnv = apiKeyEnv || resolved.mergedConfig.apiKeyEnv || '';
|
|
160
|
+
baseUrl = baseUrl || resolved.mergedConfig.baseUrl || '';
|
|
161
|
+
timeoutMs = timeoutMs ?? resolved.mergedConfig.timeoutMs;
|
|
162
|
+
maxRetries = maxRetries ?? resolved.mergedConfig.maxRetries;
|
|
163
|
+
} else if (isRuntimeConfigError(resolved.result)) {
|
|
164
|
+
console.warn(`Warning: could not resolve runtime from .pd/config.yaml — ${resolved.result.message}`);
|
|
161
165
|
}
|
|
162
166
|
}
|
|
163
167
|
|
|
164
168
|
if (!provider) {
|
|
165
|
-
console.error("error: --provider is required for --runtime pi-ai (or set in
|
|
169
|
+
console.error("error: --provider is required for --runtime pi-ai (or set in .pd/config.yaml)");
|
|
166
170
|
console.error(" e.g.: pd runtime probe --runtime pi-ai --provider openrouter --model anthropic/claude-sonnet-4 --apiKeyEnv OPENROUTER_API_KEY");
|
|
167
171
|
process.exit(1);
|
|
172
|
+
return;
|
|
168
173
|
}
|
|
169
174
|
if (!model) {
|
|
170
|
-
console.error("error: --model is required for --runtime pi-ai (or set in
|
|
175
|
+
console.error("error: --model is required for --runtime pi-ai (or set in .pd/config.yaml)");
|
|
171
176
|
console.error(" e.g.: pd runtime probe --runtime pi-ai --provider openrouter --model anthropic/claude-sonnet-4 --apiKeyEnv OPENROUTER_API_KEY");
|
|
172
177
|
process.exit(1);
|
|
178
|
+
return;
|
|
173
179
|
}
|
|
174
180
|
if (!apiKeyEnv) {
|
|
175
|
-
console.error("error: --apiKeyEnv is required for --runtime pi-ai (or set in
|
|
181
|
+
console.error("error: --apiKeyEnv is required for --runtime pi-ai (or set in .pd/config.yaml)");
|
|
176
182
|
console.error(" e.g.: pd runtime probe --runtime pi-ai --provider openrouter --model anthropic/claude-sonnet-4 --apiKeyEnv OPENROUTER_API_KEY");
|
|
177
183
|
process.exit(1);
|
|
184
|
+
return;
|
|
178
185
|
}
|
|
179
186
|
|
|
180
187
|
// D-09: check env var exists before calling probeRuntime
|
|
181
188
|
if (!process.env[apiKeyEnv]) {
|
|
182
189
|
console.error(`error: environment variable '${apiKeyEnv}' is not set`);
|
|
183
190
|
process.exit(1);
|
|
191
|
+
return;
|
|
184
192
|
}
|
|
185
193
|
|
|
186
194
|
try {
|
|
@@ -190,7 +198,7 @@ async function handlePiAiProbe(opts: RuntimeProbeOptions): Promise<void> {
|
|
|
190
198
|
model,
|
|
191
199
|
apiKeyEnv,
|
|
192
200
|
baseUrl,
|
|
193
|
-
maxRetries:
|
|
201
|
+
maxRetries: maxRetries,
|
|
194
202
|
timeoutMs: timeoutMs ?? 120_000, // D-04: probe timeout 120s (matches Runtime defaults)
|
|
195
203
|
});
|
|
196
204
|
|
|
@@ -265,7 +273,66 @@ async function handlePiAiProbe(opts: RuntimeProbeOptions): Promise<void> {
|
|
|
265
273
|
}
|
|
266
274
|
|
|
267
275
|
/**
|
|
268
|
-
*
|
|
276
|
+
* --runtime config probe branch — PRI-393
|
|
277
|
+
* Resolves runtime from .pd/config.yaml, then dispatches to pi-ai or openclaw-cli probe.
|
|
278
|
+
*/
|
|
279
|
+
async function handleConfigProbe(opts: RuntimeProbeOptions): Promise<void> {
|
|
280
|
+
const workspaceDir = opts.workspace ? path.resolve(opts.workspace) : undefined;
|
|
281
|
+
if (!workspaceDir) {
|
|
282
|
+
console.error('error: --workspace is required for --runtime config');
|
|
283
|
+
process.exit(1);
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const resolved = resolveRuntimeFromPdConfig(workspaceDir);
|
|
288
|
+
for (const w of resolved.legacyWarnings) console.warn(`Warning: ${w}`);
|
|
289
|
+
|
|
290
|
+
if (isRuntimeConfigError(resolved.result)) {
|
|
291
|
+
if (opts.json) {
|
|
292
|
+
console.log(JSON.stringify({
|
|
293
|
+
status: 'failed',
|
|
294
|
+
errorCategory: 'config_error',
|
|
295
|
+
message: resolved.result.message,
|
|
296
|
+
reason: resolved.result.reason,
|
|
297
|
+
nextAction: resolved.result.nextAction,
|
|
298
|
+
configSource: resolved.configSource,
|
|
299
|
+
}, null, 2));
|
|
300
|
+
} else {
|
|
301
|
+
console.error(`error: ${resolved.result.message}`);
|
|
302
|
+
console.error(`nextAction: ${resolved.result.nextAction}`);
|
|
303
|
+
}
|
|
304
|
+
process.exit(1);
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const config = resolved.result;
|
|
309
|
+
// Dispatch to the appropriate runtime probe
|
|
310
|
+
if (config.runtimeKind === 'pi-ai') {
|
|
311
|
+
return handlePiAiProbe({
|
|
312
|
+
...opts,
|
|
313
|
+
provider: opts.provider ?? config.provider,
|
|
314
|
+
model: opts.model ?? config.model,
|
|
315
|
+
apiKeyEnv: opts.apiKeyEnv ?? config.apiKeyEnv,
|
|
316
|
+
baseUrl: opts.baseUrl ?? config.baseUrl,
|
|
317
|
+
timeoutMs: opts.timeoutMs ?? config.timeoutMs,
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (config.runtimeKind === 'openclaw-cli') {
|
|
322
|
+
return handleOpenClawProbe({
|
|
323
|
+
...opts,
|
|
324
|
+
openclawLocal: config.openclawMode === 'local' ? true : opts.openclawLocal,
|
|
325
|
+
openclawGateway: config.openclawMode === 'gateway' ? true : opts.openclawGateway,
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
console.error(`error: unsupported runtimeKind '${config.runtimeKind}' from .pd/config.yaml`);
|
|
330
|
+
process.exit(1);
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* pd runtime probe — dispatches to openclaw-cli, pi-ai, or config branch.
|
|
269
336
|
*/
|
|
270
337
|
export async function handleRuntimeProbe(opts: RuntimeProbeOptions): Promise<void> {
|
|
271
338
|
if (opts.runtime === 'openclaw-cli') {
|
|
@@ -276,6 +343,11 @@ export async function handleRuntimeProbe(opts: RuntimeProbeOptions): Promise<voi
|
|
|
276
343
|
return handlePiAiProbe(opts);
|
|
277
344
|
}
|
|
278
345
|
|
|
279
|
-
|
|
346
|
+
if (opts.runtime === 'config') {
|
|
347
|
+
return handleConfigProbe(opts);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
console.error(`error: unsupported --runtime '${opts.runtime}' (supported: openclaw-cli, pi-ai, config)`);
|
|
280
351
|
process.exit(1);
|
|
352
|
+
return;
|
|
281
353
|
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified runtime config resolver from .pd/config.yaml — PRI-393
|
|
3
|
+
*
|
|
4
|
+
* All MVP mainline execution paths (probe, run-once, diagnose, pain-retry)
|
|
5
|
+
* MUST use this helper instead of the legacy resolveRuntimeConfig(stateDir)
|
|
6
|
+
* which reads .state/workflows.yaml.
|
|
7
|
+
*
|
|
8
|
+
* Canonical source: .pd/config.yaml via loadPdConfig → resolveRuntimeConfigFromPdConfig.
|
|
9
|
+
* .state/workflows.yaml is reported as a legacy warning only.
|
|
10
|
+
*
|
|
11
|
+
* ERR refs:
|
|
12
|
+
* - ERR-002: fail loud with reason + nextAction
|
|
13
|
+
* - EP-07: runtime state source alignment
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import {
|
|
17
|
+
resolveRuntimeConfigFromPdConfig,
|
|
18
|
+
isRuntimeConfigError,
|
|
19
|
+
} from '@principles/core/runtime-v2';
|
|
20
|
+
import type {
|
|
21
|
+
RuntimeConfigResult,
|
|
22
|
+
RuntimeConfig,
|
|
23
|
+
RuntimeConfigError,
|
|
24
|
+
} from '@principles/core/runtime-v2';
|
|
25
|
+
import { loadPdConfig } from './pd-config-loader.js';
|
|
26
|
+
import type { PdConfigLoadResult } from './pd-config-loader.js';
|
|
27
|
+
|
|
28
|
+
export interface ResolvedRuntimeFromPdConfig {
|
|
29
|
+
/** The resolved runtime config (or error). */
|
|
30
|
+
result: RuntimeConfigResult;
|
|
31
|
+
/** Legacy files detected (.state/workflows.yaml etc.) — informational only. */
|
|
32
|
+
legacyWarnings: string[];
|
|
33
|
+
/** The PD config load result for downstream use (feature flags, etc.). */
|
|
34
|
+
configLoadResult: PdConfigLoadResult;
|
|
35
|
+
/** Canonical config source label, e.g. ".pd/config.yaml". */
|
|
36
|
+
configSource: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Resolve runtime configuration exclusively from .pd/config.yaml.
|
|
41
|
+
*
|
|
42
|
+
* This is the ONLY production entry point for runtime config resolution
|
|
43
|
+
* in pd-cli commands. Legacy resolveRuntimeConfig(stateDir) must NOT be
|
|
44
|
+
* called by probe/run-once/diagnose/pain-retry.
|
|
45
|
+
*
|
|
46
|
+
* @param workspaceDir - The resolved workspace directory.
|
|
47
|
+
* @param getEnvVar - Env var accessor, defaults to process.env.
|
|
48
|
+
* @returns Resolved runtime config with legacy warnings.
|
|
49
|
+
*/
|
|
50
|
+
export function resolveRuntimeFromPdConfig(
|
|
51
|
+
workspaceDir: string,
|
|
52
|
+
getEnvVar: (name: string) => string | undefined = (name) => process.env[name],
|
|
53
|
+
): ResolvedRuntimeFromPdConfig {
|
|
54
|
+
const configLoadResult = loadPdConfig(workspaceDir);
|
|
55
|
+
|
|
56
|
+
// Malformed config → fail loud. Do NOT fall back to defaults for execution.
|
|
57
|
+
// Missing config is ok (loadPdConfig returns ok:true with defaults), but
|
|
58
|
+
// ok:false always means the file exists and is broken.
|
|
59
|
+
if (!configLoadResult.ok) {
|
|
60
|
+
const [firstError] = configLoadResult.errors;
|
|
61
|
+
const result: RuntimeConfigError = {
|
|
62
|
+
ok: false,
|
|
63
|
+
reason: `config_malformed:${firstError?.reason ?? 'unknown'}`,
|
|
64
|
+
message: firstError?.reason ?? '.pd/config.yaml is malformed',
|
|
65
|
+
nextAction: firstError?.nextAction ?? 'Fix .pd/config.yaml syntax and retry',
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const legacyWarnings = configLoadResult.legacyFilesDetected.length > 0
|
|
69
|
+
? [
|
|
70
|
+
`Legacy config files detected: ${configLoadResult.legacyFilesDetected.join(', ')}. ` +
|
|
71
|
+
`These are NOT used for runtime resolution. PD uses .pd/config.yaml exclusively.`,
|
|
72
|
+
]
|
|
73
|
+
: [];
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
result,
|
|
77
|
+
legacyWarnings,
|
|
78
|
+
configLoadResult,
|
|
79
|
+
configSource: '.pd/config.yaml',
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const result = resolveRuntimeConfigFromPdConfig(configLoadResult.effective, getEnvVar);
|
|
84
|
+
|
|
85
|
+
const legacyWarnings = configLoadResult.legacyFilesDetected.length > 0
|
|
86
|
+
? [
|
|
87
|
+
`Legacy config files detected: ${configLoadResult.legacyFilesDetected.join(', ')}. ` +
|
|
88
|
+
`These are NOT used for runtime resolution. PD uses .pd/config.yaml exclusively.`,
|
|
89
|
+
]
|
|
90
|
+
: [];
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
result,
|
|
94
|
+
legacyWarnings,
|
|
95
|
+
configLoadResult,
|
|
96
|
+
configSource: '.pd/config.yaml',
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Resolve runtime config from .pd/config.yaml, then merge with CLI flag overrides.
|
|
103
|
+
*
|
|
104
|
+
* CLI flags take priority over config values (same semantics as before PRI-393,
|
|
105
|
+
* but reading from .pd/config.yaml instead of .state/workflows.yaml).
|
|
106
|
+
*
|
|
107
|
+
* @param workspaceDir - The resolved workspace directory.
|
|
108
|
+
* @param overrides - CLI flag overrides (provider, model, apiKeyEnv, etc.).
|
|
109
|
+
* @param getEnvVar - Env var accessor.
|
|
110
|
+
*/
|
|
111
|
+
export function resolveRuntimeWithOverrides(
|
|
112
|
+
workspaceDir: string,
|
|
113
|
+
overrides: {
|
|
114
|
+
provider?: string;
|
|
115
|
+
model?: string;
|
|
116
|
+
apiKeyEnv?: string;
|
|
117
|
+
baseUrl?: string;
|
|
118
|
+
maxRetries?: number;
|
|
119
|
+
timeoutMs?: number;
|
|
120
|
+
},
|
|
121
|
+
getEnvVar: (name: string) => string | undefined = (name) => process.env[name],
|
|
122
|
+
): ResolvedRuntimeFromPdConfig & { mergedConfig: RuntimeConfig | null } {
|
|
123
|
+
const base = resolveRuntimeFromPdConfig(workspaceDir, getEnvVar);
|
|
124
|
+
|
|
125
|
+
if (isRuntimeConfigError(base.result)) {
|
|
126
|
+
return { ...base, mergedConfig: null };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const config = base.result;
|
|
130
|
+
// CLI flags override config values
|
|
131
|
+
const merged: RuntimeConfig = {
|
|
132
|
+
...config,
|
|
133
|
+
provider: overrides.provider || config.provider,
|
|
134
|
+
model: overrides.model || config.model,
|
|
135
|
+
apiKeyEnv: overrides.apiKeyEnv || config.apiKeyEnv,
|
|
136
|
+
baseUrl: overrides.baseUrl || config.baseUrl,
|
|
137
|
+
maxRetries: overrides.maxRetries ?? config.maxRetries,
|
|
138
|
+
timeoutMs: overrides.timeoutMs ?? config.timeoutMs,
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
return { ...base, mergedConfig: merged };
|
|
142
|
+
}
|
|
@@ -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);
|