//! Capability-claim verification for v1 conformance manifests.
//!
//! Per `docs/specs/lifecycle-contract/body.md` ("Conformance Expectations")
//! and issue #6 acceptance: "For each Codex and Claude capability
//! claim that depends on extracted behavior, an integration test runs
//! the corresponding renderer or telemetry code path and verifies the
//! manifest claim is true."
//!
//! When a manifest claim drifts from its backing implementation,
//! the assertion that fails should name the specific capability so
//! the migration is mechanical.

use std::collections::BTreeSet;
use std::fs;
use std::path::PathBuf;

use lifeloop::host_assets;
use lifeloop::protocol::codex_hook_event_for;
use lifeloop::{
    AdapterManifest, AdapterRole, CallbackRequest, CallbackResponse, ConformanceLevel,
    IntegrationMode, LifecycleEventKind, LifecycleReceipt, ManifestPlacementClass, ReceiptStatus,
    SupportState, claude_manifest, codex_manifest, lookup_manifest, manifest_registry,
};

fn repo_path(parts: &[&str]) -> PathBuf {
    let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
    for part in parts {
        path.push(part);
    }
    path
}

fn read_json(parts: &[&str]) -> serde_json::Value {
    let path = repo_path(parts);
    let raw = fs::read_to_string(&path).unwrap_or_else(|e| panic!("read {}: {e}", path.display()));
    serde_json::from_str(&raw).unwrap_or_else(|e| panic!("parse {}: {e}", path.display()))
}

fn read_live_codex_fixture(name: &str) -> serde_json::Value {
    read_json(&["tests", "fixtures", "live_codex", "codex_cli_0_122_0", name])
}

fn read_live_codex_0_129_fixture(name: &str) -> serde_json::Value {
    read_json(&["tests", "fixtures", "live_codex", "codex_cli_0_129_0", name])
}

fn lifeloop_events(value: &serde_json::Value) -> Vec<LifecycleEventKind> {
    value
        .get("lifeloop_events")
        .and_then(|v| v.as_array())
        .expect("fixture lifeloop_events array")
        .iter()
        .map(|v| serde_json::from_value(v.clone()).expect("lifeloop event name"))
        .collect()
}

// ============================================================================
// Registry shape
// ============================================================================

#[test]
fn registry_lists_all_six_initial_adapters() {
    let entries = manifest_registry();
    let ids: Vec<&str> = entries
        .iter()
        .map(|e| e.manifest.adapter_id.as_str())
        .collect();
    assert_eq!(
        ids,
        vec![
            "codex", "claude", "hermes", "openclaw", "gemini", "opencode"
        ]
    );
}

#[test]
fn registry_classifies_codex_and_claude_as_v1_conformance() {
    let entries = manifest_registry();
    for entry in &entries {
        let expected = match entry.manifest.adapter_id.as_str() {
            "codex" | "claude" => ConformanceLevel::V1Conformance,
            _ => ConformanceLevel::PreConformance,
        };
        assert_eq!(
            entry.conformance, expected,
            "{}: unexpected conformance",
            entry.manifest.adapter_id
        );
    }
}

#[test]
fn lookup_manifest_finds_each_registered_adapter() {
    for entry in manifest_registry() {
        let id = &entry.manifest.adapter_id;
        let resolved = lookup_manifest(id).expect("lookup");
        assert_eq!(&resolved.manifest.adapter_id, id);
    }
}

#[test]
fn lookup_manifest_rejects_unknown_adapter() {
    assert!(lookup_manifest("nonexistent").is_none());
}

#[test]
fn every_registered_manifest_validates() {
    for entry in manifest_registry() {
        entry
            .manifest
            .validate()
            .unwrap_or_else(|err| panic!("{} validation: {err}", entry.manifest.adapter_id));
    }
}

#[test]
fn built_in_manifests_omit_renewal_except_evaluated_codex_claim() {
    for entry in manifest_registry() {
        if entry.manifest.adapter_id == "codex" {
            assert!(
                entry.manifest.renewal.is_some(),
                "Codex carries the evaluated renewal claim backed by CCD client subprocess renewal evidence"
            );
        } else {
            assert!(
                entry.manifest.renewal.is_none(),
                "{} must omit renewal until reset/continuation support is backed by adapter evidence",
                entry.manifest.adapter_id
            );
        }
    }
}

#[test]
fn codex_conformance_manifest_fixture_matches_builtin_manifest() {
    let fixture = read_json(&["tests", "conformance", "manifests", "codex_v1.json"]);
    let manifest: AdapterManifest =
        serde_json::from_value(fixture["data"].clone()).expect("codex manifest fixture");
    manifest.validate().expect("fixture manifest validates");
    assert_eq!(
        serde_json::to_value(manifest).unwrap(),
        serde_json::to_value(codex_manifest()).unwrap(),
        "tests/conformance/manifests/codex_v1.json must mirror lifeloop::codex_manifest()"
    );
}

#[test]
fn claude_conformance_manifest_fixture_matches_builtin_manifest() {
    let fixture = read_json(&["tests", "conformance", "manifests", "claude_v1.json"]);
    let manifest: AdapterManifest =
        serde_json::from_value(fixture["data"].clone()).expect("claude manifest fixture");
    manifest.validate().expect("fixture manifest validates");
    assert_eq!(
        serde_json::to_value(manifest).unwrap(),
        serde_json::to_value(claude_manifest()).unwrap(),
        "tests/conformance/manifests/claude_v1.json must mirror lifeloop::claude_manifest()"
    );
}

// ============================================================================
// Codex capability claims
// ============================================================================

#[test]
fn codex_claims_native_hook_integration_mode_with_asset_rendering() {
    let m = codex_manifest();
    assert!(m.integration_modes.contains(&IntegrationMode::NativeHook));

    // The claim is grounded in host_assets: native-hook Codex must
    // produce the .codex/config.toml + .codex/hooks.json applied
    // assets.
    let applied = host_assets::render_applied_assets(
        host_assets::HostAdapter::Codex,
        host_assets::IntegrationMode::NativeHook,
    );
    let paths: Vec<&str> = applied.iter().map(|a| a.relative_path).collect();
    assert!(
        paths.contains(&host_assets::CODEX_TARGET_HOOKS),
        "Codex native_hook claim must produce hooks.json: {paths:?}"
    );
    assert!(
        paths.contains(&host_assets::CODEX_TARGET_CONFIG),
        "Codex native_hook claim must produce config.toml: {paths:?}"
    );
}

#[test]
fn codex_claims_manual_skill_integration_mode_with_scaffold_only() {
    let m = codex_manifest();
    assert!(m.integration_modes.contains(&IntegrationMode::ManualSkill));

    // Manual-skill mode is scaffold-only — no applied assets, but a
    // README scaffold under .ccd-hosts/ must exist.
    let applied = host_assets::render_applied_assets(
        host_assets::HostAdapter::Codex,
        host_assets::IntegrationMode::ManualSkill,
    );
    assert!(
        applied.is_empty(),
        "Codex manual_skill must not install runtime files"
    );
    let scaffold = host_assets::render_required_source_assets(
        host_assets::HostAdapter::Codex,
        host_assets::IntegrationMode::ManualSkill,
    );
    let scaffold_paths: Vec<&str> = scaffold.iter().map(|a| a.relative_path).collect();
    assert!(
        scaffold_paths.contains(&host_assets::CODEX_SOURCE_README),
        "Codex manual_skill scaffold must include README: {scaffold_paths:?}"
    );
}

