mod support;
use std::path::Path;
use support::{
authoring_cassette, code, fs_server, repo_root, run, run_env, stderr, stdout, stub_provider,
text_reply, toml_string, TempDir, EXIT_DIAGNOSTICS, EXIT_OK,
};
const DIGEST_SOURCE: &str = include_str!("../../../examples/repo-digest/main.ing");
const NO_TOOLS_SOURCE: &str = include_str!("../../../examples/document-summarizer/main.ing");
struct Project {
dir: TempDir,
}
impl Project {
fn new(tag: &str, source: &str, configure_tools: bool) -> Project {
let dir = TempDir::new(tag);
let root = dir.path();
let data = root.join("data");
std::fs::create_dir_all(&data).expect("creating the sample data");
std::fs::write(data.join("README.md"), "# Sample\n\nA sample workspace.\n").unwrap();
std::fs::write(data.join("notes.md"), "notes\n").unwrap();
std::fs::write(root.join("secret.txt"), "do not read me\n").unwrap();
std::fs::write(root.join("main.ing"), source).expect("writing the source");
let mut manifest = String::from(
"[project]\nname = \"digest\"\n\n[build]\nentry = \"main.ing\"\nout-dir = \"target/ingot\"\n",
);
if configure_tools {
manifest.push_str(&format!(
"\n[mcp]\ntimeout-seconds = 10\n\n[[mcp.server]]\nname = \"workspace\"\ncommand = {}\nargs = [\"--root\", \"data\", \"--allow-write\"]\npass-env = [\"MCP_TEST_SECRET\"]\n",
toml_string(&fs_server().display().to_string())
));
}
std::fs::write(root.join("ingot.toml"), manifest).expect("writing the manifest");
Project { dir }
}
fn path(&self) -> String {
self.dir.path().display().to_string()
}
fn workspace(&self) -> &Path {
self.dir.path()
}
}
fn digest_args(project: &Project) -> Vec<String> {
vec![
"run".to_string(),
project.path(),
"--input".to_string(),
"directory=.".to_string(),
"--input".to_string(),
"out=out/digest.md".to_string(),
"--events".to_string(),
"quiet".to_string(),
]
}
fn as_args(owned: &[String]) -> Vec<&str> {
owned.iter().map(String::as_str).collect()
}
#[test]
fn a_run_reaches_real_tools_over_stdio_and_the_bytes_land_on_disk() {
let project = Project::new("digest", DIGEST_SOURCE, true);
let stub = stub_provider(vec![text_reply("# Digest\n\nTwo markdown files.\n")]);
let args = digest_args(&project);
let output = run(&as_args(&args), Some(&stub.url));
assert_eq!(code(&output), EXIT_OK, "{}", stderr(&output));
assert!(stdout(&output).contains("# Digest"), "{}", stdout(&output));
let written = project.workspace().join("data/out/digest.md");
assert!(written.is_file(), "expected {}", written.display());
assert_eq!(
std::fs::read_to_string(&written).unwrap(),
"# Digest\n\nTwo markdown files.\n"
);
}
#[test]
fn the_resolved_routing_is_reported_before_the_run() {
let project = Project::new("routing", DIGEST_SOURCE, true);
let stub = stub_provider(vec![text_reply("# Digest")]);
let args = digest_args(&project);
let output = run(&as_args(&args), Some(&stub.url));
assert_eq!(code(&output), EXIT_OK, "{}", stderr(&output));
let log = stderr(&output);
assert!(
log.contains("tool fs.read_file <- workspace:fs.read_file"),
"{log}"
);
assert!(
log.contains("tool fs.write_file <- workspace:fs.write_file"),
"{log}"
);
}
#[test]
fn a_recorded_run_replays_with_the_tools_still_live() {
let project = Project::new("replay", DIGEST_SOURCE, true);
let cassette = project.workspace().join("digest.json");
let stub = stub_provider(vec![text_reply("# Recorded digest\n")]);
let mut record = digest_args(&project);
record.push("--record".to_string());
record.push(cassette.display().to_string());
let recorded = run(&as_args(&record), Some(&stub.url));
assert_eq!(code(&recorded), EXIT_OK, "{}", stderr(&recorded));
assert!(cassette.is_file());
let written = project.workspace().join("data/out/digest.md");
assert!(written.is_file(), "the first run must have written it");
std::fs::remove_dir_all(project.workspace().join("data/out")).unwrap();
let mut replay = digest_args(&project);
replay.push("--provider".to_string());
replay.push("replay".to_string());
replay.push("--cassette".to_string());
replay.push(cassette.display().to_string());
let replayed = run(&as_args(&replay), None);
assert_eq!(code(&replayed), EXIT_OK, "{}", stderr(&replayed));
assert_eq!(stdout(&replayed).trim(), "# Recorded digest");
assert_eq!(
std::fs::read_to_string(&written).unwrap(),
"# Recorded digest\n"
);
}
#[test]
fn no_tools_makes_the_agent_stop_at_the_call_naming_the_tool() {
let project = Project::new("no-tools", DIGEST_SOURCE, true);
let stub = stub_provider(vec![text_reply("# never reached")]);
let mut args = digest_args(&project);
args.push("--no-tools".to_string());
let output = run(&as_args(&args), Some(&stub.url));
assert_eq!(code(&output), EXIT_DIAGNOSTICS);
let message = stderr(&output);
assert!(message.contains("fs.list_dir"), "{message}");
assert!(message.contains("no host provides"), "{message}");
}
#[test]
fn a_project_with_tools_and_no_server_warns_before_it_fails() {
let project = Project::new("unconfigured", DIGEST_SOURCE, false);
let stub = stub_provider(vec![text_reply("# never reached")]);
let args = digest_args(&project);
let output = run(&as_args(&args), Some(&stub.url));
assert_eq!(code(&output), EXIT_DIAGNOSTICS);
let message = stderr(&output);
assert!(message.contains("configures no MCP server"), "{message}");
assert!(message.contains("ingot tools"), "{message}");
}
const ESCAPE_SOURCE: &str = r#"language 0.1
tool fs.read_file(path: string) -> text !filesystem_read
agent Escape() -> leak<markdown> {
model requires {
structured_output
}
tools {
mcp fs.read_file
}
budget {
steps <= 4
}
policy {
filesystem_read allow ["."]
network deny
}
flow {
stolen = call fs.read_file("../secret.txt")
emit leak = ask<markdown>("Report: ${stolen}")
}
}
"#;
#[test]
fn a_path_out_of_the_server_root_is_refused_even_though_the_policy_allows_reading() {
let project = Project::new("escape", ESCAPE_SOURCE, true);
let stub = stub_provider(vec![text_reply("# never reached")]);
let output = run(
&["run", &project.path(), "--events", "quiet"],
Some(&stub.url),
);
assert_eq!(code(&output), EXIT_DIAGNOSTICS, "{}", stdout(&output));
let message = stderr(&output);
assert!(message.contains("refused"), "{message}");
assert!(
!message.contains("do not read me"),
"the file's contents must never appear: {message}"
);
}
#[test]
fn ingot_tools_lists_the_servers_and_the_routing() {
let project = Project::new("tools-ok", DIGEST_SOURCE, true);
let output = run(&["tools", &project.path()], None);
assert_eq!(code(&output), EXIT_OK, "{}", stderr(&output));
let listing = stdout(&output);
assert!(listing.contains("ingot-mcp-fs"), "{listing}");
assert!(listing.contains("fs.write_file"), "{listing}");
assert!(listing.contains("-> workspace:fs.list_dir"), "{listing}");
}
#[test]
fn ingot_tools_json_is_typed_machine_readable_and_never_contains_env_values() {
let project = Project::new("tools-json", DIGEST_SOURCE, true);
let output = run_env(
&["tools", "--json", &project.path()],
&[("MCP_TEST_SECRET", "do-not-print-this-value")],
);
assert_eq!(code(&output), EXIT_OK, "{}", stderr(&output));
let listing = stdout(&output);
assert!(!listing.contains("do-not-print-this-value"), "{listing}");
let report: serde_json::Value = serde_json::from_str(&listing).expect("valid JSON report");
assert_eq!(report["schemaVersion"], 1);
assert_eq!(report["ready"], true);
assert_eq!(
report["requiredEnvironment"],
serde_json::json!(["MCP_TEST_SECRET"])
);
assert_eq!(report["servers"][0]["manifestName"], "workspace");
assert_eq!(
report["servers"][0]["tools"][0]["inputSchema"]["type"],
"object"
);
assert!(report["servers"][0]["tools"]
.as_array()
.unwrap()
.iter()
.any(|tool| tool.get("outputSchema").is_some()));
assert!(report["declaredTools"]
.as_array()
.unwrap()
.iter()
.all(|tool| tool["schemaCompatibility"]["status"] == "match"));
}
#[test]
fn ingot_tools_preflight_rejects_source_schema_drift() {
let source = DIGEST_SOURCE
.replace("content: text", "content: int")
.replace(
"call fs.write_file(out, summary)",
"call fs.write_file(out, 1)",
);
let project = Project::new("tools-drift", &source, true);
let output = run(&["tools", "--json", &project.path()], None);
assert_eq!(code(&output), EXIT_DIAGNOSTICS, "{}", stderr(&output));
let report: serde_json::Value =
serde_json::from_str(&stdout(&output)).expect("valid JSON report");
assert_eq!(report["ready"], false);
let write = report["declaredTools"]
.as_array()
.unwrap()
.iter()
.find(|tool| tool["name"] == "fs.write_file")
.expect("write tool");
assert_eq!(write["schemaCompatibility"]["status"], "drift");
assert!(write["schemaCompatibility"]["issues"]
.as_array()
.unwrap()
.iter()
.any(|problem| problem["code"] == "MCP_SCHEMA_TYPE_MISMATCH"));
}
#[test]
fn ingot_tools_proposes_typed_source_without_writing_project_files() {
let project = Project::new("tools-source-proposal", NO_TOOLS_SOURCE, true);
let source_path = project.workspace().join("main.ing");
let manifest_path = project.workspace().join("ingot.toml");
let source_before = std::fs::read(&source_path).unwrap();
let manifest_before = std::fs::read(&manifest_path).unwrap();
let output = run(&["tools", "--propose", &project.path()], None);
assert_eq!(code(&output), EXIT_OK, "{}", stderr(&output));
let listing = stdout(&output);
assert!(
listing.contains("authoring proposals (nothing was written)"),
"{listing}"
);
assert!(
listing.contains("tool fs.list_dir(path: string) -> string[] !TODO_EFFECT"),
"{listing}"
);
assert!(listing.contains("replace `TODO_EFFECT`"), "{listing}");
assert_eq!(std::fs::read(source_path).unwrap(), source_before);
assert_eq!(std::fs::read(manifest_path).unwrap(), manifest_before);
}
#[test]
fn ingot_tools_proposes_an_unambiguous_manifest_alias() {
let source = DIGEST_SOURCE.replace("fs.read_file", "repo.read_file");
let project = Project::new("tools-manifest-proposal", &source, true);
let output = run(&["tools", "--json", &project.path()], None);
assert_eq!(code(&output), EXIT_DIAGNOSTICS, "{}", stderr(&output));
let report: serde_json::Value =
serde_json::from_str(&stdout(&output)).expect("valid JSON report");
let proposals = report["proposals"]["manifest"]
.as_array()
.expect("manifest proposals");
assert_eq!(proposals.len(), 1, "{report:#}");
assert_eq!(proposals[0]["tool"], "repo.read_file");
assert_eq!(proposals[0]["server"], "workspace");
assert_eq!(proposals[0]["remote"], "fs.read_file");
assert!(proposals[0]["stanza"]
.as_str()
.unwrap()
.contains("\"repo.read_file\" = \"fs.read_file\""));
assert!(report["proposals"]["source"].as_array().unwrap().is_empty());
}
#[test]
fn ingot_tools_exits_non_zero_when_a_declared_tool_has_no_server() {
let project = Project::new("tools-missing", DIGEST_SOURCE, false);
let output = run(&["tools", &project.path()], None);
assert_eq!(code(&output), EXIT_DIAGNOSTICS);
let listing = stdout(&output);
assert!(listing.contains("no MCP server is configured"), "{listing}");
assert!(listing.contains("fs.read_file"), "{listing}");
assert!(listing.contains("[[mcp.server]]"), "{listing}");
}
#[test]
fn sandbox_derives_the_boundary_from_the_policy_and_names_its_source() {
let project = Project::new("sandbox", DIGEST_SOURCE, true);
let output = run(&["sandbox", &project.path()], None);
assert_eq!(code(&output), EXIT_OK, "{}", stderr(&output));
let plan = stdout(&output);
assert!(plan.contains("/workspace/data ro"), "{plan}");
assert!(plan.contains("/workspace/data/out rw"), "{plan}");
assert!(plan.contains("filesystem_read allow [\"data\"]"), "{plan}");
assert!(plan.contains("network none"), "{plan}");
assert!(
plan.contains("every policy rule above is enforced"),
"{plan}"
);
}
#[test]
fn sandbox_refuses_a_policy_path_that_is_not_there() {
let source = DIGEST_SOURCE.replace(
"filesystem_read allow [\"data\"]",
"filesystem_read allow [\"absent\"]",
);
let project = Project::new("sandbox-missing", &source, true);
let output = run(&["sandbox", &project.path()], None);
assert_eq!(code(&output), EXIT_DIAGNOSTICS);
let message = stderr(&output);
assert!(message.contains("absent"), "{message}");
assert!(message.contains("does not exist"), "{message}");
assert!(message.contains("--workspace"), "{message}");
}
#[test]
fn the_workspace_can_be_moved_from_the_command_line() {
let project = Project::new("sandbox-workspace", DIGEST_SOURCE, true);
let elsewhere = TempDir::new("sandbox-elsewhere");
let output = run(
&[
"sandbox",
&project.path(),
"--workspace",
&elsewhere.path().display().to_string(),
],
None,
);
assert_eq!(code(&output), EXIT_DIAGNOSTICS, "{}", stdout(&output));
assert!(
stderr(&output).contains("does not exist"),
"{}",
stderr(&output)
);
}
#[test]
fn sandbox_plans_are_machine_readable() {
let project = Project::new("sandbox-json", DIGEST_SOURCE, true);
let output = run(&["sandbox", &project.path(), "--json"], None);
assert_eq!(code(&output), EXIT_OK, "{}", stderr(&output));
let plans: serde_json::Value =
serde_json::from_str(&stdout(&output)).expect("--json must emit JSON on stdout");
let plan = &plans[0];
assert_eq!(plan["network"]["mode"], "none");
assert_eq!(plan["workdir"], "/workspace");
assert_eq!(plan["mounts"][0]["guest"], "/workspace/data");
assert_eq!(plan["mounts"][0]["writable"], false);
assert_eq!(plan["unenforceable"].as_array().unwrap().len(), 0);
}
#[test]
fn sandbox_says_so_when_nothing_would_be_contained() {
let project = Project::new("sandbox-untooled", DIGEST_SOURCE, false);
let output = run(&["sandbox", &project.path()], None);
assert_eq!(code(&output), EXIT_OK);
assert!(
stderr(&output).contains("nothing would be contained"),
"{}",
stderr(&output)
);
}
#[test]
fn run_sandbox_refuses_before_starting_anything_it_cannot_enforce() {
let source = DIGEST_SOURCE.replace("network deny", "network allow [\"example.org\"]");
let project = Project::new("sandbox-unenforced", &source, true);
let stub = stub_provider(vec![text_reply("# never reached")]);
let mut args = digest_args(&project);
args.push("--sandbox".to_string());
let owned = as_args(&args);
let output = run_env(
&owned,
&[
("ANTHROPIC_API_KEY", "stub-key"),
("INGOT_ANTHROPIC_BASE_URL", &stub.url),
("INGOT_EGRESS_IMAGE", "ingot/egress:nothing-built-this"),
],
);
assert_ne!(code(&output), EXIT_OK);
let message = stderr(&output);
assert!(message.contains("cannot honour every rule"), "{message}");
assert!(message.contains("example.org"), "{message}");
assert!(message.contains("--sandbox-allow-unenforced"), "{message}");
assert!(message.contains("tools/egress.Dockerfile"), "{message}");
}
#[test]
fn run_sandbox_reports_a_policy_path_that_is_not_there() {
let source = DIGEST_SOURCE.replace(
"filesystem_read allow [\"data\"]",
"filesystem_read allow [\"absent\"]",
);
let project = Project::new("sandbox-run-missing", &source, true);
let stub = stub_provider(vec![text_reply("# never reached")]);
let mut args = digest_args(&project);
args.push("--sandbox".to_string());
let output = run(&as_args(&args), Some(&stub.url));
assert_ne!(code(&output), EXIT_OK);
assert!(
stderr(&output).contains("does not exist"),
"{}",
stderr(&output)
);
}
#[test]
fn a_run_says_whether_the_policy_is_enforced_or_merely_checked() {
let project = Project::new("sandbox-unstated", DIGEST_SOURCE, true);
let stub = stub_provider(vec![text_reply("# Digest")]);
let output = run(&as_args(&digest_args(&project)), Some(&stub.url));
assert_eq!(code(&output), EXIT_OK, "{}", stderr(&output));
assert!(
stderr(&output).contains("checked, not enforced"),
"{}",
stderr(&output)
);
}
#[test]
fn a_program_without_tools_needs_no_servers() {
let path = repo_root()
.join("examples/document-summarizer")
.display()
.to_string();
let output = run(&["tools", &path], None);
assert_eq!(code(&output), EXIT_OK, "{}", stderr(&output));
assert!(
stdout(&output).contains("declares no tools"),
"{}",
stdout(&output)
);
}
#[test]
fn model_assisted_authoring_writes_against_routed_tool_schemas() {
let project = Project::new("authoring-routed-tools", DIGEST_SOURCE, true);
let kept = DIGEST_SOURCE.replace(
"Write a digest of this directory for someone seeing it for the first time.",
"Write a short digest of this directory.",
);
let cassette = authoring_cassette(project.workspace(), "kept.json", &[&kept]);
let output = run(
&[
"new",
"--project",
&project.path(),
"--provider",
"replay",
"--cassette",
&cassette.display().to_string(),
"--max-repairs",
"0",
"shorten",
"the",
"digest",
],
None,
);
assert_eq!(code(&output), EXIT_OK, "{}", stderr(&output));
assert!(stdout(&output).contains("@@ "), "{}", stdout(&output));
let invented = DIGEST_SOURCE.replace(
"/// Summarises a directory",
"/// Removes a file from the workspace.\n\
tool fs.delete_file(path: string) -> bool !filesystem_write\n\n\
/// Summarises a directory",
);
let cassette = authoring_cassette(project.workspace(), "invented.json", &[&invented]);
let output = run(
&[
"new",
"--project",
&project.path(),
"--provider",
"replay",
"--cassette",
&cassette.display().to_string(),
"--max-repairs",
"0",
"allow",
"deleting",
"a",
"file",
],
None,
);
assert_eq!(code(&output), EXIT_DIAGNOSTICS, "{}", stderr(&output));
let out = stdout(&output);
assert!(out.contains("AUTHORING_UNROUTED_TOOL"), "{out}");
assert!(out.contains("fs.delete_file"), "{out}");
assert!(out.contains("run `ingot tools` to see them"), "{out}");
assert_eq!(
std::fs::read_to_string(project.workspace().join("main.ing")).expect("source"),
DIGEST_SOURCE,
"a refused proposal must leave the project alone"
);
}
#[test]
fn a_tool_using_agent_can_be_tested_offline_after_one_recording() {
let project = Project::new("cassette-tools", DIGEST_SOURCE, true);
let stub = stub_provider(vec![text_reply("# Digest\n\nTwo markdown files.\n")]);
let cassette = project.workspace().join("tests/cassettes/example.json");
let mut args = digest_args(&project);
args.push("--record".to_string());
args.push(cassette.display().to_string());
let recorded = run(&as_args(&args), Some(&stub.url));
assert_eq!(code(&recorded), EXIT_OK, "{}", stderr(&recorded));
assert!(
stderr(&recorded).contains("tool call(s) to"),
"the recording must say what it captured:\n{}",
stderr(&recorded)
);
let written: serde_json::Value =
serde_json::from_str(&std::fs::read_to_string(&cassette).expect("a cassette"))
.expect("canonical json");
assert_eq!(written["cassetteVersion"], "0.2");
let calls = written["toolCalls"]
.as_array()
.expect("recorded tool calls");
assert_eq!(
calls.len(),
3,
"the digest agent lists, reads and writes:\n{written:#}"
);
for call in calls {
assert!(
call["invocationDigest"]
.as_str()
.is_some_and(|d| d.len() == 64),
"every recorded call is keyed by a digest of the invocation: {call}"
);
}
assert_eq!(calls[0]["tool"], "fs.list_dir");
assert_eq!(calls[2]["tool"], "fs.write_file");
let output = run(&["test", &project.path()], None);
assert_eq!(
code(&output),
EXIT_OK,
"a tool-using agent must be testable offline:\n{}\n{}",
stdout(&output),
stderr(&output)
);
assert!(stdout(&output).contains("1 passed"), "{}", stdout(&output));
let written_by_replay = project.workspace().join("data/out/digest.md");
std::fs::remove_file(&written_by_replay).expect("the recorded run wrote it");
let again = run(&["test", &project.path()], None);
assert_eq!(code(&again), EXIT_OK, "{}", stderr(&again));
assert!(
!written_by_replay.exists(),
"replay must return the recorded result, not repeat the effect"
);
}
#[test]
fn a_tool_call_that_changed_since_recording_fails_loudly() {
let project = Project::new("cassette-tools-drift", DIGEST_SOURCE, true);
let stub = stub_provider(vec![text_reply("# Digest\n\nTwo markdown files.\n")]);
let cassette = project.workspace().join("tests/cassettes/example.json");
let mut args = digest_args(&project);
args.push("--record".to_string());
args.push(cassette.display().to_string());
assert_eq!(
code(&run(&as_args(&args), Some(&stub.url))),
EXIT_OK,
"the recording must succeed first"
);
let source = std::fs::read_to_string(project.workspace().join("main.ing")).expect("source");
std::fs::write(
project.workspace().join("main.ing"),
source.replace(
r#"call fs.read_file("README.md")"#,
r#"call fs.read_file("notes.md")"#,
),
)
.expect("editing the source");
let output = run(&["test", &project.path()], None);
assert_eq!(code(&output), EXIT_DIAGNOSTICS, "{}", stdout(&output));
let message = format!("{}{}", stdout(&output), stderr(&output));
assert!(message.contains("re-record"), "{message}");
}