//! Integration tests for the pod-comm tools (`SendToPod`,
//! `ReadPodOutput`, `StopPod`).
//!
//! The real child Pod binary is not started. Instead each test stands
//! up a mock `UnixListener` that speaks the socket protocol directly:
//! it emits the connect-time `Event::Snapshot`, accepts methods such as
//! `Method::Run` / `Method::Shutdown`, and responds with the relevant
//! events when needed. This keeps the tests fast and independent of the
//! LLM layer — the tools are exercised for their wire behaviour alone.

use std::path::{Path, PathBuf};
use std::sync::{Arc, LazyLock, Mutex};

use llm_worker::llm_client::types::{ContentPart, Item, Role};
use llm_worker::tool::ToolOutput;
use manifest::{Permission, Scope, ScopeRule, SharedScope};
use pod::runtime::dir::{RuntimeDir, SpawnedPodRecord};
use pod::runtime::pod_registry::{self, LockFileGuard};
use pod::spawn::comm_tools::{read_pod_output_tool, send_to_pod_tool, stop_pod_tool};
use pod::spawn::registry::SpawnedPodRegistry;
use pod_store::{CombinedStore, FsPodStore, PodMetadataStore};
use protocol::stream::{JsonLineReader, JsonLineWriter};
use protocol::{ErrorCode, Event, Greeting, Method};
use serde_json::json;
use session_store::FsStore;
use tempfile::TempDir;
use tokio::net::UnixListener;
use tokio::sync::mpsc;
use tokio::task::JoinHandle;

/// Serialises env-mutating tests. The test harness runs tasks across
/// threads, and `YOI_RUNTIME_DIR` is a process-wide resource.
static ENV_LOCK: LazyLock<Mutex<()>> = LazyLock::new(|| Mutex::new(()));

/// Take `ENV_LOCK` and clear any env vars that would outrank
/// `YOI_RUNTIME_DIR` in `paths::runtime_dir` resolution; restore
/// previous values on drop.
struct EnvGuard {
    prev_home: Option<String>,
    prev_xdg: Option<String>,
    _lock: std::sync::MutexGuard<'static, ()>,
}

impl EnvGuard {
    fn acquire() -> Self {
        let lock = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
        let prev_home = std::env::var("YOI_HOME").ok();
        let prev_xdg = std::env::var("XDG_RUNTIME_DIR").ok();
        unsafe {
            std::env::remove_var("YOI_HOME");
            std::env::remove_var("XDG_RUNTIME_DIR");
        }
        Self {
            prev_home,
            prev_xdg,
            _lock: lock,
        }
    }
}

impl Drop for EnvGuard {
    fn drop(&mut self) {
        unsafe {
            match &self.prev_home {
                Some(v) => std::env::set_var("YOI_HOME", v),
                None => std::env::remove_var("YOI_HOME"),
            }
            match &self.prev_xdg {
                Some(v) => std::env::set_var("XDG_RUNTIME_DIR", v),
                None => std::env::remove_var("XDG_RUNTIME_DIR"),
            }
            std::env::remove_var("YOI_RUNTIME_DIR");
        }
    }
}

/// Create a spawner-owned `RuntimeDir` + `SpawnedPodRegistry` scoped to
/// a fresh tempdir. The returned `TempDir` must be kept alive by the
/// caller for the duration of the test.
async fn setup_registry() -> (TempDir, Arc<SpawnedPodRegistry>, Arc<RuntimeDir>) {
    let tmp = TempDir::new().unwrap();
    let rd = RuntimeDir::create(tmp.path(), "spawner").await.unwrap();
    let rd = Arc::new(rd);
    let registry = SpawnedPodRegistry::new(rd.clone());
    (tmp, registry, rd)
}

/// Register a fake spawned-child record pointing at a given socket
/// path, with a trivial write-scope for `scope_path`. Does not touch
/// pods.json.
async fn register_child(
    registry: &SpawnedPodRegistry,
    name: &str,
    socket: &Path,
    scope_path: &Path,
) {
    let record = SpawnedPodRecord {
        pod_name: name.into(),
        socket_path: socket.to_path_buf(),
        scope_delegated: vec![ScopeRule {
            target: scope_path.to_path_buf(),
            permission: Permission::Write,
            recursive: true,
        }],
        callback_address: "/dev/null".into(),
    };
    registry.add(record).await.unwrap();
}

