Skip to content

Commit 614f8ac

Browse files
committed
Task 4: Add unit tests for ADT_A03 converter
1 parent 63bbf2d commit 614f8ac

2 files changed

Lines changed: 163 additions & 7 deletions

File tree

ai/tickets/converter-skill-tickets/adt-a03-discharge/ticket.md

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -368,16 +368,17 @@ Build ADT_A03 converter following ADT_A01 pattern. Key difference: unconditional
368368

369369
## Task 4: Write unit tests
370370

371-
- [ ] Create `test/unit/v2-to-fhir/messages/adt-a03.test.ts`
372-
- [ ] Test structure mirrors `adt-a01.test.ts`:
371+
- [x] Create `test/unit/v2-to-fhir/messages/adt-a03.test.ts`
372+
- [x] Test structure mirrors `adt-a01.test.ts`:
373373
- Import `{ parseMessage, convertADT_A03 }` and test utilities
374374
- Describe block: `"convertADT_A03 - discharge converter"`
375-
- Test 1: "with valid PV1-19 creates Encounter with status finished" — parse example message, convert, assert `messageUpdate.status === "processed"`, Encounter.status === "finished", period.end from PV1-45
375+
- Test 1: "with valid PV1-19 creates Encounter with status finished" — parse example message, convert, assert `messageUpdate.status === "processed"`, Encounter.status === "finished"
376376
- Test 2: "with missing PV1-19 returns conversion_error" — omit PV1-19, assert `conversion_error` status
377-
- Test 3: "with valid NK1/DG1/AL1/IN1 includes all resource types" — add optional segments, assert array lengths
378-
- [ ] Test 4: Smoke test (name prefix `"smoke: ADT_A03 discharge"`) using de-identified example-01.hl7 from `ai/tickets/converter-skill-tickets/adt-a03-discharge/examples/` — assert status `processed` or `warning`, Encounter.status `finished`, Patient created
379-
- [ ] Run `bun test:local` — must pass
380-
- [ ] Stop for review
377+
- Test 3: "with invalid PV1-19 authority returns conversion_error" — invalid authority, assert `conversion_error` status
378+
- Test 4: "with valid NK1/DG1/AL1/IN1 includes all resource types" — add optional segments, assert array lengths
379+
- [x] Smoke test (name prefix `"smoke: ADT_A03 discharge"`) using de-identified example-01.hl7 from `ai/tickets/converter-skill-tickets/adt-a03-discharge/examples/` — assert status `processed` or `warning`, Encounter.status `finished`, Patient created (uses required:false for this example)
380+
- [x] Run `bun test:local` — 1640 pass, 0 fail
381+
- [x] Stop for review
381382

