@principles/pd-cli 1.147.6 → 1.147.8
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-quarantine.d.ts +12 -0
- package/dist/commands/codex-ingest-quarantine.d.ts.map +1 -0
- package/dist/commands/codex-ingest-quarantine.js +116 -0
- package/dist/commands/codex-ingest-quarantine.js.map +1 -0
- package/dist/commands/codex-setup.d.ts +46 -0
- package/dist/commands/codex-setup.d.ts.map +1 -0
- package/dist/commands/codex-setup.js +423 -0
- package/dist/commands/codex-setup.js.map +1 -0
- package/dist/commands/health-codex.d.ts.map +1 -1
- package/dist/commands/health-codex.js +350 -67
- package/dist/commands/health-codex.js.map +1 -1
- package/dist/index.js +51 -0
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/commands/codex-ingest-quarantine.ts +127 -0
- package/src/commands/codex-setup.ts +476 -0
- package/src/commands/health-codex.ts +396 -69
- package/src/index.ts +53 -0
- package/tests/commands/codex-consent-reversibility.steps.test.ts +299 -0
- package/tests/commands/codex-ingest-quarantine.test.ts +136 -0
- package/tests/commands/codex-setup.test.ts +297 -0
- package/tests/commands/codex-worker-registration.test.ts +29 -0
- package/tests/commands/health-codex.test.ts +278 -172
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pd codex setup command tests (Slice D consent UX).
|
|
3
|
+
*
|
|
4
|
+
* Real-filesystem integration style: every test builds a disposable
|
|
5
|
+
* workspace (.pd/config.yaml included) and exercises the production config
|
|
6
|
+
* editor + consent store — no fs mocks, so round-trip verification, comment
|
|
7
|
+
* preservation, and atomic-rename behavior are proven, not assumed.
|
|
8
|
+
*
|
|
9
|
+
* Config fixtures are built from the production `getDefaultPdConfig()` (same
|
|
10
|
+
* source as `pd runtime init`) so they pass the full validator.
|
|
11
|
+
*
|
|
12
|
+
* Covers:
|
|
13
|
+
* - --show-disclosure: frozen text printed (zh + en), zero mutation.
|
|
14
|
+
* - --accept: consent recorded granted BEFORE flag flip; config.yaml gains
|
|
15
|
+
* features.codex_conversation_ingestion.enabled=true; comments preserved;
|
|
16
|
+
* production loader sees the flag enabled.
|
|
17
|
+
* - --decline: consent declined; a hand-enabled flag is regularized back off;
|
|
18
|
+
* host.codex and other flags untouched.
|
|
19
|
+
* - cli-4: --accept --decline mutex refusal.
|
|
20
|
+
* - cli-5: refusals (no workspace config, malformed config) mutate nothing.
|
|
21
|
+
* - decision_required: --json without explicit decision; non-TTY interactive.
|
|
22
|
+
*/
|
|
23
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
24
|
+
import fs from 'node:fs';
|
|
25
|
+
import os from 'node:os';
|
|
26
|
+
import path from 'node:path';
|
|
27
|
+
import * as yaml from 'js-yaml';
|
|
28
|
+
import { getDefaultPdConfig } from '@principles/core/runtime-v2';
|
|
29
|
+
|
|
30
|
+
let logLines: string[] = [];
|
|
31
|
+
let savedExitCode: number | undefined;
|
|
32
|
+
|
|
33
|
+
const HEADER_COMMENT = '# PD Runtime Configuration — single source of truth (.pd/config.yaml, ADR-0016)\n# Edited by the Owner. Comments must survive consent-driven flag edits.\n';
|
|
34
|
+
|
|
35
|
+
const CORE = getDefaultPdConfig();
|
|
36
|
+
// Everything except `features` — features blocks are appended per-test so the
|
|
37
|
+
// editor's insert/replace/append paths are all exercised on real content.
|
|
38
|
+
// Feature override keys are FLAT flag ids ('host.codex' is a literal key),
|
|
39
|
+
// and every override requires category+enabled (validatePdConfig).
|
|
40
|
+
const { features: _coreFeatures, ...coreWithoutFeatures } = CORE as unknown as Record<string, unknown>;
|
|
41
|
+
const PRELUDE = HEADER_COMMENT + yaml.dump(
|
|
42
|
+
{ ...coreWithoutFeatures, workspace: { default: '%WS%' } },
|
|
43
|
+
{ indent: 2, lineWidth: 200, noRefs: true },
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
function makeWorkspace(configYaml: string | null): string {
|
|
47
|
+
const ws = fs.mkdtempSync(path.join(os.tmpdir(), 'pd-codex-setup-'));
|
|
48
|
+
if (configYaml !== null) {
|
|
49
|
+
fs.mkdirSync(path.join(ws, '.pd'), { recursive: true });
|
|
50
|
+
fs.writeFileSync(path.join(ws, '.pd', 'config.yaml'), renderConfig(ws, configYaml), 'utf8');
|
|
51
|
+
}
|
|
52
|
+
return ws;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function renderConfig(ws: string, template: string): string {
|
|
56
|
+
// workspace.default must be an absolute path (validatePdConfig); forward
|
|
57
|
+
// slashes keep the YAML plain scalar portable across platforms.
|
|
58
|
+
return template.replaceAll('%WS%', ws.replace(/\\/g, '/'));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function run(options: Record<string, unknown>): Promise<{ reports: string[]; json: unknown[] }> {
|
|
62
|
+
const { handleCodexSetup } = await import('../../src/commands/codex-setup.js');
|
|
63
|
+
await handleCodexSetup(options as never);
|
|
64
|
+
const json: unknown[] = [];
|
|
65
|
+
const reports: string[] = [];
|
|
66
|
+
for (const line of logLines) {
|
|
67
|
+
if (line.startsWith('{')) {
|
|
68
|
+
try {
|
|
69
|
+
json.push(JSON.parse(line));
|
|
70
|
+
} catch {
|
|
71
|
+
reports.push(line);
|
|
72
|
+
}
|
|
73
|
+
} else {
|
|
74
|
+
reports.push(line);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return { reports, json };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function readConsent(ws: string): Record<string, unknown> | null {
|
|
81
|
+
const p = path.join(ws, '.pd', 'codex-ingestion-consent.json');
|
|
82
|
+
if (!fs.existsSync(p)) return null;
|
|
83
|
+
return JSON.parse(fs.readFileSync(p, 'utf8')) as Record<string, unknown>;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function readConfig(ws: string): string {
|
|
87
|
+
return fs.readFileSync(path.join(ws, '.pd', 'config.yaml'), 'utf8');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
beforeEach(() => {
|
|
91
|
+
logLines = [];
|
|
92
|
+
savedExitCode = process.exitCode;
|
|
93
|
+
process.exitCode = undefined;
|
|
94
|
+
vi.spyOn(console, 'log').mockImplementation((...args: unknown[]) => {
|
|
95
|
+
logLines.push(args.map(String).join(' '));
|
|
96
|
+
});
|
|
97
|
+
// --show-disclosure writes via process.stdout.write; capture it too.
|
|
98
|
+
vi.spyOn(process.stdout, 'write').mockImplementation(((chunk: unknown) => {
|
|
99
|
+
logLines.push(String(chunk));
|
|
100
|
+
return true;
|
|
101
|
+
}) as typeof process.stdout.write);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
afterEach(() => {
|
|
105
|
+
vi.restoreAllMocks();
|
|
106
|
+
process.exitCode = savedExitCode;
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
describe('pd codex setup — show-disclosure', () => {
|
|
110
|
+
it('prints the frozen Chinese SSoT by default and English with --lang en, mutating nothing', async () => {
|
|
111
|
+
const ws = makeWorkspace(PRELUDE + 'features: {}\n');
|
|
112
|
+
await run({ workspace: ws, showDisclosure: true });
|
|
113
|
+
let all = logLines.join('\n');
|
|
114
|
+
expect(all).toContain('对话观察与治理闭环');
|
|
115
|
+
expect(all).toContain('默认关闭。只有你在看到本说明后明确选择开启才会生效');
|
|
116
|
+
await run({ workspace: ws, showDisclosure: true, lang: 'en' });
|
|
117
|
+
all = logLines.join('\n');
|
|
118
|
+
expect(all).toContain('Off by default');
|
|
119
|
+
expect(readConsent(ws)).toBeNull();
|
|
120
|
+
expect(readConfig(ws)).toBe(renderConfig(ws, PRELUDE + 'features: {}\n'));
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
describe('pd codex setup — accept flow', () => {
|
|
125
|
+
it('records granted consent, enables the flag, preserves comments, and round-trips', async () => {
|
|
126
|
+
const ws = makeWorkspace(PRELUDE + 'features: {}\n');
|
|
127
|
+
const { json } = await run({ workspace: ws, accept: true, json: true });
|
|
128
|
+
expect(json).toHaveLength(1);
|
|
129
|
+
const report = json[0] as Record<string, unknown>;
|
|
130
|
+
expect(report.status, JSON.stringify(report)).toBe('ok');
|
|
131
|
+
expect(report.decision).toBe('granted');
|
|
132
|
+
expect((report.ingestionFlag as Record<string, unknown>).enabled).toBe(true);
|
|
133
|
+
expect(report.disclosureVersion).toBe('g2a-2026-08-28');
|
|
134
|
+
|
|
135
|
+
const consent = readConsent(ws);
|
|
136
|
+
expect(consent?.decision).toBe('granted');
|
|
137
|
+
expect(consent?.decidedVia).toBe('pd_codex_setup');
|
|
138
|
+
|
|
139
|
+
const configAfter = readConfig(ws);
|
|
140
|
+
expect(configAfter).toContain('Comments must survive consent-driven flag edits.');
|
|
141
|
+
expect(configAfter).toContain(' codex_conversation_ingestion:');
|
|
142
|
+
|
|
143
|
+
// Production loader is the round-trip authority.
|
|
144
|
+
const { computeFeatureFlagsFromConfig, isFeatureEnabled } = await import('@principles/core/runtime-v2');
|
|
145
|
+
const { loadPdConfigForPlugin } = await import('@principles/host-runtime');
|
|
146
|
+
const result = loadPdConfigForPlugin(ws);
|
|
147
|
+
expect(result.ok).toBe(true);
|
|
148
|
+
expect(isFeatureEnabled(computeFeatureFlagsFromConfig(result.effective), 'codex_conversation_ingestion')).toBe(true);
|
|
149
|
+
expect(process.exitCode).toBeUndefined();
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('replaces an empty features mapping with the enabled flag block', async () => {
|
|
153
|
+
const ws = makeWorkspace(PRELUDE + 'features: {}\n');
|
|
154
|
+
const { json } = await run({ workspace: ws, accept: true, json: true });
|
|
155
|
+
expect((json[0] as Record<string, unknown>).status, JSON.stringify(json[0])).toBe('ok');
|
|
156
|
+
expect(readConfig(ws)).toMatch(/features:\r?\n {2}codex_conversation_ingestion:\r?\n {4}category: quiet\r?\n {4}enabled: true/);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('inserts the flag into an existing features block that has other overrides', async () => {
|
|
160
|
+
const withOtherFlags = PRELUDE + 'features:\n prompt:\n category: core\n enabled: true\n internalization_auto_consumer:\n category: core\n enabled: true\n';
|
|
161
|
+
const ws = makeWorkspace(withOtherFlags);
|
|
162
|
+
const { json } = await run({ workspace: ws, accept: true, json: true });
|
|
163
|
+
expect((json[0] as Record<string, unknown>).status, JSON.stringify(json[0])).toBe('ok');
|
|
164
|
+
const configAfter = readConfig(ws);
|
|
165
|
+
expect(configAfter).toMatch(/ {2}codex_conversation_ingestion:\r?\n {4}category: quiet\r?\n {4}enabled: true/);
|
|
166
|
+
// Sibling overrides survive byte-for-byte.
|
|
167
|
+
expect(configAfter).toContain(' internalization_auto_consumer:');
|
|
168
|
+
expect(configAfter).toContain(' prompt:');
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
describe('pd codex setup — decline flow', () => {
|
|
173
|
+
it('records revoked consent and regularizes a hand-enabled flag back off', async () => {
|
|
174
|
+
const handEnabled = PRELUDE + 'features:\n host.codex:\n category: core\n enabled: true\n codex_conversation_ingestion:\n category: quiet\n enabled: true # hand-edited without consent\n';
|
|
175
|
+
const ws = makeWorkspace(handEnabled);
|
|
176
|
+
const { json } = await run({ workspace: ws, decline: true, json: true });
|
|
177
|
+
const report = json[0] as Record<string, unknown>;
|
|
178
|
+
expect(report.status, JSON.stringify(report)).toBe('ok');
|
|
179
|
+
expect(report.decision).toBe('revoked');
|
|
180
|
+
expect((report.ingestionFlag as Record<string, unknown>).enabled).toBe(false);
|
|
181
|
+
expect(report.nextAction).toContain('No transcript');
|
|
182
|
+
|
|
183
|
+
expect(readConsent(ws)?.decision).toBe('revoked');
|
|
184
|
+
const configAfter = readConfig(ws);
|
|
185
|
+
// The inline comment survives; only the value flips.
|
|
186
|
+
expect(configAfter).toContain('enabled: false # hand-edited without consent');
|
|
187
|
+
// host.codex governance is untouched by the decline.
|
|
188
|
+
expect(configAfter).toContain('category: core');
|
|
189
|
+
expect(configAfter).toContain('host.codex:');
|
|
190
|
+
expect(process.exitCode).toBeUndefined();
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
describe('pd codex setup — consent transition state machine (review round 2)', () => {
|
|
195
|
+
it('Case 1: consent write ok + flag enable ok ⇒ state=granted, runtime enabled=true', async () => {
|
|
196
|
+
const ws = makeWorkspace(PRELUDE + 'features: {}\n');
|
|
197
|
+
const { json } = await run({ workspace: ws, accept: true, json: true });
|
|
198
|
+
const report = json[0] as Record<string, unknown>;
|
|
199
|
+
expect(report.status, JSON.stringify(report)).toBe('ok');
|
|
200
|
+
expect(report.decision).toBe('granted');
|
|
201
|
+
expect(report.consentState).toBe('granted');
|
|
202
|
+
expect((report.ingestionFlag as Record<string, unknown>).enabled).toBe(true);
|
|
203
|
+
expect(readConsent(ws)?.decision).toBe('granted');
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it('Case 2: flag enable fails ⇒ state != granted, runtime enabled=false, failure reason recorded and surfaced', async () => {
|
|
207
|
+
const ws = makeWorkspace(PRELUDE + 'features: {}\n');
|
|
208
|
+
// Break the config AFTER the initial validation so the round-trip
|
|
209
|
+
// verification inside the flag editor fails: consent went to 'pending'
|
|
210
|
+
// first, then the activation fails ⇒ terminal state 'failed' with the
|
|
211
|
+
// reason, and the flag stays off (no granted/disabled mismatch).
|
|
212
|
+
const original = await import('../../src/commands/codex-setup.js');
|
|
213
|
+
const brokenLoader = await import('@principles/host-runtime');
|
|
214
|
+
const realLoad = brokenLoader.loadPdConfigForPlugin;
|
|
215
|
+
let loadCalls = 0;
|
|
216
|
+
const spy = vi.spyOn(brokenLoader, 'loadPdConfigForPlugin').mockImplementation((dir: string) => {
|
|
217
|
+
loadCalls += 1;
|
|
218
|
+
// Call 1 is the handler's pre-check (ok); call 2 is the round-trip
|
|
219
|
+
// verification INSIDE setCodexConversationIngestionFlag — break it there.
|
|
220
|
+
if (loadCalls === 1) return realLoad(dir);
|
|
221
|
+
return {
|
|
222
|
+
ok: false,
|
|
223
|
+
effective: realLoad(dir).effective,
|
|
224
|
+
source: 'malformed',
|
|
225
|
+
configPath: dir,
|
|
226
|
+
warnings: [],
|
|
227
|
+
errors: [{ path: 'features', reason: 'injected round-trip failure (test)', nextAction: 'fix yaml' }],
|
|
228
|
+
};
|
|
229
|
+
});
|
|
230
|
+
try {
|
|
231
|
+
const { json } = await run({ workspace: ws, accept: true, json: true });
|
|
232
|
+
const report = json[0] as Record<string, unknown>;
|
|
233
|
+
expect(report.status, JSON.stringify(report)).toBe('degraded');
|
|
234
|
+
expect(report.decision).toBe('failed');
|
|
235
|
+
expect(report.consentState).toBe('failed');
|
|
236
|
+
expect(report.reason).toContain('injected round-trip failure');
|
|
237
|
+
expect((report.ingestionFlag as Record<string, unknown>).enabled).toBe(false);
|
|
238
|
+
// NOT granted — the whole point of the state machine.
|
|
239
|
+
expect(report.consentState).not.toBe('granted');
|
|
240
|
+
const consent = readConsent(ws);
|
|
241
|
+
expect(consent?.decision).toBe('failed');
|
|
242
|
+
expect(String(consent?.failureReason)).toContain('injected round-trip failure');
|
|
243
|
+
expect(process.exitCode).toBe(1);
|
|
244
|
+
} finally {
|
|
245
|
+
spy.mockRestore();
|
|
246
|
+
void original;
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it('re-running accept after a failed attempt reaches granted (state machine recovers)', async () => {
|
|
251
|
+
const ws = makeWorkspace(PRELUDE + 'features: {}\n');
|
|
252
|
+
// Simulate the failed state on disk, then accept again.
|
|
253
|
+
fs.mkdirSync(path.join(ws, '.pd'), { recursive: true });
|
|
254
|
+
fs.writeFileSync(path.join(ws, '.pd', 'codex-ingestion-consent.json'), JSON.stringify({
|
|
255
|
+
decision: 'failed', disclosureVersion: 'g2a-2026-08-28', decidedAt: new Date().toISOString(),
|
|
256
|
+
decidedVia: 'pd_codex_setup', failureReason: 'flag activation failed: prior run', schemaVersion: '2',
|
|
257
|
+
}), 'utf8');
|
|
258
|
+
const { json } = await run({ workspace: ws, accept: true, json: true });
|
|
259
|
+
const report = json[0] as Record<string, unknown>;
|
|
260
|
+
expect(report.status, JSON.stringify(report)).toBe('ok');
|
|
261
|
+
expect(report.decision).toBe('granted');
|
|
262
|
+
expect(readConsent(ws)?.decision).toBe('granted');
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
describe('pd codex setup — refusals mutate nothing (cli-4/cli-5/cli-6)', () => {
|
|
267
|
+
it('refuses --accept together with --decline', async () => {
|
|
268
|
+
const ws = makeWorkspace(PRELUDE + 'features: {}\n');
|
|
269
|
+
const { json } = await run({ workspace: ws, accept: true, decline: true, json: true });
|
|
270
|
+
expect((json[0] as Record<string, unknown>).reason).toBe('accept_decline_mutex');
|
|
271
|
+
expect(readConsent(ws)).toBeNull();
|
|
272
|
+
expect(process.exitCode).toBe(1);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it('refuses when the workspace has no .pd/config.yaml', async () => {
|
|
276
|
+
const ws = makeWorkspace(null);
|
|
277
|
+
const { json } = await run({ workspace: ws, accept: true, json: true });
|
|
278
|
+
expect((json[0] as Record<string, unknown>).reason).toBe('workspace_config_not_found');
|
|
279
|
+
expect(fs.existsSync(path.join(ws, '.pd'))).toBe(false);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it('refuses a malformed config without recording consent', async () => {
|
|
283
|
+
const ws = makeWorkspace(PRELUDE + 'features: [not, a, mapping]\n');
|
|
284
|
+
const { json } = await run({ workspace: ws, accept: true, json: true });
|
|
285
|
+
expect((json[0] as Record<string, unknown>).reason).toMatch(/^workspace_config_malformed/);
|
|
286
|
+
expect(readConsent(ws)).toBeNull();
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
it('requires an explicit decision in --json mode and without a TTY', async () => {
|
|
290
|
+
const ws = makeWorkspace(PRELUDE + 'features: {}\n');
|
|
291
|
+
const viaJson = await run({ workspace: ws, json: true });
|
|
292
|
+
expect((viaJson.json[0] as Record<string, unknown>).reason).toBe('decision_required');
|
|
293
|
+
const viaNonTty = await run({ workspace: ws });
|
|
294
|
+
expect(viaNonTty.reports.join('\n')).toContain('decision_required');
|
|
295
|
+
expect(readConsent(ws)).toBeNull();
|
|
296
|
+
});
|
|
297
|
+
});
|
|
@@ -18,6 +18,16 @@ function buildTestProgram(): Command {
|
|
|
18
18
|
const program = new Command();
|
|
19
19
|
const codex = program.command('codex');
|
|
20
20
|
codex.command('reconcile');
|
|
21
|
+
// PRI-625 Slice D: consent UX command (mirrors src/index.ts registration).
|
|
22
|
+
codex
|
|
23
|
+
.command('setup')
|
|
24
|
+
.option('-w, --workspace <path>', 'Workspace directory')
|
|
25
|
+
.option('--accept', 'Explicitly accept after the disclosure has been presented (non-interactive)')
|
|
26
|
+
.option('--decline', 'Explicitly decline; the ingestion flag stays off and no transcript is ever read')
|
|
27
|
+
.option('--show-disclosure', 'Print the frozen disclosure text (zh default, --lang en) and exit without mutating anything')
|
|
28
|
+
.option('--lang <zh|en>', 'Disclosure language for presentation')
|
|
29
|
+
.option('--json', 'Output raw JSON (decision must be explicit: --accept or --decline)')
|
|
30
|
+
.action(() => {});
|
|
21
31
|
const ingest = codex.command('ingest');
|
|
22
32
|
ingest
|
|
23
33
|
.command('catch-up')
|
|
@@ -36,6 +46,25 @@ function buildTestProgram(): Command {
|
|
|
36
46
|
return program;
|
|
37
47
|
}
|
|
38
48
|
|
|
49
|
+
describe('codex Slice D setup command registration (cli-7)', () => {
|
|
50
|
+
it('parses codex setup flags including --accept/--decline/--show-disclosure', () => {
|
|
51
|
+
const program = buildTestProgram();
|
|
52
|
+
program.parse(['node', 'pd', 'codex', 'setup', '--workspace', '/tmp/ws', '--accept', '--lang', 'en']);
|
|
53
|
+
const codex = program.commands.find((c) => c.name() === 'codex');
|
|
54
|
+
const setup = codex?.commands.find((c) => c.name() === 'setup');
|
|
55
|
+
expect(setup).toBeDefined();
|
|
56
|
+
expect(setup?.opts().workspace).toBe('/tmp/ws');
|
|
57
|
+
expect(setup?.opts().accept).toBe(true);
|
|
58
|
+
expect(setup?.opts().decline).toBeUndefined();
|
|
59
|
+
expect(setup?.opts().lang).toBe('en');
|
|
60
|
+
|
|
61
|
+
const program2 = buildTestProgram();
|
|
62
|
+
program2.parse(['node', 'pd', 'codex', 'setup', '--show-disclosure']);
|
|
63
|
+
const setup2 = program2.commands.find((c) => c.name() === 'codex')?.commands.find((c) => c.name() === 'setup');
|
|
64
|
+
expect(setup2?.opts().showDisclosure).toBe(true);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
39
68
|
describe('codex Slice C command registration (cli-7)', () => {
|
|
40
69
|
it('parses codex ingest catch-up flags', () => {
|
|
41
70
|
const program = buildTestProgram();
|