Close Soul's Cut 5 findings: pin one handle, and every write

Soul mutated the collapsed commit owner twice and the suite survived both.
Neither rule was pinned; both are now, each by a test that fails under its own
mutation, and both mutations join the suite so the gap cannot reopen.

S7, a regression from base. The owner resolves the backing store once and
reuses it for the cache load, the CAS and the conflict re-read, and its doc
comment says no path in the function can target a second store. Re-deriving the
CAS handle from the same path left the whole suite green. The old profile test
had caught it only because its store was deliberately not the commit path; the
replacement asserted the temp dir holds one store, which a same-path
re-derivation satisfies exactly. A handle carries nothing but its path today,
so no assertion about files or contents can see the second one. The `.redb` arm
makes the difference real -- redb permits one writable handle per path -- and
the map plans redb for the organ's minds.

So the probe is the resolution itself: `runtime_spine_backing_store` records
every path it hands out, under `cfg(test)` only, and
`mind_commit_reads_and_writes_one_store` asserts a commit resolves its store
exactly once. The log is per-path and compared as a delta, so tests running in
parallel cannot see each other. Mutation MS1 re-derives from the same path and
the count reads 2.

S8, pre-existing. Narrowing validation to the first write also survived,
because no test committed a batch carrying more than one distinct write. The
existing mutation neuters validation for every write, which any single-write
batch catches; nothing covered the loop's reach. Added
`mind_commit_validates_every_write_in_the_batch`: two distinct writes, the
second refused, and the batch is refused whole with nothing written -- the
valid first write included. Mutation MS2 takes the first write only and the
test gets a commit where it expected a refusal.

M2's rule text is corrected to match what it actually pins, now that MS2 owns
the stronger half.

S10, cosmetic. The renamed test's fixture still said "mind-profile-test" after
the profile it named was deleted. The pinned receipt digest does not depend on
it and is unchanged.

Builds clean: 172 tests, 0 warnings. Seven mutations, each killed by its own
test.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-09-16 11:21:16 +02:00
commit 617c397d88
3 changed files with 152 additions and 5 deletions

View file

