okr 0.1.7

Reproducible R source context for coding agents
Documentation
#![cfg(unix)]

use std::fs;
use std::os::unix::fs::PermissionsExt;
use std::os::unix::fs::symlink;
use std::path::{Path, PathBuf};

use assert_cmd::cargo::cargo_bin_cmd;
use predicates::prelude::*;
use tempfile::tempdir;
use xshell::{Shell, cmd};

#[test]
fn cran_sync_offline_noop_and_mutation_verification_need_no_host_tools() {
    let project = tempdir().unwrap();
    let cache = project.path().join("cache");
    let empty_path = project.path().join("empty-path");
    fs::create_dir(&empty_path).unwrap();
    write_cran_config(project.path());
    let rbuildignore = project.path().join(".Rbuildignore");
    fs::write(&rbuildignore, "^README[.]md$\n").unwrap();

    okr(project.path(), &cache, &empty_path)
        .arg("sync")
        .assert()
        .success()
        .stdout(predicate::str::contains("synchronized 1 source entry"))
        .stdout(predicate::str::contains("sha256:").not())
        .stdout(predicate::str::contains("Rscript was not found"))
        .stderr(predicate::str::contains("Resolving sources").not())
        .stderr(predicate::str::contains("Preparing tinyone").not());
    let first_lock = fs::read(project.path().join("okr.lock")).unwrap();
    let first_json = fs::read(project.path().join("deps-src/_manifest.json")).unwrap();
    let first_markdown = fs::read(project.path().join("deps-src/_manifest.md")).unwrap();
    let lock_text = String::from_utf8(first_lock.clone()).unwrap();
    let manifest: serde_json::Value = serde_json::from_slice(&first_json).unwrap();
    assert!(lock_text.starts_with("version = 1\n"));
    assert!(!lock_text.contains(".files]"));
    assert_eq!(manifest["schema"], 1);
    assert!(manifest["entries"][0].get("files").is_none());
    assert_eq!(
        fs::read_to_string(&rbuildignore).unwrap(),
        "^README[.]md$\n^deps-src$\n^okr\\.toml$\n^okr\\.lock$\n"
    );

    fs::write(&rbuildignore, "^README[.]md$\n").unwrap();

    okr(project.path(), &cache, &empty_path)
        .arg("sync")
        .assert()
        .success()
        .stdout(predicate::str::contains("already synchronized; no changes"))
        .stdout(predicate::str::contains("sha256:").not());
    assert_eq!(
        fs::read_to_string(&rbuildignore).unwrap(),
        "^README[.]md$\n^deps-src$\n^okr\\.toml$\n^okr\\.lock$\n"
    );

    okr(project.path(), &cache, &empty_path)
        .args(["--verbose", "sync"])
        .assert()
        .success()
        .stdout(predicate::str::contains("environment digest: sha256:"));

    fs::remove_dir_all(project.path().join("deps-src")).unwrap();
    fs::remove_file(project.path().join("okr.lock")).unwrap();
    okr(project.path(), &cache, &empty_path)
        .args(["--quiet", "sync", "--offline"])
        .assert()
        .success()
        .stdout(predicate::str::is_empty())
        .stderr(predicate::str::is_empty());
    assert_eq!(
        fs::read(project.path().join("okr.lock")).unwrap(),
        first_lock
    );
    assert_eq!(
        fs::read(project.path().join("deps-src/_manifest.json")).unwrap(),
        first_json
    );
    assert_eq!(
        fs::read(project.path().join("deps-src/_manifest.md")).unwrap(),
        first_markdown
    );

    okr(project.path(), &cache, &empty_path)
        .arg("verify")
        .assert()
        .success()
        .stdout(predicate::str::contains("verified sha256:"));

    fs::write(
        project.path().join("deps-src/tinyone/R/hello.R"),
        b"hello <- function() 'tampered'\n",
    )
    .unwrap();
    okr(project.path(), &cache, &empty_path)
        .args(["verify", "--json"])
        .assert()
        .code(4)
        .stdout(predicate::str::contains("\"schema\": 1"))
        .stdout(predicate::str::contains("\"path\": \".\""))
        .stdout(predicate::str::contains("\"mismatch\": \"modified\""));
}

