principles-disciple 1.121.0 → 1.123.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
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"id": "principles-disciple",
|
|
3
3
|
"name": "Principles Disciple",
|
|
4
4
|
"description": "Evolutionary programming agent framework with strategic guardrails and reflection loops.",
|
|
5
|
-
"version": "1.
|
|
5
|
+
"version": "1.123.0",
|
|
6
6
|
"activation": {
|
|
7
7
|
"onCapabilities": [
|
|
8
8
|
"hook"
|
package/package.json
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
DefaultDreamerValidator,
|
|
7
7
|
PiAiRuntimeAdapter,
|
|
8
8
|
L2AgentLoopAdapter,
|
|
9
|
+
buildL2PrincipleReaderFromLedger,
|
|
9
10
|
loadLedger,
|
|
10
11
|
OpenClawCliRuntimeAdapter,
|
|
11
12
|
storeEmitter,
|
|
@@ -15,7 +16,6 @@ import {
|
|
|
15
16
|
InternalizationQueueReadModel,
|
|
16
17
|
MVP_CORE_TASK_KINDS,
|
|
17
18
|
type PDRuntimeAdapter,
|
|
18
|
-
type PdL2PrincipleReader,
|
|
19
19
|
} from '@principles/core/runtime-v2';
|
|
20
20
|
import { loadPdConfigForPlugin, loadFeatureFlagFromConfig } from '../core/pd-config-loader.js';
|
|
21
21
|
import { SystemLogger } from '../core/system-logger.js';
|
|
@@ -176,21 +176,9 @@ export async function runConsumerCycle(
|
|
|
176
176
|
const l2Flag = loadFeatureFlagFromConfig(workspaceDir, 'l2_dreamer');
|
|
177
177
|
if (l2Flag.enabled) {
|
|
178
178
|
const stateDir = `${workspaceDir}/.state`;
|
|
179
|
-
const principleReader
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
const ledger = loadLedger(stateDir);
|
|
183
|
-
const principles = ledger.tree.principles ?? {};
|
|
184
|
-
return Object.values(principles)
|
|
185
|
-
.filter(p => p.status === 'active' && typeof p.id === 'string' && typeof p.text === 'string')
|
|
186
|
-
.map(p => ({ id: p.id, statement: p.text }));
|
|
187
|
-
} catch (error) {
|
|
188
|
-
const reason = error instanceof Error ? error.message : String(error);
|
|
189
|
-
logger.warn(`[PD:AutoConsumer] L2 dreamer principle reader degraded: ${reason}`);
|
|
190
|
-
return [];
|
|
191
|
-
}
|
|
192
|
-
},
|
|
193
|
-
};
|
|
179
|
+
const principleReader = buildL2PrincipleReaderFromLedger(loadLedger(stateDir), {
|
|
180
|
+
logger: { warn: (msg) => logger.warn(msg) },
|
|
181
|
+
});
|
|
194
182
|
adapter = new L2AgentLoopAdapter(
|
|
195
183
|
{
|
|
196
184
|
provider: runtimeConfigResult.provider ?? 'openai',
|
|
@@ -364,7 +364,7 @@ describe('PRI-288: EvolutionWorkerService quarantine', () => {
|
|
|
364
364
|
expect(flags.flags['evolution_worker']?.enabled).toBe(true);
|
|
365
365
|
});
|
|
366
366
|
|
|
367
|
-
it('core flags
|
|
367
|
+
it('PRI-435: core flags can be explicitly emergency-disabled by user override with warning', () => {
|
|
368
368
|
writeConfigYaml(workspaceDir, {
|
|
369
369
|
prompt: { enabled: false },
|
|
370
370
|
code_tool_hook: { enabled: false },
|
|
@@ -377,9 +377,11 @@ describe('PRI-288: EvolutionWorkerService quarantine', () => {
|
|
|
377
377
|
expect(isRecord(parsed)).toBe(true);
|
|
378
378
|
const features = (parsed as Record<string, unknown>).features;
|
|
379
379
|
const flags = computeEffectiveFlags(features as Record<string, unknown>, DEFAULT_FEATURE_FLAGS, configPath);
|
|
380
|
-
|
|
381
|
-
expect(flags.flags['
|
|
382
|
-
expect(flags.
|
|
380
|
+
// PRI-435: core flags honor explicit emergency disable when deliberately configured
|
|
381
|
+
expect(flags.flags['prompt']?.enabled).toBe(false);
|
|
382
|
+
expect(flags.flags['code_tool_hook']?.enabled).toBe(false);
|
|
383
|
+
expect(flags.warnings.length).toBeGreaterThan(0); // warnings about emergency disable
|
|
384
|
+
expect(flags.warnings.some(w => w.includes('core flag explicitly disabled'))).toBe(true);
|
|
383
385
|
});
|
|
384
386
|
});
|
|
385
387
|
|
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PRI-433: PainAdmissionEmitter characterization tests (safety net).
|
|
3
|
+
*
|
|
4
|
+
* These tests capture the CURRENT behavior of the 4 hook sites that emit
|
|
5
|
+
* pain_detected events via emitPainDetectedEvent. They act as a safety net
|
|
6
|
+
* for a future extraction refactor (consolidating into a PainAdmissionEmitter).
|
|
7
|
+
*
|
|
8
|
+
* Scope: static source code analysis only. No runtime mocking needed.
|
|
9
|
+
* This follows the pattern established by runtime-v2-pain-guard.test.ts.
|
|
10
|
+
*
|
|
11
|
+
* Each section captures:
|
|
12
|
+
* - Pain ID format (regex)
|
|
13
|
+
* - Provenance field value
|
|
14
|
+
* - Required fields present/missing (documents inconsistencies)
|
|
15
|
+
* - Gate function used before emit
|
|
16
|
+
* - Emit conditions (what must be true for emit to proceed)
|
|
17
|
+
*
|
|
18
|
+
* Known inconsistencies (to be resolved by future extraction):
|
|
19
|
+
* | Site | painId format | provenance | evidence | traceId |
|
|
20
|
+
* |--------------------------|----------------------------------|---------------------------|----------|---------|
|
|
21
|
+
* | after-tool-call-helpers | pain_${ts}_${hash.slice(0,8)} | 'automatic_hook' | yes | yes |
|
|
22
|
+
* | prompt.ts (GFI) | empathy_gfi_${ts} | 'openclaw_context_bound' | yes | no |
|
|
23
|
+
* | prompt.ts (observer) | empathy_gfi_${ts} | 'openclaw_context_bound' | yes | no |
|
|
24
|
+
* | llm.ts | llm_${ts} | 'openclaw_context_bound' | yes | no |
|
|
25
|
+
* | gate-block-helper | gate_${ts}_${random} | MISSING | MISSING | MISSING |
|
|
26
|
+
*
|
|
27
|
+
* ERR refs:
|
|
28
|
+
* - ERR-009 (fail-loud): tests fail if emit pattern changes without update
|
|
29
|
+
* - ERR-006 (lineage consistency): documents traceId presence/absence per site
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import { describe, expect, it } from 'vitest';
|
|
33
|
+
import * as fs from 'fs';
|
|
34
|
+
import * as path from 'path';
|
|
35
|
+
|
|
36
|
+
// ── Helpers ────────────────────────────────────────────────────────────────
|
|
37
|
+
|
|
38
|
+
function findRepoRoot(cwd: string): string {
|
|
39
|
+
let dir = cwd;
|
|
40
|
+
while (dir !== path.dirname(dir)) {
|
|
41
|
+
if (fs.existsSync(path.join(dir, '.git'))) {
|
|
42
|
+
return dir;
|
|
43
|
+
}
|
|
44
|
+
dir = path.dirname(dir);
|
|
45
|
+
}
|
|
46
|
+
throw new Error(`Could not find repo root from ${cwd} — .git directory not found in any parent`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const repoRoot = findRepoRoot(process.cwd());
|
|
50
|
+
|
|
51
|
+
function read(relativePath: string): string {
|
|
52
|
+
return fs.readFileSync(path.join(repoRoot, relativePath), 'utf8');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Extract the gate-block pain event object from source code.
|
|
57
|
+
* Uses brace-depth tracking so template-string braces inside the object
|
|
58
|
+
* do not prematurely terminate the match (ERR-009 false-positive guard).
|
|
59
|
+
*/
|
|
60
|
+
function extractGateBlockObject(source: string): string | null {
|
|
61
|
+
const marker = /painId:\s*`gate_[^`]+`/.exec(source);
|
|
62
|
+
if (!marker) return null;
|
|
63
|
+
const start = source.lastIndexOf('{', marker.index);
|
|
64
|
+
if (start === -1) return null;
|
|
65
|
+
let depth = 1;
|
|
66
|
+
let i = start + 1;
|
|
67
|
+
while (i < source.length && depth > 0) {
|
|
68
|
+
const ch = source[i];
|
|
69
|
+
if (ch === '{') depth++;
|
|
70
|
+
else if (ch === '}') depth--;
|
|
71
|
+
i++;
|
|
72
|
+
}
|
|
73
|
+
if (depth !== 0) return null;
|
|
74
|
+
return source.slice(start, i);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ── Source file paths ─────────────────────────────────────────────────────
|
|
78
|
+
|
|
79
|
+
const AFTER_TOOL_CALL_HELPERS = 'packages/openclaw-plugin/src/hooks/after-tool-call-helpers.ts';
|
|
80
|
+
const PROMPT = 'packages/openclaw-plugin/src/hooks/prompt.ts';
|
|
81
|
+
const LLM = 'packages/openclaw-plugin/src/hooks/llm.ts';
|
|
82
|
+
const GATE_BLOCK_HELPER = 'packages/openclaw-plugin/src/hooks/gate-block-helper.ts';
|
|
83
|
+
|
|
84
|
+
// ── Tests ─────────────────────────────────────────────────────────────────
|
|
85
|
+
|
|
86
|
+
describe('PRI-433: PainAdmissionEmitter characterization (safety net)', () => {
|
|
87
|
+
|
|
88
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
89
|
+
// Section 1: after-tool-call-helpers.ts — tool failure path
|
|
90
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
91
|
+
|
|
92
|
+
describe('after-tool-call-helpers.ts emit site', () => {
|
|
93
|
+
const source = read(AFTER_TOOL_CALL_HELPERS);
|
|
94
|
+
|
|
95
|
+
it('uses painId format: pain_${Date.now()}_${errorHash.slice(0,8)}', () => {
|
|
96
|
+
expect(source).toMatch(/painId\s*=\s*`pain_\$\{Date\.now\(\)\}_\$\{observation\.errorHash\.slice\(0,\s*8\)\}`/);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('sets provenance to "automatic_hook"', () => {
|
|
100
|
+
expect(source).toMatch(/provenance:\s*'automatic_hook'/);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('includes evidence field via buildTrajectoryEvidence', () => {
|
|
104
|
+
expect(source).toMatch(/evidence:\s*buildTrajectoryEvidence\(wctx,\s*sessionId\)/);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('includes traceId from observation', () => {
|
|
108
|
+
expect(source).toMatch(/traceId:\s*observation\.traceId/);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('sets painType to failureSource variable (tool_failure or dispatch_error)', () => {
|
|
112
|
+
expect(source).toMatch(/painType:\s*failureSource/);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('sets agentId from context variable', () => {
|
|
116
|
+
expect(source).toMatch(/agentId,/);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('uses evaluateTriggerController as gate (PRI-363 single gate)', () => {
|
|
120
|
+
expect(source).toMatch(/evaluateTriggerController/);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('calls emitPainDetectedEvent (not legacy writePainFlag)', () => {
|
|
124
|
+
expect(source).toMatch(/emitPainDetectedEvent\(wctx,/);
|
|
125
|
+
expect(source).not.toMatch(/\bwritePainFlag\b/);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('early-returns when admission.admitted is false', () => {
|
|
129
|
+
expect(source).toMatch(/if\s*\(!admission\.admitted\)\s*return/);
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
134
|
+
// Section 2: prompt.ts — empathy GFI path (2 instances)
|
|
135
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
136
|
+
|
|
137
|
+
describe('prompt.ts emit sites (2 empathy GFI instances)', () => {
|
|
138
|
+
const source = read(PROMPT);
|
|
139
|
+
|
|
140
|
+
it('uses painId format: empathy_gfi_${Date.now()} (both instances)', () => {
|
|
141
|
+
const matches = source.match(/painId:\s*`empathy_gfi_\$\{Date\.now\(\)\}`/g);
|
|
142
|
+
expect(matches).not.toBeNull();
|
|
143
|
+
expect(matches!.length).toBe(2);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('sets provenance to "openclaw_context_bound" (both instances)', () => {
|
|
147
|
+
const matches = source.match(/provenance:\s*'openclaw_context_bound'/g);
|
|
148
|
+
expect(matches).not.toBeNull();
|
|
149
|
+
expect(matches!.length).toBeGreaterThanOrEqual(2);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('sets painType to "user_frustration" (both instances)', () => {
|
|
153
|
+
const matches = source.match(/painType:\s*'user_frustration'/g);
|
|
154
|
+
expect(matches).not.toBeNull();
|
|
155
|
+
expect(matches!.length).toBeGreaterThanOrEqual(2);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('sets source to "user_empathy" (both instances)', () => {
|
|
159
|
+
const matches = source.match(/source:\s*'user_empathy'/g);
|
|
160
|
+
expect(matches).not.toBeNull();
|
|
161
|
+
expect(matches!.length).toBeGreaterThanOrEqual(2);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('hardcodes agentId to "main" (both instances)', () => {
|
|
165
|
+
const matches = source.match(/agentId:\s*'main'/g);
|
|
166
|
+
expect(matches).not.toBeNull();
|
|
167
|
+
expect(matches!.length).toBeGreaterThanOrEqual(2);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('includes evidence field via buildTrajectoryEvidence (both instances)', () => {
|
|
171
|
+
const matches = source.match(/evidence,\s*\n\s*}/g);
|
|
172
|
+
expect(matches).not.toBeNull();
|
|
173
|
+
expect(matches!.length).toBeGreaterThanOrEqual(2);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('does NOT include traceId field (known inconsistency)', () => {
|
|
177
|
+
// Extract the data blocks for empathy emit and verify no traceId.
|
|
178
|
+
// Use a non-greedy dot-all match to avoid truncation at template literal braces like ${Date.now()}
|
|
179
|
+
const empathyBlocks = source.match(/painId:\s*`empathy_gfi_[^`]*`[\s\S]*?^\s{8}\},?/gm);
|
|
180
|
+
expect(empathyBlocks).not.toBeNull();
|
|
181
|
+
for (const block of empathyBlocks!) {
|
|
182
|
+
expect(block).not.toMatch(/traceId/);
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('uses evaluatePainDiagnosticGate as gate (not evaluateTriggerController)', () => {
|
|
187
|
+
expect(source).toMatch(/evaluatePainDiagnosticGate/);
|
|
188
|
+
expect(source).not.toMatch(/evaluateTriggerController/);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('gates emit on gate.shouldDiagnose being true', () => {
|
|
192
|
+
expect(source).toMatch(/if\s*\(gate\.shouldDiagnose\)/);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('calls emitPainDetectedEvent with await (not fire-and-forget)', () => {
|
|
196
|
+
expect(source).toMatch(/await\s+emitPainDetectedEvent/);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('wraps emit in try/catch for error handling', () => {
|
|
200
|
+
expect(source).toMatch(/try\s*\{[\s\S]*?await\s+emitPainDetectedEvent[\s\S]*?\}\s*catch\s*\(emitErr\)/);
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
205
|
+
// Section 3: llm.ts — semantic pain detection
|
|
206
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
207
|
+
|
|
208
|
+
describe('llm.ts emit site', () => {
|
|
209
|
+
const source = read(LLM);
|
|
210
|
+
|
|
211
|
+
it('uses painId format: llm_${Date.now()}', () => {
|
|
212
|
+
expect(source).toMatch(/painId:\s*`llm_\$\{Date\.now\(\)\}`/);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it('sets provenance to "openclaw_context_bound"', () => {
|
|
216
|
+
expect(source).toMatch(/provenance:\s*'openclaw_context_bound'/);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it('sets painType to "user_frustration" (as const)', () => {
|
|
220
|
+
expect(source).toMatch(/painType:\s*'user_frustration'\s+as\s+const/);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it('includes evidence field via buildTrajectoryEvidence', () => {
|
|
224
|
+
expect(source).toMatch(/evidence,\s*\n\s*}/);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it('does NOT include traceId field (known inconsistency)', () => {
|
|
228
|
+
// Match the entire data block from painId to the closing brace of the data object.
|
|
229
|
+
// Use a non-greedy dot-all match to avoid truncation at template literal braces like ${Date.now()}
|
|
230
|
+
const llmBlock = source.match(/painId:\s*`llm_\$\{Date\.now\(\)\}`[\s\S]*?^\s{8}\},?/m);
|
|
231
|
+
expect(llmBlock).not.toBeNull();
|
|
232
|
+
expect(llmBlock![0]).not.toMatch(/traceId/);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it('sets agentId from ctx.agentId (not hardcoded)', () => {
|
|
236
|
+
expect(source).toMatch(/agentId:\s*ctx\.agentId/);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it('uses evaluatePainDiagnosticGate as gate', () => {
|
|
240
|
+
expect(source).toMatch(/evaluatePainDiagnosticGate/);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it('gates emit on gate.shouldDiagnose being true', () => {
|
|
244
|
+
expect(source).toMatch(/if\s*\(gate\.shouldDiagnose\)/);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it('calls emitPainDetectedEvent WITHOUT await (fire-and-forget)', () => {
|
|
248
|
+
// llm.ts calls emitPainDetectedEvent without await — known inconsistency
|
|
249
|
+
expect(source).toMatch(/[^a]\s*emitPainDetectedEvent\(wctx,/);
|
|
250
|
+
expect(source).not.toMatch(/await\s+emitPainDetectedEvent/);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it('uses PEAT-B1 evidence triage (feature-flagged)', () => {
|
|
254
|
+
expect(source).toMatch(/evaluateEvidenceTriage/);
|
|
255
|
+
expect(source).toMatch(/loadFeatureFlagFromConfig/);
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
260
|
+
// Section 4: gate-block-helper.ts — gate block persistence
|
|
261
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
262
|
+
|
|
263
|
+
describe('gate-block-helper.ts emit site', () => {
|
|
264
|
+
const source = read(GATE_BLOCK_HELPER);
|
|
265
|
+
|
|
266
|
+
it('uses painId format: gate_${Date.now()}_${random}', () => {
|
|
267
|
+
expect(source).toMatch(/painId:\s*`gate_\$\{Date\.now\(\)\}_\$\{Math\.random\(\)\.toString\(36\)\.slice\(2,\s*10\)\}`/);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it('sets painType to "user_frustration"', () => {
|
|
271
|
+
expect(source).toMatch(/painType:\s*'user_frustration'/);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it('sets source to "gate_blocked"', () => {
|
|
275
|
+
expect(source).toMatch(/source:\s*'gate_blocked'/);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it('hardcodes agentId to "main"', () => {
|
|
279
|
+
expect(source).toMatch(/agentId:\s*'main'/);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it('uses GATE_BLOCK_PAIN_SCORE constant (45)', () => {
|
|
283
|
+
expect(source).toMatch(/GATE_BLOCK_PAIN_SCORE\s*=\s*45/);
|
|
284
|
+
expect(source).toMatch(/score:\s*GATE_BLOCK_PAIN_SCORE/);
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it('does NOT include provenance field (known inconsistency)', () => {
|
|
288
|
+
const gateBlock = extractGateBlockObject(source);
|
|
289
|
+
expect(gateBlock).not.toBeNull();
|
|
290
|
+
expect(gateBlock).not.toMatch(/provenance/);
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it('does NOT include evidence field (known inconsistency)', () => {
|
|
294
|
+
const gateBlock = extractGateBlockObject(source);
|
|
295
|
+
expect(gateBlock).not.toBeNull();
|
|
296
|
+
expect(gateBlock).not.toMatch(/evidence/);
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
it('does NOT include traceId field (known inconsistency)', () => {
|
|
300
|
+
const gateBlock = extractGateBlockObject(source);
|
|
301
|
+
expect(gateBlock).not.toBeNull();
|
|
302
|
+
expect(gateBlock).not.toMatch(/traceId/);
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
it('uses evaluatePainDiagnosticGate as gate', () => {
|
|
306
|
+
expect(source).toMatch(/evaluatePainDiagnosticGate/);
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
it('gates emit on gate.shouldDiagnose being true', () => {
|
|
310
|
+
expect(source).toMatch(/if\s*\(gate\.shouldDiagnose\)/);
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
it('uses PEAT-B1 evidence triage (feature-flagged)', () => {
|
|
314
|
+
expect(source).toMatch(/evaluateEvidenceTriage/);
|
|
315
|
+
expect(source).toMatch(/loadFeatureFlagFromConfig/);
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
it('calls emitPainDetectedEvent with void + .catch() (fire-and-forget with error handler)', () => {
|
|
319
|
+
expect(source).toMatch(/void\s+emitPainDetectedEvent/);
|
|
320
|
+
expect(source).toMatch(/\.catch\(\(emitErr\)/);
|
|
321
|
+
});
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
325
|
+
// Section 5: Cross-site consistency documentation
|
|
326
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
327
|
+
|
|
328
|
+
describe('cross-site consistency (documents known inconsistencies)', () => {
|
|
329
|
+
it('all 4 sites call emitPainDetectedEvent (not legacy APIs)', () => {
|
|
330
|
+
const files = [AFTER_TOOL_CALL_HELPERS, PROMPT, LLM, GATE_BLOCK_HELPER];
|
|
331
|
+
for (const file of files) {
|
|
332
|
+
const src = read(file);
|
|
333
|
+
expect(src).toMatch(/emitPainDetectedEvent/);
|
|
334
|
+
expect(src).not.toMatch(/\bwritePainFlag\b/);
|
|
335
|
+
expect(src).not.toMatch(/\bcreatePainSignalBridge\b/);
|
|
336
|
+
}
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
it('all 4 sites emit type: "pain_detected"', () => {
|
|
340
|
+
const files = [AFTER_TOOL_CALL_HELPERS, PROMPT, LLM, GATE_BLOCK_HELPER];
|
|
341
|
+
for (const file of files) {
|
|
342
|
+
const src = read(file);
|
|
343
|
+
expect(src).toMatch(/type:\s*'pain_detected'/);
|
|
344
|
+
}
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
it('all 4 sites include ts: new Date().toISOString()', () => {
|
|
348
|
+
const files = [AFTER_TOOL_CALL_HELPERS, PROMPT, LLM, GATE_BLOCK_HELPER];
|
|
349
|
+
for (const file of files) {
|
|
350
|
+
const src = read(file);
|
|
351
|
+
expect(src).toMatch(/ts:\s*new Date\(\)\.toISOString\(\)/);
|
|
352
|
+
}
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
it('all 4 sites include sessionId field', () => {
|
|
356
|
+
const files = [AFTER_TOOL_CALL_HELPERS, PROMPT, LLM, GATE_BLOCK_HELPER];
|
|
357
|
+
for (const file of files) {
|
|
358
|
+
const src = read(file);
|
|
359
|
+
expect(src).toMatch(/sessionId,/);
|
|
360
|
+
}
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
it('all 4 sites include score field', () => {
|
|
364
|
+
const files = [AFTER_TOOL_CALL_HELPERS, PROMPT, LLM, GATE_BLOCK_HELPER];
|
|
365
|
+
for (const file of files) {
|
|
366
|
+
const src = read(file);
|
|
367
|
+
expect(src).toMatch(/score:/);
|
|
368
|
+
}
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
it('all 4 sites include reason field', () => {
|
|
372
|
+
const files = [AFTER_TOOL_CALL_HELPERS, PROMPT, LLM, GATE_BLOCK_HELPER];
|
|
373
|
+
for (const file of files) {
|
|
374
|
+
const src = read(file);
|
|
375
|
+
expect(src).toMatch(/reason:/);
|
|
376
|
+
}
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
it('all 4 sites include source field', () => {
|
|
380
|
+
const files = [AFTER_TOOL_CALL_HELPERS, PROMPT, LLM, GATE_BLOCK_HELPER];
|
|
381
|
+
for (const file of files) {
|
|
382
|
+
const src = read(file);
|
|
383
|
+
expect(src).toMatch(/source:/);
|
|
384
|
+
}
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
it('all 4 sites include painId field', () => {
|
|
388
|
+
// after-tool-call-helpers uses shorthand `painId,`; others use `painId:`
|
|
389
|
+
const files = [AFTER_TOOL_CALL_HELPERS, PROMPT, LLM, GATE_BLOCK_HELPER];
|
|
390
|
+
for (const file of files) {
|
|
391
|
+
const src = read(file);
|
|
392
|
+
expect(src).toMatch(/painId[,:]/);
|
|
393
|
+
}
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
it('documents: only after-tool-call-helpers uses evaluateTriggerController', () => {
|
|
397
|
+
const atc = read(AFTER_TOOL_CALL_HELPERS);
|
|
398
|
+
const prompt = read(PROMPT);
|
|
399
|
+
const llm = read(LLM);
|
|
400
|
+
const gate = read(GATE_BLOCK_HELPER);
|
|
401
|
+
|
|
402
|
+
expect(atc).toMatch(/evaluateTriggerController/);
|
|
403
|
+
expect(prompt).not.toMatch(/evaluateTriggerController/);
|
|
404
|
+
expect(llm).not.toMatch(/evaluateTriggerController/);
|
|
405
|
+
expect(gate).not.toMatch(/evaluateTriggerController/);
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
it('documents: 3 of 4 sites use evaluatePainDiagnosticGate (not after-tool-call-helpers)', () => {
|
|
409
|
+
const atc = read(AFTER_TOOL_CALL_HELPERS);
|
|
410
|
+
const prompt = read(PROMPT);
|
|
411
|
+
const llm = read(LLM);
|
|
412
|
+
const gate = read(GATE_BLOCK_HELPER);
|
|
413
|
+
|
|
414
|
+
expect(atc).not.toMatch(/evaluatePainDiagnosticGate/);
|
|
415
|
+
expect(prompt).toMatch(/evaluatePainDiagnosticGate/);
|
|
416
|
+
expect(llm).toMatch(/evaluatePainDiagnosticGate/);
|
|
417
|
+
expect(gate).toMatch(/evaluatePainDiagnosticGate/);
|
|
418
|
+
});
|
|
419
|
+
});
|
|
420
|
+
});
|