groove-dev 0.27.102 → 0.27.104

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.
Files changed (66) hide show
  1. package/CLAUDE.md +0 -7
  2. package/moe-training/client/domain-tagger.js +205 -0
  3. package/moe-training/client/edit-normalizer.js +188 -0
  4. package/moe-training/client/envelope-builder.js +1 -1
  5. package/moe-training/client/index.js +1 -0
  6. package/moe-training/client/parsers/claude-code.js +56 -9
  7. package/moe-training/client/parsers/codex.js +25 -5
  8. package/moe-training/client/parsers/gemini.js +21 -2
  9. package/moe-training/client/parsers/grok.js +18 -0
  10. package/moe-training/client/step-classifier.js +1 -1
  11. package/moe-training/client/trajectory-capture.js +109 -8
  12. package/moe-training/server/routes/ingest.js +26 -0
  13. package/moe-training/server/verifier.js +34 -0
  14. package/moe-training/shared/constants.js +9 -0
  15. package/moe-training/shared/envelope-schema.js +128 -2
  16. package/moe-training/test/client/domain-tagger.test.js +203 -0
  17. package/moe-training/test/client/edit-normalizer.test.js +376 -0
  18. package/moe-training/test/client/envelope-builder.test.js +28 -0
  19. package/moe-training/test/client/parsers/claude-code.test.js +248 -38
  20. package/moe-training/test/client/parsers/codex.test.js +2 -0
  21. package/moe-training/test/client/parsers/gemini.test.js +2 -0
  22. package/moe-training/test/client/step-classifier.test.js +42 -0
  23. package/moe-training/test/client/trajectory-capture.test.js +440 -0
  24. package/moe-training/test/server/verifier.test.js +94 -0
  25. package/moe-training/test/shared/envelope-schema.test.js +291 -0
  26. package/node_modules/@groove-dev/cli/package.json +1 -1
  27. package/node_modules/@groove-dev/daemon/package.json +1 -1
  28. package/node_modules/@groove-dev/daemon/src/api.js +19 -3
  29. package/node_modules/@groove-dev/gui/dist/assets/{index-8gdXdRnq.js → index-oUBAPJv6.js} +15 -15
  30. package/node_modules/@groove-dev/gui/dist/index.html +1 -1
  31. package/node_modules/@groove-dev/gui/package.json +1 -1
  32. package/node_modules/@groove-dev/gui/src/components/settings/ProviderSetupWizard.jsx +28 -2
  33. package/node_modules/@groove-dev/gui/src/views/settings.jsx +23 -2
  34. package/node_modules/moe-training/client/domain-tagger.js +205 -0
  35. package/node_modules/moe-training/client/edit-normalizer.js +188 -0
  36. package/node_modules/moe-training/client/envelope-builder.js +1 -1
  37. package/node_modules/moe-training/client/index.js +1 -0
  38. package/node_modules/moe-training/client/parsers/claude-code.js +56 -9
  39. package/node_modules/moe-training/client/parsers/codex.js +25 -5
  40. package/node_modules/moe-training/client/parsers/gemini.js +21 -2
  41. package/node_modules/moe-training/client/parsers/grok.js +18 -0
  42. package/node_modules/moe-training/client/step-classifier.js +1 -1
  43. package/node_modules/moe-training/client/trajectory-capture.js +109 -8
  44. package/node_modules/moe-training/server/routes/ingest.js +26 -0
  45. package/node_modules/moe-training/server/verifier.js +34 -0
  46. package/node_modules/moe-training/shared/constants.js +9 -0
  47. package/node_modules/moe-training/shared/envelope-schema.js +128 -2
  48. package/node_modules/moe-training/test/client/domain-tagger.test.js +203 -0
  49. package/node_modules/moe-training/test/client/edit-normalizer.test.js +376 -0
  50. package/node_modules/moe-training/test/client/envelope-builder.test.js +28 -0
  51. package/node_modules/moe-training/test/client/parsers/claude-code.test.js +248 -38
  52. package/node_modules/moe-training/test/client/parsers/codex.test.js +2 -0
  53. package/node_modules/moe-training/test/client/parsers/gemini.test.js +2 -0
  54. package/node_modules/moe-training/test/client/step-classifier.test.js +42 -0
  55. package/node_modules/moe-training/test/client/trajectory-capture.test.js +440 -0
  56. package/node_modules/moe-training/test/server/verifier.test.js +94 -0
  57. package/node_modules/moe-training/test/shared/envelope-schema.test.js +291 -0
  58. package/package.json +1 -1
  59. package/packages/cli/package.json +1 -1
  60. package/packages/daemon/package.json +1 -1
  61. package/packages/daemon/src/api.js +19 -3
  62. package/packages/gui/dist/assets/{index-8gdXdRnq.js → index-oUBAPJv6.js} +15 -15
  63. package/packages/gui/dist/index.html +1 -1
  64. package/packages/gui/package.json +1 -1
  65. package/packages/gui/src/components/settings/ProviderSetupWizard.jsx +28 -2
  66. package/packages/gui/src/views/settings.jsx +23 -2
