huginn-mind: tighten the refusal and read-back tests (Soul s1 findings)
snapshot_admits_and_reads_back now counts pipeline envelopes apart from the loop, asserts validate() and pipeline_key == stored key for every one, admits a resolution for every stuck finding and a withdrawal for every unwithdrawable resolution, and fails with the id named on any refusal. The refusal tests assert the exact leaf refusal (field and value from pipeline_key) at Mind::admit and at the helper, and eureka-state gains a tool-level test that the typed refusal reaches the result intact. Tests only; no production code changes. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
790b6cd4f6
commit
f29a7ce7f0
3 changed files with 84 additions and 30 deletions
|
|
@ -11,7 +11,7 @@ use std::time::Duration;
|
|||
|
||||
use huginn_mind::envelope::{OperationFailure, encode_failure, encode_response};
|
||||
use huginn_mind::{HuginnMindRequest, HuginnMindResponse};
|
||||
use huginn_mind::epiphany_pipeline::{PipelineKind, PipelineRef, Short};
|
||||
use huginn_mind::epiphany_pipeline::{Date, PipelineDocument, PipelineKind, PipelineRef, Short};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
mod common;
|
||||
|
|
@ -530,3 +530,35 @@ fn initialize_advertises_tools() {
|
|||
let mcp = Mcp::at(INSTANCE, closed_port());
|
||||
assert!(mcp.init["capabilities"]["tools"].is_object(), "{}", mcp.init);
|
||||
}
|
||||
|
||||
/// A document the leaf cannot key is the client's fault, and the tool result
|
||||
/// carries the leaf's own refusal: the same field and the same offending
|
||||
/// value `pipeline_key` computes for that document, never a re-built one.
|
||||
#[test]
|
||||
fn admit_carries_the_leaf_key_refusal_with_its_field_and_value() {
|
||||
use huginn_mind::epiphany_pipeline::{PipelineResolution, ResolutionOutcome, pipeline_key};
|
||||
|
||||
let (root, daemon) = mind(vec![vec![stewardship()]]);
|
||||
let server = serve(root, daemon);
|
||||
let mut mcp = Mcp::at(INSTANCE, server.addr);
|
||||
// A resolution of a resolution of a resolution: it validates and has no
|
||||
// room left in the key for a third nesting.
|
||||
let subject = format!("{CAMPAIGN}:resolution:{}.{}", "x".repeat(60), "y".repeat(40));
|
||||
let document = PipelineDocument::Resolution(PipelineResolution {
|
||||
subject: PipelineRef { kind: PipelineKind::Resolution, id: Short(subject) },
|
||||
sequence: 1,
|
||||
outcome: ResolutionOutcome::Withdrawn { reason: "moot".into() },
|
||||
rationale: "Resolved.".into(),
|
||||
resolved_on: Date("2026-09-29".into()),
|
||||
});
|
||||
document.validate().unwrap();
|
||||
let leaf = pipeline_key(&document).expect_err("the fixture must not key");
|
||||
let expected = serde_json::to_value(&leaf).unwrap();
|
||||
assert_eq!(expected["InvalidFormat"]["field"], json!("resolution.key"));
|
||||
|
||||
let (error, refused) = mcp.call("admit", faculty_args(vec![serde_json::to_value(&document).unwrap()]));
|
||||
assert!(!error, "{refused}");
|
||||
assert_eq!(refused["Refused"]["Document"], expected, "{refused}");
|
||||
let (_, whoami) = mcp.call("whoami", json!({}));
|
||||
assert_eq!(whoami["status"]["receipts"], json!(2), "only the seed is committed: {whoami}");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1698,14 +1698,16 @@ mod tests {
|
|||
seed(&mut mind);
|
||||
let before = store.rows();
|
||||
let receipts = receipt_count(&mind);
|
||||
for document in unkeyable() {
|
||||
for (document, field) in unkeyable().into_iter().zip(["finding.key", "resolution.key"]) {
|
||||
document.validate().unwrap();
|
||||
match refusal(admit(&mut mind, vec![document])) {
|
||||
MindRefusal::Document(PipelineRefusal::InvalidFormat { field, .. }) => {
|
||||
assert!(field.ends_with(".key"), "{field}")
|
||||
}
|
||||
other => panic!("{other:?}"),
|
||||
}
|
||||
// The leaf owns what a key error says; admission must carry it
|
||||
// unchanged, field and offending value included.
|
||||
let leaf = pipeline_key(&document).expect_err("the fixture must not key");
|
||||
assert!(
|
||||
matches!(&leaf, PipelineRefusal::InvalidFormat { field: named, value } if named == field && !value.is_empty()),
|
||||
"{leaf:?}"
|
||||
);
|
||||
assert_eq!(refusal(admit(&mut mind, vec![document])), MindRefusal::Document(leaf), "{field}");
|
||||
assert_eq!(store.rows(), before);
|
||||
assert_eq!(receipt_count(&mind), receipts);
|
||||
}
|
||||
|
|
@ -1721,10 +1723,8 @@ mod tests {
|
|||
let keyable = question("Q1", &["A", "B"], "A");
|
||||
assert_eq!(super::prepare(&keyable, &cache).unwrap(), prepare(&keyable));
|
||||
for document in unkeyable() {
|
||||
assert!(matches!(
|
||||
super::prepare(&document, &cache),
|
||||
Err(MindRefusal::Document(PipelineRefusal::InvalidFormat { .. }))
|
||||
));
|
||||
let leaf = pipeline_key(&document).expect_err("the fixture must not key");
|
||||
assert_eq!(super::prepare(&document, &cache).unwrap_err(), MindRefusal::Document(leaf));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -788,17 +788,27 @@ mod tests {
|
|||
#[test]
|
||||
#[ignore = "reads and writes the scratch copy of a live state root named by HUGINN_MIND_SNAPSHOT"]
|
||||
fn snapshot_admits_and_reads_back() {
|
||||
use crate::fixtures::{admit, committed, r, resolution, withdrawn};
|
||||
use crate::fixtures::{admit, r, resolution, withdrawn};
|
||||
use epiphany_pipeline::{PipelineRef, ResolutionOutcome, Short, pipeline_key};
|
||||
|
||||
let root = std::env::var("HUGINN_MIND_SNAPSHOT").expect("HUGINN_MIND_SNAPSHOT names a scratch copy of the state root");
|
||||
let mut mind = Mind::open(Path::new(&root), &slug("eureka")).unwrap();
|
||||
|
||||
// The census is taken apart from the loop that checks each document.
|
||||
let pipeline_envelopes = mind
|
||||
.envelopes()
|
||||
.filter(|envelope| PipelineKind::ALL.iter().any(|kind| kind.type_id() == envelope.r#type))
|
||||
.count();
|
||||
assert!(pipeline_envelopes > 0, "the snapshot held no pipeline document");
|
||||
|
||||
let mut viewed = 0;
|
||||
let mut resolutions = Vec::new();
|
||||
for envelope in mind.envelopes() {
|
||||
let Some(kind) = PipelineKind::ALL.iter().copied().find(|kind| kind.type_id() == envelope.r#type) else { continue };
|
||||
let stored = PipelineDocument::decode(envelope).unwrap();
|
||||
stored.validate().unwrap_or_else(|refusal| panic!("{} no longer validates: {refusal:?}", envelope.key));
|
||||
let rekeyed = pipeline_key(&stored).unwrap_or_else(|refusal| panic!("{} no longer keys: {refusal:?}", envelope.key));
|
||||
assert_eq!(rekeyed, envelope.key, "stored key differs from the key the leaf computes");
|
||||
let view = mind
|
||||
.view(&PipelineRef { kind, id: Short(envelope.key.clone()) })
|
||||
.unwrap()
|
||||
|
|
@ -812,38 +822,50 @@ mod tests {
|
|||
}
|
||||
viewed += 1;
|
||||
}
|
||||
assert!(viewed > 0, "the snapshot held no pipeline document");
|
||||
println!("viewed {viewed} stored documents unchanged");
|
||||
assert_eq!(viewed, pipeline_envelopes, "every pipeline envelope validates, rekeys and views back");
|
||||
println!("validated, rekeyed and viewed {viewed} stored documents unchanged");
|
||||
|
||||
for local in [
|
||||
let stuck = [
|
||||
"cut-bifrost-retire-alarm.s2.verb-default-accepts-malformed",
|
||||
"cut-ops-notice-deploy.s2.pinned-bifrost-predates-retry",
|
||||
"cut-bifrost-notice-retry.s1.unknown-test-ignores-backoff",
|
||||
"cut-bifrost-notice-retry.s2.closed-unknown-retry-unpinned",
|
||||
"cut-bifrost-notice-retry.s2.flapping-clock-retries-every-tick",
|
||||
"cut-idunn-topology-lock.s2.boot-reconcile-skipped-on-contention",
|
||||
] {
|
||||
];
|
||||
let mut resolved = 0;
|
||||
for local in stuck {
|
||||
let subject = r(PipelineKind::Finding, &format!("idunn-watchdog:finding:{local}"));
|
||||
let outcome = admit(&mut mind, vec![resolution(subject, ResolutionOutcome::Recorded { reason: "snapshot read-back".into() })]);
|
||||
let (_, writes) = committed(outcome);
|
||||
assert_eq!(writes.len(), 1, "{local}");
|
||||
match outcome {
|
||||
PipelineAdmissionOutcome::Committed { writes, .. } => {
|
||||
assert_eq!(writes.len(), 1, "{local}");
|
||||
resolved += 1;
|
||||
}
|
||||
other => panic!("stuck finding {local} was not resolved: {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
let mut tried = 0;
|
||||
let mut withdrew = None;
|
||||
assert_eq!(resolved, stuck.len(), "every stuck finding resolves");
|
||||
|
||||
let mut candidates = Vec::new();
|
||||
for key in resolutions {
|
||||
let withdrawal = resolution(r(PipelineKind::Resolution, &key), withdrawn());
|
||||
let withdrawal_key = pipeline_key(&withdrawal).unwrap();
|
||||
if withdrawal_key.rsplit(':').next().unwrap().len() <= 64 {
|
||||
continue;
|
||||
}
|
||||
tried += 1;
|
||||
if let PipelineAdmissionOutcome::Committed { .. } = admit(&mut mind, vec![withdrawal]) {
|
||||
withdrew = Some(withdrawal_key);
|
||||
break;
|
||||
let withdrawal_key = pipeline_key(&withdrawal).unwrap_or_else(|refusal| panic!("withdrawal of {key} does not key: {refusal:?}"));
|
||||
if withdrawal_key.rsplit(':').next().unwrap().len() > 64 {
|
||||
candidates.push((key, withdrawal));
|
||||
}
|
||||
}
|
||||
let withdrew = withdrew.unwrap_or_else(|| panic!("no stored resolution with a withdrawal key over 64 bytes withdrew; tried {tried}"));
|
||||
println!("withdrew a resolution under a {}-byte key", withdrew.len());
|
||||
assert!(!candidates.is_empty(), "no stored resolution had a withdrawal key over 64 bytes");
|
||||
let wanted = candidates.len();
|
||||
let mut withdrew = 0;
|
||||
for (key, withdrawal) in candidates {
|
||||
match admit(&mut mind, vec![withdrawal]) {
|
||||
PipelineAdmissionOutcome::Committed { .. } => withdrew += 1,
|
||||
other => panic!("withdrawal of {key} was not committed: {other:?}"),
|
||||
}
|
||||
}
|
||||
assert_eq!(withdrew, wanted, "every candidate withdraws");
|
||||
println!("withdrew {withdrew} resolutions whose withdrawal keys exceed 64 bytes");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue