Residue batch on Cut 10's fourth fix batch: seal path_for, fill N1's gaps, pin the gate's other axis
S3: Mind::path_for bypassed the grammar door and the daemon's own tests constructed stores through it directly. It is now private; a test-only Mind::store_path_for validates through require_grammatical_slug first, gated behind cfg(test) here and a test-support feature dev-dependents opt into (huginn-daemon's Cargo.toml). The four daemon test call sites and one huginn-mind test call site now go through the door. S2: N1's door, require_grammatical_slug, had no mutation entry for two of its three callers (open_with and require_instance survived deleting it; open already had N1's own escape test). Added a test per caller and a mutation entry for each of the three deletions (N1open, N1openwith, N1requireinstance) in tools/eureka-cut10-mutations.psd1. S4: a Unicode hyphen (U+2010) folded to ASCII '-' before the grammar check survived. Added a fixture and mutation entry (S4fold) pinning that no such fold runs before Slug::validate_slug. S1: the size-gate fixtures varied operation and runtime id together (4/16, 4/16, 10/19), so a formula tracking one and dropping the other still passed. Added a fixture varying them independently (10/16 and 4/19) and two mutation entries (N4c, N4d) for the two formulas that each drop one term. Verified: cargo test -p huginn-mind --lib (65/65), cargo test -p huginn-daemon --lib (20/20, one more than before: the new independent-axis gate test). Mutation suites through the Eureka harness: Cut 8 67/67 killed, Cut 9 38/38 killed, Cut 10 77/77 killed (71 before this batch, +6: N1open, N1openwith, N1requireinstance, S4fold, N4c, N4d). M0 green in every suite. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
4f7e3b5edc
commit
65c46aa59d
7 changed files with 255 additions and 7 deletions
|
|
@ -30,3 +30,9 @@ signal-hook = "0.3"
|
|||
|
||||
[dev-dependencies]
|
||||
tempfile = "3"
|
||||
|
||||
# Test-only: `Mind::store_path_for`, the grammar-checked door onto a raw
|
||||
# store's path. The daemon's own store-integrity tests reach a store directly
|
||||
# (to break a row on purpose) and must go through the same door `open` does
|
||||
# rather than reconstructing the path by hand (S3, residue batch).
|
||||
huginn-mind = { path = "../huginn-mind", features = ["test-support"] }
|
||||
|
|
|
|||
|
|
@ -626,7 +626,7 @@ pub(crate) mod tests {
|
|||
use huginn_mind::store::{CacheBackingStore, DatabaseEntry};
|
||||
|
||||
let root = tempfile::tempdir().unwrap();
|
||||
let path = Mind::<OwnedRedbMessagePackBackingStore>::path_for(root.path(), &slug(INSTANCE));
|
||||
let path = Mind::<OwnedRedbMessagePackBackingStore>::store_path_for(root.path(), &slug(INSTANCE)).unwrap();
|
||||
{
|
||||
let mut daemon = Daemon::open(root.path(), &slug(INSTANCE)).unwrap();
|
||||
let outcome = daemon.handle(HuginnMindRequest::Admit(batch(INSTANCE, vec![identity(INSTANCE)])), now());
|
||||
|
|
@ -678,7 +678,7 @@ pub(crate) mod tests {
|
|||
assert_eq!(writes.len(), 1, "a cut spec alone derives nothing");
|
||||
let cut_spec_id = writes[0].id.0.clone();
|
||||
drop(daemon);
|
||||
(cut_spec_id, OwnedRedbMessagePackBackingStore::new(&Mind::<OwnedRedbMessagePackBackingStore>::path_for(root, &slug(INSTANCE))).unwrap())
|
||||
(cut_spec_id, OwnedRedbMessagePackBackingStore::new(&Mind::<OwnedRedbMessagePackBackingStore>::store_path_for(root, &slug(INSTANCE)).unwrap()).unwrap())
|
||||
}
|
||||
|
||||
let cut_spec_type = PipelineKind::CutSpec.type_id();
|
||||
|
|
@ -812,7 +812,7 @@ pub(crate) mod tests {
|
|||
fn the_daemon_refuses_loudly_when_it_cannot_open_the_mind_and_binds_nothing() {
|
||||
let root = tempfile::tempdir().unwrap();
|
||||
let held = Daemon::open(root.path(), &slug(INSTANCE)).unwrap();
|
||||
let path = Mind::<OwnedRedbMessagePackBackingStore>::path_for(root.path(), &slug(INSTANCE));
|
||||
let path = Mind::<OwnedRedbMessagePackBackingStore>::store_path_for(root.path(), &slug(INSTANCE)).unwrap();
|
||||
assert_eq!(
|
||||
Daemon::open(root.path(), &slug(INSTANCE)).err(),
|
||||
Some(MindRefusal::MindAlreadyOwned { path: path.display().to_string() })
|
||||
|
|
@ -822,7 +822,7 @@ pub(crate) mod tests {
|
|||
let mut planted = Daemon::open(root.path(), &slug(INSTANCE)).unwrap();
|
||||
planted.handle(HuginnMindRequest::Admit(batch(INSTANCE, vec![identity(INSTANCE)])), now());
|
||||
drop(planted);
|
||||
let moved = Mind::<OwnedRedbMessagePackBackingStore>::path_for(root.path(), &slug(OTHER));
|
||||
let moved = Mind::<OwnedRedbMessagePackBackingStore>::store_path_for(root.path(), &slug(OTHER)).unwrap();
|
||||
std::fs::create_dir_all(moved.parent().unwrap()).unwrap();
|
||||
std::fs::copy(&path, &moved).unwrap();
|
||||
assert_eq!(
|
||||
|
|
|
|||
|
|
@ -873,6 +873,37 @@ mod tests {
|
|||
}
|
||||
}
|
||||
|
||||
/// Residue S1: every fixture up to this one, including the pair above,
|
||||
/// moves `operation` and the runtime id together -- both short (`view`,
|
||||
/// 4 bytes / `huginn-yggdrasil`, 16) or both long (`open_items`, 10 /
|
||||
/// `huginn-thought-cage`, 19) -- so a formula that tracks only one of the
|
||||
/// two and ignores the other still lands on the right answer at both
|
||||
/// points by coincidence. This pins a point where they move apart: a long
|
||||
/// operation with the short runtime id, and a short operation with the
|
||||
/// long one, so a gate that dropped either term is wrong at one of them.
|
||||
#[test]
|
||||
fn the_gates_boundary_holds_when_operation_and_runtime_id_vary_independently() {
|
||||
for (operation, runtime_id) in [("open_items", "huginn-yggdrasil"), ("view", "huginn-thought-cage")] {
|
||||
let at = sized_exactly_full("m-g", operation, runtime_id, MAX_RESPONSE_BYTES);
|
||||
let over = sized_exactly_full("m-g", operation, runtime_id, MAX_RESPONSE_BYTES + 1);
|
||||
assert_eq!(
|
||||
within_window(at.clone(), "m-g", operation, runtime_id),
|
||||
at,
|
||||
"{operation}/{runtime_id}: exactly the limit passes"
|
||||
);
|
||||
|
||||
let refused = within_window(over.clone(), "m-g", operation, runtime_id);
|
||||
let (correlation, answered) = decode_response(&refused).unwrap();
|
||||
assert_eq!(correlation, "m-g");
|
||||
let HuginnMindResponse::Refused(MindRefusal::ResponseTooLarge { bytes, limit }) =
|
||||
answered.expect("a refusal is an answer, not an envelope failure")
|
||||
else {
|
||||
panic!("{operation}/{runtime_id}: one byte over the limit was not refused: {refused:?}");
|
||||
};
|
||||
assert_eq!((bytes, limit), (MAX_RESPONSE_BYTES + 1, MAX_RESPONSE_BYTES), "{operation}/{runtime_id}");
|
||||
}
|
||||
}
|
||||
|
||||
/// One cut spec's own reference, by its cut label.
|
||||
fn spec_ref(daemon: &mut Daemon<OwnedRedbMessagePackBackingStore, NoIndex>, cut: &str) -> PipelineRef {
|
||||
let query = PipelineQuery {
|
||||
|
|
|
|||
|
|
@ -23,3 +23,11 @@ sha2 = "0.10"
|
|||
[dev-dependencies]
|
||||
serde_json = "1"
|
||||
tempfile = "3"
|
||||
|
||||
# `test-support` exposes `Mind::store_path_for`, a grammar-checked path
|
||||
# accessor tests in other crates need to reach a raw store directly. It buys
|
||||
# no runtime surface: gated `#[cfg(any(test, feature = "test-support"))]`, so
|
||||
# it compiles only under `cargo test` in this crate or when a dependent's own
|
||||
# dev-dependencies enable it (see huginn-daemon/Cargo.toml).
|
||||
[features]
|
||||
test-support = []
|
||||
|
|
|
|||
|
|
@ -752,7 +752,10 @@ mod tests {
|
|||
let root = tempfile::tempdir().unwrap();
|
||||
let mut yggdrasil = Mind::open(root.path(), &slug(INSTANCE)).unwrap();
|
||||
let mut thought_cage = Mind::open(root.path(), &slug(OTHER_INSTANCE)).unwrap();
|
||||
assert_ne!(Mind::path_for(root.path(), &slug(INSTANCE)), Mind::path_for(root.path(), &slug(OTHER_INSTANCE)));
|
||||
assert_ne!(
|
||||
Mind::store_path_for(root.path(), &slug(INSTANCE)).unwrap(),
|
||||
Mind::store_path_for(root.path(), &slug(OTHER_INSTANCE)).unwrap()
|
||||
);
|
||||
committed(admit(&mut yggdrasil, vec![instance(INSTANCE), stewardship(INSTANCE, REPO)]));
|
||||
committed(admit(&mut thought_cage, vec![instance(OTHER_INSTANCE)]));
|
||||
let key = format!("{INSTANCE}:stewardship:GameCult_-Epiphany.n1");
|
||||
|
|
|
|||
|
|
@ -100,11 +100,26 @@ pub struct Mind<S: MindStore> {
|
|||
|
||||
impl Mind<OwnedRedbMessagePackBackingStore> {
|
||||
/// `<state_root>/minds/<instance>/mind.redb`. Derived for convenience;
|
||||
/// the `instance` document inside is the identity.
|
||||
pub fn path_for(state_root: &Path, instance: &Slug) -> PathBuf {
|
||||
/// the `instance` document inside is the identity. Private: every caller
|
||||
/// that can reach a raw path must go through the grammar door first, so
|
||||
/// this is never exposed on its own. `open` calls it only after
|
||||
/// `require_grammatical_slug` has already passed.
|
||||
fn path_for(state_root: &Path, instance: &Slug) -> PathBuf {
|
||||
state_root.join("minds").join(&instance.0).join("mind.redb")
|
||||
}
|
||||
|
||||
/// Test-only door onto the same path: validates through
|
||||
/// `require_grammatical_slug` first, exactly as `open` does, so a test
|
||||
/// that must reach the raw store directly (to break a row on purpose)
|
||||
/// cannot bypass the grammar the way the old public `path_for` did. Built
|
||||
/// only for `cfg(test)` in this crate and for `huginn-daemon`'s own tests
|
||||
/// via the `test-support` feature; never compiled into a release binary.
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn store_path_for(state_root: &Path, instance: &Slug) -> Result<PathBuf, MindRefusal> {
|
||||
require_grammatical_slug("instance", instance)?;
|
||||
Ok(Self::path_for(state_root, instance))
|
||||
}
|
||||
|
||||
/// Opens the instance's mind under the state root, taking the store's
|
||||
/// exclusive lock for the mind's lifetime. A second owner of the same
|
||||
/// path, in this process or another, is `MindAlreadyOwned`. An empty
|
||||
|
|
@ -592,4 +607,59 @@ mod tests {
|
|||
assert!(!outer.path().join("escaped").join("mind.redb").exists(), "no store escaped the state root");
|
||||
assert!(!inner.join("minds").exists(), "no store was created under the state root either");
|
||||
}
|
||||
|
||||
/// S2: `open_with` is the door `open` shares with every store already in
|
||||
/// hand (planted directly, or opened by a test through `store_path_for`),
|
||||
/// so it must run `require_grammatical_slug` too, not only `open`'s own
|
||||
/// path-joining caller. A store need not hold anything for this to
|
||||
/// refuse: the declared name is checked before the store is even pulled.
|
||||
#[test]
|
||||
fn open_with_refuses_a_declared_instance_outside_the_grammar() {
|
||||
let store = MemoryStore::new();
|
||||
let bad = Slug("not a slug!".into());
|
||||
assert_eq!(
|
||||
Mind::open_with(store.clone(), &bad).err(),
|
||||
Some(MindRefusal::Document(epiphany_pipeline::PipelineRefusal::InvalidFormat {
|
||||
field: "instance".into(),
|
||||
value: bad.0.clone(),
|
||||
}))
|
||||
);
|
||||
assert_eq!(store.pull_count(), 0, "refused before the store was ever pulled");
|
||||
}
|
||||
|
||||
/// S2: `require_instance` is A1 for admission and the daemon's reads
|
||||
/// alike, and it runs the same grammar door on the *declared* name before
|
||||
/// comparing it to the mind's own. A declared name outside the grammar is
|
||||
/// `InvalidFormat { field: "declared" }`, never folded into a comparison
|
||||
/// or silently treated as merely foreign.
|
||||
#[test]
|
||||
fn require_instance_refuses_a_declared_name_outside_the_grammar() {
|
||||
let mind = crate::fixtures::seeded();
|
||||
let bad = Slug("not a slug!".into());
|
||||
assert_eq!(
|
||||
mind.require_instance(&bad).err(),
|
||||
Some(MindRefusal::Document(epiphany_pipeline::PipelineRefusal::InvalidFormat {
|
||||
field: "declared".into(),
|
||||
value: bad.0.clone(),
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
/// S4: a Unicode hyphen (U+2010) is not ASCII `-`, so a grammatical
|
||||
/// `Slug` never contains one and a name carrying it is refused by the
|
||||
/// leaf's own grammar as written -- nothing here may fold it to plain
|
||||
/// `-` before asking `Slug::validate_slug`, the way `S3wide`'s fullwidth
|
||||
/// mutant folds a fullwidth letter to its ASCII form.
|
||||
#[test]
|
||||
fn a_unicode_hyphen_is_not_folded_before_the_grammar_check() {
|
||||
let declared = Slug(format!("ab{}cd", '\u{2010}'));
|
||||
assert!(!declared.0.is_ascii(), "U+2010 is not ASCII, unlike plain '-'");
|
||||
assert_eq!(
|
||||
Mind::open_with(MemoryStore::new(), &declared).err(),
|
||||
Some(MindRefusal::Document(epiphany_pipeline::PipelineRefusal::InvalidFormat {
|
||||
field: "instance".into(),
|
||||
value: declared.0.clone(),
|
||||
}))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -232,6 +232,88 @@ fn require_grammatical_slug(field: &str, declared: &Slug) -> Result<(), MindRefu
|
|||
})
|
||||
})
|
||||
}
|
||||
'@
|
||||
}
|
||||
# Residue S2: N1's door, `require_grammatical_slug`, had no mutation
|
||||
# entry for two of its three callers. `open`'s deletion is covered by
|
||||
# N1's own escape test; `open_with`'s survived all 81 tests before
|
||||
# this batch added one that names it; `require_instance`'s is covered
|
||||
# the same way, since neither caller's test constructs an ungrammatical
|
||||
# name the other way.
|
||||
@{
|
||||
Id = 'N1open'
|
||||
Rule = 'N1: `open` checks the declared name''s grammar before it ever joins a path from it.'
|
||||
Test = 'mind::tests::a_slug_outside_the_grammar_cannot_escape_the_state_root'
|
||||
Command = 'cargo test -p huginn-mind --lib'
|
||||
File = 'crates/huginn-mind/src/mind.rs'
|
||||
Old = @'
|
||||
pub fn open(state_root: &Path, instance: &Slug) -> Result<Self, MindRefusal> {
|
||||
require_grammatical_slug("instance", instance)?;
|
||||
let path = Self::path_for(state_root, instance);
|
||||
'@
|
||||
New = @'
|
||||
pub fn open(state_root: &Path, instance: &Slug) -> Result<Self, MindRefusal> {
|
||||
let path = Self::path_for(state_root, instance);
|
||||
'@
|
||||
}
|
||||
@{
|
||||
Id = 'N1openwith'
|
||||
Rule = 'N1: `open_with` checks the declared name''s grammar too, for every store already in hand and not only for one `open` derived a path for.'
|
||||
Test = 'mind::tests::open_with_refuses_a_declared_instance_outside_the_grammar'
|
||||
Command = 'cargo test -p huginn-mind --lib'
|
||||
File = 'crates/huginn-mind/src/mind.rs'
|
||||
Old = @'
|
||||
pub fn open_with(store: S, instance: &Slug) -> Result<Self, MindRefusal> {
|
||||
require_grammatical_slug("instance", instance)?;
|
||||
let raw = store.pull_all().map_err(unavailable)?;
|
||||
'@
|
||||
New = @'
|
||||
pub fn open_with(store: S, instance: &Slug) -> Result<Self, MindRefusal> {
|
||||
let raw = store.pull_all().map_err(unavailable)?;
|
||||
'@
|
||||
}
|
||||
@{
|
||||
Id = 'N1requireinstance'
|
||||
Rule = 'N1: `require_instance` checks the declared name''s grammar before comparing it to the mind''s own, so an ungrammatical name is refused by name rather than merely being foreign.'
|
||||
Test = 'mind::tests::require_instance_refuses_a_declared_name_outside_the_grammar'
|
||||
Command = 'cargo test -p huginn-mind --lib'
|
||||
File = 'crates/huginn-mind/src/mind.rs'
|
||||
Old = @'
|
||||
pub fn require_instance(&self, declared: &Slug) -> Result<(), MindRefusal> {
|
||||
require_grammatical_slug("declared", declared)?;
|
||||
if declared != self.instance() {
|
||||
'@
|
||||
New = @'
|
||||
pub fn require_instance(&self, declared: &Slug) -> Result<(), MindRefusal> {
|
||||
if declared != self.instance() {
|
||||
'@
|
||||
}
|
||||
@{
|
||||
Id = 'S4fold'
|
||||
Rule = 'Residue S4: the grammar check runs on the declared bytes as given: nothing folds a Unicode hyphen (U+2010) to plain ASCII `-` before asking Slug::validate_slug whether the name is grammatical.'
|
||||
Test = 'mind::tests::a_unicode_hyphen_is_not_folded_before_the_grammar_check'
|
||||
Command = 'cargo test -p huginn-mind --lib'
|
||||
File = 'crates/huginn-mind/src/mind.rs'
|
||||
Old = @'
|
||||
fn require_grammatical_slug(field: &str, declared: &Slug) -> Result<(), MindRefusal> {
|
||||
declared.validate_slug().map_err(|_| {
|
||||
MindRefusal::Document(epiphany_pipeline::PipelineRefusal::InvalidFormat {
|
||||
field: field.into(),
|
||||
value: declared.0.clone(),
|
||||
})
|
||||
})
|
||||
}
|
||||
'@
|
||||
New = @'
|
||||
fn require_grammatical_slug(field: &str, declared: &Slug) -> Result<(), MindRefusal> {
|
||||
let folded = Slug(declared.0.chars().map(|c| if c == '\u{2010}' { '-' } else { c }).collect());
|
||||
folded.validate_slug().map_err(|_| {
|
||||
MindRefusal::Document(epiphany_pipeline::PipelineRefusal::InvalidFormat {
|
||||
field: field.into(),
|
||||
value: declared.0.clone(),
|
||||
})
|
||||
})
|
||||
}
|
||||
'@
|
||||
}
|
||||
@{
|
||||
|
|
@ -451,6 +533,54 @@ fn require_grammatical_slug(field: &str, declared: &Slug) -> Result<(), MindRefu
|
|||
}
|
||||
_ => bytes.len() as u64,
|
||||
},
|
||||
'@
|
||||
}
|
||||
@{
|
||||
Id = 'N4c'
|
||||
Rule = 'Residue S1: what is measured holds for the operation and the runtime id independently, not merely for the two moving together as N4a and N4b probed them; a formula that adds a term for the operation''s length but drops the runtime id -- `228+id+(id>31)+(op.len()-4)*3/2` -- is wrong once a fixture holds the operation long and the runtime id short.'
|
||||
Test = 'serve::tests::the_gates_boundary_holds_when_operation_and_runtime_id_vary_independently'
|
||||
Command = 'cargo test -p huginn-daemon --lib'
|
||||
File = 'crates/huginn-daemon/src/serve.rs'
|
||||
Old = @'
|
||||
let encoded = match encode_cultnet_message_to_vec(&reply, CultNetWireContract::CultNetSchemaV0) {
|
||||
Ok(bytes) => bytes.len() as u64,
|
||||
'@
|
||||
New = @'
|
||||
let encoded = match encode_cultnet_message_to_vec(&reply, CultNetWireContract::CultNetSchemaV0) {
|
||||
Ok(bytes) => match &reply {
|
||||
CultNetMessage::OperationResponse { payload, .. } => {
|
||||
payload.len() as u64
|
||||
+ 228
|
||||
+ message_id.len() as u64
|
||||
+ (message_id.len() > 31) as u64
|
||||
+ (operation.len() as u64).saturating_sub(4) * 3 / 2
|
||||
}
|
||||
_ => bytes.len() as u64,
|
||||
},
|
||||
'@
|
||||
}
|
||||
@{
|
||||
Id = 'N4d'
|
||||
Rule = 'Residue S1: the same, the other way round; a formula that adds a term for the runtime id but drops the operation -- `228+id+(id>31)+3*(rt.len()-16)` -- is wrong once a fixture holds the runtime id long and the operation short.'
|
||||
Test = 'serve::tests::the_gates_boundary_holds_when_operation_and_runtime_id_vary_independently'
|
||||
Command = 'cargo test -p huginn-daemon --lib'
|
||||
File = 'crates/huginn-daemon/src/serve.rs'
|
||||
Old = @'
|
||||
let encoded = match encode_cultnet_message_to_vec(&reply, CultNetWireContract::CultNetSchemaV0) {
|
||||
Ok(bytes) => bytes.len() as u64,
|
||||
'@
|
||||
New = @'
|
||||
let encoded = match encode_cultnet_message_to_vec(&reply, CultNetWireContract::CultNetSchemaV0) {
|
||||
Ok(bytes) => match &reply {
|
||||
CultNetMessage::OperationResponse { payload, .. } => {
|
||||
payload.len() as u64
|
||||
+ 228
|
||||
+ message_id.len() as u64
|
||||
+ (message_id.len() > 31) as u64
|
||||
+ 3 * (runtime_id.len() as u64).saturating_sub(16)
|
||||
}
|
||||
_ => bytes.len() as u64,
|
||||
},
|
||||
'@
|
||||
}
|
||||
@{
|
||||
|
|
|
|||
Loading…
Reference in a new issue