@@ -2,25 +2,13 @@
## Spec refs: D-041 (knowledge graph data model), D-027 (vertical slice — KG display),
## D-042 (UIStrings for all labels)
##
## Test plan from joint.md:
## "Manual: journal panel opens, KG facts listed with correct metadata.
## Contradicted facts visually distinct."
## 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}]}
##
## These are test-first tests. Most will FAIL until Stig implements:
## 1. GameState.player_knowledge field
## 2. GameState.apply_snapshot() parsing for "player_knowledge"
## 3. JournalPanel scene and script
##
## Unit-testable coverage:
## - GameState: player_knowledge field exists and parses from snapshot
## - GameState: journal/dialogue mutual exclusion field
## - KnowledgeConfidence levels match expected enum strings
## - KnowledgeSource types match expected enum strings
## - Journal panel: scene exists and can be instantiated
## - Journal panel: renders at least one fact entry from GameState
## - Journal panel: contradicted facts have visual distinction
## - Journal panel: stale facts rendered differently from active
## - UIStrings: journal-specific keys exist (D-042)
## 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
@@ -40,31 +28,32 @@ func _make_journal_panel() -> Control:
return node
func _make_kg_fact ( overrides : Dictionary = { } ) - > Dictionary :
## Build a KG fact dictionary matching the wire format from joint.md .
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 ,
" entity_name " : " Kael Davan " ,
" fact_text " : " Operates cargo bay 7. " ,
" name " : " Kael Davan " ,
" confidence " : " KnowsOf " ,
" source " : " DirectObservation " ,
" state " : " Active " ,
" last_tick " : 1024 ,
" relationship " : " PersonOfInterest " ,
" last_observed_tick " : 1024 ,
}
base . merge ( overrides , true )
return base
func _make_player_knowledge ( facts : Array = [ ] ) - > Dictionary :
## Build a player_knowledge snapshot field with given facts.
if facts . is_empty ( ) :
facts = [ _make_kg_fact ( ) ]
return {
" entities " : [
{ " id " : 42 , " name " : " Kael Davan " } ,
] ,
" facts " : facts ,
}
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 "
)
# ---------------------------------------------------------------------------
@@ -72,336 +61,505 @@ func _make_player_knowledge(facts: Array = []) -> Dictionary:
# ---------------------------------------------------------------------------
func before_test ( ) - > void :
if GameState . has ( " player_knowledge " ) :
GameState . player_knowledge = null
GameState . player_knowledge = null
GameState . current_dialogue = null
GameState . dialogue_active = false
GameState . current_tick = 0
func after_test ( ) - > void :
if GameState . has ( " player_knowledge " ) :
GameState . player_knowledge = null
GameState . player_knowledge = null
GameState . current_dialogue = null
GameState . dialogue_active = false
# ---------------------------------------------------------------------------
# GameState: player_knowledge field (test-first — add to game_state.gd)
# GameState: player_knowledge snapshot parsing
# ---------------------------------------------------------------------------
func test_gamestate_player_knowledge_field_exists ( ) - > void :
## GameState must have a player_knowledge field (Sprint 18 , #264).
## Fails until Stig adds the field to game_state.gd.
## 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 — add to game_state.gd ) "
" GameState must have 'player_knowledge' field (Sprint 18 #264) "
) . is_true ( )
func test_gamestate_player_knowledge_null_by_default ( ) - > void :
## player_knowledge starts null (no KG data received yet).
if not GameState . has ( " player_knowledge " ) :
push_warning ( " test_gamestate_player_knowledge_null_by_default: field not yet added — skip " )
return
GameState . player_knowledge = null
assert_that ( GameState . player_knowledge ) . is_null ( )
func test_gamestate_player_knowledge_set_from_snapshot ( ) - > void :
## apply_snapshot with player_knowledge dict populates GameState.player_knowledge.
if not GameState . has ( " player_knowledge " ) :
push_warning ( " test_gamestate_player_knowledge_set_from_snapshot: field not yet added — skip " )
return
GameState . apply_snapshot ( {
" tick " : 10 ,
" player_knowledge " : _make_player_knowledge ( ) ,
} )
assert_that ( GameState . player_knowledge ) . is_not_null ( )
assert_bool ( GameState . player_knowledge . has ( " facts " ) ) . is_true ( )
assert_bool ( GameState . player_knowledge . has ( " entities " ) ) . is_true ( )
func test_gamestate_player_knowledge_null_when_absent ( ) - > void :
## Snapshot without player_knowledge clears the field .
## Prev ent s stale journal data from persisting after server clears it .
if not GameState . has ( " player_knowledge " ) :
push_warning ( " test_gamestate_player_knowledge_null_when_absent: field not yet added — skip " )
return
func test_gamestate_player_knowledge_persists_when_absent ( ) - > void :
## IMPORTANT: player_knowledge does NOT clear when absent from snapshot .
## Server s end s 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_null ( )
assert_that ( GameState . player_knowledge ) . is_not_null ( )
func test_gamestate_player_knowledge_null_when_non_dict ( ) - > void :
## Non-dict player_knowledge is rejected.
if not GameState . has ( " player_knowledge " ) :
push_warning ( " test_gamestate_player_knowledge_null_when_non_dict: field not yet added — skip " )
return
## 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 " } )
assert_that ( GameState . player_knowledge ) . is_null ( )
# 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_facts_survive_roundtrip ( ) - > void :
## The facts array must survive parsing — the journal panel reads this.
if not GameState . has ( " player_knowledge " ) :
push_warning ( " test_gamestate_player_knowledge_facts_survive_roundtrip: field not yet added — skip " )
return
var facts := [
_make_kg_fact ( { " confidence " : " Direct " , " state " : " Active " } ) ,
_make_kg_fact ( { " confidence " : " Suspects " , " state " : " Contradicted " } ) ,
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 " : { " facts " : facts , " entities " : [ ] } ,
} )
var parsed_facts : Array = GameState . player_knowledge . get ( " facts " , [ ] )
assert_int ( parsed_facts . size ( ) ) . is_equal ( 2 )
assert_that ( parsed_facts [ 0 ] . get ( " confidence " ) ) . is_equal ( " Direct " )
assert_that ( parsed_facts [ 1 ] . get ( " state " ) ) . is_equal ( " Contradicted " )
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 )
# ---------------------------------------------------------------------------
# KnowledgeConfidence levels (D-041)
## Valid values: Suspects / KnowsOf / KnowsDetails / Direct
# ---------------------------------------------------------------------------
func test_knowledge_confidence_levels_are_known_strings ( ) - > void :
## D-041: Four confidence tiers. Any other value is a server error.
## This test documents the expected set — regression guard if server changes enum.
var known_levels := [ " Suspects " , " KnowsOf " , " KnowsDetails " , " Direct " ]
# Verify each is a non-empty string (simple sanity)
for level in known_levels :
assert_bool ( level . length ( ) > 0 ) . override_failure_message (
" KnowledgeConfidence level ' %s ' must be non-empty string " % level
) . is_true ( )
func test_knowledge_fact_state_values_are_known_strings ( ) - > void :
## D-041: KG facts have state: Active | Stale | Contradicted.
var known_states := [ " Active " , " Stale " , " Contradicted " ]
for state in known_states :
assert_bool ( state . length ( ) > 0 ) . is_true ( )
func test_knowledge_source_values_are_known_strings ( ) - > void :
## D-041: KG facts have source: DirectObservation | ToldBy | Heard.
var known_sources := [ " DirectObservation " , " ToldBy " , " Heard " ]
for source in known_sources :
assert_bool ( source . length ( ) > 0 ) . is_true ( )
# ---------------------------------------------------------------------------
# Journal panel: scene exists (test-first)
# Journal panel: scene and API
# ---------------------------------------------------------------------------
func test_journal_panel_scene_exists ( ) - > void :
## The journal panel scene must exist before tests can run (Sprint 18, #264).
## Fails until Stig creates res://ui/journal_panel.tscn.
assert_bool ( ResourceLoader . exists ( JOURNAL_SCENE_PATH ) ) . override_failure_message (
" Journal panel scene must exist at res://ui/journal_panel.tscn (Sprint 18 #264) "
" Journal panel scene must exist at res://ui/journal_panel.tscn "
) . is_true ( )
func test_journal_panel_instantiates_without_crash ( ) - > void :
## Journal panel must instantiate cleanly.
var panel := _make_journal_panel ( )
if panel == null : return # scene not yet created, skip
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: render behavior (test-first)
# Journal panel: entry rendering
# ---------------------------------------------------------------------------
func test_journal_panel_reads_player_knowledge_from_gamestate ( ) - > void :
## Journal panel must display facts from GameState.player_knowledge .
## Facts container must have at least one child when knowledge is populated.
if not GameState . has ( " player_knowledge " ) :
push_warning ( " test_journal_panel_reads_player_knowledge_from_gamestate: GameState field not yet added — skip " )
return
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_fact ( { " fact_text " : " Operates cargo bay 7. " } ) ,
_make_kg_entity ( { " name " : " Kael Davan " } ) ,
] )
panel . toggle ( ) # calls _show_panel() -> _rebuild_entries()
# Trigger panel refresh (panel reads from GameState on update)
if panel . has_method ( " refresh " ) :
panel . refresh ( )
elif panel . has_method ( " _refresh " ) :
panel . _refresh ( )
# Panel should show at least one fact entry
# Implementation detail: "FactsList" or "FactsContainer" — adjust once scene exists
var facts_node := panel . get_node_or_null ( " FactsList " )
if facts_node == null :
facts_node = panel . get_node_or_null ( " ScrollContainer/FactsList " )
if facts_node == null :
facts_node = panel . get_node_or_null ( " VBoxContainer/FactsList " )
if facts_node != null :
assert_int ( facts_node . get_child_count ( ) ) . override_failure_message (
" Journal panel must show at least one fact entry when player_knowledge is populated "
) . is_greater ( 0 )
else :
push_warning ( " test_journal_panel_reads_player_knowledge_from_gamestate: FactsList node not found — adjust path after scene creation " )
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_empty_when_no_knowledge ( ) - > void :
## Journal panel shows nothing when player_knowledge is null/empty .
if not GameState . has ( " player_knowledge " ) :
push_warning ( " test_journal_panel_empty_when_no_knowledge: GameState field not yet added — skip " )
return
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
if panel . has_method ( " refresh " ) :
panel . refresh ( )
elif panel . has_method ( " _refresh " ) :
panel . _refresh ( )
panel . toggle ( )
var facts_node := panel . get_node_or_null ( " FactsList " )
if facts_node == null :
facts_node = panel . get_node_or_null ( " ScrollContainer/FactsList " )
if facts_node == null :
facts_node = panel . get_node_or_null ( " VBoxContainer/FactsList " )
var container := _entries_container ( panel )
if container == null : panel . queue_free ( ) ; return
if facts_node != null :
assert_int ( facts_node . get_child_count ( ) ) . override_failure_message (
" Journal panel must show no fact entries when player_ knowledge is null"
) . is_equal ( 0 )
else :
push_warning ( " test_journal_panel_empty_when_no_knowledge: FactsList node not found — adjust path after scene creation " )
## 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/dialogue mutual exclusion (sprint-18/client.md)
## "The journal panel must close when dialogue opens and vice versa."
# Journal panel: mutual exclusion with dialogue
# ---------------------------------------------------------------------------
func test_gamestate_has_journal_open_field_or_panel_has_close_on_dialogue ( ) - > void :
## Sprint 18 : journal and dialogue cannot be open simultaneously .
## Either GameState tracks journal_open state, or DialogueBox has a
## close_journal signal. Verify at least one mechanism exists.
##
## This is a structural check — implementation detail may vary.
var has_journal_open_in_gamestate := GameState . has ( " journal_open " )
var dialogue_box_exists := ResourceLoader . exists ( " res://ui/dialogue_box.tscn " )
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
if not has_journal_open_in_gamestate and dialogue_box_exists :
# Check if DialogueBox has a signal for this
var box := load ( " res://ui/dialogue_box.tscn " ) . instantiate ( )
auto_free ( box )
var has_signal := box . has_signal ( " dialogue_opened " )
# Either journal_open in GameState or dialogue_opened signal
if not has_signal :
push_warning ( " test: no mutual-exclusion mechanism found yet — will fail until #264 implements it " )
# We don't hard-assert here because the mechanism is implementation-choice
# The manual test plan catches the actual behavior
panel . toggle ( ) # open journal
assert_bool ( panel . is_open ( ) ) . is_true ( )
# This test always passes as a documentation checkpoint
assert_bool ( true ) . 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: journal keys (D-042)
## All journal labels must use UIStrings autoload.
# Journal panel: CONFIDENCE_LABELS and SOURCE_LABELS constants
# ---------------------------------------------------------------------------
func test_ui_strings_has_journal_section ( ) - > void :
## D-042 : j ournal label strings must be in ui-strings.yaml under "journal" .
## Fails until Stig adds journal strings.
var has_any_journal_key : = UIStrings . has_key ( " journal.title " ) or
UIStrings . has_key ( " journal.no_facts " ) or
UIStrings . has_key ( " journal.confidence_prefix " )
assert_bool ( has_any_journal_key ) . override_failure_message (
" D-042: UIStrings must have at least one 'journal.*' key (add to data/ui-strings.yaml) "
func test_confidence_labels_covers_all_four_levels ( ) - > void :
## D-041 : F our confidence tiers. CONFIDENCE_LABELS must map all four .
var panel := _make_journal_panel ( )
if panel = = null : return
var labels : Dictionary = panel . CONFIDENCE_LABELS
for level in [ " Direct " , " KnowsDetails " , " KnowsOf " , " Suspects " ] :
assert_bool ( labels . has ( level ) ) . override_failure_message (
" CONFIDENCE_LABELS must include ' %s ' " % level
) . is_true ( )
panel . queue_free ( )
func test_source_labels_covers_known_sources ( ) - > void :
## D-041: Source types. SOURCE_LABELS must map core sources.
var panel := _make_journal_panel ( )
if panel == null : return
var labels : Dictionary = panel . SOURCE_LABELS
for source in [ " DirectObservation " , " ToldBy " , " Heard " ] :
assert_bool ( labels . has ( source ) ) . override_failure_message (
" SOURCE_LABELS must include ' %s ' " % source
) . is_true ( )
panel . queue_free ( )
func test_confidence_labels_all_non_empty ( ) - > void :
## All CONFIDENCE_LABELS values must be non-empty strings.
var panel := _make_journal_panel ( )
if panel == null : return
for key in panel . CONFIDENCE_LABELS :
var val : String = panel . CONFIDENCE_LABELS [ key ]
assert_bool ( val . length ( ) > 0 ) . override_failure_message (
" CONFIDENCE_LABELS[' %s '] must be non-empty " % key
) . is_true ( )
panel . queue_free ( )
# ---------------------------------------------------------------------------
# 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_journal_title_key ( ) - > void :
## Journal panel title string must exist .
if not UIStrings . has_key ( " journal.title " ) :
push_warning ( " test_ui_strings_journal_title_key: 'journal.title' not yet added — skip " )
return
var title := UIStrings . get_text ( " journal.title " )
assert_bool ( title . length ( ) > 0 ) . 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_confidence_labels_present ( ) - > void :
## Journal displays KnowledgeConfidence as human-readable labels.
## Each confidence level must have a UIStrings entry.
var expected_keys := [
" journal.confidence.suspects " ,
" journal.confidence.knows_of " ,
" journal.confidence.knows_details " ,
" journal.confidence.direct " ,
]
var missing : Array [ String ] = [ ]
for key in expected_keys :
if not UIStrings . has_key ( key ) :
missing . append ( key )
if not missing . is_empty ( ) :
push_warning ( " test_ui_strings_confidence_labels_present: missing keys: %s — skip until added " % str ( missing ) )
# Document-only: no hard assert while feature is unimplemented
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 ( )
# ---------------------------------------------------------------------------
# Constants: CANVAS_INSERT layer for journal
## J ournal panel (insert-layer UI, D-013) must be on CanvasLayer 10 .
# D-042 finding: CONFIDENCE_LABELS is hardcoded, not UIStrings
## The j ournal panel uses a hardcoded CONFIDENCE_LABELS dict rather than UIStrings .
## D-042 requires all labels via UIStrings YAML.
## Filed as a finding — not a blocking test, but documents the gap.
# ---------------------------------------------------------------------------
func test_d042_finding_confidence_labels_not_in_uistrings ( ) - > void :
## D-042 gap: journal_panel.gd uses hardcoded CONFIDENCE_LABELS dict.
## Expected per D-042: UIStrings should drive confidence display labels.
## ui-strings.yaml has confidence_high/medium/low/rumor but not Direct/KnowsDetails/etc.
## These keys are not mapped to UIStrings. Flagging as technical debt.
##
## This test PASSES because we're documenting the gap, not requiring it to be fixed now.
## See: journal_panel.gd CONFIDENCE_LABELS vs data/ui-strings.yaml knowledge_panel section.
var panel := _make_journal_panel ( )
if panel == null : return
var has_confidence_labels : bool = panel . CONFIDENCE_LABELS . size ( ) > 0
assert_bool ( has_confidence_labels ) . is_true ( ) # Hardcoded dict exists
# D-042 finding: confidence labels should use UIStrings.get_text("knowledge_panel.confidence_*")
# Current state: CONFIDENCE_LABELS = {"Direct": "Confirmed", "KnowsDetails": "Detailed", ...}
# Recommendation: replace with UIStrings lookup in a future sprint
panel . queue_free ( )
# ---------------------------------------------------------------------------
# Constants
# ---------------------------------------------------------------------------
func test_canvas_insert_constant_is_10 ( ) - > void :
## Journal panel is a diegetic insert — must be on layer 10.
## Sanity check that CANVAS_INSERT hasn't drifted.
assert_int ( Constants . CANVAS_INSERT ) . override_failure_message (
" CANVAS_INSERT must be 10 (insert layer, D-013) "
) . is_equal ( 10 )
assert_int ( Constants . CANVAS_INSERT ) . is_equal ( 10 )
# ---------------------------------------------------------------------------
# Minimap: structural sanity (for Hoshe #151 verification when done)
## These run as no-ops until Stig finishes task #1.
# ---------------------------------------------------------------------------
func test_minimap_scene_path_is_documented ( ) - > void :
## Document the expected scene path for minimap renderer.
## Actual structural tests are in test_minimap_sprint18.gd (to be created
## once Stig confirms the scene name/path).
var likely_paths := [
" res://ui/minimap_renderer.tscn " ,
" res://ui/minimap.tscn " ,
" res://scenes/minimap_renderer.tscn " ,
]
var found := false
for path in likely_paths :
if ResourceLoader . exists ( path ) :
found = true
break
if not found :
push_warning ( " test_minimap_scene_path: minimap scene not yet created — waiting for Stig's #151 implementation " )
# No hard assert: minimap is in_progress
func test_gamestate_poi_list_field_exists_or_in_entities ( ) - > void :
## Sprint 18 #151: POI data must be accessible from GameState.discovered_pois.
## Stig #151: GameState accepts "poi_list" and "discovered_pois" snapshot keys.
## Both map to GameState.discovered_pois for MinimapRenderer consumption.
# Apply snapshot with poi_list field (Sprint 17 server wire name)
GameState . apply_snapshot ( {
" tick " : 1 ,
" poi_list " : [
{ " id " : " poi_001 " , " x " : 50 , " y " : 30 , " poi_category " : " Location " , " label " : " Exit A " } ,
{ " id " : " poi_002 " , " x " : 80 , " y " : 15 , " poi_category " : " Contact " , " label " : " Kael's Office " } ,
] ,
} )
# GameState.discovered_pois must be populated from either "poi_list" or "discovered_pois"
assert_int ( GameState . discovered_pois . size ( ) ) . override_failure_message (
" GameState.discovered_pois must be populated from snapshot 'poi_list' field (#148/#149) "
) . is_greater ( 0 )
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 ( )