@ -2564,6 +2564,62 @@ mod tests {
Ok(())
}
/// Every write in the batch is validated, not just the first. A batch of
/// two distinct writes whose second is refused is refused whole, with
/// nothing written.
#[test]
fn mind_commit_validates_every_write_in_the_batch() -> Result<()> {
let temp = tempdir()?;
let store = temp.path().join("mind.cc");
initialize_runtime_spine(
&store,
RuntimeSpineInitOptions {
runtime_id: "mind-batch-validation-test".into(),
display_name: "Mind batch validation test".into(),
created_at: "2026-09-15T00:00:00Z".into(),
},
)?;
let value = crate::EpiphanyObservation {
id: "first".into(),
summary: "one".into(),
source_kind: "test".into(),
status: "accepted".into(),
code_refs: Vec::new(),
evidence_ids: Vec::new(),
};
let accepted = runtime_spine_cache(&store)?
.prepare_entry("first", &crate::EpiphanyMindObservationDocument { value })?
.0;
let refused = refused_mind_write(&store)?;
assert_ne!(
(&accepted.r#type, &accepted.key),
(&refused.r#type, &refused.key),
"the two writes carry distinct identities, so uniqueness cannot answer first"
);
let provenance = EpiphanyMindDocumentVersion::from_envelope("epiphany-organ", &accepted)?;
let before = std::fs::read(&store)?;
let error = commit_external_typed_observation_mind_mutation(
&store,
"test-organ",
provenance,
"test-owner",
Vec::new(),
vec![accepted, refused],
"2026-09-15T00:00:01Z",
)
.unwrap_err();
assert!(
error.to_string().contains("semantic identity"),
"the second write is validated too: {error}"
);
assert_eq!(
std::fs::read(&store)?,
before,
"a refused batch writes nothing, including its valid writes"
);
Ok(())
}
/// Ruling 12 again: identity uniqueness is checked before write validation,
/// so a batch that repeats an identity reports the repeat, not the
/// validator's refusal. The order is fail-closed either way; this pins
@ -2605,6 +2661,12 @@ mod tests {
/// the cache load, the batch CAS and the conflict re-read. A commit that
/// wrote anywhere else would leave a second store beside this one, and a
/// replay read from anywhere else would not find the receipt.
///
/// The file assertions below cannot see a handle re-derived from the *same*
/// path, because a handle carries nothing but its path. The resolution
/// count can, and that is the shape the map's redb plan makes load-bearing:
/// redb permits one writable handle per path, so a second derivation is a
/// second handle there.
#[test]
fn mind_commit_reads_and_writes_one_store() -> Result<()> {
let temp = tempdir()?;
@ -2640,9 +2702,16 @@ mod tests {
at,
)
};
let resolutions_before =
crate::runtime_store_backend::backing_store_resolutions(&store);
let EpiphanyMindCommitOutcome::Committed(receipt) = commit("2026-09-15T00:00:01Z")? else {
panic!("the first Mind write must commit");
};
assert_eq!(
crate::runtime_store_backend::backing_store_resolutions(&store) - resolutions_before,
1,
"the commit resolves the backing store once and reuses that handle"
);
let mut files = std::fs::read_dir(temp.path())?
.map(|entry| Ok(entry?.file_name().to_string_lossy().into_owned()))
.collect::<Result<Vec<_>>>()?;
@ -2725,8 +2794,8 @@ mod tests {
initialize_runtime_spine(
&store,
RuntimeSpineInitOptions {
runtime_id: "mind-profile-test".into(),
display_name: "Mind profile test".into(),
runtime_id: "mind-commit-keeps-test".into(),
display_name: "Mind commit keeps test".into(),
created_at: "2026-09-15T00:00:00Z".into(),
},
)?;

View file

@ -108,7 +108,11 @@ impl CacheBackingStore for RuntimeSpineBackingStore {
pub(crate) fn runtime_spine_backing_store(path: &Path) -> Result<RuntimeSpineBackingStore> {
match path.extension().and_then(|extension| extension.to_str()) {
Some("cc" | "msgpack" | "redb") => Ok(RuntimeSpineBackingStore::new(path)),
Some("cc" | "msgpack" | "redb") => {
#[cfg(test)]
record_backing_store_resolution(path);
Ok(RuntimeSpineBackingStore::new(path))
}
extension => Err(anyhow!(
"runtime spine store must use an explicit .cc, .msgpack, or .redb extension; got {:?}",
extension
@ -116,6 +120,39 @@ pub(crate) fn runtime_spine_backing_store(path: &Path) -> Result<RuntimeSpineBac
}
}
/// Every resolution this process has made, in order, recorded only under test.
///
/// A handle carries nothing but its path today, so a second handle to the same
/// path is behaviourally invisible and no assertion about files or contents can
/// see it. The `.redb` arm makes that difference real: redb permits one
/// writable handle per path. This log is the probe that lets a caller pin
/// "resolved once and reused" rather than the much weaker "only one store was
/// written".
#[cfg(test)]
static RESOLVED_BACKING_STORES: std::sync::Mutex<Vec<PathBuf>> =
std::sync::Mutex::new(Vec::new());
#[cfg(test)]
fn record_backing_store_resolution(path: &Path) {
RESOLVED_BACKING_STORES
.lock()
.expect("the backing-store resolution log is never poisoned")
.push(path.to_path_buf());
}
/// How many times this path has been resolved into a handle. Tests run in
/// parallel, so a caller compares this around its own call against its own
/// temporary path rather than reading a global total.
#[cfg(test)]
pub(crate) fn backing_store_resolutions(path: &Path) -> usize {
RESOLVED_BACKING_STORES
.lock()
.expect("the backing-store resolution log is never poisoned")
.iter()
.filter(|resolved| resolved.as_path() == path)
.count()
}
#[cfg(test)]
mod tests {
use super::*;

View file

@ -17,8 +17,12 @@
# Requires CARGO_TARGET_DIR to be set the way the cut ran it, or it will build
# into the repo-local target/.
#
# M1-M5 are the collapse's own rules. MS1 and MS2 close Soul's findings S7 and
# S8: each is a mutation the suite used to survive, kept here so the gap cannot
# reopen.
#
# This suite differs from Cut 4's in one way: an entry carries a list of edits
# rather than a single Old/New pair, because two of the five rules are about
# rather than a single Old/New pair, because two of the seven rules are about
# the *order* of two statements. Removing a statement and reinserting it
# elsewhere is two edits and cannot be expressed as one replacement. Every edit
# is still asserted to match exactly once.
@ -56,7 +60,7 @@ $mutations = @(
},
@{
Id = 'M2'
Rule = 'Every write in the batch is validated against the Mind document rules.'
Rule = 'The batch is validated against the Mind document rules at all. MS2 pins that every write is.'
Test = 'reasoning_context::tests::mind_commit_keeps_validation_receipts_replay_and_conflicts'
Edits = @(
@{
@ -132,6 +136,43 @@ $mutations = @(
for write in &writes {
crate::mind_documents::validate_mind_write_envelope(write)?;
}
'@
}
)
},
# Soul's finding S7. M3 sends the CAS to a *different* path, which leaves a
# second file the one-store test can see. This sends it to the same path,
# which leaves no trace in the directory at all and survived M3's test. It is
# still a second handle, and the map's redb plan makes that fatal: redb
# permits one writable handle per path.
@{
Id = 'MS1'
Rule = 'The owner resolves the backing store once and reuses that handle, rather than re-deriving it.'
Test = 'reasoning_context::tests::mind_commit_reads_and_writes_one_store'
Edits = @(
@{
File = $context
Old = ' if backing_store.compare_and_swap_batch(&expected, replacements)? {'
New = ' if runtime_spine_backing_store(store_path)?.compare_and_swap_batch(&expected, replacements)? {'
}
)
},
# Soul's finding S8. M2 neuters validation for every write, which any
# single-write batch catches. This keeps validation and narrows it to the
# first write, which nothing caught until a batch of two carried an invalid
# second write.
@{
Id = 'MS2'
Rule = 'Every write in the batch is validated, not just the first.'
Test = 'reasoning_context::tests::mind_commit_validates_every_write_in_the_batch'
Edits = @(
@{
File = $context
Old = $validationLoop
New = @'
for write in writes.iter().take(1) {
crate::mind_documents::validate_mind_write_envelope(write)?;
}
'@
}
)