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.
80 lines
3.2 KiB
TypeScript
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');
|
|
});
|
|
});
|