Files
settled-reach/server/tests/tell_escalation.rs
T
jpmschweitzerandClaude Sonnet 4.6 ce0df2f320 refactor(simulation): remove v0.1 content loading system (#655)
Delete the hand-authored YAML content pipeline (server/src/content/) superseded
by the v0.2 generator-first approach (D-122, D-128). Runtime ECS types that were
co-located with content loading have been extracted to dedicated simulation modules:

- simulation/triangle.rs: TriangleState, TriangleCrisisEventQueue, tick/resolve systems
- simulation/line_pool.rs: LinePoolIndex, AccessTier, TrustTier, Mood, LinePoolIndexResource
- simulation/knowledge_grant.rs: KnowledgeGrant, Prerequisites

Monologue systems (trigger_monologue, trigger_recognition_monologue,
trigger_event_monologue) now use hardcoded fallback lines only; the
ContentStoreResource branch and select_pool_line function are removed.

Deleted: content/{loader,types,line_pool,hot_reload,spawn,instantiation,entanglement,mod}.rs
Deleted: tests/{content_loading,content_runtime,content_scaling,template_instantiation,template_schema}.rs
Deleted: bin/line_preview.rs (v0.1 tool)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-13 09:07:57 +01:00

234 lines
9.1 KiB
Rust

//! Integration tests for tell escalation on triangle activation (#589, D-024 axis 9).
//!
//! Covers:
//! - D-027 criterion 4: RoutineDeviation tell must be observable after activation
//! - D-024 axis 9: tell system is a simulation output, not authored content
//! - #589: escalate_tells_on_activation system inserts RoutineDeviation on triangle NPCs
//!
//! Test structure:
//! - Layer 1 (pure): verify RoutineDeviation dominates all other tells (already
//! covered by unit tests in tell_state.rs, regression guards here)
//! - Layer 2 (ECS world): verify activation event → RoutineDeviation insertion (pending #589)
//! - Layer 2 (ECS world): verify expired RoutineDeviation is removed (pending #589)
//!
//! Pending tests are marked #[ignore] — they compile against the current API but
//! will fail until escalate_tells_on_activation is registered in StorytellerPlugin.
use bevy_app::prelude::*;
use bevy_ecs::prelude::*;
use settled_reach_server::{
npc::{
tell_state::{DerivedTellState, TellCategory},
Contentment, DeviationTrigger, Npc, RoutineDeviation, Secret, SecretSeverity,
ToleranceThreshold,
},
npc::mood::MoodState,
simulation::tier::ActiveSim,
};
// ---------------------------------------------------------------------------
// Layer 1 regression: RoutineDeviation component presence → RoutineDeviation tell
// (These pass today; guard against future tell priority regressions)
// ---------------------------------------------------------------------------
/// Build a minimal ECS world with one NPC and run derive_tell_state.
fn make_tell_world_with_deviation(deviation: Option<RoutineDeviation>) -> (World, Entity) {
let mut world = World::new();
let mut npc = world.spawn((
Npc,
ActiveSim,
Secret {
description: "minor".into(),
severity: SecretSeverity::Minor,
known_by: vec![],
},
ToleranceThreshold { current_stress: 0, threshold: 50 },
Contentment { level: 0 },
MoodState { mood: settled_reach_server::npc::mood::NpcMood::Neutral, changed_tick: 0 },
DerivedTellState::default(),
));
let entity = if let Some(dev) = deviation {
npc.insert(dev).id()
} else {
npc.id()
};
(world, entity)
}
#[test]
fn routine_deviation_component_produces_deviation_tell_via_system() {
// Layer 1 regression: verify that inserting RoutineDeviation on an NPC and running
// the derive_tell_state system produces TellCategory::RoutineDeviation.
// This guards against priority regressions in derive_tell_state (D-027 criterion 4).
let (mut world, entity) = make_tell_world_with_deviation(Some(RoutineDeviation {
trigger: DeviationTrigger::WalkAway,
tick: 0,
expires_at_tick: 300,
}));
let mut schedule = Schedule::default();
schedule.add_systems(settled_reach_server::npc::tell_state::derive_tell_state);
schedule.run(&mut world);
let tell = world.get::<DerivedTellState>(entity).unwrap();
assert_eq!(
tell.category,
Some(TellCategory::RoutineDeviation),
"NPC with RoutineDeviation component must produce RoutineDeviation tell (D-027 criterion 4)"
);
}
#[test]
fn no_deviation_component_does_not_produce_deviation_tell() {
// Layer 1 regression: absence of RoutineDeviation must not produce deviation tell.
let (mut world, entity) = make_tell_world_with_deviation(None);
let mut schedule = Schedule::default();
schedule.add_systems(settled_reach_server::npc::tell_state::derive_tell_state);
schedule.run(&mut world);
let tell = world.get::<DerivedTellState>(entity).unwrap();
assert_ne!(
tell.category,
Some(TellCategory::RoutineDeviation),
"NPC without RoutineDeviation must not produce RoutineDeviation tell"
);
}
// ---------------------------------------------------------------------------
// Layer 2: activation event → RoutineDeviation insertion (pending #589)
// ---------------------------------------------------------------------------
/// Set up a minimal gauntlet-based app with storyteller plugin running.
#[cfg(feature = "gauntlet")]
fn build_storyteller_app() -> App {
use settled_reach_server::{
bridge::types::CharacterArchetype,
simulation::SimulationPlugin,
test_world,
};
let mut app = App::new();
app.add_plugins(SimulationPlugin);
test_world::setup_gauntlet(&mut app, CharacterArchetype::default());
app
}
/// Retrieve the first NPC entity visible in the gauntlet (used to fabricate test events).
#[cfg(feature = "gauntlet")]
fn first_npc_entity(app: &mut App) -> Entity {
use settled_reach_server::npc::Npc;
let mut q = app.world_mut().query_filtered::<Entity, With<Npc>>();
q.iter(app.world()).next().expect("gauntlet must have at least one NPC")
}
#[cfg(feature = "gauntlet")]
#[test]
#[ignore = "pending escalate_tells_on_activation system (#589)"]
fn triangle_activation_event_inserts_routine_deviation_on_anchor_npc() {
// Inject a TriangleActivatedEvent pointing to an NPC, run one tick, assert
// RoutineDeviation is inserted on that NPC by escalate_tells_on_activation.
use settled_reach_server::storyteller::{TriangleActivatedEvent, TriangleActivatedQueue};
use settled_reach_server::simulation::triangle::{TriangleId};
let mut app = build_storyteller_app();
// Run one tick so the world is fully initialized before we inject
app.update();
let anchor = first_npc_entity(&mut app);
{
let mut queue = app.world_mut().resource_mut::<TriangleActivatedQueue>();
queue.push(TriangleActivatedEvent {
triangle_id: TriangleId::from_seed_and_slug(42, "test-escalation"),
tick: 1,
anchor_entity: anchor,
anchor_score: 25.0,
});
}
// Run one tick — escalate_tells_on_activation should fire
app.update();
let deviation = app.world().get::<RoutineDeviation>(anchor);
assert!(
deviation.is_some(),
"Anchor NPC must have RoutineDeviation after TriangleActivated event (#589, D-024 axis 9)"
);
}
#[cfg(feature = "gauntlet")]
#[test]
#[ignore = "pending escalate_tells_on_activation system (#589)"]
fn triangle_activation_produces_routine_deviation_tell_in_snapshot() {
// End-to-end: after activation event, DerivedTellState on anchor NPC must be
// TellCategory::RoutineDeviation. This verifies the full axis-9 pipeline.
use settled_reach_server::storyteller::{TriangleActivatedEvent, TriangleActivatedQueue};
use settled_reach_server::simulation::triangle::TriangleId;
let mut app = build_storyteller_app();
app.update(); // initialize
let anchor = first_npc_entity(&mut app);
{
let mut queue = app.world_mut().resource_mut::<TriangleActivatedQueue>();
queue.push(TriangleActivatedEvent {
triangle_id: TriangleId::from_seed_and_slug(42, "test-tell"),
tick: 1,
anchor_entity: anchor,
anchor_score: 25.0,
});
}
app.update(); // activation tick
app.update(); // derive_tell_state tick
let tell = app.world().get::<DerivedTellState>(anchor);
assert_eq!(
tell.map(|t| t.category),
Some(Some(TellCategory::RoutineDeviation)),
"After triangle activation, anchor NPC's tell must be RoutineDeviation (D-027 criterion 4)"
);
}
#[cfg(feature = "gauntlet")]
#[test]
#[ignore = "pending expires_at_tick field and removal system (#589)"]
fn routine_deviation_expires_after_duration() {
// After TELL_ESCALATION_DURATION_TICKS ticks, RoutineDeviation must be removed
// by the expiry system. This verifies the component doesn't persist forever.
//
// Edge case: D-027 criterion 4 must continue to fire DURING the window
// and stop firing AFTER it. NPCs shouldn't be permanently flagged.
use settled_reach_server::storyteller::{TriangleActivatedEvent, TriangleActivatedQueue};
use settled_reach_server::simulation::triangle::TriangleId;
// NOTE: TELL_ESCALATION_DURATION_TICKS constant (= 300) expected in storyteller module.
// This test will need updating once the constant is public.
let mut app = build_storyteller_app();
app.update(); // initialize
let anchor = first_npc_entity(&mut app);
{
let mut queue = app.world_mut().resource_mut::<TriangleActivatedQueue>();
queue.push(TriangleActivatedEvent {
triangle_id: TriangleId::from_seed_and_slug(42, "test-expiry"),
tick: 1,
anchor_entity: anchor,
anchor_score: 25.0,
});
}
// Run enough ticks to trigger expiry (301 > TELL_ESCALATION_DURATION_TICKS = 300).
// Note: 302 updates is acceptable for a unit test; if this becomes a perf concern
// when un-ignored, consider advancing SimulationTime directly instead of looping.
for _ in 0..302 {
app.update();
}
let deviation = app.world().get::<RoutineDeviation>(anchor);
assert!(
deviation.is_none(),
"RoutineDeviation must be removed after TELL_ESCALATION_DURATION_TICKS ticks (#589). \
Persistent deviation would flag NPCs permanently — breaking the tell signal over time."
);
}