pediatric-ai-scribe-v3/shared/clinical/correction-tracker.test.ts
Daniel e6d0e5ef8c feat(notes): editable AI output + automatic correction tracking
Restores two pieces of vanilla behavior the React port silently dropped:

1. The output divs were contenteditable in vanilla — clinicians fix
   AI mistakes inline before saving the encounter or copying out.
   The early React port rendered the output as a read-only div, so
   any edit forced a copy-paste workflow.

2. correctionTracker.js (public/js/correctionTracker.js) saved every
   meaningful inline edit to user_memories under category
   correction_<section> so the AI learns user preferences. Settings
   → Corrections still showed them, but nothing in React was *writing*
   them — the loop was broken.

New EditableResult component wraps the result body in a textarea,
captures the AI baseline on first render / after refine|shorten, and
on blur posts to /api/memories/correction (only when the edit clears
the noise threshold from vanilla — wordDiff ≥ 2 OR charDiff ≥ 20 on
outputs > 100 chars).

Wired into all 7 note pages:
  Encounter   → section: 'encounter'
  Dictation   → section: 'hpi'
  SOAP        → section: 'soap'
  SickVisit   → section: 'sickvisit'
  WellVisit   → section: 'wellvisit'
  HospitalCourse → section: null   (server enum has no correction_hospital)
  ChartReview    → section: null   (server enum has no correction_chart)

Tests:
  shared/clinical/correction-tracker.ts (+ .test.ts) — pure heuristic
  with vectors covering the noise floor (single-word swap on long
  text → skipped; new sentence → tracked; large char diff → tracked).
  Lives in shared/ so the root vitest config picks it up; the React
  component imports from there.
2026-04-24 05:24:09 +02:00

50 lines
2.3 KiB
TypeScript

// Tests for the correction-tracking heuristic — should not flood the
// memories table with whitespace-only or single-word fixes on long
// outputs, but must capture all real edits.
import { describe, expect, it } from 'vitest';
import { isMeaningfulChange } from './correction-tracker';
describe('isMeaningfulChange', () => {
it('returns false for empty strings', () => {
expect(isMeaningfulChange('', 'anything')).toBe(false);
expect(isMeaningfulChange('anything', '')).toBe(false);
});
it('returns false when the trimmed text is identical', () => {
expect(isMeaningfulChange('hello', 'hello')).toBe(false);
expect(isMeaningfulChange('hello\n', ' hello ')).toBe(false);
});
it('returns true for short edits even with small word/char diffs', () => {
// The 100-char threshold guards against noise on LONG outputs only.
// Short edits always count.
expect(isMeaningfulChange('hi', 'hi.')).toBe(true);
expect(isMeaningfulChange('hi', 'hello')).toBe(true);
});
it('rejects single-word, ≤20-char edits on long original output', () => {
const long = 'Patient is a 5-year-old male presenting with fever for two days. ' +
'No respiratory symptoms. No GI symptoms. Vitals stable. Exam unremarkable. ' +
'Plan: supportive care, return precautions reviewed.';
// Same length, swap one word — wordDiff=0, charDiff=0 → noise, skip.
expect(isMeaningfulChange(long, long.replace('male', 'mail'))).toBe(false);
// Same length, swap one word for similar length — still noise, skip.
expect(isMeaningfulChange(long, long.replace('two days', 'one day '))).toBe(false);
});
it('accepts multi-word edits even on long output', () => {
const long = 'Patient is a 5-year-old male presenting with fever for two days. ' +
'No respiratory symptoms. No GI symptoms. Vitals stable. Exam unremarkable. ' +
'Plan: supportive care, return precautions reviewed.';
// Inserting a sentence is a real edit — track it.
const edited = long + ' Will follow up in 48 hours if symptoms persist.';
expect(isMeaningfulChange(long, edited)).toBe(true);
});
it('accepts single-word edits when the char diff is large enough', () => {
const long = 'a'.repeat(150);
// 30-char insertion — over the 20-char noise floor.
expect(isMeaningfulChange(long, long + 'x'.repeat(30))).toBe(true);
});
});