- test_journal_sprint18.gd: replace references to removed CONFIDENCE_LABELS/SOURCE_LABELS with UIStrings key tests and regression guards - dialogue_box.gd: escape ] as [rb] in _escape_bbcode for complete BBCode injection protection - dialogue_box.gd: fix _expire_entries to skip pinned entries with continue instead of break, cleaning expired entries behind pins Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
602 lines
22 KiB
GDScript
602 lines
22 KiB
GDScript
## Sprint 18 — Knowledge/journal display (#264)
|
||
## Spec refs: D-041 (knowledge graph data model), D-027 (vertical slice — KG display),
|
||
## D-042 (UIStrings for all labels)
|
||
##
|
||
## Tests now run against live Stig implementation.
|
||
## Wire format per game_state.gd v14:
|
||
## player_knowledge: {entities: [{entity_id, name, confidence, source, state, relationship, last_observed_tick}]}
|
||
##
|
||
## NOTE: player_knowledge PERSISTS between snapshots (no-clear behavior, by design).
|
||
## The server sends KG updates only when the graph changes — absence = no change.
|
||
## Contrast with current_examine_result which DOES clear each snapshot.
|
||
class_name TestJournalSprint18
|
||
extends GdUnitTestSuite
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Helpers
|
||
# ---------------------------------------------------------------------------
|
||
|
||
const JOURNAL_SCENE_PATH: String = "res://ui/journal_panel.tscn"
|
||
|
||
func _make_journal_panel() -> Control:
|
||
if not ResourceLoader.exists(JOURNAL_SCENE_PATH):
|
||
push_warning("TestJournalSprint18: journal_panel.tscn not found — scene tests skipped")
|
||
return null
|
||
var node: Control = load(JOURNAL_SCENE_PATH).instantiate()
|
||
add_child(node)
|
||
return node
|
||
|
||
|
||
func _make_kg_entity(overrides: Dictionary = {}) -> Dictionary:
|
||
## Wire format per game_state.gd v14 / Stig's Stig confirmation (2026-02-25).
|
||
## entities: [{entity_id, name, confidence, source, state, relationship, last_observed_tick}]
|
||
var base: Dictionary = {
|
||
"entity_id": 42,
|
||
"name": "Kael Davan",
|
||
"confidence": "KnowsOf",
|
||
"source": "DirectObservation",
|
||
"state": "Active",
|
||
"relationship": "PersonOfInterest",
|
||
"last_observed_tick": 1024,
|
||
}
|
||
base.merge(overrides, true)
|
||
return base
|
||
|
||
|
||
func _make_player_knowledge(entities: Array = []) -> Dictionary:
|
||
if entities.is_empty():
|
||
entities = [_make_kg_entity()]
|
||
return {"entities": entities}
|
||
|
||
|
||
func _entries_container(panel: Control) -> Node:
|
||
return panel.get_node_or_null(
|
||
"PanelContainer/MarginContainer/VBoxContainer/ScrollContainer/EntriesContainer"
|
||
)
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Lifecycle
|
||
# ---------------------------------------------------------------------------
|
||
|
||
func before_test() -> void:
|
||
GameState.player_knowledge = null
|
||
GameState.current_dialogue = null
|
||
GameState.dialogue_active = false
|
||
GameState.current_tick = 0
|
||
|
||
func after_test() -> void:
|
||
GameState.player_knowledge = null
|
||
GameState.current_dialogue = null
|
||
GameState.dialogue_active = false
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# GameState: player_knowledge snapshot parsing
|
||
# ---------------------------------------------------------------------------
|
||
|
||
func test_gamestate_player_knowledge_field_exists() -> void:
|
||
## GameState must have player_knowledge field (v14, #264).
|
||
assert_bool(GameState.has("player_knowledge")).override_failure_message(
|
||
"GameState must have 'player_knowledge' field (Sprint 18 #264)"
|
||
).is_true()
|
||
|
||
|
||
func test_gamestate_player_knowledge_null_by_default() -> void:
|
||
GameState.player_knowledge = null
|
||
assert_that(GameState.player_knowledge).is_null()
|
||
|
||
|
||
func test_gamestate_player_knowledge_set_from_snapshot() -> void:
|
||
GameState.apply_snapshot({
|
||
"tick": 10,
|
||
"player_knowledge": _make_player_knowledge(),
|
||
})
|
||
assert_that(GameState.player_knowledge).is_not_null()
|
||
assert_bool(GameState.player_knowledge.has("entities")).is_true()
|
||
|
||
|
||
func test_gamestate_player_knowledge_persists_when_absent() -> void:
|
||
## IMPORTANT: player_knowledge does NOT clear when absent from snapshot.
|
||
## Server sends KG updates only on change — absence means "no change since last tick".
|
||
## This is intentional behavior (journal should not flash empty every tick).
|
||
GameState.player_knowledge = _make_player_knowledge()
|
||
GameState.apply_snapshot({"tick": 11})
|
||
assert_that(GameState.player_knowledge).is_not_null()
|
||
|
||
|
||
func test_gamestate_player_knowledge_null_when_non_dict() -> void:
|
||
## Malformed player_knowledge (non-dict) must be rejected.
|
||
## First set a valid value, then try to overwrite with invalid
|
||
GameState.player_knowledge = _make_player_knowledge()
|
||
GameState.apply_snapshot({"tick": 1, "player_knowledge": "bad-value"})
|
||
# Non-dict is rejected — previous value preserved (or null if first time)
|
||
# The implementation only updates on Dictionary type, so value persists
|
||
assert_that(GameState.player_knowledge).is_not_null()
|
||
|
||
|
||
func test_gamestate_player_knowledge_entities_survive_roundtrip() -> void:
|
||
var entities := [
|
||
_make_kg_entity({"name": "Kael Davan", "state": "Active"}),
|
||
_make_kg_entity({"name": "Lysa Orin", "state": "Contradicted", "entity_id": 55}),
|
||
]
|
||
GameState.apply_snapshot({"tick": 5, "player_knowledge": {"entities": entities}})
|
||
var parsed_entities: Array = GameState.player_knowledge.get("entities", [])
|
||
assert_int(parsed_entities.size()).is_equal(2)
|
||
assert_that(parsed_entities[0].get("name")).is_equal("Kael Davan")
|
||
assert_that(parsed_entities[1].get("state")).is_equal("Contradicted")
|
||
|
||
|
||
func test_gamestate_player_knowledge_updated_when_new_data_arrives() -> void:
|
||
## When server sends a new player_knowledge, it replaces the previous value.
|
||
GameState.apply_snapshot({"tick": 1, "player_knowledge": _make_player_knowledge([
|
||
_make_kg_entity({"name": "Person A"}),
|
||
])})
|
||
GameState.apply_snapshot({"tick": 2, "player_knowledge": _make_player_knowledge([
|
||
_make_kg_entity({"name": "Person A"}),
|
||
_make_kg_entity({"name": "Person B", "entity_id": 99}),
|
||
])})
|
||
var entities: Array = GameState.player_knowledge.get("entities", [])
|
||
assert_int(entities.size()).is_equal(2)
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Journal panel: scene and API
|
||
# ---------------------------------------------------------------------------
|
||
|
||
func test_journal_panel_scene_exists() -> void:
|
||
assert_bool(ResourceLoader.exists(JOURNAL_SCENE_PATH)).override_failure_message(
|
||
"Journal panel scene must exist at res://ui/journal_panel.tscn"
|
||
).is_true()
|
||
|
||
|
||
func test_journal_panel_instantiates_without_crash() -> void:
|
||
var panel := _make_journal_panel()
|
||
if panel == null: return
|
||
assert_that(panel).is_not_null()
|
||
panel.queue_free()
|
||
|
||
|
||
func test_journal_panel_has_toggle_method() -> void:
|
||
var panel := _make_journal_panel()
|
||
if panel == null: return
|
||
assert_bool(panel.has_method("toggle")).override_failure_message(
|
||
"JournalPanel must have toggle() method"
|
||
).is_true()
|
||
panel.queue_free()
|
||
|
||
|
||
func test_journal_panel_has_close_method() -> void:
|
||
var panel := _make_journal_panel()
|
||
if panel == null: return
|
||
assert_bool(panel.has_method("close")).override_failure_message(
|
||
"JournalPanel must have close() method"
|
||
).is_true()
|
||
panel.queue_free()
|
||
|
||
|
||
func test_journal_panel_has_is_open_method() -> void:
|
||
var panel := _make_journal_panel()
|
||
if panel == null: return
|
||
assert_bool(panel.has_method("is_open")).override_failure_message(
|
||
"JournalPanel must have is_open() method"
|
||
).is_true()
|
||
panel.queue_free()
|
||
|
||
|
||
func test_journal_panel_has_update_from_state_method() -> void:
|
||
var panel := _make_journal_panel()
|
||
if panel == null: return
|
||
assert_bool(panel.has_method("update_from_state")).override_failure_message(
|
||
"JournalPanel must have update_from_state() method (called from main.gd)"
|
||
).is_true()
|
||
panel.queue_free()
|
||
|
||
|
||
func test_journal_panel_closed_on_init() -> void:
|
||
## Panel starts hidden — not open by default.
|
||
var panel := _make_journal_panel()
|
||
if panel == null: return
|
||
assert_bool(panel.is_open()).override_failure_message(
|
||
"JournalPanel must be closed on _ready()"
|
||
).is_false()
|
||
panel.queue_free()
|
||
|
||
|
||
func test_journal_panel_toggle_opens() -> void:
|
||
## First toggle() opens the panel.
|
||
var panel := _make_journal_panel()
|
||
if panel == null: return
|
||
panel.toggle()
|
||
assert_bool(panel.is_open()).override_failure_message(
|
||
"toggle() must set is_open() = true"
|
||
).is_true()
|
||
panel.queue_free()
|
||
|
||
|
||
func test_journal_panel_toggle_closes() -> void:
|
||
## Second toggle() closes the panel.
|
||
var panel := _make_journal_panel()
|
||
if panel == null: return
|
||
panel.toggle() # open
|
||
panel.toggle() # close
|
||
assert_bool(panel.is_open()).override_failure_message(
|
||
"Second toggle() must close the panel"
|
||
).is_false()
|
||
panel.queue_free()
|
||
|
||
|
||
func test_journal_panel_close_when_already_closed_is_safe() -> void:
|
||
## close() on an already-closed panel must not crash.
|
||
var panel := _make_journal_panel()
|
||
if panel == null: return
|
||
panel.close()
|
||
assert_bool(panel.is_open()).is_false()
|
||
panel.queue_free()
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Journal panel: entry rendering
|
||
# ---------------------------------------------------------------------------
|
||
|
||
func test_journal_panel_entries_container_exists() -> void:
|
||
## EntriesContainer is the VBoxContainer that holds entity entries.
|
||
var panel := _make_journal_panel()
|
||
if panel == null: return
|
||
var container := _entries_container(panel)
|
||
assert_that(container != null).override_failure_message(
|
||
"EntriesContainer must exist at PanelContainer/MarginContainer/VBoxContainer/ScrollContainer/EntriesContainer"
|
||
).is_true()
|
||
panel.queue_free()
|
||
|
||
|
||
func test_journal_panel_shows_entries_when_knowledge_populated() -> void:
|
||
## Opening panel with player_knowledge set creates entry nodes in EntriesContainer.
|
||
var panel := _make_journal_panel()
|
||
if panel == null: return
|
||
|
||
GameState.player_knowledge = _make_player_knowledge([
|
||
_make_kg_entity({"name": "Kael Davan"}),
|
||
])
|
||
panel.toggle() # calls _show_panel() -> _rebuild_entries()
|
||
|
||
var container := _entries_container(panel)
|
||
if container == null: panel.queue_free(); return
|
||
|
||
assert_int(container.get_child_count()).override_failure_message(
|
||
"EntriesContainer must have children when player_knowledge is populated"
|
||
).is_greater(0)
|
||
panel.queue_free()
|
||
|
||
|
||
func test_journal_panel_shows_empty_state_when_no_knowledge() -> void:
|
||
## Empty state Label is shown when player_knowledge is null.
|
||
var panel := _make_journal_panel()
|
||
if panel == null: return
|
||
|
||
GameState.player_knowledge = null
|
||
panel.toggle()
|
||
|
||
var container := _entries_container(panel)
|
||
if container == null: panel.queue_free(); return
|
||
|
||
## Empty state = exactly 1 child (the "Nothing logged yet." label)
|
||
assert_int(container.get_child_count()).override_failure_message(
|
||
"EntriesContainer should have 1 child (empty state label) when knowledge is null"
|
||
).is_equal(1)
|
||
panel.queue_free()
|
||
|
||
|
||
func test_journal_panel_two_entities_create_more_entries() -> void:
|
||
## Two entities create more entries than one (header + detail each, plus spacers).
|
||
var panel := _make_journal_panel()
|
||
if panel == null: return
|
||
|
||
GameState.player_knowledge = _make_player_knowledge([
|
||
_make_kg_entity({"name": "Entity A", "entity_id": 1}),
|
||
_make_kg_entity({"name": "Entity B", "entity_id": 2}),
|
||
])
|
||
panel.toggle()
|
||
|
||
var container := _entries_container(panel)
|
||
if container == null: panel.queue_free(); return
|
||
|
||
## Each entity: header_rtl + detail_rtl + spacer = 3 nodes. Two entities = 6 min.
|
||
assert_int(container.get_child_count()).override_failure_message(
|
||
"Two entities must create at least 6 child nodes (2 × [header + detail + spacer])"
|
||
).is_greater_equal(6)
|
||
panel.queue_free()
|
||
|
||
|
||
func test_journal_panel_contradicted_entity_uses_strikethrough() -> void:
|
||
## D-041: Contradicted entities must have strikethrough in their header BBCode.
|
||
## journal_panel.gd renders [s]Name[/s] for Contradicted state.
|
||
var panel := _make_journal_panel()
|
||
if panel == null: return
|
||
|
||
GameState.player_knowledge = _make_player_knowledge([
|
||
_make_kg_entity({"name": "Bad Guy", "state": "Contradicted"}),
|
||
])
|
||
panel.toggle()
|
||
|
||
var container := _entries_container(panel)
|
||
if container == null: panel.queue_free(); return
|
||
|
||
## First child should be the header RichTextLabel with [s]...[/s]
|
||
if container.get_child_count() == 0:
|
||
push_warning("test_journal_panel_contradicted_entity_uses_strikethrough: no entries — skip")
|
||
panel.queue_free(); return
|
||
|
||
var first_child := container.get_child(0)
|
||
if first_child is RichTextLabel:
|
||
assert_that(first_child.text).override_failure_message(
|
||
"Contradicted entity header must contain [s] (strikethrough) BBCode"
|
||
).contains("[s]")
|
||
panel.queue_free()
|
||
|
||
|
||
func test_journal_panel_active_entity_no_strikethrough() -> void:
|
||
## Active entity must NOT have strikethrough in its header.
|
||
var panel := _make_journal_panel()
|
||
if panel == null: return
|
||
|
||
GameState.player_knowledge = _make_player_knowledge([
|
||
_make_kg_entity({"name": "Good Guy", "state": "Active"}),
|
||
])
|
||
panel.toggle()
|
||
|
||
var container := _entries_container(panel)
|
||
if container == null: panel.queue_free(); return
|
||
|
||
if container.get_child_count() == 0:
|
||
push_warning("test_journal_panel_active_entity_no_strikethrough: no entries — skip")
|
||
panel.queue_free(); return
|
||
|
||
var first_child := container.get_child(0)
|
||
if first_child is RichTextLabel:
|
||
assert_bool(first_child.text.contains("[s]")).override_failure_message(
|
||
"Active entity header must NOT have strikethrough — only Contradicted gets [s]"
|
||
).is_false()
|
||
panel.queue_free()
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Journal panel: mutual exclusion with dialogue
|
||
# ---------------------------------------------------------------------------
|
||
|
||
func test_update_from_state_closes_journal_when_dialogue_active() -> void:
|
||
## Sprint briefing: journal must close when dialogue opens.
|
||
## update_from_state() is called from main.gd on each snapshot.
|
||
var panel := _make_journal_panel()
|
||
if panel == null: return
|
||
|
||
panel.toggle() # open journal
|
||
assert_bool(panel.is_open()).is_true()
|
||
|
||
GameState.dialogue_active = true
|
||
panel.update_from_state()
|
||
|
||
assert_bool(panel.is_open()).override_failure_message(
|
||
"Journal must close when GameState.dialogue_active = true (update_from_state() called)"
|
||
).is_false()
|
||
panel.queue_free()
|
||
|
||
|
||
func test_update_from_state_does_not_close_when_dialogue_inactive() -> void:
|
||
## update_from_state() must NOT close journal when dialogue is not active.
|
||
var panel := _make_journal_panel()
|
||
if panel == null: return
|
||
|
||
panel.toggle() # open journal
|
||
GameState.dialogue_active = false
|
||
panel.update_from_state()
|
||
|
||
assert_bool(panel.is_open()).override_failure_message(
|
||
"Journal must stay open when dialogue is inactive"
|
||
).is_true()
|
||
panel.queue_free()
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# UIStrings: confidence and source label keys (D-042 — now via UIStrings)
|
||
## CONFIDENCE_LABELS and SOURCE_LABELS dicts were removed from journal_panel.gd.
|
||
## Labels now come from UIStrings: knowledge_panel.confidence_* / knowledge_panel.source_*
|
||
# ---------------------------------------------------------------------------
|
||
|
||
func test_ui_strings_confidence_direct_exists() -> void:
|
||
## D-042: confidence label for "Direct" tier must be in UIStrings.
|
||
assert_bool(UIStrings.has_key("knowledge_panel.confidence_direct")).override_failure_message(
|
||
"UIStrings must have 'knowledge_panel.confidence_direct' (D-042)"
|
||
).is_true()
|
||
|
||
|
||
func test_ui_strings_confidence_knowsdetails_exists() -> void:
|
||
assert_bool(UIStrings.has_key("knowledge_panel.confidence_knowsdetails")).override_failure_message(
|
||
"UIStrings must have 'knowledge_panel.confidence_knowsdetails' (D-042)"
|
||
).is_true()
|
||
|
||
|
||
func test_ui_strings_confidence_knowsof_exists() -> void:
|
||
assert_bool(UIStrings.has_key("knowledge_panel.confidence_knowsof")).override_failure_message(
|
||
"UIStrings must have 'knowledge_panel.confidence_knowsof' (D-042)"
|
||
).is_true()
|
||
|
||
|
||
func test_ui_strings_confidence_suspects_exists() -> void:
|
||
assert_bool(UIStrings.has_key("knowledge_panel.confidence_suspects")).override_failure_message(
|
||
"UIStrings must have 'knowledge_panel.confidence_suspects' (D-042)"
|
||
).is_true()
|
||
|
||
|
||
func test_ui_strings_all_confidence_keys_non_empty() -> void:
|
||
## All four confidence label values must be non-empty strings.
|
||
var keys := [
|
||
"knowledge_panel.confidence_direct",
|
||
"knowledge_panel.confidence_knowsdetails",
|
||
"knowledge_panel.confidence_knowsof",
|
||
"knowledge_panel.confidence_suspects",
|
||
]
|
||
for key in keys:
|
||
if not UIStrings.has_key(key): continue
|
||
assert_bool(UIStrings.get_text(key).length() > 0).override_failure_message(
|
||
"UIStrings key '%s' must be non-empty" % key
|
||
).is_true()
|
||
|
||
|
||
func test_ui_strings_source_directobservation_exists() -> void:
|
||
assert_bool(UIStrings.has_key("knowledge_panel.source_directobservation")).override_failure_message(
|
||
"UIStrings must have 'knowledge_panel.source_directobservation' (D-042)"
|
||
).is_true()
|
||
|
||
|
||
func test_ui_strings_source_toldby_exists() -> void:
|
||
assert_bool(UIStrings.has_key("knowledge_panel.source_toldby")).override_failure_message(
|
||
"UIStrings must have 'knowledge_panel.source_toldby' (D-042)"
|
||
).is_true()
|
||
|
||
|
||
func test_ui_strings_source_heard_exists() -> void:
|
||
assert_bool(UIStrings.has_key("knowledge_panel.source_heard")).override_failure_message(
|
||
"UIStrings must have 'knowledge_panel.source_heard' (D-042)"
|
||
).is_true()
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Journal panel: _state_color() contract
|
||
# ---------------------------------------------------------------------------
|
||
|
||
func test_state_color_contradicted_is_amber() -> void:
|
||
## D-041: Contradicted → amber tint (ENTITY_COLOR_POI) — THE FRIEND arc surface.
|
||
var panel := _make_journal_panel()
|
||
if panel == null: return
|
||
var color: Color = panel._state_color("Contradicted")
|
||
assert_that(color).override_failure_message(
|
||
"_state_color('Contradicted') must return ENTITY_COLOR_POI (amber)"
|
||
).is_equal(Constants.ENTITY_COLOR_POI)
|
||
panel.queue_free()
|
||
|
||
|
||
func test_state_color_stale_is_dimmed() -> void:
|
||
## Stale → dimmed text color (IMPLANT_TEXT_DIM).
|
||
var panel := _make_journal_panel()
|
||
if panel == null: return
|
||
var color: Color = panel._state_color("Stale")
|
||
assert_that(color).override_failure_message(
|
||
"_state_color('Stale') must return IMPLANT_TEXT_DIM"
|
||
).is_equal(Constants.IMPLANT_TEXT_DIM)
|
||
panel.queue_free()
|
||
|
||
|
||
func test_state_color_active_is_normal() -> void:
|
||
## Active → normal insert text color (INSERT_COLOR_TEXT).
|
||
var panel := _make_journal_panel()
|
||
if panel == null: return
|
||
var color: Color = panel._state_color("Active")
|
||
assert_that(color).override_failure_message(
|
||
"_state_color('Active') must return INSERT_COLOR_TEXT"
|
||
).is_equal(Constants.INSERT_COLOR_TEXT)
|
||
panel.queue_free()
|
||
|
||
|
||
func test_state_color_contradicted_differs_from_active() -> void:
|
||
## Contradicted and Active must have visually distinct colors.
|
||
var panel := _make_journal_panel()
|
||
if panel == null: return
|
||
var contradicted := panel._state_color("Contradicted")
|
||
var active := panel._state_color("Active")
|
||
assert_that(contradicted).is_not_equal(active)
|
||
panel.queue_free()
|
||
|
||
|
||
func test_state_color_stale_differs_from_active() -> void:
|
||
## Stale and Active must have visually distinct colors.
|
||
var panel := _make_journal_panel()
|
||
if panel == null: return
|
||
var stale := panel._state_color("Stale")
|
||
var active := panel._state_color("Active")
|
||
assert_that(stale).is_not_equal(active)
|
||
panel.queue_free()
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# UIStrings: knowledge_panel keys (D-042)
|
||
# ---------------------------------------------------------------------------
|
||
|
||
func test_ui_strings_knowledge_panel_tab_contacts_exists() -> void:
|
||
## Journal title uses knowledge_panel.tab_contacts.
|
||
assert_bool(UIStrings.has_key("knowledge_panel.tab_contacts")).override_failure_message(
|
||
"UIStrings must have 'knowledge_panel.tab_contacts' key (D-042)"
|
||
).is_true()
|
||
|
||
|
||
func test_ui_strings_knowledge_panel_empty_state_exists() -> void:
|
||
## Empty state message uses knowledge_panel.empty_state.
|
||
assert_bool(UIStrings.has_key("knowledge_panel.empty_state")).override_failure_message(
|
||
"UIStrings must have 'knowledge_panel.empty_state' key (D-042)"
|
||
).is_true()
|
||
|
||
|
||
func test_ui_strings_knowledge_panel_empty_state_non_empty() -> void:
|
||
if not UIStrings.has_key("knowledge_panel.empty_state"): return
|
||
assert_bool(UIStrings.get_text("knowledge_panel.empty_state").length() > 0).is_true()
|
||
|
||
|
||
func test_ui_strings_knowledge_panel_tab_contacts_non_empty() -> void:
|
||
if not UIStrings.has_key("knowledge_panel.tab_contacts"): return
|
||
assert_bool(UIStrings.get_text("knowledge_panel.tab_contacts").length() > 0).is_true()
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# D-042: CONFIDENCE_LABELS/SOURCE_LABELS now via UIStrings — FIXED (2026-02-25)
|
||
## Previously filed as a gap: journal_panel.gd had hardcoded CONFIDENCE_LABELS dict.
|
||
## Fixed by Stig: dicts removed, all labels now use UIStrings.get_text("knowledge_panel.*").
|
||
## Regression guard: verify the dicts are gone and UIStrings fallback works.
|
||
# ---------------------------------------------------------------------------
|
||
|
||
func test_d042_fixed_panel_has_no_confidence_labels_dict() -> void:
|
||
## Regression: CONFIDENCE_LABELS dict must NOT exist on journal_panel — it was removed.
|
||
## If this test fails, the hardcoded dict was accidentally re-introduced.
|
||
var panel := _make_journal_panel()
|
||
if panel == null: return
|
||
assert_bool(panel.get("CONFIDENCE_LABELS") == null).override_failure_message(
|
||
"D-042 regression: CONFIDENCE_LABELS dict must be removed from journal_panel.gd"
|
||
).is_true()
|
||
panel.queue_free()
|
||
|
||
|
||
func test_d042_fixed_panel_has_no_source_labels_dict() -> void:
|
||
## Regression: SOURCE_LABELS dict must NOT exist on journal_panel — it was removed.
|
||
var panel := _make_journal_panel()
|
||
if panel == null: return
|
||
assert_bool(panel.get("SOURCE_LABELS") == null).override_failure_message(
|
||
"D-042 regression: SOURCE_LABELS dict must be removed from journal_panel.gd"
|
||
).is_true()
|
||
panel.queue_free()
|
||
|
||
|
||
func test_d042_uistrings_fallback_for_unknown_confidence() -> void:
|
||
## UIStrings falls back to the key string itself for missing keys.
|
||
## journal_panel.gd relies on this for graceful degradation.
|
||
var fallback := UIStrings.get_text("knowledge_panel.confidence_nonexistent_level")
|
||
assert_that(fallback).override_failure_message(
|
||
"UIStrings fallback must return the key string itself for unknown keys"
|
||
).is_equal("knowledge_panel.confidence_nonexistent_level")
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Constants
|
||
# ---------------------------------------------------------------------------
|
||
|
||
func test_canvas_insert_constant_is_10() -> void:
|
||
assert_int(Constants.CANVAS_INSERT).is_equal(10)
|
||
|
||
|
||
func test_journal_fade_constants_reasonable() -> void:
|
||
## FADE_IN and FADE_OUT must be short (< 0.5s) for responsive UI.
|
||
var panel := _make_journal_panel()
|
||
if panel == null: return
|
||
assert_float(panel.FADE_IN).is_between(0.0, 0.5)
|
||
assert_float(panel.FADE_OUT).is_between(0.0, 0.5)
|
||
panel.queue_free()
|