use assert_cmd::cargo;
use predicates::prelude::*;
use predicates::str::contains;
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;
use std::{env, fs, path::Path};
use tempfile::TempDir;
fn flk_cmd() -> assert_cmd::Command {
let mut cmd = cargo::cargo_bin_cmd!("flk");
cmd.env_remove("FLK_FLAKE_REF");
cmd
}
fn prepend_path(path: &Path) -> String {
match env::var_os("PATH") {
Some(existing) if !existing.is_empty() => {
format!("{}:{}", path.display(), existing.to_string_lossy())
}
_ => path.display().to_string(),
}
}
#[cfg(unix)]
fn make_executable(path: &Path) {
let mut permissions = fs::metadata(path).unwrap().permissions();
permissions.set_mode(0o755);
fs::set_permissions(path, permissions).unwrap();
}
#[cfg(unix)]
fn set_modified_time(path: &Path, modified: std::time::SystemTime) {
let file = fs::OpenOptions::new()
.read(true)
.write(true)
.open(path)
.unwrap();
let times = fs::FileTimes::new().set_modified(modified);
file.set_times(times).unwrap();
}
#[cfg(unix)]
fn real_nix_tests_enabled() -> bool {
if env::var("RUN_REAL_NIX_TESTS").as_deref() != Ok("1") {
return false;
}
std::process::Command::new("nix")
.env("NIX_CONFIG", "experimental-features = nix-command flakes")
.args(["flake", "metadata", "--help"])
.status()
.map(|status| status.success())
.unwrap_or(false)
}
#[cfg(unix)]
fn true_binary() -> String {
for candidate in &["/usr/bin/true", "/bin/true"] {
if Path::new(candidate).exists() {
return candidate.to_string();
}
}
panic!("could not find `true` binary on this system");
}
#[test]
fn test_version() {
flk_cmd()
.arg("--version")
.assert()
.success()
.stdout(predicate::str::contains(env!("CARGO_PKG_VERSION")));
}
#[test]
fn test_help() {
flk_cmd()
.arg("--help")
.assert()
.success()
.stdout(predicate::str::contains(
"A CLI tool for managing flake.nix devShell environments",
));
}
#[test]
fn test_init_without_template() {
let temp_dir = TempDir::new().unwrap();
flk_cmd()
.current_dir(temp_dir.path())
.arg("init")
.assert()
.success()
.stdout(predicate::str::contains(
"Created flk environment successfully!",
));
let flk_dir = temp_dir.path().join(".flk");
assert!(flk_dir.exists());
let profiles_dir = temp_dir.path().join(".flk/profiles");
assert!(profiles_dir.exists());
let profile_path = temp_dir.path().join(".flk/profiles/generic.nix");
assert!(profile_path.exists());
let content = fs::read_to_string(profile_path).unwrap();
assert!(content.contains("description = \"Generic Development Environment\""));
}
#[test]
fn test_init_with_rust_template() {
let temp_dir = TempDir::new().unwrap();
flk_cmd()
.current_dir(temp_dir.path())
.arg("init")
.arg("--template")
.arg("rust")
.assert()
.success()
.stdout(predicate::str::contains(
"Initializing flake for rust project",
));
let flake_path = temp_dir.path().join(".flk/profiles/rust.nix");
let content = fs::read_to_string(flake_path).unwrap();
assert!(content.contains("Rust development environment"));
assert!(content.contains("rust-bin.stable.latest.default"));
}
#[test]
fn test_init_with_python_template() {
let temp_dir = TempDir::new().unwrap();
flk_cmd()
.current_dir(temp_dir.path())
.arg("init")
.arg("--template")
.arg("python")
.assert()
.success()
.stdout(predicate::str::contains(
"Initializing flake for python project",
));
let flake_path = temp_dir.path().join(".flk/profiles/python.nix");
let content = fs::read_to_string(flake_path).unwrap();
assert!(content.contains("Python development environment"));
assert!(content.contains("python3"));
}
#[test]
fn test_init_with_node_template() {
let temp_dir = TempDir::new().unwrap();
flk_cmd()
.current_dir(temp_dir.path())
.arg("init")
.arg("--template")
.arg("node")
.assert()
.success();
let flake_path = temp_dir.path().join(".flk/profiles/node.nix");
let content = fs::read_to_string(flake_path).unwrap();
assert!(content.contains("Node.js development environment"));
}
#[test]
fn test_init_with_go_template() {
let temp_dir = TempDir::new().unwrap();
flk_cmd()
.current_dir(temp_dir.path())
.arg("init")
.arg("--template")
.arg("go")
.assert()
.success();
let flake_path = temp_dir.path().join(".flk/profiles/go.nix");
let content = fs::read_to_string(flake_path).unwrap();
assert!(content.contains("Go development environment"));
}
#[test]
fn test_init_force_overwrite() {
let temp_dir = TempDir::new().unwrap();
flk_cmd()
.current_dir(temp_dir.path())
.arg("init")
.assert()
.success();
flk_cmd()
.current_dir(temp_dir.path())
.arg("init")
.assert()
.failure()
.stderr(predicate::str::contains("already exists"));
flk_cmd()
.current_dir(temp_dir.path())
.arg("init")
.arg("--force")
.assert()
.success();
}
#[test]
fn test_init_creates_flk_directory_structure() {
let temp_dir = TempDir::new().unwrap();
flk_cmd()
.current_dir(temp_dir.path())
.arg("init")
.assert()
.success();
assert!(temp_dir.path().join(".flk").exists());
assert!(temp_dir.path().join(".flk/profiles").exists());
assert!(temp_dir.path().join("flake.nix").exists());
}
#[test]
fn test_list_empty_flake() {
let temp_dir = TempDir::new().unwrap();
flk_cmd()
.current_dir(temp_dir.path())
.arg("init")
.assert()
.success();
flk_cmd()
.current_dir(temp_dir.path())
.arg("list")
.assert()
.success()
.stdout(predicate::str::contains("No packages"));
}
#[test]
fn test_show_flake() {
let temp_dir = TempDir::new().unwrap();
flk_cmd()
.current_dir(temp_dir.path())
.arg("init")
.assert()
.success();
flk_cmd()
.current_dir(temp_dir.path())
.arg("show")
.assert()
.success()
.stdout(predicate::str::contains("Flake Configuration"))
.stdout(predicate::str::contains("nixpkgs"));
}
#[test]
fn test_add_package_without_init() {
let temp_dir = TempDir::new().unwrap();
flk_cmd()
.current_dir(temp_dir.path())
.arg("add")
.arg("ripgrep")
.assert()
.failure()
.stderr(predicate::str::contains(
"Could not find default shell profile",
));
}
#[test]
fn test_remove_package_without_init() {
let temp_dir = TempDir::new().unwrap();
flk_cmd()
.current_dir(temp_dir.path())
.arg("remove")
.arg("ripgrep")
.assert()
.failure()
.stderr(predicate::str::contains(
"Could not find default shell profile",
));
}
#[test]
fn test_add_command_without_init() {
let temp_dir = TempDir::new().unwrap();
flk_cmd()
.current_dir(temp_dir.path())
.arg("command")
.arg("add")
.arg("test")
.arg("echo hello")
.assert()
.failure()
.stderr(predicate::str::contains(
"Could not find default shell profile",
));
}
#[test]
fn test_env_add_without_init() {
let temp_dir = TempDir::new().unwrap();
flk_cmd()
.current_dir(temp_dir.path())
.arg("env")
.arg("add")
.arg("TEST_VAR")
.arg("test_value")
.assert()
.failure()
.stderr(predicate::str::contains(
"Could not find default shell profile",
));
}
#[test]
fn test_env_list_without_init() {
let temp_dir = TempDir::new().unwrap();
flk_cmd()
.current_dir(temp_dir.path())
.arg("env")
.arg("list")
.assert()
.failure()
.stderr(predicate::str::contains(
"Could not find default shell profile",
));
}
#[test]
fn test_remove_command_without_init() {
let temp_dir = TempDir::new().unwrap();
flk_cmd()
.current_dir(temp_dir.path())
.arg("command")
.arg("remove")
.arg("test")
.assert()
.failure()
.stderr(predicate::str::contains(
"Could not find default shell profile",
));
}
#[test]
fn test_invalid_command_name() {
let temp_dir = TempDir::new().unwrap();
flk_cmd()
.current_dir(temp_dir.path())
.arg("init")
.assert()
.success();
flk_cmd()
.current_dir(temp_dir.path())
.arg("command")
.arg("add")
.arg("\"-invalid-name\"")
.arg("echo test")
.assert()
.failure()
.stderr(predicate::str::contains("Invalid command name"));
}
#[test]
fn test_env_add_invalid_name() {
let temp_dir = TempDir::new().unwrap();
flk_cmd()
.current_dir(temp_dir.path())
.arg("init")
.assert()
.success();
flk_cmd()
.current_dir(temp_dir.path())
.arg("env")
.arg("add")
.arg("123INVALID")
.arg("value")
.assert()
.failure()
.stderr(predicate::str::contains(
"Invalid environment variable name",
));
}
#[test]
fn test_auto_detect_rust_project() {
let temp_dir = TempDir::new().unwrap();
fs::write(
temp_dir.path().join("Cargo.toml"),
"[package]\nname = \"test\"",
)
.unwrap();
flk_cmd()
.current_dir(temp_dir.path())
.arg("init")
.assert()
.success()
.stdout(predicate::str::contains("Detected Rust project"));
let flake_path = temp_dir.path().join(".flk/profiles/rust.nix");
let content = fs::read_to_string(flake_path).unwrap();
assert!(content.contains("Rust development environment"));
}
#[test]
fn test_auto_detect_python_project() {
let temp_dir = TempDir::new().unwrap();
fs::write(temp_dir.path().join("pyproject.toml"), "[tool.poetry]").unwrap();
flk_cmd()
.current_dir(temp_dir.path())
.arg("init")
.assert()
.success()
.stdout(predicate::str::contains("Detected Python project"));
let flake_path = temp_dir.path().join(".flk/profiles/python.nix");
let content = fs::read_to_string(flake_path).unwrap();
assert!(content.contains("Python development environment"));
}
#[test]
fn test_auto_detect_node_project() {
let temp_dir = TempDir::new().unwrap();
fs::write(temp_dir.path().join("package.json"), "{}").unwrap();
flk_cmd()
.current_dir(temp_dir.path())
.arg("init")
.assert()
.success()
.stdout(predicate::str::contains("Detected Node.js project"));
let flake_path = temp_dir.path().join(".flk/profiles/node.nix");
let content = fs::read_to_string(flake_path).unwrap();
assert!(content.contains("Node.js development environment"));
}
#[test]
fn test_auto_detect_go_project() {
let temp_dir = TempDir::new().unwrap();
fs::write(temp_dir.path().join("go.mod"), "module test").unwrap();
flk_cmd()
.current_dir(temp_dir.path())
.arg("init")
.assert()
.success()
.stdout(predicate::str::contains("Detected Go project"));
let flake_path = temp_dir.path().join(".flk/profiles/go.nix");
let content = fs::read_to_string(flake_path).unwrap();
assert!(content.contains("Go development environment"));
}
#[test]
fn test_completions_prints_bash_script() {
flk_cmd()
.args(["completions", "bash"])
.assert()
.success()
.stdout(contains("_flk()"));
}
#[test]
fn test_completions_install_creates_file() {
let temp = tempfile::tempdir().unwrap();
flk_cmd()
.env("HOME", temp.path())
.args(["completions", "--install", "zsh"])
.assert()
.success();
let installed = temp.path().join(".zsh/completions/_flk");
assert!(
installed.exists(),
"Expected completion file at {:?}",
installed
);
}
#[test]
fn test_completions_all_shells() {
let shells = vec!["bash", "zsh", "fish", "powershell", "elvish"];
for shell in shells {
flk_cmd().args(["completions", shell]).assert().success();
}
}
#[test]
fn test_hook_shells_include_shell_command() {
flk_cmd()
.args(["hook", "bash"])
.assert()
.success()
.stdout(contains("\"${SHELL:-/bin/sh}\""))
.stdout(predicate::str::contains("FLK_REF=\"").not())
.stdout(contains("exec \"$FLK_SHELL_CMD\""))
.stdout(contains("export FLK_FLAKE_REF=\".#$profile\""))
.stdout(contains("export FLK_PROFILE=\".#$profile\""))
.stdout(contains("_flk_exec_nix_develop \".#$profile\""))
.stdout(contains("[ \"$f\" -nt \"$stamp_path\" ] && return 1"))
.stdout(contains(".nix-profile-"))
.stdout(contains(".stamp"));
flk_cmd()
.args(["hook", "zsh"])
.assert()
.success()
.stdout(contains("\"${SHELL:-/bin/sh}\""))
.stdout(predicate::str::contains("FLK_REF=\"").not())
.stdout(contains("exec \"$FLK_SHELL_CMD\""))
.stdout(contains("export FLK_FLAKE_REF=\".#$profile\""))
.stdout(contains("export FLK_PROFILE=\".#$profile\""))
.stdout(contains("_flk_exec_nix_develop \".#$profile\""))
.stdout(contains("[ \"$f\" -nt \"$stamp_path\" ] && return 1"))
.stdout(contains(".nix-profile-"))
.stdout(contains(".stamp"));
flk_cmd()
.args(["hook", "fish"])
.assert()
.success()
.stdout(predicate::str::contains("FLK_REF=\"").not())
.stdout(contains(
"set -l flk_shell (test -n \"$SHELL\"; and echo \"$SHELL\"; or echo \"/bin/sh\")",
))
.stdout(contains("-c \"$flk_shell\""))
.stdout(contains("exec \"$FLK_SHELL_CMD\""))
.stdout(contains("set -gx FLK_FLAKE_REF"))
.stdout(contains("set -gx FLK_PROFILE"))
.stdout(contains("set -l flk_shell"))
.stdout(contains("test \"$f\" -nt \"$stamp_path\"; and return 1"))
.stdout(contains(".nix-profile-"))
.stdout(contains(".stamp"));
}
#[cfg(unix)]
#[test]
fn test_activate_uses_shell_fallback_and_profile_gc_root() {
let temp_dir = TempDir::new().unwrap();
let fake_bin_dir = temp_dir.path().join("bin");
let fake_nix_path = fake_bin_dir.join("nix");
let log_path = temp_dir.path().join("nix-args.log");
fs::create_dir_all(&fake_bin_dir).unwrap();
fs::write(
&fake_nix_path,
"#!/bin/sh\nprintf '%s\\n' \"$@\" > \"$FAKE_NIX_LOG\"\n",
)
.unwrap();
make_executable(&fake_nix_path);
flk_cmd()
.current_dir(temp_dir.path())
.arg("init")
.assert()
.success();
flk_cmd()
.current_dir(temp_dir.path())
.env("PATH", prepend_path(&fake_bin_dir))
.env("FAKE_NIX_LOG", &log_path)
.env_remove("SHELL")
.args(["activate", "--profile", "generic"])
.assert()
.success()
.stdout(contains("Activating nix develop shell with profile:"));
let args: Vec<String> = fs::read_to_string(&log_path)
.unwrap()
.lines()
.map(ToOwned::to_owned)
.collect();
assert_eq!(
args,
vec![
"develop",
".#generic",
"--impure",
"--profile",
".flk/.nix-profile-generic",
"-c",
"/bin/sh",
"-c",
"if [ -e \"$FLK_PROFILE_PATH\" ]; then mkdir -p \"$(dirname \"$FLK_PROFILE_STAMP\")\"; touch \"$FLK_PROFILE_STAMP\" || exit 1; fi; exec \"$FLK_SHELL_CMD\"",
]
);
}
#[cfg(unix)]
#[test]
fn test_activate_reuses_fresh_profile_cache() {
let temp_dir = TempDir::new().unwrap();
let fake_bin_dir = temp_dir.path().join("bin");
let fake_nix_path = fake_bin_dir.join("nix");
let log_path = temp_dir.path().join("nix-args.log");
let profile_path = temp_dir.path().join(".flk/.nix-profile-generic");
let stamp_path = temp_dir.path().join(".flk/.nix-profile-generic.stamp");
fs::create_dir_all(&fake_bin_dir).unwrap();
fs::write(
&fake_nix_path,
"#!/bin/sh\nprintf '%s\\n' \"$@\" > \"$FAKE_NIX_LOG\"\n",
)
.unwrap();
make_executable(&fake_nix_path);
flk_cmd()
.current_dir(temp_dir.path())
.arg("init")
.assert()
.success();
fs::write(&profile_path, "cached-profile").unwrap();
fs::write(&stamp_path, "").unwrap();
let tracked_input_mtime = std::time::UNIX_EPOCH + std::time::Duration::from_secs(1);
for input in [
"flake.nix",
"flake.lock",
".flk/default.nix",
".flk/pins.nix",
".flk/overlays.nix",
".flk/profiles/generic.nix",
] {
let input_path = temp_dir.path().join(input);
if input_path.exists() {
set_modified_time(&input_path, tracked_input_mtime);
}
}
let fresh_stamp_mtime = std::time::UNIX_EPOCH + std::time::Duration::from_secs(2);
set_modified_time(&stamp_path, fresh_stamp_mtime);
flk_cmd()
.current_dir(temp_dir.path())
.env("PATH", prepend_path(&fake_bin_dir))
.env("FAKE_NIX_LOG", &log_path)
.env_remove("SHELL")
.args(["activate", "--profile", "generic"])
.assert()
.success();
let args: Vec<String> = fs::read_to_string(&log_path)
.unwrap()
.lines()
.map(ToOwned::to_owned)
.collect();
assert_eq!(
args,
vec![
"develop",
".flk/.nix-profile-generic",
"--impure",
"-c",
"/bin/sh",
]
);
}
#[cfg(unix)]
#[test]
fn test_activate_treats_cache_fresh_when_stamp_equals_input_mtime() {
let temp_dir = TempDir::new().unwrap();
let fake_bin_dir = temp_dir.path().join("bin");
let fake_nix_path = fake_bin_dir.join("nix");
let log_path = temp_dir.path().join("nix-args.log");
let profile_path = temp_dir.path().join(".flk/.nix-profile-generic");
let stamp_path = temp_dir.path().join(".flk/.nix-profile-generic.stamp");
fs::create_dir_all(&fake_bin_dir).unwrap();
fs::write(
&fake_nix_path,
"#!/bin/sh\nprintf '%s\\n' \"$@\" > \"$FAKE_NIX_LOG\"\n",
)
.unwrap();
make_executable(&fake_nix_path);
flk_cmd()
.current_dir(temp_dir.path())
.arg("init")
.assert()
.success();
fs::write(&profile_path, "cached-profile").unwrap();
fs::write(&stamp_path, "").unwrap();
let equal_mtime = std::time::UNIX_EPOCH + std::time::Duration::from_secs(2);
for input in [
"flake.nix",
"flake.lock",
".flk/default.nix",
".flk/pins.nix",
".flk/overlays.nix",
".flk/profiles/generic.nix",
] {
let input_path = temp_dir.path().join(input);
if input_path.exists() {
set_modified_time(&input_path, equal_mtime);
}
}
set_modified_time(&stamp_path, equal_mtime);
flk_cmd()
.current_dir(temp_dir.path())
.env("PATH", prepend_path(&fake_bin_dir))
.env("FAKE_NIX_LOG", &log_path)
.env_remove("SHELL")
.args(["activate", "--profile", "generic"])
.assert()
.success();
let args: Vec<String> = fs::read_to_string(&log_path)
.unwrap()
.lines()
.map(ToOwned::to_owned)
.collect();
assert_eq!(
args,
vec![
"develop",
".flk/.nix-profile-generic",
"--impure",
"-c",
"/bin/sh",
]
);
}
#[cfg(unix)]
#[test]
fn test_activate_profile_cache_with_real_nix_when_available() {
if !real_nix_tests_enabled() {
eprintln!(
"skipping test_activate_profile_cache_with_real_nix_when_available: set RUN_REAL_NIX_TESTS=1 and ensure nix flakes support is available"
);
return;
}
let temp_dir = TempDir::new().unwrap();
let profile_path = temp_dir.path().join(".flk/.nix-profile-generic");
let stamp_path = temp_dir.path().join(".flk/.nix-profile-generic.stamp");
flk_cmd()
.current_dir(temp_dir.path())
.arg("init")
.assert()
.success();
let true_bin = true_binary();
flk_cmd()
.current_dir(temp_dir.path())
.env("SHELL", &true_bin)
.args(["activate", "--profile", "generic"])
.assert()
.success();
assert!(
profile_path.exists(),
"expected first activation to create cached profile at {}",
profile_path.display()
);
assert!(
stamp_path.exists(),
"expected first activation to create cache stamp at {}",
stamp_path.display()
);
let first_stamp_mtime = fs::metadata(&stamp_path).unwrap().modified().unwrap();
flk_cmd()
.current_dir(temp_dir.path())
.env("SHELL", &true_bin)
.args(["activate", "--profile", "generic"])
.assert()
.success();
let reused_stamp_mtime = fs::metadata(&stamp_path).unwrap().modified().unwrap();
assert_eq!(
first_stamp_mtime, reused_stamp_mtime,
"expected cache stamp to remain unchanged when reusing cached profile"
);
let flake_path = temp_dir.path().join("flake.nix");
let original_flake = fs::read_to_string(&flake_path).unwrap();
fs::write(
&flake_path,
format!("{original_flake}\n# force cache refresh\n"),
)
.unwrap();
let stale_stamp_mtime = std::time::UNIX_EPOCH + std::time::Duration::from_secs(1);
set_modified_time(&stamp_path, stale_stamp_mtime);
flk_cmd()
.current_dir(temp_dir.path())
.env("SHELL", &true_bin)
.args(["activate", "--profile", "generic"])
.assert()
.success();
let refreshed_stamp_mtime = fs::metadata(&stamp_path).unwrap().modified().unwrap();
assert!(
refreshed_stamp_mtime > first_stamp_mtime,
"expected cache stamp to be refreshed after flake input changed"
);
}
#[cfg(unix)]
#[test]
fn test_activate_refreshes_stale_profile_cache() {
let temp_dir = TempDir::new().unwrap();
let fake_bin_dir = temp_dir.path().join("bin");
let fake_nix_path = fake_bin_dir.join("nix");
let log_path = temp_dir.path().join("nix-args.log");
let profile_path = temp_dir.path().join(".flk/.nix-profile-generic");
let stamp_path = temp_dir.path().join(".flk/.nix-profile-generic.stamp");
fs::create_dir_all(&fake_bin_dir).unwrap();
fs::write(
&fake_nix_path,
"#!/bin/sh\nprintf '%s\\n' \"$@\" > \"$FAKE_NIX_LOG\"\n",
)
.unwrap();
make_executable(&fake_nix_path);
flk_cmd()
.current_dir(temp_dir.path())
.arg("init")
.assert()
.success();
fs::write(&profile_path, "cached-profile").unwrap();
fs::write(&stamp_path, "").unwrap();
let stale_stamp_mtime = std::time::UNIX_EPOCH + std::time::Duration::from_secs(1);
set_modified_time(&stamp_path, stale_stamp_mtime);
flk_cmd()
.current_dir(temp_dir.path())
.env("PATH", prepend_path(&fake_bin_dir))
.env("FAKE_NIX_LOG", &log_path)
.env_remove("SHELL")
.args(["activate", "--profile", "generic"])
.assert()
.success();
let args: Vec<String> = fs::read_to_string(&log_path)
.unwrap()
.lines()
.map(ToOwned::to_owned)
.collect();
assert_eq!(
args,
vec![
"develop",
".#generic",
"--impure",
"--profile",
".flk/.nix-profile-generic",
"-c",
"/bin/sh",
"-c",
"if [ -e \"$FLK_PROFILE_PATH\" ]; then mkdir -p \"$(dirname \"$FLK_PROFILE_STAMP\")\"; touch \"$FLK_PROFILE_STAMP\" || exit 1; fi; exec \"$FLK_SHELL_CMD\"",
]
);
}
#[test]
fn test_multiple_packages() {
let temp_dir = TempDir::new().unwrap();
flk_cmd()
.current_dir(temp_dir.path())
.arg("init")
.assert()
.success();
let packages = vec!["ripgrep", "git", "wget"];
for pkg in &packages {
flk_cmd()
.current_dir(temp_dir.path())
.arg("add")
.arg(pkg)
.assert()
.success();
}
let profile_path = temp_dir.path().join(".flk/profiles/generic.nix");
let content = fs::read_to_string(&profile_path).unwrap();
for pkg in &packages {
assert!(content.contains(pkg));
}
flk_cmd()
.current_dir(temp_dir.path())
.arg("list")
.assert()
.success();
}
#[test]
fn test_profile_add_list_set_default_remove() {
let temp_dir = TempDir::new().unwrap();
flk_cmd()
.current_dir(temp_dir.path())
.arg("init")
.assert()
.success();
flk_cmd()
.current_dir(temp_dir.path())
.arg("profile")
.arg("add")
.arg("rust")
.arg("--template")
.arg("rust")
.assert()
.success();
flk_cmd()
.current_dir(temp_dir.path())
.arg("profile")
.arg("list")
.assert()
.success()
.stdout(predicate::str::contains("generic"))
.stdout(predicate::str::contains("rust"));
flk_cmd()
.current_dir(temp_dir.path())
.arg("profile")
.arg("set-default")
.arg("rust")
.assert()
.success();
let default_content = fs::read_to_string(temp_dir.path().join(".flk/default.nix")).unwrap();
assert!(default_content.contains("defaultShell = \"rust\";"));
flk_cmd()
.current_dir(temp_dir.path())
.arg("profile")
.arg("remove")
.arg("rust")
.assert()
.failure()
.stderr(predicate::str::contains("currently set as the default"));
flk_cmd()
.current_dir(temp_dir.path())
.arg("profile")
.arg("set-default")
.arg("generic")
.assert()
.success();
flk_cmd()
.current_dir(temp_dir.path())
.arg("profile")
.arg("remove")
.arg("rust")
.assert()
.success();
assert!(!temp_dir.path().join(".flk/profiles/rust.nix").exists());
}
#[test]
fn test_profile_name_validation() {
let temp_dir = TempDir::new().unwrap();
flk_cmd()
.current_dir(temp_dir.path())
.arg("init")
.assert()
.success();
flk_cmd()
.current_dir(temp_dir.path())
.arg("profile")
.arg("add")
.arg("../bad")
.assert()
.failure()
.stderr(predicate::str::contains("Invalid profile name"));
flk_cmd()
.current_dir(temp_dir.path())
.arg("profile")
.arg("add")
.arg("has spaces")
.assert()
.failure()
.stderr(predicate::str::contains("Invalid profile name"));
}
#[test]
fn test_path_traversal_prevention() {
let temp_dir = TempDir::new().unwrap();
flk_cmd()
.current_dir(temp_dir.path())
.arg("init")
.assert()
.success();
flk_cmd()
.current_dir(temp_dir.path())
.arg("add")
.arg("ripgrep")
.arg("--profile")
.arg("../../../tmp/malicious")
.assert()
.failure()
.stderr(predicate::str::contains("Invalid profile name"));
flk_cmd()
.current_dir(temp_dir.path())
.arg("env")
.arg("--profile")
.arg("../secret")
.arg("list")
.assert()
.failure()
.stderr(predicate::str::contains("Invalid profile name"));
flk_cmd()
.current_dir(temp_dir.path())
.arg("profile")
.arg("remove")
.arg("../../../etc/passwd")
.assert()
.failure()
.stderr(predicate::str::contains("Invalid profile name"));
flk_cmd()
.current_dir(temp_dir.path())
.arg("profile")
.arg("set-default")
.arg("../../../tmp/malicious")
.assert()
.failure()
.stderr(predicate::str::contains("Invalid profile name"));
}
#[test]
fn test_add_package_to_specific_profile() {
let temp_dir = TempDir::new().unwrap();
flk_cmd()
.current_dir(temp_dir.path())
.arg("init")
.assert()
.success();
flk_cmd()
.current_dir(temp_dir.path())
.arg("profile")
.arg("add")
.arg("rust")
.arg("--template")
.arg("rust")
.assert()
.success();
flk_cmd()
.current_dir(temp_dir.path())
.arg("add")
.arg("ripgrep")
.arg("--profile")
.arg("rust")
.assert()
.success();
let rust_profile = fs::read_to_string(temp_dir.path().join(".flk/profiles/rust.nix")).unwrap();
assert!(rust_profile.contains("ripgrep"));
let generic_profile =
fs::read_to_string(temp_dir.path().join(".flk/profiles/generic.nix")).unwrap();
assert!(!generic_profile.contains("ripgrep"));
}
#[test]
fn test_env_operations_on_specific_profile() {
let temp_dir = TempDir::new().unwrap();
flk_cmd()
.current_dir(temp_dir.path())
.arg("init")
.assert()
.success();
flk_cmd()
.current_dir(temp_dir.path())
.arg("profile")
.arg("add")
.arg("dev")
.arg("--template")
.arg("base")
.assert()
.success();
flk_cmd()
.current_dir(temp_dir.path())
.arg("env")
.arg("--profile")
.arg("dev")
.arg("add")
.arg("MY_VAR")
.arg("my_value")
.assert()
.success();
let dev_profile = fs::read_to_string(temp_dir.path().join(".flk/profiles/dev.nix")).unwrap();
assert!(dev_profile.contains("MY_VAR"));
flk_cmd()
.current_dir(temp_dir.path())
.arg("env")
.arg("--profile")
.arg("dev")
.arg("list")
.assert()
.success()
.stdout(predicate::str::contains("MY_VAR"));
}
#[test]
fn test_export_json() {
let temp_dir = TempDir::new().unwrap();
flk_cmd()
.current_dir(temp_dir.path())
.arg("init")
.assert()
.success();
flk_cmd()
.current_dir(temp_dir.path())
.arg("export")
.arg("--format")
.arg("json")
.assert()
.success();
let json_path = temp_dir.path().join("flake.json");
assert!(json_path.exists());
let json_content = fs::read_to_string(json_path).unwrap();
assert!(json_content.contains("profiles"));
}
#[test]
fn test_profile_directory_isolation() {
let temp_dir = TempDir::new().unwrap();
flk_cmd()
.current_dir(temp_dir.path())
.arg("init")
.arg("--template")
.arg("rust")
.assert()
.success();
let profiles_dir = temp_dir.path().join(".flk/profiles");
let entries: Vec<_> = fs::read_dir(&profiles_dir)
.unwrap()
.filter_map(|e| e.ok())
.filter(|e| e.path().extension().map(|s| s == "nix").unwrap_or(false))
.collect();
assert!(entries.len() >= 1);
assert!(profiles_dir.join("rust.nix").exists());
}
#[test]
fn test_flake_nix_exists_at_root() {
let temp_dir = TempDir::new().unwrap();
flk_cmd()
.current_dir(temp_dir.path())
.arg("init")
.assert()
.success();
assert!(temp_dir.path().join("flake.nix").exists());
}
#[test]
fn test_dendritic_structure_complete() {
let temp_dir = TempDir::new().unwrap();
flk_cmd()
.current_dir(temp_dir.path())
.arg("init")
.assert()
.success();
assert!(temp_dir.path().join("flake.nix").exists());
assert!(temp_dir.path().join(".flk").exists());
assert!(temp_dir.path().join(".flk/profiles").exists());
let flk_dir = temp_dir.path().join(".flk");
assert!(flk_dir.is_dir());
}
#[test]
fn test_direnv_init() {
let temp_dir = TempDir::new().unwrap();
flk_cmd()
.current_dir(temp_dir.path())
.arg("direnv")
.arg("init")
.assert()
.success();
let direnv_path = temp_dir.path().join(".envrc");
assert!(direnv_path.exists());
let content = fs::read_to_string(direnv_path).unwrap();
assert_eq!(content, "# Watch flk config files so nix-direnv re-evaluates on changes\nwatch_file .flk/default.nix\nwatch_file .flk/pins.nix\nwatch_file .flk/overlays.nix\nfor f in .flk/profiles/*.nix; do watch_file \"$f\"; done\n\nuse flake \"${FLK_PROFILE:-.#}\" --impure");
}
#[test]
fn test_direnv_attach() {
let temp_dir = TempDir::new().unwrap();
let direnv_path = temp_dir.path().join(".envrc");
fs::write(&direnv_path, "export VAR=value").unwrap();
flk_cmd()
.current_dir(temp_dir.path())
.arg("direnv")
.arg("attach")
.assert()
.success();
assert!(direnv_path.exists());
let content = fs::read_to_string(direnv_path).unwrap();
assert!(content.contains("export VAR=value"));
assert!(content.contains("use flake \"${FLK_PROFILE:-.#}\" --impure"));
}
#[test]
fn test_direnv_detach() {
let temp_dir = TempDir::new().unwrap();
flk_cmd()
.current_dir(temp_dir.path())
.arg("direnv")
.arg("init")
.assert()
.success();
let direnv_path = temp_dir.path().join(".envrc");
let mut content = fs::read_to_string(&direnv_path).unwrap();
content.push_str("\nexport VAR=value\nwatch_file my_file.txt\n");
fs::write(&direnv_path, &content).unwrap();
flk_cmd()
.current_dir(temp_dir.path())
.arg("direnv")
.arg("detach")
.assert()
.success();
assert!(direnv_path.exists());
let content = fs::read_to_string(direnv_path).unwrap();
assert!(!content.contains("use flake"));
assert!(!content.contains("watch_file .flk/"));
assert!(!content.contains("for f in .flk/profiles"));
assert!(!content.contains("# Watch flk config files"));
assert!(content.contains("export VAR=value"));
assert!(content.contains("watch_file my_file.txt"));
}
#[test]
fn test_lock_show_without_lock_file() {
let temp_dir = TempDir::new().unwrap();
flk_cmd()
.current_dir(temp_dir.path())
.args(["lock", "show"])
.assert()
.failure()
.stderr(contains("No flake.lock found"));
}
#[test]
fn test_lock_show_displays_locked_inputs() {
let temp_dir = TempDir::new().unwrap();
fs::write(
temp_dir.path().join("flake.lock"),
r#"{
"version": 7,
"nodes": {
"root": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs"
}
},
"flake-utils": {
"locked": {
"type": "github",
"owner": "numtide",
"repo": "flake-utils",
"rev": "1234567890abcdef1234567890abcdef12345678",
"lastModified": 1700000000,
"narHash": "sha256-flake-utils"
}
},
"nixpkgs": {
"locked": {
"type": "github",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "abcdef1234567890abcdef1234567890abcdef12",
"lastModified": 1700000100,
"narHash": "sha256-nixpkgs"
}
}
}
}"#,
)
.unwrap();
flk_cmd()
.current_dir(temp_dir.path())
.args(["lock", "show"])
.assert()
.success()
.stdout(contains("Flake Lock File Information"))
.stdout(contains("Lock Version:"))
.stdout(contains("flake-utils"))
.stdout(contains("nixpkgs"))
.stdout(contains("1234567890ab"))
.stdout(contains("abcdef123456"));
}
#[test]
fn test_lock_history_lists_available_backups() {
let temp_dir = TempDir::new().unwrap();
let backup_dir = temp_dir.path().join(".flk/backups");
fs::create_dir_all(&backup_dir).unwrap();
fs::write(backup_dir.join("flake.lock.2025-01-27_14-30-00"), "old").unwrap();
fs::write(backup_dir.join("flake.lock.latest-manual"), "new").unwrap();
flk_cmd()
.current_dir(temp_dir.path())
.args(["lock", "history"])
.assert()
.success()
.stdout(contains("Lock File Backup History"))
.stdout(contains("2025-01-27_14-30-00"))
.stdout(contains("latest-manual"));
}
#[test]
fn test_lock_restore_replaces_current_lock_file() {
let temp_dir = TempDir::new().unwrap();
let backup_dir = temp_dir.path().join(".flk/backups");
fs::create_dir_all(&backup_dir).unwrap();
fs::write(temp_dir.path().join("flake.lock"), "{\"version\":1}").unwrap();
fs::write(backup_dir.join("flake.lock.snapshot"), "{\"version\":2}").unwrap();
flk_cmd()
.current_dir(temp_dir.path())
.args(["lock", "restore", "snapshot"])
.assert()
.success()
.stdout(contains("Lock file restored successfully"));
assert_eq!(
fs::read_to_string(temp_dir.path().join("flake.lock")).unwrap(),
"{\"version\":2}"
);
let backup_count = fs::read_dir(&backup_dir)
.unwrap()
.filter_map(|entry| entry.ok())
.filter(|entry| {
entry
.file_name()
.to_string_lossy()
.starts_with("flake.lock.")
})
.count();
assert!(backup_count >= 2);
}
#[test]
fn test_command_add_list_remove() {
let temp_dir = TempDir::new().unwrap();
flk_cmd()
.current_dir(temp_dir.path())
.arg("init")
.assert()
.success();
flk_cmd()
.current_dir(temp_dir.path())
.args(["command", "add", "greet", "echo hello"])
.assert()
.success()
.stdout(contains("Command 'greet' added successfully"));
flk_cmd()
.current_dir(temp_dir.path())
.args(["command", "list"])
.assert()
.success()
.stdout(contains("greet"));
flk_cmd()
.current_dir(temp_dir.path())
.args(["command", "remove", "greet"])
.assert()
.success()
.stdout(contains("Command 'greet' removed successfully"));
flk_cmd()
.current_dir(temp_dir.path())
.args(["command", "list"])
.assert()
.success()
.stdout(contains("No commands found"));
}
#[test]
fn test_command_add_nonexistent_profile() {
let temp_dir = TempDir::new().unwrap();
flk_cmd()
.current_dir(temp_dir.path())
.arg("init")
.assert()
.success();
flk_cmd()
.current_dir(temp_dir.path())
.args([
"command",
"--profile",
"nonexistent",
"add",
"greet",
"echo hi",
])
.assert()
.failure()
.stderr(contains("Failed to read profile file"));
}
#[test]
fn test_command_remove_nonexistent_profile() {
let temp_dir = TempDir::new().unwrap();
flk_cmd()
.current_dir(temp_dir.path())
.arg("init")
.assert()
.success();
flk_cmd()
.current_dir(temp_dir.path())
.args(["command", "--profile", "nonexistent", "remove", "greet"])
.assert()
.failure()
.stderr(contains("Profile file"));
}
#[test]
fn test_command_remove_nonexistent_command() {
let temp_dir = TempDir::new().unwrap();
flk_cmd()
.current_dir(temp_dir.path())
.arg("init")
.assert()
.success();
flk_cmd()
.current_dir(temp_dir.path())
.args(["command", "remove", "nonexistent_cmd"])
.assert()
.failure()
.stderr(contains("not found in profile"));
}
#[test]
fn test_command_list_nonexistent_profile() {
let temp_dir = TempDir::new().unwrap();
flk_cmd()
.current_dir(temp_dir.path())
.arg("init")
.assert()
.success();
flk_cmd()
.current_dir(temp_dir.path())
.args(["command", "--profile", "nonexistent", "list"])
.assert()
.failure()
.stderr(contains("Failed to read profile file"));
}
#[test]
fn test_env_add_and_remove() {
let temp_dir = TempDir::new().unwrap();
flk_cmd()
.current_dir(temp_dir.path())
.arg("init")
.assert()
.success();
flk_cmd()
.current_dir(temp_dir.path())
.args(["env", "add", "MY_VAR", "my_value"])
.assert()
.success();
flk_cmd()
.current_dir(temp_dir.path())
.args(["env", "remove", "MY_VAR"])
.assert()
.success();
flk_cmd()
.current_dir(temp_dir.path())
.args(["env", "list"])
.assert()
.success()
.stdout(predicate::str::contains("MY_VAR").not());
}
#[test]
fn test_env_add_nonexistent_profile() {
let temp_dir = TempDir::new().unwrap();
flk_cmd()
.current_dir(temp_dir.path())
.arg("init")
.assert()
.success();
flk_cmd()
.current_dir(temp_dir.path())
.args(["env", "--profile", "nonexistent", "add", "MY_VAR", "value"])
.assert()
.failure()
.stderr(contains("Failed to read profile file"));
}
#[test]
fn test_env_remove_nonexistent_profile() {
let temp_dir = TempDir::new().unwrap();
flk_cmd()
.current_dir(temp_dir.path())
.arg("init")
.assert()
.success();
flk_cmd()
.current_dir(temp_dir.path())
.args(["env", "--profile", "nonexistent", "remove", "MY_VAR"])
.assert()
.failure()
.stderr(contains("Failed to read profile file"));
}
#[test]
fn test_env_list_nonexistent_profile() {
let temp_dir = TempDir::new().unwrap();
flk_cmd()
.current_dir(temp_dir.path())
.arg("init")
.assert()
.success();
flk_cmd()
.current_dir(temp_dir.path())
.args(["env", "--profile", "nonexistent", "list"])
.assert()
.failure()
.stderr(contains("Failed to read profile file"));
}
#[test]
fn test_remove_nonexistent_profile() {
let temp_dir = TempDir::new().unwrap();
flk_cmd()
.current_dir(temp_dir.path())
.arg("init")
.assert()
.success();
flk_cmd()
.current_dir(temp_dir.path())
.args(["remove", "ripgrep", "--profile", "nonexistent"])
.assert()
.failure()
.stderr(contains("Failed to read profile file"));
}
#[test]
fn test_list_nonexistent_profile() {
let temp_dir = TempDir::new().unwrap();
flk_cmd()
.current_dir(temp_dir.path())
.arg("init")
.assert()
.success();
flk_cmd()
.current_dir(temp_dir.path())
.args(["list", "--profile", "nonexistent"])
.assert()
.failure()
.stderr(contains("Failed to read profile file"));
}
#[test]
fn test_resolve_profile_flk_flake_ref_empty_fallback() {
let temp_dir = TempDir::new().unwrap();
flk_cmd()
.current_dir(temp_dir.path())
.arg("init")
.assert()
.success();
let mut cmd = flk_cmd();
cmd.env("FLK_FLAKE_REF", ".#");
cmd.current_dir(temp_dir.path())
.arg("list")
.assert()
.success();
}
#[test]
fn test_resolve_profile_flk_flake_ref_valid() {
let temp_dir = TempDir::new().unwrap();
flk_cmd()
.current_dir(temp_dir.path())
.arg("init")
.assert()
.success();
let mut cmd = flk_cmd();
cmd.env("FLK_FLAKE_REF", ".#generic");
cmd.current_dir(temp_dir.path())
.arg("list")
.assert()
.success();
}
#[test]
fn test_no_profiles_available() {
let temp_dir = TempDir::new().unwrap();
fs::create_dir_all(temp_dir.path().join(".flk/profiles")).unwrap();
fs::write(temp_dir.path().join(".flk/default.nix"), "{ }").unwrap();
flk_cmd()
.current_dir(temp_dir.path())
.arg("list")
.assert()
.failure()
.stderr(contains("No profiles found"));
}
#[test]
fn test_update_with_specific_packages_is_rejected() {
let temp_dir = TempDir::new().unwrap();
flk_cmd()
.current_dir(temp_dir.path())
.args(["update", "ripgrep"])
.assert()
.failure()
.stderr(contains(
"Updating specific packages requires version pinning",
));
}
#[test]
fn test_update_show_requires_existing_lock_file() {
let temp_dir = TempDir::new().unwrap();
flk_cmd()
.current_dir(temp_dir.path())
.args(["update", "--show"])
.assert()
.failure()
.stderr(contains("flake.lock not found"));
}