routa-core 0.15.1

Routa.js core domain — models, stores, protocols, and JSON-RPC (transport-agnostic)
Documentation
use std::path::Path;

use super::*;

fn write_dir(path: &Path) {
    std::fs::create_dir_all(path).expect("directory should be created");
}

fn write_file(path: &Path, content: &str) {
    if let Some(parent) = path.parent() {
        write_dir(parent);
    }
    std::fs::write(path, content).expect("file should be written");
}

fn canonical(path: &Path) -> String {
    std::fs::canonicalize(path)
        .expect("path should be canonicalizable")
        .to_string_lossy()
        .to_string()
}

#[test]
fn empty_policy_is_detected() {
    assert!(SandboxPolicyInput::default().is_empty());
}

#[test]
fn resolve_policy_uses_explicit_workdir_as_scope_when_no_context() {
    let temp = tempfile::tempdir().expect("tempdir should exist");
    let repo = temp.path().join("repo");
    let child = repo.join("src");
    write_dir(&child);

    let policy = SandboxPolicyInput {
        workdir: Some(repo.to_string_lossy().to_string()),
        read_write_paths: vec![child.to_string_lossy().to_string()],
        capabilities: vec![SandboxCapability::WorkspaceWrite],
        ..Default::default()
    };

    let resolved = policy.resolve(None).expect("policy should resolve");
    assert_eq!(resolved.scope_root, canonical(&repo));
    assert_eq!(resolved.container_workdir, SANDBOX_SCOPE_CONTAINER_ROOT);
    assert_eq!(resolved.mounts[0].access, SandboxMountAccess::ReadOnly);
    assert!(resolved.mounts.iter().any(|mount| {
        mount.container_path.ends_with("/src") && mount.access == SandboxMountAccess::ReadWrite
    }));
}

#[test]
fn resolve_policy_uses_workspace_root_for_relative_paths() {
    let temp = tempfile::tempdir().expect("tempdir should exist");
    let repo = temp.path().join("repo");
    let scripts = repo.join("scripts");
    write_dir(&scripts);

    let policy = SandboxPolicyInput {
        workdir: Some("scripts".to_string()),
        read_write_paths: vec!["scripts".to_string()],
        capabilities: vec![SandboxCapability::WorkspaceWrite],
        ..Default::default()
    };
    let context = SandboxPolicyContext {
        workspace_id: Some("ws-1".to_string()),
        codebase_id: Some("cb-1".to_string()),
        workspace_root: Some(repo.clone()),
        available_worktrees: Vec::new(),
    };

    let resolved = policy
        .resolve(Some(context))
        .expect("policy should resolve");
    assert_eq!(resolved.host_workdir, canonical(&scripts));
    assert_eq!(resolved.container_workdir, "/workspace/scripts");
}

#[test]
fn resolve_policy_rejects_workdir_outside_scope_root() {
    let temp = tempfile::tempdir().expect("tempdir should exist");
    let repo = temp.path().join("repo");
    let outside = temp.path().join("outside");
    write_dir(&repo);
    write_dir(&outside);

    let policy = SandboxPolicyInput {
        workdir: Some(outside.to_string_lossy().to_string()),
        ..Default::default()
    };
    let context = SandboxPolicyContext {
        workspace_root: Some(repo),
        ..Default::default()
    };

    let err = policy
        .resolve(Some(context))
        .expect_err("workdir outside root should fail");
    assert!(err.contains("escapes scope root"));
}

#[test]
fn read_write_grant_wins_over_read_only_duplicate() {
    let temp = tempfile::tempdir().expect("tempdir should exist");
    let repo = temp.path().join("repo");
    let cache = repo.join("cache");
    write_dir(&cache);

    let policy = SandboxPolicyInput {
        workdir: Some(repo.to_string_lossy().to_string()),
        read_only_paths: vec![cache.to_string_lossy().to_string()],
        read_write_paths: vec![cache.to_string_lossy().to_string()],
        capabilities: vec![SandboxCapability::WorkspaceWrite],
        ..Default::default()
    };

    let resolved = policy.resolve(None).expect("policy should resolve");
    assert!(resolved.read_only_paths.is_empty());
    assert_eq!(resolved.read_write_paths, vec![canonical(&cache)]);
}

