principles-disciple 1.87.0 → 1.89.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/commands/pain.ts +5 -0
- package/src/core/pd-config-loader.ts +400 -0
- package/src/core/runtime-v2-prompt-activation-reader.ts +15 -63
- package/src/hooks/pain.ts +6 -0
- 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 +1 -1
- 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/service/correction-observer-service.test.ts +147 -21
- package/tests/service/evolution-worker.correction-observer.test.ts +1 -1
|
@@ -3,13 +3,13 @@ import { WorkspaceContext } from '../core/workspace-context.js';
|
|
|
3
3
|
import { TrajectoryRegistry } from '../core/trajectory.js';
|
|
4
4
|
import { CorrectionCueLearner } from '../core/correction-cue-learner.js';
|
|
5
5
|
import {
|
|
6
|
-
WorkflowFunnelLoader,
|
|
7
6
|
PiAiRuntimeAdapter,
|
|
8
7
|
CorrectionObserver,
|
|
9
8
|
AgentScheduler,
|
|
10
9
|
} from '@principles/core/runtime-v2';
|
|
11
10
|
import { KeywordOptimizationService } from './keyword-optimization-service.js';
|
|
12
11
|
import { SystemLogger } from '../core/system-logger.js';
|
|
12
|
+
import { resolveObserverConfig } from '../core/pd-config-loader.js';
|
|
13
13
|
|
|
14
14
|
export interface CorrectionObserverServiceShape {
|
|
15
15
|
id: string;
|
|
@@ -26,43 +26,53 @@ const CORRECTION_OBSERVER_INITIAL_DELAY_MS = 10_000;
|
|
|
26
26
|
const CORRECTION_OBSERVER_MAX_RECENT_SESSIONS = 20;
|
|
27
27
|
const CORRECTION_OBSERVER_MAX_PAYLOAD_SESSIONS = 5;
|
|
28
28
|
|
|
29
|
+
/**
|
|
30
|
+
* PRI-307: Resolve CorrectionObserver from .pd/config.yaml.
|
|
31
|
+
*
|
|
32
|
+
* States:
|
|
33
|
+
* - disabled: feature flag off → return null, no noisy logs
|
|
34
|
+
* - needs_setup: enabled but missing API key or profile → return null with structured reason
|
|
35
|
+
* - ready/not_ready: enabled and configured → return observer instance
|
|
36
|
+
*/
|
|
29
37
|
export function resolveCorrectionObserver(wctx: WorkspaceContext, logger?: Pick<PluginLogger, 'info' | 'warn' | 'error' | 'debug'>): CorrectionObserver | null {
|
|
30
38
|
try {
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
logger
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
logger?.debug?.(`[PD:CorrectionObserver]
|
|
43
|
-
return null;
|
|
39
|
+
const observerConfig = resolveObserverConfig(
|
|
40
|
+
wctx.workspaceDir,
|
|
41
|
+
'correction_observer',
|
|
42
|
+
'correctionObserver',
|
|
43
|
+
logger,
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
if (!observerConfig.enabled) {
|
|
47
|
+
if (observerConfig.readiness === 'config_malformed') {
|
|
48
|
+
logger?.warn?.(`[PD:CorrectionObserver] Config malformed: ${observerConfig.reason}. ${observerConfig.nextAction}`);
|
|
49
|
+
} else {
|
|
50
|
+
logger?.debug?.(`[PD:CorrectionObserver] ${observerConfig.reason}`);
|
|
44
51
|
}
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (observerConfig.readiness === 'needs_setup') {
|
|
56
|
+
logger?.info?.(`[PD:CorrectionObserver] ${observerConfig.reason}. ${observerConfig.nextAction}`);
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
45
59
|
|
|
60
|
+
// ready or not_ready — create the observer
|
|
61
|
+
if (observerConfig.runtimeProfileType === 'pi-ai') {
|
|
46
62
|
const adapter = new PiAiRuntimeAdapter({
|
|
47
|
-
provider,
|
|
48
|
-
model,
|
|
49
|
-
apiKeyEnv,
|
|
50
|
-
|
|
63
|
+
provider: observerConfig.provider ?? 'anthropic',
|
|
64
|
+
model: observerConfig.model ?? 'anthropic/claude-3-5-sonnet',
|
|
65
|
+
apiKeyEnv: observerConfig.apiKeyEnv ?? 'ANTHROPIC_API_KEY',
|
|
66
|
+
timeoutMs: observerConfig.timeoutMs ?? undefined,
|
|
67
|
+
baseUrl: observerConfig.baseUrl ?? undefined,
|
|
51
68
|
workspace: wctx.workspaceDir,
|
|
52
69
|
});
|
|
53
|
-
return new CorrectionObserver({ runtimeAdapter: adapter });
|
|
70
|
+
return new CorrectionObserver({ runtimeAdapter: adapter }, { timeoutMs: observerConfig.timeoutMs ?? undefined });
|
|
54
71
|
}
|
|
55
72
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
apiKeyEnv: String(policy.apiKeyEnv),
|
|
60
|
-
maxRetries: policy.maxRetries,
|
|
61
|
-
timeoutMs: policy.timeoutMs ?? 30_000,
|
|
62
|
-
baseUrl: policy.baseUrl,
|
|
63
|
-
workspace: wctx.workspaceDir,
|
|
64
|
-
});
|
|
65
|
-
return new CorrectionObserver({ runtimeAdapter: adapter }, { timeoutMs: policy.timeoutMs });
|
|
73
|
+
// OpenClaw profile — not yet supported for observer runtime
|
|
74
|
+
logger?.info?.(`[PD:CorrectionObserver] OpenClaw runtime profile not yet supported for correction observer. Skipping.`);
|
|
75
|
+
return null;
|
|
66
76
|
} catch (err) {
|
|
67
77
|
logger?.warn?.(`[PD:CorrectionObserver] Failed to resolve CorrectionObserver: ${String(err)}`);
|
|
68
78
|
return null;
|
|
@@ -73,7 +83,8 @@ export async function runCorrectionObserverCycle(wctx: WorkspaceContext, logger:
|
|
|
73
83
|
try {
|
|
74
84
|
const observer = resolveCorrectionObserver(wctx, logger);
|
|
75
85
|
if (!observer) {
|
|
76
|
-
|
|
86
|
+
// PRI-307: No noisy "no API key" cycling. Only log at debug level.
|
|
87
|
+
logger?.debug?.(`[PD:CorrectionObserver] Observer not resolved. Skipping cycle.`);
|
|
77
88
|
return;
|
|
78
89
|
}
|
|
79
90
|
|
|
@@ -163,8 +174,28 @@ export const CorrectionObserverService: CorrectionObserverServiceShape = {
|
|
|
163
174
|
if (logger) logger.info(`[PD:CorrectionObserver] Already started for workspace: ${workspaceDir}. Skipping duplicate start.`);
|
|
164
175
|
return;
|
|
165
176
|
}
|
|
166
|
-
startedWorkspaces.add(workspaceDir);
|
|
167
177
|
|
|
178
|
+
// PRI-307: Check observer config before starting
|
|
179
|
+
const observerConfig = resolveObserverConfig(
|
|
180
|
+
workspaceDir,
|
|
181
|
+
'correction_observer',
|
|
182
|
+
'correctionObserver',
|
|
183
|
+
logger,
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
if (!observerConfig.enabled) {
|
|
187
|
+
// Disabled → no start, no noisy cycling. Single structured log.
|
|
188
|
+
logger?.info?.(`[PD:CorrectionObserver] ${observerConfig.reason}. ${observerConfig.nextAction}`);
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (observerConfig.readiness === 'needs_setup') {
|
|
193
|
+
// Enabled but missing setup → structured needs_setup, no noisy cycling
|
|
194
|
+
logger?.info?.(`[PD:CorrectionObserver] ${observerConfig.reason}. ${observerConfig.nextAction}`);
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
startedWorkspaces.add(workspaceDir);
|
|
168
199
|
correctionObserverStopped = false;
|
|
169
200
|
|
|
170
201
|
const wctx = WorkspaceContext.fromHookContext({ workspaceDir, ...ctx.config });
|
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pd-config-loader (plugin) tests — PRI-307
|
|
3
|
+
*
|
|
4
|
+
* Covers:
|
|
5
|
+
* - Observer disabled → no start, no noisy logs
|
|
6
|
+
* - Observer enabled + missing setup → needs_setup + nextAction
|
|
7
|
+
* - Observer enabled + configured → ready
|
|
8
|
+
* - No secret output in any result
|
|
9
|
+
* - Feature flag loading from .pd/config.yaml
|
|
10
|
+
* - Missing config → defaults
|
|
11
|
+
* - Malformed config → fail loud
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
15
|
+
import * as fs from 'node:fs';
|
|
16
|
+
import * as path from 'node:path';
|
|
17
|
+
import * as os from 'node:os';
|
|
18
|
+
import * as yaml from 'js-yaml';
|
|
19
|
+
import {
|
|
20
|
+
loadPdConfigForPlugin,
|
|
21
|
+
loadFeatureFlagFromConfig,
|
|
22
|
+
resolveObserverConfig,
|
|
23
|
+
getPdConfigPath,
|
|
24
|
+
type ObserverConfigResult,
|
|
25
|
+
} from '../../src/core/pd-config-loader.js';
|
|
26
|
+
|
|
27
|
+
// ── Helpers ─────────────────────────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
function mkTmpDir(): string {
|
|
30
|
+
return fs.mkdtempSync(path.join(os.tmpdir(), 'pd-plugin-config-test-'));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function rmTmpDir(dir: string): void {
|
|
34
|
+
try { fs.rmSync(dir, { recursive: true, force: true }); } catch { /* ignore */ }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function writeConfig(workspaceDir: string, content: string): void {
|
|
38
|
+
const configDir = path.join(workspaceDir, '.pd');
|
|
39
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
40
|
+
fs.writeFileSync(path.join(configDir, 'config.yaml'), content, 'utf8');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function makeValidConfigWithObserverEnabled(): string {
|
|
44
|
+
return yaml.dump({
|
|
45
|
+
version: 1,
|
|
46
|
+
features: {
|
|
47
|
+
prompt: { category: 'core', enabled: true },
|
|
48
|
+
code_tool_hook: { category: 'core', enabled: true },
|
|
49
|
+
defer_archive: { category: 'core', enabled: true },
|
|
50
|
+
correction_observer: { category: 'quiet', enabled: true },
|
|
51
|
+
empathy_observer: { category: 'quiet', enabled: false },
|
|
52
|
+
},
|
|
53
|
+
runtimeProfiles: {
|
|
54
|
+
'openclaw.default': { type: 'openclaw', source: 'default' },
|
|
55
|
+
'pd.anthropic-sonnet': { type: 'pi-ai', provider: 'anthropic', model: 'claude-3-5-sonnet', apiKeyEnv: 'ANTHROPIC_API_KEY', timeoutMs: 300000 },
|
|
56
|
+
},
|
|
57
|
+
internalAgents: {
|
|
58
|
+
defaultRuntime: 'openclaw.default',
|
|
59
|
+
agents: {
|
|
60
|
+
diagnostician: { enabled: true },
|
|
61
|
+
dreamer: { enabled: true },
|
|
62
|
+
scribe: { enabled: true },
|
|
63
|
+
artificer: { enabled: true },
|
|
64
|
+
philosopher: { enabled: false },
|
|
65
|
+
evaluator: { enabled: false },
|
|
66
|
+
rolloutReviewer: { enabled: false },
|
|
67
|
+
trainer: { enabled: false },
|
|
68
|
+
correctionObserver: { enabled: true, runtimeProfile: 'pd.anthropic-sonnet' },
|
|
69
|
+
empathyObserver: { enabled: false },
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
ui: { diagnostics: { mode: 'simple' } },
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function makeValidConfigWithObserverDisabled(): string {
|
|
77
|
+
return yaml.dump({
|
|
78
|
+
version: 1,
|
|
79
|
+
features: {
|
|
80
|
+
prompt: { category: 'core', enabled: true },
|
|
81
|
+
code_tool_hook: { category: 'core', enabled: true },
|
|
82
|
+
defer_archive: { category: 'core', enabled: true },
|
|
83
|
+
correction_observer: { category: 'quiet', enabled: false },
|
|
84
|
+
empathy_observer: { category: 'quiet', enabled: false },
|
|
85
|
+
},
|
|
86
|
+
runtimeProfiles: {
|
|
87
|
+
'openclaw.default': { type: 'openclaw', source: 'default' },
|
|
88
|
+
},
|
|
89
|
+
internalAgents: {
|
|
90
|
+
defaultRuntime: 'openclaw.default',
|
|
91
|
+
agents: {
|
|
92
|
+
diagnostician: { enabled: true },
|
|
93
|
+
dreamer: { enabled: true },
|
|
94
|
+
scribe: { enabled: true },
|
|
95
|
+
artificer: { enabled: true },
|
|
96
|
+
philosopher: { enabled: false },
|
|
97
|
+
evaluator: { enabled: false },
|
|
98
|
+
rolloutReviewer: { enabled: false },
|
|
99
|
+
trainer: { enabled: false },
|
|
100
|
+
correctionObserver: { enabled: false },
|
|
101
|
+
empathyObserver: { enabled: false },
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
ui: { diagnostics: { mode: 'simple' } },
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ── Observer disabled ────────────────────────────────────────────────────────
|
|
109
|
+
|
|
110
|
+
describe('Observer disabled', () => {
|
|
111
|
+
it('returns readiness=disabled when feature flag is off', () => {
|
|
112
|
+
const tmp = mkTmpDir();
|
|
113
|
+
writeConfig(tmp, makeValidConfigWithObserverDisabled());
|
|
114
|
+
try {
|
|
115
|
+
const result = resolveObserverConfig(tmp, 'correction_observer', 'correctionObserver');
|
|
116
|
+
expect(result.enabled).toBe(false);
|
|
117
|
+
expect(result.readiness).toBe('disabled');
|
|
118
|
+
expect(result.reason).toContain('disabled');
|
|
119
|
+
expect(result.nextAction).toContain('.pd/config.yaml');
|
|
120
|
+
} finally { rmTmpDir(tmp); }
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('returns readiness=disabled when config is missing (defaults)', () => {
|
|
124
|
+
const tmp = mkTmpDir();
|
|
125
|
+
try {
|
|
126
|
+
const result = resolveObserverConfig(tmp, 'correction_observer', 'correctionObserver');
|
|
127
|
+
expect(result.enabled).toBe(false);
|
|
128
|
+
expect(result.readiness).toBe('disabled');
|
|
129
|
+
} finally { rmTmpDir(tmp); }
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// ── Observer enabled + missing setup → needs_setup ──────────────────────────
|
|
134
|
+
|
|
135
|
+
describe('Observer needs_setup', () => {
|
|
136
|
+
it('returns readiness=needs_setup when API key env is not set', () => {
|
|
137
|
+
const tmp = mkTmpDir();
|
|
138
|
+
writeConfig(tmp, makeValidConfigWithObserverEnabled());
|
|
139
|
+
const originalKey = process.env.ANTHROPIC_API_KEY;
|
|
140
|
+
delete process.env.ANTHROPIC_API_KEY;
|
|
141
|
+
try {
|
|
142
|
+
const result = resolveObserverConfig(tmp, 'correction_observer', 'correctionObserver');
|
|
143
|
+
expect(result.enabled).toBe(true);
|
|
144
|
+
expect(result.readiness).toBe('needs_setup');
|
|
145
|
+
expect(result.apiKeyEnv).toBe('ANTHROPIC_API_KEY');
|
|
146
|
+
expect(result.apiKeyPresent).toBe(false);
|
|
147
|
+
expect(result.nextAction).toContain('ANTHROPIC_API_KEY');
|
|
148
|
+
} finally {
|
|
149
|
+
if (originalKey !== undefined) process.env.ANTHROPIC_API_KEY = originalKey;
|
|
150
|
+
rmTmpDir(tmp);
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('returns readiness=needs_setup when runtime profile is not found', () => {
|
|
155
|
+
const tmp = mkTmpDir();
|
|
156
|
+
const config = yaml.dump({
|
|
157
|
+
version: 1,
|
|
158
|
+
features: {
|
|
159
|
+
prompt: { category: 'core', enabled: true },
|
|
160
|
+
code_tool_hook: { category: 'core', enabled: true },
|
|
161
|
+
defer_archive: { category: 'core', enabled: true },
|
|
162
|
+
correction_observer: { category: 'quiet', enabled: true },
|
|
163
|
+
empathy_observer: { category: 'quiet', enabled: false },
|
|
164
|
+
},
|
|
165
|
+
runtimeProfiles: {
|
|
166
|
+
'openclaw.default': { type: 'openclaw', source: 'default' },
|
|
167
|
+
},
|
|
168
|
+
internalAgents: {
|
|
169
|
+
defaultRuntime: 'openclaw.default',
|
|
170
|
+
agents: {
|
|
171
|
+
diagnostician: { enabled: true },
|
|
172
|
+
dreamer: { enabled: true },
|
|
173
|
+
scribe: { enabled: true },
|
|
174
|
+
artificer: { enabled: true },
|
|
175
|
+
philosopher: { enabled: false },
|
|
176
|
+
evaluator: { enabled: false },
|
|
177
|
+
rolloutReviewer: { enabled: false },
|
|
178
|
+
trainer: { enabled: false },
|
|
179
|
+
correctionObserver: { enabled: true, runtimeProfile: 'nonexistent.profile' },
|
|
180
|
+
empathyObserver: { enabled: false },
|
|
181
|
+
},
|
|
182
|
+
},
|
|
183
|
+
});
|
|
184
|
+
writeConfig(tmp, config);
|
|
185
|
+
try {
|
|
186
|
+
const result = resolveObserverConfig(tmp, 'correction_observer', 'correctionObserver');
|
|
187
|
+
expect(result.enabled).toBe(true);
|
|
188
|
+
expect(result.readiness).toBe('needs_setup');
|
|
189
|
+
expect(result.reason).toContain('not found');
|
|
190
|
+
} finally { rmTmpDir(tmp); }
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
// ── Observer ready ───────────────────────────────────────────────────────────
|
|
195
|
+
|
|
196
|
+
describe('Observer ready', () => {
|
|
197
|
+
it('returns readiness=not_ready when pi-ai profile has API key set', () => {
|
|
198
|
+
const tmp = mkTmpDir();
|
|
199
|
+
writeConfig(tmp, makeValidConfigWithObserverEnabled());
|
|
200
|
+
const originalKey = process.env.ANTHROPIC_API_KEY;
|
|
201
|
+
process.env.ANTHROPIC_API_KEY = 'sk-ant-test-key-1234567890';
|
|
202
|
+
try {
|
|
203
|
+
const result = resolveObserverConfig(tmp, 'correction_observer', 'correctionObserver');
|
|
204
|
+
expect(result.enabled).toBe(true);
|
|
205
|
+
expect(result.readiness).toBe('not_ready');
|
|
206
|
+
expect(result.apiKeyPresent).toBe(true);
|
|
207
|
+
expect(result.provider).toBe('anthropic');
|
|
208
|
+
expect(result.model).toBe('claude-3-5-sonnet');
|
|
209
|
+
} finally {
|
|
210
|
+
if (originalKey !== undefined) process.env.ANTHROPIC_API_KEY = originalKey;
|
|
211
|
+
else delete process.env.ANTHROPIC_API_KEY;
|
|
212
|
+
rmTmpDir(tmp);
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it('returns readiness=needs_setup for OpenClaw profile (not supported for observers)', () => {
|
|
217
|
+
const tmp = mkTmpDir();
|
|
218
|
+
const config = yaml.dump({
|
|
219
|
+
version: 1,
|
|
220
|
+
features: {
|
|
221
|
+
prompt: { category: 'core', enabled: true },
|
|
222
|
+
code_tool_hook: { category: 'core', enabled: true },
|
|
223
|
+
defer_archive: { category: 'core', enabled: true },
|
|
224
|
+
correction_observer: { category: 'quiet', enabled: true },
|
|
225
|
+
empathy_observer: { category: 'quiet', enabled: false },
|
|
226
|
+
},
|
|
227
|
+
runtimeProfiles: {
|
|
228
|
+
'openclaw.default': { type: 'openclaw', source: 'default' },
|
|
229
|
+
},
|
|
230
|
+
internalAgents: {
|
|
231
|
+
defaultRuntime: 'openclaw.default',
|
|
232
|
+
agents: {
|
|
233
|
+
diagnostician: { enabled: true },
|
|
234
|
+
dreamer: { enabled: true },
|
|
235
|
+
scribe: { enabled: true },
|
|
236
|
+
artificer: { enabled: true },
|
|
237
|
+
philosopher: { enabled: false },
|
|
238
|
+
evaluator: { enabled: false },
|
|
239
|
+
rolloutReviewer: { enabled: false },
|
|
240
|
+
trainer: { enabled: false },
|
|
241
|
+
correctionObserver: { enabled: true },
|
|
242
|
+
empathyObserver: { enabled: false },
|
|
243
|
+
},
|
|
244
|
+
},
|
|
245
|
+
});
|
|
246
|
+
writeConfig(tmp, config);
|
|
247
|
+
try {
|
|
248
|
+
const result = resolveObserverConfig(tmp, 'correction_observer', 'correctionObserver');
|
|
249
|
+
expect(result.enabled).toBe(true);
|
|
250
|
+
expect(result.readiness).toBe('needs_setup');
|
|
251
|
+
expect(result.runtimeProfileType).toBe('openclaw');
|
|
252
|
+
expect(result.reason).toContain('not supported');
|
|
253
|
+
expect(result.nextAction).toContain('pi-ai');
|
|
254
|
+
} finally { rmTmpDir(tmp); }
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
// ── No secret output ─────────────────────────────────────────────────────────
|
|
259
|
+
|
|
260
|
+
describe('No secret output', () => {
|
|
261
|
+
it('observer config result never contains API key values', () => {
|
|
262
|
+
const tmp = mkTmpDir();
|
|
263
|
+
writeConfig(tmp, makeValidConfigWithObserverEnabled());
|
|
264
|
+
const originalKey = process.env.ANTHROPIC_API_KEY;
|
|
265
|
+
process.env.ANTHROPIC_API_KEY = 'sk-ant-test-key-1234567890';
|
|
266
|
+
try {
|
|
267
|
+
const result = resolveObserverConfig(tmp, 'correction_observer', 'correctionObserver');
|
|
268
|
+
const json = JSON.stringify(result);
|
|
269
|
+
expect(json).not.toContain('sk-ant-test-key');
|
|
270
|
+
expect(json).not.toContain('sk-ant-');
|
|
271
|
+
expect(json).not.toMatch(/"apiKey"\s*:/);
|
|
272
|
+
} finally {
|
|
273
|
+
if (originalKey !== undefined) process.env.ANTHROPIC_API_KEY = originalKey;
|
|
274
|
+
else delete process.env.ANTHROPIC_API_KEY;
|
|
275
|
+
rmTmpDir(tmp);
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
// ── Feature flag loading ─────────────────────────────────────────────────────
|
|
281
|
+
|
|
282
|
+
describe('Feature flag loading from .pd/config.yaml', () => {
|
|
283
|
+
it('loadFeatureFlagFromConfig returns enabled for MVP core flags', () => {
|
|
284
|
+
const tmp = mkTmpDir();
|
|
285
|
+
try {
|
|
286
|
+
const result = loadFeatureFlagFromConfig(tmp, 'prompt');
|
|
287
|
+
expect(result.enabled).toBe(true);
|
|
288
|
+
expect(result.source).toBe('defaults');
|
|
289
|
+
} finally { rmTmpDir(tmp); }
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it('loadFeatureFlagFromConfig returns disabled for quiet flags by default', () => {
|
|
293
|
+
const tmp = mkTmpDir();
|
|
294
|
+
try {
|
|
295
|
+
const result = loadFeatureFlagFromConfig(tmp, 'correction_observer');
|
|
296
|
+
expect(result.enabled).toBe(false);
|
|
297
|
+
} finally { rmTmpDir(tmp); }
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it('loadFeatureFlagFromConfig reads from user config', () => {
|
|
301
|
+
const tmp = mkTmpDir();
|
|
302
|
+
writeConfig(tmp, makeValidConfigWithObserverEnabled());
|
|
303
|
+
try {
|
|
304
|
+
const result = loadFeatureFlagFromConfig(tmp, 'correction_observer');
|
|
305
|
+
expect(result.enabled).toBe(true);
|
|
306
|
+
expect(result.source).toBe('user_config');
|
|
307
|
+
} finally { rmTmpDir(tmp); }
|
|
308
|
+
});
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
// ── Plugin config load ───────────────────────────────────────────────────────
|
|
312
|
+
|
|
313
|
+
describe('Plugin config load', () => {
|
|
314
|
+
it('returns ok=true with defaults when config is missing', () => {
|
|
315
|
+
const tmp = mkTmpDir();
|
|
316
|
+
try {
|
|
317
|
+
const result = loadPdConfigForPlugin(tmp);
|
|
318
|
+
expect(result.ok).toBe(true);
|
|
319
|
+
expect(result.source).toBe('defaults');
|
|
320
|
+
expect(result.effective.config.version).toBe(1);
|
|
321
|
+
} finally { rmTmpDir(tmp); }
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
it('returns ok=false with errors for malformed config', () => {
|
|
325
|
+
const tmp = mkTmpDir();
|
|
326
|
+
writeConfig(tmp, 'version: [unterminated');
|
|
327
|
+
try {
|
|
328
|
+
const result = loadPdConfigForPlugin(tmp);
|
|
329
|
+
expect(result.ok).toBe(false);
|
|
330
|
+
expect(result.source).toBe('malformed');
|
|
331
|
+
expect(result.errors.length).toBeGreaterThan(0);
|
|
332
|
+
expect(result.effective.config.version).toBe(1); // defaults still available
|
|
333
|
+
} finally { rmTmpDir(tmp); }
|
|
334
|
+
});
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
// ── Config malformed → fail loud ────────────────────────────────────────────
|
|
338
|
+
|
|
339
|
+
describe('Config malformed → fail loud', () => {
|
|
340
|
+
it('returns readiness=config_malformed when config is invalid YAML', () => {
|
|
341
|
+
const tmp = mkTmpDir();
|
|
342
|
+
writeConfig(tmp, 'version: [unterminated');
|
|
343
|
+
try {
|
|
344
|
+
const result = resolveObserverConfig(tmp, 'correction_observer', 'correctionObserver');
|
|
345
|
+
expect(result.enabled).toBe(false);
|
|
346
|
+
expect(result.readiness).toBe('config_malformed');
|
|
347
|
+
expect(result.reason).toContain('Config validation failed');
|
|
348
|
+
expect(result.nextAction).toBeTruthy();
|
|
349
|
+
expect(result.configErrors).toBeDefined();
|
|
350
|
+
expect(result.configErrors!.length).toBeGreaterThan(0);
|
|
351
|
+
} finally { rmTmpDir(tmp); }
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
it('returns readiness=config_malformed for invalid version', () => {
|
|
355
|
+
const tmp = mkTmpDir();
|
|
356
|
+
writeConfig(tmp, yaml.dump({ version: 99, features: {}, runtimeProfiles: {}, internalAgents: { defaultRuntime: 'x', agents: {} } }));
|
|
357
|
+
try {
|
|
358
|
+
const result = resolveObserverConfig(tmp, 'correction_observer', 'correctionObserver');
|
|
359
|
+
expect(result.readiness).toBe('config_malformed');
|
|
360
|
+
} finally { rmTmpDir(tmp); }
|
|
361
|
+
});
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
// ── Feature flag vs agent enabled mismatch ──────────────────────────────────
|
|
365
|
+
|
|
366
|
+
describe('Feature flag vs agent enabled mismatch', () => {
|
|
367
|
+
it('returns readiness=disabled when feature flag is on but agent.enabled=false', () => {
|
|
368
|
+
const tmp = mkTmpDir();
|
|
369
|
+
const config = yaml.dump({
|
|
370
|
+
version: 1,
|
|
371
|
+
features: {
|
|
372
|
+
prompt: { category: 'core', enabled: true },
|
|
373
|
+
code_tool_hook: { category: 'core', enabled: true },
|
|
374
|
+
defer_archive: { category: 'core', enabled: true },
|
|
375
|
+
correction_observer: { category: 'quiet', enabled: true }, // feature flag ON
|
|
376
|
+
empathy_observer: { category: 'quiet', enabled: false },
|
|
377
|
+
},
|
|
378
|
+
runtimeProfiles: {
|
|
379
|
+
'openclaw.default': { type: 'openclaw', source: 'default' },
|
|
380
|
+
'pd.anthropic-sonnet': { type: 'pi-ai', provider: 'anthropic', model: 'claude-3-5-sonnet', apiKeyEnv: 'ANTHROPIC_API_KEY' },
|
|
381
|
+
},
|
|
382
|
+
internalAgents: {
|
|
383
|
+
defaultRuntime: 'openclaw.default',
|
|
384
|
+
agents: {
|
|
385
|
+
diagnostician: { enabled: true },
|
|
386
|
+
dreamer: { enabled: true },
|
|
387
|
+
scribe: { enabled: true },
|
|
388
|
+
artificer: { enabled: true },
|
|
389
|
+
philosopher: { enabled: false },
|
|
390
|
+
evaluator: { enabled: false },
|
|
391
|
+
rolloutReviewer: { enabled: false },
|
|
392
|
+
trainer: { enabled: false },
|
|
393
|
+
correctionObserver: { enabled: false }, // agent.enabled OFF
|
|
394
|
+
empathyObserver: { enabled: false },
|
|
395
|
+
},
|
|
396
|
+
},
|
|
397
|
+
});
|
|
398
|
+
writeConfig(tmp, config);
|
|
399
|
+
try {
|
|
400
|
+
const result = resolveObserverConfig(tmp, 'correction_observer', 'correctionObserver');
|
|
401
|
+
expect(result.enabled).toBe(false);
|
|
402
|
+
expect(result.readiness).toBe('disabled');
|
|
403
|
+
expect(result.reason).toContain('enabled is false');
|
|
404
|
+
expect(result.nextAction).toContain('internalAgents.agents.correctionObserver.enabled=true');
|
|
405
|
+
} finally { rmTmpDir(tmp); }
|
|
406
|
+
});
|
|
407
|
+
});
|
|
@@ -324,7 +324,7 @@ describe('surface-guard', () => {
|
|
|
324
324
|
|
|
325
325
|
it('no quiet surface disabledReason promises a feature flag override (PRI-298 / chatgpt P2)', () => {
|
|
326
326
|
// The runtime guard path (`isSurfaceEnabled(surfaceId)` with no
|
|
327
|
-
// overrides argument) does not consume `.pd/
|
|
327
|
+
// overrides argument) does not consume `.pd/config.yaml`, so
|
|
328
328
|
// telling operators to "enable via feature flag override" would be
|
|
329
329
|
// an impossible next action. Quiet copy must describe the surface
|
|
330
330
|
// honestly without pointing to a non-existent override path.
|