#[test]
fn codex_lifecycle_event_claims_for_native_modes_are_grounded_in_hooks_json() {
    // Codex's hooks.json wires SessionStart, UserPromptSubmit,
    // PreCompact, and Stop. The manifest's native_hook claims must be
    // backed by those entries.
    let codex_hooks = host_assets::render_applied_assets(
        host_assets::HostAdapter::Codex,
        host_assets::IntegrationMode::NativeHook,
    );
    let hooks_asset = codex_hooks
        .iter()
        .find(|a| a.relative_path == host_assets::CODEX_TARGET_HOOKS)
        .expect("hooks.json present in native_hook mode");
    let parsed: serde_json::Value = serde_json::from_str(&hooks_asset.contents).unwrap();
    assert!(
        host_assets::codex_hooks_contain_managed_lifecycle(&parsed),
        "Codex native_hook claim requires hooks.json to declare managed lifecycle events"
    );
    let hooks = parsed["hooks"].as_object().expect("hooks object");
    for hook in [
        "SessionStart",
        "UserPromptSubmit",
        "PreCompact",
        "PostCompact",
        "Stop",
    ] {
        assert!(
            hooks.contains_key(hook),
            "Codex native_hook claim must render {hook} in hooks.json"
        );
    }

    // Manifest must claim native+native_hook for the events Codex
    // hooks actually wire.
    let m = codex_manifest();
    for event in [
        LifecycleEventKind::SessionStarting,
        LifecycleEventKind::FrameOpening,
        LifecycleEventKind::ContextPressureObserved,
        LifecycleEventKind::ContextCompacted,
        LifecycleEventKind::FrameEnding,
    ] {
        let claim = m
            .lifecycle_events
            .get(&event)
            .unwrap_or_else(|| panic!("Codex manifest missing {event:?} claim"));
        assert_eq!(
            claim.support,
            SupportState::Native,
            "Codex {event:?} must claim native support"
        );
        assert!(
            claim.modes.contains(&IntegrationMode::NativeHook),
            "Codex {event:?} must claim native_hook mode"
        );
    }
}

#[test]
fn codex_live_0_122_hook_evidence_covers_core_turn_events() {
    let manifest = codex_manifest();
    let mut observed_hooks = BTreeSet::new();
    let mut observed_events = BTreeSet::new();

    for fixture_name in [
        "session_start.json",
        "user_prompt_submit.json",
        "stop_block_continuation.json",
        "stop_after_continuation.json",
    ] {
        let fixture = read_live_codex_fixture(fixture_name);
        assert_eq!(fixture["kind"], "codex_live_hook_evidence");
        assert_eq!(fixture["adapter_id"], "codex");
        assert_eq!(fixture["sanitized"], true);

        let hook = fixture["hook_event_name"]
            .as_str()
            .expect("hook_event_name");
        assert_eq!(
            fixture["input"]["hook_event_name"].as_str(),
            Some(hook),
            "{fixture_name}: captured input must agree with fixture hook"
        );
        observed_hooks.insert(hook.to_string());

        for event in lifeloop_events(&fixture) {
            let mapped = codex_hook_event_for(event)
                .unwrap_or_else(|| panic!("{fixture_name}: {event:?} must map to a Codex hook"));
            assert_eq!(
                mapped.as_str(),
                hook,
                "{fixture_name}: live hook must match protocol mapping for {event:?}"
            );
            let claim = manifest
                .lifecycle_events
                .get(&event)
                .unwrap_or_else(|| panic!("missing Codex manifest claim for {event:?}"));
            assert_eq!(
                claim.support,
                SupportState::Native,
                "{fixture_name}: live native hook evidence must back a native manifest claim"
            );
            assert!(
                claim.modes.contains(&IntegrationMode::NativeHook),
                "{fixture_name}: native claim must include native_hook mode"
            );
            observed_events.insert(event);
        }
    }

    assert_eq!(
        observed_hooks,
        BTreeSet::from([
            "SessionStart".to_string(),
            "UserPromptSubmit".to_string(),
            "Stop".to_string(),
        ])
    );
    assert_eq!(
        observed_events,
        BTreeSet::from([
            LifecycleEventKind::SessionStarting,
            LifecycleEventKind::SessionStarted,
            LifecycleEventKind::FrameOpening,
            LifecycleEventKind::FrameEnding,
            LifecycleEventKind::FrameEnded,
        ])
    );
}