@@ -0,0 +1,440 @@
1
+ // FSL-1.1-Apache-2.0 — see LICENSE
2
+
3
+ import { describe, it } from 'node:test';
4
+ import assert from 'node:assert/strict';
5
+ import { TrajectoryCapture } from '../../client/trajectory-capture.js';
6
+
7
+ function makeTc() {
8
+ const tc = new TrajectoryCapture({ centralCommandUrl: 'http://localhost:9999' });
9
+ return tc;
10
+ }
11
+
12
+ function makeCtx(overrides = {}) {
13
+ return {
14
+ sessionId: 'sess_test_1',
15
+ stepCount: overrides.stepCount ?? 20,
16
+ totalTokens: overrides.totalTokens ?? 5000,
17
+ errorsEncountered: overrides.errorsEncountered ?? 0,
18
+ errorsRecovered: overrides.errorsRecovered ?? 0,
19
+ coordinationEvents: overrides.coordinationEvents ?? 0,
20
+ revisionRounds: overrides.revisionRounds ?? 0,
21
+ allSteps: overrides.allSteps ?? [
22
+ { step: 1, type: 'thought', content: 'thinking' },
23
+ { step: 2, type: 'action', tool: 'Bash', content: 'running command' },
24
+ { step: 3, type: 'observation', content: 'output here' },
25
+ { step: 4, type: 'thought', content: 'next step' },
26
+ { step: 5, type: 'action', tool: 'Edit', content: 'editing file' },
27
+ ],
28
+ metadata: {
29
+ session_quality: overrides.quality ?? 80,
30
+ },
31
+ builder: {
32
+ buildSessionClose: (outcome) => ({
33
+ envelope_id: 'env_test_close',
34
+ session_id: 'sess_test_1',
35
+ type: 'SESSION_CLOSE',
36
+ outcome,
37
+ }),
38
+ },
39
+ };
40
+ }
41
+
42
+ describe('TrajectoryCapture — quality tier', () => {
43
+ it('TIER_A: high quality, no errors, no interventions, SUCCESS', () => {
44
+ const tc = makeTc();
45
+ const ctx = makeCtx({ quality: 80, errorsEncountered: 0 });
46
+ const result = tc._computeQualityTier(ctx, 'SUCCESS', 0);
47
+ assert.equal(result.tier, 'TIER_A');
48
+ assert.equal(result.reason, 'high_quality_no_errors');
49
+ });
50
+
51
+ it('TIER_A requires quality >= 70', () => {
52
+ const tc = makeTc();
53
+ const ctx = makeCtx({ quality: 69, errorsEncountered: 0 });
54
+ const result = tc._computeQualityTier(ctx, 'SUCCESS', 0);
55
+ assert.notEqual(result.tier, 'TIER_A');
56
+ });
57
+
58
+ it('TIER_A requires zero errors', () => {
59
+ const tc = makeTc();
60
+ const ctx = makeCtx({ quality: 80, errorsEncountered: 1 });
61
+ const result = tc._computeQualityTier(ctx, 'SUCCESS', 0);
62
+ assert.notEqual(result.tier, 'TIER_A');
63
+ });
64
+
65
+ it('TIER_A requires zero user interventions', () => {
66
+ const tc = makeTc();
67
+ const ctx = makeCtx({ quality: 80, errorsEncountered: 0 });
68
+ const result = tc._computeQualityTier(ctx, 'SUCCESS', 1);
69
+ assert.notEqual(result.tier, 'TIER_A');
70
+ });
71
+
72
+ it('TIER_A requires SUCCESS status', () => {
73
+ const tc = makeTc();
74
+ const ctx = makeCtx({ quality: 80, errorsEncountered: 0 });
75
+ const result = tc._computeQualityTier(ctx, 'CRASH', 0);
76
+ assert.notEqual(result.tier, 'TIER_A');
77
+ });
78
+
79
+ it('TIER_B: moderate quality >= 50 with SUCCESS', () => {
80
+ const tc = makeTc();
81
+ const ctx = makeCtx({ quality: 55, errorsEncountered: 0 });
82
+ const result = tc._computeQualityTier(ctx, 'SUCCESS', 0);
83
+ assert.equal(result.tier, 'TIER_B');
84
+ assert.equal(result.reason, 'moderate_quality');
85
+ });
86
+
87
+ it('TIER_C: non-SUCCESS status overrides quality', () => {
88
+ const tc = makeTc();
89
+ const ctx = makeCtx({ quality: 55, errorsEncountered: 0 });
90
+ const result = tc._computeQualityTier(ctx, 'CRASH', 0);
91
+ assert.equal(result.tier, 'TIER_C');
92
+ assert.equal(result.reason, 'non_success_status');
93
+ });
94
+
95
+ it('TIER_B: errors fully recovered', () => {
96
+ const tc = makeTc();
97
+ const ctx = makeCtx({ quality: 40, errorsEncountered: 2, errorsRecovered: 2 });
98
+ const result = tc._computeQualityTier(ctx, 'SUCCESS', 0);
99
+ assert.equal(result.tier, 'TIER_B');
100
+ assert.equal(result.reason, 'errors_recovered');
101
+ });
102
+
103
+ it('TIER_C: low quality below 50', () => {
104
+ const tc = makeTc();
105
+ const ctx = makeCtx({ quality: 30, errorsEncountered: 0 });
106
+ const result = tc._computeQualityTier(ctx, 'SUCCESS', 0);
107
+ assert.equal(result.tier, 'TIER_C');
108
+ assert.equal(result.reason, 'low_quality');
109
+ });
110
+
111
+ it('TIER_C: unrecovered errors with low quality', () => {
112
+ const tc = makeTc();
113
+ const ctx = makeCtx({ quality: 45, errorsEncountered: 3, errorsRecovered: 1 });
114
+ const result = tc._computeQualityTier(ctx, 'SUCCESS', 2);
115
+ assert.equal(result.tier, 'TIER_C');
116
+ });
117
+
118
+ it('TIER_B at exactly quality=50 boundary', () => {
119
+ const tc = makeTc();
120
+ const ctx = makeCtx({ quality: 50, errorsEncountered: 0 });
121
+ const result = tc._computeQualityTier(ctx, 'SUCCESS', 0);
122
+ assert.equal(result.tier, 'TIER_B');
123
+ });
124
+
125
+ it('TIER_A at exactly quality=70 boundary', () => {
126
+ const tc = makeTc();
127
+ const ctx = makeCtx({ quality: 70, errorsEncountered: 0 });
128
+ const result = tc._computeQualityTier(ctx, 'SUCCESS', 0);
129
+ assert.equal(result.tier, 'TIER_A');
130
+ });
131
+ });
132
+
133
+ describe('TrajectoryCapture — training eligibility', () => {
134
+ it('eligible when all criteria met', () => {
135
+ const tc = makeTc();
136
+ const ctx = makeCtx();
137
+ const result = tc._computeTrainingEligibility(ctx, 60);
138
+ assert.equal(result.eligible, true);
139
+ assert.equal(result.exclusionReason, null);
140
+ });
141
+
142
+ it('ineligible: too_few_steps (< 5)', () => {
143
+ const tc = makeTc();
144
+ const ctx = makeCtx({ stepCount: 4 });
145
+ const result = tc._computeTrainingEligibility(ctx, 60);
146
+ assert.equal(result.eligible, false);
147
+ assert.equal(result.exclusionReason, 'too_few_steps');
148
+ });
149
+
150
+ it('ineligible: no_actions (no step with type action + tool)', () => {
151
+ const tc = makeTc();
152
+ const ctx = makeCtx({
153
+ allSteps: [
154
+ { step: 1, type: 'thought', content: 'thinking' },
155
+ { step: 2, type: 'thought', content: 'more thinking' },
156
+ { step: 3, type: 'observation', content: 'output' },
157
+ { step: 4, type: 'thought', content: 'done' },
158
+ { step: 5, type: 'resolution', content: 'completed' },
159
+ ],
160
+ });
161
+ const result = tc._computeTrainingEligibility(ctx, 60);
162
+ assert.equal(result.eligible, false);
163
+ assert.equal(result.exclusionReason, 'no_actions');
164
+ });
165
+
166
+ it('ineligible: no_observations', () => {
167
+ const tc = makeTc();
168
+ const ctx = makeCtx({
169
+ allSteps: [
170
+ { step: 1, type: 'thought', content: 'thinking' },
171
+ { step: 2, type: 'action', tool: 'Bash', content: 'run' },
172
+ { step: 3, type: 'thought', content: 'hmm' },
173
+ { step: 4, type: 'action', tool: 'Edit', content: 'edit' },
174
+ { step: 5, type: 'resolution', content: 'done' },
175
+ ],
176
+ });
177
+ const result = tc._computeTrainingEligibility(ctx, 60);
178
+ assert.equal(result.eligible, false);
179
+ assert.equal(result.exclusionReason, 'no_observations');
180
+ });
181
+
182
+ it('ineligible: insufficient_tokens (< 500)', () => {
183
+ const tc = makeTc();
184
+ const ctx = makeCtx({ totalTokens: 400 });
185
+ const result = tc._computeTrainingEligibility(ctx, 60);
186
+ assert.equal(result.eligible, false);
187
+ assert.equal(result.exclusionReason, 'insufficient_tokens');
188
+ });
189
+
190
+ it('ineligible: too_short (duration < 10s)', () => {
191
+ const tc = makeTc();
192
+ const ctx = makeCtx({ totalTokens: 5000 });
193
+ const result = tc._computeTrainingEligibility(ctx, 9);
194
+ assert.equal(result.eligible, false);
195
+ assert.equal(result.exclusionReason, 'too_short');
196
+ });
197
+
198
+ it('eligible at exact boundary values', () => {
199
+ const tc = makeTc();
200
+ const ctx = makeCtx({ stepCount: 5, totalTokens: 500 });
201
+ const result = tc._computeTrainingEligibility(ctx, 10);
202
+ assert.equal(result.eligible, true);
203
+ assert.equal(result.exclusionReason, null);
204
+ });
205
+
206
+ it('exclusion reasons follow priority order', () => {
207
+ const tc = makeTc();
208
+ const ctx = makeCtx({
209
+ stepCount: 3,
210
+ totalTokens: 100,
211
+ allSteps: [
212
+ { step: 1, type: 'thought', content: 'thinking' },
213
+ { step: 2, type: 'thought', content: 'more' },
214
+ { step: 3, type: 'thought', content: 'done' },
215
+ ],
216
+ });
217
+ const result = tc._computeTrainingEligibility(ctx, 5);
218
+ assert.equal(result.exclusionReason, 'too_few_steps');
219
+ });
220
+ });
221
+
222
+ describe('TrajectoryCapture — user feedback emission', () => {
223
+ it('emits accepted signal on SUCCESS with 0 interventions and 0 revisions', () => {
224
+ const tc = makeTc();
225
+ const captured = [];
226
+ tc._signAndTransmit = (_sid, envelope) => { captured.push(envelope); };
227
+
228
+ const ctx = makeCtx({ revisionRounds: 0 });
229
+ ctx.stepCount = 10;
230
+ tc._emitUserFeedback(ctx, 'SUCCESS', 0);
231
+
232
+ assert.equal(captured.length, 1);
233
+ assert.equal(captured[0].type, 'USER_FEEDBACK');
234
+ assert.equal(captured[0].feedback.signal, 'accepted');
235
+ assert.equal(captured[0].feedback.revision_rounds, 0);
236
+ assert.equal(captured[0].feedback.target_step, 10);
237
+ });
238
+
239
+ it('emits iterated signal on SUCCESS with revision rounds > 0', () => {
240
+ const tc = makeTc();
241
+ const captured = [];
242
+ tc._signAndTransmit = (_sid, envelope) => { captured.push(envelope); };
243
+
244
+ const ctx = makeCtx({ revisionRounds: 3 });
245
+ ctx.stepCount = 25;
246
+ tc._emitUserFeedback(ctx, 'SUCCESS', 2);
247
+
248
+ assert.equal(captured.length, 1);
249
+ assert.equal(captured[0].feedback.signal, 'iterated');
250
+ assert.equal(captured[0].feedback.revision_rounds, 3);
251
+ assert.ok(captured[0].feedback.context.includes('3 revision'));
252
+ });
253
+
254
+ it('does not emit feedback on non-SUCCESS status', () => {
255
+ const tc = makeTc();
256
+ const captured = [];
257
+ tc._signAndTransmit = (_sid, envelope) => { captured.push(envelope); };
258
+
259
+ const ctx = makeCtx({ revisionRounds: 0 });
260
+ tc._emitUserFeedback(ctx, 'CRASH', 0);
261
+
262
+ assert.equal(captured.length, 0);
263
+ });
264
+
265
+ it('does not emit feedback when SHUTDOWN', () => {
266
+ const tc = makeTc();
267
+ const captured = [];
268
+ tc._signAndTransmit = (_sid, envelope) => { captured.push(envelope); };
269
+
270
+ const ctx = makeCtx({ revisionRounds: 0 });
271
+ tc._emitUserFeedback(ctx, 'SHUTDOWN', 0);
272
+
273
+ assert.equal(captured.length, 0);
274
+ });
275
+
276
+ it('feedback envelope has correct structure', () => {
277
+ const tc = makeTc();
278
+ const captured = [];
279
+ tc._signAndTransmit = (_sid, envelope) => { captured.push(envelope); };
280
+
281
+ const ctx = makeCtx({ revisionRounds: 0 });
282
+ ctx.stepCount = 15;
283
+ tc._emitUserFeedback(ctx, 'SUCCESS', 0);
284
+
285
+ const fb = captured[0];
286
+ assert.ok(fb.envelope_id.startsWith('env_'));
287
+ assert.equal(fb.session_id, 'sess_test_1');
288
+ assert.equal(fb.type, 'USER_FEEDBACK');
289
+ assert.ok(fb.attestation);
290
+ assert.ok(fb.feedback.timestamp > 0);
291
+ assert.equal(fb.feedback.delta_summary, null);
292
+ });
293
+ });
294
+
295
+ describe('TrajectoryCapture — token counting via _processStep', () => {
296
+ it('accumulates token_count from every step type', () => {
297
+ const tc = makeTc();
298
+ tc._scrubber = { scrub: (s) => s };
299
+ const ctx = makeCtx();
300
+ ctx.totalTokens = 0;
301
+ ctx.stepCount = 0;
302
+ ctx.allSteps = [];
303
+ ctx.builder = { addStep: () => null };
304
+ ctx.classifier = {
305
+ onStep: (s) => s,
306
+ };
307
+
308
+ tc._processStep('agent-1', ctx, { type: 'thought', content: 'thinking about it', token_count: 50 });
309
+ tc._processStep('agent-1', ctx, { type: 'action', content: 'run test', token_count: 30 });
310
+ tc._processStep('agent-1', ctx, { type: 'observation', content: 'test passed', token_count: 100 });
311
+ tc._processStep('agent-1', ctx, { type: 'thought', content: 'next step', token_count: 20 });
312
+
313
+ assert.equal(ctx.totalTokens, 200);
314
+ assert.equal(ctx.stepCount, 4);
315
+ });
316
+
317
+ it('estimates token_count when not provided', () => {
318
+ const tc = makeTc();
319
+ tc._scrubber = { scrub: (s) => s };
320
+ const ctx = makeCtx();
321
+ ctx.totalTokens = 0;
322
+ ctx.stepCount = 0;
323
+ ctx.allSteps = [];
324
+ ctx.builder = { addStep: () => null };
325
+ ctx.classifier = {
326
+ onStep: (s) => s,
327
+ };
328
+
329
+ tc._processStep('agent-1', ctx, { type: 'thought', content: 'a'.repeat(100) });
330
+ assert.equal(ctx.totalTokens, 25);
331
+ });
332
+
333
+ it('does not double-count tokens from onStdoutLine', () => {
334
+ const tc = makeTc();
335
+ tc._scrubber = { scrub: (s) => s };
336
+ tc._enabled = true;
337
+
338
+ const ctx = makeCtx();
339
+ ctx.totalTokens = 0;
340
+ ctx.stepCount = 0;
341
+ ctx.allSteps = [];
342
+ ctx.builder = { addStep: () => null };
343
+ ctx.classifier = {
344
+ onStep: (s) => s,
345
+ };
346
+ ctx.parser = {
347
+ parseEvent: () => ({ type: 'thought', content: 'hello', token_count: 10 }),
348
+ extractModel: () => null,
349
+ };
350
+ tc._contexts.set('agent-x', ctx);
351
+
352
+ tc.onStdoutLine('agent-x', '{"type":"assistant"}');
353
+ assert.equal(ctx.totalTokens, 10);
354
+ });
355
+ });
356
+
357
+ describe('TrajectoryCapture — TIER_A with recovered errors', () => {
358
+ it('TIER_A when all errors are recovered', () => {
359
+ const tc = makeTc();
360
+ const ctx = makeCtx({ quality: 80, errorsEncountered: 2, errorsRecovered: 2 });
361
+ const result = tc._computeQualityTier(ctx, 'SUCCESS', 0);
362
+ assert.equal(result.tier, 'TIER_A');
363
+ assert.equal(result.reason, 'high_quality_errors_recovered');
364
+ });
365
+
366
+ it('TIER_A when errors recovered exceed errors encountered', () => {
367
+ const tc = makeTc();
368
+ const ctx = makeCtx({ quality: 75, errorsEncountered: 1, errorsRecovered: 2 });
369
+ const result = tc._computeQualityTier(ctx, 'SUCCESS', 0);
370
+ assert.equal(result.tier, 'TIER_A');
371
+ assert.equal(result.reason, 'high_quality_errors_recovered');
372
+ });
373
+
374
+ it('not TIER_A when errors exceed recoveries', () => {
375
+ const tc = makeTc();
376
+ const ctx = makeCtx({ quality: 80, errorsEncountered: 3, errorsRecovered: 1 });
377
+ const result = tc._computeQualityTier(ctx, 'SUCCESS', 0);
378
+ assert.notEqual(result.tier, 'TIER_A');
379
+ });
380
+
381
+ it('TIER_A with zero errors still uses original reason', () => {
382
+ const tc = makeTc();
383
+ const ctx = makeCtx({ quality: 80, errorsEncountered: 0 });
384
+ const result = tc._computeQualityTier(ctx, 'SUCCESS', 0);
385
+ assert.equal(result.tier, 'TIER_A');
386
+ assert.equal(result.reason, 'high_quality_no_errors');
387
+ });
388
+ });
389
+
390
+ describe('TrajectoryCapture — _computeQuality', () => {
391
+ it('base score is 50', () => {
392
+ const tc = makeTc();
393
+ const ctx = makeCtx({ allSteps: [] });
394
+ ctx.coordinationEvents = 0;
395
+ ctx.errorsRecovered = 0;
396
+ ctx.stepCount = 0;
397
+ const quality = tc._computeQuality(ctx);
398
+ assert.equal(quality, 50);
399
+ });
400
+
401
+ it('correction adds 10 points', () => {
402
+ const tc = makeTc();
403
+ const ctx = makeCtx({
404
+ allSteps: [{ step: 1, type: 'correction', content: 'fix' }],
405
+ });
406
+ ctx.coordinationEvents = 0;
407
+ ctx.errorsRecovered = 0;
408
+ ctx.stepCount = 1;
409
+ const quality = tc._computeQuality(ctx);
410
+ assert.ok(quality >= 60);
411
+ });
412
+
413
+ it('coordination adds 10 points', () => {
414
+ const tc = makeTc();
415
+ const ctx = makeCtx({ allSteps: [] });
416
+ ctx.coordinationEvents = 1;
417
+ ctx.errorsRecovered = 0;
418
+ ctx.stepCount = 0;
419
+ const quality = tc._computeQuality(ctx);
420
+ assert.equal(quality, 60);
421
+ });
422
+
423
+ it('caps at 100', () => {
424
+ const tc = makeTc();
425
+ const ctx = makeCtx({
426
+ allSteps: [
427
+ { step: 1, type: 'correction' },
428
+ { step: 2, type: 'thought' },
429
+ { step: 3, type: 'action' },
430
+ { step: 4, type: 'observation' },
431
+ ...Array.from({ length: 16 }, (_, i) => ({ step: i + 5, type: 'thought' })),
432
+ ],
433
+ });
434
+ ctx.coordinationEvents = 5;
435
+ ctx.errorsRecovered = 3;
436
+ ctx.stepCount = 20;
437
+ const quality = tc._computeQuality(ctx);
438
+ assert.equal(quality, 100);
439
+ });
440
+ });
@@ -225,6 +225,100 @@ describe('EnvelopeVerifier', () => {
225
225
  assert.equal(result.valid, false);
226
226
  assert.ok(result.reason.includes('HMAC'));
227
227
  });
