principles-disciple 1.76.0 → 1.78.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/context.ts +5 -5
- package/src/core/surface-guard.ts +130 -0
- package/src/hooks/prompt.ts +9 -117
- package/src/i18n/commands.ts +2 -2
- package/src/index.ts +45 -27
- package/src/types.ts +3 -3
- package/tests/core-anti-growth.test.ts +1 -0
- package/tests/hooks/prompt-characterization.test.ts +22 -36
- package/tests/hooks/prompt-diet.test.ts +343 -0
- package/tests/integration/mvp-surface-registry-guard.test.ts +398 -0
|
@@ -188,10 +188,13 @@ function makeCtx(overrides: {
|
|
|
188
188
|
} as unknown as Parameters<typeof import('../../src/hooks/prompt.js').handleBeforePromptBuild>[1];
|
|
189
189
|
}
|
|
190
190
|
|
|
191
|
-
// ─── Tests: Attitude Directive (
|
|
191
|
+
// ─── Tests: Attitude Directive removed (PRI-291 MVP diet) ───────────────
|
|
192
|
+
// Attitude/personality prompt text was removed per PRI-291.
|
|
193
|
+
// GFI scoring (trackFriction) and empathy pain emission remain active.
|
|
194
|
+
// These tests verify that attitude text no longer appears in prompts.
|
|
192
195
|
|
|
193
|
-
describe('Attitude directive —
|
|
194
|
-
// Ensure appendParts is non-empty so
|
|
196
|
+
describe('Attitude/personality directive — removed from prompt (PRI-291)', () => {
|
|
197
|
+
// Ensure appendParts is non-empty so we can verify absence
|
|
195
198
|
beforeEach(async () => {
|
|
196
199
|
const { WorkspaceContext } = await import('../../src/core/workspace-context.js');
|
|
197
200
|
(WorkspaceContext.fromHookContext as ReturnType<typeof vi.fn>).mockReturnValueOnce({
|
|
@@ -209,59 +212,42 @@ describe('Attitude directive — GFI thresholds', () => {
|
|
|
209
212
|
});
|
|
210
213
|
});
|
|
211
214
|
|
|
212
|
-
it('GFI >= 70
|
|
215
|
+
it('GFI >= 70 does NOT inject HUMBLE_RECOVERY mode', async () => {
|
|
213
216
|
setSessionGfi(75);
|
|
214
217
|
const { handleBeforePromptBuild } = await import('../../src/hooks/prompt.js');
|
|
215
218
|
const result = await handleBeforePromptBuild(makeMinimalEvent(), makeCtx({ sessionGfi: 75 }));
|
|
216
219
|
|
|
217
|
-
|
|
218
|
-
expect(
|
|
220
|
+
const combined = (result?.prependSystemContext ?? '') + (result?.appendSystemContext ?? '');
|
|
221
|
+
expect(combined).not.toContain('HUMBLE_RECOVERY');
|
|
219
222
|
});
|
|
220
223
|
|
|
221
|
-
it('GFI >=
|
|
222
|
-
setSessionGfi(70);
|
|
223
|
-
const { handleBeforePromptBuild } = await import('../../src/hooks/prompt.js');
|
|
224
|
-
const result = await handleBeforePromptBuild(makeMinimalEvent(), makeCtx({ sessionGfi: 70 }));
|
|
225
|
-
|
|
226
|
-
expect(result?.appendSystemContext).toContain('HUMBLE_RECOVERY');
|
|
227
|
-
});
|
|
228
|
-
|
|
229
|
-
it('GFI >= 40 and < 70 injects CONCILIATORY mode', async () => {
|
|
224
|
+
it('GFI >= 40 does NOT inject CONCILIATORY mode', async () => {
|
|
230
225
|
setSessionGfi(50);
|
|
231
226
|
const { handleBeforePromptBuild } = await import('../../src/hooks/prompt.js');
|
|
232
227
|
const result = await handleBeforePromptBuild(makeMinimalEvent(), makeCtx({ sessionGfi: 50 }));
|
|
233
228
|
|
|
234
|
-
|
|
235
|
-
expect(
|
|
229
|
+
const combined = (result?.prependSystemContext ?? '') + (result?.appendSystemContext ?? '');
|
|
230
|
+
expect(combined).not.toContain('CONCILIATORY');
|
|
236
231
|
});
|
|
237
232
|
|
|
238
|
-
it('GFI
|
|
239
|
-
setSessionGfi(40);
|
|
240
|
-
const { handleBeforePromptBuild } = await import('../../src/hooks/prompt.js');
|
|
241
|
-
const result = await handleBeforePromptBuild(makeMinimalEvent(), makeCtx({ sessionGfi: 40 }));
|
|
242
|
-
|
|
243
|
-
expect(result?.appendSystemContext).toContain('CONCILIATORY');
|
|
244
|
-
});
|
|
245
|
-
|
|
246
|
-
it('GFI < 40 injects EFFICIENT mode', async () => {
|
|
233
|
+
it('GFI < 40 does NOT inject EFFICIENT mode', async () => {
|
|
247
234
|
setSessionGfi(10);
|
|
248
235
|
const { handleBeforePromptBuild } = await import('../../src/hooks/prompt.js');
|
|
249
236
|
const result = await handleBeforePromptBuild(makeMinimalEvent(), makeCtx({ sessionGfi: 10 }));
|
|
250
237
|
|
|
251
|
-
|
|
252
|
-
expect(
|
|
238
|
+
const combined = (result?.prependSystemContext ?? '') + (result?.appendSystemContext ?? '');
|
|
239
|
+
expect(combined).not.toContain('EFFICIENT');
|
|
253
240
|
});
|
|
254
241
|
|
|
255
|
-
it('
|
|
256
|
-
setSessionGfi(
|
|
257
|
-
const { getSession } = await import('../../src/core/session-tracker.js');
|
|
258
|
-
// Override to return undefined (no session found)
|
|
259
|
-
(getSession as ReturnType<typeof vi.fn>).mockReturnValueOnce(undefined);
|
|
260
|
-
|
|
242
|
+
it('no "Spicy Evolver" persona text appears in prompt', async () => {
|
|
243
|
+
setSessionGfi(20);
|
|
261
244
|
const { handleBeforePromptBuild } = await import('../../src/hooks/prompt.js');
|
|
262
|
-
const result = await handleBeforePromptBuild(makeMinimalEvent(), makeCtx({
|
|
245
|
+
const result = await handleBeforePromptBuild(makeMinimalEvent(), makeCtx({ sessionGfi: 20 }));
|
|
263
246
|
|
|
264
|
-
|
|
247
|
+
const combined = (result?.prependSystemContext ?? '') + (result?.appendSystemContext ?? '');
|
|
248
|
+
expect(combined).not.toContain('Spicy Evolver');
|
|
249
|
+
expect(combined).not.toContain('despise entropy');
|
|
250
|
+
expect(combined).not.toContain('evolve through pain');
|
|
265
251
|
});
|
|
266
252
|
});
|
|
267
253
|
|
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prompt diet tests — PRI-291
|
|
3
|
+
*
|
|
4
|
+
* Verifies that the default prompt contains only MVP-required sections
|
|
5
|
+
* and does NOT contain sections removed/default-disabled by the MVP diet.
|
|
6
|
+
*
|
|
7
|
+
* Acceptance criteria covered:
|
|
8
|
+
* 1. Default prompt does NOT contain Thinking OS text
|
|
9
|
+
* 2. Default prompt does NOT contain <routing_guidance>
|
|
10
|
+
* 3. Default prompt does NOT contain GFI attitude/personality directive text
|
|
11
|
+
* 4. Runtime V2 activation still injects validated directives into prependSystemContext
|
|
12
|
+
* 5. runtime_v2_prompt_activations_injected event still emits
|
|
13
|
+
* 6. GFI scoring/empathy evidence path still records friction and can emit pain
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
17
|
+
|
|
18
|
+
// ─── Mock dependencies ───────────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
vi.clearAllMocks();
|
|
22
|
+
process.env.PD_LEGACY_PROMPT_DIAGNOSTICIAN_ENABLED = 'true';
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
afterEach(() => {
|
|
26
|
+
process.env.PD_LEGACY_PROMPT_DIAGNOSTICIAN_ENABLED = '';
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const mockGetPendingDiagnosticianTasks = vi.fn<(stateDir: string) => unknown[]>();
|
|
30
|
+
|
|
31
|
+
vi.mock('../../src/core/diagnostician-task-store.js', async () => ({
|
|
32
|
+
getPendingDiagnosticianTasks: (...args: unknown[]) =>
|
|
33
|
+
mockGetPendingDiagnosticianTasks(...args),
|
|
34
|
+
}));
|
|
35
|
+
|
|
36
|
+
vi.mock('../../src/core/event-log.js', () => ({
|
|
37
|
+
EventLogService: {
|
|
38
|
+
get: vi.fn().mockReturnValue({
|
|
39
|
+
recordHeartbeatDiagnosis: vi.fn(),
|
|
40
|
+
recordRuntimeV2ActivationsInjected: vi.fn(),
|
|
41
|
+
}),
|
|
42
|
+
},
|
|
43
|
+
}));
|
|
44
|
+
|
|
45
|
+
vi.mock('../../src/core/workspace-context.js', () => {
|
|
46
|
+
const mockWctx = {
|
|
47
|
+
workspaceDir: '/fake/workspace',
|
|
48
|
+
stateDir: '/fake/state',
|
|
49
|
+
resolve: (key: string) => `/fake/${key}`,
|
|
50
|
+
trajectory: { recordSession: vi.fn(), recordUserTurn: vi.fn() },
|
|
51
|
+
config: { get: vi.fn() },
|
|
52
|
+
eventLog: {
|
|
53
|
+
recordRuntimeV2ActivationsInjected: vi.fn(),
|
|
54
|
+
},
|
|
55
|
+
evolutionReducer: {
|
|
56
|
+
getActivePrinciples: vi.fn().mockReturnValue([]),
|
|
57
|
+
getProbationPrinciples: vi.fn().mockReturnValue([]),
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
return {
|
|
61
|
+
WorkspaceContext: {
|
|
62
|
+
fromHookContext: vi.fn().mockReturnValue(mockWctx),
|
|
63
|
+
fromHookContextExplicit: vi.fn().mockReturnValue(mockWctx),
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
let sessionGfiValue = 20;
|
|
69
|
+
vi.mock('../../src/core/session-tracker.js', () => ({
|
|
70
|
+
getSession: vi.fn().mockImplementation(() => ({ currentGfi: sessionGfiValue })),
|
|
71
|
+
resetFriction: vi.fn(),
|
|
72
|
+
trackFriction: vi.fn(),
|
|
73
|
+
setInjectedProbationIds: vi.fn(),
|
|
74
|
+
clearInjectedProbationIds: vi.fn(),
|
|
75
|
+
decayGfi: vi.fn(),
|
|
76
|
+
getGfiDecayElapsed: vi.fn().mockReturnValue(0),
|
|
77
|
+
}));
|
|
78
|
+
|
|
79
|
+
vi.mock('../../src/core/path-resolver.js', () => ({
|
|
80
|
+
PathResolver: { getExtensionRoot: vi.fn().mockReturnValue('/fake/extension') },
|
|
81
|
+
}));
|
|
82
|
+
|
|
83
|
+
vi.mock('../../src/core/principle-injection.js', () => ({
|
|
84
|
+
selectPrinciplesForInjection: vi.fn().mockReturnValue({
|
|
85
|
+
selected: [],
|
|
86
|
+
wasTruncated: false,
|
|
87
|
+
breakdown: { p0: 0, p1: 0, p2: 0 },
|
|
88
|
+
totalChars: 0,
|
|
89
|
+
}),
|
|
90
|
+
DEFAULT_PRINCIPLE_BUDGET: 3000,
|
|
91
|
+
}));
|
|
92
|
+
|
|
93
|
+
vi.mock('../../src/core/empathy-keyword-matcher.js', () => ({
|
|
94
|
+
matchEmpathyKeywords: vi.fn().mockReturnValue({ score: 0, matched: null, severity: 'none', matchedTerms: [] }),
|
|
95
|
+
loadKeywordStore: vi.fn().mockReturnValue({ terms: {}, stats: { totalHits: 0 } }),
|
|
96
|
+
saveKeywordStore: vi.fn(),
|
|
97
|
+
shouldTriggerOptimization: vi.fn().mockReturnValue(false),
|
|
98
|
+
getKeywordStoreSummary: vi.fn().mockReturnValue({ totalTerms: 0, highFalsePositiveTerms: [] }),
|
|
99
|
+
}));
|
|
100
|
+
|
|
101
|
+
vi.mock('../../src/core/empathy-types.js', () => ({
|
|
102
|
+
severityToPenalty: vi.fn().mockReturnValue(5),
|
|
103
|
+
DEFAULT_EMPATHY_KEYWORD_CONFIG: {},
|
|
104
|
+
}));
|
|
105
|
+
|
|
106
|
+
vi.mock('../../src/core/correction-cue-learner.js', () => ({
|
|
107
|
+
CorrectionCueLearner: {
|
|
108
|
+
get: vi.fn().mockReturnValue({
|
|
109
|
+
match: vi.fn().mockReturnValue({ matched: null, matchedTerms: [], confidence: 0 }),
|
|
110
|
+
recordHits: vi.fn(),
|
|
111
|
+
recordTruePositive: vi.fn(),
|
|
112
|
+
flush: vi.fn(),
|
|
113
|
+
}),
|
|
114
|
+
},
|
|
115
|
+
}));
|
|
116
|
+
|
|
117
|
+
vi.mock('../../src/core/focus-history.js', () => ({
|
|
118
|
+
extractSummary: vi.fn().mockReturnValue(''),
|
|
119
|
+
getHistoryVersions: vi.fn().mockResolvedValue([]),
|
|
120
|
+
parseWorkingMemorySection: vi.fn().mockReturnValue(null),
|
|
121
|
+
workingMemoryToInjection: vi.fn().mockReturnValue(''),
|
|
122
|
+
autoCompressFocus: vi.fn().mockReturnValue({ compressed: false, reason: 'not_needed' }),
|
|
123
|
+
safeReadCurrentFocus: vi.fn().mockReturnValue({ content: '', recovered: false, validationErrors: [] }),
|
|
124
|
+
}));
|
|
125
|
+
|
|
126
|
+
vi.mock('../../src/service/subagent-workflow/index.js', () => ({
|
|
127
|
+
EmpathyObserverWorkflowManager: vi.fn(),
|
|
128
|
+
empathyObserverWorkflowSpec: {},
|
|
129
|
+
isExpectedSubagentError: vi.fn().mockReturnValue(false),
|
|
130
|
+
}));
|
|
131
|
+
|
|
132
|
+
vi.mock('../../src/utils/subagent-probe.js', () => ({
|
|
133
|
+
isSubagentRuntimeAvailable: vi.fn().mockReturnValue(false),
|
|
134
|
+
}));
|
|
135
|
+
|
|
136
|
+
vi.mock('../../src/core/local-worker-routing.js', () => ({
|
|
137
|
+
classifyTask: vi.fn().mockReturnValue({
|
|
138
|
+
decision: 'stay_main',
|
|
139
|
+
classification: 'unknown',
|
|
140
|
+
reason: 'mocked',
|
|
141
|
+
blockers: [],
|
|
142
|
+
}),
|
|
143
|
+
}));
|
|
144
|
+
|
|
145
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
146
|
+
|
|
147
|
+
function makeMinimalEvent(overrides: {
|
|
148
|
+
trigger?: string;
|
|
149
|
+
sessionId?: string;
|
|
150
|
+
} = {}) {
|
|
151
|
+
const { trigger = 'user', sessionId = 'test-session-diet' } = overrides;
|
|
152
|
+
return {
|
|
153
|
+
prompt: 'hello world',
|
|
154
|
+
messages: [],
|
|
155
|
+
trigger,
|
|
156
|
+
sessionId,
|
|
157
|
+
} as unknown as Parameters<typeof import('../../src/hooks/prompt.js').handleBeforePromptBuild>[0];
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function makeCtx(overrides: {
|
|
161
|
+
sessionGfi?: number;
|
|
162
|
+
trigger?: string;
|
|
163
|
+
sessionId?: string;
|
|
164
|
+
} = {}) {
|
|
165
|
+
const { sessionGfi = 20, trigger = 'user', sessionId = 'test-session-diet' } = overrides;
|
|
166
|
+
sessionGfiValue = sessionGfi;
|
|
167
|
+
return {
|
|
168
|
+
workspaceDir: '/fake/workspace',
|
|
169
|
+
trigger,
|
|
170
|
+
sessionId,
|
|
171
|
+
api: {
|
|
172
|
+
logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn() },
|
|
173
|
+
runtime: {},
|
|
174
|
+
config: {},
|
|
175
|
+
},
|
|
176
|
+
} as unknown as Parameters<typeof import('../../src/hooks/prompt.js').handleBeforePromptBuild>[1];
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async function getPromptOutput(overrides: { sessionGfi?: number } = {}) {
|
|
180
|
+
const { handleBeforePromptBuild } = await import('../../src/hooks/prompt.js');
|
|
181
|
+
const result = await handleBeforePromptBuild(makeMinimalEvent(), makeCtx(overrides));
|
|
182
|
+
return {
|
|
183
|
+
prepend: result?.prependSystemContext ?? '',
|
|
184
|
+
append: result?.appendSystemContext ?? '',
|
|
185
|
+
context: result?.prependContext ?? '',
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// ─── Tests: MVP Diet — sections that must NOT appear by default ────────────
|
|
190
|
+
|
|
191
|
+
describe('PRI-291 Prompt Diet: default prompt excludes non-MVP sections', () => {
|
|
192
|
+
it('default prompt does NOT contain Thinking OS text', async () => {
|
|
193
|
+
const { append } = await getPromptOutput();
|
|
194
|
+
expect(append).not.toContain('<thinking_os>');
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it('default prompt does NOT contain <routing_guidance>', async () => {
|
|
198
|
+
const { append } = await getPromptOutput();
|
|
199
|
+
expect(append).not.toContain('<routing_guidance>');
|
|
200
|
+
expect(append).not.toContain('DELEGATION SUGGESTION');
|
|
201
|
+
expect(append).not.toContain('ROUTING GUIDANCE');
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it('default prompt does NOT contain GFI attitude/personality text', async () => {
|
|
205
|
+
const { prepend, append } = await getPromptOutput();
|
|
206
|
+
const combined = prepend + append;
|
|
207
|
+
expect(combined).not.toContain('HUMBLE_RECOVERY');
|
|
208
|
+
expect(combined).not.toContain('CONCILIATORY');
|
|
209
|
+
expect(combined).not.toContain('EFFICIENT');
|
|
210
|
+
expect(combined).not.toContain('Spicy Evolver');
|
|
211
|
+
expect(combined).not.toContain('despise entropy');
|
|
212
|
+
expect(combined).not.toContain('evolve through pain');
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it('default prompt does NOT contain <project_context> (default is off)', async () => {
|
|
216
|
+
const { append } = await getPromptOutput();
|
|
217
|
+
// project_context tag should not appear as a content block.
|
|
218
|
+
// Note: the EXECUTION RULES section may list it as a priority description,
|
|
219
|
+
// but the actual <project_context>...</project_context> content block must be absent.
|
|
220
|
+
const hasProjectContextBlock = append.includes('<project_context>\n');
|
|
221
|
+
expect(hasProjectContextBlock).toBe(false);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it('EXECUTION RULES does not list removed sections', async () => {
|
|
225
|
+
const { append } = await getPromptOutput();
|
|
226
|
+
// Only present when appendParts is non-empty (principles exist)
|
|
227
|
+
// But even when present, removed sections should not be listed
|
|
228
|
+
if (append.includes('EXECUTION RULES')) {
|
|
229
|
+
expect(append).not.toContain('<thinking_os>');
|
|
230
|
+
expect(append).not.toContain('<routing_guidance>');
|
|
231
|
+
expect(append).not.toContain('<reflection_log>');
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
// ─── Tests: MVP Diet — sections that MUST still appear ─────────────────────
|
|
237
|
+
|
|
238
|
+
describe('PRI-291 Prompt Diet: MVP sections preserved', () => {
|
|
239
|
+
it('AGENT IDENTITY is still injected in prependSystemContext', async () => {
|
|
240
|
+
const { prepend } = await getPromptOutput();
|
|
241
|
+
expect(prepend).toContain('AGENT IDENTITY');
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it('evolution principles can still be injected when active', async () => {
|
|
245
|
+
const { WorkspaceContext } = await import('../../src/core/workspace-context.js');
|
|
246
|
+
(WorkspaceContext.fromHookContext as ReturnType<typeof vi.fn>).mockReturnValueOnce({
|
|
247
|
+
workspaceDir: '/fake/workspace',
|
|
248
|
+
stateDir: '/fake/state',
|
|
249
|
+
resolve: (key: string) => `/fake/${key}`,
|
|
250
|
+
trajectory: { recordSession: vi.fn(), recordUserTurn: vi.fn() },
|
|
251
|
+
config: { get: vi.fn() },
|
|
252
|
+
eventLog: { recordRuntimeV2ActivationsInjected: vi.fn() },
|
|
253
|
+
evolutionReducer: {
|
|
254
|
+
getActivePrinciples: vi.fn().mockReturnValue([
|
|
255
|
+
{ id: 'P1', text: 'Evolution principle still works' },
|
|
256
|
+
]),
|
|
257
|
+
getProbationPrinciples: vi.fn().mockReturnValue([]),
|
|
258
|
+
},
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
const { handleBeforePromptBuild } = await import('../../src/hooks/prompt.js');
|
|
262
|
+
const result = await handleBeforePromptBuild(makeMinimalEvent(), makeCtx());
|
|
263
|
+
|
|
264
|
+
expect(result?.appendSystemContext).toContain('<evolution_principles>');
|
|
265
|
+
expect(result?.appendSystemContext).toContain('Evolution principle still works');
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it('core principles can still be injected', async () => {
|
|
269
|
+
const { WorkspaceContext } = await import('../../src/core/workspace-context.js');
|
|
270
|
+
(WorkspaceContext.fromHookContext as ReturnType<typeof vi.fn>).mockReturnValueOnce({
|
|
271
|
+
workspaceDir: '/fake/workspace',
|
|
272
|
+
stateDir: '/fake/state',
|
|
273
|
+
resolve: (key: string) => `/fake/${key}`,
|
|
274
|
+
trajectory: { recordSession: vi.fn(), recordUserTurn: vi.fn() },
|
|
275
|
+
config: { get: vi.fn() },
|
|
276
|
+
eventLog: { recordRuntimeV2ActivationsInjected: vi.fn() },
|
|
277
|
+
evolutionReducer: {
|
|
278
|
+
getActivePrinciples: vi.fn().mockReturnValue([
|
|
279
|
+
{ id: 'CP1', text: 'Core principle preserved' },
|
|
280
|
+
]),
|
|
281
|
+
getProbationPrinciples: vi.fn().mockReturnValue([]),
|
|
282
|
+
},
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
const { handleBeforePromptBuild } = await import('../../src/hooks/prompt.js');
|
|
286
|
+
const result = await handleBeforePromptBuild(makeMinimalEvent(), makeCtx());
|
|
287
|
+
|
|
288
|
+
expect(result?.appendSystemContext).toContain('<core_principles>');
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it('GFI scoring still runs — trackFriction called on empathy match', async () => {
|
|
292
|
+
const { matchEmpathyKeywords } = await import('../../src/core/empathy-keyword-matcher.js');
|
|
293
|
+
(matchEmpathyKeywords as ReturnType<typeof vi.fn>).mockReturnValueOnce({
|
|
294
|
+
score: 0.8,
|
|
295
|
+
matched: true,
|
|
296
|
+
severity: 'moderate',
|
|
297
|
+
matchedTerms: ['frustrated'],
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
const { WorkspaceContext } = await import('../../src/core/workspace-context.js');
|
|
301
|
+
(WorkspaceContext.fromHookContext as ReturnType<typeof vi.fn>).mockReturnValueOnce({
|
|
302
|
+
workspaceDir: '/fake/workspace',
|
|
303
|
+
stateDir: '/fake/state',
|
|
304
|
+
resolve: (key: string) => `/fake/${key}`,
|
|
305
|
+
trajectory: { recordSession: vi.fn(), recordUserTurn: vi.fn(), recordPainEvent: vi.fn() },
|
|
306
|
+
config: { get: vi.fn().mockImplementation((k: string) => {
|
|
307
|
+
if (k === 'thresholds.pain_trigger') return 40;
|
|
308
|
+
if (k === 'severity_thresholds.high') return 70;
|
|
309
|
+
if (k === 'language') return 'en';
|
|
310
|
+
return undefined;
|
|
311
|
+
}) },
|
|
312
|
+
eventLog: { recordPainSignal: vi.fn() },
|
|
313
|
+
evolutionReducer: {
|
|
314
|
+
getActivePrinciples: vi.fn().mockReturnValue([]),
|
|
315
|
+
getProbationPrinciples: vi.fn().mockReturnValue([]),
|
|
316
|
+
},
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
const { handleBeforePromptBuild } = await import('../../src/hooks/prompt.js');
|
|
320
|
+
const event = {
|
|
321
|
+
prompt: 'I am frustrated with this result',
|
|
322
|
+
messages: [{ role: 'user', content: 'I am frustrated' }],
|
|
323
|
+
trigger: 'user',
|
|
324
|
+
sessionId: 'gfi-test-session',
|
|
325
|
+
} as unknown as Parameters<typeof import('../../src/hooks/prompt.js').handleBeforePromptBuild>[0];
|
|
326
|
+
|
|
327
|
+
await handleBeforePromptBuild(event, makeCtx({ sessionGfi: 80 }));
|
|
328
|
+
|
|
329
|
+
const { trackFriction } = await import('../../src/core/session-tracker.js');
|
|
330
|
+
expect(trackFriction).toHaveBeenCalled();
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
it('size guard still works — total injection under 9000', async () => {
|
|
334
|
+
const { handleBeforePromptBuild } = await import('../../src/hooks/prompt.js');
|
|
335
|
+
const result = await handleBeforePromptBuild(makeMinimalEvent(), makeCtx());
|
|
336
|
+
|
|
337
|
+
const total =
|
|
338
|
+
(result?.prependSystemContext?.length ?? 0) +
|
|
339
|
+
(result?.prependContext?.length ?? 0) +
|
|
340
|
+
(result?.appendSystemContext?.length ?? 0);
|
|
341
|
+
expect(total).toBeLessThanOrEqual(9000);
|
|
342
|
+
});
|
|
343
|
+
});
|