/// Bind a Unix listener at a socket path inside the given directory.
async fn bind_mock_socket(dir: &Path, name: &str) -> (PathBuf, UnixListener) {
    let socket = dir.join(format!("{name}.sock"));
    let listener = UnixListener::bind(&socket).unwrap();
    (socket, listener)
}

/// Minimal connect-time snapshot used by mock socket servers.
fn empty_snapshot() -> Event {
    Event::Snapshot {
        entries: Vec::new(),
        greeting: Greeting {
            pod_name: "child".into(),
            cwd: "/tmp".into(),
            provider: "anthropic".into(),
            model: "x".into(),
            scope_summary: String::new(),
            tools: Vec::new(),
            context_window: 200_000,
            context_tokens: 0,
        },
        status: protocol::PodStatus::Idle,
    }
}

/// Accept one connection, send the protocol's connect-time snapshot,
/// and read exactly one `Method` line from it.
/// The reader half is kept open; caller awaits the returned handle.
fn accept_one_method(listener: UnixListener) -> JoinHandle<Option<Method>> {
    tokio::spawn(async move {
        let (stream, _) = listener.accept().await.ok()?;
        let (r, w) = stream.into_split();
        let mut reader = JsonLineReader::new(r);
        let mut writer = JsonLineWriter::new(w);
        writer.write(&empty_snapshot()).await.ok()?;
        reader.next::<Method>().await.ok().flatten()
    })
}

/// Accept one connection, send the protocol's connect-time snapshot,
/// read one `Method`, then write `response` back. Used by `SendToPod`
/// tests to mock the real controller's `TurnStart` acknowledgement (or
/// its `AlreadyRunning` rejection).
fn accept_method_and_respond(
    listener: UnixListener,
    response: Event,
) -> JoinHandle<Option<Method>> {
    tokio::spawn(async move {
        let (stream, _) = listener.accept().await.ok()?;
        let (r, w) = stream.into_split();
        let mut reader = JsonLineReader::new(r);
        let mut writer = JsonLineWriter::new(w);
        writer.write(&empty_snapshot()).await.ok()?;
        let method = reader.next::<Method>().await.ok().flatten();
        if method.is_some() {
            let _ = writer.write(&response).await;
        }
        method
    })
}

/// Pretend to be a spawned Pod whose connect-time snapshot carries a
/// fixed set of assistant items. Sends `Event::Snapshot` immediately on
/// every accept — the real Pod does the same, so `ReadPodOutput`'s
/// `fetch_history` just consumes the first non-Alert event.
fn serve_history(listener: UnixListener, items: Vec<Item>) -> JoinHandle<()> {
    tokio::spawn(async move {
        loop {
            let Ok((stream, _)) = listener.accept().await else {
                return;
            };
            let (_r, w) = stream.into_split();
            let mut writer = JsonLineWriter::new(w);
            let entries: Vec<serde_json::Value> = items
                .iter()
                .map(|item| {
                    let entry = session_store::LogEntry::AssistantItem {
                        ts: 0,
                        item: session_store::LoggedItem::from(item),
                    };
                    serde_json::to_value(&entry).unwrap()
                })
                .collect();
            let event = Event::Snapshot {
                entries,
                greeting: Greeting {
                    pod_name: "child".into(),
                    cwd: "/tmp".into(),
                    provider: "anthropic".into(),
                    model: "x".into(),
                    scope_summary: String::new(),
                    tools: Vec::new(),
                    context_window: 200_000,
                    context_tokens: 0,
                },
                status: protocol::PodStatus::Idle,
            };
            let _ = writer.write(&event).await;
        }
    })
}

fn serve_pod_methods(listener: UnixListener) -> mpsc::Receiver<Method> {
    let (tx, rx) = mpsc::channel(8);
    tokio::spawn(async move {
        loop {
            let Ok((stream, _)) = listener.accept().await else {
                return;
            };
            let (r, w) = stream.into_split();
            let mut reader = JsonLineReader::new(r);
            let mut writer = JsonLineWriter::new(w);
            if writer.write(&empty_snapshot()).await.is_err() {
                continue;
            }
            let Some(method) = reader.next::<Method>().await.ok().flatten() else {
                continue;
            };
            let is_shutdown = matches!(method, Method::Shutdown);
            if matches!(method, Method::Run { .. }) {
                let _ = writer.write(&Event::TurnStart { turn: 1 }).await;
            }
            if tx.send(method).await.is_err() || is_shutdown {
                return;
            }
        }
    });
    rx
}