#[test]
fn trusted_workspace_config_is_loaded_and_merged() {
    let temp = tempfile::tempdir().expect("tempdir should exist");
    let repo = temp.path().join("repo");
    let scripts = repo.join("scripts");
    let cache = repo.join("cache");
    let output = repo.join("output");
    let base_env = repo.join(".env.base");
    let request_env = repo.join(".env.request");
    write_dir(&scripts);
    write_dir(&cache);
    write_dir(&output);
    write_file(&base_env, "BASE_TOKEN=base\nSHARED=base\n");
    write_file(&request_env, "REQUEST_TOKEN=request\nSHARED=request\n");
    write_file(
        &repo.join(".routa").join("sandbox.json"),
        r#"{
            "workdir": "scripts",
            "readOnlyPaths": ["cache"],
            "networkMode": "none",
            "envFile": ".env.base",
            "envAllowlist": ["OPENAI_API_KEY"],
            "capabilities": ["workspaceWrite"]
        }"#,
    );

    let policy = SandboxPolicyInput {
        trust_workspace_config: true,
        env_file: Some(".env.request".to_string()),
        read_write_paths: vec!["output".to_string()],
        env_allowlist: vec!["LANG".to_string()],
        ..Default::default()
    };
    let context = SandboxPolicyContext {
        workspace_root: Some(repo.clone()),
        ..Default::default()
    };

    let resolved = policy
        .resolve(Some(context))
        .expect("policy should resolve");

    assert_eq!(resolved.host_workdir, canonical(&scripts));
    assert_eq!(resolved.network_mode, SandboxNetworkMode::None);
    assert_eq!(
        resolved.env_allowlist,
        vec!["LANG".to_string(), "OPENAI_API_KEY".to_string()]
    );
    assert_eq!(
        resolved.workspace_config,
        Some(ResolvedSandboxWorkspaceConfig {
            path: canonical(&repo.join(".routa").join("sandbox.json")),
            trusted: true,
            loaded: true,
            reason: "loaded".to_string(),
        })
    );
    assert!(resolved.read_only_paths.contains(&canonical(&cache)));
    assert!(resolved.read_write_paths.contains(&canonical(&output)));
    assert_eq!(resolved.env_files.len(), 2);
    assert_eq!(
        resolved.env_files[0].source,
        SandboxEnvFileSource::WorkspaceConfig
    );
    assert_eq!(resolved.env_files[0].path, canonical(&base_env));
    assert_eq!(resolved.env_files[1].source, SandboxEnvFileSource::Request);
    assert_eq!(resolved.env_files[1].path, canonical(&request_env));
    assert!(resolved
        .notes
        .iter()
        .any(|note| note.contains("Loaded trusted workspace sandbox config")));
}

#[test]
fn workspace_config_is_ignored_without_trust() {
    let temp = tempfile::tempdir().expect("tempdir should exist");
    let repo = temp.path().join("repo");
    let scripts = repo.join("scripts");
    write_dir(&scripts);
    write_file(
        &repo.join(".routa").join("sandbox.json"),
        r#"{"workdir":"scripts","networkMode":"none"}"#,
    );

    let context = SandboxPolicyContext {
        workspace_root: Some(repo.clone()),
        ..Default::default()
    };
    let resolved = SandboxPolicyInput {
        workdir: Some(repo.to_string_lossy().to_string()),
        ..Default::default()
    }
    .resolve(Some(context))
    .expect("policy should resolve");

    assert_eq!(resolved.host_workdir, canonical(&repo));
    assert_eq!(resolved.network_mode, SandboxNetworkMode::None);
    assert_eq!(
        resolved.workspace_config,
        Some(ResolvedSandboxWorkspaceConfig {
            path: canonical(&repo.join(".routa").join("sandbox.json")),
            trusted: false,
            loaded: false,
            reason: "trustDisabled".to_string(),
        })
    );
}

