Sprint 37 dead-code sweep closing out two stale supersession chains: #877 (D-167, 2026-03-24): Removes HeritageRoot type alias and ZonePaletteModifier::Heritage variant from server/src/simulation/ generator.rs. The 7 abstract heritage roots were retired in favour of the corridor cultural system; these two stubs were the only remaining references. #878 (D-032 + cascade rule): Strips the entire CharacterArchetype (Smuggler/Detective) trace from the server. Per lead direction 2026-04-21 and the development cascade (CLAUDE.md), character/NPC/ verb-differentiation/monologue code is Phase 6 detail that should not exist in code yet. The running archetype trace was pre-cascade filler, not production — production is only the client's character- creation UI and insert screens (client follow-up in #882). Deleted: - CharacterArchetype enum + StartupMessage.character_archetype field - archetype_verb_label() + archetype branch of apply_phase2_verb_filter (D-057 character-verb differentiation — marked superseded) - MonologueState.character partitioning - Gauntlet archetype plumbing (setup_gauntlet no longer takes an archetype) - server/content/schemas/drama_module.schema.yaml (zero Rust consumers) - server/content/modules/tier1/smuggling_ring_v0_1.yaml - server/tests/archetype_monologue.rs (regression guard for the removed system) - server/tests/v01_integration_playthrough.rs (archetype-dependent) Decision updates: - decisions/content.md D-032 supersession rewritten to cite the cascade (v0.2 drop invalidated the prior D-117 framing). - decisions/content.md D-035 tag taxonomy: `character` enum footnote updated; field noted as unused, do not reintroduce without a confirmed Phase 6 design. - decisions/perception.md D-057: archetype-verb differentiation marked superseded. Also bundles the types.rs version-field removal from #874 since the file was already touched here. Full trace audit in docs/architecture/sprint-37-878-audit.md. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
238 lines
9.1 KiB
Rust
238 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::mood::MoodState,
|
|
npc::{
|
|
tell_state::{DerivedTellState, TellCategory},
|
|
Contentment, DeviationTrigger, Npc, RoutineDeviation, Secret, SecretSeverity,
|
|
ToleranceThreshold,
|
|
},
|
|
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::{simulation::SimulationPlugin, test_world};
|
|
let mut app = App::new();
|
|
app.add_plugins(SimulationPlugin { seed: 0 });
|
|
test_world::setup_gauntlet(&mut app);
|
|
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::simulation::triangle::TriangleId;
|
|
use settled_reach_server::storyteller::{TriangleActivatedEvent, TriangleActivatedQueue};
|
|
|
|
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::simulation::triangle::TriangleId;
|
|
use settled_reach_server::storyteller::{TriangleActivatedEvent, TriangleActivatedQueue};
|
|
|
|
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::simulation::triangle::TriangleId;
|
|
use settled_reach_server::storyteller::{TriangleActivatedEvent, TriangleActivatedQueue};
|
|
// 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."
|
|
);
|
|
}
|