pediatric-ai-scribe-v3/shared/clinical/ros-pe-dx.test.ts
Daniel 10dbabf83b feat(notes): structured ROS + PE + ICD-10 diagnosis pickers (WellVisit, SickVisit)
Ports the vanilla ROS / Physical Exam / ICD-10 diagnosis cards from
public/js/shadess.js (@be14578) — renderRosRows, wireRosContainer,
renderDxComponent — which the earlier React port had collapsed into
plain-text textareas.

Clinical data → shared/clinical/ros-pe-dx.ts:
  • ROS_SYSTEMS (15 systems, each with label + detail hint)
  • PE_SYSTEMS (17 systems)
  • COMMON_DX (28 pediatric quick-pick ICD-10 codes)
  • formatRosForAI / formatDxForAI — byte-identical to vanilla so
    the server-side prompt context is unchanged.
  Tests: ros-pe-dx.test.ts asserts the table counts, ordering, and
  format strings — catches the class of bug where an LLM silently
  drops an entry or re-orders the clinical reference during a port.

New React components:
  client/src/components/RosPeTable.tsx — tri-state row (WNL/Abnormal/
    Not reviewed) with auto-revealed note field on Abnormal.
    rosAllWnl / rosClear helpers mirror the "All WNL" / "Clear"
    buttons from vanilla. Accepts a btnLabels prop so the same
    component renders ROS ("WNL"/"Not reviewed") and PE ("Normal"/
    "Not examined").

  client/src/components/DxPicker.tsx — ICD-10 live search via NLM
    Clinical Tables API (free, no auth, CORS-OK) with 280ms debounce
    + AbortController; chip-style selected tags with × remove;
    28-entry common-diagnosis quick-pick grid. Enter key adds the
    top result.

Wired into WellVisit / VisitNote.tsx and SickVisit.tsx:
  • Submit now sends ros / physicalExam / diagnoses as formatted
    strings to /api/well-visit/note and /api/sick-visit/note.
  • State persists through the Save/Load encounter flow via
    partialData → the rosData/peData/diagnoses round-trip.
2026-04-24 05:24:09 +02:00

80 lines
3.2 KiB
TypeScript

// Tests for shared/clinical/ros-pe-dx.ts. The clinical tables here
// are load-bearing — a silent reorder or deletion changes AI output.
// These tests catch that class of regression.
import { describe, expect, it } from 'vitest';
import {
ROS_SYSTEMS,
PE_SYSTEMS,
COMMON_DX,
formatRosForAI,
formatDxForAI,
} from './ros-pe-dx';
describe('reference tables', () => {
it('ROS has the 15 systems vanilla shipped, in order', () => {
expect(ROS_SYSTEMS.length).toBe(15);
expect(ROS_SYSTEMS[0].key).toBe('constitutional');
expect(ROS_SYSTEMS[ROS_SYSTEMS.length - 1].key).toBe('developmental');
});
it('PE has the 17 systems vanilla shipped, in order', () => {
expect(PE_SYSTEMS.length).toBe(17);
expect(PE_SYSTEMS[0].key).toBe('general');
expect(PE_SYSTEMS[PE_SYSTEMS.length - 1].key).toBe('pePsych');
});
it('COMMON_DX has the 28 pediatric diagnoses vanilla shipped', () => {
expect(COMMON_DX.length).toBe(28);
// Z00.129 is the default pediatric well-visit code — must be first.
expect(COMMON_DX[0]).toEqual({ code: 'Z00.129', name: 'Routine child health exam, no abnormal findings' });
});
it('every ROS/PE entry has a non-empty label + detail string', () => {
for (const s of [...ROS_SYSTEMS, ...PE_SYSTEMS]) {
expect(s.key).toBeTruthy();
expect(s.label).toBeTruthy();
expect(s.detail).toBeTruthy();
}
});
});
describe('formatRosForAI', () => {
const TWO = [ROS_SYSTEMS[0], ROS_SYSTEMS[1]];
it('returns empty string when nothing is reviewed', () => {
expect(formatRosForAI(TWO, {}, 'ROS')).toBe('');
});
it('emits NORMAL with the domain hint for WNL systems', () => {
const out = formatRosForAI(TWO, { constitutional: { status: 'wnl' } }, 'ROS');
expect(out).toContain('Constitutional: NORMAL [domain: fever, fatigue, weight loss/gain, appetite]');
});
it('emits ABNORMAL with the user note inline', () => {
const out = formatRosForAI(TWO, { eyes: { status: 'abnormal', note: 'conjunctival injection' } }, 'ROS');
expect(out).toContain('Eyes: ABNORMAL — conjunctival injection');
});
it('calls out systems marked Not Reviewed explicitly', () => {
const out = formatRosForAI(TWO, { eyes: { status: 'notrev' } }, 'ROS');
expect(out).toContain('Eyes: Not reviewed');
});
it('omits systems with no status set', () => {
const out = formatRosForAI(TWO, { constitutional: { status: 'wnl' } }, 'ROS');
expect(out).not.toContain('Eyes');
});
});
describe('formatDxForAI', () => {
it('returns empty when no dx AND no free text', () => {
expect(formatDxForAI([], '')).toBe('');
});
it('prefixes codes when present', () => {
const out = formatDxForAI([{ code: 'J06.9', name: 'URI' }], '');
expect(out).toContain('- J06.9 URI');
});
it('skips code prefix for free-text-only diagnoses', () => {
const out = formatDxForAI([{ code: '', name: 'Parental anxiety' }], '');
expect(out).toContain('- Parental anxiety');
expect(out).not.toContain(' Parental');
});
it('appends trimmed free-text as its own bullet', () => {
const out = formatDxForAI([{ code: 'R50.9', name: 'Fever' }], ' Rule out UTI ');
expect(out).toMatch(/Fever[\s\S]*Rule out UTI/);
expect(out).not.toContain(' Rule out UTI');
});
});