@principles/pd-cli 1.132.0 → 1.134.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/__tests__/intent-flag-wiring.test.js +74 -3
- package/dist/commands/__tests__/intent-flag-wiring.test.js.map +1 -1
- package/dist/commands/__tests__/pain-retry-maxTokens-flag.test.d.ts +2 -0
- package/dist/commands/__tests__/pain-retry-maxTokens-flag.test.d.ts.map +1 -0
- package/dist/commands/__tests__/pain-retry-maxTokens-flag.test.js +60 -0
- package/dist/commands/__tests__/pain-retry-maxTokens-flag.test.js.map +1 -0
- package/dist/commands/__tests__/rulecode-flag-wiring.test.js +11 -4
- package/dist/commands/__tests__/rulecode-flag-wiring.test.js.map +1 -1
- package/dist/commands/__tests__/run-rulehost-flag-wiring.test.js +9 -0
- package/dist/commands/__tests__/run-rulehost-flag-wiring.test.js.map +1 -1
- package/dist/commands/admission-gate.d.ts +24 -0
- package/dist/commands/admission-gate.d.ts.map +1 -0
- package/dist/commands/admission-gate.js +29 -0
- package/dist/commands/admission-gate.js.map +1 -0
- package/dist/commands/candidate.d.ts.map +1 -1
- package/dist/commands/candidate.js +72 -6
- package/dist/commands/candidate.js.map +1 -1
- package/dist/commands/console.d.ts.map +1 -1
- package/dist/commands/console.js +40 -0
- package/dist/commands/console.js.map +1 -1
- package/dist/commands/demo-story-a.d.ts.map +1 -1
- package/dist/commands/demo-story-a.js +4 -2
- package/dist/commands/demo-story-a.js.map +1 -1
- package/dist/commands/diagnose.d.ts.map +1 -1
- package/dist/commands/diagnose.js +155 -8
- package/dist/commands/diagnose.js.map +1 -1
- package/dist/commands/errors-list.d.ts +25 -0
- package/dist/commands/errors-list.d.ts.map +1 -0
- package/dist/commands/errors-list.js +368 -0
- package/dist/commands/errors-list.js.map +1 -0
- package/dist/commands/health-codex.d.ts +7 -0
- package/dist/commands/health-codex.d.ts.map +1 -0
- package/dist/commands/health-codex.js +209 -0
- package/dist/commands/health-codex.js.map +1 -0
- package/dist/commands/health.d.ts +24 -0
- package/dist/commands/health.d.ts.map +1 -1
- package/dist/commands/health.js +48 -0
- package/dist/commands/health.js.map +1 -1
- package/dist/commands/intent.d.ts +21 -4
- package/dist/commands/intent.d.ts.map +1 -1
- package/dist/commands/intent.js +88 -27
- package/dist/commands/intent.js.map +1 -1
- package/dist/commands/pain-evidence.d.ts +6 -0
- package/dist/commands/pain-evidence.d.ts.map +1 -1
- package/dist/commands/pain-evidence.js +6 -0
- package/dist/commands/pain-evidence.js.map +1 -1
- package/dist/commands/pain-record.d.ts.map +1 -1
- package/dist/commands/pain-record.js +21 -0
- package/dist/commands/pain-record.js.map +1 -1
- package/dist/commands/pain-retry.d.ts +4 -0
- package/dist/commands/pain-retry.d.ts.map +1 -1
- package/dist/commands/pain-retry.js +327 -32
- package/dist/commands/pain-retry.js.map +1 -1
- package/dist/commands/proven-channel-baseline.d.ts.map +1 -1
- package/dist/commands/proven-channel-baseline.js +4 -2
- package/dist/commands/proven-channel-baseline.js.map +1 -1
- package/dist/commands/runtime-activation.d.ts +30 -0
- package/dist/commands/runtime-activation.d.ts.map +1 -1
- package/dist/commands/runtime-activation.js +567 -11
- package/dist/commands/runtime-activation.js.map +1 -1
- package/dist/commands/runtime-canary.d.ts.map +1 -1
- package/dist/commands/runtime-canary.js +32 -7
- package/dist/commands/runtime-canary.js.map +1 -1
- package/dist/commands/runtime-health-snapshot.d.ts.map +1 -1
- package/dist/commands/runtime-health-snapshot.js +4 -2
- package/dist/commands/runtime-health-snapshot.js.map +1 -1
- package/dist/commands/runtime-init.d.ts +43 -0
- package/dist/commands/runtime-init.d.ts.map +1 -0
- package/dist/commands/runtime-init.js +264 -0
- package/dist/commands/runtime-init.js.map +1 -0
- package/dist/commands/runtime-internalization-context-trace.d.ts +25 -0
- package/dist/commands/runtime-internalization-context-trace.d.ts.map +1 -0
- package/dist/commands/runtime-internalization-context-trace.js +374 -0
- package/dist/commands/runtime-internalization-context-trace.js.map +1 -0
- package/dist/commands/runtime-internalization-enqueue-successors.d.ts.map +1 -1
- package/dist/commands/runtime-internalization-enqueue-successors.js +3 -1
- package/dist/commands/runtime-internalization-enqueue-successors.js.map +1 -1
- package/dist/commands/runtime-internalization-integrity.d.ts.map +1 -1
- package/dist/commands/runtime-internalization-integrity.js +4 -2
- 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 +20 -5
- package/dist/commands/runtime-internalization-run-once.js.map +1 -1
- package/dist/commands/runtime-internalization-run-rulehost.d.ts +1 -0
- package/dist/commands/runtime-internalization-run-rulehost.d.ts.map +1 -1
- package/dist/commands/runtime-internalization-run-rulehost.js +149 -6
- package/dist/commands/runtime-internalization-run-rulehost.js.map +1 -1
- package/dist/commands/runtime-uat.d.ts.map +1 -1
- package/dist/commands/runtime-uat.guard.test.js +13 -15
- package/dist/commands/runtime-uat.guard.test.js.map +1 -1
- package/dist/commands/runtime-uat.js +4 -3
- package/dist/commands/runtime-uat.js.map +1 -1
- package/dist/config-reader.js +1 -1
- package/dist/config-reader.js.map +1 -1
- package/dist/index.js +54 -78
- package/dist/index.js.map +1 -1
- package/dist/services/__tests__/evaluator-runner-deps.test.d.ts +2 -0
- package/dist/services/__tests__/evaluator-runner-deps.test.d.ts.map +1 -0
- package/dist/services/__tests__/evaluator-runner-deps.test.js +304 -0
- package/dist/services/__tests__/evaluator-runner-deps.test.js.map +1 -0
- package/dist/services/__tests__/rulehost-readiness.test.js.map +1 -1
- package/dist/services/__tests__/runtime-adapter-resolver.test.js.map +1 -1
- package/dist/services/config-doctor.d.ts.map +1 -1
- package/dist/services/config-doctor.js +9 -2
- package/dist/services/config-doctor.js.map +1 -1
- package/dist/services/pain-flood-simulation-runner.d.ts.map +1 -1
- package/dist/services/pain-flood-simulation-runner.js +9 -0
- package/dist/services/pain-flood-simulation-runner.js.map +1 -1
- package/dist/services/pd-config-loader.js +2 -2
- package/dist/services/pd-config-loader.js.map +1 -1
- package/dist/services/proven-channel-baseline-runner.d.ts.map +1 -1
- package/dist/services/proven-channel-baseline-runner.js +10 -2
- package/dist/services/proven-channel-baseline-runner.js.map +1 -1
- package/dist/services/quality-scorecard/data-extractor.js.map +1 -1
- package/dist/services/resolve-runtime-from-pd-config.js.map +1 -1
- package/dist/services/rulehost-pipeline-runner.d.ts +77 -1
- package/dist/services/rulehost-pipeline-runner.d.ts.map +1 -1
- package/dist/services/rulehost-pipeline-runner.js +111 -7
- package/dist/services/rulehost-pipeline-runner.js.map +1 -1
- package/dist/services/rulehost-readiness.js.map +1 -1
- package/dist/services/runtime-adapter-resolver.d.ts +1 -0
- package/dist/services/runtime-adapter-resolver.d.ts.map +1 -1
- package/dist/services/runtime-adapter-resolver.js +3 -0
- package/dist/services/runtime-adapter-resolver.js.map +1 -1
- package/dist/services/synthetic-baseline-runner.d.ts.map +1 -1
- package/dist/services/synthetic-baseline-runner.js +11 -0
- package/dist/services/synthetic-baseline-runner.js.map +1 -1
- package/dist/utils/production-workspace-guard.d.ts +4 -4
- package/dist/utils/production-workspace-guard.d.ts.map +1 -1
- package/dist/utils/production-workspace-guard.js +19 -16
- package/dist/utils/production-workspace-guard.js.map +1 -1
- package/dist/utils/production-workspace-guard.test.js +54 -19
- package/dist/utils/production-workspace-guard.test.js.map +1 -1
- package/package.json +8 -5
- package/scripts/migrate-illegal-expected-decision.ts +252 -0
- package/src/commands/__tests__/intent-flag-wiring.test.ts +88 -3
- package/src/commands/__tests__/pain-retry-maxTokens-flag.test.ts +73 -0
- package/src/commands/__tests__/rulecode-flag-wiring.test.ts +31 -24
- package/src/commands/__tests__/run-rulehost-flag-wiring.test.ts +12 -0
- package/src/commands/admission-gate.ts +30 -0
- package/src/commands/candidate.ts +74 -6
- package/src/commands/console.ts +40 -0
- package/src/commands/demo-story-a.ts +4 -2
- package/src/commands/diagnose.ts +158 -10
- package/src/commands/errors-list.ts +440 -0
- package/src/commands/health-codex.ts +249 -0
- package/src/commands/health.ts +60 -0
- package/src/commands/intent.ts +98 -29
- package/src/commands/pain-evidence.ts +6 -0
- package/src/commands/pain-record.ts +21 -0
- package/src/commands/pain-retry.ts +353 -35
- package/src/commands/proven-channel-baseline.ts +4 -2
- package/src/commands/runtime-activation.ts +657 -12
- package/src/commands/runtime-canary.ts +31 -5
- package/src/commands/runtime-health-snapshot.ts +4 -2
- package/src/commands/runtime-init.ts +305 -0
- package/src/commands/runtime-internalization-context-trace.ts +445 -0
- package/src/commands/runtime-internalization-enqueue-successors.ts +3 -1
- package/src/commands/runtime-internalization-integrity.ts +4 -2
- package/src/commands/runtime-internalization-run-once.ts +20 -5
- package/src/commands/runtime-internalization-run-rulehost.ts +159 -6
- package/src/commands/runtime-uat.guard.test.ts +14 -16
- package/src/commands/runtime-uat.ts +4 -3
- package/src/config-reader.ts +1 -1
- package/src/index.ts +69 -78
- package/src/services/__tests__/evaluator-runner-deps.test.ts +341 -0
- package/src/services/config-doctor.ts +9 -2
- package/src/services/pain-flood-simulation-runner.ts +9 -0
- package/src/services/pd-config-loader.ts +2 -2
- package/src/services/proven-channel-baseline-runner.ts +10 -3
- package/src/services/rulehost-pipeline-runner.ts +157 -6
- package/src/services/runtime-adapter-resolver.ts +4 -0
- package/src/services/synthetic-baseline-runner.ts +11 -0
- package/src/utils/production-workspace-guard.test.ts +62 -20
- package/src/utils/production-workspace-guard.ts +19 -16
- package/tests/bdd/cli-contract.steps.ts +343 -0
- package/tests/bdd/context-trace.steps.ts +229 -0
- package/tests/bdd/support/gherkin-loader.ts +147 -0
- package/tests/bdd/support/repo-root.ts +62 -0
- package/tests/bdd/support/vitest-bdd.ts +179 -0
- package/tests/commands/admission-gate-coverage.test.ts +79 -0
- package/tests/commands/candidate-audit-repair.test.ts +77 -0
- package/tests/commands/candidate-intake.test.ts +155 -10
- package/tests/commands/candidate-internalization-backfill.test.ts +30 -0
- package/tests/commands/cli-help-snapshot.test.ts +15 -1
- package/tests/commands/console-open.test.ts +19 -0
- package/tests/commands/diagnose.test.ts +619 -14
- package/tests/commands/errors-list.test.ts +678 -0
- package/tests/commands/health-codex.test.ts +243 -0
- package/tests/commands/health-host-wiring.test.ts +76 -0
- package/tests/commands/intent.test.ts +4 -4
- package/tests/commands/pain-record.test.ts +80 -0
- package/tests/commands/pain-retry.test.ts +94 -3
- package/tests/commands/run-rulehost-handler.test.ts +133 -0
- package/tests/commands/runtime-activation-approve-flag-wiring.test.ts +227 -0
- package/tests/commands/runtime-activation-deactivate-flag-wiring.test.ts +167 -0
- package/tests/commands/runtime-activation-dispatch-flag-wiring.test.ts +262 -0
- package/tests/commands/runtime-activation-list-flag-wiring.test.ts +193 -0
- package/tests/commands/runtime-activation-promote-flag-wiring.test.ts +222 -0
- package/tests/commands/runtime-activation.test.ts +1090 -5
- package/tests/commands/runtime-canary.test.ts +50 -0
- package/tests/commands/runtime-features.test.ts +2 -0
- package/tests/commands/runtime-init-empty-workspace.test.ts +234 -0
- package/tests/commands/runtime-init.test.ts +346 -0
- package/tests/commands/runtime-internalization-context-trace.test.ts +110 -0
- package/tests/commands/runtime-internalization.test.ts +2 -2
- package/tests/config-reader.test.ts +6 -5
- package/tests/e2e/cross-package-acceptance.test.ts +55 -13
- package/tests/services/mainline-snapshot-assembler.test.ts +6 -3
- package/tests/services/resolve-runtime-from-pd-config.test.ts +2 -0
- package/tests/services/rulehost-pipeline-runner.test.ts +1 -1
- package/vitest.config.ts +1 -1
|
@@ -103,6 +103,7 @@ export interface ResolveAdapterOptions {
|
|
|
103
103
|
apiKeyEnv?: string;
|
|
104
104
|
baseUrl?: string;
|
|
105
105
|
maxRetries?: number;
|
|
106
|
+
maxTokens?: number;
|
|
106
107
|
};
|
|
107
108
|
/**
|
|
108
109
|
* CLI override for openclaw mode (from --openclaw-local / --openclaw-gateway flags).
|
|
@@ -215,6 +216,7 @@ export function resolveRuntimeAdapterFromConfig(opts: ResolveAdapterOptions): PD
|
|
|
215
216
|
const apiKeyEnv = opts.piAiOverrides?.apiKeyEnv ?? configFields.apiKeyEnv;
|
|
216
217
|
const baseUrl = opts.piAiOverrides?.baseUrl ?? configFields.baseUrl;
|
|
217
218
|
const maxRetries = opts.piAiOverrides?.maxRetries ?? configFields.maxRetries;
|
|
219
|
+
const maxTokens = opts.piAiOverrides?.maxTokens ?? configFields.maxTokens;
|
|
218
220
|
const adapterTimeoutMs = opts.timeoutMs ?? configFields.timeoutMs;
|
|
219
221
|
|
|
220
222
|
// PRI-431 Step 1d + PR review fix: manual missing-field check on MERGED values
|
|
@@ -283,9 +285,11 @@ export function resolveRuntimeAdapterFromConfig(opts: ResolveAdapterOptions): PD
|
|
|
283
285
|
model: String(model),
|
|
284
286
|
apiKeyEnv: String(apiKeyEnv),
|
|
285
287
|
maxRetries,
|
|
288
|
+
maxTokens,
|
|
286
289
|
timeoutMs: adapterTimeoutMs,
|
|
287
290
|
baseUrl,
|
|
288
291
|
workspace: opts.workspaceDir,
|
|
292
|
+
...(configFields.systemPrompt ? { systemPrompt: configFields.systemPrompt } : {}),
|
|
289
293
|
});
|
|
290
294
|
}
|
|
291
295
|
|
|
@@ -21,6 +21,7 @@ import { auditCandidateLedgerConsistency } from '@principles/core/runtime-v2';
|
|
|
21
21
|
import { OperatorHealthReadModel } from '@principles/core/runtime-v2';
|
|
22
22
|
import { createInternalizationQueueReadModel } from '@principles/core/runtime-v2';
|
|
23
23
|
import { createPITaskDiagnosticJson } from '@principles/core/runtime-v2';
|
|
24
|
+
import { contentHashFn } from './rulehost-pipeline-runner.js';
|
|
24
25
|
import {
|
|
25
26
|
computeOverallStatus,
|
|
26
27
|
boundedEvidence,
|
|
@@ -148,6 +149,7 @@ export async function runSyntheticBaseline(opts: SyntheticBaselineRunnerOptions)
|
|
|
148
149
|
artifactStore: stateManager.piArtifactStore,
|
|
149
150
|
validator: new DefaultDiagRootCauseValidator(),
|
|
150
151
|
contextAssembler,
|
|
152
|
+
contentHashFn,
|
|
151
153
|
},
|
|
152
154
|
{
|
|
153
155
|
owner: 'synthetic-baseline',
|
|
@@ -164,6 +166,7 @@ export async function runSyntheticBaseline(opts: SyntheticBaselineRunnerOptions)
|
|
|
164
166
|
eventEmitter,
|
|
165
167
|
artifactStore: stateManager.piArtifactStore,
|
|
166
168
|
validator: new DefaultDiagDistillerValidator(),
|
|
169
|
+
contentHashFn,
|
|
167
170
|
},
|
|
168
171
|
{
|
|
169
172
|
owner: 'synthetic-baseline',
|
|
@@ -180,6 +183,7 @@ export async function runSyntheticBaseline(opts: SyntheticBaselineRunnerOptions)
|
|
|
180
183
|
eventEmitter,
|
|
181
184
|
artifactStore: stateManager.piArtifactStore,
|
|
182
185
|
committer,
|
|
186
|
+
contentHashFn,
|
|
183
187
|
},
|
|
184
188
|
{
|
|
185
189
|
owner: 'synthetic-baseline',
|
|
@@ -214,6 +218,13 @@ export async function runSyntheticBaseline(opts: SyntheticBaselineRunnerOptions)
|
|
|
214
218
|
painType: 'tool_failure',
|
|
215
219
|
source: 'synthetic-baseline',
|
|
216
220
|
reason: 'Synthetic baseline deterministic pain signal',
|
|
221
|
+
// Synthetic baseline is a deterministic smoke tool — it has no real host
|
|
222
|
+
// session to harvest evidence from. Provide a synthetic evidence entry so
|
|
223
|
+
// the PEAT-B1 admission gate (painEvidenceAdmission, default-on) does not
|
|
224
|
+
// short-circuit the pain before diagnosis. Without this, the baseline
|
|
225
|
+
// reports `short_circuited: input evidence empty` and every downstream
|
|
226
|
+
// stage fails, even though the pipeline itself is healthy.
|
|
227
|
+
evidence: [{ sourceRef: `synthetic:${painId}`, note: 'Synthetic baseline deterministic evidence anchor' }],
|
|
217
228
|
});
|
|
218
229
|
|
|
219
230
|
const painPassed = bridgeResult.status === 'succeeded';
|
|
@@ -5,23 +5,27 @@ import {
|
|
|
5
5
|
isProductionWorkspace,
|
|
6
6
|
guardUatWorkspace,
|
|
7
7
|
getSafeUatWorkspacePath,
|
|
8
|
+
resolveWorkspacePath,
|
|
8
9
|
formatGuardRefusal,
|
|
9
10
|
type GuardRefusal,
|
|
10
11
|
} from './production-workspace-guard.js';
|
|
11
12
|
|
|
13
|
+
// Use os.homedir()-derived paths so the tests work cross-platform
|
|
14
|
+
// (previously hardcoded Windows paths like D:\.openclaw\workspace which on
|
|
15
|
+
// Linux resolve to <cwd>/D:\.openclaw\workspace — meaningless).
|
|
16
|
+
const HOMEDIR_WORKSPACE = path.join(os.homedir(), '.openclaw', 'workspace');
|
|
17
|
+
|
|
12
18
|
const PRODUCTION_PATHS = [
|
|
13
|
-
|
|
14
|
-
'C:\\.openclaw\\workspace',
|
|
15
|
-
'C:\\Users\\Administrator\\.openclaw\\workspace',
|
|
16
|
-
path.join(os.homedir(), '.openclaw', 'workspace'),
|
|
19
|
+
HOMEDIR_WORKSPACE,
|
|
17
20
|
];
|
|
18
21
|
|
|
19
22
|
const SAFE_PATHS = [
|
|
20
|
-
'
|
|
21
|
-
'
|
|
23
|
+
path.join(os.homedir(), '.openclaw', 'workspace-test'),
|
|
24
|
+
path.join(os.homedir(), '.openclaw', 'workspace-backup'),
|
|
22
25
|
path.join(os.tmpdir(), 'pd-uat-workspace'),
|
|
23
26
|
path.join(os.tmpdir(), 'pd-test-any'),
|
|
24
|
-
|
|
27
|
+
// A path that is clearly unrelated on every platform
|
|
28
|
+
path.join(os.tmpdir(), 'completely-unrelated-work'),
|
|
25
29
|
];
|
|
26
30
|
|
|
27
31
|
describe('isProductionWorkspace', () => {
|
|
@@ -32,18 +36,15 @@ describe('isProductionWorkspace', () => {
|
|
|
32
36
|
expect(isProductionWorkspace(path.resolve(safePath))).toBe(false);
|
|
33
37
|
});
|
|
34
38
|
it('detects descendant', () => {
|
|
35
|
-
|
|
36
|
-
// Windows (where D:\... resolves as-is) and Linux CI (where D:\... is
|
|
37
|
-
// treated as a relative path and resolves under cwd).
|
|
38
|
-
const prodPath = path.resolve(path.join(os.homedir(), '.openclaw', 'workspace'));
|
|
39
|
+
const prodPath = path.resolve(HOMEDIR_WORKSPACE);
|
|
39
40
|
const descendantPath = path.join(prodPath, 'sub', 'child');
|
|
40
41
|
expect(isProductionWorkspace(descendantPath)).toBe(true);
|
|
41
42
|
});
|
|
42
43
|
it('rejects sibling workspace-test (ERR-030)', () => {
|
|
43
|
-
expect(isProductionWorkspace(path.resolve('
|
|
44
|
+
expect(isProductionWorkspace(path.resolve(path.join(os.homedir(), '.openclaw', 'workspace-test')))).toBe(false);
|
|
44
45
|
});
|
|
45
46
|
it('rejects sibling workspace-backup (ERR-030)', () => {
|
|
46
|
-
expect(isProductionWorkspace(path.resolve('
|
|
47
|
+
expect(isProductionWorkspace(path.resolve(path.join(os.homedir(), '.openclaw', 'workspace-backup')))).toBe(false);
|
|
47
48
|
});
|
|
48
49
|
});
|
|
49
50
|
|
|
@@ -58,7 +59,7 @@ describe('guardUatWorkspace', () => {
|
|
|
58
59
|
}
|
|
59
60
|
});
|
|
60
61
|
it('refuses descendant', () => {
|
|
61
|
-
const prodPath = path.resolve(
|
|
62
|
+
const prodPath = path.resolve(HOMEDIR_WORKSPACE);
|
|
62
63
|
const descendantPath = path.join(prodPath, 'subdir');
|
|
63
64
|
expect(guardUatWorkspace(descendantPath, 'test').refused).toBe(true);
|
|
64
65
|
});
|
|
@@ -68,8 +69,10 @@ describe('guardUatWorkspace', () => {
|
|
|
68
69
|
const r = guardUatWorkspace(safePath, 'test');
|
|
69
70
|
expect(r.refused).toBe(false);
|
|
70
71
|
});
|
|
71
|
-
it.each([
|
|
72
|
-
|
|
72
|
+
it.each([
|
|
73
|
+
path.join(os.homedir(), '.openclaw', 'workspace-test'),
|
|
74
|
+
path.join(os.homedir(), '.openclaw', 'workspace-backup'),
|
|
75
|
+
])('allows sibling: %s (ERR-030)', (p) => {
|
|
73
76
|
expect(guardUatWorkspace(p, 'test').refused).toBe(false);
|
|
74
77
|
});
|
|
75
78
|
});
|
|
@@ -77,7 +80,7 @@ describe('guardUatWorkspace', () => {
|
|
|
77
80
|
|
|
78
81
|
describe('JSON output (EP-04)', () => {
|
|
79
82
|
it('outputs single object with reason and nextAction', () => {
|
|
80
|
-
const r = guardUatWorkspace(
|
|
83
|
+
const r = guardUatWorkspace(HOMEDIR_WORKSPACE, 'test');
|
|
81
84
|
const json = formatGuardRefusal(r as GuardRefusal, 'test', true);
|
|
82
85
|
const parsed = JSON.parse(json);
|
|
83
86
|
expect(parsed).toMatchObject({
|
|
@@ -87,7 +90,7 @@ describe('JSON output (EP-04)', () => {
|
|
|
87
90
|
expect(Array.isArray(parsed)).toBe(false);
|
|
88
91
|
});
|
|
89
92
|
it('no console prefixes in JSON', () => {
|
|
90
|
-
const r = guardUatWorkspace(
|
|
93
|
+
const r = guardUatWorkspace(HOMEDIR_WORKSPACE, 'test');
|
|
91
94
|
const json = formatGuardRefusal(r as GuardRefusal, 'test', true);
|
|
92
95
|
expect(json).not.toContain('[pd-cli]');
|
|
93
96
|
expect(json).not.toContain('ERROR:');
|
|
@@ -97,7 +100,7 @@ describe('JSON output (EP-04)', () => {
|
|
|
97
100
|
|
|
98
101
|
describe('text output (EP-03)', () => {
|
|
99
102
|
it('includes reason and nextAction', () => {
|
|
100
|
-
const r = guardUatWorkspace(
|
|
103
|
+
const r = guardUatWorkspace(HOMEDIR_WORKSPACE, 'test');
|
|
101
104
|
const text = formatGuardRefusal(r as GuardRefusal, 'test', false);
|
|
102
105
|
expect(text).toContain('Reason:');
|
|
103
106
|
expect(text).toContain('Next Action:');
|
|
@@ -112,4 +115,43 @@ describe('getSafeUatWorkspacePath', () => {
|
|
|
112
115
|
expect(p1).toContain(os.tmpdir());
|
|
113
116
|
expect(p1).toContain('pd-uat-workspace');
|
|
114
117
|
});
|
|
115
|
-
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
describe('resolveWorkspacePath', () => {
|
|
121
|
+
it('resolves an absolute path as-is', () => {
|
|
122
|
+
const abs = path.resolve('/tmp/my-workspace');
|
|
123
|
+
expect(resolveWorkspacePath(abs)).toBe(abs);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('resolves a relative path against cwd', () => {
|
|
127
|
+
const result = resolveWorkspacePath('relative/path');
|
|
128
|
+
const expected = path.resolve(process.cwd(), 'relative/path');
|
|
129
|
+
expect(result).toBe(expected);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('defaults to cwd when no input is provided', () => {
|
|
133
|
+
const result = resolveWorkspacePath(undefined);
|
|
134
|
+
expect(result).toBe(path.resolve(process.cwd()));
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
describe('PD_PRODUCTION_WORKSPACE env var (module-load-time)', () => {
|
|
139
|
+
// NOTE: PRODUCTION_WORKSPACE_PATHS is built once at module load time from
|
|
140
|
+
// process.env.PD_PRODUCTION_WORKSPACE. Setting the env var after the module
|
|
141
|
+
// is loaded does NOT change behavior. These tests verify that the default
|
|
142
|
+
// production path (HOMEDIR_WORKSPACE) is correctly protected, which is the
|
|
143
|
+
// primary regression risk.
|
|
144
|
+
|
|
145
|
+
it('guardUatWorkspace refuses the default production workspace', () => {
|
|
146
|
+
const r = guardUatWorkspace(HOMEDIR_WORKSPACE, 'test');
|
|
147
|
+
expect(r.refused).toBe(true);
|
|
148
|
+
if (r.refused) {
|
|
149
|
+
expect(r.isProduction).toBe(true);
|
|
150
|
+
expect(r.workspace).toBe(path.resolve(HOMEDIR_WORKSPACE));
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('isProductionWorkspace returns true for the default production path', () => {
|
|
155
|
+
expect(isProductionWorkspace(path.resolve(HOMEDIR_WORKSPACE))).toBe(true);
|
|
156
|
+
});
|
|
157
|
+
});
|
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Prevents UAT/runtime test commands from writing to production workspaces.
|
|
5
5
|
*
|
|
6
|
-
* Production workspaces
|
|
7
|
-
* -
|
|
8
|
-
* -
|
|
9
|
-
*
|
|
6
|
+
* Production workspaces:
|
|
7
|
+
* - Default: ~/.openclaw/workspace (cross-platform)
|
|
8
|
+
* - Additional paths declared via PD_PRODUCTION_WORKSPACE env var
|
|
9
|
+
* (use path.delimiter — `;` on Windows, `:` on Unix — to separate multiple paths)
|
|
10
10
|
*
|
|
11
11
|
* This module follows ERR-030 (path prefix matching must use segment boundaries)
|
|
12
12
|
* and EP-03/EP-04 (fail loud with structured reason and nextAction).
|
|
@@ -20,19 +20,22 @@ import { existsSync } from 'fs';
|
|
|
20
20
|
|
|
21
21
|
/**
|
|
22
22
|
* List of production workspace paths that should be protected from UAT/test writes.
|
|
23
|
-
*
|
|
23
|
+
* Built from: the universal default (~/.openclaw/workspace) plus any paths
|
|
24
|
+
* declared via PD_PRODUCTION_WORKSPACE.
|
|
24
25
|
*/
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
26
|
+
function buildProductionWorkspacePaths(): string[] {
|
|
27
|
+
const paths = [path.resolve(path.join(os.homedir(), '.openclaw', 'workspace'))];
|
|
28
|
+
const extra = process.env.PD_PRODUCTION_WORKSPACE;
|
|
29
|
+
if (extra) {
|
|
30
|
+
for (const p of extra.split(path.delimiter)) {
|
|
31
|
+
const trimmed = p.trim();
|
|
32
|
+
if (trimmed) paths.push(path.resolve(trimmed));
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return paths;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const PRODUCTION_WORKSPACE_PATHS = buildProductionWorkspacePaths();
|
|
36
39
|
|
|
37
40
|
// ── Resolution helpers ───────────────────────────────────────────────────────
|
|
38
41
|
|
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BDD step definitions for CLI strict JSON output contract (cli-1-strict-json).
|
|
3
|
+
*
|
|
4
|
+
* Approach: Command + mocks (NOT real CLI subprocess execution).
|
|
5
|
+
* Reuses the exact same mock pattern as tests/commands/pain-retry.test.ts:
|
|
6
|
+
* - vi.hoisted() + vi.mock() to stub @principles/core/runtime-v2 and friends
|
|
7
|
+
* - Import handlePainRetry directly and call it in-process
|
|
8
|
+
* - Capture console.log via vi.spyOn to verify stdout JSON contract
|
|
9
|
+
*
|
|
10
|
+
* Rationale: pd pain retry needs a real workspace + pain-id + config to execute
|
|
11
|
+
* as a subprocess. Testing the JSON output contract in-process with mocks is
|
|
12
|
+
* more reliable and follows the established pattern in this repo.
|
|
13
|
+
*/
|
|
14
|
+
import { vi, expect } from 'vitest';
|
|
15
|
+
import { Command } from 'commander';
|
|
16
|
+
import { readFileSync } from 'node:fs';
|
|
17
|
+
import { createStepRegistry, defineFeature } from './support/vitest-bdd.js';
|
|
18
|
+
import { resolveFeaturePath } from './support/repo-root.js';
|
|
19
|
+
|
|
20
|
+
// ── Mocks (mirrors tests/commands/pain-retry.test.ts) ──────────────────────────
|
|
21
|
+
|
|
22
|
+
const {
|
|
23
|
+
MockRuntimeStateManager,
|
|
24
|
+
mockGetTask,
|
|
25
|
+
mockGetCandidatesByTaskId,
|
|
26
|
+
mockUpdateCandidateStatus,
|
|
27
|
+
mockGetRunsByTask,
|
|
28
|
+
} = vi.hoisted(() => {
|
|
29
|
+
const mockGetTask = vi.fn().mockResolvedValue(null);
|
|
30
|
+
const mockGetCandidatesByTaskId = vi.fn().mockResolvedValue([]);
|
|
31
|
+
const mockUpdateCandidateStatus = vi.fn().mockResolvedValue(undefined);
|
|
32
|
+
const mockGetRunsByTask = vi.fn().mockResolvedValue([]);
|
|
33
|
+
|
|
34
|
+
class MockRuntimeStateManager {
|
|
35
|
+
initialize = vi.fn().mockResolvedValue(undefined);
|
|
36
|
+
close = vi.fn().mockResolvedValue(undefined);
|
|
37
|
+
getTask = mockGetTask;
|
|
38
|
+
getCandidatesByTaskId = mockGetCandidatesByTaskId;
|
|
39
|
+
updateCandidateStatus = mockUpdateCandidateStatus;
|
|
40
|
+
getRunsByTask = mockGetRunsByTask;
|
|
41
|
+
connection = {} as Record<string, unknown>;
|
|
42
|
+
taskStore = {};
|
|
43
|
+
runStore = {};
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
MockRuntimeStateManager,
|
|
47
|
+
mockGetTask,
|
|
48
|
+
mockGetCandidatesByTaskId,
|
|
49
|
+
mockUpdateCandidateStatus,
|
|
50
|
+
mockGetRunsByTask,
|
|
51
|
+
};
|
|
52
|
+
}, { validateType: true });
|
|
53
|
+
|
|
54
|
+
const { mockIntake, MockCandidateIntakeService } = vi.hoisted(() => {
|
|
55
|
+
const mockIntake = vi.fn();
|
|
56
|
+
function MockCandidateIntakeService(this: unknown) {
|
|
57
|
+
return { intake: mockIntake };
|
|
58
|
+
}
|
|
59
|
+
MockCandidateIntakeService.prototype = {};
|
|
60
|
+
return { mockIntake, MockCandidateIntakeService };
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const { MockPrincipleTreeLedgerAdapter } = vi.hoisted(() => {
|
|
64
|
+
function MockPrincipleTreeLedgerAdapter(this: unknown) {
|
|
65
|
+
return {};
|
|
66
|
+
}
|
|
67
|
+
MockPrincipleTreeLedgerAdapter.prototype = {};
|
|
68
|
+
return { MockPrincipleTreeLedgerAdapter };
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const { mockRun, mockResolveRuntimeConfig, mockResolveRuntimeFromPdConfig } = vi.hoisted(() => {
|
|
72
|
+
const mockRun = vi.fn().mockResolvedValue({
|
|
73
|
+
status: 'succeeded',
|
|
74
|
+
taskId: 'diagnosis_test-pain-1',
|
|
75
|
+
runId: 'run-retry-1',
|
|
76
|
+
contextHash: 'abc123',
|
|
77
|
+
});
|
|
78
|
+
const mockResolveRuntimeConfig = vi.fn().mockReturnValue({
|
|
79
|
+
runtimeKind: 'pi-ai',
|
|
80
|
+
provider: 'test-provider',
|
|
81
|
+
model: 'test-model',
|
|
82
|
+
apiKeyEnv: 'TEST_KEY',
|
|
83
|
+
timeoutMs: 300000,
|
|
84
|
+
agentId: 'main',
|
|
85
|
+
});
|
|
86
|
+
const mockResolveRuntimeFromPdConfig = vi.fn().mockReturnValue({
|
|
87
|
+
result: {
|
|
88
|
+
runtimeKind: 'pi-ai',
|
|
89
|
+
provider: 'test-provider',
|
|
90
|
+
model: 'test-model',
|
|
91
|
+
apiKeyEnv: 'TEST_KEY',
|
|
92
|
+
timeoutMs: 300000,
|
|
93
|
+
agentId: 'main',
|
|
94
|
+
},
|
|
95
|
+
legacyWarnings: [],
|
|
96
|
+
configSource: '.pd/config.yaml',
|
|
97
|
+
configLoadResult: { ok: true, effective: {}, defaults: {}, legacyFilesDetected: [] },
|
|
98
|
+
});
|
|
99
|
+
return { mockRun, mockResolveRuntimeConfig, mockResolveRuntimeFromPdConfig };
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
vi.mock('../../src/resolve-workspace.js', () => ({
|
|
103
|
+
resolveWorkspaceDir: vi.fn().mockReturnValue('/tmp/fake-workspace'),
|
|
104
|
+
}));
|
|
105
|
+
|
|
106
|
+
vi.mock('@principles/core/runtime-v2', () => {
|
|
107
|
+
return {
|
|
108
|
+
RuntimeStateManager: vi.fn().mockImplementation(function () {
|
|
109
|
+
return new MockRuntimeStateManager();
|
|
110
|
+
}),
|
|
111
|
+
SqliteHistoryQuery: vi.fn().mockImplementation(function () { return {}; }),
|
|
112
|
+
SqliteContextAssembler: vi.fn().mockImplementation(function () { return {}; }),
|
|
113
|
+
SqliteDiagnosticianCommitter: vi.fn().mockImplementation(function () { return {}; }),
|
|
114
|
+
SqliteTrajectoryLocator: vi.fn().mockImplementation(function () { return {}; }),
|
|
115
|
+
SqliteSourceTraceLocator: vi.fn().mockImplementation(function () { return {}; }),
|
|
116
|
+
// Dead-letter store mock: getByPainId returns null so the implementation
|
|
117
|
+
// produces status='not_found' with reason='task_not_found' for the
|
|
118
|
+
// "失败命令的 --json 输出含 reason 和 nextAction" scenario.
|
|
119
|
+
SqliteDeadLetterStore: vi.fn().mockImplementation(function () {
|
|
120
|
+
return { getByPainId: vi.fn().mockReturnValue(null) };
|
|
121
|
+
}),
|
|
122
|
+
PainSignalBridge: vi.fn().mockImplementation(function () { return {}; }),
|
|
123
|
+
StoreEventEmitter: vi.fn().mockImplementation(function () { return {}; }),
|
|
124
|
+
storeEmitter: { emitTelemetry: vi.fn() },
|
|
125
|
+
SplitDiagnosticianRunner: vi.fn().mockImplementation(function () { return {}; }),
|
|
126
|
+
DiagRootCauseRunner: vi.fn().mockImplementation(function () { return {}; }),
|
|
127
|
+
DiagDistillerRunner: vi.fn().mockImplementation(function () { return {}; }),
|
|
128
|
+
DiagRouterRunner: vi.fn().mockImplementation(function () { return {}; }),
|
|
129
|
+
DefaultDiagRootCauseValidator: vi.fn().mockImplementation(function () { return {}; }),
|
|
130
|
+
DefaultDiagDistillerValidator: vi.fn().mockImplementation(function () { return {}; }),
|
|
131
|
+
DisabledDiagnosticianRunner: vi.fn().mockImplementation(function () { return {}; }),
|
|
132
|
+
TestDoubleRuntimeAdapter: vi.fn().mockImplementation(function () { return {}; }),
|
|
133
|
+
OpenClawCliRuntimeAdapter: vi.fn().mockImplementation(function () { return {}; }),
|
|
134
|
+
PiAiRuntimeAdapter: vi.fn().mockImplementation(function () { return {}; }),
|
|
135
|
+
SPLIT_PIPELINE_TOTAL_TIMEOUT_MS: 300000,
|
|
136
|
+
PDRuntimeError: class PDRuntimeError extends Error {
|
|
137
|
+
constructor(public category: string, message: string) {
|
|
138
|
+
super(message);
|
|
139
|
+
this.name = 'PDRuntimeError';
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
CandidateIntakeService: MockCandidateIntakeService,
|
|
143
|
+
resolveRuntimeConfig: mockResolveRuntimeConfig,
|
|
144
|
+
isRuntimeConfigError: vi.fn().mockReturnValue(false),
|
|
145
|
+
isFeatureEnabled: vi.fn().mockReturnValue(true),
|
|
146
|
+
resolveOutputLanguage: vi.fn().mockReturnValue({ outputLanguage: 'zh-CN' }),
|
|
147
|
+
validatePdConfig: vi.fn().mockReturnValue({ valid: true, errors: [] }),
|
|
148
|
+
computeEffectivePdConfig: vi.fn().mockReturnValue({ config: {}, source: 'defaults', warnings: [] }),
|
|
149
|
+
computeFeatureFlagsFromConfig: vi.fn().mockReturnValue({}),
|
|
150
|
+
redactPdConfig: vi.fn().mockImplementation((c) => c),
|
|
151
|
+
run: mockRun,
|
|
152
|
+
status: vi.fn(),
|
|
153
|
+
PrincipleTreeLedgerAdapter: MockPrincipleTreeLedgerAdapter,
|
|
154
|
+
};
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
vi.mock('../../src/config-reader.js', () => ({
|
|
158
|
+
readOutputLanguageFromWorkspace: vi.fn().mockReturnValue({ outputLanguage: 'zh-CN' }),
|
|
159
|
+
}));
|
|
160
|
+
|
|
161
|
+
vi.mock('../../src/services/resolve-runtime-from-pd-config.js', () => ({
|
|
162
|
+
resolveRuntimeFromPdConfig: mockResolveRuntimeFromPdConfig,
|
|
163
|
+
}));
|
|
164
|
+
|
|
165
|
+
import { registerPainRetryCommand } from '../../src/commands/pain-retry.js';
|
|
166
|
+
|
|
167
|
+
// ── Test data ──────────────────────────────────────────────────────────────────
|
|
168
|
+
|
|
169
|
+
const RETRY_WAIT_TASK = {
|
|
170
|
+
taskId: 'diagnosis_pain-001',
|
|
171
|
+
taskKind: 'diagnostician',
|
|
172
|
+
status: 'retry_wait' as const,
|
|
173
|
+
attemptCount: 1,
|
|
174
|
+
maxAttempts: 3,
|
|
175
|
+
lastError: 'output_invalid',
|
|
176
|
+
leaseOwner: null,
|
|
177
|
+
leaseExpiresAt: null,
|
|
178
|
+
resultRef: null,
|
|
179
|
+
createdAt: new Date().toISOString(),
|
|
180
|
+
updatedAt: new Date().toISOString(),
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
// ── Helpers ────────────────────────────────────────────────────────────────────
|
|
184
|
+
|
|
185
|
+
/** Reset all mocks to a clean default state for the next scenario. */
|
|
186
|
+
function resetMocksForScenario(painId: string): void {
|
|
187
|
+
vi.clearAllMocks();
|
|
188
|
+
|
|
189
|
+
mockResolveRuntimeConfig.mockReturnValue({
|
|
190
|
+
runtimeKind: 'pi-ai',
|
|
191
|
+
provider: 'test-provider',
|
|
192
|
+
model: 'test-model',
|
|
193
|
+
apiKeyEnv: 'TEST_KEY',
|
|
194
|
+
timeoutMs: 300000,
|
|
195
|
+
agentId: 'main',
|
|
196
|
+
});
|
|
197
|
+
mockResolveRuntimeFromPdConfig.mockReturnValue({
|
|
198
|
+
result: {
|
|
199
|
+
runtimeKind: 'pi-ai',
|
|
200
|
+
provider: 'test-provider',
|
|
201
|
+
model: 'test-model',
|
|
202
|
+
apiKeyEnv: 'TEST_KEY',
|
|
203
|
+
timeoutMs: 300000,
|
|
204
|
+
agentId: 'main',
|
|
205
|
+
},
|
|
206
|
+
legacyWarnings: [],
|
|
207
|
+
configSource: '.pd/config.yaml',
|
|
208
|
+
configLoadResult: { ok: true, effective: {}, defaults: {}, legacyFilesDetected: [] },
|
|
209
|
+
});
|
|
210
|
+
mockGetCandidatesByTaskId.mockResolvedValue([]);
|
|
211
|
+
mockUpdateCandidateStatus.mockResolvedValue(undefined);
|
|
212
|
+
mockGetRunsByTask.mockResolvedValue([]);
|
|
213
|
+
mockIntake.mockReset();
|
|
214
|
+
|
|
215
|
+
if (painId === 'nonexistent') {
|
|
216
|
+
// Failure scenario: task not found
|
|
217
|
+
mockGetTask.mockResolvedValue(null);
|
|
218
|
+
} else {
|
|
219
|
+
// Success scenario: retry_wait task → succeeded
|
|
220
|
+
mockGetTask.mockResolvedValue({
|
|
221
|
+
...RETRY_WAIT_TASK,
|
|
222
|
+
taskId: `diagnosis_${painId}`,
|
|
223
|
+
});
|
|
224
|
+
mockRun.mockResolvedValue({
|
|
225
|
+
status: 'succeeded',
|
|
226
|
+
taskId: `diagnosis_${painId}`,
|
|
227
|
+
runId: 'run-retry-1',
|
|
228
|
+
contextHash: 'abc123',
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// ── Step definitions ───────────────────────────────────────────────────────────
|
|
234
|
+
|
|
235
|
+
const registry = createStepRegistry();
|
|
236
|
+
|
|
237
|
+
registry.given('一个可用的 pd-cli 可执行文件', () => {
|
|
238
|
+
if (typeof registerPainRetryCommand !== 'function') {
|
|
239
|
+
throw new Error('pd-cli pain retry registration not available');
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
registry.when(/operator 执行 "(.+)"/, async (ctx, command: string) => {
|
|
244
|
+
const argv = command.trim().split(/\s+/);
|
|
245
|
+
if (argv.shift() !== 'pd') {
|
|
246
|
+
throw new Error(`expected command to start with pd: ${command}`);
|
|
247
|
+
}
|
|
248
|
+
const painIdIndex = argv.indexOf('--pain-id');
|
|
249
|
+
const painId = painIdIndex >= 0 ? argv[painIdIndex + 1] : '';
|
|
250
|
+
resetMocksForScenario(painId);
|
|
251
|
+
|
|
252
|
+
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
253
|
+
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
254
|
+
const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
|
|
255
|
+
|
|
256
|
+
try {
|
|
257
|
+
const program = new Command().name('pd').exitOverride();
|
|
258
|
+
registerPainRetryCommand(program.command('pain'));
|
|
259
|
+
await program.parseAsync(['node', 'pd', ...argv]);
|
|
260
|
+
} finally {
|
|
261
|
+
expect(exitSpy).not.toHaveBeenCalledWith(0);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const allLogCalls = logSpy.mock.calls.map((c) => String(c[0]));
|
|
265
|
+
const jsonCall = allLogCalls.find((s) => {
|
|
266
|
+
try { JSON.parse(s); return true; } catch { return false; }
|
|
267
|
+
});
|
|
268
|
+
// cli-1: 统计 stdout 上可解析为 JSON 的调用数,用于 Then 步骤验证"恰好一个"
|
|
269
|
+
const jsonCallCount = allLogCalls.filter((s) => {
|
|
270
|
+
try { JSON.parse(s); return true; } catch { return false; }
|
|
271
|
+
}).length;
|
|
272
|
+
const stdout = allLogCalls.join('\n');
|
|
273
|
+
const stderr = errorSpy.mock.calls.map((c) => String(c[0])).join('\n');
|
|
274
|
+
ctx.state.cliResult = { stdout, stderr, jsonCallCount, allLogCallCount: allLogCalls.length };
|
|
275
|
+
|
|
276
|
+
// Restore spies so Then steps don't capture new calls
|
|
277
|
+
logSpy.mockRestore();
|
|
278
|
+
errorSpy.mockRestore();
|
|
279
|
+
exitSpy.mockRestore();
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
registry.then('stdout 是严格的单一 JSON 对象', (ctx) => {
|
|
283
|
+
const result = ctx.state.cliResult as {
|
|
284
|
+
stdout: string;
|
|
285
|
+
jsonCallCount: number;
|
|
286
|
+
allLogCallCount: number;
|
|
287
|
+
};
|
|
288
|
+
const out = result.stdout.trim();
|
|
289
|
+
// Must start with { and parse as a non-null object
|
|
290
|
+
expect(out.startsWith('{')).toBe(true);
|
|
291
|
+
const parsed = JSON.parse(out);
|
|
292
|
+
expect(typeof parsed).toBe('object');
|
|
293
|
+
expect(parsed).not.toBeNull();
|
|
294
|
+
expect(result.allLogCallCount).toBe(1);
|
|
295
|
+
expect(result.jsonCallCount).toBe(1);
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
registry.then('该 JSON 对象可以被 JSON.parse 解析', (ctx) => {
|
|
299
|
+
const result = ctx.state.cliResult as { stdout: string };
|
|
300
|
+
expect(() => JSON.parse(result.stdout.trim())).not.toThrow();
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
registry.then('stdout 不包含任何 banner 或 heading', (ctx) => {
|
|
304
|
+
const result = ctx.state.cliResult as { stdout: string };
|
|
305
|
+
const out = result.stdout.trim();
|
|
306
|
+
expect(out.startsWith('===')).toBe(false);
|
|
307
|
+
expect(out.startsWith('#')).toBe(false);
|
|
308
|
+
expect(out.startsWith('PD CLI')).toBe(false);
|
|
309
|
+
expect(out.startsWith('pd ')).toBe(false);
|
|
310
|
+
// Strict JSON output must start with {
|
|
311
|
+
expect(out.startsWith('{')).toBe(true);
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
registry.then('该 JSON 对象的 status 字段表示失败', (ctx) => {
|
|
315
|
+
const result = ctx.state.cliResult as { stdout: string };
|
|
316
|
+
const parsed = JSON.parse(result.stdout.trim());
|
|
317
|
+
expect(parsed).toHaveProperty('status');
|
|
318
|
+
expect(parsed.status).not.toBe('succeeded');
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
registry.then('该 JSON 对象包含 reason 字段', (ctx) => {
|
|
322
|
+
const result = ctx.state.cliResult as { stdout: string };
|
|
323
|
+
const parsed = JSON.parse(result.stdout.trim());
|
|
324
|
+
expect(parsed).toHaveProperty('reason');
|
|
325
|
+
expect(typeof parsed.reason).toBe('string');
|
|
326
|
+
expect(parsed.reason.length).toBeGreaterThan(0);
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
registry.then('该 JSON 对象包含 nextAction 字段', (ctx) => {
|
|
330
|
+
const result = ctx.state.cliResult as { stdout: string };
|
|
331
|
+
const parsed = JSON.parse(result.stdout.trim());
|
|
332
|
+
expect(parsed).toHaveProperty('nextAction');
|
|
333
|
+
expect(typeof parsed.nextAction).toBe('string');
|
|
334
|
+
expect(parsed.nextAction.length).toBeGreaterThan(0);
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
// ── Load and register feature ──────────────────────────────────────────────────
|
|
338
|
+
|
|
339
|
+
const featureText = readFileSync(
|
|
340
|
+
resolveFeaturePath('docs/specs/features/cli/json-output.feature'),
|
|
341
|
+
'utf8'
|
|
342
|
+
);
|
|
343
|
+
defineFeature(featureText, registry);
|