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:
parent
bf990d1869
commit
617c397d88
3 changed files with 152 additions and 5 deletions
|
|
@ -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(),
|
||||
},
|
||||
)?;
|
||||
|
|
|
|||
|
|
@ -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::*;
|
||||
|
|
|
|||
|
|
@ -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)?;
|
||||
}
|
||||
'@
|
||||
}
|
||||
)
|
||||
|
|
|
|||
Reference in a new issue