use assert_cmd::Command;
use predicates::prelude::*;
use rusqlite::Connection;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command as SysCommand;
use tempfile::tempdir;
fn jan() -> Command {
Command::cargo_bin("jan").unwrap()
}
fn jan_cli_manifest_dir() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
}
fn prefer_tree(tree: &Path, cfg: &Path) {
jan()
.env("JAN_CONFIG_DIR", cfg)
.args(["use", tree.to_str().unwrap()])
.assert()
.success();
}
fn write_scripts_spec(dir: &Path, body: &str) {
fs::write(dir.join("scripts.spec.yaml"), body).unwrap();
}
#[test]
fn help_root_lists_android() {
let dir = tempdir().unwrap();
write_scripts_spec(
dir.path(),
r#"
metadata:
name: demo
description: Root description line.
commands:
android:
about: Android tools
commands:
list:
about: List
exec:
argv: ["echo", "ok"]
"#,
);
let cfg = tempdir().unwrap();
prefer_tree(dir.path(), cfg.path());
jan()
.env("JAN_CONFIG_DIR", cfg.path())
.args(["--no-log", "--help"])
.assert()
.success()
.stdout(predicate::str::contains("android"))
.stdout(predicate::str::contains("Root description"))
.stdout(predicate::str::contains("Built-in commands"));
}
#[test]
fn nested_exec_runs() {
let dir = tempdir().unwrap();
write_scripts_spec(
dir.path(),
r#"
metadata:
name: demo
commands:
a:
about: A
commands:
b:
about: B
commands:
c:
about: C
exec:
argv: ["echo", "leaf"]
"#,
);
let cfg = tempdir().unwrap();
prefer_tree(dir.path(), cfg.path());
jan()
.env("JAN_CONFIG_DIR", cfg.path())
.args(["--no-log", "a", "b", "c"])
.assert()
.success()
.stdout(predicate::str::contains("leaf"));
}
#[test]
fn passthrough_forwards_args() {
let dir = tempdir().unwrap();
write_scripts_spec(
dir.path(),
r#"
metadata:
name: demo
commands:
run:
about: Run
exec:
argv: ["echo", "start"]
passthrough: true
"#,
);
let cfg = tempdir().unwrap();
prefer_tree(dir.path(), cfg.path());
jan()
.env("JAN_CONFIG_DIR", cfg.path())
.args(["--no-log", "run", "extra", "tokens"])
.assert()
.success()
.stdout(predicate::str::contains("start extra tokens"));
}
#[test]
fn preferred_spec_help_lists_commands() {
let tree = tempdir().unwrap();
write_scripts_spec(
tree.path(),
r#"
metadata:
name: demo
commands:
alpha:
about: Alpha tool
exec:
argv: ["echo", "alpha"]
beta:
about: Beta tool
exec:
argv: ["echo", "beta"]
"#,
);
let cfg = tempdir().unwrap();
prefer_tree(tree.path(), cfg.path());
jan()
.env("JAN_CONFIG_DIR", cfg.path())
.env("JAN_OS", "linux")
.args(["--no-log", "--help"])
.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");
let cfg = tempdir().unwrap();
SysCommand::new("git")
.args(["init"])
.current_dir(dir.path())
.status()
.expect("git init");
jan()
.env("JAN_CONFIG_DIR", cfg.path())
.args([
"use",
examples.to_str().unwrap(),
"--root",
"default.spec.yaml",
])
.assert()
.success();
jan()
.current_dir(dir.path())
.env("JAN_CONFIG_DIR", cfg.path())
.args(["--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();
write_scripts_spec(
dir.path(),
r#"
metadata:
name: inc-demo
commands:
outer:
include: sub/leaf.yaml
"#,
);
let cfg = tempdir().unwrap();
prefer_tree(dir.path(), cfg.path());
jan()
.env("JAN_CONFIG_DIR", cfg.path())
.args(["--no-log", "outer"])
.assert()
.success()
.stdout(predicate::str::contains("included"));
}
#[test]
fn os_filter_hides_command_on_mismatch() {
let dir = tempdir().unwrap();
write_scripts_spec(
dir.path(),
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"]
"#,
);
let cfg = tempdir().unwrap();
prefer_tree(dir.path(), cfg.path());
let assert = jan()
.env("JAN_CONFIG_DIR", cfg.path())
.env("JAN_OS", "linux")
.args(["--no-log", "--help"])
.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();
write_scripts_spec(
dir.path(),
r#"
metadata:
name: demo
commands:
leaf:
about: Leaf
exec:
argv: ["echo", "x"]
"#,
);
let cfg = tempdir().unwrap();
prefer_tree(dir.path(), cfg.path());
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())
.env("JAN_CONFIG_DIR", cfg.path())
.args([
"--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 expected_dir = dir.path().canonicalize().unwrap().to_string_lossy().to_string();
assert_eq!(spec_dir, expected_dir);
}
#[test]
fn preferred_dir_loads_includes() {
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", "from-preferred-dir"]
"#,
)
.unwrap();
write_scripts_spec(
dir.path(),
r#"
metadata:
name: dir-demo
commands:
outer:
include: sub/leaf.yaml
"#,
);
let cfg = tempdir().unwrap();
prefer_tree(dir.path(), cfg.path());
jan()
.env("JAN_CONFIG_DIR", cfg.path())
.args(["--no-log", "outer"])
.assert()
.success()
.stdout(predicate::str::contains("from-preferred-dir"));
}
#[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 helper_s = helper.to_string_lossy();
let consumer_s = consumer.to_string_lossy();
write_scripts_spec(
dir.path(),
&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"]
"#
),
);
let cfg = tempdir().unwrap();
prefer_tree(dir.path(), cfg.path());
jan()
.env("JAN_CONFIG_DIR", cfg.path())
.args(["--no-log", "consumer", "run"])
.assert()
.success()
.stdout(predicate::str::contains("from-helper"));
}
#[test]
fn env_vars_passed_to_child() {
let dir = tempdir().unwrap();
write_scripts_spec(
dir.path(),
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\""]
"#,
);
let cfg = tempdir().unwrap();
prefer_tree(dir.path(), cfg.path());
jan()
.env("JAN_CONFIG_DIR", cfg.path())
.args(["--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();
write_scripts_spec(
dir.path(),
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"]
"#,
);
let cfg = tempdir().unwrap();
prefer_tree(dir.path(), cfg.path());
jan()
.env("JAN_CONFIG_DIR", cfg.path())
.args(["--no-log", "needs-fzf"])
.assert()
.failure()
.stderr(predicate::str::contains("missing required utilities"));
}
#[test]
fn preferred_spec_logs_spec_root() {
let tree = tempdir().unwrap();
write_scripts_spec(
tree.path(),
r#"
commands:
leaf:
exec:
argv: ["echo", "logged"]
"#,
);
let cfg = tempdir().unwrap();
let cwd = tempdir().unwrap();
prefer_tree(tree.path(), cfg.path());
let db = cwd.path().join("audit.db");
jan()
.current_dir(cwd.path())
.env("JAN_CONFIG_DIR", cfg.path())
.args([
"--db",
db.to_str().unwrap(),
"--branch",
"pref-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",
["pref-branch"],
|r| Ok((r.get::<_, String>(0)?, r.get::<_, String>(1)?)),
)
.unwrap();
assert_eq!(
spec_dir,
tree.path().canonicalize().unwrap().to_string_lossy()
);
assert_eq!(root_yaml, "scripts.spec.yaml");
}
#[test]
fn scripts_bundle_dry_run_lists_portable_artifacts() {
let root = jan_cli_manifest_dir();
let cfg = tempdir().unwrap();
jan()
.env("JAN_CONFIG_DIR", cfg.path())
.args(["use", root.to_str().unwrap(), "--root", "scripts.spec.yaml"])
.assert()
.success();
let assert = jan()
.current_dir(&root)
.env("JAN_CONFIG_DIR", cfg.path())
.args(["--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_emits_bare_jan_and_sum() {
let root = jan_cli_manifest_dir();
let cfg = tempdir().unwrap();
jan()
.env("JAN_CONFIG_DIR", cfg.path())
.args(["use", root.to_str().unwrap(), "--root", "scripts.spec.yaml"])
.assert()
.success();
let assert = jan()
.current_dir(&root)
.env("JAN_CONFIG_DIR", cfg.path())
.args(["--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("alias sum="));
assert!(stdout.contains("scripts misc sum run"));
}
#[test]
fn scripts_inlined_basename_run() {
let root = jan_cli_manifest_dir();
let cfg = tempdir().unwrap();
jan()
.env("JAN_CONFIG_DIR", cfg.path())
.args(["use", root.to_str().unwrap(), "--root", "scripts.spec.yaml"])
.assert()
.success();
jan()
.current_dir(&root)
.env("JAN_CONFIG_DIR", cfg.path())
.args([
"--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 cfg = tempdir().unwrap();
jan()
.env("JAN_CONFIG_DIR", cfg.path())
.args(["use", root.to_str().unwrap(), "--root", "scripts.spec.yaml"])
.assert()
.success();
let assert = jan()
.current_dir(&root)
.env("JAN_CONFIG_DIR", cfg.path())
.args([
"--no-log",
"scripts",
"system",
"trackusage",
"help",
])
.assert()
.success();
let stdout = String::from_utf8_lossy(&assert.get_output().stdout);
assert!(stdout.contains("trackusage-impl"));
}
#[test]
fn use_saves_preference_and_loads_without_flags() {
let tree = tempdir().unwrap();
write_scripts_spec(
tree.path(),
r#"
metadata:
name: demo
commands:
hello:
about: Hello
exec:
argv: ["echo", "from-preferred"]
"#,
);
let cfg = tempdir().unwrap();
let cwd = tempdir().unwrap();
jan()
.current_dir(cwd.path())
.env("JAN_CONFIG_DIR", cfg.path())
.args(["use", tree.path().to_str().unwrap()])
.assert()
.success()
.stdout(predicate::str::contains("preferred jan directory saved"));
jan()
.current_dir(cwd.path())
.env("JAN_CONFIG_DIR", cfg.path())
.args(["--no-log", "hello"])
.assert()
.success()
.stdout(predicate::str::contains("from-preferred"));
jan()
.current_dir(cwd.path())
.env("JAN_CONFIG_DIR", cfg.path())
.args(["--help"])
.assert()
.success()
.stdout(predicate::str::contains("hello"));
jan()
.current_dir(cwd.path())
.env("JAN_CONFIG_DIR", cfg.path())
.args(["use", "--show"])
.assert()
.success()
.stdout(predicate::str::contains("scripts.spec.yaml"));
jan()
.current_dir(cwd.path())
.env("JAN_CONFIG_DIR", cfg.path())
.args(["use", "--clear"])
.assert()
.success();
jan()
.current_dir(cwd.path())
.env("JAN_CONFIG_DIR", cfg.path())
.args(["--no-log", "hello"])
.assert()
.failure()
.stderr(predicate::str::contains("jan use"));
}
#[test]
fn use_works_before_any_spec_exists() {
let cfg = tempdir().unwrap();
let cwd = tempdir().unwrap();
jan()
.current_dir(cwd.path())
.env("JAN_CONFIG_DIR", cfg.path())
.args(["use", "--show"])
.assert()
.success()
.stdout(predicate::str::contains("no preferred jan directory"));
jan()
.current_dir(cwd.path())
.env("JAN_CONFIG_DIR", cfg.path())
.args(["--help"])
.assert()
.success()
.stdout(predicate::str::contains("No preferred jan directory"))
.stdout(predicate::str::contains("Built-in commands"));
}