artifactize 0.18.2

The AI-native collaboration layer for one-of-a-kind teammates and their agents: split a project into Artifacts with their own evals, and reuse every review while it still holds.
use std::{
    fs,
    path::Path,
    process::{Command, Output},
};

use artifactize_core::store;
use serde_json::{Value, json};
use support::os::echo_program;

#[path = "../../artifactize-core/tests/support/mod.rs"]
mod support;
use support::runs::RecordOutput;

use support::os::TOOL;

fn fixture(repo: &Path) {
    fs::create_dir_all(repo.join("a")).unwrap();
    fs::create_dir_all(repo.join("b")).unwrap();
    let tool = repo.join("a").join(TOOL);
    let (command, args) = support::os::python_script(
        &tool,
        &support::os::portable_path_probe(
            "import os,pathlib,sys\nPATH_TEXT_FUNCTION\nsys.stdout.write('|'.join([os.path.expanduser('~'), os.environ.get('TOOL_CHECK_SECRET', 'unset'), path_text(pathlib.Path.cwd())]))\npathlib.Path('touched').write_text('invoked')\n",
        ),
    );
    let profile = json!({"kind":"agent","backend":"openai","model":"test"});
    support::declaration::write(
        repo.join("a/index.artf"),
        json!({
            "name":"a",
            "views":{
                "agent_tools":{
                    "read":{"builtin":"read"},
                    "image":{"builtin":"view_image"},
                    "env":{
                        "description":"Environment",
                        "command":command,
                        "args":args,
                        "protocol":"plain",
                        "input_schema":{"type":"object","additionalProperties":false},
                    },
                    "data":{
                        "description":"JSON",
                        "command":echo_program(),
                        "args":["{\"content\":[{\"type\":\"json\",\"data\":{\"answer\":42}}]}"],
                        "protocol":"json",
                        "input_schema":{"type":"object"},
                    },
                    "error":{
                        "description":"Authored error",
                        "command":echo_program(),
                        "args":[
                            "{\"content\":[{\"type\":\"text\",\"text\":\"Owner error\"}],\"isError\":true}",
                        ],
                        "protocol":"json",
                        "input_schema":{"type":"object"},
                    },
                },
                "human_tools":{
                    "env":{"description":"Environment","kind":"output","command":command,"args":args},
                },
            },
            "evals":[
                {
                    "id":"review",
                    "title":"Review",
                    "profile":profile,
                    "payload":{"instruction":"Review a with {b}."},
                },
                {
                    "id":"human",
                    "title":"Human",
                    "profile":{"kind":"human"},
                    "payload":{"instruction":"Review a."},
                },
            ],
        })
        .to_string(),
    )
    .unwrap();
    support::declaration::write(
        repo.join("b/index.artf"),
        json!({"name":"b","views":{"agent_tools":{"read":{"builtin":"read"}}},"basis":true})
            .to_string(),
    )
    .unwrap();
    fs::write(repo.join("a/text.txt"), "scoped text\n").unwrap();
    fs::write(repo.join("b/notes.md"), "b notes\n").unwrap();
}

fn check(repo: &Path, state: &Path, args: &[&str]) -> Output {
    Command::new(support::os::cli())
        .arg("--repo")
        .arg(repo)
        .arg("--state-dir")
        .arg(state)
        .args(["tools", "check"])
        .args(args)
        .env("TOOL_CHECK_SECRET", "real-environment")
        .envs(support::os::home_variables(support::os::human_tool_home()))
        .record_output()
        .unwrap()
}

fn parsed(output: &Output, code: i32) -> Value {
    assert_eq!(
        output.status.code(),
        Some(code),
        "stdout={} stderr={}",
        String::from_utf8_lossy(&output.stdout),
        String::from_utf8_lossy(&output.stderr)
    );
    serde_json::from_slice(&output.stdout).unwrap()
}

