sprawling-runtime 0.0.11

One run of a sprawling resident, from dispatch to freeze: turns, tools, replay and the sandbox that executes code.
Documentation
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
// Copyright (c) 2026 2youg1 and the sprawling contributors

#![allow(
    clippy::unwrap_used,
    clippy::expect_used,
    clippy::panic,
    clippy::indexing_slicing,
    reason = "test code"
)]

use super::*;

fn snapshot() -> StatusSnapshot {
    StatusSnapshot {
        who: "alice".to_owned(),
        addr: Address::parse("work").unwrap(),
        policy: kernel::RunPolicy::of(kernel::Mode::Work),
        ctx_limit: Tokens::new(8000),
        trust: "trusted".to_owned(),
        write_domain: "work".to_owned(),
        locks: vec!["work/a.txt".to_owned()],
        worktree: std::path::PathBuf::from("no-such-tree"),
        signals_pending: 2,
        provider_mode: ProviderMode::Normal,
        neighbours: 3,
    }
}

fn call() -> ToolCall {
    ToolCall {
        id: "s1".to_owned(),
        name: ToolName::parse("status").unwrap(),
        args: Payload::new(Map::new()).unwrap(),
    }
}

#[test]
fn the_thirteen_fields_report_in_the_frozen_order() {
    let tool = StatusTool::new(snapshot()).unwrap();
    let outcome = tool.invoke(&call()).unwrap();
    let value = serde_json::to_value(&outcome.result).unwrap();
    let text = value["text"].as_str().unwrap();
    let order = [
        "who",
        "addr",
        "mode",
        "ctx",
        "trust",
        "write_domain",
        "worktree",
        "signals_pending",
        "children",
        "now",
        "provider_mode",
        "neighbours",
        "backlog",
    ];
    let lines: Vec<&str> = text.lines().collect();
    assert_eq!(
        lines.len(),
        13,
        "the frozen list is thirteen fields: {text}"
    );
    for (line, field) in lines.iter().zip(order) {
        assert!(
            line.starts_with(&format!("{field}:")),
            "expected {field}, got {line}"
        );
    }
}

/// `children` was a hardcoded empty list, so a run that had just
/// handed work down and then asked about its own situation was told
/// it had handed nothing down.
#[test]
fn the_children_line_says_where_the_work_went_and_which_kind_of_delegate() {
    let handed = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
    let seen = std::sync::Arc::clone(&handed);
    let tool =
        StatusTool::watching(snapshot(), Box::new(move || seen.lock().unwrap().clone())).unwrap();

    let before = serde_json::to_value(&tool.invoke(&call()).unwrap().result).unwrap();
    assert!(before["text"].as_str().unwrap().contains("children: none"));

    handed.lock().unwrap().push(ChildStatus {
        room: Address::parse("work/helper").unwrap(),
        kind: DelegateKind::Ephemeral,
    });
    let after = serde_json::to_value(&tool.invoke(&call()).unwrap().result).unwrap();
    assert!(
        after["text"]
            .as_str()
            .unwrap()
            .contains("children: work/helper (ephemeral)"),
        "the desk is asked at call time, not frozen with the tool: {after}"
    );
}

#[test]
fn the_tool_reports_what_it_was_given_and_never_samples() {
    let tool = StatusTool::new(snapshot()).unwrap();
    let first = tool.invoke(&call()).unwrap();
    let second = tool.invoke(&call()).unwrap();
    assert_eq!(
        first.result, second.result,
        "two calls, one turn, one answer"
    );
}

#[test]
fn a_call_for_another_tool_is_refused() {
    let tool = StatusTool::new(snapshot()).unwrap();
    let mut wrong = call();
    wrong.name = ToolName::parse("edit").unwrap();
    assert_eq!(
        *match tool.invoke(&wrong) {
            Err(err) => err,
            Ok(_) => panic!("identity is fail-closed"),
        }
        .code(),
        AxCode::InvalidArgs
    );
}

/// The `now` line was frozen with the snapshot and never filled, so a
/// model that asked what time it was read `not stamped` in every city.
#[test]
fn the_now_line_reports_the_drivers_latest_reading_in_iso_utc() {
    let clock = ClockReading::default();
    let tool = StatusTool::new(snapshot()).unwrap().clocked(clock.clone());
    let now_line = |tool: &StatusTool| {
        let value = serde_json::to_value(&tool.invoke(&call()).unwrap().result).unwrap();
        value["text"]
            .as_str()
            .unwrap()
            .lines()
            .find(|line| line.starts_with("now:"))
            .unwrap()
            .to_owned()
    };
    assert_eq!(now_line(&tool), "now: not stamped");
    clock.keep(TimeMs::new(1_785_585_607_000));
    assert_eq!(now_line(&tool), "now: 2026-08-01T12:00:07Z");
}

/// The test city's run read `worktree: …\city (0 bytes)` over a tree of
/// files. The line reports the tree as it stands at the call: every file
/// under it, the city's own `.sprawling` state left out because a run
/// cannot read it.
#[test]
fn the_worktree_line_counts_the_files_the_tree_holds_at_the_call() {
    let tree = tempfile::tempdir().unwrap();
    std::fs::create_dir_all(tree.path().join("hall").join("Mayor")).unwrap();
    std::fs::write(tree.path().join("hall").join("Memo.md"), "1234").unwrap();
    std::fs::write(
        tree.path().join("hall").join("Mayor").join("a.md"),
        "123456",
    )
    .unwrap();
    std::fs::create_dir_all(tree.path().join(".sprawling")).unwrap();
    std::fs::write(tree.path().join(".sprawling").join("ledger"), "not counted").unwrap();
    let mut seen = snapshot();
    seen.worktree = tree.path().to_path_buf();
    let tool = StatusTool::new(seen).unwrap();
    let line = |tool: &StatusTool| -> String {
        let outcome = tool.invoke(&call()).unwrap();
        let value = serde_json::to_value(&outcome.result).unwrap();
        value["text"]
            .as_str()
            .unwrap()
            .lines()
            .find(|line| line.starts_with("worktree: "))
            .unwrap()
            .to_owned()
    };
    assert_eq!(
        line(&tool),
        format!("worktree: {} (10 bytes)", tree.path().display())
    );
    std::fs::write(tree.path().join("hall").join("New.md"), "12").unwrap();
    assert_eq!(
        line(&tool),
        format!("worktree: {} (12 bytes)", tree.path().display())
    );
}