#[test]
fn coherence_warns_by_default_and_is_fatal_only_when_strict() {
    let project = tempdir().unwrap();
    let cache = project.path().join("cache");
    let tools = project.path().join("tools");
    fs::create_dir(&tools).unwrap();
    let rscript = tools.join("Rscript");
    fs::write(
        &rscript,
        "#!/bin/sh\nprintf '__OKR_RLIB_INSPECTION_V1_BEGIN__\\n4.5.1\\ntinyone\\t9.9.9\\n__OKR_RLIB_INSPECTION_V1_END__\\n'\n",
    )
    .unwrap();
    fs::set_permissions(&rscript, fs::Permissions::from_mode(0o755)).unwrap();
    write_cran_config(project.path());

    okr(project.path(), &cache, &tools)
        .arg("sync")
        .assert()
        .success()
        .stderr(predicate::str::contains("installed 9.9.9, vendored 1.0.0"))
        .stderr(predicate::str::contains("install with:  Rscript -e"));

    okr(project.path(), &cache, &tools)
        .args(["sync", "--strict"])
        .assert()
        .code(4)
        .stderr(predicate::str::contains("coherence failed"));

    okr(project.path(), &cache, &tools)
        .args(["verify", "--strict", "--json"])
        .assert()
        .code(4)
        .stdout(predicate::str::contains("\"status\": \"mismatch\""))
        .stdout(predicate::str::contains("\"installed_version\": \"9.9.9\""));
}

#[test]
fn file_git_reference_with_a_source_link_rebuilds_offline_from_the_normalized_clone_cache() {
    if !host_git() {
        return;
    }
    let project = tempdir().unwrap();
    let cache = project.path().join("cache");
    let repository = project.path().join("reference-repository");
    copy_tree(&fixture_path("reference-repo"), &repository);
    fs::create_dir_all(repository.join("xDir/pkg")).unwrap();
    fs::write(repository.join("xDir/pkg/context.txt"), "linked context\n").unwrap();
    symlink("xDir/pkg", repository.join("linked-context")).unwrap();
    let shell = Shell::new().unwrap();
    cmd!(shell, "git init -q -b main {repository}")
        .run()
        .unwrap();
    cmd!(shell, "git -C {repository} config user.name okr-test")
        .run()
        .unwrap();
    cmd!(
        shell,
        "git -C {repository} config user.email okr@example.test"
    )
    .run()
    .unwrap();
    cmd!(shell, "git -C {repository} add .").run().unwrap();
    cmd!(shell, "git -C {repository} commit -q -m fixture")
        .run()
        .unwrap();
    let commit = cmd!(shell, "git -C {repository} rev-parse HEAD")
        .read()
        .unwrap();
    let url = format!("file://{}", repository.display());
    fs::write(
        project.path().join("okr.toml"),
        format!("[vendor]\ngitignore = false\n[references]\nstandards = \"git::{url}@{commit}\"\n"),
    )
    .unwrap();

    let normal_path = std::env::var_os("PATH").unwrap();
    okr(project.path(), &cache, Path::new(&normal_path))
        .arg("sync")
        .assert()
        .success();
    let first_lock = fs::read(project.path().join("okr.lock")).unwrap();
    let first_manifest = fs::read(project.path().join("deps-src/_manifest.json")).unwrap();
    let first_markdown = fs::read(project.path().join("deps-src/_manifest.md")).unwrap();
    let source_link = project.path().join("deps-src/standards/linked-context");
    assert!(
        fs::symlink_metadata(&source_link)
            .unwrap()
            .file_type()
            .is_file()
    );
    assert_eq!(fs::read(&source_link).unwrap(), b"xDir/pkg");
    fs::remove_dir_all(project.path().join("deps-src")).unwrap();
    fs::remove_dir_all(&repository).unwrap();
    let empty_path = project.path().join("empty-path");
    fs::create_dir(&empty_path).unwrap();

    okr(project.path(), &cache, &empty_path)
        .args(["sync", "--offline"])
        .assert()
        .success();
    assert_eq!(
        fs::read(project.path().join("okr.lock")).unwrap(),
        first_lock
    );
    assert_eq!(
        fs::read(project.path().join("deps-src/_manifest.json")).unwrap(),
        first_manifest
    );
    assert_eq!(
        fs::read(project.path().join("deps-src/_manifest.md")).unwrap(),
        first_markdown
    );
    assert!(
        project
            .path()
            .join("deps-src/standards/docs/guide.md")
            .is_file()
    );
    let source_link = project.path().join("deps-src/standards/linked-context");
    assert!(
        fs::symlink_metadata(&source_link)
            .unwrap()
            .file_type()
            .is_file()
    );
    assert_eq!(fs::read(source_link).unwrap(), b"xDir/pkg");
}

