@principles/pd-cli 1.128.2 → 1.129.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-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/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/package.json +1 -1
- package/src/commands/runtime-internalization-run-rulehost.ts +68 -6
- package/src/services/__tests__/rulehost-readiness.test.ts +366 -0
- package/src/services/rulehost-readiness.ts +326 -0
- package/tests/commands/run-rulehost-handler.test.ts +277 -0
- package/tests/services/rulehost-pipeline-e2e.test.ts +71 -61
|
@@ -71,6 +71,11 @@ function parseJsonObject(text: string): Record<string, unknown> {
|
|
|
71
71
|
return parsed;
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
/** Type guard: narrows `unknown` to `Record<string, unknown>` without `as`. */
|
|
75
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
76
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
77
|
+
}
|
|
78
|
+
|
|
74
79
|
function captureStdio(fn: () => Promise<void>): Promise<StdIoState> {
|
|
75
80
|
return new Promise((resolve, reject) => {
|
|
76
81
|
const origExitCode = process.exitCode;
|
|
@@ -251,3 +256,275 @@ describe('handleRunRuleHost — dry-run mode output shape (with minimal pd-confi
|
|
|
251
256
|
}
|
|
252
257
|
});
|
|
253
258
|
});
|
|
259
|
+
|
|
260
|
+
// ── PRI-461: readiness integration tests ──────────────────────────────────
|
|
261
|
+
//
|
|
262
|
+
// Verifies the handler emits the three readiness statuses (ready /
|
|
263
|
+
// text_principle_only / refused) with the correct exit codes and JSON shape.
|
|
264
|
+
// These tests exercise the full path: config → resolveRuleHostReadiness →
|
|
265
|
+
// handler output, ensuring the readiness gate is wired into production code
|
|
266
|
+
// (EP-02) and that refused statuses fail loud with reason + nextAction (EP-03).
|
|
267
|
+
|
|
268
|
+
function writeFullReadyConfig(workspaceDir: string): void {
|
|
269
|
+
const configDir = path.join(workspaceDir, '.pd');
|
|
270
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
271
|
+
const cfg = {
|
|
272
|
+
version: 1,
|
|
273
|
+
features: {
|
|
274
|
+
prompt: { category: 'core', enabled: true },
|
|
275
|
+
code_tool_hook: { category: 'core', enabled: true },
|
|
276
|
+
defer_archive: { category: 'core', enabled: true },
|
|
277
|
+
code_rule_capability: { category: 'core', enabled: true },
|
|
278
|
+
},
|
|
279
|
+
workspace: { default: workspaceDir },
|
|
280
|
+
runtimeProfiles: {
|
|
281
|
+
'pi-ai.default': { type: 'pi-ai', provider: 'anthropic', model: 'claude-sonnet', apiKeyEnv: 'ANTHROPIC_API_KEY' },
|
|
282
|
+
},
|
|
283
|
+
internalAgents: {
|
|
284
|
+
defaultRuntime: 'pi-ai.default',
|
|
285
|
+
agents: {
|
|
286
|
+
dreamer: { enabled: true, runtimeProfile: 'pi-ai.default' },
|
|
287
|
+
philosopher: { enabled: true, runtimeProfile: 'pi-ai.default' },
|
|
288
|
+
scribe: { enabled: true, runtimeProfile: 'pi-ai.default' },
|
|
289
|
+
artificer: { enabled: true, runtimeProfile: 'pi-ai.default' },
|
|
290
|
+
evaluator: { enabled: true, runtimeProfile: 'pi-ai.default' },
|
|
291
|
+
},
|
|
292
|
+
},
|
|
293
|
+
};
|
|
294
|
+
fs.writeFileSync(path.join(configDir, 'config.yaml'), yaml.dump(cfg), 'utf8');
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function writeTextPrincipleOnlyConfig(workspaceDir: string): void {
|
|
298
|
+
// code_rule_capability explicitly OFF → text_principle_only
|
|
299
|
+
const configDir = path.join(workspaceDir, '.pd');
|
|
300
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
301
|
+
const cfg = {
|
|
302
|
+
version: 1,
|
|
303
|
+
features: {
|
|
304
|
+
prompt: { category: 'core', enabled: true },
|
|
305
|
+
code_tool_hook: { category: 'core', enabled: true },
|
|
306
|
+
defer_archive: { category: 'core', enabled: true },
|
|
307
|
+
code_rule_capability: { category: 'core', enabled: false },
|
|
308
|
+
},
|
|
309
|
+
workspace: { default: workspaceDir },
|
|
310
|
+
runtimeProfiles: {
|
|
311
|
+
'pi-ai.default': { type: 'pi-ai', provider: 'anthropic', model: 'claude-sonnet', apiKeyEnv: 'ANTHROPIC_API_KEY' },
|
|
312
|
+
},
|
|
313
|
+
internalAgents: {
|
|
314
|
+
defaultRuntime: 'pi-ai.default',
|
|
315
|
+
agents: {
|
|
316
|
+
dreamer: { enabled: true, runtimeProfile: 'pi-ai.default' },
|
|
317
|
+
philosopher: { enabled: true, runtimeProfile: 'pi-ai.default' },
|
|
318
|
+
scribe: { enabled: true, runtimeProfile: 'pi-ai.default' },
|
|
319
|
+
artificer: { enabled: true, runtimeProfile: 'pi-ai.default' },
|
|
320
|
+
evaluator: { enabled: true, runtimeProfile: 'pi-ai.default' },
|
|
321
|
+
},
|
|
322
|
+
},
|
|
323
|
+
};
|
|
324
|
+
fs.writeFileSync(path.join(configDir, 'config.yaml'), yaml.dump(cfg), 'utf8');
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function writeEvaluatorDisabledConfig(workspaceDir: string): void {
|
|
328
|
+
const configDir = path.join(workspaceDir, '.pd');
|
|
329
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
330
|
+
const cfg = {
|
|
331
|
+
version: 1,
|
|
332
|
+
features: {
|
|
333
|
+
prompt: { category: 'core', enabled: true },
|
|
334
|
+
code_tool_hook: { category: 'core', enabled: true },
|
|
335
|
+
defer_archive: { category: 'core', enabled: true },
|
|
336
|
+
code_rule_capability: { category: 'core', enabled: true },
|
|
337
|
+
},
|
|
338
|
+
workspace: { default: workspaceDir },
|
|
339
|
+
runtimeProfiles: {
|
|
340
|
+
'pi-ai.default': { type: 'pi-ai', provider: 'anthropic', model: 'claude-sonnet', apiKeyEnv: 'ANTHROPIC_API_KEY' },
|
|
341
|
+
},
|
|
342
|
+
internalAgents: {
|
|
343
|
+
defaultRuntime: 'pi-ai.default',
|
|
344
|
+
agents: {
|
|
345
|
+
dreamer: { enabled: true, runtimeProfile: 'pi-ai.default' },
|
|
346
|
+
philosopher: { enabled: true, runtimeProfile: 'pi-ai.default' },
|
|
347
|
+
scribe: { enabled: true, runtimeProfile: 'pi-ai.default' },
|
|
348
|
+
artificer: { enabled: true, runtimeProfile: 'pi-ai.default' },
|
|
349
|
+
evaluator: { enabled: false, runtimeProfile: 'pi-ai.default' },
|
|
350
|
+
},
|
|
351
|
+
},
|
|
352
|
+
};
|
|
353
|
+
fs.writeFileSync(path.join(configDir, 'config.yaml'), yaml.dump(cfg), 'utf8');
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function writeRefusedConfig(workspaceDir: string): void {
|
|
357
|
+
// dreamer disabled → required agent missing → refused
|
|
358
|
+
const configDir = path.join(workspaceDir, '.pd');
|
|
359
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
360
|
+
const cfg = {
|
|
361
|
+
version: 1,
|
|
362
|
+
features: {
|
|
363
|
+
prompt: { category: 'core', enabled: true },
|
|
364
|
+
code_tool_hook: { category: 'core', enabled: true },
|
|
365
|
+
defer_archive: { category: 'core', enabled: true },
|
|
366
|
+
code_rule_capability: { category: 'core', enabled: true },
|
|
367
|
+
},
|
|
368
|
+
workspace: { default: workspaceDir },
|
|
369
|
+
runtimeProfiles: {
|
|
370
|
+
'pi-ai.default': { type: 'pi-ai', provider: 'anthropic', model: 'claude-sonnet', apiKeyEnv: 'ANTHROPIC_API_KEY' },
|
|
371
|
+
},
|
|
372
|
+
internalAgents: {
|
|
373
|
+
defaultRuntime: 'pi-ai.default',
|
|
374
|
+
agents: {
|
|
375
|
+
dreamer: { enabled: false, runtimeProfile: 'pi-ai.default' },
|
|
376
|
+
philosopher: { enabled: true, runtimeProfile: 'pi-ai.default' },
|
|
377
|
+
scribe: { enabled: true, runtimeProfile: 'pi-ai.default' },
|
|
378
|
+
artificer: { enabled: true, runtimeProfile: 'pi-ai.default' },
|
|
379
|
+
evaluator: { enabled: true, runtimeProfile: 'pi-ai.default' },
|
|
380
|
+
},
|
|
381
|
+
},
|
|
382
|
+
};
|
|
383
|
+
fs.writeFileSync(path.join(configDir, 'config.yaml'), yaml.dump(cfg), 'utf8');
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
describe('handleRunRuleHost — PRI-461 readiness integration', () => {
|
|
387
|
+
let workspaceDir: string;
|
|
388
|
+
let savedEnv: NodeJS.ProcessEnv;
|
|
389
|
+
|
|
390
|
+
beforeEach(() => {
|
|
391
|
+
workspaceDir = mkTmpDir();
|
|
392
|
+
savedEnv = { ...process.env };
|
|
393
|
+
process.env.ANTHROPIC_API_KEY = 'sk-ant-test-key';
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
afterEach(() => {
|
|
397
|
+
process.env = savedEnv;
|
|
398
|
+
try { fs.rmSync(workspaceDir, { recursive: true, force: true }); } catch { /* ignore */ }
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
// ── ready ───────────────────────────────────────────────────────────────
|
|
402
|
+
|
|
403
|
+
it('emits readiness=ready in --json dry-run when all agents and code-rule capability are ON', async () => {
|
|
404
|
+
writeFullReadyConfig(workspaceDir);
|
|
405
|
+
const { stdout, exitCode } = await captureStdio(() =>
|
|
406
|
+
handleRunRuleHost({ painId: 'pain-1', dryRun: true, json: true, workspace: workspaceDir }),
|
|
407
|
+
);
|
|
408
|
+
const payload = parseJsonObject(stdout.trim());
|
|
409
|
+
expect(payload.status).toBe('dry_run');
|
|
410
|
+
expect(payload.readinessStatus).toBe('ready');
|
|
411
|
+
const readiness = payload.readiness;
|
|
412
|
+
expect(isRecord(readiness)).toBe(true);
|
|
413
|
+
if (!isRecord(readiness)) {
|
|
414
|
+
throw new Error('readiness is not a record');
|
|
415
|
+
}
|
|
416
|
+
expect(readiness.status).toBe('ready');
|
|
417
|
+
expect(exitCode).toBeUndefined();
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
it('emits readiness=ready in plain-text dry-run output', async () => {
|
|
421
|
+
writeFullReadyConfig(workspaceDir);
|
|
422
|
+
const { stdout } = await captureStdio(() =>
|
|
423
|
+
handleRunRuleHost({ painId: 'pain-1', dryRun: true, workspace: workspaceDir }),
|
|
424
|
+
);
|
|
425
|
+
expect(stdout).toMatch(/readiness:\s*READY/i);
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
// ── text_principle_only ─────────────────────────────────────────────────
|
|
429
|
+
|
|
430
|
+
it('emits readiness=text_principle_only in --json dry-run when code_rule_capability is OFF', async () => {
|
|
431
|
+
writeTextPrincipleOnlyConfig(workspaceDir);
|
|
432
|
+
const { stdout, exitCode } = await captureStdio(() =>
|
|
433
|
+
handleRunRuleHost({ painId: 'pain-1', dryRun: true, json: true, workspace: workspaceDir }),
|
|
434
|
+
);
|
|
435
|
+
const payload = parseJsonObject(stdout.trim());
|
|
436
|
+
expect(payload.status).toBe('dry_run');
|
|
437
|
+
expect(payload.readinessStatus).toBe('text_principle_only');
|
|
438
|
+
const readiness = payload.readiness;
|
|
439
|
+
expect(isRecord(readiness)).toBe(true);
|
|
440
|
+
if (!isRecord(readiness)) {
|
|
441
|
+
throw new Error('readiness is not a record');
|
|
442
|
+
}
|
|
443
|
+
expect(readiness.status).toBe('text_principle_only');
|
|
444
|
+
expect(typeof readiness.reason).toBe('string');
|
|
445
|
+
expect(typeof readiness.nextAction).toBe('string');
|
|
446
|
+
expect(exitCode).toBeUndefined();
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
it('does not reclassify evaluator-disabled text_principle_only as runtime resolution failure', async () => {
|
|
450
|
+
writeEvaluatorDisabledConfig(workspaceDir);
|
|
451
|
+
const { stdout, exitCode } = await captureStdio(() =>
|
|
452
|
+
handleRunRuleHost({ painId: 'pain-1', dryRun: true, json: true, workspace: workspaceDir }),
|
|
453
|
+
);
|
|
454
|
+
const payload = parseJsonObject(stdout.trim());
|
|
455
|
+
expect(payload.status).toBe('dry_run');
|
|
456
|
+
expect(payload.readinessStatus).toBe('text_principle_only');
|
|
457
|
+
const readiness = payload.readiness;
|
|
458
|
+
expect(isRecord(readiness)).toBe(true);
|
|
459
|
+
if (!isRecord(readiness)) {
|
|
460
|
+
throw new Error('readiness is not a record');
|
|
461
|
+
}
|
|
462
|
+
expect(readiness.status).toBe('text_principle_only');
|
|
463
|
+
expect(String(readiness.reason)).toContain('evaluator');
|
|
464
|
+
expect(String(payload.capabilityStatus)).toContain('evaluator');
|
|
465
|
+
expect(exitCode).toBeUndefined();
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
it('emits readiness=text_principle_only in plain-text dry-run output', async () => {
|
|
469
|
+
writeTextPrincipleOnlyConfig(workspaceDir);
|
|
470
|
+
const { stdout } = await captureStdio(() =>
|
|
471
|
+
handleRunRuleHost({ painId: 'pain-1', dryRun: true, workspace: workspaceDir }),
|
|
472
|
+
);
|
|
473
|
+
expect(stdout).toMatch(/readiness:\s*TEXT_PRINCIPLE_ONLY/i);
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
// ── refused ─────────────────────────────────────────────────────────────
|
|
477
|
+
|
|
478
|
+
it('exits with code=1 and emits status=refused in --json when dreamer is disabled', async () => {
|
|
479
|
+
writeRefusedConfig(workspaceDir);
|
|
480
|
+
const { stdout, exitCode } = await captureStdio(() =>
|
|
481
|
+
handleRunRuleHost({ painId: 'pain-1', dryRun: true, json: true, workspace: workspaceDir }),
|
|
482
|
+
);
|
|
483
|
+
const payload = parseJsonObject(stdout.trim());
|
|
484
|
+
expect(payload.status).toBe('refused');
|
|
485
|
+
expect(typeof payload.reason).toBe('string');
|
|
486
|
+
expect(typeof payload.nextAction).toBe('string');
|
|
487
|
+
expect(exitCode).toBe(1);
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
it('exits with code=1 and emits REFUSED in plain-text when dreamer is disabled', async () => {
|
|
491
|
+
writeRefusedConfig(workspaceDir);
|
|
492
|
+
const { stderr, exitCode } = await captureStdio(() =>
|
|
493
|
+
handleRunRuleHost({ painId: 'pain-1', dryRun: true, workspace: workspaceDir }),
|
|
494
|
+
);
|
|
495
|
+
expect(stderr).toMatch(/REFUSED/i);
|
|
496
|
+
expect(stderr).toMatch(/dreamer/i);
|
|
497
|
+
expect(exitCode).toBe(1);
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
it('refused status includes the full readiness object in --json output', async () => {
|
|
501
|
+
writeRefusedConfig(workspaceDir);
|
|
502
|
+
const { stdout } = await captureStdio(() =>
|
|
503
|
+
handleRunRuleHost({ painId: 'pain-1', dryRun: true, json: true, workspace: workspaceDir }),
|
|
504
|
+
);
|
|
505
|
+
const payload = parseJsonObject(stdout.trim());
|
|
506
|
+
expect(payload.readiness).toBeDefined();
|
|
507
|
+
const readiness = payload.readiness;
|
|
508
|
+
expect(isRecord(readiness)).toBe(true);
|
|
509
|
+
if (!isRecord(readiness)) {
|
|
510
|
+
throw new Error('readiness is not a record');
|
|
511
|
+
}
|
|
512
|
+
expect(readiness.status).toBe('refused');
|
|
513
|
+
expect(readiness.agentStatuses).toBeDefined();
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
it('refused status does NOT attempt pipeline execution or adapter construction', async () => {
|
|
517
|
+
// If the handler tried to construct adapters with a disabled dreamer,
|
|
518
|
+
// resolveRunRuleHostRuntime would throw. The readiness gate must prevent
|
|
519
|
+
// that by exiting before resolveRunRuleHostRuntime is called.
|
|
520
|
+
writeRefusedConfig(workspaceDir);
|
|
521
|
+
const { stdout, exitCode } = await captureStdio(() =>
|
|
522
|
+
handleRunRuleHost({ painId: 'pain-1', dryRun: true, json: true, workspace: workspaceDir }),
|
|
523
|
+
);
|
|
524
|
+
const payload = parseJsonObject(stdout.trim());
|
|
525
|
+
// Must be 'refused', NOT 'failed' with 'agent_runtime_resolution_failed'
|
|
526
|
+
expect(payload.status).toBe('refused');
|
|
527
|
+
expect(payload.reason).not.toMatch(/agent_runtime_resolution_failed/);
|
|
528
|
+
expect(exitCode).toBe(1);
|
|
529
|
+
});
|
|
530
|
+
});
|
|
@@ -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
|
});
|