@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.
- package/dist/commands/config-doctor.d.ts.map +1 -1
- package/dist/commands/config-doctor.js +18 -0
- package/dist/commands/config-doctor.js.map +1 -1
- package/dist/commands/console.d.ts.map +1 -1
- package/dist/commands/console.js +15 -3
- package/dist/commands/console.js.map +1 -1
- package/dist/commands/runtime-init.d.ts.map +1 -1
- package/dist/commands/runtime-init.js +2 -0
- package/dist/commands/runtime-init.js.map +1 -1
- package/dist/commands/runtime-internalization-run-rulehost.d.ts.map +1 -1
- package/dist/commands/runtime-internalization-run-rulehost.js +8 -0
- package/dist/commands/runtime-internalization-run-rulehost.js.map +1 -1
- package/dist/services/config-doctor.d.ts +24 -0
- package/dist/services/config-doctor.d.ts.map +1 -1
- package/dist/services/config-doctor.js +87 -1
- package/dist/services/config-doctor.js.map +1 -1
- package/dist/services/console-launcher.d.ts +9 -0
- package/dist/services/console-launcher.d.ts.map +1 -1
- package/dist/services/console-launcher.js +47 -4
- package/dist/services/console-launcher.js.map +1 -1
- package/package.json +1 -1
- package/src/commands/config-doctor.ts +15 -0
- package/src/commands/console.ts +16 -3
- package/src/commands/runtime-init.ts +2 -0
- package/src/commands/runtime-internalization-run-rulehost.ts +8 -0
- package/src/services/config-doctor.ts +111 -1
- package/src/services/console-launcher.ts +57 -5
- package/tests/commands/console-open.test.ts +59 -1
- package/tests/commands/runtime-init-empty-workspace.test.ts +5 -1
- 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
|
|
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
|
+
});
|