#[test]
fn sync_explains_when_a_reference_was_declared_as_a_package() {
    if !host_git() {
        return;
    }
    let project = tempdir().unwrap();
    let cache = project.path().join("cache");
    let repository = project.path().join("not-an-r-package");
    fs::create_dir(&repository).unwrap();
    fs::write(repository.join("README.md"), "# Reference project\n").unwrap();
    let shell = Shell::new().unwrap();
    cmd!(shell, "git init -q -b main {repository}")
        .run()
        .unwrap();
    cmd!(shell, "git -C {repository} config user.name okr-test")
        .run()
        .unwrap();
    cmd!(
        shell,
        "git -C {repository} config user.email okr@example.test"
    )
    .run()
    .unwrap();
    cmd!(shell, "git -C {repository} add README.md")
        .run()
        .unwrap();
    cmd!(shell, "git -C {repository} commit -q -m fixture")
        .run()
        .unwrap();
    let commit = cmd!(shell, "git -C {repository} rev-parse HEAD")
        .read()
        .unwrap();
    let source = format!("git::file://{}@{commit}", repository.display());
    fs::write(
        project.path().join("okr.toml"),
        format!("[packages]\nnot-an-r-package = \"{source}\"\n"),
    )
    .unwrap();
    let normal_path = std::env::var_os("PATH").unwrap();

    okr(project.path(), &cache, Path::new(&normal_path))
        .arg("sync")
        .assert()
        .code(3)
        .stderr(predicate::str::contains(
            "source `not-an-r-package` was declared in `[packages]`",
        ))
        .stderr(predicate::str::contains(
            "move its `not-an-r-package` declaration from `[packages]` to `[references]`",
        ))
        .stderr(predicate::str::contains(format!(
            "okr add {source} --reference"
        )));
}

fn okr(project: &Path, cache: &Path, path: &Path) -> assert_cmd::Command {
    let mut command = cargo_bin_cmd!("okr");
    command
        .current_dir(project)
        .env("OKR_CACHE_DIR", cache)
        .env("PATH", path);
    command
}

fn write_cran_config(project: &Path) {
    let repository = fixture_path("fixture-repo");
    fs::write(
        project.join("okr.toml"),
        format!(
            "[project]\nsnapshot = \"2026-06-30\"\nrepo-url = \"file://{}\"\n\n[vendor]\ninclude-tests = false\n\n[packages]\ntinyone = \"*\"\n",
            repository.display()
        ),
    )
    .unwrap();
}

fn fixture_path(relative: &str) -> PathBuf {
    Path::new(env!("CARGO_MANIFEST_DIR"))
        .join("tests/fixtures")
        .join(relative)
}

fn copy_tree(source: &Path, destination: &Path) {
    for entry in walkdir::WalkDir::new(source) {
        let entry = entry.unwrap();
        let relative = entry.path().strip_prefix(source).unwrap();
        let target = destination.join(relative);
        if entry.file_type().is_dir() {
            fs::create_dir_all(target).unwrap();
        } else {
            fs::copy(entry.path(), target).unwrap();
        }
    }
}

fn host_git() -> bool {
    let Ok(shell) = Shell::new() else {
        return false;
    };
    cmd!(shell, "git --version").quiet().run().is_ok()
}