fn assistant(text: &str) -> Item {
    Item::Message {
        id: None,
        role: Role::Assistant,
        content: vec![ContentPart::Text { text: text.into() }],
        status: None,
    }
}

// ---------------------------------------------------------------------------
// SendToPod
// ---------------------------------------------------------------------------

#[tokio::test]
async fn send_to_pod_delivers_run_method() {
    let (tmp, registry, _rd) = setup_registry().await;
    let (socket, listener) = bind_mock_socket(tmp.path(), "child").await;
    // Mock the controller's accept path: after reading the method,
    // ack with `TurnStart` so `SendToPod`'s confirmation loop succeeds.
    let received = accept_method_and_respond(listener, Event::TurnStart { turn: 1 });
    register_child(&registry, "child", &socket, tmp.path()).await;

    let def = send_to_pod_tool(registry);
    let (_meta, tool) = def();
    let input = json!({ "name": "child", "message": "hello there" }).to_string();
    let output: ToolOutput = tool.execute(&input, Default::default()).await.unwrap();
    assert!(
        output.summary.contains("child"),
        "summary: {}",
        output.summary
    );

    let method = received.await.unwrap().expect("expected a method");
    match method {
        Method::Run { input } => match input.as_slice() {
            [protocol::Segment::Text { content }] => assert_eq!(content, "hello there"),
            other => panic!("expected single Text segment, got {other:?}"),
        },
        other => panic!("expected Run, got {other:?}"),
    }
}

#[tokio::test]
async fn send_to_pod_errors_on_unknown_pod() {
    let (_tmp, registry, _rd) = setup_registry().await;
    let def = send_to_pod_tool(registry);
    let (_meta, tool) = def();
    let input = json!({ "name": "nope", "message": "hi" }).to_string();
    let err = tool.execute(&input, Default::default()).await.unwrap_err();
    assert!(err.to_string().contains("no spawned pod"), "{err}");
}

#[tokio::test]
async fn send_to_pod_errors_when_pod_already_running() {
    let (tmp, registry, _rd) = setup_registry().await;
    let (socket, listener) = bind_mock_socket(tmp.path(), "child").await;
    // Respond with the same `Error { AlreadyRunning }` that the real
    // controller emits when `Method::Run` arrives during RUNNING.
    let received = accept_method_and_respond(
        listener,
        Event::Error {
            code: ErrorCode::AlreadyRunning,
            message: "Pod is already executing a turn".into(),
        },
    );
    register_child(&registry, "child", &socket, tmp.path()).await;

    let def = send_to_pod_tool(registry);
    let (_meta, tool) = def();
    let input = json!({ "name": "child", "message": "hi" }).to_string();
    let err = tool.execute(&input, Default::default()).await.unwrap_err();
    assert!(
        err.to_string().contains("already running"),
        "expected AlreadyRunning wording: {err}"
    );

    // Ensure the listener was in fact hit with a Method::Run before the
    // rejection path fired — otherwise we'd be asserting on an error
    // that came from a connect failure.
    let method = received.await.unwrap().expect("expected a method");
    assert!(matches!(method, Method::Run { .. }));
}

// ---------------------------------------------------------------------------
// ReadPodOutput
// ---------------------------------------------------------------------------

#[tokio::test]
async fn read_pod_output_returns_new_assistant_text_then_empty_on_second_call() {
    let (tmp, registry, _rd) = setup_registry().await;
    let (socket, listener) = bind_mock_socket(tmp.path(), "child").await;
    register_child(&registry, "child", &socket, tmp.path()).await;

    let items = vec![
        Item::user_message("hello"),
        assistant("hi back"),
        assistant("still working"),
    ];
    let _server = serve_history(listener, items);

    let def = read_pod_output_tool(registry);
    let (_meta, tool) = def();
    let input = json!({ "name": "child" }).to_string();

    let first: ToolOutput = tool.execute(&input, Default::default()).await.unwrap();
    let body = first.content.expect("first read should have content");
    assert!(body.contains("hi back"), "body: {body}");
    assert!(body.contains("still working"), "body: {body}");

    // Cursor now points past all items — second call returns no new text.
    let second: ToolOutput = tool.execute(&input, Default::default()).await.unwrap();
    assert!(
        second.content.is_none(),
        "unexpected content: {:?}",
        second.content
    );
    assert!(
        second.summary.contains("no new assistant text"),
        "summary: {}",
        second.summary
    );
}

