feature: Explainable Alerts

This commit is contained in:
voltsrage
2026-06-25 00:25:31 +08:00
parent 279add1e45
commit 666d683d67
61 changed files with 9553 additions and 125 deletions
@@ -29,6 +29,20 @@ describe('AlertCard', () => {
expect(wrapper.text()).toContain('Sepsis Warning (SIRS — Legacy)')
})
it('showsNarrativeSummaryWhenExplanationPresent', () => {
const wrapper = mount(AlertCard, {
props: {
alert: {
...openAlert,
explanation: {
narrativeSummary: 'NEWS2 8 — respiratory rate (+3), SpO2 (+2).',
},
},
},
})
expect(wrapper.text()).toContain('NEWS2 8 — respiratory rate (+3), SpO2 (+2).')
})
it('acknowledgeButtonEmitsEvent', async () => {
const wrapper = mount(AlertCard, { props: { alert: openAlert } })
const ackButton = wrapper.findAll('button').find(b => b.text() === 'Acknowledge')
@@ -50,4 +50,41 @@ describe('AlertReasoning', () => {
expect(wrapper.text()).toContain('CustomUnknown')
expect(wrapper.text()).toContain('Something unusual happened')
})
it('showsStructuredExplanationWhenPresent', () => {
const news2Alert = {
id: 'alert-4',
alertType: 'News2Emergency',
severity: 'Critical',
details: 'NEWS2 score 8 (HIGH): RESP_RATE=3, SPO2=2',
triggeredAt: '2026-06-19T14:00:00Z',
explanation: {
narrativeSummary: 'NEWS2 8 — respiratory rate (+3), SpO2 (+2).',
scoreContributors: [
{ parameter: 'Respiratory Rate', points: 3, rawValue: '28', normalRange: '12-20' },
{ parameter: 'SpO2', points: 2, rawValue: '92%', normalRange: '96-100%' },
],
trend: {
parameter: 'Respiratory Rate',
percentChange: 37,
duration: '02:00:00',
direction: 'Increasing',
},
medicationContext: {
drugName: 'acetaminophen',
dose: '1000mg',
route: 'PO',
administeredAt: '2026-06-19T13:30:00Z',
relevanceNote: 'Antipyretic — may affect temperature trend',
},
},
}
const wrapper = mount(AlertReasoning, { props: { alert: news2Alert } })
expect(wrapper.text()).toContain('NEWS2 8 — respiratory rate (+3), SpO2 (+2).')
expect(wrapper.text()).toContain('Respiratory Rate')
expect(wrapper.text()).toContain('Correlated medication')
expect(wrapper.text()).toContain('acetaminophen')
expect(wrapper.text()).toContain('increasing 37% over 2 hours')
})
})
@@ -22,11 +22,14 @@ describe('CriticalAlertBanner', () => {
severity: 'Critical',
status: 'Open',
details: 'SOFA delta >= 2',
explanation: {
narrativeSummary: 'SOFA 6 — respiratory (+2), renal (+2).',
},
}]
const wrapper = mount(CriticalAlertBanner)
expect(wrapper.text()).toContain('Sepsis Alert (SOFA)')
expect(wrapper.text()).toContain('SOFA delta >= 2')
expect(wrapper.text()).toContain('SOFA 6 — respiratory (+2), renal (+2).')
const dismissButton = wrapper.findAll('button').find(button => button.text() === 'Dismiss')
await dismissButton.trigger('click')
@@ -0,0 +1,59 @@
import { describe, it, expect } from 'vitest'
import {
alertSummaryText,
elevatedContributors,
formatDuration,
formatMedicationContext,
formatTrendLine,
hasExplanation,
} from '@/composables/alertExplanation'
describe('alertExplanation', () => {
it('prefersNarrativeSummaryOverDetails', () => {
const alert = {
details: 'NEWS2 score 8 (HIGH): ...',
explanation: {
narrativeSummary: 'NEWS2 8 — respiratory rate (+3), SpO2 (+2).',
},
}
expect(alertSummaryText(alert)).toBe('NEWS2 8 — respiratory rate (+3), SpO2 (+2).')
})
it('fallsBackToDetailsWhenExplanationMissing', () => {
expect(alertSummaryText({ details: 'Legacy details' })).toBe('Legacy details')
expect(alertSummaryText({})).toBe('')
})
it('filtersElevatedContributors', () => {
const explanation = {
scoreContributors: [
{ parameter: 'Respiratory Rate', points: 3, rawValue: '28', normalRange: '12-20' },
{ parameter: 'Heart Rate', points: 0, rawValue: '72', normalRange: '51-90' },
],
}
expect(elevatedContributors(explanation)).toHaveLength(1)
expect(elevatedContributors(explanation)[0].parameter).toBe('Respiratory Rate')
})
it('formatsTrendAndMedicationContext', () => {
expect(formatDuration('02:00:00')).toBe('2 hours')
expect(formatTrendLine({
parameter: 'Respiratory Rate',
percentChange: 37,
duration: '02:00:00',
direction: 'Increasing',
})).toContain('Respiratory Rate')
expect(formatMedicationContext({
drugName: 'acetaminophen',
dose: '1000mg',
route: 'PO',
administeredAt: '2026-06-22T10:15:00Z',
relevanceNote: 'Antipyretic — may affect temperature trend',
})).toContain('acetaminophen')
})
it('detectsStructuredExplanation', () => {
expect(hasExplanation({ explanation: { narrativeSummary: 'x' } })).toBe(true)
expect(hasExplanation({ details: 'only details' })).toBe(false)
})
})