#[test]
fn invalid_trusted_workspace_config_fails_resolution() {
    let temp = tempfile::tempdir().expect("tempdir should exist");
    let repo = temp.path().join("repo");
    write_dir(&repo);
    write_file(&repo.join(".routa").join("sandbox.json"), "{not-json");

    let err = SandboxPolicyInput {
        trust_workspace_config: true,
        ..Default::default()
    }
    .resolve(Some(SandboxPolicyContext {
        workspace_root: Some(repo),
        ..Default::default()
    }))
    .expect_err("invalid trusted config should fail");

    assert!(err.contains("Failed to parse trusted workspace sandbox config"));
}

#[test]
fn read_write_grants_require_workspace_write_capability() {
    let temp = tempfile::tempdir().expect("tempdir should exist");
    let repo = temp.path().join("repo");
    let cache = repo.join("cache");
    write_dir(&cache);

    let err = SandboxPolicyInput {
        workdir: Some(repo.to_string_lossy().to_string()),
        read_write_paths: vec![cache.to_string_lossy().to_string()],
        ..Default::default()
    }
    .resolve(None)
    .expect_err("write grants should require explicit capability");

    assert!(err.contains("workspaceWrite capability"));
}

#[test]
fn linked_worktrees_are_mounted_read_only_when_capability_is_enabled() {
    let temp = tempfile::tempdir().expect("tempdir should exist");
    let repo = temp.path().join("repo");
    let worktree = temp.path().join("wt-review");
    write_dir(&repo);
    write_dir(&worktree);

    let resolved = SandboxPolicyInput {
        workdir: Some(repo.to_string_lossy().to_string()),
        capabilities: vec![SandboxCapability::LinkedWorktreeRead],
        linked_worktree_mode: Some(SandboxLinkedWorktreeMode::All),
        ..Default::default()
    }
    .resolve(Some(SandboxPolicyContext {
        workspace_root: Some(repo.clone()),
        available_worktrees: vec![SandboxPolicyWorktree {
            id: "wt-1".to_string(),
            codebase_id: "cb-1".to_string(),
            worktree_path: worktree.to_string_lossy().to_string(),
            branch: "review".to_string(),
        }],
        ..Default::default()
    }))
    .expect("linked worktrees should resolve");

    assert_eq!(resolved.linked_worktrees.len(), 1);
    assert_eq!(resolved.linked_worktrees[0].id, "wt-1");
    assert!(resolved.mounts.iter().any(|mount| {
        mount.reason.as_deref() == Some("linkedWorktree")
            && mount.access == SandboxMountAccess::ReadOnly
    }));
    assert!(resolved
        .capabilities
        .iter()
        .any(|cap| cap.capability == SandboxCapability::LinkedWorktreeRead && cap.enabled));
}

#[test]
fn network_defaults_to_none_without_network_access_capability() {
    let temp = tempfile::tempdir().expect("tempdir should exist");
    let repo = temp.path().join("repo");
    write_dir(&repo);

    let resolved = SandboxPolicyInput {
        workdir: Some(repo.to_string_lossy().to_string()),
        ..Default::default()
    }
    .resolve(None)
    .expect("policy should resolve");

    assert_eq!(resolved.network_mode, SandboxNetworkMode::None);
    assert!(resolved
        .notes
        .iter()
        .any(|note| note.contains("Defaulted network mode to none")));
}

#[test]
fn explicit_env_file_is_resolved_and_explained() {
    let temp = tempfile::tempdir().expect("tempdir should exist");
    let repo = temp.path().join("repo");
    let env_file = repo.join(".env.runtime");
    write_dir(&repo);
    write_file(&env_file, "OPENAI_API_KEY=sk-test\nFOO=bar\n");

    let resolved = SandboxPolicyInput {
        workdir: Some(repo.to_string_lossy().to_string()),
        env_file: Some(".env.runtime".to_string()),
        ..Default::default()
    }
    .resolve(None)
    .expect("policy should resolve");

    assert_eq!(resolved.env_files.len(), 1);
    assert_eq!(resolved.env_files[0].path, canonical(&env_file));
    assert_eq!(resolved.env_files[0].source, SandboxEnvFileSource::Request);
    assert_eq!(
        resolved.env_files[0].keys,
        vec!["FOO".to_string(), "OPENAI_API_KEY".to_string()]
    );
}