principles-disciple 1.86.0 → 1.88.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/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/src/core/pd-config-loader.ts +400 -0
- package/src/core/runtime-v2-prompt-activation-reader.ts +15 -63
- package/src/core/surface-guard.ts +62 -4
- package/src/index.ts +8 -56
- package/src/service/correction-observer-service.ts +62 -31
- package/tests/core/pd-config-loader.test.ts +407 -0
- package/tests/core/surface-guard.test.ts +142 -0
- package/tests/core-anti-growth.test.ts +1 -0
- package/tests/evolution-worker-quarantine.test.ts +83 -27
- package/tests/evolution-worker-slimming.test.ts +63 -5
- package/tests/hooks/runtime-v2-prompt-activation.test.ts +9 -3
- package/tests/integration/mvp-surface-registry-guard.test.ts +131 -1
- package/tests/service/correction-observer-service.test.ts +147 -21
- package/tests/service/evolution-worker.correction-observer.test.ts +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
1
|
+
import { describe, expect, it, beforeEach } from 'vitest';
|
|
2
2
|
import * as fs from 'fs';
|
|
3
3
|
import * as path from 'path';
|
|
4
4
|
import {
|
|
@@ -109,6 +109,42 @@ describe('MVP Surface Registry Guard (PRI-289)', () => {
|
|
|
109
109
|
expect(surface.disabledReason!.length).toBeGreaterThan(0);
|
|
110
110
|
}
|
|
111
111
|
});
|
|
112
|
+
|
|
113
|
+
it('no disabledReason references Story A / Story A\' / MVP 验收 / 测试任务 (PRI-298)', () => {
|
|
114
|
+
const disabled = PLUGIN_SURFACE_REGISTRY.filter(
|
|
115
|
+
s => s.category === 'quiet' || s.category === 'gone' || s.category === 'legacy_retire',
|
|
116
|
+
);
|
|
117
|
+
expect(disabled.length).toBeGreaterThan(0);
|
|
118
|
+
for (const surface of disabled) {
|
|
119
|
+
expect(surface.disabledReason).toBeDefined();
|
|
120
|
+
expect(surface.disabledReason).not.toMatch(/Story A/);
|
|
121
|
+
expect(surface.disabledReason).not.toMatch(/MVP\s*验收/);
|
|
122
|
+
expect(surface.disabledReason).not.toMatch(/测试任务/);
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('disabledReason copy is opt-in / feature-flag oriented for quiet surfaces (PRI-298)', () => {
|
|
127
|
+
const quiet = PLUGIN_SURFACE_REGISTRY.filter(s => s.category === 'quiet');
|
|
128
|
+
expect(quiet.length).toBeGreaterThan(0);
|
|
129
|
+
for (const surface of quiet) {
|
|
130
|
+
// Every quiet surface should anchor its reason in at least one
|
|
131
|
+
// stable, long-lived framing so the log copy can live in the product
|
|
132
|
+
// long after MVP. Acceptable framings:
|
|
133
|
+
// - opt-in / disabled language (new quiet entries),
|
|
134
|
+
// - feature-flag path (most existing entries),
|
|
135
|
+
// - ADR reference (entries gated by a specific ADR section).
|
|
136
|
+
// What we still reject: ephemeral MVP-phase copy (covered by the
|
|
137
|
+
// Story A / MVP 验收 test above).
|
|
138
|
+
const reason = surface.disabledReason!.toLowerCase();
|
|
139
|
+
const hasOptInOrDisabled = /opt-?in|disabled/.test(reason);
|
|
140
|
+
const hasFeatureFlag = reason.includes('feature flag');
|
|
141
|
+
const hasAdrReference = /adr-?\d+|adr\s+\d+/.test(reason);
|
|
142
|
+
expect(
|
|
143
|
+
hasOptInOrDisabled || hasFeatureFlag || hasAdrReference,
|
|
144
|
+
`quiet surface ${surface.id} disabledReason must reference opt-in, feature flag, or an ADR: "${surface.disabledReason}"`,
|
|
145
|
+
).toBe(true);
|
|
146
|
+
}
|
|
147
|
+
});
|
|
112
148
|
});
|
|
113
149
|
|
|
114
150
|
describe('api.on() registration coverage — every hook must be guarded', () => {
|
|
@@ -292,6 +328,17 @@ describe('MVP Surface Registry Guard (PRI-289)', () => {
|
|
|
292
328
|
});
|
|
293
329
|
|
|
294
330
|
describe('surface guard runtime', () => {
|
|
331
|
+
let resetSurfaceGuardLogState: () => void;
|
|
332
|
+
|
|
333
|
+
beforeEach(async () => {
|
|
334
|
+
// Lazy import so the module state is freshly required per describe and
|
|
335
|
+
// we can reset the PRI-298 rate-limit bookkeeping before every runtime
|
|
336
|
+
// assertion that depends on the first-fire log firing.
|
|
337
|
+
const mod = await import('../../src/core/surface-guard.js');
|
|
338
|
+
resetSurfaceGuardLogState = mod.__resetSurfaceGuardSkipLogStateForTests;
|
|
339
|
+
resetSurfaceGuardLogState();
|
|
340
|
+
});
|
|
341
|
+
|
|
295
342
|
it('checkSurfaceGuard passes with current registry', async () => {
|
|
296
343
|
const { checkSurfaceGuard } = await import('../../src/core/surface-guard.js');
|
|
297
344
|
const result = checkSurfaceGuard();
|
|
@@ -403,5 +450,88 @@ describe('MVP Surface Registry Guard (PRI-289)', () => {
|
|
|
403
450
|
const guarded = guardService('service:nonexistent_service', service);
|
|
404
451
|
expect(guarded).toBeNull();
|
|
405
452
|
});
|
|
453
|
+
|
|
454
|
+
it('PRI-298 rate-limit: quiet surface logs once, not per invocation', async () => {
|
|
455
|
+
const { guardHook } = await import('../../src/core/surface-guard.js');
|
|
456
|
+
const logs: string[] = [];
|
|
457
|
+
const logger = { info: (msg: string) => { logs.push(msg); } };
|
|
458
|
+
const handler = () => 'result';
|
|
459
|
+
const guarded = guardHook('hook:after_tool_call.trajectory', logger, handler);
|
|
460
|
+
|
|
461
|
+
// First invocation must surface the disabled reason.
|
|
462
|
+
guarded({} as never, {} as never);
|
|
463
|
+
expect(logs.length).toBe(1);
|
|
464
|
+
expect(logs[0]).toContain('[PD:surface-guard] SKIP');
|
|
465
|
+
expect(logs[0]).toContain('hook:after_tool_call.trajectory');
|
|
466
|
+
|
|
467
|
+
// Subsequent invocations on the same surfaceId stay silent.
|
|
468
|
+
for (let i = 0; i < 10; i += 1) {
|
|
469
|
+
guarded({} as never, {} as never);
|
|
470
|
+
}
|
|
471
|
+
expect(logs.length).toBe(1);
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
it('PRI-298 rate-limit: resetSurfaceGuardSkipLogStateForTests re-arms first-fire', async () => {
|
|
475
|
+
const { guardHook } = await import('../../src/core/surface-guard.js');
|
|
476
|
+
const logs: string[] = [];
|
|
477
|
+
const logger = { info: (msg: string) => { logs.push(msg); } };
|
|
478
|
+
const handler = () => 'result';
|
|
479
|
+
const guarded = guardHook('hook:after_tool_call.trajectory', logger, handler);
|
|
480
|
+
|
|
481
|
+
guarded({} as never, {} as never);
|
|
482
|
+
expect(logs.length).toBe(1);
|
|
483
|
+
|
|
484
|
+
// Additional fires on the same surface: still 1 log.
|
|
485
|
+
guarded({} as never, {} as never);
|
|
486
|
+
expect(logs.length).toBe(1);
|
|
487
|
+
|
|
488
|
+
// Reset the per-process bookkeeping (simulating a fresh process / test
|
|
489
|
+
// isolation). The next fire on a freshly-constructed guarded handler
|
|
490
|
+
// should log again.
|
|
491
|
+
resetSurfaceGuardLogState();
|
|
492
|
+
const guarded2 = guardHook('hook:after_tool_call.trajectory', logger, handler);
|
|
493
|
+
guarded2({} as never, {} as never);
|
|
494
|
+
expect(logs.length).toBe(2);
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
it('PRI-298 / chatgpt P2: guardHook does NOT log at construction time', async () => {
|
|
498
|
+
const { guardHook } = await import('../../src/core/surface-guard.js');
|
|
499
|
+
const logs: string[] = [];
|
|
500
|
+
const logger = { info: (msg: string) => { logs.push(msg); } };
|
|
501
|
+
// The act of constructing the guard must not emit a SKIP line. Plugin
|
|
502
|
+
// startup that registers 7 quiet hooks would otherwise log 7 SKIP
|
|
503
|
+
// lines before any real traffic.
|
|
504
|
+
guardHook('hook:after_tool_call.trajectory', logger, () => 'result');
|
|
505
|
+
expect(logs.length).toBe(0);
|
|
506
|
+
|
|
507
|
+
// The first INVOCATION is when the log fires (and only once).
|
|
508
|
+
const guarded = guardHook('hook:llm_output.trajectory', logger, () => 'result');
|
|
509
|
+
guarded({} as never, {} as never);
|
|
510
|
+
expect(logs.length).toBe(1);
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
it('PRI-298 / coderabbit Major: guardHook logger undefined on first fire does not consume the one-shot slot', async () => {
|
|
514
|
+
const { guardHook } = await import('../../src/core/surface-guard.js');
|
|
515
|
+
|
|
516
|
+
// First call: no logger. The no-op suppresses the handler, but the
|
|
517
|
+
// once-only slot is preserved (a missing logger must not eat the
|
|
518
|
+
// chance to surface the disabled reason later).
|
|
519
|
+
const handler1 = guardHook('hook:after_tool_call.trajectory', undefined, () => 'result');
|
|
520
|
+
handler1({} as never, {} as never);
|
|
521
|
+
|
|
522
|
+
// Second call: real logger. This is now the first log emission for
|
|
523
|
+
// this surfaceId.
|
|
524
|
+
const logs: string[] = [];
|
|
525
|
+
const logger = { info: (msg: string) => { logs.push(msg); } };
|
|
526
|
+
const handler2 = guardHook('hook:after_tool_call.trajectory', logger, () => 'result');
|
|
527
|
+
handler2({} as never, {} as never);
|
|
528
|
+
expect(logs.length).toBe(1);
|
|
529
|
+
expect(logs[0]).toContain('[PD:surface-guard] SKIP');
|
|
530
|
+
|
|
531
|
+
// Third call: slot is now consumed; the third call is silent.
|
|
532
|
+
const handler3 = guardHook('hook:after_tool_call.trajectory', logger, () => 'result');
|
|
533
|
+
handler3({} as never, {} as never);
|
|
534
|
+
expect(logs.length).toBe(1);
|
|
535
|
+
});
|
|
406
536
|
});
|
|
407
537
|
});
|
|
@@ -2,6 +2,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
|
2
2
|
import * as fs from 'fs';
|
|
3
3
|
import * as os from 'os';
|
|
4
4
|
import * as path from 'path';
|
|
5
|
+
import * as yaml from 'js-yaml';
|
|
5
6
|
import { WorkspaceContext } from '../../src/core/workspace-context.js';
|
|
6
7
|
|
|
7
8
|
const mockLearner = {
|
|
@@ -35,6 +36,89 @@ vi.mock('../../src/service/keyword-optimization-service.js', () => ({
|
|
|
35
36
|
KeywordOptimizationService: { get: vi.fn(() => mockOptimizationService) },
|
|
36
37
|
}));
|
|
37
38
|
|
|
39
|
+
// PRI-307: Mock the pd-config-loader instead of @principles/core/runtime-v2
|
|
40
|
+
// The service now reads .pd/config.yaml via resolveObserverConfig
|
|
41
|
+
vi.mock('../../src/core/pd-config-loader.js', () => {
|
|
42
|
+
return {
|
|
43
|
+
loadPdConfigForPlugin: vi.fn(() => ({
|
|
44
|
+
ok: true,
|
|
45
|
+
effective: {
|
|
46
|
+
config: {
|
|
47
|
+
version: 1,
|
|
48
|
+
features: {
|
|
49
|
+
prompt: { category: 'core', enabled: true },
|
|
50
|
+
code_tool_hook: { category: 'core', enabled: true },
|
|
51
|
+
defer_archive: { category: 'core', enabled: true },
|
|
52
|
+
correction_observer: { category: 'quiet', enabled: true },
|
|
53
|
+
empathy_observer: { category: 'quiet', enabled: false },
|
|
54
|
+
},
|
|
55
|
+
runtimeProfiles: {
|
|
56
|
+
'openclaw.default': { type: 'openclaw', source: 'default' },
|
|
57
|
+
'pd.anthropic-sonnet': { type: 'pi-ai', provider: 'anthropic', model: 'claude-3-5-sonnet', apiKeyEnv: 'ANTHROPIC_API_KEY', timeoutMs: 30000 },
|
|
58
|
+
},
|
|
59
|
+
internalAgents: {
|
|
60
|
+
defaultRuntime: 'openclaw.default',
|
|
61
|
+
agents: {
|
|
62
|
+
diagnostician: { enabled: true },
|
|
63
|
+
dreamer: { enabled: true },
|
|
64
|
+
scribe: { enabled: true },
|
|
65
|
+
artificer: { enabled: true },
|
|
66
|
+
philosopher: { enabled: false },
|
|
67
|
+
evaluator: { enabled: false },
|
|
68
|
+
rolloutReviewer: { enabled: false },
|
|
69
|
+
trainer: { enabled: false },
|
|
70
|
+
correctionObserver: { enabled: true, runtimeProfile: 'pd.anthropic-sonnet' },
|
|
71
|
+
empathyObserver: { enabled: false },
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
warnings: [],
|
|
76
|
+
},
|
|
77
|
+
source: 'defaults',
|
|
78
|
+
configPath: '.pd/config.yaml',
|
|
79
|
+
warnings: [],
|
|
80
|
+
errors: [],
|
|
81
|
+
})),
|
|
82
|
+
loadFeatureFlagFromConfig: vi.fn(() => ({ enabled: true, source: 'defaults' })),
|
|
83
|
+
resolveObserverConfig: vi.fn((_workspaceDir: string, flagId: string, _agentName: string) => {
|
|
84
|
+
// Default: return disabled for correction_observer (no config file in test tmp dirs)
|
|
85
|
+
if (flagId === 'correction_observer') {
|
|
86
|
+
return {
|
|
87
|
+
enabled: true,
|
|
88
|
+
readiness: 'not_ready',
|
|
89
|
+
source: 'defaults',
|
|
90
|
+
reason: 'pi-ai profile configured with apiKeyEnv',
|
|
91
|
+
nextAction: 'Run pd runtime probe',
|
|
92
|
+
runtimeProfileId: 'pd.anthropic-sonnet',
|
|
93
|
+
runtimeProfileType: 'pi-ai',
|
|
94
|
+
apiKeyEnv: 'ANTHROPIC_API_KEY',
|
|
95
|
+
apiKeyPresent: !!process.env.ANTHROPIC_API_KEY,
|
|
96
|
+
provider: 'anthropic',
|
|
97
|
+
model: 'claude-3-5-sonnet',
|
|
98
|
+
timeoutMs: 30000,
|
|
99
|
+
baseUrl: null,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
return {
|
|
103
|
+
enabled: false,
|
|
104
|
+
readiness: 'disabled',
|
|
105
|
+
source: 'defaults',
|
|
106
|
+
reason: `${flagId} is disabled`,
|
|
107
|
+
nextAction: `Set features.${flagId}.enabled=true in .pd/config.yaml`,
|
|
108
|
+
runtimeProfileId: null,
|
|
109
|
+
runtimeProfileType: null,
|
|
110
|
+
apiKeyEnv: null,
|
|
111
|
+
apiKeyPresent: false,
|
|
112
|
+
provider: null,
|
|
113
|
+
model: null,
|
|
114
|
+
timeoutMs: null,
|
|
115
|
+
baseUrl: null,
|
|
116
|
+
};
|
|
117
|
+
}),
|
|
118
|
+
getPdConfigPath: vi.fn((workspaceDir: string) => path.join(workspaceDir, '.pd', 'config.yaml')),
|
|
119
|
+
};
|
|
120
|
+
});
|
|
121
|
+
|
|
38
122
|
const mockDispatch = vi.fn().mockResolvedValue({
|
|
39
123
|
updated: true,
|
|
40
124
|
summary: 'Keyword store optimized',
|
|
@@ -45,23 +129,12 @@ const mockRegister = vi.fn();
|
|
|
45
129
|
|
|
46
130
|
vi.mock('@principles/core/runtime-v2', () => {
|
|
47
131
|
return {
|
|
48
|
-
WorkflowFunnelLoader: class {
|
|
49
|
-
getFunnel = vi.fn(() => ({
|
|
50
|
-
policy: {
|
|
51
|
-
runtimeKind: 'pi-ai',
|
|
52
|
-
provider: 'anthropic',
|
|
53
|
-
model: 'anthropic/claude-3-5-sonnet',
|
|
54
|
-
apiKeyEnv: 'ANTHROPIC_API_KEY',
|
|
55
|
-
timeoutMs: 30000,
|
|
56
|
-
}
|
|
57
|
-
}));
|
|
58
|
-
},
|
|
59
132
|
PiAiRuntimeAdapter: class {},
|
|
60
133
|
CorrectionObserver: class {},
|
|
61
134
|
AgentScheduler: class {
|
|
62
135
|
register = mockRegister;
|
|
63
136
|
dispatch = mockDispatch;
|
|
64
|
-
}
|
|
137
|
+
},
|
|
65
138
|
};
|
|
66
139
|
});
|
|
67
140
|
|
|
@@ -330,12 +403,12 @@ describe('runCorrectionObserverCycle — Independent Execution', () => {
|
|
|
330
403
|
});
|
|
331
404
|
});
|
|
332
405
|
|
|
333
|
-
describe('resolveCorrectionObserver — Configuration Resolution', () => {
|
|
406
|
+
describe('resolveCorrectionObserver — Configuration Resolution (PRI-307)', () => {
|
|
334
407
|
beforeEach(() => {
|
|
335
408
|
vi.clearAllMocks();
|
|
336
409
|
});
|
|
337
410
|
|
|
338
|
-
it('returns observer when API key env is set with
|
|
411
|
+
it('returns observer when API key env is set with pi-ai profile', async () => {
|
|
339
412
|
const workspaceDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pd-corr-resolve-'));
|
|
340
413
|
const stateDir = path.join(workspaceDir, '.state');
|
|
341
414
|
fs.mkdirSync(stateDir, { recursive: true });
|
|
@@ -348,7 +421,7 @@ describe('resolveCorrectionObserver — Configuration Resolution', () => {
|
|
|
348
421
|
const wctx = WorkspaceContext.fromHookContext({ workspaceDir });
|
|
349
422
|
const result = resolveCorrectionObserver(wctx, logger as any);
|
|
350
423
|
|
|
351
|
-
// With mocked
|
|
424
|
+
// With mocked resolveObserverConfig returning enabled + not_ready, should return observer
|
|
352
425
|
expect(result).not.toBeNull();
|
|
353
426
|
} finally {
|
|
354
427
|
delete process.env.ANTHROPIC_API_KEY;
|
|
@@ -356,23 +429,76 @@ describe('resolveCorrectionObserver — Configuration Resolution', () => {
|
|
|
356
429
|
}
|
|
357
430
|
});
|
|
358
431
|
|
|
359
|
-
it('returns
|
|
360
|
-
const workspaceDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pd-corr-
|
|
432
|
+
it('returns null when observer is disabled in config', async () => {
|
|
433
|
+
const workspaceDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pd-corr-disabled-'));
|
|
361
434
|
const stateDir = path.join(workspaceDir, '.state');
|
|
362
435
|
fs.mkdirSync(stateDir, { recursive: true });
|
|
363
436
|
|
|
364
|
-
|
|
437
|
+
const logger = { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() };
|
|
438
|
+
|
|
439
|
+
// Override the mock to return disabled
|
|
440
|
+
const { resolveObserverConfig } = await import('../../src/core/pd-config-loader.js');
|
|
441
|
+
vi.mocked(resolveObserverConfig).mockReturnValueOnce({
|
|
442
|
+
enabled: false,
|
|
443
|
+
readiness: 'disabled',
|
|
444
|
+
source: 'defaults',
|
|
445
|
+
reason: 'correction_observer is disabled in .pd/config.yaml',
|
|
446
|
+
nextAction: 'Set features.correction_observer.enabled=true in .pd/config.yaml to enable',
|
|
447
|
+
runtimeProfileId: null,
|
|
448
|
+
runtimeProfileType: null,
|
|
449
|
+
apiKeyEnv: null,
|
|
450
|
+
apiKeyPresent: false,
|
|
451
|
+
provider: null,
|
|
452
|
+
model: null,
|
|
453
|
+
timeoutMs: null,
|
|
454
|
+
baseUrl: null,
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
try {
|
|
458
|
+
const wctx = WorkspaceContext.fromHookContext({ workspaceDir });
|
|
459
|
+
const result = resolveCorrectionObserver(wctx, logger as any);
|
|
460
|
+
|
|
461
|
+
expect(result).toBeNull();
|
|
462
|
+
} finally {
|
|
463
|
+
safeRmDir(workspaceDir);
|
|
464
|
+
}
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
it('returns null when observer needs setup (no API key)', async () => {
|
|
468
|
+
const workspaceDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pd-corr-needs-setup-'));
|
|
469
|
+
const stateDir = path.join(workspaceDir, '.state');
|
|
470
|
+
fs.mkdirSync(stateDir, { recursive: true });
|
|
365
471
|
|
|
366
472
|
const logger = { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() };
|
|
367
473
|
|
|
474
|
+
// Override the mock to return needs_setup
|
|
475
|
+
const { resolveObserverConfig } = await import('../../src/core/pd-config-loader.js');
|
|
476
|
+
vi.mocked(resolveObserverConfig).mockReturnValueOnce({
|
|
477
|
+
enabled: true,
|
|
478
|
+
readiness: 'needs_setup',
|
|
479
|
+
source: 'defaults',
|
|
480
|
+
reason: "Environment variable 'ANTHROPIC_API_KEY' is not set or empty",
|
|
481
|
+
nextAction: 'Set the environment variable ANTHROPIC_API_KEY with a valid API key',
|
|
482
|
+
runtimeProfileId: 'pd.anthropic-sonnet',
|
|
483
|
+
runtimeProfileType: 'pi-ai',
|
|
484
|
+
apiKeyEnv: 'ANTHROPIC_API_KEY',
|
|
485
|
+
apiKeyPresent: false,
|
|
486
|
+
provider: 'anthropic',
|
|
487
|
+
model: 'claude-3-5-sonnet',
|
|
488
|
+
timeoutMs: 30000,
|
|
489
|
+
baseUrl: null,
|
|
490
|
+
});
|
|
491
|
+
|
|
368
492
|
try {
|
|
369
493
|
const wctx = WorkspaceContext.fromHookContext({ workspaceDir });
|
|
370
494
|
const result = resolveCorrectionObserver(wctx, logger as any);
|
|
371
495
|
|
|
372
|
-
|
|
373
|
-
|
|
496
|
+
expect(result).toBeNull();
|
|
497
|
+
// Should log the needs_setup reason, not noisy "no API key" cycling
|
|
498
|
+
expect(logger.info).toHaveBeenCalledWith(
|
|
499
|
+
expect.stringContaining('ANTHROPIC_API_KEY')
|
|
500
|
+
);
|
|
374
501
|
} finally {
|
|
375
|
-
delete process.env.ANTHROPIC_API_KEY;
|
|
376
502
|
safeRmDir(workspaceDir);
|
|
377
503
|
}
|
|
378
504
|
});
|
|
@@ -14,7 +14,7 @@ describe('Correction Observer Ownership — Feature Flag & Surface Registry Cons
|
|
|
14
14
|
const result = computeEffectiveFlags(
|
|
15
15
|
{ correction_observer: { enabled: false } },
|
|
16
16
|
DEFAULT_FEATURE_FLAGS,
|
|
17
|
-
'.pd/
|
|
17
|
+
'.pd/config.yaml',
|
|
18
18
|
);
|
|
19
19
|
expect(result.flags['correction_observer'].enabled).toBe(false);
|
|
20
20
|
expect(result.warnings).not.toContain(expect.stringContaining('core flag cannot be disabled'));
|