feature: Sepsis Engine Refactor: Remove SIRS, Rewire qSOFA + Bundle

Frontend: GCS Entry, SOFA Display, Sepsis UI Refactor
This commit is contained in:
voltsrage
2026-06-21 03:56:27 +08:00
parent 93ea473d2b
commit bf46e6554a
48 changed files with 2686 additions and 714 deletions
@@ -26,7 +26,7 @@ describe('AlertCard', () => {
it('showsAlertTypeAndSeverity', () => {
const wrapper = mount(AlertCard, { props: { alert: openAlert } })
expect(wrapper.text()).toContain('Critical')
expect(wrapper.text()).toContain('SEPSIS_WARNING')
expect(wrapper.text()).toContain('Sepsis Warning (SIRS — Legacy)')
})
it('acknowledgeButtonEmitsEvent', async () => {
@@ -0,0 +1,28 @@
import { describe, it, expect } from 'vitest'
import { alertTypeLabel, bundleTriggerLabel } from '@/api/normalize'
describe('AlertLabels', () => {
it('sofaSepsisLabel', () => {
expect(alertTypeLabel('SofaSepsis')).toBe('Sepsis Alert (SOFA)')
})
it('legacySepsisLabel', () => {
expect(alertTypeLabel('SepsisWarning')).toBe('Sepsis Warning (SIRS — Legacy)')
})
it('qsofaScreenLabel', () => {
expect(alertTypeLabel('QsofaScreen')).toBe('qSOFA Screen')
})
it('gcsCriticalLabel', () => {
expect(alertTypeLabel('GcsCritical')).toBe('GCS Critical (≤ 8)')
})
it('bundleTriggerSofa', () => {
expect(bundleTriggerLabel('SOFA_SEPSIS')).toBe('SOFA delta ≥ 2')
})
it('bundleTriggerLegacy', () => {
expect(bundleTriggerLabel('SEPSIS_WARNING')).toBe('SIRS criteria (Legacy)')
})
})
@@ -11,11 +11,11 @@ const sepsisAlert = {
triggeredAt: '2026-06-19T12:00:00Z',
}
const qsofaAlert = {
const qsofaScreenAlert = {
id: 'alert-2',
alertType: 'QsofaWarning',
alertType: 'QsofaScreen',
severity: 'Warning',
details: 'qSOFA score elevated',
details: 'qSOFA screen positive',
triggeredAt: '2026-06-19T12:30:00Z',
}
@@ -24,16 +24,18 @@ describe('AlertReasoning', () => {
setActivePinia(createPinia())
})
it('showsExplanationForSepsisWarning', () => {
it('showsExplanationForLegacySepsisWarning', () => {
const wrapper = mount(AlertReasoning, { props: { alert: sepsisAlert } })
expect(wrapper.text()).toContain('SIRS / Sepsis Alert')
expect(wrapper.text()).toContain('≥2 of 4 SIRS criteria met')
expect(wrapper.text()).toContain('SIRS / Sepsis Alert (Legacy)')
expect(wrapper.text()).toContain('Historical alert')
expect(wrapper.text()).toContain('SOFA delta')
})
it('showsExplanationForQsofa', () => {
const wrapper = mount(AlertReasoning, { props: { alert: qsofaAlert } })
expect(wrapper.text()).toContain('qSOFA Alert')
expect(wrapper.text()).toContain('≥2 of 3 qSOFA criteria met')
it('showsExplanationForQsofaScreen', () => {
const wrapper = mount(AlertReasoning, { props: { alert: qsofaScreenAlert } })
expect(wrapper.text()).toContain('qSOFA Screen')
expect(wrapper.text()).toContain('Bedside screen positive')
expect(wrapper.text()).toContain('Recommend ordering SOFA labs (PaO₂')
})
it('showsRawDetailsForUnknownType', () => {
@@ -48,4 +50,4 @@ describe('AlertReasoning', () => {
expect(wrapper.text()).toContain('CustomUnknown')
expect(wrapper.text()).toContain('Something unusual happened')
})
})
})
@@ -28,8 +28,8 @@ describe('FeedbackSummary', () => {
it('showsPerAlertTypeBreakdown', () => {
const wrapper = mount(FeedbackSummary)
expect(wrapper.text()).toContain('SEPSIS_WARNING')
expect(wrapper.text()).toContain('WARNING_HEART_RATE')
expect(wrapper.text()).toContain('Sepsis Warning (SIRS — Legacy)')
expect(wrapper.text()).toContain('Warning Heart Rate')
expect(wrapper.text()).toContain('2 ratings')
expect(wrapper.text()).toContain('1 ratings')
})
@@ -0,0 +1,63 @@
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import GcsEntryForm from '@/components/patient/GcsEntryForm.vue'
describe('GcsEntryForm', () => {
it('rendersThreeDropdowns', () => {
const wrapper = mount(GcsEntryForm)
expect(wrapper.findAll('select')).toHaveLength(3)
expect(wrapper.text()).toContain('Eye (E)')
expect(wrapper.text()).toContain('Verbal (V)')
expect(wrapper.text()).toContain('Motor (M)')
})
it('computesTotalCorrectly', async () => {
const wrapper = mount(GcsEntryForm)
await wrapper.findAll('select')[0].setValue(2)
await wrapper.findAll('select')[1].setValue(3)
await wrapper.findAll('select')[2].setValue(4)
expect(wrapper.text()).toContain('GCS 9/15')
})
it('showsClassification', async () => {
const wrapper = mount(GcsEntryForm)
await wrapper.findAll('select')[0].setValue(2)
await wrapper.findAll('select')[1].setValue(3)
await wrapper.findAll('select')[2].setValue(3)
expect(wrapper.text()).toContain('Severe')
await wrapper.findAll('select')[0].setValue(4)
await wrapper.findAll('select')[1].setValue(4)
await wrapper.findAll('select')[2].setValue(4)
expect(wrapper.text()).toContain('Moderate')
await wrapper.findAll('select')[0].setValue(4)
await wrapper.findAll('select')[1].setValue(5)
await wrapper.findAll('select')[2].setValue(6)
expect(wrapper.text()).toContain('Mild')
})
it('emitsSubmitWithComponents', async () => {
const wrapper = mount(GcsEntryForm)
await wrapper.findAll('select')[0].setValue(3)
await wrapper.findAll('select')[1].setValue(4)
await wrapper.findAll('select')[2].setValue(5)
await wrapper.find('button').trigger('click')
expect(wrapper.emitted('submit')?.[0]?.[0]).toEqual({ eye: 3, verbal: 4, motor: 5 })
})
it('defaultsToNormalValues', () => {
const wrapper = mount(GcsEntryForm)
expect(wrapper.text()).toContain('GCS 15/15')
expect(wrapper.text()).toContain('E4 V5 M6')
})
it('usesResponsiveLayout', () => {
const wrapper = mount(GcsEntryForm)
const grid = wrapper.find('.grid')
expect(grid.classes()).toContain('grid-cols-1')
expect(grid.classes()).toContain('sm:grid-cols-3')
expect(wrapper.find('button').classes()).toContain('w-full')
expect(wrapper.find('button').classes()).toContain('sm:w-auto')
})
})
@@ -0,0 +1,49 @@
import { describe, it, expect, beforeEach } from 'vitest'
import { mount } from '@vue/test-utils'
import { createPinia, setActivePinia } from 'pinia'
import ScoresPanel from '@/components/patient/ScoresPanel.vue'
import { useScoringStore } from '@/stores/scoring'
describe('ScoresPanel', () => {
beforeEach(() => {
setActivePinia(createPinia())
const store = useScoringStore()
store.$patch({
news2: { totalScore: 5, riskLevel: 'Medium' },
gcs: { eyeScore: 4, verbalScore: 5, motorScore: 6, totalScore: 15 },
sofa: { totalScore: 4, deltaFromBaseline: 0, respiratoryScore: 1, coagulationScore: 0, liverScore: 0, cardiovascularScore: 1, cnsScore: 1, renalScore: 1 },
qsofa: { activeCriteria: 2 },
})
})
it('showsGcsSection', () => {
const wrapper = mount(ScoresPanel)
expect(wrapper.text()).toContain('GCS')
expect(wrapper.text()).toContain('15/15')
expect(wrapper.text()).toContain('E4 V5 M6')
})
it('showsSofaSection', () => {
const wrapper = mount(ScoresPanel)
expect(wrapper.text()).toContain('SOFA')
expect(wrapper.text()).toContain('4/24')
})
it('noSirsReference', () => {
const wrapper = mount(ScoresPanel)
expect(wrapper.text().toUpperCase()).not.toContain('SIRS')
})
it('qsofaLabeledAsScreen', () => {
const wrapper = mount(ScoresPanel)
expect(wrapper.text()).toContain('qSOFA Screen')
expect(wrapper.text()).toContain('Bedside screening')
})
it('sofaOrganBadgesUseResponsiveGrid', () => {
const wrapper = mount(ScoresPanel)
const grid = wrapper.find('.grid.grid-cols-2')
expect(grid.exists()).toBe(true)
expect(grid.classes()).toContain('sm:flex')
})
})
@@ -0,0 +1,110 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { mount } from '@vue/test-utils'
import SofaScorePanel from '@/components/patient/SofaScorePanel.vue'
const mockFetch = vi.fn()
const mockState = {
total: null,
delta: null,
organs: [],
hasStaleData: false,
staleness: null,
loading: false,
fetch: mockFetch,
}
vi.mock('@/composables/useSofa', () => ({
useSofa: () => mockState,
}))
describe('SofaScorePanel', () => {
beforeEach(() => {
mockState.total = null
mockState.delta = null
mockState.organs = []
mockState.hasStaleData = false
mockState.staleness = null
mockState.loading = false
mockFetch.mockClear()
})
it('showsLabsPending_WhenNoScore', () => {
const wrapper = mount(SofaScorePanel, {
props: { encounterId: 'enc-1' },
})
expect(wrapper.text()).toContain('Labs pending')
})
it('showsOrganBreakdown', () => {
mockState.total = 8
mockState.organs = [
{ name: 'Respiratory', score: 2, max: 4 },
{ name: 'Coagulation', score: 1, max: 4 },
{ name: 'Liver', score: 0, max: 4 },
{ name: 'Cardiovascular', score: 3, max: 4 },
{ name: 'CNS', score: 1, max: 4 },
{ name: 'Renal', score: 1, max: 4 },
]
const wrapper = mount(SofaScorePanel, {
props: { encounterId: 'enc-1' },
})
expect(wrapper.text()).toContain('Respiratory')
expect(wrapper.text()).toContain('Renal')
expect(wrapper.text()).toContain('8/24')
})
it('showsDeltaBadge_WhenDeltaGe2', () => {
mockState.total = 10
mockState.delta = 2
mockState.organs = [{ name: 'CNS', score: 2, max: 4 }]
const wrapper = mount(SofaScorePanel, {
props: { encounterId: 'enc-1' },
})
expect(wrapper.text()).toContain('+2 from baseline')
})
it('showsStalenessWarning', () => {
mockState.total = 6
mockState.hasStaleData = true
mockState.staleness = {
staleComponents: ['Platelets'],
missingComponents: ['PaO2'],
usedSpO2Fallback: true,
}
mockState.organs = [{ name: 'Coagulation', score: 1, max: 4 }]
const wrapper = mount(SofaScorePanel, {
props: { encounterId: 'enc-1' },
})
expect(wrapper.text()).toContain('Missing: PaO2')
expect(wrapper.text()).toContain('Stale: Platelets')
expect(wrapper.text()).toContain('SpO₂/FiO₂ proxy')
})
it('organScoreColors', () => {
mockState.total = 3
mockState.organs = [
{ name: 'Liver', score: 0, max: 4 },
{ name: 'CNS', score: 2, max: 4 },
{ name: 'Renal', score: 4, max: 4 },
]
const wrapper = mount(SofaScorePanel, {
props: { encounterId: 'enc-1' },
})
const badges = wrapper.findAll('span')
expect(badges.some(b => b.text().includes('0/4'))).toBe(true)
expect(badges.some(b => b.text().includes('2/4'))).toBe(true)
expect(badges.some(b => b.text().includes('4/4'))).toBe(true)
})
it('usesResponsiveOrganGrid', () => {
mockState.total = 4
mockState.organs = [{ name: 'CNS', score: 1, max: 4 }]
const wrapper = mount(SofaScorePanel, {
props: { encounterId: 'enc-1' },
})
const grid = wrapper.find('.grid')
expect(grid.classes()).toContain('grid-cols-2')
expect(grid.classes()).toContain('sm:grid-cols-3')
expect(grid.classes()).toContain('lg:grid-cols-6')
})
})