#[test]
fn codex_live_0_129_hook_registry_covers_compaction_hooks_and_trust_gate() {
    let fixture = read_live_codex_0_129_fixture("hook_registry.json");
    assert_eq!(fixture["kind"], "codex_hook_registry_evidence");
    assert_eq!(fixture["adapter_id"], "codex");
    assert_eq!(fixture["codex_cli_version"], "codex-cli 0.129.0");
    assert_eq!(fixture["feature_flag"], "hooks");
    assert_eq!(fixture["sanitized"], true);

    let configured = fixture["configured_events"]
        .as_array()
        .expect("configured events");
    let events: BTreeSet<_> = configured
        .iter()
        .map(|event| {
            event["codex_protocol_event"]
                .as_str()
                .expect("codex_protocol_event")
                .to_string()
        })
        .collect();
    assert_eq!(
        events,
        BTreeSet::from([
            "PreCompact".to_string(),
            "PostCompact".to_string(),
            "SessionStart".to_string(),
            "Stop".to_string(),
            "UserPromptSubmit".to_string(),
        ])
    );

    let precompact = configured
        .iter()
        .find(|event| event["codex_protocol_event"] == "PreCompact")
        .expect("PreCompact event");
    assert_eq!(precompact["app_server_event_name"], "preCompact");
    assert_eq!(precompact["config_key_fragment"], "pre_compact");
    assert_eq!(precompact["trust_status_before_state"], "untrusted");
    assert_eq!(precompact["trust_status_after_state"], "trusted");

    let context_pressure = codex_manifest()
        .lifecycle_events
        .get(&LifecycleEventKind::ContextPressureObserved)
        .expect("context.pressure_observed claim")
        .clone();
    assert_eq!(context_pressure.support, SupportState::Native);
    assert!(
        context_pressure
            .modes
            .contains(&IntegrationMode::NativeHook)
    );
    assert_eq!(
        codex_hook_event_for(LifecycleEventKind::ContextPressureObserved)
            .map(|event| event.as_str()),
        Some("PreCompact")
    );

    let context_compacted = codex_manifest()
        .lifecycle_events
        .get(&LifecycleEventKind::ContextCompacted)
        .expect("context.compacted claim")
        .clone();
    assert_eq!(context_compacted.support, SupportState::Native);
    assert!(
        context_compacted
            .modes
            .contains(&IntegrationMode::NativeHook)
    );
    assert_eq!(
        codex_hook_event_for(LifecycleEventKind::ContextCompacted).map(|event| event.as_str()),
        Some("PostCompact")
    );
}