#[test]
fn static_check_never_runs_processes_and_execute_uses_the_selected_audience() {
    let root = support::os::tempdir();
    let repo = root.path().join("repo");
    let state = root.path().join("state");
    fixture(&repo);
    let report = parsed(&check(&repo, &state, &[]), 0);
    assert_eq!(report["scopes"].as_array().unwrap().len(), 2);
    assert!(
        report["checks"]
            .as_array()
            .unwrap()
            .iter()
            .all(|check| check["stage"] == "preflight")
    );
    assert!(report["scopes"].as_array().unwrap().iter().all(|scope| {
        scope["tools"]
            .as_array()
            .unwrap()
            .iter()
            .all(|t| t["inputSchema"]["type"] == "object")
    }));
    assert!(!repo.join("a/touched").exists());
    assert!(!state.exists());
    let declaration_path = repo.join("a/index.artf");
    let original = fs::read(&declaration_path).unwrap();
    let mut declaration: Value = support::declaration::read(&original).unwrap();
    declaration["views"]["agent_tools"]["env"]["execution_paths"] = json!(["missing-input"]);
    support::declaration::write(&declaration_path, declaration.to_string()).unwrap();
    assert_eq!(parsed(&check(&repo, &state, &["a/review"]), 1)["ok"], false);
    assert!(!repo.join("a/touched").exists());
    support::declaration::write(&declaration_path, original).unwrap();
    let scoped = parsed(&check(&repo, &state, &["a/review"]), 0);
    assert_eq!(scoped["scopes"].as_array().unwrap().len(), 1);
    // The eval's scope: its own Artifact's tools plus those of the b it references.
    let owners: std::collections::BTreeSet<_> = scoped["scopes"][0]["tools"]
        .as_array()
        .unwrap()
        .iter()
        .map(|t| t["artifactId"].as_str().unwrap())
        .collect();
    assert_eq!(owners, ["a", "b"].into());
    // Windows has no execute bit to take away.
    if support::os::revoke_execution(&repo.join("a").join(TOOL)) {
        assert_eq!(
            parsed(
                &check(
                    &repo,
                    &state,
                    &["--artifact", "a", "--audience", "agent", "--tool", "env"]
                ),
                1
            )["ok"],
            false
        );
        assert!(!repo.join("a/touched").exists());
        support::os::make_executable(&repo.join("a").join(TOOL));
    }
    let agent = parsed(
        &check(
            &repo,
            &state,
            &[
                "--execute",
                "--artifact",
                "a",
                "--audience",
                "agent",
                "--tool",
                "env",
            ],
        ),
        0,
    );
    assert_eq!(
        agent["checks"].as_array().unwrap().last().unwrap()["stage"],
        "execute"
    );
    let text = agent["result"]["content"][0]["text"].as_str().unwrap();
    // The Agent tool runs in the author's environment, as the Human tool does: the same home
    // and the same variables.
    let expected = format!(
        "{}{}",
        support::os::human_tool_prefix(),
        support::os::path_text(&support::os::canonical(&repo.join("a")))
    );
    assert_eq!(text, expected);
    assert_eq!(
        fs::read_to_string(repo.join("a/touched")).unwrap(),
        "invoked"
    );
    fs::remove_file(repo.join("a/touched")).unwrap();
    let human = parsed(
        &check(
            &repo,
            &state,
            &[
                "--execute",
                "--artifact",
                "a",
                "--audience",
                "human",
                "--tool",
                "env",
            ],
        ),
        0,
    );
    assert_eq!(human["result"]["content"][0]["text"], expected);
    assert_eq!(
        fs::read_to_string(repo.join("a/touched")).unwrap(),
        "invoked"
    );
    let error = parsed(
        &check(
            &repo,
            &state,
            &[
                "--execute",
                "--artifact",
                "a",
                "--audience",
                "agent",
                "--tool",
                "error",
            ],
        ),
        1,
    );
    assert_eq!(error["result"]["isError"], true);
    assert!(!state.join(store::DATABASE).exists());
    assert_eq!(fs::read_dir(&state).unwrap().count(), 0);
    // An eval selector runs one tool in that eval's own scope, which admits what it
    // references; an Artifact-only check admits only the Artifact and its children.
    let in_eval = parsed(
        &check(
            &repo,
            &state,
            &[
                "--execute",
                "--eval",
                "a/review",
                "--tool",
                "read_b",
                "--args",
                r#"{"path":"notes.md"}"#,
            ],
        ),
        0,
    );
    assert_eq!(in_eval["scopes"][0]["evalId"], "a/review");
    let read = &in_eval["result"]["content"][0]["data"];
    assert_eq!(read["resolvedArtifactId"], "b");
    assert_eq!(read["lines"][0]["text"], "b notes\n");
    let unpublished = parsed(
        &check(
            &repo,
            &state,
            &["--execute", "--eval", "a/human", "--tool", "read_b"],
        ),
        1,
    );
    assert_eq!(unpublished["ok"], false);
    assert!(unpublished.get("result").is_none_or(Value::is_null));
    for args in [
        vec!["--eval", "a/review", "--artifact", "a"],
        vec!["--eval", "a/review", "--audience", "agent"],
        vec!["--eval", "a/review", "--execute"],
        vec![
            "--eval",
            "a/human",
            "--execute",
            "--tool",
            "env_a",
            "--args",
            "{}",
        ],
        vec!["--execute", "--artifact", "a"],
        vec!["--args", "{}"],
        vec![
            "--execute",
            "--artifact",
            "a",
            "--audience",
            "human",
            "--tool",
            "env",
            "--args",
            "{}",
        ],
    ] {
        assert_eq!(check(&repo, &state, &args).status.code(), Some(2));
    }
}

#[test]
fn declaration_failure_keeps_the_public_stage_label() {
    let root = support::os::tempdir();
    let repo = root.path().join("repo");
    let state = root.path().join("state");
    fixture(&repo);
    fs::write(repo.join("a/index.artf"), "invalid declaration").unwrap();
    let report = parsed(&check(&repo, &state, &[]), 1);
    assert_eq!(report["ok"], false);
    assert_eq!(report["checks"][0]["stage"], "declarations");
    assert!(!state.exists());
}