keyhog 0.5.73

GPU-accelerated secret scanner for code, Git history, cloud, containers, browser assets, and live credential verification
use super::protocol::{Response, WarmBackendIdentity, WarmBackendStatus};
use super::server::warm_route_error;
use super::warm_identity::{evaluate_status, validate_for_client};

fn identity() -> WarmBackendIdentity {
    WarmBackendIdentity {
        engine: "engine-a".into(),
        gpu_artifact: Some("gpu-a".into()),
        binary_sha256: "binary-a".into(),
        detector_rules_digest: "detectors-a".into(),
        config_digest: "config-a".into(),
    }
}

fn ready_status(generation: &str) -> WarmBackendStatus {
    let identity = identity();
    evaluate_status(
        generation.into(),
        identity.clone(),
        Ok(identity),
        vec!["gpu-wgpu".into()],
        vec!["gpu-wgpu".into()],
    )
}

/// Regression: an exact engine/binary/detector/config match accepts a daemon
/// whose server-side GPU artifact and required backend are both ready.
#[test]
fn matching_ready_identity_satisfies_client_and_server() {
    let status = ready_status("gen-a");
    let mut expected = identity();
    expected.gpu_artifact = None;
    assert!(status.ready);
    assert!(validate_for_client(&status, &expected).is_empty());
    assert!(warm_route_error(&status).is_none());
}

/// Regression: an old in-memory engine cannot claim readiness to a client
/// running a different autoroute engine feature identity.
#[test]
fn stale_engine_is_rejected_with_exact_identity_pair() {
    let status = ready_status("gen-a");
    let mut expected = identity();
    expected.engine = "engine-b".into();
    expected.gpu_artifact = None;
    assert_eq!(
        validate_for_client(&status, &expected),
        vec!["engine daemon=engine-a, client=engine-b"]
    );
}

/// Regression: replacing the acquired GPU artifact after warmup invalidates
/// readiness instead of replaying evidence for the old accelerator peer.
#[test]
fn replaced_gpu_artifact_invalidates_server_readiness() {
    let startup = identity();
    let mut current = startup.clone();
    current.gpu_artifact = Some("gpu-b".into());
    let status = evaluate_status(
        "gen-a".into(),
        startup,
        Ok(current),
        vec!["gpu-wgpu".into()],
        vec!["gpu-wgpu".into()],
    );
    assert!(!status.ready);
    assert_eq!(
        status.reason.as_deref(),
        Some("warm backend identity drift: GPU artifact expected=Some(\"gpu-a\") current=Some(\"gpu-b\")")
    );
    assert_eq!(
        status.repair_command.as_deref(),
        Some("keyhog daemon stop && keyhog daemon start")
    );
}

/// Regression: replacing the executable at the same package/Git version is
/// detected by the exact autoroute SHA-256 artifact identity.
#[test]
fn replaced_binary_artifact_is_rejected() {
    let status = ready_status("gen-a");
    let mut expected = identity();
    expected.binary_sha256 = "binary-b".into();
    expected.gpu_artifact = None;
    assert_eq!(
        validate_for_client(&status, &expected),
        vec!["binary artifact daemon=binary-a, client=binary-b"]
    );
}

/// Regression: detector and resolved-config drift are both surfaced, in stable
/// field order, so operators know whether to restart or recalibrate policy.
#[test]
fn detector_and_config_drift_are_both_actionable() {
    let status = ready_status("gen-a");
    let mut expected = identity();
    expected.detector_rules_digest = "detectors-b".into();
    expected.config_digest = "config-b".into();
    expected.gpu_artifact = None;
    assert_eq!(
        validate_for_client(&status, &expected),
        vec![
            "detector rules daemon=detectors-a, client=detectors-b",
            "resolved config daemon=config-a, client=config-b",
        ]
    );
}