#[test]
fn codex_live_0_129_hook_evidence_covers_core_turn_events() {
    let manifest = codex_manifest();
    let mut observed_hooks = BTreeSet::new();
    let mut observed_events = BTreeSet::new();

    for fixture_name in [
        "session_start.json",
        "user_prompt_submit.json",
        "stop_normal.json",
    ] {
        let fixture = read_live_codex_0_129_fixture(fixture_name);
        assert_eq!(fixture["kind"], "codex_live_hook_evidence");
        assert_eq!(fixture["adapter_id"], "codex");
        assert_eq!(fixture["codex_cli_version"], "codex-cli 0.129.0");
        assert_eq!(fixture["sanitized"], true);

        let hook = fixture["hook_event_name"]
            .as_str()
            .expect("hook_event_name");
        assert_eq!(
            fixture["input"]["hook_event_name"].as_str(),
            Some(hook),
            "{fixture_name}: captured input must agree with fixture hook"
        );
        observed_hooks.insert(hook.to_string());

        for event in lifeloop_events(&fixture) {
            let mapped = codex_hook_event_for(event)
                .unwrap_or_else(|| panic!("{fixture_name}: {event:?} must map to a Codex hook"));
            assert_eq!(
                mapped.as_str(),
                hook,
                "{fixture_name}: live hook must match protocol mapping for {event:?}"
            );
            let claim = manifest
                .lifecycle_events
                .get(&event)
                .unwrap_or_else(|| panic!("missing Codex manifest claim for {event:?}"));
            assert_eq!(
                claim.support,
                SupportState::Native,
                "{fixture_name}: live native hook evidence must back a native manifest claim"
            );
            assert!(
                claim.modes.contains(&IntegrationMode::NativeHook),
                "{fixture_name}: native claim must include native_hook mode"
            );
            observed_events.insert(event);
        }
    }

    assert_eq!(
        observed_hooks,
        BTreeSet::from([
            "SessionStart".to_string(),
            "UserPromptSubmit".to_string(),
            "Stop".to_string(),
        ])
    );
    assert_eq!(
        observed_events,
        BTreeSet::from([
            LifecycleEventKind::SessionStarting,
            LifecycleEventKind::SessionStarted,
            LifecycleEventKind::FrameOpening,
            LifecycleEventKind::FrameEnding,
            LifecycleEventKind::FrameEnded,
        ])
    );
}

#[test]
fn codex_live_0_129_stop_block_evidence_proves_continuation_shape() {
    let block = read_live_codex_0_129_fixture("stop_block_continuation.json");
    assert_eq!(block["hook_event_name"], "Stop");
    assert_eq!(block["input"]["stop_hook_active"], false);
    assert_eq!(block["output"]["decision"], "block");
    assert_eq!(block["output"]["reason"], "0.129 continuation probe");
    assert_eq!(block["observed_codex_result"]["continued_same_turn"], true);

    let after = read_live_codex_0_129_fixture("stop_after_continuation.json");
    assert_eq!(after["hook_event_name"], "Stop");
    assert_eq!(after["input"]["stop_hook_active"], true);
    assert_eq!(
        after["input"]["last_assistant_message"].as_str(),
        Some("0.129 continuation probe")
    );
    assert_eq!(after["output"]["systemMessage"], "saw Stop");
}

#[test]
fn codex_live_stop_block_evidence_proves_continuation_shape() {
    let block = read_live_codex_fixture("stop_block_continuation.json");
    assert_eq!(block["hook_event_name"], "Stop");
    assert_eq!(block["input"]["stop_hook_active"], false);
    assert_eq!(block["output"]["decision"], "block");
    assert!(
        block["output"]["reason"]
            .as_str()
            .expect("block reason")
            .contains("lifeloop continuation ok")
    );
    assert_eq!(block["observed_codex_result"]["continued_same_turn"], true);

    let after = read_live_codex_fixture("stop_after_continuation.json");
    assert_eq!(after["hook_event_name"], "Stop");
    assert_eq!(after["input"]["stop_hook_active"], true);
    assert_eq!(
        after["input"]["last_assistant_message"].as_str(),
        Some("lifeloop continuation ok")
    );
    assert_eq!(after["output"]["continue"], true);
}

#[test]
fn codex_live_receipt_evidence_validates_against_manifest() {
    let fixture = read_live_codex_fixture("receipt_emitted.json");
    assert_eq!(fixture["kind"], "lifeloop_receipt_evidence");

    let request: CallbackRequest =
        serde_json::from_value(fixture["request"].clone()).expect("callback request");
    let response: CallbackResponse =
        serde_json::from_value(fixture["response"].clone()).expect("callback response");
    let receipt: LifecycleReceipt =
        serde_json::from_value(fixture["receipt"].clone()).expect("lifecycle receipt");

    request.validate().expect("request validates");
    response.validate().expect("response validates");
    receipt.validate().expect("receipt validates");

    let claim = codex_manifest()
        .lifecycle_events
        .get(&request.event)
        .cloned()
        .expect("Codex manifest claim for receipt event");
    assert_eq!(claim.support, SupportState::Native);
    assert!(claim.modes.contains(&IntegrationMode::NativeHook));
    assert_eq!(response.status, ReceiptStatus::Delivered);
    assert_eq!(receipt.status, ReceiptStatus::Delivered);
    assert_eq!(receipt.adapter_id, "codex");
    assert_eq!(receipt.event, request.event);
    assert_eq!(receipt.event_id, request.event_id);
    assert_eq!(receipt.invocation_id, request.invocation_id);
    assert_eq!(receipt.harness_session_id, request.harness_session_id);
}

