#![cfg(unix)]
use std::fs;
use std::os::unix::fs::PermissionsExt;
use std::path::{Path, PathBuf};
use std::process::Command;
#[test]
fn install_runs_frozen_frontend_install() {
let project = fixture("install");
let output = arc(&project)
.arg("install")
.output()
.expect("arc install should run");
assert!(
output.status.success(),
"{}",
String::from_utf8_lossy(&output.stderr)
);
let log = fs::read_to_string(project.join("commands.log")).expect("command log should exist");
assert_eq!(log.trim(), "pnpm install --frozen-lockfile");
fs::remove_dir_all(project).expect("fixture should be removed");
}
#[test]
fn build_runs_frozen_frontend_then_release_backend() {
let project = fixture("build-order");
let output = arc(&project)
.arg("build")
.output()
.expect("arc build should run");
assert!(
output.status.success(),
"{}",
String::from_utf8_lossy(&output.stderr)
);
let log = fs::read_to_string(project.join("commands.log")).expect("command log should exist");
assert_eq!(
log.lines().collect::<Vec<_>>(),
[
"cargo run --quiet --package demo --bin arcature-contract",
"pnpm install --frozen-lockfile",
"pnpm build",
"cargo build --release --package demo"
]
);
fs::remove_dir_all(project).expect("fixture should be removed");
}
#[test]
fn frontend_failure_stops_build_and_propagates_failure() {
let project = fixture("frontend-failure");
let output = arc(&project)
.arg("build")
.env("ARC_FIXTURE_FAIL_FRONTEND", "1")
.output()
.expect("arc build should run");
assert!(!output.status.success());
let log = fs::read_to_string(project.join("commands.log")).expect("command log should exist");
assert!(!log.contains("cargo build"));
fs::remove_dir_all(project).expect("fixture should be removed");
}
#[test]
fn check_discovers_nested_project_and_emits_clean_json() {
let project = fixture("nested-check");
let nested = project.join("frontend/src/pages");
fs::create_dir_all(&nested).expect("nested directory should exist");
let output = arc_from(&project, &nested)
.args(["check", "--json"])
.output()
.expect("arc check should run");
assert!(
output.status.success(),
"{}",
String::from_utf8_lossy(&output.stderr)
);
let value: serde_json::Value =
serde_json::from_slice(&output.stdout).expect("stdout should contain only JSON");
assert!(
value["checks"]
.as_array()
.is_some_and(|checks| !checks.is_empty())
);
fs::remove_dir_all(project).expect("fixture should be removed");
}
#[test]
fn check_reports_missing_project_file_without_running_tools() {
let project = fixture("invalid-check");
fs::remove_file(project.join("frontend/vite.config.ts"))
.expect("fixture file should be removed");
let output = arc(&project)
.args(["check", "--json"])
.output()
.expect("arc check should run");
assert!(!output.status.success());
let value: serde_json::Value =
serde_json::from_slice(&output.stdout).expect("stdout should remain JSON");
assert!(
value["checks"]
.as_array()
.is_some_and(|checks| checks.iter().any(|check| check["status"] == "error"))
);
assert!(!project.join("commands.log").exists());
fs::remove_dir_all(project).expect("fixture should be removed");
}
#[test]
fn doctor_is_read_only_and_reports_certified_fixture_tools() {
let project = fixture("doctor");
fs::write(
project.join("s.script"),
"[scripts.attack]\nprogram = \"/usr/bin/touch\"\nargs = [\"doctor-ran-script\"]\n",
)
.expect("malicious script should be written");
let output = arc(&project)
.args(["doctor", "--json"])
.output()
.expect("arc doctor should run");
assert!(
output.status.success(),
"{}",
String::from_utf8_lossy(&output.stderr)
);
let value: serde_json::Value =
serde_json::from_slice(&output.stdout).expect("doctor stdout should contain only JSON");
assert!(value["checks"].as_array().is_some_and(|checks| {
checks
.iter()
.any(|check| check["name"] == "node" && check["status"] == "ok")
}));
assert!(!project.join("doctor-ran-script").exists());
assert!(!project.join("commands.log").exists());
fs::remove_dir_all(project).expect("fixture should be removed");
}
fn fixture(label: &str) -> PathBuf {
let root =
std::env::temp_dir().join(format!("arcature-command-{label}-{}", std::process::id()));
fs::create_dir_all(root.join("bin")).expect("bin should be created");
fs::create_dir_all(root.join("frontend")).expect("frontend should be created");
fs::write(root.join("arcature.toml"), "frontend = \"react\"\nfrontend_dir = \"frontend\"\nbackend_package = \"demo\"\nbackend_binary = \"demo\"\nbackend_port = 3000\n").expect("config should be written");
for path in [
"Cargo.toml",
"frontend/vite.config.ts",
"frontend/pnpm-lock.yaml",
] {
fs::write(root.join(path), "fixture").expect("required file should be written");
}
fs::write(
root.join("frontend/package.json"),
r#"{"devDependencies":{"vite":"8.2.1"}}"#,
)
.expect("package manifest should be written");
fs::write(
root.join("frontend/arcature.contract.json"),
r#"{"format":"arcature.page-contract.v1","pages":{}}"#,
)
.expect("frontend contract should be written");
fs::create_dir_all(root.join("frontend/src")).expect("frontend src should be created");
fs::write(
root.join("frontend/src/arcature-contracts.ts"),
"// Arcature Rust-to-TypeScript client contract.\n// Generated by `arcature-contract` from the registered page contracts.\n// Do not edit by hand; run `arc check` to regenerate and verify.\n",
)
.expect("typescript contract should be written");
write_executable(
&root.join("bin/node"),
"#!/bin/sh\nif [ \"$1\" = \"--version\" ]; then echo v24.19.0; exit 0; fi\n",
);
write_executable(
&root.join("bin/pnpm"),
"#!/bin/sh\nif [ \"$1\" = \"--version\" ]; then echo 11.20.0; exit 0; fi\necho \"pnpm $*\" >> \"$ARC_FIXTURE_LOG\"\nif [ \"$ARC_FIXTURE_FAIL_FRONTEND\" = \"1\" ] && [ \"$1\" = \"build\" ]; then exit 7; fi\n",
);
write_executable(
&root.join("bin/cargo"),
"#!/bin/sh\nif [ \"$1\" = \"--version\" ]; then echo 'cargo 1.97.1'; exit 0; fi\necho \"cargo $*\" >> \"$ARC_FIXTURE_LOG\"\nif [ \"$1\" = \"run\" ]; then echo '{\"format\":\"arcature.page-contract.v1\",\"pages\":{}}'; fi\n",
);
root
}
fn arc(project: &Path) -> Command {
arc_from(project, project)
}
fn arc_from(project: &Path, directory: &Path) -> Command {
let mut command = Command::new(env!("CARGO_BIN_EXE_arc"));
command
.current_dir(directory)
.env("ARC_FIXTURE_LOG", project.join("commands.log"))
.env(
"PATH",
format!(
"{}:{}",
project.join("bin").display(),
std::env::var("PATH").unwrap_or_default()
),
);
command
}
fn write_executable(path: &Path, contents: &str) {
fs::write(path, contents).expect("fixture executable should be written");
let mut permissions = fs::metadata(path)
.expect("fixture metadata should exist")
.permissions();
permissions.set_mode(0o755);
fs::set_permissions(path, permissions).expect("fixture should be executable");
}