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:
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user