@principles/pd-cli 1.128.2 → 1.130.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/runtime-canary.d.ts.map +1 -1
- package/dist/commands/runtime-canary.js +10 -11
- package/dist/commands/runtime-canary.js.map +1 -1
- package/dist/commands/runtime-internalization-queue.d.ts.map +1 -1
- package/dist/commands/runtime-internalization-queue.js +13 -16
- package/dist/commands/runtime-internalization-queue.js.map +1 -1
- package/dist/commands/runtime-internalization-run-rulehost.d.ts.map +1 -1
- package/dist/commands/runtime-internalization-run-rulehost.js +61 -7
- package/dist/commands/runtime-internalization-run-rulehost.js.map +1 -1
- package/dist/services/__tests__/rulehost-readiness.test.d.ts +2 -0
- package/dist/services/__tests__/rulehost-readiness.test.d.ts.map +1 -0
- package/dist/services/__tests__/rulehost-readiness.test.js +314 -0
- package/dist/services/__tests__/rulehost-readiness.test.js.map +1 -0
- package/dist/services/__tests__/runtime-adapter-resolver.test.js +20 -12
- package/dist/services/__tests__/runtime-adapter-resolver.test.js.map +1 -1
- package/dist/services/rulehost-readiness.d.ts +62 -0
- package/dist/services/rulehost-readiness.d.ts.map +1 -0
- package/dist/services/rulehost-readiness.js +214 -0
- package/dist/services/rulehost-readiness.js.map +1 -0
- package/dist/services/runtime-adapter-resolver.d.ts +1 -1
- package/dist/services/runtime-adapter-resolver.js +3 -3
- package/dist/services/runtime-adapter-resolver.js.map +1 -1
- package/package.json +1 -1
- package/src/commands/runtime-canary.ts +11 -13
- package/src/commands/runtime-internalization-queue.ts +14 -19
- package/src/commands/runtime-internalization-run-rulehost.ts +68 -6
- package/src/services/__tests__/rulehost-readiness.test.ts +366 -0
- package/src/services/__tests__/runtime-adapter-resolver.test.ts +20 -12
- package/src/services/rulehost-readiness.ts +326 -0
- package/src/services/runtime-adapter-resolver.ts +3 -3
- package/tests/commands/run-rulehost-handler.test.ts +277 -0
- package/tests/commands/runtime-canary.test.ts +46 -36
- package/tests/commands/runtime-internalization-queue.test.ts +11 -24
- package/tests/commands/runtime-internalization-run-once.test.ts +14 -6
- package/tests/services/rulehost-pipeline-e2e.test.ts +71 -61
- package/dist/services/feature-flag-loader.d.ts +0 -6
- package/dist/services/feature-flag-loader.d.ts.map +0 -1
- package/dist/services/feature-flag-loader.js +0 -53
- package/dist/services/feature-flag-loader.js.map +0 -1
- package/src/services/feature-flag-loader.ts +0 -73
- package/tests/services/feature-flag-loader.test.ts +0 -207
|
@@ -46,22 +46,17 @@ vi.mock('@principles/core/runtime-v2', () => ({
|
|
|
46
46
|
resolveOutputLanguage: vi.fn().mockReturnValue({ outputLanguage: 'zh-CN' }),
|
|
47
47
|
}));
|
|
48
48
|
|
|
49
|
-
vi.
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
gfi: { id: 'gfi', category: 'quiet', enabled: true, since: '2026-05-24' },
|
|
58
|
-
},
|
|
59
|
-
warnings: [],
|
|
60
|
-
}),
|
|
49
|
+
const { mockLoadPdConfig, mockComputeFlagsFromLoadResult } = vi.hoisted(() => ({
|
|
50
|
+
mockLoadPdConfig: vi.fn(),
|
|
51
|
+
mockComputeFlagsFromLoadResult: vi.fn(),
|
|
52
|
+
}));
|
|
53
|
+
|
|
54
|
+
vi.mock('../../src/services/pd-config-loader.js', () => ({
|
|
55
|
+
loadPdConfig: mockLoadPdConfig,
|
|
56
|
+
computeFlagsFromLoadResult: mockComputeFlagsFromLoadResult,
|
|
61
57
|
}));
|
|
62
58
|
|
|
63
59
|
import { runCanaryChecks } from '../../src/commands/runtime-canary.js';
|
|
64
|
-
import { loadEffectiveFeatureFlags } from '../../src/services/feature-flag-loader.js';
|
|
65
60
|
|
|
66
61
|
const WS = '/fake/workspace';
|
|
67
62
|
|
|
@@ -126,6 +121,26 @@ describe('runCanaryChecks', () => {
|
|
|
126
121
|
mockAuditConsistency.mockResolvedValue({ status: 'ok', consumedCount: 0, orphanCandidateCount: 0, missingLedgerCount: 0 });
|
|
127
122
|
mockBuildGfiSnapshot.mockReturnValue(healthyGfiSnapshot());
|
|
128
123
|
mockClassifyGfiHealth.mockReturnValue({ status: 'healthy', reason: '0 active, 0 stale sessions', staleGfiDegradedThreshold: 40 });
|
|
124
|
+
// Default: GFI enabled, no warnings (canonical config loader)
|
|
125
|
+
mockLoadPdConfig.mockReturnValue({
|
|
126
|
+
ok: true,
|
|
127
|
+
effective: {},
|
|
128
|
+
source: 'defaults',
|
|
129
|
+
configPath: `${WS}/.pd/config.yaml`,
|
|
130
|
+
warnings: [],
|
|
131
|
+
legacyFilesDetected: [],
|
|
132
|
+
legacyFileNextActions: [],
|
|
133
|
+
});
|
|
134
|
+
mockComputeFlagsFromLoadResult.mockReturnValue({
|
|
135
|
+
flags: {
|
|
136
|
+
prompt: { id: 'prompt', category: 'core', enabled: true },
|
|
137
|
+
code_tool_hook: { id: 'code_tool_hook', category: 'core', enabled: true },
|
|
138
|
+
defer_archive: { id: 'defer_archive', category: 'core', enabled: true },
|
|
139
|
+
gfi: { id: 'gfi', category: 'quiet', enabled: true },
|
|
140
|
+
},
|
|
141
|
+
enabledChannels: ['prompt', 'code_tool_hook', 'defer_archive'],
|
|
142
|
+
warnings: [],
|
|
143
|
+
});
|
|
129
144
|
});
|
|
130
145
|
|
|
131
146
|
it('returns healthy when all checks are healthy', async () => {
|
|
@@ -275,12 +290,11 @@ describe('runCanaryChecks', () => {
|
|
|
275
290
|
|
|
276
291
|
describe('GFI config warning degraded status', () => {
|
|
277
292
|
it('returns healthy when GFI disabled with no warnings', async () => {
|
|
278
|
-
|
|
279
|
-
source: 'defaults',
|
|
280
|
-
configPath: `${WS}/.pd/feature-flags.yaml`,
|
|
293
|
+
mockComputeFlagsFromLoadResult.mockReturnValue({
|
|
281
294
|
flags: {
|
|
282
|
-
gfi: { id: 'gfi', category: 'quiet', enabled: false
|
|
295
|
+
gfi: { id: 'gfi', category: 'quiet', enabled: false },
|
|
283
296
|
},
|
|
297
|
+
enabledChannels: [],
|
|
284
298
|
warnings: [],
|
|
285
299
|
});
|
|
286
300
|
|
|
@@ -292,13 +306,12 @@ describe('runCanaryChecks', () => {
|
|
|
292
306
|
});
|
|
293
307
|
|
|
294
308
|
it('returns degraded when GFI disabled but config has warnings (malformed YAML)', async () => {
|
|
295
|
-
|
|
296
|
-
source: 'defaults',
|
|
297
|
-
configPath: `${WS}/.pd/feature-flags.yaml`,
|
|
309
|
+
mockComputeFlagsFromLoadResult.mockReturnValue({
|
|
298
310
|
flags: {
|
|
299
|
-
gfi: { id: 'gfi', category: 'quiet', enabled: false
|
|
311
|
+
gfi: { id: 'gfi', category: 'quiet', enabled: false },
|
|
300
312
|
},
|
|
301
|
-
|
|
313
|
+
enabledChannels: [],
|
|
314
|
+
warnings: ['.pd/config.yaml: YAML parse error, using defaults'],
|
|
302
315
|
});
|
|
303
316
|
|
|
304
317
|
const result = await runCanaryChecks(WS);
|
|
@@ -307,16 +320,15 @@ describe('runCanaryChecks', () => {
|
|
|
307
320
|
expect(gfiCheck?.status).toBe('degraded');
|
|
308
321
|
expect(gfiCheck?.summary).toContain('warnings');
|
|
309
322
|
expect(gfiCheck?.details).toBeDefined();
|
|
310
|
-
expect(result.recommendedNextActions.some(a => a.includes('
|
|
323
|
+
expect(result.recommendedNextActions.some(a => a.includes('config.yaml'))).toBe(true);
|
|
311
324
|
});
|
|
312
325
|
|
|
313
326
|
it('returns degraded when GFI disabled but config has malformed override warning', async () => {
|
|
314
|
-
|
|
315
|
-
source: 'workspace_file',
|
|
316
|
-
configPath: `${WS}/.pd/feature-flags.yaml`,
|
|
327
|
+
mockComputeFlagsFromLoadResult.mockReturnValue({
|
|
317
328
|
flags: {
|
|
318
|
-
gfi: { id: 'gfi', category: 'quiet', enabled: false
|
|
329
|
+
gfi: { id: 'gfi', category: 'quiet', enabled: false },
|
|
319
330
|
},
|
|
331
|
+
enabledChannels: [],
|
|
320
332
|
warnings: ["flag 'gfi': malformed override kept default (enabled must be boolean)"],
|
|
321
333
|
});
|
|
322
334
|
|
|
@@ -324,16 +336,15 @@ describe('runCanaryChecks', () => {
|
|
|
324
336
|
const gfiCheck = result.checks.find(c => c.name === 'gfi_snapshot');
|
|
325
337
|
|
|
326
338
|
expect(gfiCheck?.status).toBe('degraded');
|
|
327
|
-
expect(result.recommendedNextActions.some(a => a.includes('
|
|
339
|
+
expect(result.recommendedNextActions.some(a => a.includes('config.yaml') || a.includes('pd runtime features'))).toBe(true);
|
|
328
340
|
});
|
|
329
341
|
|
|
330
342
|
it('runs GFI snapshot when flag enabled with no warnings', async () => {
|
|
331
|
-
|
|
332
|
-
source: 'workspace_file',
|
|
333
|
-
configPath: `${WS}/.pd/feature-flags.yaml`,
|
|
343
|
+
mockComputeFlagsFromLoadResult.mockReturnValue({
|
|
334
344
|
flags: {
|
|
335
|
-
gfi: { id: 'gfi', category: 'quiet', enabled: true
|
|
345
|
+
gfi: { id: 'gfi', category: 'quiet', enabled: true },
|
|
336
346
|
},
|
|
347
|
+
enabledChannels: [],
|
|
337
348
|
warnings: [],
|
|
338
349
|
});
|
|
339
350
|
|
|
@@ -346,12 +357,11 @@ describe('runCanaryChecks', () => {
|
|
|
346
357
|
});
|
|
347
358
|
|
|
348
359
|
it('recommends session lifecycle review for GFI session issues (not config)', async () => {
|
|
349
|
-
|
|
350
|
-
source: 'defaults',
|
|
351
|
-
configPath: `${WS}/.pd/feature-flags.yaml`,
|
|
360
|
+
mockComputeFlagsFromLoadResult.mockReturnValue({
|
|
352
361
|
flags: {
|
|
353
|
-
gfi: { id: 'gfi', category: 'quiet', enabled: true
|
|
362
|
+
gfi: { id: 'gfi', category: 'quiet', enabled: true },
|
|
354
363
|
},
|
|
364
|
+
enabledChannels: [],
|
|
355
365
|
warnings: [],
|
|
356
366
|
});
|
|
357
367
|
mockClassifyGfiHealth.mockReturnValue({
|
|
@@ -24,27 +24,14 @@ vi.mock('@principles/core/runtime-v2', () => ({
|
|
|
24
24
|
resolveOutputLanguage: vi.fn().mockReturnValue({ outputLanguage: 'zh-CN' }),
|
|
25
25
|
}));
|
|
26
26
|
|
|
27
|
-
vi.mock('../../src/services/feature-flag-loader.js', () => ({
|
|
28
|
-
loadEffectiveFeatureFlags: vi.fn().mockReturnValue({
|
|
29
|
-
flags: {
|
|
30
|
-
prompt: { id: 'prompt', enabled: true, category: 'core' },
|
|
31
|
-
code_tool_hook: { id: 'code_tool_hook', enabled: true, category: 'core' },
|
|
32
|
-
defer_archive: { id: 'defer_archive', enabled: true, category: 'core' },
|
|
33
|
-
},
|
|
34
|
-
source: 'defaults',
|
|
35
|
-
configPath: '/fake/workspace/.pd/feature-flags.yaml',
|
|
36
|
-
warnings: [],
|
|
37
|
-
}),
|
|
38
|
-
}));
|
|
39
|
-
|
|
40
27
|
const { mockLoadPdConfig, mockComputeFlagsFromLoadResult } = vi.hoisted(() => ({
|
|
41
|
-
mockLoadPdConfig: vi.fn().mockReturnValue({
|
|
28
|
+
mockLoadPdConfig: vi.fn().mockReturnValue({ ok: true, effective: {}, source: 'defaults' }),
|
|
42
29
|
mockComputeFlagsFromLoadResult: vi.fn().mockReturnValue({
|
|
43
30
|
flags: {
|
|
44
31
|
internalization_auto_consumer: { id: 'internalization_auto_consumer', enabled: true, category: 'quiet' },
|
|
45
32
|
},
|
|
46
|
-
|
|
47
|
-
|
|
33
|
+
enabledChannels: ['prompt', 'code_tool_hook', 'defer_archive'],
|
|
34
|
+
warnings: [],
|
|
48
35
|
}),
|
|
49
36
|
}));
|
|
50
37
|
|
|
@@ -89,8 +76,8 @@ describe('handleRuntimeInternalizationQueue', () => {
|
|
|
89
76
|
flags: {
|
|
90
77
|
internalization_auto_consumer: { id: 'internalization_auto_consumer', enabled: true, category: 'quiet' },
|
|
91
78
|
},
|
|
92
|
-
|
|
93
|
-
|
|
79
|
+
enabledChannels: ['prompt', 'code_tool_hook', 'defer_archive'],
|
|
80
|
+
warnings: [],
|
|
94
81
|
});
|
|
95
82
|
});
|
|
96
83
|
|
|
@@ -308,8 +295,8 @@ describe('handleRuntimeInternalizationQueue', () => {
|
|
|
308
295
|
flags: {
|
|
309
296
|
internalization_auto_consumer: { id: 'internalization_auto_consumer', enabled: true, category: 'quiet' },
|
|
310
297
|
},
|
|
311
|
-
|
|
312
|
-
|
|
298
|
+
enabledChannels: ['prompt', 'code_tool_hook', 'defer_archive'],
|
|
299
|
+
warnings: [],
|
|
313
300
|
});
|
|
314
301
|
|
|
315
302
|
await handleRuntimeInternalizationQueue({ workspace: WS, json: true });
|
|
@@ -332,8 +319,8 @@ describe('handleRuntimeInternalizationQueue', () => {
|
|
|
332
319
|
flags: {
|
|
333
320
|
internalization_auto_consumer: { id: 'internalization_auto_consumer', enabled: false, category: 'quiet' },
|
|
334
321
|
},
|
|
335
|
-
|
|
336
|
-
|
|
322
|
+
enabledChannels: ['prompt', 'code_tool_hook', 'defer_archive'],
|
|
323
|
+
warnings: [],
|
|
337
324
|
});
|
|
338
325
|
|
|
339
326
|
await handleRuntimeInternalizationQueue({ workspace: WS, json: true });
|
|
@@ -379,8 +366,8 @@ describe('handleRuntimeInternalizationQueue', () => {
|
|
|
379
366
|
flags: {
|
|
380
367
|
internalization_auto_consumer: { id: 'internalization_auto_consumer', enabled: false, category: 'quiet' },
|
|
381
368
|
},
|
|
382
|
-
|
|
383
|
-
|
|
369
|
+
enabledChannels: ['prompt', 'code_tool_hook', 'defer_archive'],
|
|
370
|
+
warnings: [],
|
|
384
371
|
});
|
|
385
372
|
|
|
386
373
|
await handleRuntimeInternalizationQueue({ workspace: WS, json: false });
|
|
@@ -41,14 +41,22 @@ vi.mock('../../src/services/resolve-runtime-from-pd-config.js', () => ({
|
|
|
41
41
|
resolveRuntimeFromPdConfig: mockResolveRuntimeFromPdConfig,
|
|
42
42
|
}));
|
|
43
43
|
|
|
44
|
-
// PRI-
|
|
45
|
-
// doesn't call the real
|
|
46
|
-
|
|
47
|
-
vi.
|
|
48
|
-
|
|
44
|
+
// PRI-460: Mock pd-config-loader so the shared resolver's L2 dreamer sub-branch
|
|
45
|
+
// doesn't call the real loadPdConfig (which needs fs access).
|
|
46
|
+
vi.mock('../../src/services/pd-config-loader.js', () => ({
|
|
47
|
+
loadPdConfig: vi.fn().mockReturnValue({
|
|
48
|
+
ok: true,
|
|
49
|
+
effective: {},
|
|
50
|
+
source: 'defaults',
|
|
51
|
+
configPath: '/fake/workspace/.pd/config.yaml',
|
|
52
|
+
warnings: [],
|
|
53
|
+
legacyFilesDetected: [],
|
|
54
|
+
legacyFileNextActions: [],
|
|
55
|
+
}),
|
|
56
|
+
computeFlagsFromLoadResult: vi.fn().mockReturnValue({
|
|
49
57
|
flags: {},
|
|
58
|
+
enabledChannels: [],
|
|
50
59
|
warnings: [],
|
|
51
|
-
configPath: '/fake/workspace/.pd/feature-flags.yaml',
|
|
52
60
|
}),
|
|
53
61
|
}));
|
|
54
62
|
|
|
@@ -52,6 +52,18 @@ function writeConfig(workspaceDir: string, content: object): void {
|
|
|
52
52
|
);
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
/** Type guard: narrows `unknown` to `Record<string, unknown>` without `as`. */
|
|
56
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
57
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function requireRecord(value: unknown, label: string): Record<string, unknown> {
|
|
61
|
+
if (!isRecord(value)) {
|
|
62
|
+
throw new Error(`${label} is not a record`);
|
|
63
|
+
}
|
|
64
|
+
return value;
|
|
65
|
+
}
|
|
66
|
+
|
|
55
67
|
/** Config with both artificer+evaluator enabled, pi-ai profile, API key env set. */
|
|
56
68
|
function makeCapabilityOnConfig(workspaceDir: string): object {
|
|
57
69
|
return {
|
|
@@ -90,17 +102,19 @@ function makeCapabilityOnConfig(workspaceDir: string): object {
|
|
|
90
102
|
|
|
91
103
|
/** Config with artificer disabled (capability must be OFF). */
|
|
92
104
|
function makeArtificerDisabledConfig(workspaceDir: string): object {
|
|
93
|
-
const cfg = makeCapabilityOnConfig(workspaceDir)
|
|
94
|
-
const internalAgents = cfg
|
|
95
|
-
|
|
105
|
+
const cfg = makeCapabilityOnConfig(workspaceDir);
|
|
106
|
+
const internalAgents = requireRecord(Reflect.get(cfg, 'internalAgents'), 'internalAgents');
|
|
107
|
+
const agents = requireRecord(Reflect.get(internalAgents, 'agents'), 'internalAgents.agents');
|
|
108
|
+
Reflect.set(agents, 'artificer', { enabled: false });
|
|
96
109
|
return cfg;
|
|
97
110
|
}
|
|
98
111
|
|
|
99
112
|
/** Config with evaluator disabled (capability must be OFF). */
|
|
100
113
|
function makeEvaluatorDisabledConfig(workspaceDir: string): object {
|
|
101
|
-
const cfg = makeCapabilityOnConfig(workspaceDir)
|
|
102
|
-
const internalAgents = cfg
|
|
103
|
-
|
|
114
|
+
const cfg = makeCapabilityOnConfig(workspaceDir);
|
|
115
|
+
const internalAgents = requireRecord(Reflect.get(cfg, 'internalAgents'), 'internalAgents');
|
|
116
|
+
const agents = requireRecord(Reflect.get(internalAgents, 'agents'), 'internalAgents.agents');
|
|
117
|
+
Reflect.set(agents, 'evaluator', { enabled: false });
|
|
104
118
|
return cfg;
|
|
105
119
|
}
|
|
106
120
|
|
|
@@ -158,10 +172,11 @@ describe('runRuleHost production-wiring (PRI-429) — deterministic, no LLM', ()
|
|
|
158
172
|
}
|
|
159
173
|
|
|
160
174
|
/** Extract the single JSON object written to stdout. Fails if not exactly one write. */
|
|
161
|
-
function parseJsonOutput(): unknown {
|
|
175
|
+
function parseJsonOutput(): Record<string, unknown> {
|
|
162
176
|
expect(stdoutSpy).toHaveBeenCalledTimes(1);
|
|
163
177
|
const raw = stdoutSpy.mock.calls[0][0] as string;
|
|
164
|
-
|
|
178
|
+
const parsed: unknown = JSON.parse(raw);
|
|
179
|
+
return requireRecord(parsed, 'stdout JSON');
|
|
165
180
|
}
|
|
166
181
|
|
|
167
182
|
// ── Capability ON: both agents enabled, API key set ──────────────────────
|
|
@@ -178,14 +193,11 @@ describe('runRuleHost production-wiring (PRI-429) — deterministic, no LLM', ()
|
|
|
178
193
|
});
|
|
179
194
|
|
|
180
195
|
const output = parseJsonOutput();
|
|
181
|
-
|
|
182
|
-
expect(output).
|
|
183
|
-
|
|
184
|
-
expect(
|
|
185
|
-
expect(
|
|
186
|
-
expect(obj.codeRuleCapability?.enabled).toBe(true);
|
|
187
|
-
expect(obj.codeRuleCapability?.disabledReason).toBeUndefined();
|
|
188
|
-
expect(obj.capabilityStatus).toContain('ON');
|
|
196
|
+
const codeRuleCapability = requireRecord(output.codeRuleCapability, 'codeRuleCapability');
|
|
197
|
+
expect(output.status).toBe('dry_run');
|
|
198
|
+
expect(codeRuleCapability.enabled).toBe(true);
|
|
199
|
+
expect(codeRuleCapability.disabledReason).toBeUndefined();
|
|
200
|
+
expect(String(output.capabilityStatus)).toContain('ON');
|
|
189
201
|
// exitCode must NOT be set for dry_run success
|
|
190
202
|
expect(process.exitCode).toBeUndefined();
|
|
191
203
|
});
|
|
@@ -203,16 +215,13 @@ describe('runRuleHost production-wiring (PRI-429) — deterministic, no LLM', ()
|
|
|
203
215
|
await handleRunRuleHost({ workspace, painId: 'pain-flag-default', dryRun: true, json: true });
|
|
204
216
|
|
|
205
217
|
const output = parseJsonOutput();
|
|
206
|
-
|
|
207
|
-
expect(
|
|
208
|
-
expect(output).not.toBeNull();
|
|
209
|
-
const obj = output as { status: string; codeRuleCapability?: { enabled: boolean; disabledReason?: string }; capabilityStatus?: string };
|
|
210
|
-
expect(obj.status).toBe('dry_run');
|
|
218
|
+
const codeRuleCapability = requireRecord(output.codeRuleCapability, 'codeRuleCapability');
|
|
219
|
+
expect(output.status).toBe('dry_run');
|
|
211
220
|
// PRI-435: code_rule_capability is now a core flag — defaults ON even when omitted from config.
|
|
212
221
|
// It cannot be disabled by config. The capability is ON when artificer+evaluator are configured.
|
|
213
|
-
expect(
|
|
214
|
-
expect(
|
|
215
|
-
expect(String(
|
|
222
|
+
expect(codeRuleCapability).toEqual(expect.objectContaining({ enabled: true }));
|
|
223
|
+
expect(codeRuleCapability.disabledReason).toBeUndefined();
|
|
224
|
+
expect(String(output.capabilityStatus)).toContain('ON');
|
|
216
225
|
});
|
|
217
226
|
|
|
218
227
|
it('PRI-435: explicit emergency disable via code_rule_capability.enabled=false is observable', async () => {
|
|
@@ -229,16 +238,13 @@ describe('runRuleHost production-wiring (PRI-429) — deterministic, no LLM', ()
|
|
|
229
238
|
await handleRunRuleHost({ workspace, painId: 'pain-emergency-disable', dryRun: true, json: true });
|
|
230
239
|
|
|
231
240
|
const output = parseJsonOutput();
|
|
232
|
-
|
|
233
|
-
expect(
|
|
234
|
-
expect(output).not.toBeNull();
|
|
235
|
-
const obj = output as { status: string; codeRuleCapability?: { enabled: boolean; disabledReason?: string }; capabilityStatus?: string };
|
|
236
|
-
expect(obj.status).toBe('dry_run');
|
|
241
|
+
const codeRuleCapability = requireRecord(output.codeRuleCapability, 'codeRuleCapability');
|
|
242
|
+
expect(output.status).toBe('dry_run');
|
|
237
243
|
// PRI-435: Emergency disable via code_rule_capability.enabled=false is preserved.
|
|
238
244
|
// The capability is OFF with a structured reason.
|
|
239
|
-
expect(
|
|
240
|
-
expect(String(
|
|
241
|
-
expect(String(
|
|
245
|
+
expect(codeRuleCapability).toEqual(expect.objectContaining({ enabled: false }));
|
|
246
|
+
expect(String(codeRuleCapability.disabledReason)).toContain('feature flag');
|
|
247
|
+
expect(String(output.capabilityStatus)).toContain('OFF');
|
|
242
248
|
});
|
|
243
249
|
|
|
244
250
|
it('reports the resolved runtime profile for every executed agent', async () => {
|
|
@@ -271,10 +277,17 @@ describe('runRuleHost production-wiring (PRI-429) — deterministic, no LLM', ()
|
|
|
271
277
|
|
|
272
278
|
await handleRunRuleHost({ workspace, painId: 'pain-disabled-philosopher', confirm: true, json: true });
|
|
273
279
|
|
|
280
|
+
// PRI-461: readiness gate catches disabled required agents BEFORE adapter
|
|
281
|
+
// construction, returning status='refused' with a structured reason instead
|
|
282
|
+
// of the old opaque 'agent_runtime_resolution_failed' error.
|
|
274
283
|
const output = parseJsonOutput();
|
|
275
|
-
expect(output
|
|
276
|
-
|
|
277
|
-
|
|
284
|
+
expect(isRecord(output)).toBe(true);
|
|
285
|
+
if (!isRecord(output)) {
|
|
286
|
+
throw new Error('output is not a record');
|
|
287
|
+
}
|
|
288
|
+
expect(output.status).toBe('refused');
|
|
289
|
+
expect(String(output.reason)).toContain('philosopher');
|
|
290
|
+
expect(typeof output.nextAction).toBe('string');
|
|
278
291
|
expect(process.exitCode).toBe(1);
|
|
279
292
|
expect(fs.existsSync(path.join(workspace, '.state', 'runtime-v2.sqlite'))).toBe(false);
|
|
280
293
|
});
|
|
@@ -308,10 +321,10 @@ describe('runRuleHost production-wiring (PRI-429) — deterministic, no LLM', ()
|
|
|
308
321
|
});
|
|
309
322
|
|
|
310
323
|
const output = parseJsonOutput();
|
|
311
|
-
const
|
|
312
|
-
expect(
|
|
313
|
-
expect(
|
|
314
|
-
expect(
|
|
324
|
+
const codeRuleCapability = requireRecord(output.codeRuleCapability, 'codeRuleCapability');
|
|
325
|
+
expect(output.status).toBe('dry_run');
|
|
326
|
+
expect(codeRuleCapability.enabled).toBe(false);
|
|
327
|
+
expect(String(codeRuleCapability.disabledReason)).toContain('artificer');
|
|
315
328
|
});
|
|
316
329
|
|
|
317
330
|
// ── Capability OFF: evaluator disabled ───────────────────────────────────
|
|
@@ -328,10 +341,10 @@ describe('runRuleHost production-wiring (PRI-429) — deterministic, no LLM', ()
|
|
|
328
341
|
});
|
|
329
342
|
|
|
330
343
|
const output = parseJsonOutput();
|
|
331
|
-
const
|
|
332
|
-
expect(
|
|
333
|
-
expect(
|
|
334
|
-
expect(
|
|
344
|
+
const codeRuleCapability = requireRecord(output.codeRuleCapability, 'codeRuleCapability');
|
|
345
|
+
expect(output.status).toBe('dry_run');
|
|
346
|
+
expect(codeRuleCapability.enabled).toBe(false);
|
|
347
|
+
expect(String(codeRuleCapability.disabledReason)).toContain('evaluator');
|
|
335
348
|
});
|
|
336
349
|
|
|
337
350
|
// ── Capability OFF: API key not set ──────────────────────────────────────
|
|
@@ -393,10 +406,11 @@ describe('runRuleHost production-wiring (PRI-429) — deterministic, no LLM', ()
|
|
|
393
406
|
});
|
|
394
407
|
|
|
395
408
|
const output = parseJsonOutput();
|
|
396
|
-
const
|
|
397
|
-
expect(
|
|
398
|
-
expect(
|
|
399
|
-
expect(
|
|
409
|
+
const codeRuleCapability = requireRecord(output.codeRuleCapability, 'codeRuleCapability');
|
|
410
|
+
expect(output.status).toBe('dry_run');
|
|
411
|
+
expect(codeRuleCapability.enabled).toBe(false);
|
|
412
|
+
expect(String(codeRuleCapability.disabledReason)).toContain('TEST_RULEHOST_ARTIFICER_KEY');
|
|
413
|
+
expect(String(codeRuleCapability.disabledReason)).toContain('not set');
|
|
400
414
|
});
|
|
401
415
|
|
|
402
416
|
// ── CLI gate: mutual exclusivity ─────────────────────────────────────────
|
|
@@ -414,10 +428,9 @@ describe('runRuleHost production-wiring (PRI-429) — deterministic, no LLM', ()
|
|
|
414
428
|
});
|
|
415
429
|
|
|
416
430
|
const output = parseJsonOutput();
|
|
417
|
-
|
|
418
|
-
expect(
|
|
419
|
-
expect(
|
|
420
|
-
expect(obj.nextAction).toBeTruthy();
|
|
431
|
+
expect(output.status).toBe('failed');
|
|
432
|
+
expect(String(output.reason)).toContain('mutually exclusive');
|
|
433
|
+
expect(output.nextAction).toBeTruthy();
|
|
421
434
|
expect(process.exitCode).toBe(1);
|
|
422
435
|
});
|
|
423
436
|
|
|
@@ -435,10 +448,9 @@ describe('runRuleHost production-wiring (PRI-429) — deterministic, no LLM', ()
|
|
|
435
448
|
});
|
|
436
449
|
|
|
437
450
|
const output = parseJsonOutput();
|
|
438
|
-
|
|
439
|
-
expect(
|
|
440
|
-
expect(
|
|
441
|
-
expect(obj.nextAction).toBeTruthy();
|
|
451
|
+
expect(output.status).toBe('failed');
|
|
452
|
+
expect(String(output.reason)).toContain('painId');
|
|
453
|
+
expect(output.nextAction).toBeTruthy();
|
|
442
454
|
expect(process.exitCode).toBe(1);
|
|
443
455
|
});
|
|
444
456
|
|
|
@@ -480,8 +492,7 @@ describe('runRuleHost production-wiring (PRI-429) — deterministic, no LLM', ()
|
|
|
480
492
|
});
|
|
481
493
|
|
|
482
494
|
const output = parseJsonOutput();
|
|
483
|
-
|
|
484
|
-
expect(obj.status).toBe('dry_run');
|
|
495
|
+
expect(output.status).toBe('dry_run');
|
|
485
496
|
// Must NOT set exitCode for dry_run
|
|
486
497
|
expect(process.exitCode).toBeUndefined();
|
|
487
498
|
});
|
|
@@ -501,10 +512,9 @@ describe('runRuleHost production-wiring (PRI-429) — deterministic, no LLM', ()
|
|
|
501
512
|
});
|
|
502
513
|
|
|
503
514
|
const output = parseJsonOutput();
|
|
504
|
-
|
|
505
|
-
expect(
|
|
506
|
-
expect(
|
|
507
|
-
expect(obj.nextAction).toBeTruthy();
|
|
515
|
+
expect(output.status).toBe('failed');
|
|
516
|
+
expect(String(output.reason)).toContain('unsupported channel');
|
|
517
|
+
expect(output.nextAction).toBeTruthy();
|
|
508
518
|
expect(process.exitCode).toBe(1);
|
|
509
519
|
});
|
|
510
520
|
});
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
import type { EffectiveFeatureFlags } from '@principles/core/runtime-v2';
|
|
2
|
-
export declare const FEATURE_FLAGS_CONFIG_FILENAME = "feature-flags.yaml";
|
|
3
|
-
export declare const FEATURE_FLAGS_CONFIG_DIR = ".pd";
|
|
4
|
-
export declare function getFeatureFlagsConfigPath(workspaceDir: string): string;
|
|
5
|
-
export declare function loadEffectiveFeatureFlags(workspaceDir: string): EffectiveFeatureFlags;
|
|
6
|
-
//# sourceMappingURL=feature-flag-loader.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"feature-flag-loader.d.ts","sourceRoot":"","sources":["../../src/services/feature-flag-loader.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,6BAA6B,CAAC;AAEzE,eAAO,MAAM,6BAA6B,uBAAuB,CAAC;AAClE,eAAO,MAAM,wBAAwB,QAAQ,CAAC;AAQ9C,wBAAgB,yBAAyB,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAEtE;AAED,wBAAgB,yBAAyB,CAAC,YAAY,EAAE,MAAM,GAAG,qBAAqB,CAkDrF"}
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
import * as path from 'path';
|
|
2
|
-
import * as fs from 'fs';
|
|
3
|
-
import * as yaml from 'js-yaml';
|
|
4
|
-
import { computeEffectiveFlags, DEFAULT_FEATURE_FLAGS, } from '@principles/core/runtime-v2';
|
|
5
|
-
export const FEATURE_FLAGS_CONFIG_FILENAME = 'feature-flags.yaml';
|
|
6
|
-
export const FEATURE_FLAGS_CONFIG_DIR = '.pd';
|
|
7
|
-
const DANGEROUS_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
|
|
8
|
-
function isRecord(value) {
|
|
9
|
-
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
10
|
-
}
|
|
11
|
-
export function getFeatureFlagsConfigPath(workspaceDir) {
|
|
12
|
-
return path.join(workspaceDir, FEATURE_FLAGS_CONFIG_DIR, FEATURE_FLAGS_CONFIG_FILENAME);
|
|
13
|
-
}
|
|
14
|
-
export function loadEffectiveFeatureFlags(workspaceDir) {
|
|
15
|
-
const configPath = getFeatureFlagsConfigPath(workspaceDir);
|
|
16
|
-
if (!fs.existsSync(configPath)) {
|
|
17
|
-
return computeEffectiveFlags({}, DEFAULT_FEATURE_FLAGS, configPath);
|
|
18
|
-
}
|
|
19
|
-
const raw = fs.readFileSync(configPath, 'utf8');
|
|
20
|
-
let parsed;
|
|
21
|
-
try {
|
|
22
|
-
parsed = yaml.load(raw, { schema: yaml.JSON_SCHEMA });
|
|
23
|
-
}
|
|
24
|
-
catch {
|
|
25
|
-
return {
|
|
26
|
-
...computeEffectiveFlags({}, DEFAULT_FEATURE_FLAGS, configPath),
|
|
27
|
-
warnings: ['feature-flags.yaml: YAML parse error, using defaults'],
|
|
28
|
-
};
|
|
29
|
-
}
|
|
30
|
-
if (parsed === null || parsed === undefined || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
31
|
-
return {
|
|
32
|
-
...computeEffectiveFlags({}, DEFAULT_FEATURE_FLAGS, configPath),
|
|
33
|
-
warnings: ['feature-flags.yaml: expected a mapping, using defaults'],
|
|
34
|
-
};
|
|
35
|
-
}
|
|
36
|
-
const parsedRecord = Object.create(null);
|
|
37
|
-
const warnings = [];
|
|
38
|
-
for (const key of Object.keys(parsed)) {
|
|
39
|
-
if (DANGEROUS_KEYS.has(key)) {
|
|
40
|
-
warnings.push(`feature-flags.yaml: dangerous key '${key}' rejected`);
|
|
41
|
-
continue;
|
|
42
|
-
}
|
|
43
|
-
if (Object.hasOwn(parsed, key) && isRecord(parsed)) {
|
|
44
|
-
parsedRecord[key] = parsed[key];
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
const result = computeEffectiveFlags(parsedRecord, DEFAULT_FEATURE_FLAGS, configPath);
|
|
48
|
-
if (warnings.length > 0) {
|
|
49
|
-
result.warnings = [...warnings, ...result.warnings];
|
|
50
|
-
}
|
|
51
|
-
return result;
|
|
52
|
-
}
|
|
53
|
-
//# sourceMappingURL=feature-flag-loader.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"feature-flag-loader.js","sourceRoot":"","sources":["../../src/services/feature-flag-loader.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,IAAI,MAAM,MAAM,CAAC;AAC7B,OAAO,KAAK,EAAE,MAAM,IAAI,CAAC;AACzB,OAAO,KAAK,IAAI,MAAM,SAAS,CAAC;AAChC,OAAO,EACL,qBAAqB,EACrB,qBAAqB,GACtB,MAAM,6BAA6B,CAAC;AAGrC,MAAM,CAAC,MAAM,6BAA6B,GAAG,oBAAoB,CAAC;AAClE,MAAM,CAAC,MAAM,wBAAwB,GAAG,KAAK,CAAC;AAE9C,MAAM,cAAc,GAAG,IAAI,GAAG,CAAC,CAAC,WAAW,EAAE,aAAa,EAAE,WAAW,CAAC,CAAC,CAAC;AAE1E,SAAS,QAAQ,CAAC,KAAc;IAC9B,OAAO,KAAK,KAAK,IAAI,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;AAC9E,CAAC;AAED,MAAM,UAAU,yBAAyB,CAAC,YAAoB;IAC5D,OAAO,IAAI,CAAC,IAAI,CAAC,YAAY,EAAE,wBAAwB,EAAE,6BAA6B,CAAC,CAAC;AAC1F,CAAC;AAED,MAAM,UAAU,yBAAyB,CAAC,YAAoB;IAC5D,MAAM,UAAU,GAAG,yBAAyB,CAAC,YAAY,CAAC,CAAC;IAE3D,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;QAC/B,OAAO,qBAAqB,CAAC,EAAE,EAAE,qBAAqB,EAAE,UAAU,CAAC,CAAC;IACtE,CAAC;IAED,MAAM,GAAG,GAAG,EAAE,CAAC,YAAY,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC;IAGhD,IAAI,MAAe,CAAC;IACpB,IAAI,CAAC;QACH,MAAM,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,EAAE,MAAM,EAAE,IAAI,CAAC,WAAW,EAAE,CAAC,CAAC;IACxD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO;YACL,GAAG,qBAAqB,CAAC,EAAE,EAAE,qBAAqB,EAAE,UAAU,CAAC;YAC/D,QAAQ,EAAE,CAAC,sDAAsD,CAAC;SACnE,CAAC;IACJ,CAAC;IAED,IAAI,MAAM,KAAK,IAAI,IAAI,MAAM,KAAK,SAAS,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;QACnG,OAAO;YACL,GAAG,qBAAqB,CAAC,EAAE,EAAE,qBAAqB,EAAE,UAAU,CAAC;YAC/D,QAAQ,EAAE,CAAC,wDAAwD,CAAC;SACrE,CAAC;IACJ,CAAC;IAED,MAAM,YAAY,GAA4B,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;IAClE,MAAM,QAAQ,GAAa,EAAE,CAAC;IAC9B,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC;QACtC,IAAI,cAAc,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC;YAC5B,QAAQ,CAAC,IAAI,CAAC,sCAAsC,GAAG,YAAY,CAAC,CAAC;YACrE,SAAS;QACX,CAAC;QACD,IAAI,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,IAAI,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC;YACnD,YAAY,CAAC,GAAG,CAAC,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC;QAClC,CAAC;IACH,CAAC;IAED,MAAM,MAAM,GAAG,qBAAqB,CAClC,YAAY,EACZ,qBAAqB,EACrB,UAAU,CACX,CAAC;IAEF,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACxB,MAAM,CAAC,QAAQ,GAAG,CAAC,GAAG,QAAQ,EAAE,GAAG,MAAM,CAAC,QAAQ,CAAC,CAAC;IACtD,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC"}
|
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
import * as path from 'path';
|
|
2
|
-
import * as fs from 'fs';
|
|
3
|
-
import * as yaml from 'js-yaml';
|
|
4
|
-
import {
|
|
5
|
-
computeEffectiveFlags,
|
|
6
|
-
DEFAULT_FEATURE_FLAGS,
|
|
7
|
-
} from '@principles/core/runtime-v2';
|
|
8
|
-
import type { EffectiveFeatureFlags } from '@principles/core/runtime-v2';
|
|
9
|
-
|
|
10
|
-
export const FEATURE_FLAGS_CONFIG_FILENAME = 'feature-flags.yaml';
|
|
11
|
-
export const FEATURE_FLAGS_CONFIG_DIR = '.pd';
|
|
12
|
-
|
|
13
|
-
const DANGEROUS_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
|
|
14
|
-
|
|
15
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
16
|
-
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export function getFeatureFlagsConfigPath(workspaceDir: string): string {
|
|
20
|
-
return path.join(workspaceDir, FEATURE_FLAGS_CONFIG_DIR, FEATURE_FLAGS_CONFIG_FILENAME);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export function loadEffectiveFeatureFlags(workspaceDir: string): EffectiveFeatureFlags {
|
|
24
|
-
const configPath = getFeatureFlagsConfigPath(workspaceDir);
|
|
25
|
-
|
|
26
|
-
if (!fs.existsSync(configPath)) {
|
|
27
|
-
return computeEffectiveFlags({}, DEFAULT_FEATURE_FLAGS, configPath);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
const raw = fs.readFileSync(configPath, 'utf8');
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
let parsed: unknown;
|
|
34
|
-
try {
|
|
35
|
-
parsed = yaml.load(raw, { schema: yaml.JSON_SCHEMA });
|
|
36
|
-
} catch {
|
|
37
|
-
return {
|
|
38
|
-
...computeEffectiveFlags({}, DEFAULT_FEATURE_FLAGS, configPath),
|
|
39
|
-
warnings: ['feature-flags.yaml: YAML parse error, using defaults'],
|
|
40
|
-
};
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
if (parsed === null || parsed === undefined || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
44
|
-
return {
|
|
45
|
-
...computeEffectiveFlags({}, DEFAULT_FEATURE_FLAGS, configPath),
|
|
46
|
-
warnings: ['feature-flags.yaml: expected a mapping, using defaults'],
|
|
47
|
-
};
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const parsedRecord: Record<string, unknown> = Object.create(null);
|
|
51
|
-
const warnings: string[] = [];
|
|
52
|
-
for (const key of Object.keys(parsed)) {
|
|
53
|
-
if (DANGEROUS_KEYS.has(key)) {
|
|
54
|
-
warnings.push(`feature-flags.yaml: dangerous key '${key}' rejected`);
|
|
55
|
-
continue;
|
|
56
|
-
}
|
|
57
|
-
if (Object.hasOwn(parsed, key) && isRecord(parsed)) {
|
|
58
|
-
parsedRecord[key] = parsed[key];
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
const result = computeEffectiveFlags(
|
|
63
|
-
parsedRecord,
|
|
64
|
-
DEFAULT_FEATURE_FLAGS,
|
|
65
|
-
configPath,
|
|
66
|
-
);
|
|
67
|
-
|
|
68
|
-
if (warnings.length > 0) {
|
|
69
|
-
result.warnings = [...warnings, ...result.warnings];
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
return result;
|
|
73
|
-
}
|