fix(simulation): address PR #89 review — version comment, settings contract, docs, dead process recovery

Must-fix: protocol version comment now references PROTOCOL_VERSION
(no hardcoded number), settings delete is idempotent no-op.

Suggestions addressed: BehaviorModifier dedup claim dropped, hash
collision safety documented, FK pragma in test store, columns_to_value
consolidated, modifier_hint mismatch logging, OffDuty test coverage,
batching tradeoff documented, unknown value_type warning, occluded
text risk documented, dead child respawn in voice worker, INJECT
block ambiguity documented.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-13 09:34:33 +01:00
co-authored by Claude Opus 4.6
parent b37583ddfc
commit 51414b0b63
7 changed files with 146 additions and 38 deletions
+6 -6
View File
@@ -139,6 +139,9 @@ pub fn process_settings_commands(
};
// Process all commands, last response wins (same tick batching).
// If multiple settings commands arrive in one tick, only the final response
// reaches the client. This is acceptable: the client sends one action per tick
// in normal use, and batch changes via RequestAll after bulk updates.
for cmd in commands {
match cmd {
SettingsCommand::Change { ref key, ref value } => {
@@ -171,16 +174,13 @@ pub fn process_settings_commands(
});
}
SettingsCommand::Delete { ref key } => {
let deleted = store.delete(key);
// Idempotent: deleting a non-existent key is a silent no-op.
let _deleted = store.delete(key);
snapshot_buffer.pending_settings_response = Some(SettingsResponseWire {
success: true,
kind: "ack".into(),
settings: vec![],
error: if deleted {
None
} else {
Some(format!("setting not found: {}", key))
},
error: None,
});
}
}
+17 -24
View File
@@ -49,6 +49,7 @@ impl SettingsStore {
/// Open an in-memory database (for tests).
pub fn open_in_memory() -> Result<Self, rusqlite::Error> {
let conn = Connection::open_in_memory()?;
conn.execute_batch("PRAGMA foreign_keys = ON;")?;
conn.execute(
"CREATE TABLE settings (
player_id TEXT NOT NULL,
@@ -92,7 +93,7 @@ impl SettingsStore {
match rows.next()? {
Some(row) => {
let vtype: String = row.get(0)?;
Ok(Some(columns_to_value(&vtype, row)?))
Ok(Some(columns_to_value(&vtype, row, 1)?))
}
None => Ok(None),
}
@@ -112,7 +113,7 @@ impl SettingsStore {
while let Some(row) = rows.next()? {
let key: String = row.get(0)?;
let vtype: String = row.get(1)?;
let value = columns_to_value_offset(&vtype, row)?;
let value = columns_to_value(&vtype, row, 2)?;
result.insert(key, value);
}
Ok(result)
@@ -158,33 +159,25 @@ fn value_to_columns(
}
}
/// Read a SettingValue from a row where value columns start at index 1.
/// Used by `get()` which selects (value_type, value_text, value_int, value_float, value_bool).
/// Read a SettingValue from a row where value columns start at `offset`.
/// `offset` is the index of the value_text column (value_int = offset+1, etc.).
///
/// Used by `get()` (offset=1) and `get_all()` (offset=2) which select different
/// column prefixes before the value columns.
fn columns_to_value(
vtype: &str,
row: &rusqlite::Row<'_>,
offset: usize,
) -> Result<SettingValue, rusqlite::Error> {
match vtype {
"string" => Ok(SettingValue::String(row.get::<_, String>(1)?)),
"int" => Ok(SettingValue::Int(row.get::<_, i64>(2)?)),
"float" => Ok(SettingValue::Float(row.get::<_, f64>(3)?)),
"bool" => Ok(SettingValue::Bool(row.get::<_, bool>(4)?)),
other => Ok(SettingValue::String(format!("<unknown type: {}>", other))),
}
}
/// Read a SettingValue from a row where value columns start at index 2.
/// Used by `get_all()` which selects (key, value_type, value_text, ...).
fn columns_to_value_offset(
vtype: &str,
row: &rusqlite::Row<'_>,
) -> Result<SettingValue, rusqlite::Error> {
match vtype {
"string" => Ok(SettingValue::String(row.get::<_, String>(2)?)),
"int" => Ok(SettingValue::Int(row.get::<_, i64>(3)?)),
"float" => Ok(SettingValue::Float(row.get::<_, f64>(4)?)),
"bool" => Ok(SettingValue::Bool(row.get::<_, bool>(5)?)),
other => Ok(SettingValue::String(format!("<unknown type: {}>", other))),
"string" => Ok(SettingValue::String(row.get::<_, String>(offset)?)),
"int" => Ok(SettingValue::Int(row.get::<_, i64>(offset + 1)?)),
"float" => Ok(SettingValue::Float(row.get::<_, f64>(offset + 2)?)),
"bool" => Ok(SettingValue::Bool(row.get::<_, bool>(offset + 3)?)),
other => {
tracing::warn!("unknown settings value_type '{}', treating as String", other);
Ok(SettingValue::String(format!("<unknown type: {}>", other)))
}
}
}