use std::fs;
use std::path::PathBuf;
use std::process::Command;
use std::time::{SystemTime, UNIX_EPOCH};
fn repo_root() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()
.and_then(|crates| crates.parent())
.expect("crate lives under <repo>/crates/zynk-cli")
.to_path_buf()
}
fn temp_dir(name: &str) -> PathBuf {
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("system time after unix epoch")
.as_nanos();
std::env::temp_dir().join(format!("zynk-cli-{name}-{}-{nanos}", std::process::id()))
}
fn zynk_bin() -> &'static str {
env!("CARGO_BIN_EXE_zynk")
}
fn uv_python() -> PathBuf {
repo_root().join("bindings/python/.venv/bin/python")
}
#[test]
fn gen_typescript_python_writes_api_and_internal_files() {
let out = temp_dir("typescript");
let output = Command::new(zynk_bin())
.args(["gen", "typescript", "--target", "python", "--out"])
.arg(&out)
.args(["--app", "tests.fixtures.roundtrip_schema_fixture:bridge"])
.arg("--python")
.arg(uv_python())
.current_dir(repo_root().join("bindings/python"))
.output()
.expect("run zynk gen typescript");
assert!(
output.status.success(),
"stdout:\n{}\nstderr:\n{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
assert!(out.join("api.ts").is_file());
assert!(out.join("_internal.ts").is_file());
let api = fs::read_to_string(out.join("api.ts")).expect("read api.ts");
assert!(api.contains("Generated by zynk-gen-ts"));
assert!(api.contains("export async function updateProfile"));
}
#[test]
fn gen_effect_python_writes_api_and_effect_internal_files() {
let out = temp_dir("effect");
let output = Command::new(zynk_bin())
.args(["gen", "effect", "--target", "python", "--out"])
.arg(&out)
.args(["--app", "tests.fixtures.roundtrip_schema_fixture:bridge"])
.arg("--python")
.arg(uv_python())
.current_dir(repo_root().join("bindings/python"))
.output()
.expect("run zynk gen effect");
assert!(
output.status.success(),
"stdout:\n{}\nstderr:\n{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
assert!(out.join("api.ts").is_file());
assert!(out.join("_effect_internal.ts").is_file());
let api = fs::read_to_string(out.join("api.ts")).expect("read api.ts");
assert!(api.contains("Auto-generated by Zynk Effect connector"));
assert!(api.contains("export const updateProfile"));
}
#[test]
fn python_import_errors_exit_nonzero_and_surface_stderr() {
let out = temp_dir("bad-import");
let output = Command::new(zynk_bin())
.args(["gen", "typescript", "--target", "python", "--out"])
.arg(&out)
.args(["--app", "nonexistent:bad"])
.arg("--python")
.arg(uv_python())
.current_dir(repo_root().join("bindings/python"))
.output()
.expect("run zynk gen bad import");
assert!(!output.status.success());
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("ModuleNotFoundError"),
"stderr was:\n{stderr}"
);
assert!(
stderr.contains("user app schema dump subprocess failed"),
"stderr was:\n{stderr}"
);
assert!(!out.join("api.ts").exists());
}
#[test]
fn garbage_stdout_exits_nonzero_without_writing_api() {
let fixture_dir = temp_dir("garbage-fixture");
fs::create_dir_all(&fixture_dir).expect("create fixture dir");
fs::write(
fixture_dir.join("garbage_app.py"),
"class App:\n def dump_schema_json(self):\n return '{not json'\nbridge = App()\n",
)
.expect("write fixture");
let out = temp_dir("garbage-out");
let output = Command::new(zynk_bin())
.args(["gen", "typescript", "--target", "python", "--out"])
.arg(&out)
.args(["--app", "garbage_app:bridge"])
.current_dir(&fixture_dir)
.output()
.expect("run zynk gen garbage stdout");
assert!(!output.status.success());
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("failed to parse schema dump stdout as ApiGraph JSON"),
"stderr was:\n{stderr}"
);
assert!(!out.join("api.ts").exists());
}