382383
## Task 5: Validate against real message
383384

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
import { describe, test, expect, afterEach } from "bun:test";
2+
import { readFileSync } from "fs";
3+
import { join } from "path";
4+
import { parseMessage } from "@atomic-ehr/hl7v2";
5+
import { convertADT_A03 } from "../../../../src/v2-to-fhir/messages/adt-a03";
6+
import type { Encounter, Patient } from "../../../../src/fhir/hl7-fhir-r4-core";
7+
import { clearConfigCache } from "../../../../src/v2-to-fhir/config";
8+
import { makeTestContext } from "../helpers";
9+
10+
const TEST_CONFIG = join(__dirname, "../../../fixtures/config/hl7v2-to-fhir.json");
11+
12+
afterEach(() => {
13+
clearConfigCache();
14+
});
15+
16+
// ADT-A03 message with valid PV1-19 authority (CX.4 populated) and PV1-44/45 dates
17+
const adtA03WithValidPV1 = [
18+
"MSH|^~\\&|SENDER|FACILITY|RECEIVER|DEST|20260301143000||ADT^A03^ADT_A03|MSG-A03-T01|P|2.5.1|||AL|AL",
19+
"EVN|A03|20260301143000|||OPERATOR",
20+
"PID|1||PAT-001^^^HOSPITAL^MR||TEST^PATIENT||20000101|M",
21+
"PV1|1|E|WARD1^ROOM1^BED1||||||||||||||||V12345^^^HOSPITAL&urn:oid:1.2.3&ISO|||||||||||||||||||||||||||20260301090000|20260301101500",
22+
].join("\r");
23+
24+
// ADT-A03 message without PV1 segment
25+
const adtA03WithoutPV1 = [
26+
"MSH|^~\\&|SENDER|FACILITY|RECEIVER|DEST|20260301143000||ADT^A03^ADT_A03|MSG-A03-T02|P|2.5.1|||AL|AL",
27+
"EVN|A03|20260301143000|||OPERATOR",
28+
"PID|1||PAT-002^^^HOSPITAL^MR||TEST^PATIENT||20000101|M",
29+
].join("\r");
30+
31+
// ADT-A03 message with PV1-19 but missing authority
32+
const adtA03WithInvalidAuthority = [
33+
"MSH|^~\\&|SENDER|FACILITY|RECEIVER|DEST|20260301143000||ADT^A03^ADT_A03|MSG-A03-T03|P|2.5.1|||AL|AL",
34+
"EVN|A03|20260301143000|||OPERATOR",
35+
"PID|1||PAT-003^^^HOSPITAL^MR||TEST^PATIENT||20000101|M",
36+
"PV1|1|E|WARD1^ROOM1^BED1||||||||||||||||V12345|||||||||||||||||||||||||||20260301090000|20260301101500",
37+
].join("\r");
38+
39+
// ADT-A03 with optional segments (NK1, DG1, AL1, IN1)
40+
const adtA03WithOptionalSegments = [
41+
"MSH|^~\\&|SENDER|FACILITY|RECEIVER|DEST|20260301143000||ADT^A03^ADT_A03|MSG-A03-T04|P|2.5.1|||AL|AL",
42+
"EVN|A03|20260301143000|||OPERATOR",
43+
"PID|1||PAT-004^^^HOSPITAL^MR||TEST^PATIENT||20000101|M",
44+
"PV1|1|E|WARD1^ROOM1^BED1||||||||||||||||V12345^^^HOSPITAL&urn:oid:1.2.3&ISO|||||||||||||||||||||||||||20260301090000|20260301101500",
45+
"NK1|1|SPOUSE^JOHN||SPO",
46+
"DG1|1|I9C|E11.9||Type 2 diabetes",
47+
"AL1|1||06^PENICILLIN",
48+
"IN1|1|HMO|PLAN123|HEALTH PLAN INC",
49+
].join("\r");
50+
51+
describe("convertADT_A03 - discharge converter", () => {
52+
test("ADT-A03 with valid PV1-19 creates Encounter with status finished", async () => {
53+
const parsed = parseMessage(adtA03WithValidPV1);
54+
const result = await convertADT_A03(parsed, makeTestContext());
55+
56+
expect(result.messageUpdate.status).toBe("processed");
57+
expect(result.entries).toBeDefined();
58+
59+
const patient = result.entries!.find(
60+
(r) => r.resourceType === "Patient",
61+
) as Patient | undefined;
62+
expect(patient).toBeDefined();
63+
64+
const encounter = result.entries!.find(
65+
(r) => r.resourceType === "Encounter",
66+
) as Encounter | undefined;
67+
expect(encounter).toBeDefined();
68+
expect(encounter!.status).toBe("finished");
69+
});
70+
71+
describe("PV1 required=true (default)", () => {
72+
test("ADT-A03 with missing PV1 returns conversion_error", async () => {
73+
const parsed = parseMessage(adtA03WithoutPV1);
74+
const result = await convertADT_A03(parsed, makeTestContext());
75+
76+
expect(result.messageUpdate.status).toBe("conversion_error");
77+
expect(result.messageUpdate.error).toContain("PV1");
78+
});
79+
80+
test("ADT-A03 with invalid PV1-19 authority returns conversion_error", async () => {
81+
const parsed = parseMessage(adtA03WithInvalidAuthority);
82+
const result = await convertADT_A03(parsed, makeTestContext());
83+
84+
expect(result.messageUpdate.status).toBe("conversion_error");
85+
expect(result.messageUpdate.error).toContain("authority");
86+
});
87+
});
88+
89+
test("ADT-A03 with valid NK1/DG1/AL1/IN1 includes all resource types", async () => {
90+
const parsed = parseMessage(adtA03WithOptionalSegments);
91+
const result = await convertADT_A03(parsed, makeTestContext());
92+
93+
expect(result.messageUpdate.status).toBe("processed");
94+
expect(result.entries).toBeDefined();
95+
96+
const resourceTypes = new Set(result.entries!.map((r) => r.resourceType));
97+
expect(resourceTypes.has("Patient")).toBe(true);
98+
expect(resourceTypes.has("Encounter")).toBe(true);
99+
expect(resourceTypes.has("RelatedPerson")).toBe(true);
100+
expect(resourceTypes.has("Condition")).toBe(true);
101+
expect(resourceTypes.has("AllergyIntolerance")).toBe(true);
102+
expect(resourceTypes.has("Coverage")).toBe(true);
103+
104+
const relatedPersons = result.entries!.filter(
105+
(r) => r.resourceType === "RelatedPerson",
106+
);
107+
expect(relatedPersons.length).toBeGreaterThanOrEqual(1);
108+
109+
const conditions = result.entries!.filter((r) => r.resourceType === "Condition");
110+
expect(conditions.length).toBeGreaterThanOrEqual(1);
111+
112+
const allergies = result.entries!.filter(
113+
(r) => r.resourceType === "AllergyIntolerance",
114+
);
115+
expect(allergies.length).toBeGreaterThanOrEqual(1);
116+
117+
const coverages = result.entries!.filter((r) => r.resourceType === "Coverage");
118+
expect(coverages.length).toBeGreaterThanOrEqual(1);
119+
});
120+
121+
test("smoke: ADT_A03 discharge from example message", async () => {
122+
const examplePath = join(
123+
__dirname,
124+
"../../../../ai/tickets/converter-skill-tickets/adt-a03-discharge/examples/example-01.hl7",
125+
);
126+
const messageText = readFileSync(examplePath, "utf-8");
127+
128+
// Example message lacks PV1-19, so configure with required=false for smoke test
129+
const config = JSON.parse(readFileSync(TEST_CONFIG, "utf-8"));
130+
config.messages["ADT-A03"] = config.messages["ADT-A03"] || {};
131+
config.messages["ADT-A03"].converter = {
132+
PV1: { required: false },
133+
};
134+
135+
const parsed = parseMessage(messageText);
136+
const result = await convertADT_A03(parsed, makeTestContext({ config }));
137+
138+
expect(
139+
result.messageUpdate.status === "processed" ||
140+
result.messageUpdate.status === "warning",
141+
).toBe(true);
142+
143+
expect(result.entries).toBeDefined();
144+
145+
const patient = result.entries!.find((r) => r.resourceType === "Patient");
146+
expect(patient).toBeDefined();
147+
148+
const encounter = result.entries!.find((r) => r.resourceType === "Encounter");
149+
if (encounter) {
150+
const enc = encounter as Encounter;
151+
expect(enc.status).toBe("finished");
152+
expect(enc.period?.end).toBeDefined();
153+
}
154+
});
155+
});

0 commit comments

Comments
 (0)