@principles/pd-cli 1.152.0 → 1.152.2

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.
Files changed (30) hide show
  1. package/dist/commands/config-doctor.d.ts.map +1 -1
  2. package/dist/commands/config-doctor.js +18 -0
  3. package/dist/commands/config-doctor.js.map +1 -1
  4. package/dist/commands/console.d.ts.map +1 -1
  5. package/dist/commands/console.js +15 -3
  6. package/dist/commands/console.js.map +1 -1
  7. package/dist/commands/runtime-init.d.ts.map +1 -1
  8. package/dist/commands/runtime-init.js +2 -0
  9. package/dist/commands/runtime-init.js.map +1 -1
  10. package/dist/commands/runtime-internalization-run-rulehost.d.ts.map +1 -1
  11. package/dist/commands/runtime-internalization-run-rulehost.js +8 -0
  12. package/dist/commands/runtime-internalization-run-rulehost.js.map +1 -1
  13. package/dist/services/config-doctor.d.ts +24 -0
  14. package/dist/services/config-doctor.d.ts.map +1 -1
  15. package/dist/services/config-doctor.js +87 -1
  16. package/dist/services/config-doctor.js.map +1 -1
  17. package/dist/services/console-launcher.d.ts +9 -0
  18. package/dist/services/console-launcher.d.ts.map +1 -1
  19. package/dist/services/console-launcher.js +47 -4
  20. package/dist/services/console-launcher.js.map +1 -1
  21. package/package.json +1 -1
  22. package/src/commands/config-doctor.ts +15 -0
  23. package/src/commands/console.ts +16 -3
  24. package/src/commands/runtime-init.ts +2 -0
  25. package/src/commands/runtime-internalization-run-rulehost.ts +8 -0
  26. package/src/services/config-doctor.ts +111 -1
  27. package/src/services/console-launcher.ts +57 -5
  28. package/tests/commands/console-open.test.ts +59 -1
  29. package/tests/commands/runtime-init-empty-workspace.test.ts +5 -1
  30. package/tests/services/config-doctor-signal-health.test.ts +109 -0
@@ -328,7 +328,9 @@ describe('planConsoleLaunch — reused (healthy console on preferred port)', ()
328
328
  expect(result.reason).toBe('console_authentication_mode_mismatch');
329
329
  // PRI-695: the guidance names the actual running state and both ways
330
330
  // out — it no longer references "Companion" (meaningless to CLI users)
331
- // or PD_CONSOLE_TOKEN (never set by the default install).
331
+ // or PD_CONSOLE_TOKEN (never set by the default install). PRI-784: the
332
+ // no-source form stays source-neutral; the env/flag variants have
333
+ // their own cases below.
332
334
  expect(result.nextAction).toContain('without authentication');
333
335
  expect(result.nextAction).toContain(`port ${addr.port}`);
334
336
  expect(result.nextAction).not.toContain('Companion');
@@ -338,6 +340,56 @@ describe('planConsoleLaunch — reused (healthy console on preferred port)', ()
338
340
  }
339
341
  });
340
342
 