228
+
229
+ // --- verifyFeedback ---
230
+
231
+ it('verifyFeedback accepts valid USER_FEEDBACK envelope', () => {
232
+ const envelope = {
233
+ envelope_id: 'env_fb_1',
234
+ session_id: sessionId,
235
+ type: 'USER_FEEDBACK',
236
+ feedback: {
237
+ signal: 'accepted',
238
+ timestamp: Date.now() / 1000,
239
+ context: 'completed with no interventions',
240
+ target_step: 10,
241
+ revision_rounds: 0,
242
+ delta_summary: null,
243
+ },
244
+ };
245
+
246
+ const forHmac = { ...envelope };
247
+ const envelopeBytes = JSON.stringify(forHmac);
248
+ const hmac = signEnvelope(sharedSecret, envelopeBytes, 0);
249
+ envelope.attestation = { session_hmac: hmac, sequence: 0, app_version_hash: VALID_APP_HASH };
250
+
251
+ const result = verifier.verifyFeedback(envelope);
252
+ assert.equal(result.valid, true);
253
+ });
254
+
255
+ it('verifyFeedback rejects unknown session_id', () => {
256
+ const envelope = {
257
+ envelope_id: 'env_fb_2',
258
+ session_id: 'sess_nonexistent',
259
+ type: 'USER_FEEDBACK',
260
+ feedback: { signal: 'accepted', timestamp: Date.now() / 1000 },
261
+ attestation: { session_hmac: 'a'.repeat(64), sequence: 0, app_version_hash: VALID_APP_HASH },
262
+ };
263
+ const result = verifier.verifyFeedback(envelope);
264
+ assert.equal(result.valid, false);
265
+ assert.ok(result.reason.includes('unknown session_id'));
266
+ });
267
+
268
+ it('verifyFeedback rejects missing attestation', () => {
269
+ const envelope = {
270
+ envelope_id: 'env_fb_3',
271
+ session_id: sessionId,
272
+ type: 'USER_FEEDBACK',
273
+ feedback: { signal: 'accepted', timestamp: Date.now() / 1000 },
274
+ };
275
+ const result = verifier.verifyFeedback(envelope);
276
+ assert.equal(result.valid, false);
277
+ assert.ok(result.reason.includes('attestation'));
278
+ });
279
+
280
+ it('verifyFeedback rejects tampered HMAC', () => {
281
+ const envelope = {
282
+ envelope_id: 'env_fb_4',
283
+ session_id: sessionId,
284
+ type: 'USER_FEEDBACK',
285
+ feedback: { signal: 'accepted', timestamp: Date.now() / 1000 },
286
+ attestation: { session_hmac: 'f'.repeat(64), sequence: 0, app_version_hash: VALID_APP_HASH },
287
+ };
288
+ const result = verifier.verifyFeedback(envelope);
289
+ assert.equal(result.valid, false);
290
+ assert.ok(result.reason.includes('HMAC'));
291
+ });
292
+
293
+ it('verifyFeedback rejects invalid signal via schema', () => {
294
+ const envelope = {
295
+ envelope_id: 'env_fb_5',
296
+ session_id: sessionId,
297
+ type: 'USER_FEEDBACK',
298
+ feedback: { signal: 'thumbs_up', timestamp: Date.now() / 1000 },
299
+ };
300
+
301
+ const forHmac = { ...envelope };
302
+ const envelopeBytes = JSON.stringify(forHmac);
303
+ const hmac = signEnvelope(sharedSecret, envelopeBytes, 0);
304
+ envelope.attestation = { session_hmac: hmac, sequence: 0, app_version_hash: VALID_APP_HASH };
305
+
306
+ const result = verifier.verifyFeedback(envelope);
307
+ assert.equal(result.valid, false);
308
+ assert.ok(result.reason.includes('schema'));
309
+ });
310
+
311
+ it('verifyFeedback rejects missing session_id', () => {
312
+ const envelope = {
313
+ envelope_id: 'env_fb_6',
314
+ type: 'USER_FEEDBACK',
315
+ feedback: { signal: 'accepted', timestamp: Date.now() / 1000 },
316
+ attestation: { session_hmac: 'a'.repeat(64), sequence: 0, app_version_hash: VALID_APP_HASH },
317
+ };
318
+ const result = verifier.verifyFeedback(envelope);
319
+ assert.equal(result.valid, false);
320
+ assert.ok(result.reason.includes('session_id'));
321
+ });
228
322
  });
229
323
 
230
324
  function verifyClose(verifier, envelope) {