@principles/pd-cli 1.147.11 → 1.147.13

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.
@@ -0,0 +1,292 @@
1
+ /**
2
+ * PRI-714 (review fix #3): run-once language wiring regression.
3
+ *
4
+ * The PR's original fault class was "the parameter exists but is not wired" —
5
+ * builder-only tests still pass after deleting the production pass-through.
6
+ * This regression drives the REAL `handleRuntimeInternalizationRunOnce`
7
+ * handler against a REAL workspace directory whose `.pd/config.yaml` carries
8
+ * `principles.outputLanguage`, through the REAL `readOutputLanguageFromWorkspace`
9
+ * reader, and asserts the language value the handler passes into the runner
10
+ * constructor options (the same options object the runner forwards to its
11
+ * prompt builder — the runner→startRun message is proven message-level by
12
+ * rulehost-pipeline-runner.test.ts and the consumer-cycle language test):
13
+ *
14
+ * - explicit `principles.outputLanguage: 'en'` overrides the zh-CN default;
15
+ * - no config file resolves the zh-CN default.
16
+ */
17
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
18
+ import * as fs from 'node:fs';
19
+ import * as os from 'node:os';
20
+ import * as path from 'node:path';
21
+
22
+ const mockWakeOnce = vi.fn();
23
+ const mockRun = vi.fn();
24
+ const mockCommitNextTaskProposal = vi.fn().mockResolvedValue({ decision: 'no_successor', sourceTaskId: '', reason: '' });
25
+ const mockClose = vi.fn().mockResolvedValue(undefined);
26
+ const mockInitialize = vi.fn().mockResolvedValue(undefined);
27
+ const mockPiArtifactStore = {
28
+ createArtifact: vi.fn().mockResolvedValue({}),
29
+ upsertArtifact: vi.fn().mockResolvedValue({}),
30
+ getArtifactById: vi.fn().mockResolvedValue(null),
31
+ listBySourceTaskId: vi.fn().mockResolvedValue([]),
32
+ listLineage: vi.fn().mockResolvedValue([]),
33
+ };
34
+
35
+ /** Captured runner constructor options — the handler → runner language seam. */
36
+ const runnerCtorOptions: Array<{ kind: string; options: Record<string, unknown> }> = [];
37
+
38
+ function recordRunnerCtor(kind: string) {
39
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
40
+ return function (this: unknown, _deps: unknown, options: Record<string, unknown>) {
41
+ runnerCtorOptions.push({ kind, options: { ...options } });
42
+ return { run: mockRun };
43
+ };
44
+ }
45
+
46
+ const tempDirs: string[] = [];
47
+
48
+ function makeWorkspace(outputLanguage?: 'en'): string {
49
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'pd-runonce-lang-'));
50
+ tempDirs.push(dir);
51
+ if (outputLanguage !== undefined) {
52
+ fs.mkdirSync(path.join(dir, '.pd'), { recursive: true });
53
+ // Full valid-config shape so validation keeps `principles` intact.
54
+ const config = {
55
+ version: 1,
56
+ features: {
57
+ prompt: { category: 'core', enabled: true },
58
+ code_tool_hook: { category: 'core', enabled: true },
59
+ defer_archive: { category: 'core', enabled: true },
60
+ correction_observer: { category: 'quiet', enabled: false },
61
+ empathy_observer: { category: 'quiet', enabled: false },
62
+ },
63
+ runtimeProfiles: { 'openclaw.default': { type: 'openclaw', source: 'default' } },
64
+ internalAgents: {
65
+ defaultRuntime: 'openclaw.default',
66
+ agents: {
67
+ diagnostician: { enabled: true, runtimeProfile: 'openclaw.default' },
68
+ dreamer: { enabled: true },
69
+ scribe: { enabled: true },
70
+ artificer: { enabled: true },
71
+ philosopher: { enabled: false },
72
+ evaluator: { enabled: false },
73
+ rolloutReviewer: { enabled: false },
74
+ correctionObserver: { enabled: false },
75
+ empathyObserver: { enabled: false },
76
+ },
77
+ },
78
+ ui: { diagnostics: { mode: 'simple' } },
79
+ principles: { outputLanguage },
80
+ };
81
+ // JSON is valid YAML — same trick the shared executor tests use.
82
+ fs.writeFileSync(path.join(dir, '.pd', 'config.yaml'), JSON.stringify(config));
83
+ }
84
+ return dir;
85
+ }
86
+
87
+ vi.mock('../../src/resolve-workspace.js', () => ({
88
+ resolveWorkspaceDir: vi.fn(),
89
+ }));
90
+
91
+ const { mockResolveRuntimeFromPdConfig } = vi.hoisted(() => {
92
+ const mockResolveRuntimeFromPdConfig = vi.fn().mockReturnValue({
93
+ result: {
94
+ runtimeKind: 'pi-ai',
95
+ provider: 'test-provider',
96
+ model: 'test-model',
97
+ apiKeyEnv: 'TEST_API_KEY',
98
+ timeoutMs: 300_000,
99
+ agentId: 'main',
100
+ },
101
+ legacyWarnings: [],
102
+ configSource: '.pd/config.yaml',
103
+ configLoadResult: { ok: true, effective: {}, defaults: {}, legacyFilesDetected: [] },
104
+ });
105
+ return { mockResolveRuntimeFromPdConfig };
106
+ });
107
+
108
+ vi.mock('../../src/services/resolve-runtime-from-pd-config.js', () => ({
109
+ resolveRuntimeFromPdConfig: mockResolveRuntimeFromPdConfig,
110
+ }));
111
+
112
+ vi.mock('@principles/core/runtime-v2', () => ({
113
+ RuntimeStateManager: vi.fn().mockImplementation(function () {
114
+ return {
115
+ initialize: mockInitialize,
116
+ close: mockClose,
117
+ connection: {},
118
+ taskStore: {},
119
+ runStore: {},
120
+ piArtifactStore: mockPiArtifactStore,
121
+ };
122
+ }),
123
+ InternalizationOrchestrator: vi.fn().mockImplementation(function () {
124
+ return { wakeOnce: mockWakeOnce, commitNextTaskProposal: mockCommitNextTaskProposal };
125
+ }),
126
+ DreamerRunner: vi.fn().mockImplementation(recordRunnerCtor('dreamer')),
127
+ PhilosopherRunner: vi.fn().mockImplementation(recordRunnerCtor('philosopher')),
128
+ ScribeRunner: vi.fn().mockImplementation(recordRunnerCtor('scribe')),
129
+ ArtificerRunner: vi.fn().mockImplementation(recordRunnerCtor('artificer')),
130
+ EvaluatorRunner: vi.fn().mockImplementation(recordRunnerCtor('evaluator')),
131
+ RolloutReviewerRunner: vi.fn().mockImplementation(recordRunnerCtor('rollout_reviewer')),
132
+ StoreEventEmitter: vi.fn().mockImplementation(function () {
133
+ return { emitTelemetry: vi.fn() };
134
+ }),
135
+ TestDoubleRuntimeAdapter: vi.fn().mockImplementation(function () {
136
+ return {
137
+ kind: vi.fn().mockReturnValue('test-double'),
138
+ getCapabilities: vi.fn(),
139
+ healthCheck: vi.fn(),
140
+ startRun: vi.fn().mockResolvedValue({ runId: 'run-test-001', runtimeKind: 'test-double', startedAt: new Date().toISOString() }),
141
+ pollRun: vi.fn().mockResolvedValue({ runId: 'run-test-001', status: 'succeeded' }),
142
+ cancelRun: vi.fn().mockResolvedValue(undefined),
143
+ fetchOutput: vi.fn().mockResolvedValue({ runId: 'run-test-001', payload: {} }),
144
+ fetchArtifacts: vi.fn().mockResolvedValue([]),
145
+ };
146
+ }),
147
+ PiAiRuntimeAdapter: vi.fn().mockImplementation(function () {
148
+ return {
149
+ kind: vi.fn().mockReturnValue('pi-ai'),
150
+ getCapabilities: vi.fn(),
151
+ healthCheck: vi.fn(),
152
+ startRun: vi.fn().mockResolvedValue({ runId: 'run-pi-001', runtimeKind: 'pi-ai', startedAt: new Date().toISOString() }),
153
+ pollRun: vi.fn().mockResolvedValue({ runId: 'run-pi-001', status: 'succeeded' }),
154
+ cancelRun: vi.fn().mockResolvedValue(undefined),
155
+ fetchOutput: vi.fn().mockResolvedValue({ runId: 'run-pi-001', payload: {} }),
156
+ fetchArtifacts: vi.fn().mockResolvedValue([]),
157
+ };
158
+ }),
159
+ OpenClawCliRuntimeAdapter: vi.fn().mockImplementation(function () {
160
+ return {
161
+ kind: vi.fn().mockReturnValue('openclaw-cli'),
162
+ getCapabilities: vi.fn(),
163
+ healthCheck: vi.fn(),
164
+ startRun: vi.fn().mockResolvedValue({ runId: 'run-oc-001', runtimeKind: 'openclaw-cli', startedAt: new Date().toISOString() }),
165
+ pollRun: vi.fn().mockResolvedValue({ runId: 'run-oc-001', status: 'succeeded' }),
166
+ cancelRun: vi.fn().mockResolvedValue(undefined),
167
+ fetchOutput: vi.fn().mockResolvedValue({ runId: 'run-oc-001', payload: {} }),
168
+ fetchArtifacts: vi.fn().mockResolvedValue([]),
169
+ };
170
+ }),
171
+ DefaultDreamerValidator: vi.fn().mockImplementation(function () {
172
+ return { validate: vi.fn().mockResolvedValue({ valid: true, errors: [] }) };
173
+ }),
174
+ DefaultPhilosopherValidator: vi.fn().mockImplementation(function () {
175
+ return { validate: vi.fn().mockResolvedValue({ valid: true, errors: [] }) };
176
+ }),
177
+ DefaultScribeValidator: vi.fn().mockImplementation(function () {
178
+ return { validate: vi.fn().mockResolvedValue({ valid: true, errors: [] }) };
179
+ }),
180
+ DefaultArtificerValidator: vi.fn().mockImplementation(function () {
181
+ return { validate: vi.fn().mockResolvedValue({ valid: true, errors: [] }) };
182
+ }),
183
+ DefaultEvaluatorValidator: vi.fn().mockImplementation(function () {
184
+ return { validate: vi.fn().mockResolvedValue({ valid: true, errors: [] }) };
185
+ }),
186
+ DefaultRolloutReviewerValidator: vi.fn().mockImplementation(function () {
187
+ return { validate: vi.fn().mockResolvedValue({ valid: true, errors: [] }) };
188
+ }),
189
+ resolveRuntimeConfigFromPdConfig: vi.fn().mockReturnValue({ runtimeKind: 'pi-ai', provider: 'test-provider', model: 'test-model', apiKeyEnv: 'TEST_API_KEY', timeoutMs: 300_000, agentId: 'main' }),
190
+ resolveRuntimeConfig: vi.fn().mockReturnValue({ runtimeKind: 'pi-ai', timeoutMs: 300_000, agentId: 'main', provider: 'test-provider', model: 'test-model', apiKeyEnv: 'TEST_API_KEY' }),
191
+ validateRuntimeConfig: vi.fn(),
192
+ isRuntimeConfigError: vi.fn().mockImplementation((result: unknown) => result != null && typeof result === 'object' && Object.hasOwn(result, 'reason') && !Object.hasOwn(result, 'runtimeKind')),
193
+ // Minimal faithful re-implementation: the real resolver's behavior on the
194
+ // values this test exercises ('en' explicit / undefined default).
195
+ resolveOutputLanguage: vi.fn().mockImplementation((raw: unknown) =>
196
+ (raw === 'en' ? { outputLanguage: 'en' as const } : { outputLanguage: 'zh-CN' as const })),
197
+ }));
198
+
199
+ // NOTE: `../../src/config-reader.js` is deliberately NOT mocked — the REAL
200
+ // readOutputLanguageFromWorkspace runs against the temp workspace, proving
201
+ // the handler consumes the actual workspace config.
202
+ vi.mock('../../src/services/pd-config-loader.js', () => ({
203
+ loadPdConfig: vi.fn().mockReturnValue({
204
+ ok: true,
205
+ effective: {},
206
+ source: 'defaults',
207
+ configPath: '/fake/workspace/.pd/config.yaml',
208
+ warnings: [],
209
+ legacyFilesDetected: [],
210
+ legacyFileNextActions: [],
211
+ }),
212
+ computeFlagsFromLoadResult: vi.fn().mockReturnValue({ flags: {}, enabledChannels: [], warnings: [] }),
213
+ }));
214
+
215
+ vi.mock('@principles/host-runtime', () => ({
216
+ createEvaluatorRuntimeContext: vi.fn().mockReturnValue({ ok: true, gateDeps: { evaluateInSandbox: vi.fn() } }),
217
+ createRolloutGovernanceDeps: vi.fn().mockReturnValue({ dispatchActivation: vi.fn(), reopenRevisionTarget: vi.fn() }),
218
+ }));
219
+
220
+ import { handleRuntimeInternalizationRunOnce } from '../../src/commands/runtime-internalization-run-once.js';
221
+
222
+ function dreamerRunResult(taskId: string) {
223
+ return {
224
+ status: 'succeeded',
225
+ taskId,
226
+ runId: 'run-001',
227
+ artifactId: 'pi-art-task-dreamer-001-run-001',
228
+ resultRef: 'dreamer://run-001',
229
+ contextHash: 'ctx-abc',
230
+ output: {
231
+ valid: true,
232
+ taskId,
233
+ candidates: [{ candidateIndex: 0, badDecision: 'Ignored validation', betterDecision: 'Validate inputs', rationale: 'Prevents errors', confidence: 0.9, riskLevel: 'low', strategicPerspective: 'defensive-programming' }],
234
+ contextRefs: [],
235
+ generatedAt: new Date().toISOString(),
236
+ },
237
+ attemptCount: 1,
238
+ };
239
+ }
240
+
241
+ describe('handleRuntimeInternalizationRunOnce — outputLanguage wiring (PRI-714 review fix)', () => {
242
+ let consoleLogSpy: ReturnType<typeof vi.spyOn>;
243
+ let consoleErrorSpy: ReturnType<typeof vi.spyOn>;
244
+
245
+ beforeEach(() => {
246
+ vi.clearAllMocks();
247
+ runnerCtorOptions.length = 0;
248
+ process.exitCode = 0;
249
+ consoleLogSpy = vi.spyOn(console, 'log');
250
+ consoleErrorSpy = vi.spyOn(console, 'error');
251
+ mockWakeOnce.mockResolvedValue({
252
+ decision: 'would_lease',
253
+ taskId: 'task-dreamer-001',
254
+ taskKind: 'dreamer',
255
+ });
256
+ mockRun.mockImplementation(async (taskId: string) => dreamerRunResult(taskId));
257
+ });
258
+
259
+ afterEach(async () => {
260
+ process.exitCode = 0;
261
+ consoleLogSpy.mockRestore();
262
+ consoleErrorSpy.mockRestore();
263
+ for (const dir of tempDirs.splice(0)) {
264
+ try { fs.rmSync(dir, { recursive: true, force: true }); } catch { /* temp */ }
265
+ }
266
+ });
267
+
268
+ it('explicit principles.outputLanguage=en reaches the runner options (overrides zh-CN default)', async () => {
269
+ const workspace = makeWorkspace('en');
270
+ (vi.mocked(await import('../../src/resolve-workspace.js')).resolveWorkspaceDir as ReturnType<typeof vi.fn>).mockReturnValue(workspace);
271
+
272
+ await handleRuntimeInternalizationRunOnce({ workspace, json: true });
273
+
274
+ expect(runnerCtorOptions.length).toBeGreaterThan(0);
275
+ const dreamer = runnerCtorOptions.find((r) => r.kind === 'dreamer');
276
+ expect(dreamer).toBeDefined();
277
+ if (!dreamer) return;
278
+ expect(dreamer.options.outputLanguage).toBe('en');
279
+ });
280
+
281
+ it('no config file resolves the zh-CN default into the runner options', async () => {
282
+ const workspace = makeWorkspace();
283
+ (vi.mocked(await import('../../src/resolve-workspace.js')).resolveWorkspaceDir as ReturnType<typeof vi.fn>).mockReturnValue(workspace);
284
+
285
+ await handleRuntimeInternalizationRunOnce({ workspace, json: true });
286
+
287
+ const dreamer = runnerCtorOptions.find((r) => r.kind === 'dreamer');
288
+ expect(dreamer).toBeDefined();
289
+ if (!dreamer) return;
290
+ expect(dreamer.options.outputLanguage).toBe('zh-CN');
291
+ });
292
+ });