#[tokio::test]
async fn read_pod_output_reports_stopped_on_dead_socket() {
    let (tmp, registry, _rd) = setup_registry().await;
    // Register a record pointing at a socket that nobody is listening
    // on. Connect must fail → tool reports "stopped".
    let dead_socket = tmp.path().join("dead.sock");
    register_child(&registry, "child", &dead_socket, tmp.path()).await;

    let def = read_pod_output_tool(registry);
    let (_meta, tool) = def();
    let input = json!({ "name": "child" }).to_string();
    let output: ToolOutput = tool.execute(&input, Default::default()).await.unwrap();
    assert!(output.summary.contains("stopped"), "{}", output.summary);
}

// ---------------------------------------------------------------------------
// StopPod
// ---------------------------------------------------------------------------

#[tokio::test]
async fn stop_pod_sends_shutdown_and_releases_scope() {
    let _env = EnvGuard::acquire();
    let tmp = TempDir::new().unwrap();
    let store_tmp = TempDir::new().unwrap();
    let store = CombinedStore::new(
        FsStore::new(store_tmp.path()).unwrap(),
        FsPodStore::new(store_tmp.path().join("pods")).unwrap(),
    );
    let rd = Arc::new(RuntimeDir::create(tmp.path(), "spawner").await.unwrap());
    let parent_scope = SharedScope::new(
        Scope::writable(tmp.path())
            .unwrap()
            .with_added_deny_rules([ScopeRule {
                target: tmp.path().to_path_buf(),
                permission: Permission::Write,
                recursive: true,
            }])
            .unwrap(),
    );
    unsafe {
        std::env::set_var("YOI_RUNTIME_DIR", tmp.path());
    }
    let lock_path = tmp.path().join("pods.json");

    // Seed pods.json with a restored top-level `spawner` allocation whose
    // scope_deny contains the delegated child path plus the live child
    // allocation — mimics a parent resumed after SpawnPod.
    {
        let mut g = LockFileGuard::open(&lock_path).unwrap();
        let rule = ScopeRule {
            target: tmp.path().to_path_buf(),
            permission: Permission::Write,
            recursive: true,
        };
        pod_registry::register_pod_with_deny(
            &mut g,
            "spawner".into(),
            std::process::id(),
            "/tmp/spawner.sock".into(),
            vec![rule.clone()],
            vec![rule.clone()],
            session_store::new_segment_id(),
        )
        .unwrap();
        pod_registry::register_pod(
            &mut g,
            "child".into(),
            std::process::id(),
            "/tmp/child.sock".into(),
            vec![rule],
            session_store::new_segment_id(),
        )
        .unwrap();
    }

    let loaded = SpawnedPodRegistry::load_from_pod_state_with_reclaim(
        rd.clone(),
        store.clone(),
        "spawner".into(),
        Some(parent_scope.clone()),
    )
    .await
    .unwrap();
    let registry = loaded.registry;

    let (socket, listener) = bind_mock_socket(tmp.path(), "child").await;
    let received = accept_one_method(listener);
    register_child(&registry, "child", &socket, tmp.path()).await;

    let def = stop_pod_tool(registry.clone());
    let (_meta, tool) = def();
    let input = json!({ "name": "child" }).to_string();
    let output: ToolOutput = tool.execute(&input, Default::default()).await.unwrap();
    assert!(output.summary.contains("stopped"), "{}", output.summary);

    // The child got a Shutdown.
    let method = received.await.unwrap().expect("expected shutdown");
    assert!(matches!(method, Method::Shutdown));

    // Allocation for `child` is gone; `spawner` remains and its restored
    // dynamic deny layer has been reclaimed.
    {
        let g = LockFileGuard::open(&lock_path).unwrap();
        assert!(g.data().find("child").is_none(), "child still allocated");
        let spawner = g.data().find("spawner").expect("spawner missing");
        assert!(spawner.scope_deny.is_empty(), "deny not reclaimed");
    }
    assert_eq!(
        parent_scope
            .snapshot()
            .permission_at(&tmp.path().join("file.txt")),
        Some(Permission::Write)
    );

    // spawned_pods.json now lists zero children.
    let spawned = rd.path().join("spawned_pods.json");
    let contents = std::fs::read_to_string(&spawned).unwrap();
    let records: Vec<SpawnedPodRecord> = serde_json::from_str(&contents).unwrap();
    assert!(records.is_empty());
}

