rpi-cli 0.21.1

Terminal coding-agent CLI (the `rpi` binary) built on the rpi-* library crates
Documentation
use super::*;
use rpi_extensions::RuntimeActionHost;
struct Environment(Vec<(&'static str, Option<std::ffi::OsString>)>);
impl Environment {
    fn new() -> Self {
        Self(
            [
                crate::config::CONFIG_DIR_ENV,
                "RPI_FORCE_TUI",
                "RPI_SESSION_ID",
            ]
            .into_iter()
            .map(|key| (key, std::env::var_os(key)))
            .collect(),
        )
    }
}
impl Drop for Environment {
    fn drop(&mut self) {
        for (key, value) in self.0.drain(..) {
            match value {
                Some(v) => std::env::set_var(key, v),
                None => std::env::remove_var(key),
            }
        }
    }
}
fn args() -> Args {
    Args {
        print: true,
        experimental_native_store: Some("native".into()),
        no_extensions: true,
        no_tools: true,
        no_skills: true,
        no_prompt_templates: true,
        no_context_files: true,
        system_prompt: Some("fixture".into()),
        ..Default::default()
    }
}
fn resolved() -> ResolvedModel {
    crate::provider::resolve(
        Some("anthropic"),
        Some(crate::provider::DEFAULT_MODEL_ID),
        None,
        Some("fixture-key"),
        None,
    )
    .unwrap()
}
fn tree(root: &Path) -> BTreeMap<PathBuf, Vec<u8>> {
    fn walk(root: &Path, path: &Path, out: &mut BTreeMap<PathBuf, Vec<u8>>) {
        if !path.exists() {
            return;
        }
        for item in std::fs::read_dir(path).unwrap() {
            let path = item.unwrap().path();
            if path.is_dir() {
                walk(root, &path, out)
            } else {
                out.insert(
                    path.strip_prefix(root).unwrap().to_owned(),
                    std::fs::read(&path).unwrap(),
                );
            }
        }
    }
    let mut out = BTreeMap::new();
    walk(root, root, &mut out);
    out
}
async fn assert_routes_denied(host: &dyn RuntimeActionHost, old_id: &str) {
    for result in [
        host.new_session(serde_json::json!({})).await,
        host.fork(serde_json::json!({})).await,
        host.navigate_tree(serde_json::json!({})).await,
        host.switch_session(serde_json::json!({"id":old_id})).await,
    ] {
        assert!(result.unwrap_err().contains("forbids session routing"));
    }
}
#[tokio::test]
async fn native_build_is_fresh_and_routing_policy_survives_both_reload_hosts() {
    let temp = tempfile::tempdir().unwrap();
    let old = temp.path().join("old");
    let _env = crate::config::test_support::SessionDirEnv::new(Some(&old));
    let _restore = Environment::new();
    std::env::remove_var("RPI_FORCE_TUI");
    let agent = temp.path().join("agent");
    std::fs::create_dir_all(&agent).unwrap();
    std::env::set_var(crate::config::CONFIG_DIR_ENV, &agent);
    use rpi_harness::session::{
        jsonl::{JsonlSessionCreateOptions, JsonlSessionRepo, JsonlSessionRepoOptions},
        types::SessionStorage,
    };
    let repo = JsonlSessionRepo::with_env_cwd(JsonlSessionRepoOptions {
        fs: Arc::new(OsExecutionEnv::with_cwd(temp.path().to_owned())),
        sessions_root: old.to_string_lossy().into_owned(),
        clock: Arc::new(SystemClock),
        ids: Arc::new(DefaultIdGenerator::new()),
    });
    let legacy = repo
        .create_typed(&JsonlSessionCreateOptions {
            cwd: temp.path().to_string_lossy().into_owned(),
            id: None,
            parent_session_id: None,
            metadata: None,
        })
        .await
        .unwrap();
    legacy.set_name(Some("legacy sentinel")).await.unwrap();
    let old_id = legacy.metadata().id.clone();
    drop(legacy);
    let before = tree(&old);
    let arguments = args();
    let prepared = prepare_extensions(&arguments, temp.path(), false).unwrap();
    assert_eq!(prepared.native_store, Some(temp.path().join("native")));
    let (harness, _events, context) =
        build_prepared(&resolved(), &arguments, temp.path(), false, prepared)
            .await
            .unwrap();
    let id = harness.session().storage().metadata().id.clone();
    assert!(context.fresh_session_only);
    assert!(temp
        .path()
        .join("native")
        .join(format!("{id}.rpinative"))
        .is_file());
    for step in 0..3 {
        if step > 0 {
            if step == 2 {
                context.action_bridge.lock().unwrap().take();
            }
            assert!(
                !reload_extension_resources(&harness, &context)
                    .await
                    .had_errors
            );
        }
        let host = context
            .action_bridge
            .lock()
            .unwrap()
            .as_ref()
            .unwrap()
            .clone_host();
        assert_routes_denied(host.as_ref(), &old_id).await;
        assert_eq!(harness.session().storage().metadata().id, id);
        assert_eq!(
            context.tool_context.session_id().as_deref(),
            Some(id.as_str())
        );
        assert_eq!(std::env::var("RPI_SESSION_ID").unwrap(), id);
        assert_eq!(tree(&old), before);
    }
    assert!(!temp.path().join(".rpi/sessions").exists());
    let (second, _events, _ctx) = build(&resolved(), &arguments, temp.path(), false)
        .await
        .unwrap();
    assert_ne!(second.session().storage().metadata().id, id);
}
#[tokio::test]
async fn native_prepare_build_reject_bypasses_and_stdio_before_artifact_creation() {
    let temp = tempfile::tempdir().unwrap();
    let _env = crate::config::test_support::SessionDirEnv::new(None);
    let _restore = Environment::new();
    let agent = temp.path().join("agent");
    std::fs::create_dir_all(&agent).unwrap();
    std::env::set_var(crate::config::CONFIG_DIR_ENV, &agent);
    std::env::remove_var("RPI_FORCE_TUI");
    let mut arguments = args();
    arguments.continue_session = true;
    assert!(prepare_extensions(&arguments, temp.path(), false).is_err());
    arguments.continue_session = false;
    let mut prepared = prepare_extensions(&arguments, temp.path(), false).unwrap();
    let mut registry = rpi_extensions::ExtensionRegistry::new();
    assert!(registry.apply_declaration_json(r#"{"ownsStdio":true}"#));
    prepared.extension_session = ExtensionSession::from_parts(
        Arc::new(registry.snapshot()),
        Arc::new(rpi_extensions::PluginKeepalive::new(vec![], None)),
        vec![],
        None,
    );
    assert!(matches!(
        build_prepared(&resolved(), &arguments, temp.path(), false, prepared).await,
        Err(BuildError::NativeStorePolicy(_))
    ));
    assert!(!temp.path().join("native").exists());
    let prepared = prepare_extensions(&arguments, temp.path(), false).unwrap();
    arguments.experimental_native_store = None;
    assert!(
        build_prepared(&resolved(), &arguments, temp.path(), false, prepared)
            .await
            .is_err()
    );
    arguments = args();
    std::env::set_var("RPI_FORCE_TUI", "1");
    assert!(prepare_extensions(&arguments, temp.path(), false).is_err());
    std::env::remove_var("RPI_FORCE_TUI");
    std::fs::write(temp.path().join("native"), "file sentinel").unwrap();
    assert!(prepare_extensions(&arguments, temp.path(), false).is_err());
    assert_eq!(
        std::fs::read(temp.path().join("native")).unwrap(),
        b"file sentinel"
    );
    assert!(!temp.path().join(".rpi/sessions").exists());
}
#[path = "native_store_tests/guards.rs"]
mod guards;