use assert_cmd::Command;
use predicates::prelude::*;
use rusqlite::Connection;
use std::fs;
use std::process::Command as SysCommand;
use tempfile::tempdir;
fn jan() -> Command {
Command::cargo_bin("jan").unwrap()
}
#[test]
fn help_root_lists_android() {
let dir = tempdir().unwrap();
let spec = dir.path().join("spec.yaml");
fs::write(
&spec,
r#"
metadata:
name: demo
description: Root description line.
commands:
android:
about: Android tools
commands:
list:
about: List
exec:
argv: ["echo", "ok"]
"#,
)
.unwrap();
jan()
.current_dir(dir.path())
.args(["--spec", spec.to_str().unwrap(), "--no-log"])
.assert()
.success()
.stdout(predicate::str::contains("android"))
.stdout(predicate::str::contains("Root description"));
}
#[test]
fn nested_exec_runs() {
let dir = tempdir().unwrap();
let spec = dir.path().join("spec.yaml");
fs::write(
&spec,
r#"
metadata:
name: demo
commands:
a:
about: A
commands:
b:
about: B
commands:
c:
about: C
exec:
argv: ["echo", "leaf"]
"#,
)
.unwrap();
jan()
.current_dir(dir.path())
.args(["--spec", spec.to_str().unwrap(), "--no-log", "a", "b", "c"])
.assert()
.success()
.stdout(predicate::str::contains("leaf"));
}
#[test]
fn passthrough_forwards_args() {
let dir = tempdir().unwrap();
let spec = dir.path().join("spec.yaml");
fs::write(
&spec,
r#"
metadata:
name: demo
commands:
run:
about: Run
exec:
argv: ["echo", "start"]
passthrough: true
"#,
)
.unwrap();
jan()
.current_dir(dir.path())
.args([
"--spec",
spec.to_str().unwrap(),
"--no-log",
"run",
"extra",
"tokens",
])
.assert()
.success()
.stdout(predicate::str::contains("start extra tokens"));
}
#[test]
fn well_known_spec_help_lists_commands() {
let install = tempdir().unwrap();
fs::write(
install.path().join("scripts.spec.yaml"),
r#"
metadata:
name: demo
commands:
alpha:
about: Alpha tool
exec:
argv: ["echo", "alpha"]
beta:
about: Beta tool
exec:
argv: ["echo", "beta"]
"#,
)
.unwrap();
let cwd = tempdir().unwrap();
jan()
.current_dir(cwd.path())
.env("JAN_INSTALL_DIR", install.path())
.env("JAN_OS", "linux")
.args(["--no-log"])
.assert()
.success()
.stdout(predicate::str::contains("alpha"))
.stdout(predicate::str::contains("beta"));
}
#[test]
fn examples_default_git_r_passthrough() {
let dir = tempdir().unwrap();
let examples = jan_cli_manifest_dir().join("examples");
SysCommand::new("git")
.args(["init"])
.current_dir(dir.path())
.status()
.expect("git init");
jan()
.current_dir(dir.path())
.args([
"--spec-dir",
examples.to_str().unwrap(),
"--spec-root",
"default.spec.yaml",
"--no-log",
"git",
"r",
"rev-parse",
"--is-inside-work-tree",
])
.assert()
.success()
.stdout(predicate::str::contains("true"));
}
#[test]
fn spec_include_resolves_subtree_from_disk() {
let dir = tempdir().unwrap();
let sub = dir.path().join("sub");
fs::create_dir_all(&sub).unwrap();
fs::write(
sub.join("leaf.yaml"),
r#"
about: From include
exec:
argv: ["echo", "included"]
"#,
)
.unwrap();
let spec = dir.path().join("root.yaml");
fs::write(
&spec,
r#"
metadata:
name: inc-demo
commands:
outer:
include: sub/leaf.yaml
"#,
)
.unwrap();
jan()
.current_dir(dir.path())
.args(["--spec", spec.to_str().unwrap(), "--no-log", "outer"])
.assert()
.success()
.stdout(predicate::str::contains("included"));
}
#[test]
fn os_filter_hides_command_on_mismatch() {
let dir = tempdir().unwrap();
let spec = dir.path().join("os.yaml");
fs::write(
&spec,
r#"
metadata:
name: os-demo
commands:
only_win:
os: [windows]
about: Hidden on Linux
exec:
argv: ["echo", "win"]
always:
about: Shown
exec:
argv: ["echo", "ok"]
"#,
)
.unwrap();
let assert = jan()
.current_dir(dir.path())
.env("JAN_OS", "linux")
.args(["--spec", spec.to_str().unwrap(), "--no-log"])
.assert()
.success();
let stdout = String::from_utf8_lossy(&assert.get_output().stdout);
assert!(stdout.contains("always"));
assert!(!stdout.contains("only_win"));
}
#[test]
fn sqlite_log_written() {
let dir = tempdir().unwrap();
let spec = dir.path().join("spec.yaml");
fs::write(
&spec,
r#"
metadata:
name: demo
commands:
leaf:
about: Leaf
exec:
argv: ["echo", "x"]
"#,
)
.unwrap();
let db = dir.path().join("audit.db");
SysCommand::new("git")
.args(["init"])
.current_dir(dir.path())
.status()
.expect("git init");
jan()
.current_dir(dir.path())
.args([
"--spec",
spec.to_str().unwrap(),
"--db",
db.to_str().unwrap(),
"--branch",
"test-branch-2120",
"leaf",
])
.assert()
.success();
let conn = Connection::open(&db).unwrap();
let n: i64 = conn
.query_row(
"SELECT COUNT(*) FROM invocations WHERE git_branch = ?1",
["test-branch-2120"],
|r| r.get(0),
)
.unwrap();
assert!(n >= 1);
let spec_dir: String = conn
.query_row(
"SELECT s.spec_dir FROM invocations i JOIN spec_roots s ON i.spec_root_id = s.id \
WHERE i.git_branch = ?1 ORDER BY i.id DESC LIMIT 1",
["test-branch-2120"],
|r| r.get(0),
)
.unwrap();
let canon = spec.canonicalize().unwrap();
let expected_dir = canon.parent().unwrap().to_string_lossy().to_string();
assert_eq!(spec_dir, expected_dir);
}
#[test]
fn spec_dir_entry_loads_includes() {
let dir = tempdir().unwrap();
let spec_root = dir.path().join("jan.spec.yaml");
let sub = dir.path().join("sub");
fs::create_dir_all(&sub).unwrap();
fs::write(
sub.join("leaf.yaml"),
r#"
about: From include
exec:
argv: ["echo", "from-spec-dir"]
"#,
)
.unwrap();
fs::write(
&spec_root,
r#"
metadata:
name: dir-demo
commands:
outer:
include: sub/leaf.yaml
"#,
)
.unwrap();
jan()
.current_dir(dir.path())
.args([
"--spec-dir",
dir.path().to_str().unwrap(),
"--spec-root",
"jan.spec.yaml",
"--no-log",
"outer",
])
.assert()
.success()
.stdout(predicate::str::contains("from-spec-dir"));
}
#[test]
fn extra_spec_merges_commands() {
let install = tempdir().unwrap();
fs::write(install.path().join("scripts.spec.yaml"), "commands: {}\n").unwrap();
let cwd = tempdir().unwrap();
let extra = cwd.path().join("extra.yaml");
fs::write(
&extra,
r#"
commands:
zz_extra:
about: From merge
exec:
argv: ["echo", "merged"]
"#,
)
.unwrap();
jan()
.current_dir(cwd.path())
.env("JAN_INSTALL_DIR", install.path())
.env("JAN_OS", "linux")
.args([
"--no-log",
"--extra-spec",
extra.to_str().unwrap(),
"zz_extra",
])
.assert()
.success()
.stdout(predicate::str::contains("merged"));
}
#[test]
fn spec_dir_and_spec_file_mutually_exclusive() {
let dir = tempdir().unwrap();
let spec = dir.path().join("a.yaml");
fs::write(
&spec,
r#"
metadata:
name: x
commands:
leaf:
exec:
argv: ["echo", "x"]
"#,
)
.unwrap();
jan()
.current_dir(dir.path())
.args([
"--spec",
spec.to_str().unwrap(),
"--spec-dir",
dir.path().to_str().unwrap(),
"--no-log",
])
.assert()
.failure();
}
#[test]
fn script_dependencies_prepended_to_path() {
let dir = tempdir().unwrap();
let helper = dir.path().join("helper-bin");
fs::create_dir_all(&helper).unwrap();
let helper_script = helper.join("helper-cmd");
fs::write(&helper_script, "#!/bin/sh\nprintf 'from-helper'\n").unwrap();
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mut perms = fs::metadata(&helper_script).unwrap().permissions();
perms.set_mode(0o755);
fs::set_permissions(&helper_script, perms).unwrap();
}
let consumer = dir.path().join("consumer");
fs::create_dir_all(&consumer).unwrap();
let spec = dir.path().join("spec.yaml");
let helper_s = helper.to_string_lossy();
let consumer_s = consumer.to_string_lossy();
fs::write(
&spec,
format!(
r#"
metadata:
name: demo
commands:
helper:
path: {helper_s}
commands:
run:
exec:
argv: ["echo", "unused"]
consumer:
path: {consumer_s}
dependencies: [helper]
commands:
run:
exec:
argv: ["bash", "-lc", "helper-cmd"]
"#
),
)
.unwrap();
jan()
.current_dir(dir.path())
.args([
"--spec",
spec.to_str().unwrap(),
"--no-log",
"consumer",
"run",
])
.assert()
.success()
.stdout(predicate::str::contains("from-helper"));
}
#[test]
fn env_vars_passed_to_child() {
let dir = tempdir().unwrap();
let spec = dir.path().join("spec.yaml");
fs::write(
&spec,
r#"
metadata:
name: demo
commands:
outer:
env:
OUTER: outer-value
SHARED: from-outer
commands:
inner:
env:
INNER: inner-value
SHARED: from-inner
exec:
argv: ["bash", "-lc", "printf '%s %s %s' \"$OUTER\" \"$INNER\" \"$SHARED\""]
"#,
)
.unwrap();
jan()
.current_dir(dir.path())
.args([
"--spec",
spec.to_str().unwrap(),
"--no-log",
"outer",
"inner",
])
.assert()
.success()
.stdout(predicate::str::contains(
"outer-value inner-value from-inner",
));
}
#[test]
fn requires_blocks_when_utility_missing() {
let dir = tempdir().unwrap();
let spec = dir.path().join("spec.yaml");
fs::write(
&spec,
r#"
metadata:
name: demo
commands:
needs-fzf:
about: Needs fzf
requires: [definitely-not-a-real-binary-xyz-jan-test]
exec:
argv: ["echo", "should-not-run"]
"#,
)
.unwrap();
jan()
.current_dir(dir.path())
.args(["--spec", spec.to_str().unwrap(), "--no-log", "needs-fzf"])
.assert()
.failure()
.stderr(predicate::str::contains("missing required utilities"));
}
#[test]
fn well_known_spec_logs_spec_root() {
let install = tempdir().unwrap();
fs::write(
install.path().join("scripts.spec.yaml"),
r#"
commands:
leaf:
exec:
argv: ["echo", "logged"]
"#,
)
.unwrap();
let cwd = tempdir().unwrap();
let db = cwd.path().join("audit.db");
jan()
.current_dir(cwd.path())
.env("JAN_INSTALL_DIR", install.path())
.args([
"--db",
db.to_str().unwrap(),
"--branch",
"wk-branch",
"leaf",
])
.assert()
.success();
let conn = Connection::open(&db).unwrap();
let (spec_dir, root_yaml): (String, String) = conn
.query_row(
"SELECT s.spec_dir, s.root_yaml FROM invocations i JOIN spec_roots s ON i.spec_root_id = s.id \
WHERE i.git_branch = ?1 ORDER BY i.id DESC LIMIT 1",
["wk-branch"],
|r| Ok((r.get::<_, String>(0)?, r.get::<_, String>(1)?)),
)
.unwrap();
assert_eq!(
spec_dir,
install.path().canonicalize().unwrap().to_string_lossy()
);
assert_eq!(root_yaml, "scripts.spec.yaml");
}
fn jan_cli_manifest_dir() -> std::path::PathBuf {
std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
}
#[test]
fn scripts_bundle_dry_run_lists_portable_artifacts() {
let root = jan_cli_manifest_dir();
let assert = jan()
.current_dir(&root)
.args([
"--spec-dir",
".",
"--spec-root",
"scripts.spec.yaml",
"--no-log",
"bundle",
"--dry-run",
])
.assert()
.success();
let stdout = String::from_utf8_lossy(&assert.get_output().stdout);
assert!(stdout.contains("scripts.spec.yaml"));
assert!(stdout.contains("generated/scripts/scripts.yaml"));
assert!(stdout.contains("env.sh"));
assert!(stdout.contains("manifest.json"));
}
#[test]
fn scripts_alias_includes_spec_flags_and_sum() {
let root = jan_cli_manifest_dir();
let assert = jan()
.current_dir(&root)
.args([
"--spec-dir",
".",
"--spec-root",
"scripts.spec.yaml",
"--no-log",
"alias",
])
.assert()
.success();
let stdout = String::from_utf8_lossy(&assert.get_output().stdout);
assert!(stdout.contains("--spec-dir"));
assert!(stdout.contains("--spec-root"));
assert!(stdout.contains("scripts.spec.yaml"));
assert!(stdout.contains("alias sum="));
assert!(stdout.contains("scripts misc sum run"));
}
#[test]
fn scripts_inlined_basename_run() {
let root = jan_cli_manifest_dir();
jan()
.current_dir(&root)
.args([
"--spec-dir",
".",
"--spec-root",
"scripts.spec.yaml",
"--no-log",
"scripts",
"files",
"basename",
"run",
"--",
"/tmp/foo/bar.txt",
])
.assert()
.success()
.stdout(predicate::str::contains("bar.txt"));
}
#[test]
fn scripts_trackusage_help_lists_dependency() {
let root = jan_cli_manifest_dir();
let assert = jan()
.current_dir(&root)
.args([
"--spec-dir",
".",
"--spec-root",
"scripts.spec.yaml",
"--no-log",
"scripts",
"system",
"trackusage",
"help",
])
.assert()
.success();
let stdout = String::from_utf8_lossy(&assert.get_output().stdout);
assert!(stdout.contains("trackusage-impl"));
}