343
+ it('auth-mismatch guidance names PD_CONSOLE_TOKEN and its durable removal when the token came from the env (PRI-784)', async () => {
344
+ const server = http.createServer((_req, res) => {
345
+ res.statusCode = 200;
346
+ res.end(JSON.stringify({ success: true, data: { authenticationMode: 'no_auth' } }));
347
+ });
348
+ await new Promise<void>((resolve) => server.listen(0, '127.0.0.1', resolve));
349
+ const addr = server.address();
350
+ if (typeof addr !== 'object' || !addr) throw new Error('no addr');
351
+ try {
352
+ const result = await planConsoleLaunch({
353
+ workspaceDir: '/tmp/anywhere', preferredPort: addr.port,
354
+ host: '127.0.0.1', token: 'configured-token', tokenSource: 'env',
355
+ });
356
+ expect(result.status).toBe('refused');
357
+ expect(result.reason).toBe('console_authentication_mode_mismatch');
358
+ // The env source is the recurring real-world trap: a persistently set
359
+ // variable makes "reopen without a token" impossible to follow, so the
360
+ // guidance must name the variable and how to remove it at its source.
361
+ expect(result.nextAction).toContain('PD_CONSOLE_TOKEN');
362
+ expect(result.nextAction).toContain("SetEnvironmentVariable('PD_CONSOLE_TOKEN', $null, 'User')");
363
+ expect(result.nextAction).toContain('--no-auth');
364
+ expect(result.nextAction).toContain('--token');
365
+ } finally {
366
+ await new Promise<void>((resolve) => server.close(resolve));
367
+ }
368
+ });
369
+
370
+ it('auth-mismatch guidance names --token, never the env var, when the token came from the flag (PRI-784)', async () => {
371
+ const server = http.createServer((_req, res) => {
372
+ res.statusCode = 200;
373
+ res.end(JSON.stringify({ success: true, data: { authenticationMode: 'no_auth' } }));
374
+ });
375
+ await new Promise<void>((resolve) => server.listen(0, '127.0.0.1', resolve));
376
+ const addr = server.address();
377
+ if (typeof addr !== 'object' || !addr) throw new Error('no addr');
378
+ try {
379
+ const result = await planConsoleLaunch({
380
+ workspaceDir: '/tmp/anywhere', preferredPort: addr.port,
381
+ host: '127.0.0.1', token: 'configured-token', tokenSource: 'flag',
382
+ });
383
+ expect(result.status).toBe('refused');
384
+ expect(result.reason).toBe('console_authentication_mode_mismatch');
385
+ expect(result.nextAction).toContain('--token');
386
+ expect(result.nextAction).toContain('--no-auth');
387
+ expect(result.nextAction).not.toContain('PD_CONSOLE_TOKEN');
388
+ } finally {
389
+ await new Promise<void>((resolve) => server.close(resolve));
390
+ }
391
+ });
392
+
341
393
  it('reuses a no-auth Console for a tokenless caller (PRI-695 default post-install state)', async () => {
342
394
  // The installer auto-launches the Console with --no-auth; the documented
343
395
  // reopen command carries no flags and no token. That combination must
@@ -724,6 +776,12 @@ describe('CLI command wiring (pd console open)', () => {
724
776
  expect(run.parsed.status).toBe('refused');
725
777
  expect(run.parsed.reason).toBe('console_authentication_mode_mismatch');
726
778
  expect(run.parsed).not.toHaveProperty('serverPid');
779
+ // PRI-784 review fix (PR #1670): the fresh-start env guidance is part of
780
+ // the user contract — assert its content, not just the refusal shell.
781
+ expect(run.parsed.nextAction).toContain('PD_CONSOLE_TOKEN');
782
+ expect(run.parsed.nextAction).toContain('not forwarded');
783
+ expect(run.parsed.nextAction).toContain('--token "$PD_CONSOLE_TOKEN"');
784
+ expect(run.parsed.nextAction).toContain('--no-auth');
727
785
  await new Promise((resolve) => setTimeout(resolve, 100));
728
786
  expect(await isPortInUse('127.0.0.1', 49391)).toBe(false);
729
787
  } finally {
@@ -70,6 +70,8 @@ const EXPECTED_TRAJECTORY_TABLES = [
70
70
  'schema_version', 'ingest_checkpoint', 'sessions', 'assistant_turns',
71
71
  'user_turns', 'tool_calls', 'pain_events', 'gate_blocks', 'trust_changes',
72
72
  'principle_events', 'task_outcomes', 'correction_samples', 'sample_reviews',
73
+ // PRI-790 G2: Stage2 待确认信号的持久队列(observer 周期批量确认的输入)。
74
+ 'signal_confirmations',
73
75
  'exports_audit',
74
76
  ];
75
77
 
@@ -83,6 +85,8 @@ const EXPECTED_TRAJECTORY_INDEXES = [
83
85
  'idx_pain_events_session_id',
84
86
  'idx_pain_events_canonical_pain_id',
85
87
  'idx_correction_samples_review_status',
88
+ // PRI-790 G2: 队列扫描索引(status + attempts)。
89
+ 'idx_signal_confirmations_status',
86
90
  ];
87
91
 
88
92
  // ── Tests ───────────────────────────────────────────────────────────────────
@@ -121,7 +125,7 @@ describe('pd runtime init — empty workspace integration', () => {
121
125
  }
122
126
  });
123
127
 
124
- it('creates trajectory.db with all 14 expected tables', () => {
128
+ it('creates trajectory.db with all 15 expected tables', () => {
125
129
  buildRuntimeInitOutput(tmpDir, true);
126
130
  const trajDbPath = path.join(tmpDir, '.state', 'trajectory.db');
127
131
  expect(fs.existsSync(trajDbPath)).toBe(true);
@@ -0,0 +1,109 @@
1
+ /**
2
+ * PRI-788 G4 — pd config doctor 的 signalHealth 分级测试。
3
+ */
4
+ import { afterEach, describe, expect, it } from 'vitest';
5
+ import * as fs from 'fs';
6
+ import * as os from 'os';
7
+ import * as path from 'path';
8
+
9
+ import { buildSignalHealthEntry } from '../../src/services/config-doctor.js';
10
+
11
+ const tempDirs: string[] = [];
12
+
13
+ afterEach(() => {
14
+ for (const dir of tempDirs.splice(0)) {
15
+ fs.rmSync(dir, { recursive: true, force: true });
16
+ }
17
+ });
18
+
19
+ function makeStateDir(): string {
20
+ const workspace = fs.mkdtempSync(path.join(os.tmpdir(), 'pd-doctor-signal-'));
21
+ const stateDir = path.join(workspace, '.state');
22
+ fs.mkdirSync(stateDir, { recursive: true });
23
+ tempDirs.push(workspace);
24
+ return stateDir;
25
+ }
26
+
27
+ function writeHealth(stateDir: string, state: Record<string, unknown>): void {
28
+ fs.writeFileSync(path.join(stateDir, 'signal-health.json'), JSON.stringify({
29
+ stage1Strong: 0,
30
+ stage2Confirmed: 0,
31
+ stage2Queued: 0,
32
+ stage2Dropped: 0,
33
+ pendingCount: 0,
34
+ lastStage2SuccessAt: null,
35
+ observerLastSuccessAt: null,
36
+ observerConsecutiveFailures: 0,
37
+ day: new Date().toISOString().slice(0, 10),
38
+ updatedAt: new Date().toISOString(),
39
+ ...state,
40
+ }));
41
+ }
42
+
43
+ describe('buildSignalHealthEntry (PRI-788 G4)', () => {
44
+ it('missing file → unknown with nextAction, never throws (rc-1)', () => {
45
+ const entry = buildSignalHealthEntry(makeStateDir());
46
+ expect(entry.status).toBe('unknown');
47
+ expect(entry.counts).toBeNull();
48
+ expect(entry.nextAction).toMatch(/signal_collector/);
49
+ });
50
+
51
+ it('corrupt file → unknown', () => {
52
+ const stateDir = makeStateDir();
53
+ fs.writeFileSync(path.join(stateDir, 'signal-health.json'), '{not-json');
54
+ expect(buildSignalHealthEntry(stateDir).status).toBe('unknown');
55
+ });
56
+
57
+ it('fresh healthy file → ok with counts', () => {
58
+ const stateDir = makeStateDir();
59
+ writeHealth(stateDir, { stage1Strong: 3, pendingCount: 2 });
60
+ const entry = buildSignalHealthEntry(stateDir);
61
+ expect(entry.status).toBe('ok');
62
+ expect(entry.counts).toMatchObject({ stage1Strong: 3, pendingCount: 2 });
63
+ expect(entry.observerConsecutiveFailures).toBe(0);
64
+ });
65
+
66
+ it('observerConsecutiveFailures > 4 → degraded with probe nextAction', () => {
67
+ const stateDir = makeStateDir();
68
+ writeHealth(stateDir, { observerConsecutiveFailures: 40 });
69
+ const entry = buildSignalHealthEntry(stateDir);
70
+ expect(entry.status).toBe('degraded');
71
+ expect(entry.reason).toContain('40 cycles');
72
+ expect(entry.nextAction).toContain('pd runtime probe');
73
+ });
74
+
75
+ it('stale updatedAt (>24h) → degraded', () => {
76
+ const stateDir = makeStateDir();
77
+ const stale = new Date(Date.now() - 30 * 60 * 60 * 1000).toISOString();
78
+ writeHealth(stateDir, { updatedAt: stale });
79
+ const entry = buildSignalHealthEntry(stateDir);
80
+ expect(entry.status).toBe('degraded');
81
+ expect(entry.reason).toContain('>24h');
82
+ });
83
+
84
+ it('恰好超过 24h(24.5h)即 degraded — 常量必须与「>24h」契约一致', () => {
85
+ const stateDir = makeStateDir();
86
+ const stale = new Date(Date.now() - 24.5 * 60 * 60 * 1000).toISOString();
87
+ writeHealth(stateDir, { updatedAt: stale });
88
+ expect(buildSignalHealthEntry(stateDir).status).toBe('degraded');
89
+ });
90
+
91
+ it('23h 未更新仍是 ok(不误报 degraded)', () => {
92
+ const stateDir = makeStateDir();
93
+ const fresh = new Date(Date.now() - 23 * 60 * 60 * 1000).toISOString();
94
+ writeHealth(stateDir, { updatedAt: fresh });
95
+ expect(buildSignalHealthEntry(stateDir).status).toBe('ok');
96
+ });
97
+
98
+ it('non-finite garbage fields degrade gracefully without throwing (rc-2)', () => {
99
+ const stateDir = makeStateDir();
100
+ writeHealth(stateDir, {
101
+ stage1Strong: 'lots',
102
+ observerConsecutiveFailures: 'many',
103
+ updatedAt: 12345,
104
+ });
105
+ const entry = buildSignalHealthEntry(stateDir);
106
+ expect(entry.status).toBe('degraded'); // updatedAt 非法 → stale 分支
107
+ expect(entry.counts?.stage1Strong).toBe(0);
108
+ });
109
+ });