use std::path::PathBuf;
use std::sync::atomic::{AtomicU64, Ordering};
use sha2::{Digest, Sha256};
use shepherd_registry::{
DispatchSingletonInput, DispatchSingletonPublicationInput, Error, Registry,
SingletonPublicationState,
};
static NEXT: AtomicU64 = AtomicU64::new(0);
fn fixture(label: &str) -> PathBuf {
#[cfg(target_os = "wasi")]
let fixture_root = PathBuf::from("/tmp");
#[cfg(not(target_os = "wasi"))]
let fixture_root = std::env::temp_dir();
let temp_root =
std::fs::canonicalize(fixture_root).expect("canonicalize isolated fixture root");
loop {
let ordinal = NEXT.fetch_add(1, Ordering::Relaxed);
let root = temp_root.join(format!("shepherd-registry-correction-{label}-{ordinal}"));
match std::fs::create_dir(&root) {
Ok(()) => return root,
Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => continue,
Err(error) => panic!("create isolated fixture {}: {error}", root.display()),
}
}
}
#[test]
fn fixture_allocator_is_process_api_free_and_collision_safe() {
let source = include_str!("correction.rs");
let forbidden_namespaces = [
["std", "::", "process"].concat(),
["process", "::"].concat(),
];
assert!(
forbidden_namespaces
.iter()
.all(|namespace| !source.contains(namespace)),
"WASI registry tests must not depend on ambient process identity"
);
let first = fixture("allocator");
let second = fixture("allocator");
assert_ne!(first, second);
std::fs::remove_dir(&first).expect("remove first allocator fixture");
std::fs::remove_dir(&second).expect("remove second allocator fixture");
}
fn seed_project(registry: &Registry) {
registry
.execute(
"INSERT INTO projects (id, name, created_at, updated_at) VALUES (?1, ?2, ?3, ?3)",
("0192f6e8-7b2c-7abc-8def-0123456789ab", "fixture", 1_i64),
)
.expect("insert fixture project");
}
fn claim(agent_id: &str, resumes_agent_id: Option<&str>) -> DispatchSingletonInput {
DispatchSingletonInput {
project_id: "0192f6e8-7b2c-7abc-8def-0123456789ab".into(),
run_id: "v657".into(),
role: "engineer".into(),
lane_id: None,
agent_id: agent_id.into(),
harness: "claude".into(),
agent_type: "shepherd:engineer".into(),
parent_agent_id: None,
session_id: format!("session-{agent_id}"),
write_scope: vec![".shepherd/runs/v657/plan.md".into()],
claimed_at: 1,
resumes_agent_id: resumes_agent_id.map(str::to_owned),
}
}
fn publication_input(
nonce: &str,
claim: DispatchSingletonInput,
) -> DispatchSingletonPublicationInput {
let record_json = format!(
"{{\"schema\":\"shepherd.dispatch/3\",\"agent_id\":\"{}\"}}",
claim.agent_id
);
let record_sha256 = Sha256::digest(record_json.as_bytes())
.iter()
.map(|byte| format!("{byte:02x}"))
.collect();
DispatchSingletonPublicationInput {
nonce: nonce.into(),
record_path: format!("v657/dispatch/{}.json", claim.agent_id),
claim,
record_sha256,
record_json,
prepared_at: 1,
}
}
#[test]
fn migration_rejects_a_missing_baseline_object_when_version_one_is_unrecorded() {
let root = fixture("baseline-tamper");
let path = root.join("shepherd.db");
let registry = Registry::open_migrated(&path).expect("migrate registry");
registry
.execute("DELETE FROM schema_versions WHERE version = 1", ())
.expect("remove baseline ledger row");
registry
.execute("DROP TABLE projects", ())
.expect("tamper baseline object");
let error = registry
.apply_migrations()
.expect_err("missing baseline object must fail closed");
assert!(matches!(
error,
Error::MigrationPostcondition { version: 1, .. }
));
drop(registry);
std::fs::remove_dir_all(root).expect("cleanup");
}
#[test]
fn migration_rejects_a_catalog_object_with_the_wrong_type() {
let root = fixture("wrong-object-type");
let path = root.join("shepherd.db");
let registry = Registry::open_migrated(&path).expect("migrate registry");
registry
.execute("DROP VIEW v_cache_usage", ())
.expect("drop expected view");
registry
.execute("CREATE TABLE v_cache_usage (sentinel TEXT)", ())
.expect("replace view with table");
let error = registry
.apply_migrations()
.expect_err("wrong catalog object type must fail closed");
assert!(matches!(
error,
Error::MigrationPostcondition { version: 6, .. }
));
drop(registry);
std::fs::remove_dir_all(root).expect("cleanup");
}
#[test]
fn migration_rejects_a_recorded_checksum_mutation() {
let root = fixture("checksum");
let path = root.join("shepherd.db");
let registry = Registry::open_migrated(&path).expect("migrate registry");
registry
.execute(
"INSERT INTO projects (id, name, created_at, updated_at) VALUES (?1, ?2, ?3, ?3)",
("0192f6e8-7b2c-7abc-8def-0123456789ab", "fixture", 1_i64),
)
.expect("insert fixture project");
registry
.execute(
"UPDATE schema_versions SET checksum = ?1 WHERE version = 22",
["forged-checksum"],
)
.expect("mutate recorded checksum");
let error = registry
.apply_migrations()
.expect_err("checksum drift must fail closed");
assert!(matches!(
error,
Error::MigrationChecksum { version: 22, .. }
));
drop(registry);
std::fs::remove_dir_all(root).expect("cleanup");
}
#[test]
fn migration_rejects_a_missing_postcondition_even_when_version_is_recorded() {
let root = fixture("postcondition");
let path = root.join("shepherd.db");
let registry = Registry::open_migrated(&path).expect("migrate registry");
registry
.execute(
"INSERT INTO projects (id, name, created_at, updated_at) VALUES (?1, ?2, ?3, ?3)",
("0192f6e8-7b2c-7abc-8def-0123456789ab", "fixture", 1_i64),
)
.expect("insert fixture project");
registry
.execute("DROP TABLE dispatch_singleton_claims", ())
.expect("corrupt postcondition");
let error = registry
.apply_migrations()
.expect_err("missing schema object must fail closed");
assert!(matches!(
error,
Error::MigrationPostcondition { version: 22, .. }
));
drop(registry);
std::fs::remove_dir_all(root).expect("cleanup");
}
#[test]
fn singleton_publication_rejects_noncanonical_ids_before_sql() {
let root = fixture("invalid-id");
let path = root.join("shepherd.db");
let mut registry = Registry::open_migrated(&path).expect("migrate registry");
let mut invalid = claim("engineer-a", None);
invalid.project_id = "project-1".into();
let error = registry
.transaction_immediate::<_, Error, _>(|tx| {
tx.prepare_dispatch_singleton(&publication_input("nonce-invalid", invalid))
})
.expect_err("noncanonical project id must be refused");
assert!(matches!(error, Error::InvalidDispatchClaim(_)));
drop(registry);
std::fs::remove_dir_all(root).expect("cleanup");
}
#[test]
fn singleton_publication_is_nonce_keyed_and_replayable() {
let root = fixture("publication");
let path = root.join("shepherd.db");
let mut registry = Registry::open_migrated(&path).expect("migrate registry");
registry
.execute(
"INSERT INTO projects (id, name, created_at, updated_at) VALUES (?1, ?2, ?3, ?3)",
("0192f6e8-7b2c-7abc-8def-0123456789ab", "fixture", 1_i64),
)
.expect("insert fixture project");
let prepared = registry
.transaction_immediate::<_, Error, _>(|tx| {
tx.prepare_dispatch_singleton(&publication_input(
"nonce-aaa",
claim("engineer-a", None),
))
})
.expect("prepare publication");
assert_eq!(prepared.state, SingletonPublicationState::Preparing);
assert_eq!(prepared.nonce, "nonce-aaa");
let loaded = registry
.load_dispatch_publication("nonce-aaa")
.expect("load publication")
.expect("publication exists");
assert_eq!(loaded.state, SingletonPublicationState::Preparing);
assert_eq!(loaded.record_path, "v657/dispatch/engineer-a.json");
registry
.transaction_immediate::<_, Error, _>(|tx| {
tx.mark_dispatch_singleton_published("nonce-aaa", 2)
})
.expect("replay marks publication published");
let published = registry
.load_dispatch_publication("nonce-aaa")
.expect("load published")
.expect("published row");
assert_eq!(published.state, SingletonPublicationState::Published);
registry
.transaction_immediate::<_, Error, _>(|tx| {
tx.quarantine_dispatch_singleton("nonce-aaa", "corrupt", 3)
})
.expect("corruption quarantine is durable");
let quarantined = registry
.load_dispatch_publication("nonce-aaa")
.expect("load quarantined")
.expect("quarantine row");
assert_eq!(quarantined.state, SingletonPublicationState::Quarantined);
assert_eq!(quarantined.quarantine_reason.as_deref(), Some("corrupt"));
let replay_error = registry
.transaction_immediate::<_, Error, _>(|tx| {
tx.prepare_dispatch_singleton(&publication_input(
"nonce-aaa",
claim("engineer-a", None),
))
})
.expect_err("a quarantined nonce cannot be reanimated");
assert!(matches!(
replay_error,
Error::SingletonPublicationConflict { .. }
));
drop(registry);
std::fs::remove_dir_all(root).expect("cleanup");
}
#[test]
fn a_nonce_replay_mismatch_is_quarantined_and_releases_the_claim() {
let root = fixture("nonce-mismatch");
let path = root.join("shepherd.db");
let mut registry = Registry::open_migrated(&path).expect("migrate registry");
seed_project(®istry);
registry
.transaction_immediate::<_, Error, _>(|tx| {
tx.prepare_dispatch_singleton(&publication_input(
"nonce-mismatch",
claim("engineer-a", None),
))
})
.expect("prepare original publication");
let mut mismatch = publication_input("nonce-mismatch", claim("engineer-a", None));
mismatch.record_json = "{\"schema\":\"shepherd.dispatch/3\",\"forged\":true}".into();
mismatch.record_sha256 = Sha256::digest(mismatch.record_json.as_bytes())
.iter()
.map(|byte| format!("{byte:02x}"))
.collect();
let error = registry
.transaction_immediate::<_, Error, _>(|tx| tx.prepare_dispatch_singleton(&mismatch))
.expect_err("nonce reuse with different bytes must fail");
assert!(matches!(error, Error::SingletonPublicationConflict { .. }));
let publication = registry
.load_dispatch_publication("nonce-mismatch")
.expect("load quarantined publication")
.expect("publication remains audit history");
assert_eq!(publication.state, SingletonPublicationState::Quarantined);
assert!(
registry
.load_dispatch_singleton(
"0192f6e8-7b2c-7abc-8def-0123456789ab",
"v657",
"engineer",
"__run__"
)
.expect("load current claim")
.is_none()
);
drop(registry);
std::fs::remove_dir_all(root).expect("cleanup");
}
#[test]
fn loaded_claim_and_publication_rows_reject_tampered_facts() {
let root = fixture("loaded-tamper");
let path = root.join("shepherd.db");
let mut registry = Registry::open_migrated(&path).expect("migrate registry");
seed_project(®istry);
registry
.transaction_immediate::<_, Error, _>(|tx| {
tx.prepare_dispatch_singleton(&publication_input(
"nonce-tamper",
claim("engineer-a", None),
))
})
.expect("prepare publication");
registry
.execute(
"UPDATE dispatch_singleton_claims SET identity_fingerprint = ?1 WHERE publication_nonce = ?2",
("0".repeat(64), "nonce-tamper"),
)
.expect("tamper claim fingerprint");
assert!(matches!(
registry.load_dispatch_singleton(
"0192f6e8-7b2c-7abc-8def-0123456789ab",
"v657",
"engineer",
"__run__"
),
Err(Error::InvalidDispatchClaim(_))
));
registry
.execute(
"UPDATE dispatch_singleton_publications SET record_json = ?1 WHERE nonce = ?2",
("{\"schema\":\"tampered\"}", "nonce-tamper"),
)
.expect("tamper publication bytes");
assert!(matches!(
registry.load_dispatch_publication("nonce-tamper"),
Err(Error::InvalidSingletonPublication(_))
));
drop(registry);
std::fs::remove_dir_all(root).expect("cleanup");
}
#[test]
fn a_preparing_singleton_reserves_the_logical_key_until_quarantined() {
let root = fixture("reservation");
let path = root.join("shepherd.db");
let mut registry = Registry::open_migrated(&path).expect("migrate registry");
registry
.execute(
"INSERT INTO projects (id, name, created_at, updated_at) VALUES (?1, ?2, ?3, ?3)",
("0192f6e8-7b2c-7abc-8def-0123456789ab", "fixture", 1_i64),
)
.expect("insert fixture project");
registry
.transaction_immediate::<_, Error, _>(|tx| {
tx.prepare_dispatch_singleton(&publication_input(
"nonce-aaa",
claim("engineer-a", None),
))
})
.expect("prepare first owner");
let conflict = registry
.transaction_immediate::<_, Error, _>(|tx| {
tx.prepare_dispatch_singleton(&publication_input(
"nonce-bbb",
claim("engineer-b", None),
))
})
.expect_err("a second owner cannot bypass a preparing row");
assert!(matches!(conflict, Error::DispatchClaimConflict { .. }));
registry
.transaction_immediate::<_, Error, _>(|tx| {
tx.quarantine_dispatch_singleton("nonce-aaa", "injected", 2)
})
.expect("quarantine first owner");
let replacement = registry
.transaction_immediate::<_, Error, _>(|tx| {
tx.prepare_dispatch_singleton(&publication_input(
"nonce-bbb",
claim("engineer-b", None),
))
})
.expect("quarantined owner no longer reserves key");
assert_eq!(replacement.nonce, "nonce-bbb");
drop(registry);
std::fs::remove_dir_all(root).expect("cleanup");
}
#[test]
fn standalone_resume_clears_the_prior_agent_publication_pointer() {
let root = fixture("resume-publication");
let path = root.join("shepherd.db");
let mut registry = Registry::open_migrated(&path).expect("migrate registry");
seed_project(®istry);
registry
.transaction_immediate::<_, Error, _>(|transaction| {
transaction.prepare_dispatch_singleton(&publication_input(
"nonce-old",
claim("engineer-a", None),
))
})
.expect("prepare original publication");
registry
.transaction_immediate::<_, Error, _>(|transaction| {
transaction.mark_dispatch_singleton_published("nonce-old", 2)
})
.expect("publish original claim");
registry
.transaction_immediate::<_, Error, _>(|transaction| {
transaction.claim_dispatch_singleton(&claim("engineer-b", Some("engineer-a")))
})
.expect("resume singleton");
let current = registry
.load_dispatch_singleton(
"0192f6e8-7b2c-7abc-8def-0123456789ab",
"v657",
"engineer",
"__run__",
)
.expect("load resumed singleton")
.expect("current singleton");
assert_eq!(current.agent_id, "engineer-b");
assert_eq!(current.resumed_from_agent_id.as_deref(), Some("engineer-a"));
assert_eq!(current.publication_nonce, None);
assert_eq!(current.publication_state, None);
assert_eq!(current.record_path, None);
drop(registry);
std::fs::remove_dir_all(root).expect("cleanup");
}