@principles/pd-cli 1.135.1 → 1.136.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/demo-story-a.d.ts +2 -0
- package/dist/commands/demo-story-a.d.ts.map +1 -1
- package/dist/commands/demo-story-a.js +41 -0
- package/dist/commands/demo-story-a.js.map +1 -1
- package/dist/commands/runtime-activation.d.ts.map +1 -1
- package/dist/commands/runtime-activation.js +36 -7
- package/dist/commands/runtime-activation.js.map +1 -1
- package/dist/commands/runtime-compatibility-scan.d.ts +32 -0
- package/dist/commands/runtime-compatibility-scan.d.ts.map +1 -0
- package/dist/commands/runtime-compatibility-scan.js +94 -0
- package/dist/commands/runtime-compatibility-scan.js.map +1 -0
- package/dist/commands/runtime-internalization-enqueue-successors.d.ts.map +1 -1
- package/dist/commands/runtime-internalization-enqueue-successors.js +48 -0
- package/dist/commands/runtime-internalization-enqueue-successors.js.map +1 -1
- package/dist/commands/runtime-internalization-retry.d.ts +38 -0
- package/dist/commands/runtime-internalization-retry.d.ts.map +1 -0
- package/dist/commands/runtime-internalization-retry.js +143 -0
- package/dist/commands/runtime-internalization-retry.js.map +1 -0
- package/dist/index.js +15 -0
- package/dist/index.js.map +1 -1
- package/dist/services/__tests__/evaluator-runner-deps.test.js +19 -7
- package/dist/services/__tests__/evaluator-runner-deps.test.js.map +1 -1
- package/dist/services/rulehost-pipeline-runner.d.ts.map +1 -1
- package/dist/services/rulehost-pipeline-runner.js +6 -2
- package/dist/services/rulehost-pipeline-runner.js.map +1 -1
- package/package.json +1 -1
- package/src/commands/demo-story-a.ts +44 -0
- package/src/commands/runtime-activation.ts +38 -6
- package/src/commands/runtime-compatibility-scan.ts +105 -0
- package/src/commands/runtime-internalization-enqueue-successors.ts +48 -0
- package/src/commands/runtime-internalization-retry.ts +163 -0
- package/src/index.ts +17 -0
- package/src/services/__tests__/evaluator-runner-deps.test.ts +20 -8
- package/src/services/rulehost-pipeline-runner.ts +5 -2
- package/tests/commands/cli-command-tree.test.ts +15 -0
- package/tests/commands/demo-story-a.test.ts +63 -0
- package/tests/commands/runtime-compatibility-scan.test.ts +146 -0
- package/tests/commands/runtime-internalization-retry-owner-authority.test.ts +431 -0
- package/tests/e2e/cross-package-acceptance.test.ts +2 -2
- package/tests/services/demo-rule-compiler.test.ts +2 -2
|
@@ -100,6 +100,8 @@ function createMockStateManager(): {
|
|
|
100
100
|
} {
|
|
101
101
|
const createdTasks: TaskRecord[] = [];
|
|
102
102
|
const stateManager = {
|
|
103
|
+
getTask: vi.fn(async (taskId: string) =>
|
|
104
|
+
createdTasks.find((t) => t.taskId === taskId) ?? null),
|
|
103
105
|
createTask: vi.fn(async (record: Omit<TaskRecord, 'createdAt' | 'updatedAt'>) => {
|
|
104
106
|
const now = new Date().toISOString();
|
|
105
107
|
const task: TaskRecord = { ...record, createdAt: now, updatedAt: now };
|
|
@@ -185,7 +187,7 @@ describe('PRI-510 (DEFECT-004): createEvaluatorRunnerDeps wires repair loop into
|
|
|
185
187
|
expect(deps.isRepairLoopEnabled()).toBe(false);
|
|
186
188
|
});
|
|
187
189
|
|
|
188
|
-
it('flag absent in config → isRepairLoopEnabled() returns
|
|
190
|
+
it('flag absent in config → isRepairLoopEnabled() returns true (P0-D: default-on since core-loop closure)', () => {
|
|
189
191
|
const workspaceDir = createTempWorkspace(null);
|
|
190
192
|
tmpWorkspaces.push(workspaceDir);
|
|
191
193
|
const { stateManager } = createMockStateManager();
|
|
@@ -201,10 +203,11 @@ describe('PRI-510 (DEFECT-004): createEvaluatorRunnerDeps wires repair loop into
|
|
|
201
203
|
|
|
202
204
|
expect(typeof deps.isRepairLoopEnabled).toBe('function');
|
|
203
205
|
if (typeof deps.isRepairLoopEnabled !== 'function') throw new Error('isRepairLoopEnabled missing');
|
|
204
|
-
|
|
206
|
+
// 契约变更 (2026-08-18, INV-02): registry 默认 ON;flag 缺省 = 默认生效
|
|
207
|
+
expect(deps.isRepairLoopEnabled()).toBe(true);
|
|
205
208
|
});
|
|
206
209
|
|
|
207
|
-
it('malformed config → isRepairLoopEnabled()
|
|
210
|
+
it('malformed config → isRepairLoopEnabled() falls back to registry defaults (default-on), never throws', () => {
|
|
208
211
|
// CodeQL: use mkdtempSync for atomic, unpredictable temp dir creation.
|
|
209
212
|
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pd-pri-510-malformed-'));
|
|
210
213
|
tmpWorkspaces.push(tmpDir);
|
|
@@ -223,10 +226,11 @@ describe('PRI-510 (DEFECT-004): createEvaluatorRunnerDeps wires repair loop into
|
|
|
223
226
|
workspaceDir: tmpDir,
|
|
224
227
|
});
|
|
225
228
|
|
|
226
|
-
// Malformed config must NOT throw —
|
|
229
|
+
// Malformed config must NOT throw — falls back to registry defaults
|
|
230
|
+
// (P0-D: evaluator_artificer_repair_loop default-on since core-loop closure).
|
|
227
231
|
expect(typeof deps.isRepairLoopEnabled).toBe('function');
|
|
228
232
|
if (typeof deps.isRepairLoopEnabled !== 'function') throw new Error('isRepairLoopEnabled missing');
|
|
229
|
-
expect(deps.isRepairLoopEnabled()).toBe(
|
|
233
|
+
expect(deps.isRepairLoopEnabled()).toBe(true);
|
|
230
234
|
});
|
|
231
235
|
|
|
232
236
|
it('seedArtificerRepairTask → creates artificer task with repairPayload in diagnosticJson (rc-1, rc-6)', async () => {
|
|
@@ -286,7 +290,7 @@ describe('PRI-510 (DEFECT-004): createEvaluatorRunnerDeps wires repair loop into
|
|
|
286
290
|
expect(meta.inputArtifactRefs).toEqual(params.inheritedInputArtifactRefs);
|
|
287
291
|
});
|
|
288
292
|
|
|
289
|
-
it('seedArtificerRepairTask →
|
|
293
|
+
it('seedArtificerRepairTask → deterministic id + replay reuse (P0-4); 不同 iteration 不同 id (rc-7)', async () => {
|
|
290
294
|
const workspaceDir = createTempWorkspace(true);
|
|
291
295
|
tmpWorkspaces.push(workspaceDir);
|
|
292
296
|
const { stateManager } = createMockStateManager();
|
|
@@ -303,9 +307,17 @@ describe('PRI-510 (DEFECT-004): createEvaluatorRunnerDeps wires repair loop into
|
|
|
303
307
|
|
|
304
308
|
if (typeof deps.seedArtificerRepairTask !== 'function') throw new Error('seedArtificerRepairTask missing');
|
|
305
309
|
const id1 = await deps.seedArtificerRepairTask(params);
|
|
310
|
+
// P0-4: 同一 evaluator+iteration 的重放 (consumer 重复周期 / crash 恢复)
|
|
311
|
+
// reuse 同一确定性 id,不重复创建
|
|
312
|
+
expect(id1).toBe(`artificer-repair-${params.repairPayload.sourceEvaluatorTaskId}-r${params.repairPayload.repairIteration}`);
|
|
306
313
|
const id2 = await deps.seedArtificerRepairTask(params);
|
|
307
|
-
|
|
308
|
-
|
|
314
|
+
expect(id2).toBe(id1);
|
|
315
|
+
// 不同 iteration (下一逻辑修复轮) → 不同 id
|
|
316
|
+
const id3 = await deps.seedArtificerRepairTask({
|
|
317
|
+
...params,
|
|
318
|
+
repairPayload: { ...params.repairPayload, repairIteration: params.repairPayload.repairIteration + 1 },
|
|
319
|
+
});
|
|
320
|
+
expect(id3).not.toBe(id1);
|
|
309
321
|
});
|
|
310
322
|
|
|
311
323
|
it('deps spread contains all required base PeerRunnerDeps fields (EP-02: real path gets full deps)', () => {
|
|
@@ -66,7 +66,7 @@ import type {
|
|
|
66
66
|
SeedArtificerRepairParams,
|
|
67
67
|
EvaluatorValidator,
|
|
68
68
|
} from '@principles/core/runtime-v2';
|
|
69
|
-
import {
|
|
69
|
+
import { createHash } from 'node:crypto';
|
|
70
70
|
import { loadPdConfig } from './pd-config-loader.js';
|
|
71
71
|
/* eslint-disable @typescript-eslint/no-use-before-define -- helpers declared after main, matching codebase convention */
|
|
72
72
|
import { compileDemoRule } from './demo-rule-compiler.js';
|
|
@@ -646,7 +646,10 @@ export function createEvaluatorRunnerDeps(inputs: CreateEvaluatorRunnerDepsInput
|
|
|
646
646
|
},
|
|
647
647
|
seedArtificerRepairTask: async (params: SeedArtificerRepairParams): Promise<string> => {
|
|
648
648
|
// rc-7: each call gets a fresh task ID — never reuse a cached ID.
|
|
649
|
-
|
|
649
|
+
// P0-4: deterministic revision identity + reuse on replay
|
|
650
|
+
const repairTaskId = `artificer-repair-${params.repairPayload.sourceEvaluatorTaskId}-r${params.repairPayload.repairIteration}`;
|
|
651
|
+
const existing = await stateManager.getTask(repairTaskId);
|
|
652
|
+
if (existing) return repairTaskId;
|
|
650
653
|
await stateManager.createTask({
|
|
651
654
|
taskId: repairTaskId,
|
|
652
655
|
// D1 (PRI-509): task kind is 'artificer' — reuses the artificer
|
|
@@ -126,4 +126,19 @@ describe('CLI command tree structure', () => {
|
|
|
126
126
|
const output = runPdHelp(['legacy', 'cleanup', '--help']);
|
|
127
127
|
expect(output).toContain('V1 Artificer');
|
|
128
128
|
});
|
|
129
|
+
|
|
130
|
+
// cli-7-test-wiring (PR #1358 known gap 补齐): retry 是 INV-03 的核心 Owner
|
|
131
|
+
// 出边,command-tree 注册必须有 wiring 证明,不能只靠 handler 测试。
|
|
132
|
+
it('internalization retry command exists under runtime internalization (pd runtime internalization retry --help)', () => {
|
|
133
|
+
const output = runPdHelp(['runtime', 'internalization', 'retry', '--help']);
|
|
134
|
+
expect(output).toContain('--task');
|
|
135
|
+
expect(output).toContain('--confirm');
|
|
136
|
+
expect(output).toContain('--workspace');
|
|
137
|
+
expect(output).toContain('--json');
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('internalization subcommand list includes retry (pd runtime internalization --help)', () => {
|
|
141
|
+
const output = runPdHelp(['runtime', 'internalization', '--help']);
|
|
142
|
+
expect(output).toMatch(/retry\s/);
|
|
143
|
+
});
|
|
129
144
|
});
|
|
@@ -238,6 +238,69 @@ describe('pd demo story-a CLI', () => {
|
|
|
238
238
|
expect(parsed.narrative).toContain('[SIMULATED]');
|
|
239
239
|
expect(parsed.narrative).toContain('[REAL]');
|
|
240
240
|
});
|
|
241
|
+
// ── Demo isolation (2026-08-19): demo must not pollute real PD workspaces ──
|
|
242
|
+
|
|
243
|
+
it('refuses to write into a workspace that already contains PD state (default)', async () => {
|
|
244
|
+
const existing = fs.mkdtempSync(path.join(os.tmpdir(), 'pd-demo-isolation-'));
|
|
245
|
+
try {
|
|
246
|
+
fs.mkdirSync(path.join(existing, '.pd'), { recursive: true });
|
|
247
|
+
fs.writeFileSync(path.join(existing, '.pd', 'state.db'), '');
|
|
248
|
+
|
|
249
|
+
await handleDemoStoryA({ workspace: existing, json: true });
|
|
250
|
+
|
|
251
|
+
expect(process.exitCode).toBe(1);
|
|
252
|
+
const output = stdoutSpy.mock.calls.map(c => c[0]).join('');
|
|
253
|
+
const parsed = JSON.parse(output);
|
|
254
|
+
expect(parsed.status).toBe('refused');
|
|
255
|
+
expect(parsed.refusal.reason).toBe('demo_write_to_existing_workspace');
|
|
256
|
+
expect(parsed.refusal.nextAction).toContain('--allow-demo-write-to-existing-workspace');
|
|
257
|
+
// cli-5: no mutation on the refused path — the marker file is untouched.
|
|
258
|
+
const stat = fs.statSync(path.join(existing, '.pd', 'state.db'));
|
|
259
|
+
expect(stat.size).toBe(0);
|
|
260
|
+
} finally {
|
|
261
|
+
fs.rmSync(existing, { recursive: true, force: true });
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it('text mode refusal points at the override flag', async () => {
|
|
266
|
+
const existing = fs.mkdtempSync(path.join(os.tmpdir(), 'pd-demo-isolation-'));
|
|
267
|
+
try {
|
|
268
|
+
fs.mkdirSync(path.join(existing, '.pd'), { recursive: true });
|
|
269
|
+
fs.writeFileSync(path.join(existing, '.pd', 'state.db'), '');
|
|
270
|
+
|
|
271
|
+
await handleDemoStoryA({ workspace: existing });
|
|
272
|
+
|
|
273
|
+
expect(process.exitCode).toBe(1);
|
|
274
|
+
const output = stderrSpy.mock.calls.map(c => c[0]).join('');
|
|
275
|
+
expect(output).toContain('existing PD workspace');
|
|
276
|
+
expect(output).toContain('--allow-demo-write-to-existing-workspace');
|
|
277
|
+
} finally {
|
|
278
|
+
fs.rmSync(existing, { recursive: true, force: true });
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it('developer override allows writing into the existing workspace with origin:demo provenance', async () => {
|
|
283
|
+
const existing = fs.mkdtempSync(path.join(os.tmpdir(), 'pd-demo-override-'));
|
|
284
|
+
try {
|
|
285
|
+
fs.mkdirSync(path.join(existing, '.pd'), { recursive: true });
|
|
286
|
+
fs.writeFileSync(path.join(existing, '.pd', 'config.yaml'), 'features: {}');
|
|
287
|
+
|
|
288
|
+
await handleDemoStoryA({ workspace: existing, json: true, allowDemoWriteToExistingWorkspace: true });
|
|
289
|
+
|
|
290
|
+
const output = stdoutSpy.mock.calls.map(c => c[0]).join('');
|
|
291
|
+
const parsed = JSON.parse(output);
|
|
292
|
+
expect(parsed.status).not.toBe('refused');
|
|
293
|
+
const db = new Database(path.join(existing, '.pd', 'state.db'), { readonly: true });
|
|
294
|
+
const rows = db.prepare('SELECT content_json FROM pi_artifacts').all() as { content_json: string }[];
|
|
295
|
+
db.close();
|
|
296
|
+
expect(rows.length).toBeGreaterThan(0);
|
|
297
|
+
for (const row of rows) {
|
|
298
|
+
expect(JSON.parse(row.content_json).origin).toBe('demo');
|
|
299
|
+
}
|
|
300
|
+
} finally {
|
|
301
|
+
fs.rmSync(existing, { recursive: true, force: true });
|
|
302
|
+
}
|
|
303
|
+
});
|
|
241
304
|
});
|
|
242
305
|
|
|
243
306
|
describe('cleanupTempWorkspace', () => {
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* runtime compatibility-scan tests — pd runtime compatibility-scan (P1-3).
|
|
3
|
+
*
|
|
4
|
+
* Real persisted-workspace fixtures (SqliteConnection + activation store) —
|
|
5
|
+
* no DB mocks, the production read path is exercised end to end (EP-09).
|
|
6
|
+
*
|
|
7
|
+
* Covers:
|
|
8
|
+
* - SCAN-01: clean RuleContextV2-only active rule → exit 0, status clean (cli-1/cli-6)
|
|
9
|
+
* - SCAN-02: active rule reading session.recentThinking → exit 1,
|
|
10
|
+
* reason legacy_rule_contract_dependency, remediation names the rule (cli-6)
|
|
11
|
+
* - SCAN-03: workspace without state.db → exit 0, status no_state_db
|
|
12
|
+
* - SCAN-04: --json emits exactly one parseable JSON object (cli-1)
|
|
13
|
+
* - SCAN-05: command wiring — real Commander registration (cli-7)
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
17
|
+
import * as path from 'node:path';
|
|
18
|
+
import * as os from 'node:os';
|
|
19
|
+
import * as fs from 'node:fs';
|
|
20
|
+
import { SqliteConnection, SqliteActivationStateStore } from '@principles/core/runtime-v2';
|
|
21
|
+
import { handleRuntimeCompatibilityScan } from '../../src/commands/runtime-compatibility-scan.js';
|
|
22
|
+
|
|
23
|
+
const LEGACY_CODE = `
|
|
24
|
+
function evaluate(input, helpers) {
|
|
25
|
+
if (input.session && input.session.recentThinking === true) {
|
|
26
|
+
return { decision: 'block', matched: true };
|
|
27
|
+
}
|
|
28
|
+
return { decision: 'allow', matched: false };
|
|
29
|
+
}
|
|
30
|
+
`;
|
|
31
|
+
|
|
32
|
+
const CLEAN_CODE = `
|
|
33
|
+
function evaluate(input, helpers) {
|
|
34
|
+
var h = input.context && input.context.history;
|
|
35
|
+
return { decision: 'allow', matched: false };
|
|
36
|
+
}
|
|
37
|
+
`;
|
|
38
|
+
|
|
39
|
+
let tempWorkspaceDir: string;
|
|
40
|
+
let conn: SqliteConnection;
|
|
41
|
+
|
|
42
|
+
beforeEach(() => {
|
|
43
|
+
vi.spyOn(console, 'log').mockImplementation(() => undefined);
|
|
44
|
+
vi.spyOn(console, 'error').mockImplementation(() => undefined);
|
|
45
|
+
process.exitCode = undefined;
|
|
46
|
+
tempWorkspaceDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pd-compat-cmd-'));
|
|
47
|
+
conn = new SqliteConnection(tempWorkspaceDir);
|
|
48
|
+
conn.getDb();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
afterEach(() => {
|
|
52
|
+
vi.restoreAllMocks();
|
|
53
|
+
try { conn?.close(); } catch { /* best-effort */ }
|
|
54
|
+
try { fs.rmSync(tempWorkspaceDir, { recursive: true, force: true }); } catch { /* Windows */ }
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
async function seedActiveRule(artifactId: string, ruleId: string, implementationCode: string): Promise<void> {
|
|
58
|
+
const now = new Date().toISOString();
|
|
59
|
+
conn.getDb().prepare(`
|
|
60
|
+
INSERT INTO pi_artifacts (artifact_id, artifact_kind, source_task_id, source_principle_id, source_rule_id, lineage_artifact_ids, validation_status, content_json, created_at, updated_at)
|
|
61
|
+
VALUES (?, 'rule', ?, ?, ?, '[]', 'validated', ?, ?, ?)
|
|
62
|
+
`).run(artifactId, `task-${artifactId}`, `principle-${ruleId}`, ruleId, JSON.stringify({ ruleId, implementationCode }), now, now);
|
|
63
|
+
await new SqliteActivationStateStore(conn).recordActivation({
|
|
64
|
+
activationId: `act-${artifactId}`,
|
|
65
|
+
idempotencyKey: `${artifactId}::code_tool_hook`,
|
|
66
|
+
artifactId,
|
|
67
|
+
channel: 'code_tool_hook',
|
|
68
|
+
action: 'code_tool_hook_live_activate',
|
|
69
|
+
targetRef: `impl://${ruleId}`,
|
|
70
|
+
activatedAt: now,
|
|
71
|
+
deactivatedAt: null,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function capturedStdout(): string {
|
|
76
|
+
const calls = (console.log as unknown as { mock: { calls: unknown[][] } }).mock.calls;
|
|
77
|
+
return calls.map(c => String(c[0] ?? '')).join('\n');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
describe('pd runtime compatibility-scan', () => {
|
|
81
|
+
it('SCAN-01: clean current-contract rule exits 0 with status clean', async () => {
|
|
82
|
+
await seedActiveRule('art-clean', 'rule-clean', CLEAN_CODE);
|
|
83
|
+
await handleRuntimeCompatibilityScan({ workspace: tempWorkspaceDir, json: true });
|
|
84
|
+
expect(process.exitCode).toBeUndefined();
|
|
85
|
+
const parsed = JSON.parse(capturedStdout()) as Record<string, unknown>;
|
|
86
|
+
expect(parsed['status']).toBe('clean');
|
|
87
|
+
expect(parsed['ok']).toBe(true);
|
|
88
|
+
expect(parsed['findings']).toEqual([]);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('SCAN-02: legacy recentThinking rule exits 1 with structured reason + remediation', async () => {
|
|
92
|
+
await seedActiveRule('art-legacy', 'rule-real-diagnosis-first', LEGACY_CODE);
|
|
93
|
+
await handleRuntimeCompatibilityScan({ workspace: tempWorkspaceDir, json: true });
|
|
94
|
+
expect(process.exitCode).toBe(1);
|
|
95
|
+
const parsed = JSON.parse(capturedStdout()) as Record<string, unknown>;
|
|
96
|
+
expect(parsed['ok']).toBe(false);
|
|
97
|
+
expect(parsed['status']).toBe('legacy_dependency');
|
|
98
|
+
expect(parsed['reason']).toBe('legacy_rule_contract_dependency');
|
|
99
|
+
const findings = parsed['findings'] as Array<Record<string, unknown>>;
|
|
100
|
+
expect(findings).toHaveLength(1);
|
|
101
|
+
expect(findings[0]).toMatchObject({ symbol: 'recentThinking', ruleId: 'rule-real-diagnosis-first' });
|
|
102
|
+
const remediation = parsed['remediation'] as string;
|
|
103
|
+
expect(remediation).toContain('rule-real-diagnosis-first');
|
|
104
|
+
expect(remediation).toContain('igrate or deactivate');
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('SCAN-03: workspace without state.db exits 0 with status no_state_db', async () => {
|
|
108
|
+
const emptyDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pd-compat-empty-cmd-'));
|
|
109
|
+
try {
|
|
110
|
+
await handleRuntimeCompatibilityScan({ workspace: emptyDir, json: true });
|
|
111
|
+
expect(process.exitCode).toBeUndefined();
|
|
112
|
+
const parsed = JSON.parse(capturedStdout()) as Record<string, unknown>;
|
|
113
|
+
expect(parsed['status']).toBe('no_state_db');
|
|
114
|
+
expect(parsed['ok']).toBe(true);
|
|
115
|
+
// Side-effect-free: the scan must not create a state.db (cli-5).
|
|
116
|
+
expect(fs.existsSync(path.join(emptyDir, '.pd', 'state.db'))).toBe(false);
|
|
117
|
+
} finally {
|
|
118
|
+
fs.rmSync(emptyDir, { recursive: true, force: true });
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('SCAN-04: --json stdout is exactly one parseable JSON object (cli-1)', async () => {
|
|
123
|
+
await seedActiveRule('art-clean2', 'rule-clean2', CLEAN_CODE);
|
|
124
|
+
await handleRuntimeCompatibilityScan({ workspace: tempWorkspaceDir, json: true });
|
|
125
|
+
const out = capturedStdout().trim();
|
|
126
|
+
expect(out.startsWith('{')).toBe(true);
|
|
127
|
+
expect(out.endsWith('}')).toBe(true);
|
|
128
|
+
expect(() => JSON.parse(out)).not.toThrow();
|
|
129
|
+
expect((out.match(/\{/g) ?? []).length).toBeGreaterThanOrEqual(1);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('SCAN-05: command is registered on a real Commander program (cli-7)', async () => {
|
|
133
|
+
const { Command } = await import('commander');
|
|
134
|
+
const { registerRuntimeCompatibilityScanCommand } = await import('../../src/commands/runtime-compatibility-scan.js');
|
|
135
|
+
const program = new Command();
|
|
136
|
+
program.name('pd').exitOverride();
|
|
137
|
+
const runtimeCmd = program.command('runtime').description('Runtime inspection and health checks');
|
|
138
|
+
registerRuntimeCompatibilityScanCommand(runtimeCmd);
|
|
139
|
+
const scanCmd = runtimeCmd.commands.find(c => c.name() === 'compatibility-scan');
|
|
140
|
+
expect(scanCmd).toBeDefined();
|
|
141
|
+
expect(scanCmd?.description()).toContain('retired RuleHost contract');
|
|
142
|
+
// Flag wiring: -w/--workspace and --json registered; no mutating flags exist.
|
|
143
|
+
expect(scanCmd?.options.map(o => o.long)).toContain('--workspace');
|
|
144
|
+
expect(scanCmd?.options.map(o => o.long)).toContain('--json');
|
|
145
|
+
});
|
|
146
|
+
});
|