#[test]
fn codex_manifest_distinguishes_context_hooks_from_unexposed_session_end_hooks() {
    let manifest = codex_manifest();

    let frame_opened = manifest
        .lifecycle_events
        .get(&LifecycleEventKind::FrameOpened)
        .expect("frame.opened claim");
    assert_eq!(frame_opened.support, SupportState::Synthesized);
    assert!(frame_opened.modes.contains(&IntegrationMode::NativeHook));
    assert!(codex_hook_event_for(LifecycleEventKind::FrameOpened).is_none());

    let context_pressure = manifest
        .lifecycle_events
        .get(&LifecycleEventKind::ContextPressureObserved)
        .expect("context.pressure_observed claim");
    assert_eq!(context_pressure.support, SupportState::Native);
    assert!(
        context_pressure
            .modes
            .contains(&IntegrationMode::NativeHook)
    );
    assert_eq!(
        codex_hook_event_for(LifecycleEventKind::ContextPressureObserved)
            .map(|event| event.as_str()),
        Some("PreCompact")
    );

    let context_compacted = manifest
        .lifecycle_events
        .get(&LifecycleEventKind::ContextCompacted)
        .expect("context.compacted claim");
    assert_eq!(context_compacted.support, SupportState::Native);
    assert!(
        context_compacted
            .modes
            .contains(&IntegrationMode::NativeHook)
    );
    assert_eq!(
        codex_hook_event_for(LifecycleEventKind::ContextCompacted).map(|event| event.as_str()),
        Some("PostCompact")
    );

    for event in [
        LifecycleEventKind::SessionEnding,
        LifecycleEventKind::SessionEnded,
    ] {
        let claim = manifest
            .lifecycle_events
            .get(&event)
            .unwrap_or_else(|| panic!("missing Codex manifest claim for {event:?}"));
        assert_eq!(claim.support, SupportState::Unavailable);
        assert!(
            claim.modes.is_empty(),
            "unavailable Codex {event:?} claim must not advertise modes"
        );
        assert!(codex_hook_event_for(event).is_none());
    }
}

#[test]
fn codex_supervisor_tick_is_unavailable_consistently_with_install_matrix() {
    // Codex has no supervisor.tick wiring — the manifest must say so,
    // and the host_assets install matrix must not produce supervisor
    // assets for Codex either.
    let m = codex_manifest();
    let claim = m
        .lifecycle_events
        .get(&LifecycleEventKind::SupervisorTick)
        .expect("Codex must declare supervisor.tick claim");
    assert_eq!(claim.support, SupportState::Unavailable);
    assert!(claim.modes.is_empty());

    // No "supervisor" Codex asset exists in any mode.
    for mode in host_assets::IntegrationMode::ALL {
        let assets = host_assets::render_applied_assets(host_assets::HostAdapter::Codex, *mode);
        assert!(
            !assets
                .iter()
                .any(|a| a.relative_path.contains("supervisor")),
            "Codex {mode:?} must not produce supervisor assets"
        );
    }
}

#[test]
fn codex_role_is_primary_worker() {
    // The 'primary_worker' role is a capability claim about how Codex
    // participates in lifecycle dispatch.
    assert_eq!(codex_manifest().role, AdapterRole::PrimaryWorker);
}

