use super::super::workflow_state::*;
use keyhog_core::MerkleLoadStatus;
use std::path::PathBuf;
fn status_missing() -> MerkleLoadStatus {
MerkleLoadStatus::Missing {
path: PathBuf::from("/tmp/cache.json"),
}
}
#[test]
fn merkle_load_transition_maps_every_status_to_exact_evidence() {
use keyhog_profile::CacheLayerKindV2;
let disabled = merkle_load_transition(None);
assert_eq!(disabled.layer, CacheLayerKindV2::Merkle);
assert_eq!(disabled.evidence, "merkle-not-configured");
assert_eq!(disabled.transition, CacheTransition::Disabled);
let missing = merkle_load_transition(Some(&status_missing()));
assert_eq!(missing.evidence, "merkle-load-missing");
assert_eq!(missing.transition, CacheTransition::ColdStart);
let loaded = merkle_load_transition(Some(&MerkleLoadStatus::Loaded {
path: PathBuf::from("/tmp/cache.json"),
entries: 17,
}));
assert_eq!(loaded.evidence, "merkle-load-ok");
assert_eq!(loaded.transition, CacheTransition::WarmLoad);
let failures = [
(
MerkleLoadStatus::ReadFailed {
path: PathBuf::from("/tmp/cache.json"),
error: "io".into(),
},
"merkle-load-read-failed",
),
(
MerkleLoadStatus::ParseFailed {
path: PathBuf::from("/tmp/cache.json"),
error: "json".into(),
},
"merkle-load-parse-failed",
),
(
MerkleLoadStatus::SchemaMismatch {
path: PathBuf::from("/tmp/cache.json"),
version: 1,
expected: 2,
},
"merkle-load-schema-mismatch",
),
(
MerkleLoadStatus::SpecChanged {
path: PathBuf::from("/tmp/cache.json"),
},
"merkle-load-spec-changed",
),
(
MerkleLoadStatus::InvalidEntryHash {
path: PathBuf::from("/tmp/cache.json"),
entry_path: "src/a.rs".into(),
hash: "zz".into(),
},
"merkle-load-invalid-entry-hash",
),
];
for (status, evidence) in failures {
let record = merkle_load_transition(Some(&status));
assert_eq!(record.evidence, evidence);
assert_eq!(
record.transition,
CacheTransition::ColdStart,
"{evidence} must cold-start: the cache is rebuilt, never `unknown`"
);
}
}
#[test]
fn merkle_warmth_refinement_requires_zero_rescans_and_a_real_skip() {
assert_eq!(
refine_merkle_warmth(CacheTransition::WarmLoad, 3, 0),
CacheTransition::SteadyState
);
for (skipped, scanned) in [(0_u64, 0_u64), (0, 2), (3, 1)] {
assert_eq!(
refine_merkle_warmth(CacheTransition::WarmLoad, skipped, scanned),
CacheTransition::WarmLoad,
"skipped={skipped} scanned={scanned} must stay a warm load"
);
}
assert_eq!(
refine_merkle_warmth(CacheTransition::ColdStart, 5, 0),
CacheTransition::ColdStart
);
assert_eq!(
refine_merkle_warmth(CacheTransition::Disabled, 5, 0),
CacheTransition::Disabled
);
}
#[test]
fn autoroute_transition_tracks_persisted_decision_presence() {
let unconfigured = autoroute_transition(None);
assert_eq!(unconfigured.transition, CacheTransition::Disabled);
let dir = tempfile::TempDir::new().expect("tempdir");
let present = dir.path().join("autoroute.json");
std::fs::write(&present, b"{}").expect("write decision cache");
let warm = autoroute_transition(Some(present.as_path()));
assert_eq!(warm.evidence, "autoroute-decision-cache-present");
assert_eq!(warm.transition, CacheTransition::WarmLoad);
let absent = dir.path().join("missing.json");
let cold = autoroute_transition(Some(absent.as_path()));
assert_eq!(cold.evidence, "autoroute-decision-cache-absent");
assert_eq!(cold.transition, CacheTransition::ColdStart);
}
#[test]
fn verifier_transition_warms_only_on_recorded_hits() {
let disabled = verifier_transition(false, 0);
assert_eq!(disabled.evidence, "verifier-policy-disabled");
assert_eq!(disabled.transition, CacheTransition::Disabled);
let cold = verifier_transition(true, 0);
assert_eq!(cold.evidence, "verifier-cache-empty-at-process-start");
assert_eq!(cold.transition, CacheTransition::ColdStart);
let warm = verifier_transition(true, 2);
assert_eq!(warm.evidence, "verifier-cache-hit-spans");
assert_eq!(warm.transition, CacheTransition::WarmLoad);
}
#[test]
fn verification_aggregate_buckets_every_outcome_variant() {
use keyhog_core::VerificationResult;
let results = [
VerificationResult::Live,
VerificationResult::Revoked,
VerificationResult::Dead,
VerificationResult::RateLimited,
VerificationResult::Error("timeout".into()),
VerificationResult::Unverifiable,
VerificationResult::Skipped,
];
let aggregate = aggregate_verification_results(true, results.iter());
assert_eq!(aggregate.queued, 6);
assert_eq!(aggregate.network, 5);
assert_eq!(aggregate.cached, 0);
assert_eq!(aggregate.unverifiable, 1);
assert_eq!(aggregate.skipped, 1);
assert_eq!(aggregate.state_label(), "network");
let disabled = aggregate_verification_results(false, results.iter());
assert_eq!(disabled.queued, 0);
assert_eq!(disabled.network, 0);
assert_eq!(disabled.skipped, 7);
assert_eq!(disabled.state_label(), "disabled");
}
#[test]
fn verification_state_label_precedence_is_exact() {
fn label(enabled: bool, queued: u64, network: u64, cached: u64) -> &'static str {
VerificationAggregate {
enabled,
queued,
network,
cached,
unverifiable: 0,
skipped: 0,
}
.state_label()
}
assert_eq!(label(false, 0, 0, 0), "disabled");
assert_eq!(label(false, 3, 3, 0), "disabled");
assert_eq!(label(true, 0, 0, 0), "idle");
assert_eq!(label(true, 2, 0, 0), "queued");
assert_eq!(label(true, 2, 2, 0), "network");
assert_eq!(label(true, 2, 0, 2), "cached");
assert_eq!(label(true, 3, 1, 2), "mixed");
}
#[test]
fn verifier_cache_hits_count_only_lookup_spans_under_live_verification() {
let identity = keyhog_profile::RunIdentity::new(
"test",
"detectors",
"config",
"filesystem",
"filesystem",
"cpu-fallback",
);
let session = keyhog_profile::Session::start(identity).expect("start session");
let runtime = session.runtime();
{
let _context = runtime.enter();
let verification = keyhog_profile::span(keyhog_profile::Stage::LiveVerification);
let hit = keyhog_profile::span(keyhog_profile::Stage::IncrementalLookup);
drop(hit);
drop(verification);
let merkle_lookup = keyhog_profile::span(keyhog_profile::Stage::IncrementalLookup);
drop(merkle_lookup);
}
let (spans, dropped) = runtime.take_session_span_records();
assert_eq!(dropped, 0, "three spans must fit bounded storage");
assert_eq!(spans.len(), 3);
assert_eq!(count_verifier_cache_hits(&spans), 1);
session.finish(keyhog_profile::RunState::Completed);
}
#[test]
fn partition_sink_is_bounded_with_explicit_loss() {
static PARTITION_TEST_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
let _guard = PARTITION_TEST_LOCK
.lock()
.unwrap_or_else(|error| error.into_inner());
reset_workflow_state();
record_source_partition("filesystem", 3, 120);
record_source_partition("git", 2, 44);
let (records, dropped) = take_source_partitions();
assert_eq!(dropped, 0);
assert_eq!(records.len(), 2);
assert_eq!(records[0].index, 0);
assert_eq!(records[0].kind, "filesystem");
assert_eq!(records[0].units, 3);
assert_eq!(records[0].bytes, 120);
assert_eq!(records[1].index, 1);
assert_eq!(records[1].kind, "git");
let (empty, dropped_again) = take_source_partitions();
assert!(empty.is_empty());
assert_eq!(dropped_again, 0);
reset_workflow_state();
for unit in 0..(MAX_RECORDED_PARTITIONS + 5) {
record_source_partition("filesystem", unit as u64, 0);
}
let (capped, dropped) = take_source_partitions();
assert_eq!(capped.len(), MAX_RECORDED_PARTITIONS);
assert_eq!(dropped, 5, "over-cap records must be counted, never silent");
reset_workflow_state();
}
#[test]
fn daemon_request_identity_parses_generation_and_sequence() {
let identity = parse_daemon_request_identity(
"4242-after-0000000000000000000018d4d3f2a1b0-0000000000000003-0000000000000007",
)
.expect("well-formed request id");
assert_eq!(
identity.generation,
"4242-after-0000000000000000000018d4d3f2a1b0-0000000000000003"
);
assert_eq!(identity.sequence, 7);
for malformed in [
"noseparator",
"-0000000000000007",
"gen-",
"gen-7",
"gen-000000000000000zz",
] {
assert_eq!(
parse_daemon_request_identity(malformed),
None,
"{malformed} must not parse"
);
}
}