fix(server): address PR #23 review — 4 critical bugs, 3 warnings, 11 suggestions

Critical fixes:
- Add PendingRecognitionWire serialization roundtrip test
- Check Option return from delay.cancel() before logging
- InputQueue capacity limit (1000) with drop-oldest and warning
- TODO in observation.rs references ticket #450

Warning fixes:
- Hot-reload guards against invalid/empty content root
- Location header mismatch warning in line pool indexing
- walk_yaml() depth limit (100) against symlink loops
- Consecutive reload failure counter (warns after 5+)

Test additions:
- Negative prerequisite filtering test for monologue lines
- Integration test for pending_recognitions in observer snapshot
- Eavesdrop threshold ordering assertion (Careful < default)

Documentation:
- Playtesting expectation comments on delay constants
- is_pending() scalability note for future NPC cognitive delay
- ID format regex validation in line pool spec
- BTreeMap vs sort() ordering clarification in loader
- Multiplayer TODO in relationships.rs references D-010
- ContentSlug ticket #452 filed for entity slug resolution

394 tests, 0 failures.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-16 01:03:51 +01:00
co-authored by Claude Opus 4.6
parent 8a334b47f3
commit d4fdbf426e
11 changed files with 379 additions and 17 deletions
+13
View File
@@ -14,6 +14,10 @@ use crate::simulation::time::{SimulationTime, TickRate};
use bevy_ecs::prelude::*;
use std::collections::VecDeque;
/// Maximum number of inputs the queue will hold before dropping oldest.
/// Prevents unbounded memory growth from input flooding.
pub const INPUT_QUEUE_CAPACITY: usize = 1000;
/// Queue of pending player inputs, ordered by tick
#[derive(Resource, Debug, Default)]
pub struct InputQueue {
@@ -24,6 +28,7 @@ impl InputQueue {
/// Add a new input to the queue.
/// Inputs must be pushed in tick order for deterministic processing.
/// Panics in debug builds if tick ordering is violated.
/// Drops oldest inputs when capacity is exceeded.
pub fn push(&mut self, input: PlayerInput) {
debug_assert!(
self.queue.back().is_none_or(|last| last.tick <= input.tick),
@@ -31,6 +36,14 @@ impl InputQueue {
self.queue.back().map_or(0, |last| last.tick),
input.tick,
);
if self.queue.len() >= INPUT_QUEUE_CAPACITY {
let dropped = self.queue.pop_front();
tracing::warn!(
"InputQueue at capacity ({}), dropping oldest input (tick={})",
INPUT_QUEUE_CAPACITY,
dropped.map_or(0, |d| d.tick),
);
}
self.queue.push_back(input);
}
+24
View File
@@ -91,6 +91,13 @@ pub fn update_listening_focus(
// Sprint stance: stationary but too high-alert to listen
let Some(threshold) = ListeningFocus::threshold_for_stance(stance) else {
if focus.eavesdrop_target.is_some() || focus.stationary_ticks > 0 {
tracing::debug!(
"Sprint stance resets eavesdrop: stationary_ticks={}, had_target={}",
focus.stationary_ticks,
focus.eavesdrop_target.is_some(),
);
}
focus.stationary_ticks = 0;
focus.eavesdrop_target = None;
continue;
@@ -426,6 +433,23 @@ mod tests {
assert_eq!(focus.stationary_ticks, EAVESDROP_THRESHOLD);
}
// -----------------------------------------------------------------------
// Constant invariants
// -----------------------------------------------------------------------
#[test]
fn eavesdrop_threshold_careful_less_than_normal() {
// T4: The careful threshold MUST be strictly less than the normal
// threshold — careful stance rewards patience with faster eavesdrop
// activation (D-053, D-018).
assert!(
EAVESDROP_THRESHOLD_CAREFUL < EAVESDROP_THRESHOLD,
"EAVESDROP_THRESHOLD_CAREFUL ({}) must be < EAVESDROP_THRESHOLD ({})",
EAVESDROP_THRESHOLD_CAREFUL,
EAVESDROP_THRESHOLD,
);
}
// -----------------------------------------------------------------------
// Edge cases
// -----------------------------------------------------------------------