#[test]
fn codex_placement_pre_session_is_native_with_byte_budget() {
    // Codex's session-start hook delivers context before any frame —
    // pre_session placement is native. The byte budget reflects what
    // the runtime considers safe.
    let m = codex_manifest();
    let claim = m
        .placement
        .get(&ManifestPlacementClass::PreSession)
        .expect("Codex must declare pre_session placement");
    assert_eq!(claim.support, SupportState::Native);
    assert!(
        claim.max_bytes.is_some(),
        "native pre_session placement must declare a byte budget"
    );
}

#[test]
fn codex_renewal_claim_is_backed_by_ccd_renewal_profile_and_stop_evidence() {
    let manifest = codex_manifest();
    let renewal = manifest
        .renewal
        .as_ref()
        .expect("Codex renewal claim must be present once evaluated");
    assert_eq!(renewal.reset.native, SupportState::Unavailable);
    assert_eq!(renewal.reset.wrapper_mediated, SupportState::Synthesized);
    assert_eq!(renewal.reset.manual, SupportState::Manual);
    assert_eq!(renewal.continuation.observation, SupportState::Native);
    assert_eq!(
        renewal.continuation.payload_delivery,
        SupportState::Synthesized
    );
    assert_eq!(renewal.profiles, vec!["ccd-renewal".to_string()]);
    assert!(
        renewal
            .evidence
            .as_deref()
            .unwrap_or_default()
            .contains("subprocess client callback evidence")
    );

    let assets = host_assets::render_applied_assets_with_profile(
        host_assets::HostAdapter::Codex,
        host_assets::IntegrationMode::NativeHook,
        &host_assets::CCD_RENEWAL_PROFILE,
    );
    let hooks_asset = assets
        .iter()
        .find(|asset| asset.relative_path == host_assets::CODEX_TARGET_HOOKS)
        .expect("Codex renewal profile must render hooks.json");
    let parsed: serde_json::Value = serde_json::from_str(&hooks_asset.contents).unwrap();
    let stop_cmd = parsed["hooks"]["Stop"][0]["hooks"][0]["command"]
        .as_str()
        .expect("Stop command");
    assert!(
        stop_cmd.contains("lifeloop") && stop_cmd.contains("host-hook"),
        "renewal profile must invoke Lifeloop's host-hook broker: {stop_cmd}"
    );
    assert!(
        stop_cmd.contains("--client-cmd") && stop_cmd.contains("CCD_BIN"),
        "renewal profile must mediate CCD as a client command: {stop_cmd}"
    );
    assert!(
        !stop_cmd.contains("ccd\" --output hook-protocol host-hook"),
        "renewal profile must not depend on removed `ccd host-hook`: {stop_cmd}"
    );

    let block = read_live_codex_0_129_fixture("stop_block_continuation.json");
    assert_eq!(block["hook_event_name"], "Stop");
    assert_eq!(block["output"]["decision"], "block");
    assert_eq!(block["observed_codex_result"]["continued_same_turn"], true);
}

// ============================================================================
// Claude capability claims
// ============================================================================

#[test]
fn claude_claims_native_hook_integration_with_settings_json() {
    let m = claude_manifest();
    assert_eq!(
        m.integration_modes,
        vec![IntegrationMode::NativeHook],
        "Claude only supports native_hook"
    );

    // Backed by host_assets: native-hook Claude must produce
    // .claude/settings.json with the managed event set.
    let applied = host_assets::render_applied_assets(
        host_assets::HostAdapter::Claude,
        host_assets::IntegrationMode::NativeHook,
    );
    let paths: Vec<&str> = applied.iter().map(|a| a.relative_path).collect();
    assert_eq!(paths, vec![host_assets::CLAUDE_TARGET_SETTINGS]);

    // Round-trip: the rendered settings must merge-equivalent to itself.
    assert_eq!(
        host_assets::claude_settings_status(Some(&applied[0].contents)),
        host_assets::AssetStatus::Present
    );
}

