@principles/pd-cli 1.132.0 → 1.134.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.
Files changed (212) hide show
  1. package/dist/commands/__tests__/intent-flag-wiring.test.js +74 -3
  2. package/dist/commands/__tests__/intent-flag-wiring.test.js.map +1 -1
  3. package/dist/commands/__tests__/pain-retry-maxTokens-flag.test.d.ts +2 -0
  4. package/dist/commands/__tests__/pain-retry-maxTokens-flag.test.d.ts.map +1 -0
  5. package/dist/commands/__tests__/pain-retry-maxTokens-flag.test.js +60 -0
  6. package/dist/commands/__tests__/pain-retry-maxTokens-flag.test.js.map +1 -0
  7. package/dist/commands/__tests__/rulecode-flag-wiring.test.js +11 -4
  8. package/dist/commands/__tests__/rulecode-flag-wiring.test.js.map +1 -1
  9. package/dist/commands/__tests__/run-rulehost-flag-wiring.test.js +9 -0
  10. package/dist/commands/__tests__/run-rulehost-flag-wiring.test.js.map +1 -1
  11. package/dist/commands/admission-gate.d.ts +24 -0
  12. package/dist/commands/admission-gate.d.ts.map +1 -0
  13. package/dist/commands/admission-gate.js +29 -0
  14. package/dist/commands/admission-gate.js.map +1 -0
  15. package/dist/commands/candidate.d.ts.map +1 -1
  16. package/dist/commands/candidate.js +72 -6
  17. package/dist/commands/candidate.js.map +1 -1
  18. package/dist/commands/console.d.ts.map +1 -1
  19. package/dist/commands/console.js +40 -0
  20. package/dist/commands/console.js.map +1 -1
  21. package/dist/commands/demo-story-a.d.ts.map +1 -1
  22. package/dist/commands/demo-story-a.js +4 -2
  23. package/dist/commands/demo-story-a.js.map +1 -1
  24. package/dist/commands/diagnose.d.ts.map +1 -1
  25. package/dist/commands/diagnose.js +155 -8
  26. package/dist/commands/diagnose.js.map +1 -1
  27. package/dist/commands/errors-list.d.ts +25 -0
  28. package/dist/commands/errors-list.d.ts.map +1 -0
  29. package/dist/commands/errors-list.js +368 -0
  30. package/dist/commands/errors-list.js.map +1 -0
  31. package/dist/commands/health-codex.d.ts +7 -0
  32. package/dist/commands/health-codex.d.ts.map +1 -0
  33. package/dist/commands/health-codex.js +209 -0
  34. package/dist/commands/health-codex.js.map +1 -0
  35. package/dist/commands/health.d.ts +24 -0
  36. package/dist/commands/health.d.ts.map +1 -1
  37. package/dist/commands/health.js +48 -0
  38. package/dist/commands/health.js.map +1 -1
  39. package/dist/commands/intent.d.ts +21 -4
  40. package/dist/commands/intent.d.ts.map +1 -1
  41. package/dist/commands/intent.js +88 -27
  42. package/dist/commands/intent.js.map +1 -1
  43. package/dist/commands/pain-evidence.d.ts +6 -0
  44. package/dist/commands/pain-evidence.d.ts.map +1 -1
  45. package/dist/commands/pain-evidence.js +6 -0
  46. package/dist/commands/pain-evidence.js.map +1 -1
  47. package/dist/commands/pain-record.d.ts.map +1 -1
  48. package/dist/commands/pain-record.js +21 -0
  49. package/dist/commands/pain-record.js.map +1 -1
  50. package/dist/commands/pain-retry.d.ts +4 -0
  51. package/dist/commands/pain-retry.d.ts.map +1 -1
  52. package/dist/commands/pain-retry.js +327 -32
  53. package/dist/commands/pain-retry.js.map +1 -1
  54. package/dist/commands/proven-channel-baseline.d.ts.map +1 -1
  55. package/dist/commands/proven-channel-baseline.js +4 -2
  56. package/dist/commands/proven-channel-baseline.js.map +1 -1
  57. package/dist/commands/runtime-activation.d.ts +30 -0
  58. package/dist/commands/runtime-activation.d.ts.map +1 -1
  59. package/dist/commands/runtime-activation.js +567 -11
  60. package/dist/commands/runtime-activation.js.map +1 -1
  61. package/dist/commands/runtime-canary.d.ts.map +1 -1
  62. package/dist/commands/runtime-canary.js +32 -7
  63. package/dist/commands/runtime-canary.js.map +1 -1
  64. package/dist/commands/runtime-health-snapshot.d.ts.map +1 -1
  65. package/dist/commands/runtime-health-snapshot.js +4 -2
  66. package/dist/commands/runtime-health-snapshot.js.map +1 -1
  67. package/dist/commands/runtime-init.d.ts +43 -0
  68. package/dist/commands/runtime-init.d.ts.map +1 -0
  69. package/dist/commands/runtime-init.js +264 -0
  70. package/dist/commands/runtime-init.js.map +1 -0
  71. package/dist/commands/runtime-internalization-context-trace.d.ts +25 -0
  72. package/dist/commands/runtime-internalization-context-trace.d.ts.map +1 -0
  73. package/dist/commands/runtime-internalization-context-trace.js +374 -0
  74. package/dist/commands/runtime-internalization-context-trace.js.map +1 -0
  75. package/dist/commands/runtime-internalization-enqueue-successors.d.ts.map +1 -1
  76. package/dist/commands/runtime-internalization-enqueue-successors.js +3 -1
  77. package/dist/commands/runtime-internalization-enqueue-successors.js.map +1 -1
  78. package/dist/commands/runtime-internalization-integrity.d.ts.map +1 -1
  79. package/dist/commands/runtime-internalization-integrity.js +4 -2
  80. package/dist/commands/runtime-internalization-integrity.js.map +1 -1
  81. package/dist/commands/runtime-internalization-run-once.d.ts.map +1 -1
  82. package/dist/commands/runtime-internalization-run-once.js +20 -5
  83. package/dist/commands/runtime-internalization-run-once.js.map +1 -1
  84. package/dist/commands/runtime-internalization-run-rulehost.d.ts +1 -0
  85. package/dist/commands/runtime-internalization-run-rulehost.d.ts.map +1 -1
  86. package/dist/commands/runtime-internalization-run-rulehost.js +149 -6
  87. package/dist/commands/runtime-internalization-run-rulehost.js.map +1 -1
  88. package/dist/commands/runtime-uat.d.ts.map +1 -1
  89. package/dist/commands/runtime-uat.guard.test.js +13 -15
  90. package/dist/commands/runtime-uat.guard.test.js.map +1 -1
  91. package/dist/commands/runtime-uat.js +4 -3
  92. package/dist/commands/runtime-uat.js.map +1 -1
  93. package/dist/config-reader.js +1 -1
  94. package/dist/config-reader.js.map +1 -1
  95. package/dist/index.js +54 -78
  96. package/dist/index.js.map +1 -1
  97. package/dist/services/__tests__/evaluator-runner-deps.test.d.ts +2 -0
  98. package/dist/services/__tests__/evaluator-runner-deps.test.d.ts.map +1 -0
  99. package/dist/services/__tests__/evaluator-runner-deps.test.js +304 -0
  100. package/dist/services/__tests__/evaluator-runner-deps.test.js.map +1 -0
  101. package/dist/services/__tests__/rulehost-readiness.test.js.map +1 -1
  102. package/dist/services/__tests__/runtime-adapter-resolver.test.js.map +1 -1
  103. package/dist/services/config-doctor.d.ts.map +1 -1
  104. package/dist/services/config-doctor.js +9 -2
  105. package/dist/services/config-doctor.js.map +1 -1
  106. package/dist/services/pain-flood-simulation-runner.d.ts.map +1 -1
  107. package/dist/services/pain-flood-simulation-runner.js +9 -0
  108. package/dist/services/pain-flood-simulation-runner.js.map +1 -1
  109. package/dist/services/pd-config-loader.js +2 -2
  110. package/dist/services/pd-config-loader.js.map +1 -1
  111. package/dist/services/proven-channel-baseline-runner.d.ts.map +1 -1
  112. package/dist/services/proven-channel-baseline-runner.js +10 -2
  113. package/dist/services/proven-channel-baseline-runner.js.map +1 -1
  114. package/dist/services/quality-scorecard/data-extractor.js.map +1 -1
  115. package/dist/services/resolve-runtime-from-pd-config.js.map +1 -1
  116. package/dist/services/rulehost-pipeline-runner.d.ts +77 -1
  117. package/dist/services/rulehost-pipeline-runner.d.ts.map +1 -1
  118. package/dist/services/rulehost-pipeline-runner.js +111 -7
  119. package/dist/services/rulehost-pipeline-runner.js.map +1 -1
  120. package/dist/services/rulehost-readiness.js.map +1 -1
  121. package/dist/services/runtime-adapter-resolver.d.ts +1 -0
  122. package/dist/services/runtime-adapter-resolver.d.ts.map +1 -1
  123. package/dist/services/runtime-adapter-resolver.js +3 -0
  124. package/dist/services/runtime-adapter-resolver.js.map +1 -1
  125. package/dist/services/synthetic-baseline-runner.d.ts.map +1 -1
  126. package/dist/services/synthetic-baseline-runner.js +11 -0
  127. package/dist/services/synthetic-baseline-runner.js.map +1 -1
  128. package/dist/utils/production-workspace-guard.d.ts +4 -4
  129. package/dist/utils/production-workspace-guard.d.ts.map +1 -1
  130. package/dist/utils/production-workspace-guard.js +19 -16
  131. package/dist/utils/production-workspace-guard.js.map +1 -1
  132. package/dist/utils/production-workspace-guard.test.js +54 -19
  133. package/dist/utils/production-workspace-guard.test.js.map +1 -1
  134. package/package.json +8 -5
  135. package/scripts/migrate-illegal-expected-decision.ts +252 -0
  136. package/src/commands/__tests__/intent-flag-wiring.test.ts +88 -3
  137. package/src/commands/__tests__/pain-retry-maxTokens-flag.test.ts +73 -0
  138. package/src/commands/__tests__/rulecode-flag-wiring.test.ts +31 -24
  139. package/src/commands/__tests__/run-rulehost-flag-wiring.test.ts +12 -0
  140. package/src/commands/admission-gate.ts +30 -0
  141. package/src/commands/candidate.ts +74 -6
  142. package/src/commands/console.ts +40 -0
  143. package/src/commands/demo-story-a.ts +4 -2
  144. package/src/commands/diagnose.ts +158 -10
  145. package/src/commands/errors-list.ts +440 -0
  146. package/src/commands/health-codex.ts +249 -0
  147. package/src/commands/health.ts +60 -0
  148. package/src/commands/intent.ts +98 -29
  149. package/src/commands/pain-evidence.ts +6 -0
  150. package/src/commands/pain-record.ts +21 -0
  151. package/src/commands/pain-retry.ts +353 -35
  152. package/src/commands/proven-channel-baseline.ts +4 -2
  153. package/src/commands/runtime-activation.ts +657 -12
  154. package/src/commands/runtime-canary.ts +31 -5
  155. package/src/commands/runtime-health-snapshot.ts +4 -2
  156. package/src/commands/runtime-init.ts +305 -0
  157. package/src/commands/runtime-internalization-context-trace.ts +445 -0
  158. package/src/commands/runtime-internalization-enqueue-successors.ts +3 -1
  159. package/src/commands/runtime-internalization-integrity.ts +4 -2
  160. package/src/commands/runtime-internalization-run-once.ts +20 -5
  161. package/src/commands/runtime-internalization-run-rulehost.ts +159 -6
  162. package/src/commands/runtime-uat.guard.test.ts +14 -16
  163. package/src/commands/runtime-uat.ts +4 -3
  164. package/src/config-reader.ts +1 -1
  165. package/src/index.ts +69 -78
  166. package/src/services/__tests__/evaluator-runner-deps.test.ts +341 -0
  167. package/src/services/config-doctor.ts +9 -2
  168. package/src/services/pain-flood-simulation-runner.ts +9 -0
  169. package/src/services/pd-config-loader.ts +2 -2
  170. package/src/services/proven-channel-baseline-runner.ts +10 -3
  171. package/src/services/rulehost-pipeline-runner.ts +157 -6
  172. package/src/services/runtime-adapter-resolver.ts +4 -0
  173. package/src/services/synthetic-baseline-runner.ts +11 -0
  174. package/src/utils/production-workspace-guard.test.ts +62 -20
  175. package/src/utils/production-workspace-guard.ts +19 -16
  176. package/tests/bdd/cli-contract.steps.ts +343 -0
  177. package/tests/bdd/context-trace.steps.ts +229 -0
  178. package/tests/bdd/support/gherkin-loader.ts +147 -0
  179. package/tests/bdd/support/repo-root.ts +62 -0
  180. package/tests/bdd/support/vitest-bdd.ts +179 -0
  181. package/tests/commands/admission-gate-coverage.test.ts +79 -0
  182. package/tests/commands/candidate-audit-repair.test.ts +77 -0
  183. package/tests/commands/candidate-intake.test.ts +155 -10
  184. package/tests/commands/candidate-internalization-backfill.test.ts +30 -0
  185. package/tests/commands/cli-help-snapshot.test.ts +15 -1
  186. package/tests/commands/console-open.test.ts +19 -0
  187. package/tests/commands/diagnose.test.ts +619 -14
  188. package/tests/commands/errors-list.test.ts +678 -0
  189. package/tests/commands/health-codex.test.ts +243 -0
  190. package/tests/commands/health-host-wiring.test.ts +76 -0
  191. package/tests/commands/intent.test.ts +4 -4
  192. package/tests/commands/pain-record.test.ts +80 -0
  193. package/tests/commands/pain-retry.test.ts +94 -3
  194. package/tests/commands/run-rulehost-handler.test.ts +133 -0
  195. package/tests/commands/runtime-activation-approve-flag-wiring.test.ts +227 -0
  196. package/tests/commands/runtime-activation-deactivate-flag-wiring.test.ts +167 -0
  197. package/tests/commands/runtime-activation-dispatch-flag-wiring.test.ts +262 -0
  198. package/tests/commands/runtime-activation-list-flag-wiring.test.ts +193 -0
  199. package/tests/commands/runtime-activation-promote-flag-wiring.test.ts +222 -0
  200. package/tests/commands/runtime-activation.test.ts +1090 -5
  201. package/tests/commands/runtime-canary.test.ts +50 -0
  202. package/tests/commands/runtime-features.test.ts +2 -0
  203. package/tests/commands/runtime-init-empty-workspace.test.ts +234 -0
  204. package/tests/commands/runtime-init.test.ts +346 -0
  205. package/tests/commands/runtime-internalization-context-trace.test.ts +110 -0
  206. package/tests/commands/runtime-internalization.test.ts +2 -2
  207. package/tests/config-reader.test.ts +6 -5
  208. package/tests/e2e/cross-package-acceptance.test.ts +55 -13
  209. package/tests/services/mainline-snapshot-assembler.test.ts +6 -3
  210. package/tests/services/resolve-runtime-from-pd-config.test.ts +2 -0
  211. package/tests/services/rulehost-pipeline-runner.test.ts +1 -1
  212. package/vitest.config.ts +1 -1
