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);
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());
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();
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);
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());
}