#[tokio::test]
async fn stop_pod_succeeds_even_when_child_unreachable() {
    let _env = EnvGuard::acquire();
    let (tmp, registry, _rd) = setup_registry().await;
    unsafe {
        std::env::set_var("YOI_RUNTIME_DIR", tmp.path());
    }

    // No live listener — socket never bound. Registered record points
    // at a dead path. StopPod should still clean up local bookkeeping.
    let dead_socket = tmp.path().join("dead.sock");
    register_child(&registry, "child", &dead_socket, tmp.path()).await;

    let def = stop_pod_tool(registry.clone());
    let (_meta, tool) = def();
    let input = json!({ "name": "child" }).to_string();
    let output: ToolOutput = tool.execute(&input, Default::default()).await.unwrap();
    assert!(output.summary.contains("stopped"), "{}", output.summary);

    // Registry no longer knows about the child.
    assert!(registry.get("child").await.is_none());
}

// ---------------------------------------------------------------------------
// Persistence / restore
// ---------------------------------------------------------------------------

#[tokio::test]
async fn restored_registry_uses_pod_state_without_runtime_file() {
    let _env = EnvGuard::acquire();
    let runtime_tmp = TempDir::new().unwrap();
    let store_tmp = TempDir::new().unwrap();
    let store = CombinedStore::new(
        FsStore::new(store_tmp.path()).unwrap(),
        FsPodStore::new(store_tmp.path().join("pods")).unwrap(),
    );
    unsafe {
        std::env::set_var("YOI_RUNTIME_DIR", runtime_tmp.path());
    }

    let rd = Arc::new(
        RuntimeDir::create(runtime_tmp.path(), "spawner")
            .await
            .unwrap(),
    );
    let registry =
        SpawnedPodRegistry::load_from_pod_state(rd.clone(), store.clone(), "spawner".to_string())
            .await
            .unwrap();

    let (socket, listener) = bind_mock_socket(runtime_tmp.path(), "child").await;
    let mut received = serve_pod_methods(listener);
    register_child(&registry, "child", &socket, runtime_tmp.path()).await;

    std::fs::remove_file(rd.path().join("spawned_pods.json")).unwrap();

    let restored =
        SpawnedPodRegistry::load_from_pod_state(rd.clone(), store.clone(), "spawner".to_string())
            .await
            .unwrap();

    let def = send_to_pod_tool(restored.clone());
    let (_meta, tool) = def();
    let input = json!({ "name": "child", "message": "after restart" }).to_string();
    tool.execute(&input, Default::default()).await.unwrap();
    match received.recv().await.expect("expected Run") {
        Method::Run { input } => match input.as_slice() {
            [protocol::Segment::Text { content }] => assert_eq!(content, "after restart"),
            other => panic!("expected single Text segment, got {other:?}"),
        },
        other => panic!("expected Run, got {other:?}"),
    }

    let def = stop_pod_tool(restored.clone());
    let (_meta, tool) = def();
    tool.execute(&json!({ "name": "child" }).to_string(), Default::default())
        .await
        .unwrap();
    assert!(matches!(
        received.recv().await.expect("expected Shutdown"),
        Method::Shutdown
    ));
    assert!(restored.get("child").await.is_none());

    let metadata = store
        .read_by_name("spawner")
        .unwrap()
        .expect("spawner metadata should remain");
    assert!(metadata.spawned_children.is_empty());
    assert_eq!(metadata.reclaimed_children.len(), 1);
    assert_eq!(metadata.reclaimed_children[0].pod_name, "child");
    let runtime_contents = std::fs::read_to_string(rd.path().join("spawned_pods.json")).unwrap();
    let runtime_records: Vec<SpawnedPodRecord> = serde_json::from_str(&runtime_contents).unwrap();
    assert!(runtime_records.is_empty());
}