/// Regression: publishing an identity before every selected backend retains an
/// initialized execution handle cannot accidentally satisfy warm readiness.
#[test]
fn partial_initialization_fails_closed_with_missing_backend() {
    let startup = identity();
    let status = evaluate_status(
        "gen-a".into(),
        startup.clone(),
        Ok(startup),
        vec!["gpu-wgpu".into(), "simd".into()],
        vec!["simd".into()],
    );
    assert_eq!(
        status,
        WarmBackendStatus {
            ready: false,
            daemon_generation: "gen-a".into(),
            identity: identity(),
            required_backends: vec!["gpu-wgpu".into(), "simd".into()],
            initialized_backends: vec!["simd".into()],
            reason: Some(
                "warm backend initialization incomplete: missing [gpu-wgpu] from required [gpu-wgpu,simd]"
                    .into(),
            ),
            repair_command: Some("keyhog daemon stop && keyhog daemon start".into()),
        }
    );
    assert_eq!(
        serde_json::to_string(&status).expect("serialize incomplete warm status"),
        "{\"ready\":false,\"daemon_generation\":\"gen-a\",\"identity\":{\"engine\":\"engine-a\",\"gpu_artifact\":\"gpu-a\",\"binary_sha256\":\"binary-a\",\"detector_rules_digest\":\"detectors-a\",\"config_digest\":\"config-a\"},\"required_backends\":[\"gpu-wgpu\",\"simd\"],\"initialized_backends\":[\"simd\"],\"reason\":\"warm backend initialization incomplete: missing [gpu-wgpu] from required [gpu-wgpu,simd]\",\"repair_command\":\"keyhog daemon stop && keyhog daemon start\"}"
    );
    match warm_route_error(&status) {
        Some(Response::Error { message }) => assert_eq!(
            message,
            "daemon warm route is not ready: warm backend initialization incomplete: missing [gpu-wgpu] from required [gpu-wgpu,simd]. Repair with `keyhog daemon stop && keyhog daemon start`."
        ),
        other => panic!("partial initialization must return an exact route denial, got {other:?}"),
    }
}

/// Regression: a restarted daemon publishes a new generation even when every
/// autoroute identity remains identical, preventing a cached readiness lease
/// from being mistaken for the new process.
#[test]
fn daemon_restart_changes_generation_without_changing_identity() {
    let before = ready_status("gen-before");
    let after = ready_status("gen-after");
    assert!(before.ready && after.ready);
    assert_eq!(before.identity, after.identity);
    assert_ne!(before.daemon_generation, after.daemon_generation);
}

/// Regression: the v7 status JSON is exact and includes every identity,
/// initialization list, and nullable actionable-status field.
#[test]
fn exact_ready_status_json_is_stable() {
    let response = Response::Health {
        uptime_secs: 12,
        scans_served: 3,
        active_scans: 1,
        detector_count: 923,
        backend_recoveries: 0,
        last_backend_fault: None,
        guard_roots_registered: 0,
        guard_roots_current: 0,
        guard_roots_blocked: 0,
        guard_roots_degraded: 0,
        guard_active_transactions: 0,
        warm_backend: ready_status("gen-a"),
    };
    let json = serde_json::to_string(&response).expect("serialize warm health status");
    assert_eq!(
        json,
        "{\"kind\":\"health\",\"uptime_secs\":12,\"scans_served\":3,\"active_scans\":1,\"detector_count\":923,\"backend_recoveries\":0,\"last_backend_fault\":null,\"guard_roots_registered\":0,\"guard_roots_current\":0,\"guard_roots_blocked\":0,\"guard_roots_degraded\":0,\"guard_active_transactions\":0,\"warm_backend\":{\"ready\":true,\"daemon_generation\":\"gen-a\",\"identity\":{\"engine\":\"engine-a\",\"gpu_artifact\":\"gpu-a\",\"binary_sha256\":\"binary-a\",\"detector_rules_digest\":\"detectors-a\",\"config_digest\":\"config-a\"},\"required_backends\":[\"gpu-wgpu\"],\"initialized_backends\":[\"gpu-wgpu\"],\"reason\":null,\"repair_command\":null}}"
    );
}