test(simulation): QA coverage for Phase 2 verbs, sprint suppression, occupancy, inventory
Add comprehensive test suites across sprint 6 systems: - Phase 2 observer filter: Confront injection, archetype label relabeling, contradiction marking, POI priority ordering (12 tests) - Sprint anomaly: detection during sprint, queue semantics, delay timing, D-055 visible-but-suppressed integration (7 tests) - Wire format: Confront and CharacterArchetype msgpack roundtrip (4 tests) - TilePresence: 4-layer coexistence on same tile (1 test) - Inventory: Take/Place full roundtrip, capacity enforcement (3 tests) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -819,4 +819,105 @@ mod tests {
|
||||
assert!(world.get::<TilePosition>(item).is_some(), "item stays on ground");
|
||||
assert!(world.get::<CarriedBy>(item).is_none(), "no CarriedBy when full");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn take_then_place_roundtrip() {
|
||||
// D-065: full cycle — item on ground → Take → carried → Place → ground again
|
||||
let mut world = bevy_ecs::world::World::new();
|
||||
world.insert_resource(InputQueue::default());
|
||||
world.insert_resource(SimulationTime::default());
|
||||
world.init_resource::<crate::knowledge::EntityRegistry>();
|
||||
|
||||
let player = world
|
||||
.spawn((PlayerCharacter, TilePosition::new(5, 5, 0)))
|
||||
.id();
|
||||
let player_sid = world.resource_mut::<crate::knowledge::EntityRegistry>().register(player);
|
||||
|
||||
let item = world
|
||||
.spawn((
|
||||
TilePosition::new(5, 4, 0),
|
||||
ItemName("Manifest Copy".into()),
|
||||
))
|
||||
.id();
|
||||
let item_sid = world.resource_mut::<crate::knowledge::EntityRegistry>().register(item);
|
||||
|
||||
let mut schedule = bevy_ecs::schedule::Schedule::default();
|
||||
schedule.add_systems(process_player_input);
|
||||
|
||||
// Step 1: Take
|
||||
world.resource_mut::<InputQueue>().push(PlayerInput {
|
||||
tick: 0,
|
||||
action: PlayerAction::Interact {
|
||||
target_entity_id: Some(item_sid.0),
|
||||
verb: Some("Take".into()),
|
||||
},
|
||||
});
|
||||
schedule.run(&mut world);
|
||||
|
||||
assert!(world.get::<TilePosition>(item).is_none(), "item off ground after Take");
|
||||
assert_eq!(world.get::<CarriedBy>(item).unwrap().0, player_sid);
|
||||
assert_eq!(world.get::<InventorySlot>(item).unwrap().0, 0);
|
||||
|
||||
// Step 2: Place
|
||||
world.resource_mut::<InputQueue>().push(PlayerInput {
|
||||
tick: 1,
|
||||
action: PlayerAction::Interact {
|
||||
target_entity_id: Some(item_sid.0),
|
||||
verb: Some("Place".into()),
|
||||
},
|
||||
});
|
||||
world.resource_mut::<SimulationTime>().tick = 1;
|
||||
schedule.run(&mut world);
|
||||
|
||||
let pos = world.get::<TilePosition>(item).expect("item back on ground after Place");
|
||||
assert_eq!(*pos, TilePosition::new(5, 5, 0), "placed at player position");
|
||||
assert!(world.get::<CarriedBy>(item).is_none(), "CarriedBy removed after Place");
|
||||
assert!(world.get::<InventorySlot>(item).is_none(), "InventorySlot removed after Place");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn take_without_target_id_is_noop() {
|
||||
// Edge case: Take verb with no target_entity_id should not panic
|
||||
let mut world = bevy_ecs::world::World::new();
|
||||
world.insert_resource(InputQueue::default());
|
||||
world.insert_resource(SimulationTime::default());
|
||||
world.init_resource::<crate::knowledge::EntityRegistry>();
|
||||
|
||||
world.spawn((PlayerCharacter, TilePosition::new(5, 5, 0)));
|
||||
|
||||
world.resource_mut::<InputQueue>().push(PlayerInput {
|
||||
tick: 0,
|
||||
action: PlayerAction::Interact {
|
||||
target_entity_id: None,
|
||||
verb: Some("Take".into()),
|
||||
},
|
||||
});
|
||||
|
||||
let mut schedule = bevy_ecs::schedule::Schedule::default();
|
||||
schedule.add_systems(process_player_input);
|
||||
schedule.run(&mut world); // should not panic
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn place_without_target_id_is_noop() {
|
||||
// Edge case: Place verb with no target_entity_id should not panic
|
||||
let mut world = bevy_ecs::world::World::new();
|
||||
world.insert_resource(InputQueue::default());
|
||||
world.insert_resource(SimulationTime::default());
|
||||
world.init_resource::<crate::knowledge::EntityRegistry>();
|
||||
|
||||
world.spawn((PlayerCharacter, TilePosition::new(5, 5, 0)));
|
||||
|
||||
world.resource_mut::<InputQueue>().push(PlayerInput {
|
||||
tick: 0,
|
||||
action: PlayerAction::Interact {
|
||||
target_entity_id: None,
|
||||
verb: Some("Place".into()),
|
||||
},
|
||||
});
|
||||
|
||||
let mut schedule = bevy_ecs::schedule::Schedule::default();
|
||||
schedule.add_systems(process_player_input);
|
||||
schedule.run(&mut world); // should not panic
|
||||
}
|
||||
}
|
||||
|
||||
@@ -857,4 +857,50 @@ mod tests {
|
||||
"two Fixtures on same tile should collide"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_four_layers_coexist_on_same_tile() {
|
||||
// D-054: Standing + Prone + Seated + Fixture all share one tile
|
||||
let mut world = bevy_ecs::world::World::new();
|
||||
world.insert_resource(WalkabilityMap::new(10, 10, 1));
|
||||
|
||||
let target = TilePosition::new(5, 4, 0);
|
||||
|
||||
// Fixture and Prone already at tile
|
||||
world.spawn((target, TilePresence::Fixture));
|
||||
world.spawn((target, TilePresence::Prone));
|
||||
|
||||
// Standing mover enters
|
||||
let standing = world
|
||||
.spawn((
|
||||
TilePosition::new(5, 5, 0),
|
||||
TilePresence::Standing,
|
||||
MoveIntent { target },
|
||||
))
|
||||
.id();
|
||||
|
||||
// Seated mover enters from elsewhere
|
||||
let seated = world
|
||||
.spawn((
|
||||
TilePosition::new(5, 3, 0),
|
||||
TilePresence::Seated,
|
||||
MoveIntent { target },
|
||||
))
|
||||
.id();
|
||||
|
||||
let mut schedule = bevy_ecs::schedule::Schedule::default();
|
||||
schedule.add_systems(validate_movement);
|
||||
schedule.run(&mut world);
|
||||
|
||||
assert_eq!(
|
||||
*world.get::<TilePosition>(standing).unwrap(),
|
||||
target,
|
||||
"Standing should share tile with Fixture + Prone"
|
||||
);
|
||||
assert_eq!(
|
||||
*world.get::<TilePosition>(seated).unwrap(),
|
||||
target,
|
||||
"Seated should share tile with Fixture + Prone + Standing"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -506,3 +506,96 @@ fn all_object_type_variants_roundtrip() {
|
||||
assert_eq!(decoded, obj_type, "ObjectType::{:?} roundtrip failed", obj_type);
|
||||
}
|
||||
}
|
||||
|
||||
/// VerbKind::Confront (Phase 2, #422) must survive MessagePack round-trip.
|
||||
/// Guards against Confront being omitted from serde mapping.
|
||||
#[test]
|
||||
fn verb_kind_confront_roundtrip() {
|
||||
let mut snapshot = test_snapshot(0, vec![]);
|
||||
snapshot.nearby_interactions = vec![NearbyInteraction {
|
||||
entity_id: 1,
|
||||
entity_type: EntityKind::Npc,
|
||||
distance: 1,
|
||||
verbs: vec![VerbOption {
|
||||
kind: VerbKind::Confront,
|
||||
label: "Confront".into(),
|
||||
priority: 3,
|
||||
available: true,
|
||||
}],
|
||||
object_type: None,
|
||||
contradicted: false,
|
||||
}];
|
||||
|
||||
let bytes = rmp_serde::to_vec_named(&snapshot).expect("serialize");
|
||||
let decoded: ObserverSnapshot = rmp_serde::from_slice(&bytes).expect("deserialize");
|
||||
|
||||
assert_eq!(decoded.nearby_interactions.len(), 1);
|
||||
assert_eq!(decoded.nearby_interactions[0].verbs[0].kind, VerbKind::Confront);
|
||||
assert_eq!(decoded.nearby_interactions[0].verbs[0].label, "Confront");
|
||||
}
|
||||
|
||||
/// CharacterArchetype enum round-trips through MessagePack (#422).
|
||||
/// Used in Phase 2 label relabeling — must survive the wire.
|
||||
#[test]
|
||||
fn all_character_archetype_variants_roundtrip() {
|
||||
let archetypes = [
|
||||
CharacterArchetype::Smuggler,
|
||||
CharacterArchetype::Detective,
|
||||
];
|
||||
|
||||
for archetype in archetypes {
|
||||
let bytes = rmp_serde::to_vec_named(&archetype).expect("serialize");
|
||||
let decoded: CharacterArchetype = rmp_serde::from_slice(&bytes).expect("deserialize");
|
||||
assert_eq!(decoded, archetype, "CharacterArchetype::{:?} roundtrip failed", archetype);
|
||||
}
|
||||
}
|
||||
|
||||
/// NearbyInteraction.contradicted=true round-trips through MessagePack (#422).
|
||||
/// Guards the contradiction flag survives serialization.
|
||||
#[test]
|
||||
fn nearby_interaction_contradicted_roundtrip() {
|
||||
let mut snapshot = test_snapshot(0, vec![]);
|
||||
snapshot.nearby_interactions = vec![NearbyInteraction {
|
||||
entity_id: 1,
|
||||
entity_type: EntityKind::Npc,
|
||||
distance: 1,
|
||||
verbs: vec![VerbOption {
|
||||
kind: VerbKind::Talk,
|
||||
label: "Talk".into(),
|
||||
priority: 1,
|
||||
available: true,
|
||||
}],
|
||||
object_type: None,
|
||||
contradicted: true,
|
||||
}];
|
||||
|
||||
let bytes = rmp_serde::to_vec_named(&snapshot).expect("serialize");
|
||||
let decoded: ObserverSnapshot = rmp_serde::from_slice(&bytes).expect("deserialize");
|
||||
|
||||
assert!(decoded.nearby_interactions[0].contradicted, "contradicted flag should survive roundtrip");
|
||||
}
|
||||
|
||||
/// NearbyInteraction.object_type round-trips through MessagePack (#422).
|
||||
/// Verifies object_type=Some(Container) survives the wire.
|
||||
#[test]
|
||||
fn nearby_interaction_object_type_roundtrip() {
|
||||
let mut snapshot = test_snapshot(0, vec![]);
|
||||
snapshot.nearby_interactions = vec![NearbyInteraction {
|
||||
entity_id: 1,
|
||||
entity_type: EntityKind::Object,
|
||||
distance: 1,
|
||||
verbs: vec![VerbOption {
|
||||
kind: VerbKind::Open,
|
||||
label: "Open".into(),
|
||||
priority: 1,
|
||||
available: true,
|
||||
}],
|
||||
object_type: Some(ObjectType::Container),
|
||||
contradicted: false,
|
||||
}];
|
||||
|
||||
let bytes = rmp_serde::to_vec_named(&snapshot).expect("serialize");
|
||||
let decoded: ObserverSnapshot = rmp_serde::from_slice(&bytes).expect("deserialize");
|
||||
|
||||
assert_eq!(decoded.nearby_interactions[0].object_type, Some(ObjectType::Container));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user