@principles/pd-cli 1.143.0 → 1.145.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/codex-ingest-catchup.d.ts +20 -0
- package/dist/commands/codex-ingest-catchup.d.ts.map +1 -0
- package/dist/commands/codex-ingest-catchup.js +95 -0
- package/dist/commands/codex-ingest-catchup.js.map +1 -0
- package/dist/commands/codex-reconcile.d.ts +22 -0
- package/dist/commands/codex-reconcile.d.ts.map +1 -0
- package/dist/commands/codex-reconcile.js +61 -0
- package/dist/commands/codex-reconcile.js.map +1 -0
- package/dist/commands/codex-worker.d.ts +20 -0
- package/dist/commands/codex-worker.d.ts.map +1 -0
- package/dist/commands/codex-worker.js +156 -0
- package/dist/commands/codex-worker.js.map +1 -0
- package/dist/commands/console.d.ts.map +1 -1
- package/dist/commands/console.js +34 -1
- package/dist/commands/console.js.map +1 -1
- package/dist/commands/pain-retry.d.ts.map +1 -1
- package/dist/commands/pain-retry.js +4 -2
- package/dist/commands/pain-retry.js.map +1 -1
- package/dist/index.js +70 -0
- package/dist/index.js.map +1 -1
- package/dist/services/console-launcher.d.ts +8 -2
- package/dist/services/console-launcher.d.ts.map +1 -1
- package/dist/services/console-launcher.js +34 -4
- package/dist/services/console-launcher.js.map +1 -1
- package/package.json +1 -1
- package/src/commands/codex-ingest-catchup.ts +114 -0
- package/src/commands/codex-reconcile.ts +84 -0
- package/src/commands/codex-worker.ts +174 -0
- package/src/commands/console.ts +35 -2
- package/src/commands/pain-retry.ts +4 -2
- package/src/index.ts +75 -0
- package/src/services/console-launcher.ts +49 -6
- package/tests/commands/codex-worker-registration.test.ts +178 -0
- package/tests/commands/console-open.test.ts +83 -2
- package/tests/commands/health.test.ts +4 -0
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command-registration / parser + functional tests for the PRI-624 Slice C
|
|
3
|
+
* Codex commands (cli-7): `pd codex ingest catch-up` and `pd codex worker`.
|
|
4
|
+
*
|
|
5
|
+
* Registration mirrors src/index.ts. Functional tests exercise the real
|
|
6
|
+
* handlers against a temp workspace: --json emits exactly one parseable
|
|
7
|
+
* JSON object (cli-1), flag-off skips carry reason + nextAction (cli-6), and
|
|
8
|
+
* failed paths mutate nothing.
|
|
9
|
+
*/
|
|
10
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
11
|
+
import fs from 'node:fs';
|
|
12
|
+
import os from 'node:os';
|
|
13
|
+
import path from 'node:path';
|
|
14
|
+
import { Command } from 'commander';
|
|
15
|
+
import { getDefaultPdConfig } from '@principles/core/runtime-v2';
|
|
16
|
+
|
|
17
|
+
function buildTestProgram(): Command {
|
|
18
|
+
const program = new Command();
|
|
19
|
+
const codex = program.command('codex');
|
|
20
|
+
codex.command('reconcile');
|
|
21
|
+
const ingest = codex.command('ingest');
|
|
22
|
+
ingest
|
|
23
|
+
.command('catch-up')
|
|
24
|
+
.option('-w, --workspace <path>', 'Workspace directory')
|
|
25
|
+
.option('--max-rollouts <n>', 'Maximum rollouts to catch up per pass (1-32, default 8)')
|
|
26
|
+
.option('--json', 'Output raw JSON')
|
|
27
|
+
.action(() => {});
|
|
28
|
+
codex
|
|
29
|
+
.command('worker')
|
|
30
|
+
.option('-w, --workspace <path>', 'Workspace directory')
|
|
31
|
+
.option('--once', 'Run exactly one bounded cycle and exit')
|
|
32
|
+
.option('--interval <ms>', 'Cycle interval for continuous mode')
|
|
33
|
+
.option('--status', 'Report the SPEC §15 worker mode without executing anything')
|
|
34
|
+
.option('--json', 'Output raw JSON')
|
|
35
|
+
.action(() => {});
|
|
36
|
+
return program;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
describe('codex Slice C command registration (cli-7)', () => {
|
|
40
|
+
it('parses codex ingest catch-up flags', () => {
|
|
41
|
+
const program = buildTestProgram();
|
|
42
|
+
program.parse(['node', 'pd', 'codex', 'ingest', 'catch-up', '--workspace', '/tmp/ws', '--max-rollouts', '4', '--json']);
|
|
43
|
+
const codex = program.commands.find((c) => c.name() === 'codex');
|
|
44
|
+
const ingest = codex?.commands.find((c) => c.name() === 'ingest');
|
|
45
|
+
const catchUp = ingest?.commands.find((c) => c.name() === 'catch-up');
|
|
46
|
+
expect(catchUp).toBeDefined();
|
|
47
|
+
expect(catchUp?.opts().workspace).toBe('/tmp/ws');
|
|
48
|
+
expect(catchUp?.opts().maxRollouts).toBe('4');
|
|
49
|
+
expect(catchUp?.opts().json).toBe(true);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('parses codex worker flags including --once and --status', () => {
|
|
53
|
+
const program = buildTestProgram();
|
|
54
|
+
program.parse(['node', 'pd', 'codex', 'worker', '--workspace', '/tmp/ws', '--once', '--json']);
|
|
55
|
+
const codex = program.commands.find((c) => c.name() === 'codex');
|
|
56
|
+
const worker = codex?.commands.find((c) => c.name() === 'worker');
|
|
57
|
+
expect(worker).toBeDefined();
|
|
58
|
+
expect(worker?.opts().workspace).toBe('/tmp/ws');
|
|
59
|
+
expect(worker?.opts().once).toBe(true);
|
|
60
|
+
expect(worker?.opts().json).toBe(true);
|
|
61
|
+
|
|
62
|
+
const program2 = buildTestProgram();
|
|
63
|
+
program2.parse(['node', 'pd', 'codex', 'worker', '--status']);
|
|
64
|
+
const worker2 = program2.commands.find((c) => c.name() === 'codex')?.commands.find((c) => c.name() === 'worker');
|
|
65
|
+
expect(worker2?.opts().status).toBe(true);
|
|
66
|
+
// --interval is registered for continuous mode
|
|
67
|
+
const intervalOption = worker?.options.find((o) => o.long === '--interval');
|
|
68
|
+
expect(intervalOption).toBeDefined();
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
describe('codex ingest catch-up handler (functional)', () => {
|
|
73
|
+
let workspaceDir: string;
|
|
74
|
+
let logSpy: ReturnType<typeof vi.spyOn>;
|
|
75
|
+
|
|
76
|
+
beforeEach(() => {
|
|
77
|
+
workspaceDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pd-cli-catchup-'));
|
|
78
|
+
fs.mkdirSync(path.join(workspaceDir, '.pd'), { recursive: true });
|
|
79
|
+
fs.mkdirSync(path.join(workspaceDir, '.state'), { recursive: true });
|
|
80
|
+
fs.writeFileSync(path.join(workspaceDir, '.state', 'trajectory.db'), '');
|
|
81
|
+
const config = getDefaultPdConfig();
|
|
82
|
+
config.features['host.codex'].enabled = true;
|
|
83
|
+
config.features.codex_conversation_ingestion.enabled = false;
|
|
84
|
+
fs.writeFileSync(path.join(workspaceDir, '.pd', 'config.yaml'), JSON.stringify(config));
|
|
85
|
+
logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
afterEach(() => {
|
|
89
|
+
logSpy.mockRestore();
|
|
90
|
+
fs.rmSync(workspaceDir, { recursive: true, force: true });
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('emits exactly one JSON object and skips with reason+nextAction when ingestion is off', { timeout: 20_000 }, async () => {
|
|
94
|
+
const { handleCodexIngestCatchUp } = await import('../../src/commands/codex-ingest-catchup.js');
|
|
95
|
+
await handleCodexIngestCatchUp({ workspace: workspaceDir, json: true });
|
|
96
|
+
expect(logSpy).toHaveBeenCalledTimes(1);
|
|
97
|
+
const report = JSON.parse(logSpy.mock.calls[0]?.[0] as string) as { status: string; reason: string; nextAction: string };
|
|
98
|
+
expect(report.status).toBe('skipped');
|
|
99
|
+
expect(report.reason).toBe('feature_disabled');
|
|
100
|
+
expect(report.nextAction).toContain('codex_conversation_ingestion');
|
|
101
|
+
expect(process.exitCode ?? 0).toBe(0);
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
describe('codex worker handler (functional)', () => {
|
|
106
|
+
let workspaceDir: string;
|
|
107
|
+
let logSpy: ReturnType<typeof vi.spyOn>;
|
|
108
|
+
|
|
109
|
+
beforeEach(() => {
|
|
110
|
+
workspaceDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pd-cli-worker-'));
|
|
111
|
+
fs.mkdirSync(path.join(workspaceDir, '.pd'), { recursive: true });
|
|
112
|
+
fs.mkdirSync(path.join(workspaceDir, '.state'), { recursive: true });
|
|
113
|
+
fs.writeFileSync(path.join(workspaceDir, '.state', 'trajectory.db'), '');
|
|
114
|
+
const config = getDefaultPdConfig();
|
|
115
|
+
config.features['host.codex'].enabled = true;
|
|
116
|
+
config.features.internalization_auto_consumer.enabled = false;
|
|
117
|
+
fs.writeFileSync(path.join(workspaceDir, '.pd', 'config.yaml'), JSON.stringify(config));
|
|
118
|
+
logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
afterEach(() => {
|
|
122
|
+
logSpy.mockRestore();
|
|
123
|
+
process.exitCode = 0;
|
|
124
|
+
fs.rmSync(workspaceDir, { recursive: true, force: true });
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('--once reports paused with reason+nextAction when the consumer flag is off', { timeout: 20_000 }, async () => {
|
|
128
|
+
const { handleCodexWorker } = await import('../../src/commands/codex-worker.js');
|
|
129
|
+
await handleCodexWorker({ workspace: workspaceDir, once: true, json: true });
|
|
130
|
+
expect(logSpy).toHaveBeenCalledTimes(1);
|
|
131
|
+
const report = JSON.parse(logSpy.mock.calls[0]?.[0] as string) as { mode: string; reason: string; nextAction: string };
|
|
132
|
+
expect(report.mode).toBe('paused');
|
|
133
|
+
expect(report.reason).toBe('internalization_auto_consumer_disabled');
|
|
134
|
+
expect(report.nextAction).toContain('pd diagnose');
|
|
135
|
+
expect(process.exitCode ?? 0).toBe(0);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('--json without --once/--status is refused (continuous mode streams, it is not one JSON document)', async () => {
|
|
139
|
+
const { handleCodexWorker } = await import('../../src/commands/codex-worker.js');
|
|
140
|
+
await handleCodexWorker({ workspace: workspaceDir, json: true });
|
|
141
|
+
expect(logSpy).toHaveBeenCalledTimes(1);
|
|
142
|
+
const error = JSON.parse(logSpy.mock.calls[0]?.[0] as string) as { error: string; reason: string; nextAction: string };
|
|
143
|
+
expect(error.error).toBe('cli_contract');
|
|
144
|
+
expect(error.reason).toBe('json_without_once_or_status');
|
|
145
|
+
expect(error.nextAction).toContain('--once');
|
|
146
|
+
expect(process.exitCode).toBe(1);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('--once without --json prints the human-readable cycle report', { timeout: 20_000 }, async () => {
|
|
150
|
+
const { handleCodexWorker } = await import('../../src/commands/codex-worker.js');
|
|
151
|
+
await handleCodexWorker({ workspace: workspaceDir, once: true, json: false });
|
|
152
|
+
// Human-readable form: one multi-line report, no JSON envelope.
|
|
153
|
+
const output = logSpy.mock.calls.map((call) => String(call[0])).join('\n');
|
|
154
|
+
expect(output).toContain('Codex workspace worker');
|
|
155
|
+
expect(output).toContain('mode: paused');
|
|
156
|
+
expect(output).toContain('catch-up:');
|
|
157
|
+
expect(output).toContain('reconcile:');
|
|
158
|
+
expect(output).toContain('next action:');
|
|
159
|
+
expect(process.exitCode ?? 0).toBe(0);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('--status reports manual_action_required for a workspace absent from the install manifest', async () => {
|
|
163
|
+
const { handleCodexWorker } = await import('../../src/commands/codex-worker.js');
|
|
164
|
+
// Execution enabled — the manifest absence is then the deciding fact.
|
|
165
|
+
const config = getDefaultPdConfig();
|
|
166
|
+
config.features['host.codex'].enabled = true;
|
|
167
|
+
config.features.internalization_auto_consumer.enabled = true;
|
|
168
|
+
fs.writeFileSync(path.join(workspaceDir, '.pd', 'config.yaml'), JSON.stringify(config));
|
|
169
|
+
await handleCodexWorker({ workspace: workspaceDir, status: true, json: true });
|
|
170
|
+
expect(logSpy).toHaveBeenCalledTimes(1);
|
|
171
|
+
const report = JSON.parse(logSpy.mock.calls[0]?.[0] as string) as { mode: string; reason: string; registeredInInstallManifest: boolean; nextAction: string };
|
|
172
|
+
expect(report.mode).toBe('manual_action_required');
|
|
173
|
+
expect(report.reason).toBe('workspace_not_in_install_manifest');
|
|
174
|
+
expect(report.registeredInInstallManifest).toBe(false);
|
|
175
|
+
expect(report.nextAction).toContain('pd codex ingest catch-up');
|
|
176
|
+
expect(process.exitCode ?? 0).toBe(0);
|
|
177
|
+
});
|
|
178
|
+
});
|
|
@@ -311,6 +311,46 @@ describe('planConsoleLaunch — reused (healthy console on preferred port)', ()
|
|
|
311
311
|
}
|
|
312
312
|
});
|
|
313
313
|
|
|
314
|
+
it('does not reuse a verified no-auth Console when a token is configured', async () => {
|
|
315
|
+
const server = http.createServer((_req, res) => {
|
|
316
|
+
res.statusCode = 200;
|
|
317
|
+
res.end(JSON.stringify({ success: true, data: { authenticationMode: 'no_auth' } }));
|
|
318
|
+
});
|
|
319
|
+
await new Promise<void>((resolve) => server.listen(0, '127.0.0.1', resolve));
|
|
320
|
+
const addr = server.address();
|
|
321
|
+
if (typeof addr !== 'object' || !addr) throw new Error('no addr');
|
|
322
|
+
try {
|
|
323
|
+
const result = await planConsoleLaunch({
|
|
324
|
+
workspaceDir: '/tmp/anywhere', preferredPort: addr.port,
|
|
325
|
+
host: '127.0.0.1', token: 'configured-token',
|
|
326
|
+
});
|
|
327
|
+
expect(result.status).toBe('refused');
|
|
328
|
+
expect(result.reason).toBe('console_authentication_mode_mismatch');
|
|
329
|
+
} finally {
|
|
330
|
+
await new Promise<void>((resolve) => server.close(resolve));
|
|
331
|
+
}
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
it('fails loud when the configured token is rejected by an existing Console', async () => {
|
|
335
|
+
const server = http.createServer((_req, res) => {
|
|
336
|
+
res.statusCode = 401;
|
|
337
|
+
res.end('unauthorized');
|
|
338
|
+
});
|
|
339
|
+
await new Promise<void>((resolve) => server.listen(0, '127.0.0.1', resolve));
|
|
340
|
+
const addr = server.address();
|
|
341
|
+
if (typeof addr !== 'object' || !addr) throw new Error('no addr');
|
|
342
|
+
try {
|
|
343
|
+
const result = await planConsoleLaunch({
|
|
344
|
+
workspaceDir: '/tmp/anywhere', preferredPort: addr.port,
|
|
345
|
+
host: '127.0.0.1', token: 'wrong-token',
|
|
346
|
+
});
|
|
347
|
+
expect(result.status).toBe('refused');
|
|
348
|
+
expect(result.reason).toBe('console_authentication_failed');
|
|
349
|
+
} finally {
|
|
350
|
+
await new Promise<void>((resolve) => server.close(resolve));
|
|
351
|
+
}
|
|
352
|
+
});
|
|
353
|
+
|
|
314
354
|
it('does NOT classify a non-console responder as reused', async () => {
|
|
315
355
|
// Server returns 200 on any path but with a non-OK status code from /api/health
|
|
316
356
|
const server = http.createServer((req, res) => {
|
|
@@ -387,6 +427,22 @@ describe('probeConsoleHealth', () => {
|
|
|
387
427
|
}
|
|
388
428
|
});
|
|
389
429
|
|
|
430
|
+
it('returns the verified authentication mode from the health contract', async () => {
|
|
431
|
+
const server = http.createServer((_req, res) => {
|
|
432
|
+
res.statusCode = 200;
|
|
433
|
+
res.end(JSON.stringify({ success: true, data: { authenticationMode: 'authenticated' } }));
|
|
434
|
+
});
|
|
435
|
+
await new Promise<void>((resolve) => server.listen(0, '127.0.0.1', resolve));
|
|
436
|
+
const addr = server.address();
|
|
437
|
+
if (typeof addr !== 'object' || !addr) throw new Error('no addr');
|
|
438
|
+
try {
|
|
439
|
+
const h = await probeConsoleHealth({ host: '127.0.0.1', port: addr.port, token: 'token' });
|
|
440
|
+
expect(h).toMatchObject({ healthy: true, authenticationMode: 'authenticated' });
|
|
441
|
+
} finally {
|
|
442
|
+
await new Promise<void>((resolve) => server.close(resolve));
|
|
443
|
+
}
|
|
444
|
+
});
|
|
445
|
+
|
|
390
446
|
it('returns healthy=false with reason for a server that returns 500', async () => {
|
|
391
447
|
const server = http.createServer((req, res) => {
|
|
392
448
|
res.statusCode = 500;
|
|
@@ -491,8 +547,11 @@ describe('CLI command wiring (pd console open)', () => {
|
|
|
491
547
|
let cliPath: string;
|
|
492
548
|
let workspaceRoot: string;
|
|
493
549
|
let tmp: string;
|
|
550
|
+
let originalConsoleToken: string | undefined;
|
|
494
551
|
|
|
495
552
|
beforeEach(() => {
|
|
553
|
+
originalConsoleToken = process.env.PD_CONSOLE_TOKEN;
|
|
554
|
+
delete process.env.PD_CONSOLE_TOKEN;
|
|
496
555
|
workspaceRoot = path.resolve(__dirname, '../../../..');
|
|
497
556
|
cliPath = path.join(workspaceRoot, 'packages', 'pd-cli', 'dist', 'index.js');
|
|
498
557
|
tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'pd-console-open-test-'));
|
|
@@ -520,7 +579,7 @@ describe('CLI command wiring (pd console open)', () => {
|
|
|
520
579
|
const server = http.createServer((req, res) => {
|
|
521
580
|
if (req.url === '/api/health') {
|
|
522
581
|
res.writeHead(200, {'Content-Type': 'application/json'});
|
|
523
|
-
res.end(JSON.stringify({success: true}));
|
|
582
|
+
res.end(JSON.stringify({success: true, data: {authenticationMode: 'no_auth'}}));
|
|
524
583
|
} else {
|
|
525
584
|
res.writeHead(404);
|
|
526
585
|
res.end();
|
|
@@ -533,6 +592,8 @@ describe('CLI command wiring (pd console open)', () => {
|
|
|
533
592
|
});
|
|
534
593
|
|
|
535
594
|
afterEach(() => {
|
|
595
|
+
if (originalConsoleToken === undefined) delete process.env.PD_CONSOLE_TOKEN;
|
|
596
|
+
else process.env.PD_CONSOLE_TOKEN = originalConsoleToken;
|
|
536
597
|
delete process.env.__PD_CONSOLE_TEST_FAKE_HOME;
|
|
537
598
|
try { fs.rmSync(tmp, { recursive: true, force: true }); } catch { /* ignore */ }
|
|
538
599
|
});
|
|
@@ -598,13 +659,32 @@ describe('CLI command wiring (pd console open)', () => {
|
|
|
598
659
|
}
|
|
599
660
|
}, 20_000);
|
|
600
661
|
|
|
662
|
+
it('kills and refuses a fresh Console whose reported authentication mode mismatches the configured token', async () => {
|
|
663
|
+
process.env.PD_CONSOLE_TOKEN = 'configured-token';
|
|
664
|
+
let run: CliJsonRun | undefined;
|
|
665
|
+
try {
|
|
666
|
+
run = await runPdUntilJson(
|
|
667
|
+
['console', 'open', '--workspace', tmp, '--port', '49391', '--json', '--no-browser'],
|
|
668
|
+
workspaceRoot,
|
|
669
|
+
);
|
|
670
|
+
if (!isRecord(run.parsed)) throw new Error('CLI JSON output was not an object');
|
|
671
|
+
expect(run.parsed.status).toBe('refused');
|
|
672
|
+
expect(run.parsed.reason).toBe('console_authentication_mode_mismatch');
|
|
673
|
+
expect(run.parsed).not.toHaveProperty('serverPid');
|
|
674
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
675
|
+
expect(await isPortInUse('127.0.0.1', 49391)).toBe(false);
|
|
676
|
+
} finally {
|
|
677
|
+
await teardownCliTree(run);
|
|
678
|
+
}
|
|
679
|
+
}, 20_000);
|
|
680
|
+
|
|
601
681
|
it('pd console open --json reused path does NOT include serverPid (PRI-526)', async () => {
|
|
602
682
|
// Stand up a fake healthy console in-process, then point the CLI at its
|
|
603
683
|
// port: planConsoleLaunch probes /api/health → 200 → reused, no spawn.
|
|
604
684
|
const server = http.createServer((req, res) => {
|
|
605
685
|
if (req.url === '/api/health') {
|
|
606
686
|
res.statusCode = 200;
|
|
607
|
-
res.end(JSON.stringify({ success: true }));
|
|
687
|
+
res.end(JSON.stringify({ success: true, data: { authenticationMode: 'no_auth' } }));
|
|
608
688
|
return;
|
|
609
689
|
}
|
|
610
690
|
res.statusCode = 404;
|
|
@@ -622,6 +702,7 @@ describe('CLI command wiring (pd console open)', () => {
|
|
|
622
702
|
if (!isRecord(run.parsed)) throw new Error('CLI JSON output was not an object');
|
|
623
703
|
expect(run.parsed.status).toBe('reused');
|
|
624
704
|
expect(run.parsed.reused).toBe(true);
|
|
705
|
+
expect(run.parsed.authenticationMode).toBe('no_auth');
|
|
625
706
|
expect(run.parsed).not.toHaveProperty('serverPid');
|
|
626
707
|
} finally {
|
|
627
708
|
await teardownCliTree(run);
|
|
@@ -39,6 +39,10 @@ vi.mock('@principles/core/runtime-v2', () => ({
|
|
|
39
39
|
}),
|
|
40
40
|
auditCandidateLedgerConsistency: mockAuditCandidateLedgerConsistency,
|
|
41
41
|
resolveOutputLanguage: vi.fn().mockReturnValue({ outputLanguage: 'zh-CN' }),
|
|
42
|
+
// host-runtime's workspace-telemetry-emitter (transitively imported via the
|
|
43
|
+
// host-runtime barrel) extends this class at module-load time — the mock
|
|
44
|
+
// must provide a constructible base or the import chain throws.
|
|
45
|
+
StoreEventEmitter: class {},
|
|
42
46
|
}));
|
|
43
47
|
|
|
44
48
|
// PRI-443 Phase 5: getLedgerFilePathPublic now imported from
|