principles-disciple 1.26.0 → 1.28.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 +4 -4
- package/package.json +1 -1
- package/scripts/diagnose-nocturnal.mjs +139 -2
- package/scripts/seed-nocturnal-scenarios.mjs +377 -0
- package/src/core/nocturnal-trinity.ts +8 -7
- package/src/core/trajectory.ts +74 -10
- package/src/index.ts +2 -0
- package/src/service/evolution-worker.ts +137 -43
- package/src/tools/write-pain-flag.ts +191 -0
- package/templates/langs/en/skills/pd-pain-signal/SKILL.md +34 -20
- package/templates/langs/zh/skills/pd-pain-signal/SKILL.md +34 -20
- package/tests/core/nocturnal-e2e.test.ts +224 -0
- package/tests/core/trajectory-correction-pain.test.ts +180 -0
- package/tests/tools/write-pain-flag.test.ts +240 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: pd-pain-signal
|
|
3
|
-
description:
|
|
3
|
+
description: 手动注入痛苦信号到进化系统。TRIGGER CONDITIONS: (1) 用户报告 agent 卡住/循环/无响应 (2) 用户说"记录这个问题"、"强制反思"、"触发痛觉" (3) 工具失败后 agent 没有后续动作 (4) 用户提供人工干预反馈。
|
|
4
4
|
disable-model-invocation: true
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -9,30 +9,44 @@ disable-model-invocation: true
|
|
|
9
9
|
你现在是"人工干预痛觉"组件。
|
|
10
10
|
|
|
11
11
|
**任务**:
|
|
12
|
-
1. 将用户的反馈 `$ARGUMENTS`
|
|
12
|
+
1. 将用户的反馈 `$ARGUMENTS` 作为一条**高优先级**的痛苦信号记录下来。
|
|
13
13
|
2. 告知用户信号已注入,并建议其等待下一个 Hook 触发(如 Stop 或 PreCompact)或手动运行 `/reflection-log`。
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
**⚠️ 写入规则(必须遵守)**
|
|
16
|
+
|
|
17
|
+
**唯一正确的方式**: 使用 `write_pain_flag` 工具。
|
|
16
18
|
|
|
17
19
|
```
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
time: <ISO 8601 时间>
|
|
20
|
+
write_pain_flag({
|
|
21
|
+
reason: "用户反馈原文或错误描述",
|
|
22
|
+
score: 80,
|
|
23
|
+
source: "human_intervention",
|
|
24
|
+
is_risky: false
|
|
25
|
+
})
|
|
25
26
|
```
|
|
26
27
|
|
|
27
|
-
|
|
28
|
-
-
|
|
29
|
-
-
|
|
30
|
-
- `
|
|
31
|
-
- `
|
|
28
|
+
**绝对禁止**:
|
|
29
|
+
- ❌ 直接写 `.state/.pain_flag` 文件(任何方式都不行)
|
|
30
|
+
- ❌ 使用 bash heredoc(`cat <<EOF > .pain_flag`)
|
|
31
|
+
- ❌ 使用 `echo "..." > .pain_flag`
|
|
32
|
+
- ❌ 使用 `node -e` 调用 `writePainFlag` 或 `buildPainFlag`
|
|
33
|
+
- ❌ 任何将 JavaScript 对象 `toString()` 写入文件的方式
|
|
34
|
+
|
|
35
|
+
**为什么必须用工具?**
|
|
36
|
+
`write_pain_flag` 工具封装了正确的序列化逻辑(KV 格式),确保 `.pain_flag` 文件不会被写坏。历史上多次因为直接写文件导致 `[object Object]` 损坏。
|
|
32
37
|
|
|
33
|
-
|
|
34
|
-
- `
|
|
35
|
-
- `
|
|
36
|
-
- `
|
|
38
|
+
**参数说明**:
|
|
39
|
+
- `reason` (必填): 痛苦的原因,描述具体发生了什么
|
|
40
|
+
- `score` (可选): 痛苦分数 0-100,默认 80(人工干预)
|
|
41
|
+
- `source` (可选): 来源,默认 `human_intervention`
|
|
42
|
+
- `is_risky` (可选): 是否高风险,默认 false
|
|
37
43
|
|
|
38
|
-
|
|
44
|
+
**示例**:
|
|
45
|
+
```
|
|
46
|
+
write_pain_flag({
|
|
47
|
+
reason: "Agent 没有读取文件就直接编辑,导致现有逻辑被破坏",
|
|
48
|
+
score: 85,
|
|
49
|
+
source: "human_intervention",
|
|
50
|
+
is_risky: false
|
|
51
|
+
})
|
|
52
|
+
```
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import * as os from 'os';
|
|
4
|
+
import * as path from 'path';
|
|
5
|
+
import { TrajectoryDatabase } from '../../src/core/trajectory.js';
|
|
6
|
+
import { NocturnalTrajectoryExtractor } from '../../src/core/nocturnal-trajectory-extractor.js';
|
|
7
|
+
import { detectViolation } from '../../src/core/nocturnal-compliance.js';
|
|
8
|
+
|
|
9
|
+
function safeRmDir(dir: string): void {
|
|
10
|
+
try { fs.rmSync(dir, { recursive: true, force: true }); } catch { /* ignore */ }
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// ─────────────────────────────────────────────────────────
|
|
14
|
+
// Phase 4a: Correction rejected → pain event → nocturnal selection
|
|
15
|
+
// ─────────────────────────────────────────────────────────
|
|
16
|
+
describe('Phase 4a: Correction rejected integration', () => {
|
|
17
|
+
let workspaceDir: string;
|
|
18
|
+
let trajectory: TrajectoryDatabase;
|
|
19
|
+
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
workspaceDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pd-e2e-correction-'));
|
|
22
|
+
trajectory = new TrajectoryDatabase({ workspaceDir });
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
afterEach(() => {
|
|
26
|
+
trajectory?.dispose();
|
|
27
|
+
safeRmDir(workspaceDir);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('rejected correction creates a pain event with source=correction_rejected', () => {
|
|
31
|
+
// 1. Create session + correction sample
|
|
32
|
+
trajectory.recordSession({ sessionId: 'corr-session', startedAt: new Date().toISOString() });
|
|
33
|
+
const atId = trajectory.recordAssistantTurn({
|
|
34
|
+
sessionId: 'corr-session', runId: 'run-1', provider: 'local', model: 'main',
|
|
35
|
+
rawText: 'Here is my code', sanitizedText: 'Here is my code', usageJson: {}, empathySignalJson: {},
|
|
36
|
+
createdAt: new Date().toISOString(),
|
|
37
|
+
});
|
|
38
|
+
trajectory.recordUserTurn({
|
|
39
|
+
sessionId: 'corr-session', turnIndex: 1, rawText: 'This is wrong!',
|
|
40
|
+
correctionDetected: true, correctionCue: '错了',
|
|
41
|
+
referencesAssistantTurnId: atId, createdAt: new Date().toISOString(),
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// Verify sample was created
|
|
45
|
+
const samples = trajectory.listCorrectionSamples('pending');
|
|
46
|
+
expect(samples.length).toBe(1);
|
|
47
|
+
|
|
48
|
+
// 2. Reject the sample
|
|
49
|
+
trajectory.reviewCorrectionSample(samples[0].sampleId, 'rejected', 'Bad approach');
|
|
50
|
+
|
|
51
|
+
// 3. Verify pain event was created
|
|
52
|
+
const painEvents = trajectory.listPainEventsForSession('corr-session');
|
|
53
|
+
const correctionPain = painEvents.find(e => e.source === 'correction_rejected');
|
|
54
|
+
expect(correctionPain).toBeDefined();
|
|
55
|
+
expect(correctionPain!.score).toBeGreaterThanOrEqual(0);
|
|
56
|
+
expect(correctionPain!.score).toBeLessThanOrEqual(100);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('approved correction does NOT create a pain event', () => {
|
|
60
|
+
trajectory.recordSession({ sessionId: 'approved-session', startedAt: new Date().toISOString() });
|
|
61
|
+
const atId = trajectory.recordAssistantTurn({
|
|
62
|
+
sessionId: 'approved-session', runId: 'run-2', provider: 'local', model: 'main',
|
|
63
|
+
rawText: 'Good code', sanitizedText: 'Good code', usageJson: {}, empathySignalJson: {},
|
|
64
|
+
createdAt: new Date().toISOString(),
|
|
65
|
+
});
|
|
66
|
+
trajectory.recordUserTurn({
|
|
67
|
+
sessionId: 'approved-session', turnIndex: 1, rawText: 'Looks better',
|
|
68
|
+
correctionDetected: true, correctionCue: '改进',
|
|
69
|
+
referencesAssistantTurnId: atId, createdAt: new Date().toISOString(),
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const samples = trajectory.listCorrectionSamples('pending');
|
|
73
|
+
expect(samples.length).toBe(1);
|
|
74
|
+
|
|
75
|
+
trajectory.reviewCorrectionSample(samples[0].sampleId, 'approved', 'Good');
|
|
76
|
+
|
|
77
|
+
const painEvents = trajectory.listPainEventsForSession('approved-session');
|
|
78
|
+
const correctionPain = painEvents.find(e => e.source === 'correction_rejected');
|
|
79
|
+
expect(correctionPain).toBeUndefined();
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// ─────────────────────────────────────────────────────────
|
|
84
|
+
// Phase 4b: Gate block + pain multi-signal test
|
|
85
|
+
// ─────────────────────────────────────────────────────────
|
|
86
|
+
describe('Phase 4b: Multi-signal session selection', () => {
|
|
87
|
+
let workspaceDir: string;
|
|
88
|
+
let trajectory: TrajectoryDatabase;
|
|
89
|
+
|
|
90
|
+
beforeEach(() => {
|
|
91
|
+
workspaceDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pd-e2e-multisignal-'));
|
|
92
|
+
trajectory = new TrajectoryDatabase({ workspaceDir });
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
afterEach(() => {
|
|
96
|
+
trajectory?.dispose();
|
|
97
|
+
safeRmDir(workspaceDir);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('session with more failures has higher violation density', () => {
|
|
101
|
+
// Create session A: just 1 failure
|
|
102
|
+
trajectory.recordSession({ sessionId: 'session-a-pain-only', startedAt: new Date().toISOString() });
|
|
103
|
+
const atIdA = trajectory.recordAssistantTurn({
|
|
104
|
+
sessionId: 'session-a-pain-only', runId: 'run-a', provider: 'local', model: 'main',
|
|
105
|
+
rawText: 'Code here', sanitizedText: 'Code here', usageJson: {}, empathySignalJson: {},
|
|
106
|
+
createdAt: new Date().toISOString(),
|
|
107
|
+
});
|
|
108
|
+
trajectory.recordUserTurn({
|
|
109
|
+
sessionId: 'session-a-pain-only', turnIndex: 1, rawText: '错了',
|
|
110
|
+
correctionDetected: true, correctionCue: '错了',
|
|
111
|
+
referencesAssistantTurnId: atIdA, createdAt: new Date().toISOString(),
|
|
112
|
+
});
|
|
113
|
+
trajectory.recordToolCall({
|
|
114
|
+
sessionId: 'session-a-pain-only', toolName: 'write', outcome: 'failure',
|
|
115
|
+
errorMessage: 'Write failed', errorType: 'Error', createdAt: new Date().toISOString(),
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// Create session B: 2 failures
|
|
119
|
+
trajectory.recordSession({ sessionId: 'session-b-multi', startedAt: new Date().toISOString() });
|
|
120
|
+
const atIdB = trajectory.recordAssistantTurn({
|
|
121
|
+
sessionId: 'session-b-multi', runId: 'run-b', provider: 'local', model: 'main',
|
|
122
|
+
rawText: 'Code here', sanitizedText: 'Code here', usageJson: {}, empathySignalJson: {},
|
|
123
|
+
createdAt: new Date().toISOString(),
|
|
124
|
+
});
|
|
125
|
+
trajectory.recordUserTurn({
|
|
126
|
+
sessionId: 'session-b-multi', turnIndex: 1, rawText: '太复杂了',
|
|
127
|
+
correctionDetected: true, correctionCue: '太复杂了',
|
|
128
|
+
referencesAssistantTurnId: atIdB, createdAt: new Date().toISOString(),
|
|
129
|
+
});
|
|
130
|
+
trajectory.recordToolCall({
|
|
131
|
+
sessionId: 'session-b-multi', toolName: 'edit', outcome: 'failure',
|
|
132
|
+
errorMessage: 'Edit failed', errorType: 'Error', createdAt: new Date().toISOString(),
|
|
133
|
+
});
|
|
134
|
+
trajectory.recordToolCall({
|
|
135
|
+
sessionId: 'session-b-multi', toolName: 'write', outcome: 'failure',
|
|
136
|
+
errorMessage: 'Write failed too', errorType: 'Error', createdAt: new Date().toISOString(),
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// Verify session B has more failure signals
|
|
140
|
+
const extractor = new NocturnalTrajectoryExtractor(trajectory);
|
|
141
|
+
const snapshotA = extractor.getNocturnalSessionSnapshot('session-a-pain-only');
|
|
142
|
+
const snapshotB = extractor.getNocturnalSessionSnapshot('session-b-multi');
|
|
143
|
+
|
|
144
|
+
expect(snapshotA).not.toBeNull();
|
|
145
|
+
expect(snapshotB).not.toBeNull();
|
|
146
|
+
|
|
147
|
+
// Session B should have more violation signals
|
|
148
|
+
const densityA = (snapshotA!.stats.failureCount ?? 0) + (snapshotA!.stats.totalPainEvents ?? 0) * 0.5;
|
|
149
|
+
const densityB = (snapshotB!.stats.failureCount ?? 0) + (snapshotB!.stats.totalPainEvents ?? 0) * 0.5;
|
|
150
|
+
expect(densityB).toBeGreaterThan(densityA);
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// ─────────────────────────────────────────────────────────
|
|
155
|
+
// Phase 4c: Boundary value test matrix
|
|
156
|
+
// ─────────────────────────────────────────────────────────
|
|
157
|
+
describe('Phase 4c: Boundary value tests', () => {
|
|
158
|
+
let workspaceDir: string;
|
|
159
|
+
let trajectory: TrajectoryDatabase;
|
|
160
|
+
|
|
161
|
+
beforeEach(() => {
|
|
162
|
+
workspaceDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pd-e2e-boundary-'));
|
|
163
|
+
trajectory = new TrajectoryDatabase({ workspaceDir });
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
afterEach(() => {
|
|
167
|
+
trajectory?.dispose();
|
|
168
|
+
safeRmDir(workspaceDir);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('session with correction cue is listed as candidate', () => {
|
|
172
|
+
trajectory.recordSession({ sessionId: 'single-pain', startedAt: new Date().toISOString() });
|
|
173
|
+
const atIdC = trajectory.recordAssistantTurn({
|
|
174
|
+
sessionId: 'single-pain', runId: 'run-c', provider: 'local', model: 'main',
|
|
175
|
+
rawText: 'Agent response', sanitizedText: 'Agent response', usageJson: {}, empathySignalJson: {},
|
|
176
|
+
createdAt: new Date().toISOString(),
|
|
177
|
+
});
|
|
178
|
+
trajectory.recordUserTurn({
|
|
179
|
+
sessionId: 'single-pain', turnIndex: 1, rawText: '错了',
|
|
180
|
+
correctionDetected: true, correctionCue: '错了',
|
|
181
|
+
referencesAssistantTurnId: atIdC, createdAt: new Date().toISOString(),
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
const extractor = new NocturnalTrajectoryExtractor(trajectory);
|
|
185
|
+
const candidates = extractor.listRecentNocturnalCandidateSessions({ limit: 10, minToolCalls: 0 });
|
|
186
|
+
|
|
187
|
+
const painCandidate = candidates.find(c => c.sessionId === 'single-pain');
|
|
188
|
+
expect(painCandidate).toBeDefined();
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('detectViolation returns violated for P_* principles with tool failure', () => {
|
|
192
|
+
trajectory.recordSession({ sessionId: 'violation-session', startedAt: new Date().toISOString() });
|
|
193
|
+
trajectory.recordAssistantTurn({
|
|
194
|
+
sessionId: 'violation-session', runId: 'run-d', provider: 'local', model: 'main',
|
|
195
|
+
rawText: 'Code', sanitizedText: 'Code', usageJson: {}, empathySignalJson: {},
|
|
196
|
+
createdAt: new Date().toISOString(),
|
|
197
|
+
});
|
|
198
|
+
trajectory.recordToolCall({
|
|
199
|
+
sessionId: 'violation-session', toolName: 'write', outcome: 'failure',
|
|
200
|
+
errorMessage: 'Failed', errorType: 'Error', createdAt: new Date().toISOString(),
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
const extractor = new NocturnalTrajectoryExtractor(trajectory);
|
|
204
|
+
const snapshot = extractor.getNocturnalSessionSnapshot('violation-session');
|
|
205
|
+
expect(snapshot).not.toBeNull();
|
|
206
|
+
|
|
207
|
+
// P_* principles should be violated with any failure
|
|
208
|
+
const violation = detectViolation('P_001', {
|
|
209
|
+
sessionId: 'violation-session',
|
|
210
|
+
toolCalls: snapshot!.toolCalls.map(tc => ({
|
|
211
|
+
toolName: tc.toolName, outcome: tc.outcome as 'success' | 'failure' | 'blocked',
|
|
212
|
+
errorMessage: tc.errorMessage ?? undefined,
|
|
213
|
+
})),
|
|
214
|
+
painSignals: snapshot!.painEvents.map(pe => ({
|
|
215
|
+
source: pe.source, score: pe.score, severity: pe.severity as 'mild' | 'moderate' | 'severe' | undefined,
|
|
216
|
+
})),
|
|
217
|
+
gateBlocks: [],
|
|
218
|
+
userCorrections: [],
|
|
219
|
+
planApprovals: [],
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
expect(violation.violated).toBe(true);
|
|
223
|
+
});
|
|
224
|
+
});
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import * as os from 'os';
|
|
4
|
+
import * as path from 'path';
|
|
5
|
+
import { TrajectoryDatabase } from '../../src/core/trajectory.js';
|
|
6
|
+
|
|
7
|
+
function safeRmDir(dir: string): void {
|
|
8
|
+
try { fs.rmSync(dir, { recursive: true, force: true }); } catch { /* ignore */ }
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
describe('Trajectory — correction_rejected pain event (Phase 2b)', () => {
|
|
12
|
+
let workspaceDir: string;
|
|
13
|
+
let trajectory: TrajectoryDatabase;
|
|
14
|
+
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
workspaceDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pd-correction-pain-'));
|
|
17
|
+
trajectory = new TrajectoryDatabase({ workspaceDir });
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
afterEach(() => {
|
|
21
|
+
trajectory?.dispose();
|
|
22
|
+
safeRmDir(workspaceDir);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('emits a pain event when a correction sample is rejected', async () => {
|
|
26
|
+
// Step 1: Create a session
|
|
27
|
+
trajectory.recordSession({
|
|
28
|
+
sessionId: 'test-session-001',
|
|
29
|
+
startedAt: new Date().toISOString(),
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// Step 2: Create an assistant turn (to be referenced)
|
|
33
|
+
const assistantTurnId = trajectory.recordAssistantTurn({
|
|
34
|
+
sessionId: 'test-session-001',
|
|
35
|
+
turnIndex: 0,
|
|
36
|
+
rawText: 'Here is some code I wrote',
|
|
37
|
+
createdAt: new Date().toISOString(),
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// Step 3: Create a user turn with correction_cue (triggers auto-creation)
|
|
41
|
+
trajectory.recordUserTurn({
|
|
42
|
+
sessionId: 'test-session-001',
|
|
43
|
+
turnIndex: 1,
|
|
44
|
+
rawText: 'This is wrong. Fix it properly.',
|
|
45
|
+
correctionDetected: true,
|
|
46
|
+
correctionCue: 'This is wrong. Fix it properly.',
|
|
47
|
+
referencesAssistantTurnId: assistantTurnId,
|
|
48
|
+
createdAt: new Date().toISOString(),
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// Wait for async sample creation
|
|
52
|
+
await new Promise(resolve => setTimeout(resolve, 50));
|
|
53
|
+
|
|
54
|
+
// Verify sample was created as pending
|
|
55
|
+
const pendingSamples = trajectory.listCorrectionSamples('pending');
|
|
56
|
+
expect(pendingSamples.length).toBe(1);
|
|
57
|
+
const sampleId = pendingSamples[0].sampleId;
|
|
58
|
+
|
|
59
|
+
// Verify no pain events yet
|
|
60
|
+
const painEventsBefore = trajectory.listPainEventsForSession('test-session-001');
|
|
61
|
+
expect(painEventsBefore.length).toBe(0);
|
|
62
|
+
|
|
63
|
+
// Step 4: Review as rejected
|
|
64
|
+
trajectory.reviewCorrectionSample(sampleId, 'rejected', 'Does not match requirements');
|
|
65
|
+
|
|
66
|
+
// Step 5: Verify pain event was created
|
|
67
|
+
const painEventsAfter = trajectory.listPainEventsForSession('test-session-001');
|
|
68
|
+
expect(painEventsAfter.length).toBe(1);
|
|
69
|
+
|
|
70
|
+
const painEvent = painEventsAfter[0];
|
|
71
|
+
expect(painEvent.source).toBe('correction_rejected');
|
|
72
|
+
expect(painEvent.reason).toContain('Correction rejected');
|
|
73
|
+
expect(painEvent.origin).toBe('system_infer');
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('does NOT emit a pain event when a correction sample is approved', async () => {
|
|
77
|
+
// Setup: session + assistant turn + user correction turn
|
|
78
|
+
trajectory.recordSession({
|
|
79
|
+
sessionId: 'test-session-002',
|
|
80
|
+
startedAt: new Date().toISOString(),
|
|
81
|
+
});
|
|
82
|
+
const assistantTurnId = trajectory.recordAssistantTurn({
|
|
83
|
+
sessionId: 'test-session-002',
|
|
84
|
+
turnIndex: 0,
|
|
85
|
+
rawText: 'Here is some code',
|
|
86
|
+
createdAt: new Date().toISOString(),
|
|
87
|
+
});
|
|
88
|
+
trajectory.recordUserTurn({
|
|
89
|
+
sessionId: 'test-session-002',
|
|
90
|
+
turnIndex: 1,
|
|
91
|
+
rawText: 'This needs work',
|
|
92
|
+
correctionDetected: true,
|
|
93
|
+
correctionCue: 'This needs work',
|
|
94
|
+
referencesAssistantTurnId: assistantTurnId,
|
|
95
|
+
createdAt: new Date().toISOString(),
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// Wait for async sample creation
|
|
99
|
+
await new Promise(resolve => setTimeout(resolve, 50));
|
|
100
|
+
|
|
101
|
+
// Get pending sample
|
|
102
|
+
const pendingSamples = trajectory.listCorrectionSamples('pending');
|
|
103
|
+
expect(pendingSamples.length).toBe(1);
|
|
104
|
+
const sampleId = pendingSamples[0].sampleId;
|
|
105
|
+
|
|
106
|
+
// Review as approved - should NOT trigger pain event
|
|
107
|
+
trajectory.reviewCorrectionSample(sampleId, 'approved', 'Looks good');
|
|
108
|
+
|
|
109
|
+
// Verify NO pain event was created (approved != rejected)
|
|
110
|
+
const painEvents = trajectory.listPainEventsForSession('test-session-002');
|
|
111
|
+
expect(painEvents.length).toBe(0);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('maps quality_score to pain_score correctly (0-100 range)', async () => {
|
|
115
|
+
// Setup: with quality score components
|
|
116
|
+
trajectory.recordSession({
|
|
117
|
+
sessionId: 'test-session-003',
|
|
118
|
+
startedAt: new Date().toISOString(),
|
|
119
|
+
});
|
|
120
|
+
const assistantTurnId = trajectory.recordAssistantTurn({
|
|
121
|
+
sessionId: 'test-session-003',
|
|
122
|
+
turnIndex: 0,
|
|
123
|
+
rawText: 'Code here',
|
|
124
|
+
createdAt: new Date().toISOString(),
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// Create user turn with correction_cue (adds 20 points)
|
|
128
|
+
trajectory.recordUserTurn({
|
|
129
|
+
sessionId: 'test-session-003',
|
|
130
|
+
turnIndex: 1,
|
|
131
|
+
rawText: 'Wrong approach. Try a different algorithm.',
|
|
132
|
+
correctionDetected: true,
|
|
133
|
+
correctionCue: 'Wrong approach. Try a different algorithm.',
|
|
134
|
+
referencesAssistantTurnId: assistantTurnId,
|
|
135
|
+
createdAt: new Date().toISOString(),
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
// Add a failed tool call (adds 20 points)
|
|
139
|
+
trajectory.recordToolCall({
|
|
140
|
+
sessionId: 'test-session-003',
|
|
141
|
+
turnIndex: 2,
|
|
142
|
+
toolName: 'write',
|
|
143
|
+
toolCallIndex: 0,
|
|
144
|
+
paramsJson: { path: '/tmp/test.txt', content: 'test' },
|
|
145
|
+
outcome: 'failure',
|
|
146
|
+
errorMessage: 'Permission denied',
|
|
147
|
+
errorType: 'PermissionError',
|
|
148
|
+
createdAt: new Date().toISOString(),
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// Add successful calls (adds 25 points)
|
|
152
|
+
trajectory.recordToolCall({
|
|
153
|
+
sessionId: 'test-session-003',
|
|
154
|
+
turnIndex: 3,
|
|
155
|
+
toolName: 'read',
|
|
156
|
+
toolCallIndex: 1,
|
|
157
|
+
paramsJson: { path: '/tmp/test.txt' },
|
|
158
|
+
outcome: 'success',
|
|
159
|
+
resultJson: { content: 'file content' },
|
|
160
|
+
createdAt: new Date().toISOString(),
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// Wait for async sample creation
|
|
164
|
+
await new Promise(resolve => setTimeout(resolve, 50));
|
|
165
|
+
|
|
166
|
+
// Get pending sample (quality_score ~65: 20 + 20 + 25)
|
|
167
|
+
const pendingSamples = trajectory.listCorrectionSamples('pending');
|
|
168
|
+
expect(pendingSamples.length).toBe(1);
|
|
169
|
+
const sampleId = pendingSamples[0].sampleId;
|
|
170
|
+
|
|
171
|
+
// Review as rejected
|
|
172
|
+
trajectory.reviewCorrectionSample(sampleId, 'rejected', 'Test rejection');
|
|
173
|
+
|
|
174
|
+
// Verify pain score is clamped to 0-100
|
|
175
|
+
const painEvents = trajectory.listPainEventsForSession('test-session-003');
|
|
176
|
+
expect(painEvents.length).toBe(1);
|
|
177
|
+
expect(painEvents[0].score).toBeGreaterThanOrEqual(0);
|
|
178
|
+
expect(painEvents[0].score).toBeLessThanOrEqual(100);
|
|
179
|
+
});
|
|
180
|
+
});
|