Rebuild CCS case workflow

This commit is contained in:
hz
2026-07-23 04:43:02 -05:00
parent 489b9c3d7c
commit 8e216eac32
13 changed files with 1374 additions and 690 deletions
+50 -53
View File
@@ -1,8 +1,5 @@
extends SceneTree
const Style := preload("res://scripts/ui/ui_style.gd")
const Brand := preload("res://scripts/ui/medmind_style.gd")
var failures := 0
var ccs_service
@@ -13,8 +10,6 @@ func _initialize() -> void:
func _run() -> void:
ccs_service = root.get_node("CCSService")
var theme_service = root.get_node("UIThemeService")
var prior_dark_mode: bool = theme_service.is_dark_mode()
var prior_attempt: Dictionary = ccs_service.attempt.duplicate(true)
var prior_orders: Array = ccs_service.orders.duplicate(true)
var prior_scale: float = ccs_service.text_scale
@@ -33,58 +28,42 @@ func _run() -> void:
player._render()
await process_frame
_assert(player.heading.text == "CCS Case | Emergency Department", "CCS header should show the readable location without a case id")
_assert(player._ordered_vital_keys(ccs_service.attempt.vitals) == ["temperature", "bloodPressure", "pulse", "respirations", "oxygenSaturation"], "vital chips should use the clinical priority order")
_assert(player.vitals_flow.get_child_count() == 5, "every supplied vital should render as a chip")
_assert(player.search_button.icon != null, "Order Clerk search should use a bundled icon rather than a font glyph")
_assert(player.summary.get_meta("ccs_region", "") == "summary", "summary highlighting should use its own region")
_assert(player.history.get_meta("ccs_region", "") == "history", "history highlighting should use its own region")
_assert(player.appearance.get_meta("ccs_region", "") == "appearance", "appearance highlighting should use its own region")
_assert(player.chart.get_meta("ccs_region", "") == "chart", "chart highlighting should use its own region")
_assert(not player.workspace.vertical, "wide CCS screens should use chart and Order Clerk columns")
_assert(player.orders.get_child_count() == 1, "active orders should render in the Order Clerk")
var active_order_row := player.orders.get_child(0) as HBoxContainer
var active_order_label := active_order_row.get_child(0) as Label
_assert(active_order_label.text == "Normal Saline Bolus", "internal order codes should be human readable")
_assert(player.actions.get_child_count() == 2, "available physical examinations should render as actions")
_assert(player.locations.get_child_count() == 2, "allowed locations should render as move actions")
_assert(not player.clock.visible, "the normal active-case countdown should remain hidden")
_assert(player.heading.text == "CCS Case | Emergency Department", "header should show the readable location")
_assert(player.action_buttons.size() == 4, "CCS workspace should expose four persistent clinical tasks")
_assert(player.tabs.get_tab_count() == 7, "CCS chart should expose seven predictable tabs")
_assert(player.tabs.get_tab_title(0) == "Order Sheet", "the first chart tab should be the order sheet")
_assert(player.allotted_label.text.contains("18 minutes + 2 minutes"), "20-minute cases should reserve two final-order minutes")
_assert(player.active_orders.get_child_count() == 1, "active orders should render as structured rows")
_assert(player._ordered_vital_keys(ccs_service.attempt.vitals) == ["temperature", "bloodPressure", "pulse", "respirations", "oxygenSaturation"], "vitals should use clinical priority order")
_assert(player._chart_entries("lab").size() == 1, "categorized results should render in their assigned tab")
_assert(player._simulated_time_text(60).contains("10:00 AM"), "simulated time should derive from the case start")
_assert(not player.opening_active, "completed opening flows should not replay on resume")
_assert(not player.final_orders_notice.visible, "active cases should not show final-order instructions")
var clerk_font_size: int = player.search.get_theme_font_size("font_size")
var chart_font_size: int = player.summary.get_theme_font_size("normal_font_size")
var original_size: int = player.clinical_labels[0].get_theme_font_size("normal_font_size")
var prior_profile_api_url: String = ProjectSettings.get_setting("step3/profile_api_url", "")
ProjectSettings.set_setting("step3/profile_api_url", "http://127.0.0.1:1")
player._zoom(ccs_service.TEXT_SCALE_STEP)
ProjectSettings.set_setting("step3/profile_api_url", prior_profile_api_url)
_assert(player.summary.get_theme_font_size("normal_font_size") > chart_font_size, "text zoom should enlarge clinical chart text")
_assert(player.search.get_theme_font_size("font_size") == clerk_font_size, "text zoom should not resize Order Clerk controls")
_assert(player.clinical_labels[0].get_theme_font_size("normal_font_size") > original_size, "clinical text zoom should remain available")
_assert(player.search.get_theme_font_size("font_size") == clerk_font_size, "clinical text zoom should not resize order controls")
player._stage_order("bmp")
_assert(player.staged_order_codes == ["bmp"], "orders should stage before submission")
_assert(not player.commit_orders_button.disabled, "staged orders should enable confirmation")
player._clear_staged_orders()
_assert(player.commit_orders_button.disabled, "clearing staged orders should disable confirmation")
player.size = Vector2(800, 900)
player._update_responsive_layout()
_assert(player.workspace.vertical, "narrow CCS screens should stack the Order Clerk below the chart")
_assert(player.action_grid.columns == 2, "narrow CCS screens should use a two-column action workspace")
ccs_service.attempt.state = "final_orders"
ccs_service.attempt.finalOrdersDeadlineUtc = Time.get_datetime_string_from_unix_time(Time.get_unix_time_from_system() + 90.0, true)
player._render()
_assert(player.final_orders_notice.visible and player.clock.visible, "final-orders mode should show instructions and its deadline")
_assert(not player.time_row.visible, "time advancement should be unavailable during final orders")
_assert(player.final_orders_notice.visible, "final-orders mode should show instructions")
_assert(player.exit_button.text == "Submit Final Orders", "final-orders mode should expose final submission")
_assert(player.action_buttons[0].disabled and not player.action_buttons[1].disabled, "only order review should remain available in final orders")
theme_service.set_dark_mode(true)
await process_frame
theme_service.set_dark_mode(false)
Style.refresh_tree(player, true)
await process_frame
var light_panel := player.chart_panel.get_theme_stylebox("panel") as StyleBoxFlat
_assert(light_panel.bg_color.is_equal_approx(Brand.content_card_box(false, false).bg_color), "CCS light styles should win after the shell palette translation")
_assert(player.summary.get_theme_color("default_color").is_equal_approx(Brand.text_color(false)), "CCS light chart text should remain readable")
theme_service.set_dark_mode(true)
Style.refresh_tree(player, false)
await process_frame
var dark_panel := player.chart_panel.get_theme_stylebox("panel") as StyleBoxFlat
_assert(dark_panel.bg_color.is_equal_approx(Brand.content_card_box(true, false).bg_color), "CCS dark styles should win after the shell palette translation")
_assert(player.summary.get_theme_color("default_color").is_equal_approx(Brand.text_color(true)), "CCS dark chart text should remain readable")
theme_service.set_dark_mode(prior_dark_mode)
await process_frame
host.queue_free()
await process_frame
ccs_service.attempt = prior_attempt
@@ -94,14 +73,24 @@ func _run() -> void:
func _sample_attempt() -> Dictionary:
var now := Time.get_unix_time_from_system()
return {
"id": "attempt-1",
"state": "active",
"revision": 1,
"openingCompleted": true,
"location": "emergency_department",
"patientSummary": "24-year-old woman with type 1 diabetes mellitus",
"reasonForEncounter": "Two days of vomiting and progressive lethargy.",
"history": "She stopped insulin when oral intake declined.",
"historySections": [{"code": "hpi", "title": "History of present illness", "text": "She stopped insulin when oral intake declined."}],
"appearance": "She is lethargic but arousable.",
"simulatedStartUtc": "2026-07-15T09:00:00Z",
"simulatedMinute": 0,
"realTimeMinutes": 20,
"createdUtc": Time.get_datetime_string_from_unix_time(now - 60, true),
"deadlineUtc": Time.get_datetime_string_from_unix_time(now + 1020, true),
"hardDeadlineUtc": Time.get_datetime_string_from_unix_time(now + 1140, true),
"vitals": {
"oxygenSaturation": "98% on room air",
"pulse": "118/min",
@@ -109,14 +98,22 @@ func _sample_attempt() -> Dictionary:
"respirations": "28/min",
"bloodPressure": "88/56 mm Hg"
},
"chart": [{"simulatedMinute": 0, "category": "progress", "text": "Initial evaluation is in progress."}],
"activeOrderCodes": ["normal_saline_bolus"],
"availableExams": [
{"code": "general", "name": "General and hydration examination"},
{"code": "neurologic", "name": "Neurologic examination"}
"vitalHistory": [{"simulatedMinute": 0, "values": {"pulse": "118/min"}}],
"chart": [
{"simulatedMinute": 0, "category": "progress", "code": "reason", "title": "Reason for encounter", "text": "Initial evaluation."},
{"simulatedMinute": 12, "category": "lab", "code": "bmp", "title": "Basic metabolic panel", "text": "Glucose 580 mg/dL."}
],
"allowedLocations": ["emergency_department", "icu"],
"deadlineUtc": Time.get_datetime_string_from_unix_time(Time.get_unix_time_from_system() + 600.0, true)
"activeOrderCodes": ["normal_saline_bolus"],
"activeOrders": [{
"code": "normal_saline_bolus", "name": "0.9% sodium chloride intravenous bolus",
"category": "medication", "route": "IV", "frequency": "Once",
"orderedMinute": 0, "reportMinute": 0
}],
"availableExams": [
{"code": "general", "name": "General and hydration examination", "durationMinutes": 1},
{"code": "neurologic", "name": "Neurologic examination", "durationMinutes": 1}
],
"allowedLocations": ["emergency_department", "icu"]
}
+28 -3
View File
@@ -161,15 +161,40 @@ test("public API and disposable learner journey", async () => {
expect(Array.isArray(cases)).toBeTruthy();
expect(cases.length).toBeGreaterThan(0);
const { data: attempt } = await call("/api/ccs/attempts", {
method: "POST", token, body: { caseExternalId: "", mode: "practice" }
method: "POST", token, body: { caseExternalId: "", mode: "timed" }
});
activeCcsAttempt = attempt.id;
expect(activeCcsAttempt).toBeTruthy();
expect(attempt.caseExternalId).toBeTruthy();
expect(attempt.state).toBe("active");
await call(`/api/ccs/attempts/${activeCcsAttempt}/abandon`, {
method: "POST", token, expected: [204]
const { data: opened } = await call(`/api/ccs/attempts/${activeCcsAttempt}/commands`, {
method: "POST", token, body: {
commandId: `smoke-opening-${suffix}`, expectedRevision: attempt.revision,
type: "acknowledge_opening", code: "", minutes: 0
}
});
expect(opened.openingCompleted).toBe(true);
const exam = opened.availableExams[0];
const { data: examined } = await call(`/api/ccs/attempts/${activeCcsAttempt}/commands/batch`, {
method: "POST", token, body: {
commandId: `smoke-exam-${suffix}`, expectedRevision: opened.revision,
commands: [{ type: "exam", code: exam.code, minutes: 0 }]
}
});
const { data: finalOrders } = await call(`/api/ccs/attempts/${activeCcsAttempt}/commands`, {
method: "POST", token, body: {
commandId: `smoke-final-${suffix}`, expectedRevision: examined.revision,
type: "end_case", code: "", minutes: 0
}
});
expect(finalOrders.state).toBe("final_orders");
const { data: completedCcs } = await call(`/api/ccs/attempts/${activeCcsAttempt}/commands`, {
method: "POST", token, body: {
commandId: `smoke-complete-${suffix}`, expectedRevision: finalOrders.revision,
type: "exit_case", code: "", minutes: 0
}
});
expect(completedCcs.state).toBe("completed");
activeCcsAttempt = "";
// Flashcards: create a deck and card, study and grade it, verify it drops out of the due