principles-disciple 1.94.0 → 1.95.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 +23 -2
- package/tests/commands/pain.test.ts +180 -1
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
package/src/commands/pain.ts
CHANGED
|
@@ -349,10 +349,31 @@ export async function handlePainReportCommand(ctx: PluginCommandContext): Promis
|
|
|
349
349
|
};
|
|
350
350
|
}
|
|
351
351
|
|
|
352
|
+
if (result.status === 'retried') {
|
|
353
|
+
const errorInfo = result.failureCategory
|
|
354
|
+
? (isZh ? `\n⚠️ **错误类别**: ${result.failureCategory}` : `\n⚠️ **Error category**: ${result.failureCategory}`)
|
|
355
|
+
: '';
|
|
356
|
+
const messageInfo = result.message
|
|
357
|
+
? (isZh ? `\n📝 **详情**: ${result.message}` : `\n📝 **Detail**: ${result.message}`)
|
|
358
|
+
: '';
|
|
359
|
+
return {
|
|
360
|
+
text: isZh
|
|
361
|
+
? `✅ Pain 已记录,诊断任务已进入重试\n\n📋 **Pain ID**: ${result.painId}\n🔧 **Task ID**: ${result.taskId}${errorInfo}${messageInfo}\n\n诊断任务将在后台自动重试。使用 \`/pd-status\` 查看任务状态。`
|
|
362
|
+
: `✅ Pain recorded, diagnosis task entered retry\n\n📋 **Pain ID**: ${result.painId}\n🔧 **Task ID**: ${result.taskId}${errorInfo}${messageInfo}\n\nThe diagnosis task will retry automatically in the background. Use \`/pd-status\` to check task status.`,
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// status === 'failed' | 'skipped' | 'degraded' — pain was NOT accepted
|
|
367
|
+
const reasonInfo = result.failureCategory
|
|
368
|
+
? (isZh ? `\n⚠️ **原因**: ${result.failureCategory}` : `\n⚠️ **Reason**: ${result.failureCategory}`)
|
|
369
|
+
: '';
|
|
370
|
+
const messageInfo = result.message
|
|
371
|
+
? (isZh ? `\n📝 **详情**: ${result.message}` : `\n📝 **Detail**: ${result.message}`)
|
|
372
|
+
: '';
|
|
352
373
|
return {
|
|
353
374
|
text: isZh
|
|
354
|
-
?
|
|
355
|
-
:
|
|
375
|
+
? `❌ Pain 记录未成功 (status: ${result.status})${reasonInfo}${messageInfo}\n\n请检查系统日志或使用 \`/pd-status\` 查看状态。`
|
|
376
|
+
: `❌ Pain recording not accepted (status: ${result.status})${reasonInfo}${messageInfo}\n\nCheck system logs or use \`/pd-status\` for status.`,
|
|
356
377
|
};
|
|
357
378
|
} catch (err) {
|
|
358
379
|
return {
|
|
@@ -1,10 +1,19 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
-
import { handlePainCommand } from '../../src/commands/pain.js';
|
|
2
|
+
import { handlePainCommand, handlePainReportCommand } from '../../src/commands/pain.js';
|
|
3
3
|
import * as sessionTracker from '../../src/core/session-tracker.js';
|
|
4
4
|
import { WorkspaceContext } from '../../src/core/workspace-context.js';
|
|
5
5
|
|
|
6
6
|
vi.mock('../../src/core/session-tracker.js');
|
|
7
7
|
vi.mock('../../src/core/workspace-context.js');
|
|
8
|
+
vi.mock('../../src/core/pd-config-loader.js', () => ({
|
|
9
|
+
loadPdConfigForPlugin: vi.fn().mockReturnValue({ ok: true, effective: {}, source: 'defaults', warnings: [], errors: [] }),
|
|
10
|
+
}));
|
|
11
|
+
vi.mock('@principles/core/runtime-v2', () => ({
|
|
12
|
+
PainToPrincipleService: vi.fn(),
|
|
13
|
+
PrincipleTreeLedgerAdapter: vi.fn(function(this: any) { this.stateDir = ''; }),
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
import { PainToPrincipleService } from '@principles/core/runtime-v2';
|
|
8
17
|
|
|
9
18
|
describe('Pain Command', () => {
|
|
10
19
|
const workspaceDir = '/mock/workspace';
|
|
@@ -106,3 +115,173 @@ describe('Pain Command', () => {
|
|
|
106
115
|
expect(result.text).toContain('approved samples');
|
|
107
116
|
});
|
|
108
117
|
});
|
|
118
|
+
|
|
119
|
+
describe('Pain Report Command (/pd-pain)', () => {
|
|
120
|
+
const workspaceDir = '/mock/workspace';
|
|
121
|
+
const sessionId = 's1';
|
|
122
|
+
|
|
123
|
+
const mockEvolutionReducer = { emitSync: vi.fn() };
|
|
124
|
+
const mockWctx = {
|
|
125
|
+
workspaceDir,
|
|
126
|
+
stateDir: '/mock/workspace/.state',
|
|
127
|
+
evolutionReducer: mockEvolutionReducer,
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
beforeEach(() => {
|
|
131
|
+
vi.clearAllMocks();
|
|
132
|
+
vi.mocked(WorkspaceContext.fromHookContext).mockReturnValue(mockWctx as any);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
async function runPainReport(args: string, lang = 'en') {
|
|
136
|
+
return handlePainReportCommand({
|
|
137
|
+
args,
|
|
138
|
+
config: { workspaceDir, language: lang },
|
|
139
|
+
sessionId,
|
|
140
|
+
} as any);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
it('rejects empty args', async () => {
|
|
144
|
+
const result = await runPainReport('');
|
|
145
|
+
expect(result.text).toContain('Please provide a pain reason');
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('rejects missing session ID', async () => {
|
|
149
|
+
const result = await handlePainReportCommand({
|
|
150
|
+
args: 'something broke',
|
|
151
|
+
config: { workspaceDir, language: 'en' },
|
|
152
|
+
} as any);
|
|
153
|
+
expect(result.text).toContain('Session ID not available');
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('reports success when recordPain returns succeeded', async () => {
|
|
157
|
+
const mockRecordPain = vi.fn().mockResolvedValue({
|
|
158
|
+
status: 'succeeded',
|
|
159
|
+
painId: 'manual_123_abc',
|
|
160
|
+
taskId: 'diagnosis_manual_123_abc',
|
|
161
|
+
candidateIds: [],
|
|
162
|
+
ledgerEntryIds: [],
|
|
163
|
+
observabilityWarnings: [],
|
|
164
|
+
latencyMs: 100,
|
|
165
|
+
});
|
|
166
|
+
vi.mocked(PainToPrincipleService).mockImplementation(function(this: any) { this.recordPain = mockRecordPain; } as any);
|
|
167
|
+
|
|
168
|
+
const result = await runPainReport('something broke');
|
|
169
|
+
expect(result.text).toContain('Pain recorded');
|
|
170
|
+
expect(result.text).toContain('manual_');
|
|
171
|
+
expect(result.text).not.toContain('not accepted');
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('reports retried as pain recorded with retry info, NOT as "not accepted"', async () => {
|
|
175
|
+
const mockRecordPain = vi.fn().mockResolvedValue({
|
|
176
|
+
status: 'retried',
|
|
177
|
+
painId: 'manual_456_def',
|
|
178
|
+
taskId: 'diagnosis_manual_456_def',
|
|
179
|
+
failureCategory: 'output_invalid',
|
|
180
|
+
message: 'Diagnostician output failed validation',
|
|
181
|
+
candidateIds: [],
|
|
182
|
+
ledgerEntryIds: [],
|
|
183
|
+
observabilityWarnings: [],
|
|
184
|
+
latencyMs: 200,
|
|
185
|
+
});
|
|
186
|
+
vi.mocked(PainToPrincipleService).mockImplementation(function(this: any) { this.recordPain = mockRecordPain; } as any);
|
|
187
|
+
|
|
188
|
+
const result = await runPainReport('something broke');
|
|
189
|
+
expect(result.text).toContain('Pain recorded');
|
|
190
|
+
expect(result.text).toContain('retry');
|
|
191
|
+
expect(result.text).toContain('diagnosis_manual_456_def');
|
|
192
|
+
expect(result.text).toContain('output_invalid');
|
|
193
|
+
expect(result.text).toContain('/pd-status');
|
|
194
|
+
// Must NOT say "not accepted" or "failed"
|
|
195
|
+
expect(result.text).not.toContain('not accepted');
|
|
196
|
+
expect(result.text).not.toContain('未成功');
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('reports retried in Chinese correctly', async () => {
|
|
200
|
+
const mockRecordPain = vi.fn().mockResolvedValue({
|
|
201
|
+
status: 'retried',
|
|
202
|
+
painId: 'manual_789_xyz',
|
|
203
|
+
taskId: 'diagnosis_manual_789_xyz',
|
|
204
|
+
failureCategory: 'output_invalid',
|
|
205
|
+
candidateIds: [],
|
|
206
|
+
ledgerEntryIds: [],
|
|
207
|
+
observabilityWarnings: [],
|
|
208
|
+
latencyMs: 200,
|
|
209
|
+
});
|
|
210
|
+
vi.mocked(PainToPrincipleService).mockImplementation(function(this: any) { this.recordPain = mockRecordPain; } as any);
|
|
211
|
+
|
|
212
|
+
const result = await runPainReport('something broke', 'zh');
|
|
213
|
+
expect(result.text).toContain('Pain 已记录');
|
|
214
|
+
expect(result.text).toContain('重试');
|
|
215
|
+
expect(result.text).not.toContain('未成功');
|
|
216
|
+
expect(result.text).not.toContain('not accepted');
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it('reports retried without failureCategory or message', async () => {
|
|
220
|
+
const mockRecordPain = vi.fn().mockResolvedValue({
|
|
221
|
+
status: 'retried',
|
|
222
|
+
painId: 'manual_000_nocat',
|
|
223
|
+
taskId: 'diagnosis_manual_000_nocat',
|
|
224
|
+
candidateIds: [],
|
|
225
|
+
ledgerEntryIds: [],
|
|
226
|
+
observabilityWarnings: [],
|
|
227
|
+
latencyMs: 150,
|
|
228
|
+
});
|
|
229
|
+
vi.mocked(PainToPrincipleService).mockImplementation(function(this: any) { this.recordPain = mockRecordPain; } as any);
|
|
230
|
+
|
|
231
|
+
const result = await runPainReport('something broke');
|
|
232
|
+
expect(result.text).toContain('Pain recorded');
|
|
233
|
+
expect(result.text).toContain('retry');
|
|
234
|
+
expect(result.text).toContain('diagnosis_manual_000_nocat');
|
|
235
|
+
// No error category or detail lines when absent
|
|
236
|
+
expect(result.text).not.toContain('Error category');
|
|
237
|
+
expect(result.text).not.toContain('Detail');
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it('reports failed as "not accepted" with reason', async () => {
|
|
241
|
+
const mockRecordPain = vi.fn().mockResolvedValue({
|
|
242
|
+
status: 'failed',
|
|
243
|
+
painId: 'manual_fail_1',
|
|
244
|
+
taskId: 'diagnosis_manual_fail_1',
|
|
245
|
+
failureCategory: 'runtime_unavailable',
|
|
246
|
+
message: 'No runner available',
|
|
247
|
+
candidateIds: [],
|
|
248
|
+
ledgerEntryIds: [],
|
|
249
|
+
observabilityWarnings: [],
|
|
250
|
+
latencyMs: 50,
|
|
251
|
+
});
|
|
252
|
+
vi.mocked(PainToPrincipleService).mockImplementation(function(this: any) { this.recordPain = mockRecordPain; } as any);
|
|
253
|
+
|
|
254
|
+
const result = await runPainReport('something broke');
|
|
255
|
+
expect(result.text).toContain('not accepted');
|
|
256
|
+
expect(result.text).toContain('failed');
|
|
257
|
+
expect(result.text).toContain('runtime_unavailable');
|
|
258
|
+
expect(result.text).toContain('No runner available');
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it('reports degraded as "not accepted"', async () => {
|
|
262
|
+
const mockRecordPain = vi.fn().mockResolvedValue({
|
|
263
|
+
status: 'degraded',
|
|
264
|
+
painId: 'manual_deg_1',
|
|
265
|
+
taskId: 'diagnosis_manual_deg_1',
|
|
266
|
+
candidateIds: [],
|
|
267
|
+
ledgerEntryIds: [],
|
|
268
|
+
observabilityWarnings: [],
|
|
269
|
+
latencyMs: 30,
|
|
270
|
+
});
|
|
271
|
+
vi.mocked(PainToPrincipleService).mockImplementation(function(this: any) { this.recordPain = mockRecordPain; } as any);
|
|
272
|
+
|
|
273
|
+
const result = await runPainReport('something broke');
|
|
274
|
+
expect(result.text).toContain('not accepted');
|
|
275
|
+
expect(result.text).toContain('degraded');
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it('reports error on exception', async () => {
|
|
279
|
+
vi.mocked(PainToPrincipleService).mockImplementation(function(this: any) {
|
|
280
|
+
throw new Error('DB connection failed');
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
const result = await runPainReport('something broke');
|
|
284
|
+
expect(result.text).toContain('Failed to record pain');
|
|
285
|
+
expect(result.text).toContain('DB connection failed');
|
|
286
|
+
});
|
|
287
|
+
});
|