use assert_cmd::Command;
use predicates::prelude::*;
use rusqlite::Connection;
use std::fs;
use std::io::Write;
use std::path::{Path, PathBuf};
use std::process::Command as SysCommand;
use tempfile::tempdir;
use zip::write::FileOptions;
fn jan() -> Command {
Command::cargo_bin("jan").unwrap()
}
fn jan_cli_manifest_dir() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
}
fn portable_scripts_fixture() -> PathBuf {
jan_cli_manifest_dir().join("tests/fixtures/portable-scripts")
}
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();
}
fn write_test_zip(path: &Path, entries: &[(&str, &[u8])]) {
let file = fs::File::create(path).unwrap();
let mut zip = zip::ZipWriter::new(file);
let options = FileOptions::<'_, ()>::default();
for (name, content) in entries {
zip.start_file(*name, options).unwrap();
zip.write_all(content).unwrap();
}
zip.finish().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 shell_c_passthrough_puts_args_in_dollar1() {
let dir = tempdir().unwrap();
write_scripts_spec(
dir.path(),
r#"
metadata:
name: demo
commands:
issue:
about: note app
commands:
run:
about: run
exec:
argv:
- bash
- -lc
- |
case "$1" in
create) echo "got-create argc=$#" ;;
*) echo "miss arg1=${1-} argc=$#" ;;
esac
passthrough: true
"#,
);
let cfg = tempdir().unwrap();
prefer_tree(dir.path(), cfg.path());
jan()
.env("JAN_CONFIG_DIR", cfg.path())
.args(["--no-log", "issue", "run", "create"])
.assert()
.success()
.stdout(predicate::str::contains("got-create argc=1"));
jan()
.env("JAN_CONFIG_DIR", cfg.path())
.args(["--no-log", "issue", "run", "--", "create"])
.assert()
.success()
.stdout(predicate::str::contains("got-create argc=1"));
}
#[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"));
}
#[cfg(unix)]
#[test]
fn spec_path_cannot_shadow_system_interpreter() {
let dir = tempdir().unwrap();
let evil = dir.path().join("evil-bin");
fs::create_dir_all(&evil).unwrap();
let fake_bash = evil.join("bash");
fs::write(&fake_bash, "#!/bin/sh\nprintf 'shadowed'\n").unwrap();
{
use std::os::unix::fs::PermissionsExt;
let mut perms = fs::metadata(&fake_bash).unwrap().permissions();
perms.set_mode(0o755);
fs::set_permissions(&fake_bash, perms).unwrap();
}
let evil_s = evil.to_string_lossy();
write_scripts_spec(
dir.path(),
&format!(
r#"
metadata:
name: demo
commands:
victim:
path: {evil_s}
commands:
run:
exec:
argv: ["bash", "-lc", "printf real"]
"#
),
);
let cfg = tempdir().unwrap();
prefer_tree(dir.path(), cfg.path());
jan()
.env("JAN_CONFIG_DIR", cfg.path())
.args(["--no-log", "victim", "run"])
.assert()
.success()
.stdout(predicate::str::contains("real"))
.stdout(predicate::str::contains("shadowed").not());
}
#[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 env_private_required_and_restricts_leakage() {
let dir = tempdir().unwrap();
write_scripts_spec(
dir.path(),
r#"
metadata:
name: demo
commands:
secure:
env:
public:
VISIBLE: from-yaml
private:
- JAN_TEST_PRIVATE_TOKEN
exec:
argv:
- bash
- -lc
- |
printf 'vis=%s tok=%s leak=%s' "$VISIBLE" "$JAN_TEST_PRIVATE_TOKEN" "${JAN_TEST_LEAK_MARKER:-unset}"
"#,
);
let cfg = tempdir().unwrap();
prefer_tree(dir.path(), cfg.path());
jan()
.env("JAN_CONFIG_DIR", cfg.path())
.env_remove("JAN_TEST_PRIVATE_TOKEN")
.args(["--no-log", "secure"])
.assert()
.failure()
.stderr(predicate::str::contains("missing required environment variables"));
jan()
.env("JAN_CONFIG_DIR", cfg.path())
.env("JAN_TEST_PRIVATE_TOKEN", "s3cret")
.env("JAN_TEST_LEAK_MARKER", "should-not-pass")
.args(["--no-log", "secure"])
.assert()
.success()
.stdout(predicate::str::contains("vis=from-yaml tok=s3cret leak=unset"));
}
#[test]
fn typed_inputs_flags_and_interpolation() {
let dir = tempdir().unwrap();
write_scripts_spec(
dir.path(),
r#"
metadata:
name: demo
commands:
backup:
about: Backup demo
inputs:
path:
description: Source directory
required: true
dest:
default: ~/Backups
env:
public:
SRC: ${{ inputs.path }}
commands:
run:
exec:
argv:
- bash
- -lc
- printf 'src=%s dest=%s' "$SRC" "${{ inputs.dest }}"
"#,
);
let cfg = tempdir().unwrap();
prefer_tree(dir.path(), cfg.path());
jan()
.env("JAN_CONFIG_DIR", cfg.path())
.args(["--no-log", "backup", "run"])
.assert()
.failure()
.stderr(predicate::str::contains("missing required input"));
jan()
.env("JAN_CONFIG_DIR", cfg.path())
.args(["--no-log", "backup", "run", "--path", "/tmp/vault"])
.assert()
.success()
.stdout(predicate::str::contains("src=/tmp/vault dest=~/Backups"));
jan()
.env("JAN_CONFIG_DIR", cfg.path())
.args([
"--no-log",
"backup",
"run",
"--path=/data",
"--dest",
"/mnt/b",
])
.assert()
.success()
.stdout(predicate::str::contains("src=/data dest=/mnt/b"));
jan()
.env("JAN_CONFIG_DIR", cfg.path())
.args(["--no-log", "backup", "run", "--help"])
.assert()
.success()
.stdout(predicate::str::contains("Inputs:"))
.stdout(predicate::str::contains("--path"))
.stdout(predicate::str::contains("--dest"));
}
#[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 = portable_scripts_fixture();
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("scripts.yaml"));
assert!(stdout.contains("env.sh"));
assert!(stdout.contains("manifest.json"));
}
#[test]
fn inspect_list_and_show_script_leaves() {
let root = portable_scripts_fixture();
let cfg = tempdir().unwrap();
jan()
.env("JAN_CONFIG_DIR", cfg.path())
.args(["use", root.to_str().unwrap(), "--root", "scripts.spec.yaml"])
.assert()
.success();
jan()
.env("JAN_CONFIG_DIR", cfg.path())
.args(["--no-log", "list"])
.assert()
.success()
.stdout(predicate::str::contains("sum"))
.stdout(predicate::str::contains("basename"));
jan()
.env("JAN_CONFIG_DIR", cfg.path())
.args(["--no-log", "show", "sum"])
.assert()
.success()
.stdout(predicate::str::contains("scripts misc sum"))
.stdout(predicate::str::contains("category: misc"))
.stdout(predicate::str::contains("cron:"));
jan()
.env("JAN_CONFIG_DIR", cfg.path())
.args(["--no-log", "search", "--keyword", "comma"])
.assert()
.success()
.stdout(predicate::str::contains("sum"));
jan()
.env("JAN_CONFIG_DIR", cfg.path())
.args(["--no-log", "validate"])
.assert()
.success()
.stdout(predicate::str::contains("ok:"));
}
#[test]
fn cron_lists_and_runs_matching_scripts() {
let dir = tempdir().unwrap();
write_scripts_spec(
dir.path(),
r#"
metadata:
name: cron-demo
commands:
scripts:
commands:
misc:
commands:
tick:
about: Runs on the half hour
cron: "30 10 * * *"
commands:
run:
exec:
argv: ["echo", "tick-ok"]
silent:
about: Never matches this test clock
cron: "0 0 1 1 *"
commands:
run:
exec:
argv: ["echo", "silent"]
"#,
);
let cfg = tempdir().unwrap();
prefer_tree(dir.path(), cfg.path());
jan()
.env("JAN_CONFIG_DIR", cfg.path())
.args(["--no-log", "cron", "--list"])
.assert()
.success()
.stdout(predicate::str::contains("scripts misc tick"))
.stdout(predicate::str::contains("30 10 * * *"))
.stdout(predicate::str::contains("scripts misc silent"));
jan()
.env("JAN_CONFIG_DIR", cfg.path())
.args([
"--no-log",
"cron",
"--dry-run",
"--at",
"2026-08-05 10:30",
])
.assert()
.success()
.stdout(predicate::str::contains("match: scripts misc tick"))
.stdout(predicate::str::contains("silent").not());
jan()
.env("JAN_CONFIG_DIR", cfg.path())
.args(["--no-log", "cron", "--at", "2026-08-05 10:30"])
.assert()
.success()
.stdout(predicate::str::contains("tick-ok"));
jan()
.env("JAN_CONFIG_DIR", cfg.path())
.args(["--no-log", "cron", "--at", "2026-08-05 10:31", "-v"])
.assert()
.success()
.stdout(predicate::str::contains("(no scripts match)"));
}
#[test]
fn scripts_alias_emits_bare_jan_and_sum() {
let root = portable_scripts_fixture();
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"));
assert!(stdout.contains("misc"));
assert!(stdout.contains("sum"));
assert!(stdout.contains("run"));
}
#[test]
fn alias_rejects_shell_metacharacters_in_alias_name() {
let tree = tempdir().unwrap();
write_scripts_spec(
tree.path(),
r#"
commands:
"safe-parent":
commands:
"x; touch /tmp/jan-alias-injection":
exec:
argv: ["echo", "never"]
"#,
);
let cfg = tempdir().unwrap();
prefer_tree(tree.path(), cfg.path());
jan()
.env("JAN_CONFIG_DIR", cfg.path())
.args(["--no-log", "alias"])
.assert()
.failure()
.stderr(predicate::str::contains("refusing unsafe alias name"));
}
#[test]
fn scripts_inlined_basename_run() {
let root = portable_scripts_fixture();
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 = portable_scripts_fixture();
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"));
}
#[test]
fn use_help_documents_remote_bundle() {
jan()
.args(["use", "--help"])
.assert()
.success()
.stdout(predicate::str::contains("--sha256"))
.stdout(predicate::str::contains("HTTPS_URL"))
.stdout(predicate::str::contains("--allow-http"));
}
#[test]
fn remote_use_requires_sha256() {
let cfg = tempdir().unwrap();
jan()
.env("JAN_CONFIG_DIR", cfg.path())
.args(["use", "https://example.com/tree.zip"])
.assert()
.failure()
.stderr(predicate::str::contains("--sha256"));
}
#[test]
fn installer_rejects_zip_slip() {
let tmp = tempdir().unwrap();
let archive = tmp.path().join("malicious.zip");
let install = tmp.path().join("install");
let escaped = tmp.path().join("escaped.yaml");
write_test_zip(
&archive,
&[
("../escaped.yaml", b"commands: {}\n"),
("manifest.json", br#"{"files":{}}"#),
("env.sh", b""),
],
);
let status = SysCommand::new("bash")
.arg(jan_cli_manifest_dir().join("scripts/jan-install.sh"))
.arg(&archive)
.arg(&install)
.status()
.expect("run installer");
assert!(!status.success(), "zip-slip archive must be rejected");
assert!(!escaped.exists());
assert!(!install.exists());
}
#[test]
fn installer_rejects_manifest_hash_mismatch() {
let tmp = tempdir().unwrap();
let archive = tmp.path().join("tampered.zip");
let install = tmp.path().join("install");
let manifest = br#"{
"root_yaml": "scripts.spec.yaml",
"files": {
"scripts.spec.yaml": {
"sha256": "0000000000000000000000000000000000000000000000000000000000000000",
"size": 13
}
}
}"#;
write_test_zip(
&archive,
&[
("scripts.spec.yaml", b"commands: {}\n"),
("manifest.json", manifest),
("env.sh", b""),
],
);
let status = SysCommand::new("bash")
.arg(jan_cli_manifest_dir().join("scripts/jan-install.sh"))
.arg(&archive)
.arg(&install)
.status()
.expect("run installer");
assert!(!status.success());
assert!(!install.exists());
}
#[test]
fn installer_verifies_bundle_without_changing_jan_use() {
let root = portable_scripts_fixture();
let tmp = tempdir().unwrap();
let cfg = tmp.path().join("config");
let archive = tmp.path().join("bundle.zip");
let install = tmp.path().join("install");
let install_sh = jan_cli_manifest_dir().join("scripts/jan-install.sh");
jan()
.env("JAN_CONFIG_DIR", &cfg)
.args(["use", root.to_str().unwrap(), "--root", "scripts.spec.yaml"])
.assert()
.success();
let config_before = fs::read(cfg.join("config.json")).unwrap();
jan()
.current_dir(&root)
.env("JAN_CONFIG_DIR", &cfg)
.args(["--no-log", "bundle", "-o", archive.to_str().unwrap()])
.assert()
.success();
let output = SysCommand::new("bash")
.arg(&install_sh)
.arg(&archive)
.arg(&install)
.env("JAN_CONFIG_DIR", &cfg)
.output()
.expect("run installer");
assert!(
output.status.success(),
"{}",
String::from_utf8_lossy(&output.stderr)
);
assert!(install.join("scripts.spec.yaml").is_file());
assert!(install.join("manifest.json").is_file());
assert_eq!(fs::read(cfg.join("config.json")).unwrap(), config_before);
assert!(String::from_utf8_lossy(&output.stdout).contains("Run: jan use"));
}