#[tokio::test]
async fn load_from_pod_state_prunes_runtime_children_and_reclaims_durable_delegation() {
    let runtime_tmp = TempDir::new().unwrap();
    let store_tmp = TempDir::new().unwrap();
    let store = CombinedStore::new(
        FsStore::new(store_tmp.path()).unwrap(),
        FsPodStore::new(store_tmp.path().join("pods")).unwrap(),
    );
    let rd = Arc::new(
        RuntimeDir::create(runtime_tmp.path(), "spawner")
            .await
            .unwrap(),
    );
    let registry =
        SpawnedPodRegistry::load_from_pod_state(rd.clone(), store.clone(), "spawner".to_string())
            .await
            .unwrap();

    let (live_socket, listener) = bind_mock_socket(runtime_tmp.path(), "alive").await;
    let _server = serve_pod_methods(listener);
    register_child(&registry, "alive", &live_socket, runtime_tmp.path()).await;
    register_child(
        &registry,
        "missing",
        &runtime_tmp.path().join("missing.sock"),
        runtime_tmp.path(),
    )
    .await;

    let restored =
        SpawnedPodRegistry::load_from_pod_state(rd.clone(), store.clone(), "spawner".to_string())
            .await
            .unwrap();

    assert!(restored.get("alive").await.is_some());
    assert!(restored.get("missing").await.is_none());
    let metadata = store
        .read_by_name("spawner")
        .unwrap()
        .expect("spawner metadata should be written");
    assert_eq!(metadata.spawned_children.len(), 1);
    assert_eq!(metadata.spawned_children[0].pod_name, "alive");
    assert_eq!(metadata.reclaimed_children.len(), 1);
    assert_eq!(metadata.reclaimed_children[0].pod_name, "missing");
}

#[tokio::test]
async fn load_from_pod_state_reclaims_missing_child_scope_and_records_history() {
    let _env = EnvGuard::acquire();
    let runtime_tmp = TempDir::new().unwrap();
    let store_tmp = TempDir::new().unwrap();
    let store = CombinedStore::new(
        FsStore::new(store_tmp.path()).unwrap(),
        FsPodStore::new(store_tmp.path().join("pods")).unwrap(),
    );
    unsafe {
        std::env::set_var("YOI_RUNTIME_DIR", runtime_tmp.path());
    }
    let rd = Arc::new(
        RuntimeDir::create(runtime_tmp.path(), "spawner")
            .await
            .unwrap(),
    );
    let missing_rule = ScopeRule {
        target: runtime_tmp.path().to_path_buf(),
        permission: Permission::Write,
        recursive: true,
    };

    {
        let mut g = LockFileGuard::open(&runtime_tmp.path().join("pods.json")).unwrap();
        pod_registry::register_pod_with_deny(
            &mut g,
            "spawner".into(),
            std::process::id(),
            "/tmp/spawner.sock".into(),
            vec![missing_rule.clone()],
            vec![missing_rule.clone()],
            session_store::new_segment_id(),
        )
        .unwrap();
    }

    let parent_scope = SharedScope::new(
        Scope::writable(runtime_tmp.path())
            .unwrap()
            .with_added_deny_rules([missing_rule.clone()])
            .unwrap(),
    );
    let seed = SpawnedPodRegistry::load_from_pod_state(rd.clone(), store.clone(), "spawner".into())
        .await
        .unwrap();
    seed.add(SpawnedPodRecord {
        pod_name: "missing".into(),
        socket_path: runtime_tmp.path().join("missing.sock"),
        scope_delegated: vec![missing_rule.clone()],
        callback_address: "/dev/null".into(),
    })
    .await
    .unwrap();

    let loaded = SpawnedPodRegistry::load_from_pod_state_with_reclaim(
        rd.clone(),
        store.clone(),
        "spawner".into(),
        Some(parent_scope.clone()),
    )
    .await
    .unwrap();
    assert!(loaded.reclaimed_unreachable);
    assert!(loaded.registry.get("missing").await.is_none());
    assert_eq!(
        parent_scope
            .snapshot()
            .permission_at(&runtime_tmp.path().join("file.txt")),
        Some(Permission::Write)
    );

    let g = LockFileGuard::open(&runtime_tmp.path().join("pods.json")).unwrap();
    assert!(g.data().find("missing").is_none());
    assert!(g.data().find("spawner").unwrap().scope_deny.is_empty());
    let metadata = store
        .read_by_name("spawner")
        .unwrap()
        .expect("spawner metadata should remain");
    assert!(metadata.spawned_children.is_empty());
    assert_eq!(metadata.reclaimed_children.len(), 1);
    assert_eq!(metadata.reclaimed_children[0].pod_name, "missing");
    let runtime_contents = std::fs::read_to_string(rd.path().join("spawned_pods.json")).unwrap();
    let runtime_records: Vec<SpawnedPodRecord> = serde_json::from_str(&runtime_contents).unwrap();
    assert!(runtime_records.is_empty());
}