#[test]
fn claude_lifecycle_event_claims_match_managed_settings_events() {
    // The Claude manifest claims native support for session.starting,
    // frame.opening, frame.ending, etc. via native_hook. These must
    // be grounded in the managed event table baked into
    // claude_settings_json (which we verify by parsing the rendered
    // settings).
    let applied = host_assets::render_applied_assets(
        host_assets::HostAdapter::Claude,
        host_assets::IntegrationMode::NativeHook,
    );
    let parsed: serde_json::Value = serde_json::from_str(&applied[0].contents).unwrap();
    let hooks_obj = parsed["hooks"].as_object().expect("hooks object");

    // Every Lifeloop lifecycle event Claude claims as native_hook
    // must correspond to at least one wired Claude hook event.
    let claude_event_keys: Vec<&str> = hooks_obj.keys().map(String::as_str).collect();
    let m = claude_manifest();
    for (event, claim) in &m.lifecycle_events {
        if claim.support == SupportState::Native
            && claim.modes.contains(&IntegrationMode::NativeHook)
        {
            // The Claude hook protocol's event name is the harness's
            // own (SessionStart, UserPromptSubmit, Stop, PreCompact,
            // SessionEnd). We can't 1:1 map Lifeloop event names to
            // Claude hook names without the protocol renderer (#3),
            // but we can at least verify the hooks object is
            // non-empty when native claims exist.
            assert!(
                !claude_event_keys.is_empty(),
                "Claude claims native {event:?} but settings.json declares no hooks"
            );
        }
    }
}

#[test]
fn claude_context_pressure_claims_native() {
    // Claude has PreCompact and SessionEnd, both of which surface
    // context.pressure_observed evidence directly. Native support is
    // the honest claim.
    let m = claude_manifest();
    assert_eq!(m.context_pressure.support, SupportState::Native);
}

#[test]
fn claude_placement_pre_frame_leading_is_native() {
    let m = claude_manifest();
    let claim = m
        .placement
        .get(&ManifestPlacementClass::PreFrameLeading)
        .expect("Claude must declare pre_frame_leading placement");
    assert_eq!(claim.support, SupportState::Native);
}

// ============================================================================
// Spec-narrative claims that depend on shared infrastructure
// ============================================================================

#[test]
fn v1_conformance_manifests_use_only_supported_host_adapter_modes() {
    // Each integration_mode the manifest declares must be a real
    // installable mode for the corresponding HostAdapter. This is
    // the static cross-check between the manifest registry (issue
    // #6) and the host_assets supports_mode matrix (issue #4).
    for entry in manifest_registry()
        .into_iter()
        .filter(|e| e.conformance == ConformanceLevel::V1Conformance)
    {
        let host = host_assets::HostAdapter::from_id(&entry.manifest.adapter_id)
            .expect("v1 conformance adapter must have a HostAdapter");
        for mode in &entry.manifest.integration_modes {
            let Some(asset_mode) = host_assets::IntegrationMode::from_lifecycle_mode(*mode) else {
                // TelemetryOnly has no asset surface — skip.
                continue;
            };
            assert!(
                host_assets::supports_mode(host, asset_mode),
                "{} claims mode {:?} but host_assets does not support it",
                entry.manifest.adapter_id,
                mode
            );
        }
    }
}

#[test]
fn pre_conformance_manifests_use_partial_or_native_support_only() {
    // Pre-conformance manifests are honest about not being verified —
    // they should not claim 'native' for capabilities they cannot back
    // with a code path. This test pins that they use partial or
    // weaker for lifecycle events. Adjust this contract when a
    // pre-conformance adapter graduates to v1.
    for entry in manifest_registry()
        .into_iter()
        .filter(|e| e.conformance == ConformanceLevel::PreConformance)
    {
        for (event, claim) in &entry.manifest.lifecycle_events {
            assert_ne!(
                claim.support,
                SupportState::Native,
                "{}: pre-conformance manifest must not claim native for {event:?}",
                entry.manifest.adapter_id
            );
        }
    }
}