@@ -0,0 +1,243 @@
1
+ /**
2
+ * pd health --host codex command unit tests.
3
+ *
4
+ * Covers:
5
+ * - cli-1: JSON mode outputs exactly one parseable JSON object.
6
+ * - cli-2: exit paths stop execution (process.exitCode set, no later side effects).
7
+ * - cli-5: failure paths do not mutate state (read-only).
8
+ * - cli-6: every degraded result includes reason + nextAction.
9
+ * - flag-off path (host.codex disabled → warning + exitCode).
10
+ * - hooks trust undetectable (no ~/.codex) → reason + nextAction.
11
+ * - dual registration detection (global hooks.json present → reason + nextAction).
12
+ * - happy path (flag on, hooks trusted) → clean JSON, no warnings.
13
+ */
14
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
15
+
16
+ const {
17
+ mockResolveNearestPdWorkspace,
18
+ mockLoadPdConfigForPlugin,
19
+ mockComputeFeatureFlagsFromConfig,
20
+ mockIsFeatureEnabled,
21
+ mockExistsSync,
22
+ mockReadFileSync,
23
+ mockHomedir,
24
+ mockRequireResolve,
25
+ mockConsoleLog,
26
+ mockConsoleWarn,
27
+ } = vi.hoisted(() => {
28
+ return {
29
+ mockResolveNearestPdWorkspace: vi.fn(),
30
+ mockLoadPdConfigForPlugin: vi.fn(),
31
+ mockComputeFeatureFlagsFromConfig: vi.fn(),
32
+ mockIsFeatureEnabled: vi.fn(),
33
+ mockExistsSync: vi.fn(),
34
+ mockReadFileSync: vi.fn(),
35
+ mockHomedir: vi.fn(),
36
+ mockRequireResolve: vi.fn(),
37
+ mockConsoleLog: vi.fn(),
38
+ mockConsoleWarn: vi.fn(),
39
+ };
40
+ });
41
+
42
+ vi.mock('@principles/host-runtime', () => ({
43
+ resolveNearestPdWorkspace: mockResolveNearestPdWorkspace,
44
+ loadPdConfigForPlugin: mockLoadPdConfigForPlugin,
45
+ }));
46
+
47
+ vi.mock('@principles/core/runtime-v2', () => ({
48
+ computeFeatureFlagsFromConfig: mockComputeFeatureFlagsFromConfig,
49
+ isFeatureEnabled: mockIsFeatureEnabled,
50
+ }));
51
+
52
+ vi.mock('../../src/resolve-workspace.js', () => ({
53
+ resolveWorkspaceDir: vi.fn().mockReturnValue('/fake/workspace'),
54
+ }));
55
+
56
+ vi.mock('fs', () => ({
57
+ existsSync: mockExistsSync,
58
+ readFileSync: mockReadFileSync,
59
+ }));
60
+
61
+ vi.mock('os', () => ({
62
+ homedir: mockHomedir,
63
+ }));
64
+
65
+ vi.mock('module', () => ({
66
+ createRequire: () => ({
67
+ resolve: mockRequireResolve,
68
+ }),
69
+ }));
70
+
71
+ import { handleHealthCodex } from '../../src/commands/health-codex.js';
72
+
73
+ describe('pd health --host codex', () => {
74
+ beforeEach(() => {
75
+ vi.clearAllMocks();
76
+ process.exitCode = undefined;
77
+ mockHomedir.mockReturnValue('/fake/home');
78
+ mockRequireResolve.mockImplementation((name: string) => `/fake/node_modules/${name}/package.json`);
79
+ mockReadFileSync.mockImplementation((p: string) => {
80
+ if (typeof p === 'string' && p.includes('package.json')) {
81
+ return JSON.stringify({ version: '0.1.0' });
82
+ }
83
+ return '';
84
+ });
85
+ mockResolveNearestPdWorkspace.mockReturnValue({
86
+ ok: true,
87
+ workspaceDir: '/fake/workspace',
88
+ configPath: '/fake/workspace/.pd/config.yaml',
89
+ source: 'nearest',
90
+ });
91
+ mockLoadPdConfigForPlugin.mockReturnValue({
92
+ ok: true,
93
+ effective: {},
94
+ source: 'user_config',
95
+ configPath: '/fake/workspace/.pd/config.yaml',
96
+ warnings: [],
97
+ errors: [],
98
+ });
99
+ mockComputeFeatureFlagsFromConfig.mockReturnValue({ flags: {} });
100
+ mockIsFeatureEnabled.mockReturnValue(true);
101
+ // No ~/.codex by default → hooks trust undetectable
102
+ mockExistsSync.mockImplementation((p: string) => !p.includes('.codex'));
103
+ mockConsoleLog.mockImplementation(() => {});
104
+ mockConsoleWarn.mockImplementation(() => {});
105
+ vi.spyOn(console, 'log').mockImplementation(mockConsoleLog);
106
+ vi.spyOn(console, 'warn').mockImplementation(mockConsoleWarn);
107
+ });
108
+
109
+ afterEach(() => {
110
+ process.exitCode = undefined;
111
+ vi.restoreAllMocks();
112
+ });
113
+
114
+ it('cli-1: --json outputs exactly one parseable JSON object on stdout', async () => {
115
+ // ~/.codex exists with trusted hooks for happy path
116
+ mockExistsSync.mockImplementation((p: string) => true);
117
+ mockReadFileSync.mockImplementation((p: string) => {
118
+ if (typeof p === 'string' && p.includes('package.json')) return JSON.stringify({ version: '0.1.0' });
119
+ if (typeof p === 'string' && p.includes('config.toml')) return '[features]\nhooks = true\n';
120
+ return '';
121
+ });
122
+
123
+ await handleHealthCodex({ json: true });
124
+
125
+ expect(mockConsoleLog).toHaveBeenCalledTimes(1);
126
+ const output = mockConsoleLog.mock.calls[0][0] as string;
127
+ const parsed = JSON.parse(output);
128
+ expect(parsed.host).toBe('codex');
129
+ expect(parsed.adapterVersion).toBe('0.1.0');
130
+ expect(parsed.runtimeVersion).toBe('0.1.0');
131
+ expect(parsed.featureFlag.enabled).toBe(true);
132
+ expect(parsed.hooksTrust.detectable).toBe(true);
133
+ expect(parsed.hooksTrust.trusted).toBe(true);
134
+ expect(process.exitCode).toBeUndefined();
135
+ });
136
+
137
+ it('cli-6: when ~/.codex is missing, reports reason + nextAction', async () => {
138
+ mockExistsSync.mockImplementation((p: string) => !p.includes('.codex'));
139
+
140
+ await handleHealthCodex({ json: true });
141
+
142
+ const output = mockConsoleLog.mock.calls[0][0] as string;
143
+ const parsed = JSON.parse(output);
144
+ expect(parsed.hooksTrust.detectable).toBe(false);
145
+ expect(parsed.hooksTrust.reason).toBe('codex_config_dir_not_found');
146
+ expect(parsed.hooksTrust.nextAction).toContain('Install Codex CLI');
147
+ expect(parsed.warnings).toContainEqual(expect.stringContaining('hooks_trust'));
148
+ });
149
+
150
+ it('flag-off + hooks untrusted → exitCode 1', async () => {
151
+ mockIsFeatureEnabled.mockReturnValue(false);
152
+ mockExistsSync.mockImplementation((p: string) => !p.includes('.codex'));
153
+
154
+ await handleHealthCodex({ json: true });
155
+
156
+ expect(process.exitCode).toBe(1);
157
+ const output = mockConsoleLog.mock.calls[0][0] as string;
158
+ const parsed = JSON.parse(output);
159
+ expect(parsed.featureFlag.enabled).toBe(false);
160
+ });
161
+
162
+ it('cli-6: config.toml exists but hooks setting missing → reason + nextAction', async () => {
163
+ mockExistsSync.mockImplementation((p: string) => true);
164
+ mockReadFileSync.mockImplementation((p: string) => {
165
+ if (typeof p === 'string' && p.includes('package.json')) return JSON.stringify({ version: '0.1.0' });
166
+ if (typeof p === 'string' && p.includes('config.toml')) return '[features]\nmodel = "gpt-4"\n';
167
+ return '';
168
+ });
169
+
170
+ await handleHealthCodex({ json: true });
171
+
172
+ const output = mockConsoleLog.mock.calls[0][0] as string;
173
+ const parsed = JSON.parse(output);
174
+ expect(parsed.hooksTrust.detectable).toBe(false);
175
+ expect(parsed.hooksTrust.reason).toBe('hooks_setting_not_found_in_config');
176
+ expect(parsed.hooksTrust.nextAction).toContain('/hooks');
177
+ });
178
+
179
+ it('dual registration: global hooks.json present → detected + reason + nextAction', async () => {
180
+ mockExistsSync.mockImplementation((p: string) => true);
181
+ mockReadFileSync.mockImplementation((p: string) => {
182
+ if (typeof p === 'string' && p.includes('package.json')) return JSON.stringify({ version: '0.1.0' });
183
+ if (typeof p === 'string' && p.includes('config.toml')) return '[features]\nhooks = true\n';
184
+ return '';
185
+ });
186
+
187
+ await handleHealthCodex({ json: true });
188
+
189
+ const output = mockConsoleLog.mock.calls[0][0] as string;
190
+ const parsed = JSON.parse(output);
191
+ expect(parsed.dualRegistration.detected).toBe(true);
192
+ expect(parsed.dualRegistration.globalHooksPath).toContain('hooks.json');
193
+ expect(parsed.dualRegistration.reason).toBe('global_hooks_json_present');
194
+ expect(parsed.dualRegistration.nextAction).toContain('double-registration');
195
+ });
196
+
197
+ it('cli-5: workspace not resolved does not mutate state, reports warning', async () => {
198
+ mockResolveNearestPdWorkspace.mockReturnValue({
199
+ ok: false,
200
+ cwd: '/fake/workspace',
201
+ reason: 'config_not_found',
202
+ nextAction: 'Create .pd/config.yaml',
203
+ });
204
+
205
+ await handleHealthCodex({ json: true });
206
+
207
+ const output = mockConsoleLog.mock.calls[0][0] as string;
208
+ const parsed = JSON.parse(output);
209
+ expect(parsed.workspace).toBe('/fake/workspace');
210
+ expect(parsed.featureFlag.enabled).toBe(false);
211
+ expect(parsed.featureFlag.source).toBe('defaults');
212
+ expect(parsed.warnings).toContainEqual(expect.stringContaining('workspace_not_resolved'));
213
+ });
214
+
215
+ it('text output includes nextAction for degraded hooks trust', async () => {
216
+ mockExistsSync.mockImplementation((p: string) => !p.includes('.codex'));
217
+
218
+ await handleHealthCodex({ json: false });
219
+
220
+ const calls = mockConsoleLog.mock.calls.map((c) => c[0] as string);
221
+ expect(calls.some((s) => s.includes('hooksTrust.nextAction:'))).toBe(true);
222
+ expect(calls.some((s) => s.includes('host: codex'))).toBe(true);
223
+ expect(mockConsoleWarn).toHaveBeenCalled();
224
+ });
225
+
226
+ it('adapter/runtime version unknown when package.json unreadable', async () => {
227
+ mockRequireResolve.mockImplementation(() => {
228
+ throw new Error('not found');
229
+ });
230
+ mockExistsSync.mockImplementation((p: string) => true);
231
+ mockReadFileSync.mockImplementation((p: string) => {
232
+ if (typeof p === 'string' && p.includes('config.toml')) return '[features]\nhooks = true\n';
233
+ return '';
234
+ });
235
+
236
+ await handleHealthCodex({ json: true });
237
+
238
+ const output = mockConsoleLog.mock.calls[0][0] as string;
239
+ const parsed = JSON.parse(output);
240
+ expect(parsed.adapterVersion).toBe('unknown');
241
+ expect(parsed.runtimeVersion).toBe('unknown');
242
+ });
243
+ });
@@ -0,0 +1,76 @@
1
+ /**
2
+ * cli-7 test-wiring: prove the real `registerHealthCommand` (used by the CLI
3
+ * entrypoint) wires the `--host codex` flag, dispatches to handleHealthCodex,
4
+ * and rejects unknown host values with a structured error + non-zero exit.
5
+ *
6
+ * Handlers are injected so the test exercises the real production registration
7
+ * without executing real workspace/DB I/O.
8
+ */
9
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
10
+ import { Command } from 'commander';
11
+ import { registerHealthCommand } from '../../src/commands/health.js';
12
+
13
+ const mockHandleHealth = vi.fn();
14
+ const mockHandleHealthCodex = vi.fn();
15
+
16
+ const originalExitCode = process.exitCode;
17
+
18
+ function buildHealthCommand(): Command {
19
+ const program = new Command();
20
+ registerHealthCommand(program, { health: mockHandleHealth, healthCodex: mockHandleHealthCodex });
21
+ return program;
22
+ }
23
+
24
+ describe('cli-7: pd health --host flag wiring (production registration)', () => {
25
+ beforeEach(() => {
26
+ vi.clearAllMocks();
27
+ process.exitCode = originalExitCode;
28
+ });
29
+
30
+ afterEach(() => {
31
+ process.exitCode = originalExitCode;
32
+ });
33
+
34
+ it('dispatches to handleHealthCodex when --host codex is passed', async () => {
35
+ const program = buildHealthCommand();
36
+ await program.parseAsync(['node', 'pd', 'health', '--host', 'codex', '--json']);
37
+ expect(mockHandleHealthCodex).toHaveBeenCalledTimes(1);
38
+ expect(mockHandleHealthCodex).toHaveBeenCalledWith(expect.objectContaining({ host: 'codex', json: true }));
39
+ expect(mockHandleHealth).not.toHaveBeenCalled();
40
+ });
41
+
42
+ it('dispatches to handleHealth when --host is omitted (defaults to openclaw)', async () => {
43
+ const program = buildHealthCommand();
44
+ await program.parseAsync(['node', 'pd', 'health']);
45
+ expect(mockHandleHealth).toHaveBeenCalledTimes(1);
46
+ expect(mockHandleHealthCodex).not.toHaveBeenCalled();
47
+ });
48
+
49
+ it('dispatches to handleHealth when --host openclaw is passed', async () => {
50
+ const program = buildHealthCommand();
51
+ await program.parseAsync(['node', 'pd', 'health', '--host', 'openclaw']);
52
+ expect(mockHandleHealth).toHaveBeenCalledTimes(1);
53
+ expect(mockHandleHealthCodex).not.toHaveBeenCalled();
54
+ });
55
+
56
+ it('passes --workspace through to handleHealthCodex', async () => {
57
+ const program = buildHealthCommand();
58
+ await program.parseAsync(['node', 'pd', 'health', '--host', 'codex', '-w', '/some/path']);
59
+ expect(mockHandleHealthCodex).toHaveBeenCalledWith(expect.objectContaining({ workspace: '/some/path' }));
60
+ });
61
+
62
+ it('rejects an unknown --host value without dispatching', async () => {
63
+ const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
64
+ const program = buildHealthCommand();
65
+ await program.parseAsync(['node', 'pd', 'health', '--host', 'codxe', '--json']);
66
+ expect(mockHandleHealth).not.toHaveBeenCalled();
67
+ expect(mockHandleHealthCodex).not.toHaveBeenCalled();
68
+ expect(process.exitCode).toBe(1);
69
+ const payload = JSON.parse(String(logSpy.mock.calls[0][0]));
70
+ expect(payload.ok).toBe(false);
71
+ expect(payload.reason).toBe('invalid_host');
72
+ expect(payload.host).toBe('codxe');
73
+ expect(payload.nextAction).toBeTruthy();
74
+ logSpy.mockRestore();
75
+ });
76
+ });
@@ -59,7 +59,7 @@ function writeConfig(intentEnabled: boolean): void {
59
59
  }
60
60
 
61
61
  function getIntentPath(): string {
62
- return path.join(workspaceDir, '.principles', 'INTENT.md');
62
+ return path.join(workspaceDir, '.principles', 'INTENT.zh-CN.md');
63
63
  }
64
64
 
65
65
  const VALID_INTENT = `# INTENT.md
@@ -107,7 +107,7 @@ describe('handleIntentInit', () => {
107
107
  expect(logSpy).toHaveBeenCalled();
108
108
  const jsonOutput = JSON.parse(logSpy.mock.calls[0][0] as string);
109
109
  expect(jsonOutput.status).toBe('dry_run');
110
- expect(jsonOutput.path).toContain('INTENT.md');
110
+ expect(jsonOutput.path).toContain('INTENT.zh-CN.md');
111
111
  expect(jsonOutput.reason).toBe('dry_run');
112
112
  expect(jsonOutput.nextAction).toContain('--confirm');
113
113
 
@@ -177,7 +177,7 @@ describe('handleIntentInit', () => {
177
177
  expect(logSpy).toHaveBeenCalled();
178
178
  const jsonOutput = JSON.parse(logSpy.mock.calls[0][0] as string);
179
179
  expect(jsonOutput.status).toBe('ok');
180
- expect(jsonOutput.path).toContain('INTENT.md');
180
+ expect(jsonOutput.path).toContain('INTENT.zh-CN.md');
181
181
  expect(jsonOutput.overwritten).toBe(false);
182
182
 
183
183
  logSpy.mockRestore();
@@ -311,7 +311,7 @@ describe('handleIntentShow', () => {
311
311
  await handleIntentShow({ workspace: workspaceDir, json: false });
312
312
 
313
313
  const textOutput = logSpy.mock.calls[0][0] as string;
314
- expect(textOutput).toContain('INTENT.md');
314
+ expect(textOutput).toContain('INTENT.zh-CN.md');
315
315
  expect(textOutput).toContain('Content hash:');
316
316
  expect(textOutput).toContain('Last edited:');
317
317
  expect(textOutput).toContain('## 1. Why');
@@ -398,4 +398,84 @@ describe('pd pain record', () => {
398
398
  logSpy.mockRestore();
399
399
  exitSpy.mockRestore();
400
400
  });
401
+
402
+ // ── Bug-P fix: reason length validation ──────────────────────────────────
403
+
404
+ // RL-01: reason length within limit processes normally
405
+ it('RL-01: accepts reason with length ≤ 500 (boundary)', async () => {
406
+ const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
407
+ const exitSpy = mockProcessExit();
408
+
409
+ const reason500 = 'a'.repeat(500);
410
+ await handlePainRecord({ reason: reason500, json: true });
411
+
412
+ expect(exitSpy).not.toHaveBeenCalledWith(1);
413
+ expect(logSpy).toHaveBeenCalled();
414
+
415
+ logSpy.mockRestore();
416
+ exitSpy.mockRestore();
417
+ });
418
+
419
+ // RL-02: reason length over limit exits 1 with error message
420
+ it('RL-02: exits 1 when reason length > 500', async () => {
421
+ const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
422
+ const exitSpy = mockProcessExit();
423
+
424
+ const reason501 = 'a'.repeat(501);
425
+ await handlePainRecord({ reason: reason501 });
426
+
427
+ expect(errorSpy).toHaveBeenCalledWith(expect.stringContaining('must be at most 500 characters'));
428
+ expect(errorSpy).toHaveBeenCalledWith(expect.stringContaining('got 501'));
429
+ expect(errorSpy).toHaveBeenCalledWith(expect.stringContaining('Next action'));
430
+ expect(exitSpy).toHaveBeenCalledWith(1);
431
+
432
+ errorSpy.mockRestore();
433
+ exitSpy.mockRestore();
434
+ });
435
+
436
+ // RL-03: short reason processes normally (smoke test)
437
+ it('RL-03: accepts short reason without error', async () => {
438
+ const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
439
+ const exitSpy = mockProcessExit();
440
+
441
+ await handlePainRecord({ reason: 'short reason', json: true });
442
+
443
+ expect(exitSpy).not.toHaveBeenCalledWith(1);
444
+ expect(logSpy).toHaveBeenCalled();
445
+
446
+ logSpy.mockRestore();
447
+ exitSpy.mockRestore();
448
+ });
449
+
450
+ // RL-04: CodeRabbit review fix — --json mode emits structured JSON (cli-1, cli-2, cli-5)
451
+ it('RL-04: --json mode emits structured JSON on reason_too_long and stops execution', async () => {
452
+ const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
453
+ const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
454
+ const exitSpy = mockProcessExit();
455
+
456
+ const reason501 = 'a'.repeat(501);
457
+ await handlePainRecord({ reason: reason501, json: true });
458
+
459
+ // cli-1: exactly one JSON object on stdout
460
+ expect(logSpy).toHaveBeenCalledTimes(1);
461
+ const jsonOutput = JSON.parse(logSpy.mock.calls[0][0]);
462
+ expect(jsonOutput.status).toBe('failed');
463
+ expect(jsonOutput.reason).toBe('reason_too_long');
464
+ expect(jsonOutput.message).toContain('must be at most 500 characters');
465
+ expect(jsonOutput.message).toContain('got 501');
466
+ expect(jsonOutput.nextAction).toContain('Shorten the reason text');
467
+
468
+ // cli-2: execution stopped — recordPain must NOT have been called
469
+ expect(lastRecordPainInput).toBeNull();
470
+
471
+ // cli-5: failure path exits 1
472
+ expect(exitSpy).toHaveBeenCalledWith(1);
473
+
474
+ // No text leaked to stderr
475
+ expect(errorSpy).not.toHaveBeenCalled();
476
+
477
+ logSpy.mockRestore();
478
+ errorSpy.mockRestore();
479
+ exitSpy.mockRestore();
480
+ });
401
481
  });
@@ -90,6 +90,35 @@ vi.mock('../../src/resolve-workspace.js', () => ({
90
90
  resolveWorkspaceDir: vi.fn().mockReturnValue('/tmp/fake-workspace'),
91
91
  }));
92
92
 
93
+ // BUG-1 (PRI-442): capture runner constructor args to verify effectiveConfig wiring
94
+ const { diagRootCauseRunnerCtor, diagDistillerRunnerCtor, diagRouterRunnerCtor } = vi.hoisted(() => {
95
+ const diagRootCauseRunnerCtor = vi.fn().mockImplementation(function () { return {}; });
96
+ const diagDistillerRunnerCtor = vi.fn().mockImplementation(function () { return {}; });
97
+ const diagRouterRunnerCtor = vi.fn().mockImplementation(function () { return {}; });
98
+ return { diagRootCauseRunnerCtor, diagDistillerRunnerCtor, diagRouterRunnerCtor };
99
+ });
100
+
101
+ vi.mock('../../src/services/pd-config-loader.js', () => ({
102
+ loadPdConfig: vi.fn().mockReturnValue({
103
+ ok: true,
104
+ effective: { config: { featureFlags: { diagnostician_llm_degradation: true } }, source: 'file', warnings: [] },
105
+ defaults: { config: {}, source: 'defaults', warnings: [] },
106
+ }),
107
+ computeFlagsFromLoadResult: vi.fn().mockReturnValue({}),
108
+ }));
109
+
110
+ // Dead-letter store mock: getByPainId returns null by default so the
111
+ // implementation produces status='not_found' with reason='task_not_found'.
112
+ // Tests that need to exercise the dead-letter replay path override this.
113
+ const { MockSqliteDeadLetterStore, mockDeadLetterGetByPainId } = vi.hoisted(() => {
114
+ const mockDeadLetterGetByPainId = vi.fn().mockReturnValue(null);
115
+ class MockSqliteDeadLetterStore {
116
+ getByPainId = mockDeadLetterGetByPainId;
117
+ constructor(_connection: unknown) {}
118
+ }
119
+ return { MockSqliteDeadLetterStore, mockDeadLetterGetByPainId };
120
+ });
121
+
93
122
  vi.mock('@principles/core/runtime-v2', () => {
94
123
  return {
95
124
  RuntimeStateManager: vi.fn().mockImplementation(function () {
@@ -100,12 +129,14 @@ vi.mock('@principles/core/runtime-v2', () => {
100
129
  SqliteDiagnosticianCommitter: vi.fn().mockImplementation(function () { return {}; }),
101
130
  SqliteTrajectoryLocator: vi.fn().mockImplementation(function () { return {}; }),
102
131
  SqliteSourceTraceLocator: vi.fn().mockImplementation(function () { return {}; }),
132
+ SqliteDeadLetterStore: MockSqliteDeadLetterStore,
133
+ PainSignalBridge: vi.fn().mockImplementation(function () { return {}; }),
103
134
  StoreEventEmitter: vi.fn().mockImplementation(function () { return {}; }),
104
135
  storeEmitter: { emitTelemetry: vi.fn() },
105
136
  SplitDiagnosticianRunner: vi.fn().mockImplementation(function () { return {}; }),
106
- DiagRootCauseRunner: vi.fn().mockImplementation(function () { return {}; }),
107
- DiagDistillerRunner: vi.fn().mockImplementation(function () { return {}; }),
108
- DiagRouterRunner: vi.fn().mockImplementation(function () { return {}; }),
137
+ DiagRootCauseRunner: diagRootCauseRunnerCtor,
138
+ DiagDistillerRunner: diagDistillerRunnerCtor,
139
+ DiagRouterRunner: diagRouterRunnerCtor,
109
140
  DefaultDiagRootCauseValidator: vi.fn().mockImplementation(function () { return {}; }),
110
141
  DefaultDiagDistillerValidator: vi.fn().mockImplementation(function () { return {}; }),
111
142
  DisabledDiagnosticianRunner: vi.fn().mockImplementation(function () { return {}; }),
@@ -120,6 +151,15 @@ vi.mock('@principles/core/runtime-v2', () => {
120
151
  }
121
152
  },
122
153
  CandidateIntakeService: MockCandidateIntakeService,
154
+ // PRI-503: admission gate mock — admit by default so existing retry/intake
155
+ // tests keep their original flow. Tests that need to assert refusal behavior
156
+ // can override this mock per-test.
157
+ evaluateCandidateAdmissionFromRecord: vi.fn().mockReturnValue({
158
+ decision: 'admitted',
159
+ reason: 'mock_admitted',
160
+ nextAction: 'none',
161
+ evidenceStatus: 'unknown',
162
+ }),
123
163
  resolveRuntimeConfig: mockResolveRuntimeConfig,
124
164
  isRuntimeConfigError: vi.fn().mockReturnValue(false),
125
165
  isFeatureEnabled: vi.fn().mockReturnValue(true),
@@ -233,6 +273,7 @@ describe('pd pain retry — validation and error paths', () => {
233
273
  mockUpdateCandidateStatus.mockResolvedValue(undefined);
234
274
  mockGetRunsByTask.mockResolvedValue([]);
235
275
  mockIntake.mockReset();
276
+ mockDeadLetterGetByPainId.mockReturnValue(null);
236
277
  mockRun.mockResolvedValue({
237
278
  status: 'succeeded',
238
279
  taskId: 'diagnosis_test-pain-1',
@@ -770,6 +811,7 @@ describe('pd pain retry — human-readable output', () => {
770
811
  mockUpdateCandidateStatus.mockResolvedValue(undefined);
771
812
  mockGetRunsByTask.mockResolvedValue([]);
772
813
  mockIntake.mockReset();
814
+ mockDeadLetterGetByPainId.mockReturnValue(null);
773
815
  mockRun.mockResolvedValue({
774
816
  status: 'succeeded',
775
817
  taskId: 'diagnosis_test-pain-1',
@@ -957,3 +999,52 @@ describe('Commander wiring for pd pain retry', () => {
957
999
  expect(capturedOpts.openclawGateway).toBe(true);
958
1000
  });
959
1001
  });
1002
+
1003
+ // BUG-1 (PRI-442): effectiveConfig must be passed to split-pipeline runners
1004
+ // so that ADR-0019 LLM rate-limit degradation (isDegradationEnabled) can fire.
1005
+ // EP-02: production path wiring.
1006
+ describe('BUG-1 (PRI-442): pain retry — effectiveConfig wiring to split-pipeline runners', () => {
1007
+ beforeEach(() => {
1008
+ vi.clearAllMocks();
1009
+ mockGetCandidatesByTaskId.mockResolvedValue([]);
1010
+ mockUpdateCandidateStatus.mockResolvedValue(undefined);
1011
+ mockGetRunsByTask.mockResolvedValue([]);
1012
+ mockIntake.mockReset();
1013
+ mockRun.mockResolvedValue({
1014
+ status: 'succeeded',
1015
+ taskId: 'diagnosis_test-pain-1',
1016
+ runId: 'run-retry-1',
1017
+ contextHash: 'abc123',
1018
+ });
1019
+ });
1020
+
1021
+ it('passes effectiveConfig to all three runners when split pipeline is enabled', async () => {
1022
+ mockGetTask.mockResolvedValue(RETRY_WAIT_TASK);
1023
+
1024
+ const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
1025
+ const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
1026
+
1027
+ await handlePainRetry({
1028
+ painId: 'test-pain-1',
1029
+ workspace: '/tmp/fake-workspace',
1030
+ runtime: 'test-double',
1031
+ json: true,
1032
+ });
1033
+
1034
+ expect(diagRootCauseRunnerCtor).toHaveBeenCalled();
1035
+ const rootOptions = diagRootCauseRunnerCtor.mock.calls[0]?.[1];
1036
+ expect(rootOptions?.effectiveConfig).toBeDefined();
1037
+ expect(rootOptions?.effectiveConfig).toEqual(
1038
+ expect.objectContaining({ config: expect.anything(), source: 'file' }),
1039
+ );
1040
+
1041
+ const distillerOptions = diagDistillerRunnerCtor.mock.calls[0]?.[1];
1042
+ expect(distillerOptions?.effectiveConfig).toBeDefined();
1043
+
1044
+ const routerOptions = diagRouterRunnerCtor.mock.calls[0]?.[1];
1045
+ expect(routerOptions?.effectiveConfig).toBeDefined();
1046
+
1047
+ logSpy.mockRestore();
1048
+ exitSpy.mockRestore();
1049
+ });
1050
+ });