use assert_cmd::Command;
use predicates::prelude::*;
use serde_json::Value;
use std::fs;
use tempfile::tempdir;
#[test]
fn save_then_restore_exact_cache_hit() {
let dir = tempdir().unwrap();
let workspace = dir.path().join("workspace");
fs::create_dir_all(workspace.join("deps")).unwrap();
fs::write(workspace.join("deps/lock.txt"), "cache me").unwrap();
let store = dir.path().join("store");
Command::cargo_bin("gha-cache-proof")
.unwrap()
.args([
"save",
"--store",
store.to_str().unwrap(),
"--workspace",
workspace.to_str().unwrap(),
"--key",
"Linux-deps-123",
"--path",
"deps",
])
.assert()
.success();
fs::remove_dir_all(workspace.join("deps")).unwrap();
let output = Command::cargo_bin("gha-cache-proof")
.unwrap()
.args([
"restore",
"--store",
store.to_str().unwrap(),
"--workspace",
workspace.to_str().unwrap(),
"--key",
"Linux-deps-123",
"--path",
"deps",
"--format",
"json",
])
.assert()
.success()
.get_output()
.stdout
.clone();
assert_eq!(
fs::read_to_string(workspace.join("deps/lock.txt")).unwrap(),
"cache me"
);
let receipt: Value = serde_json::from_slice(&output).unwrap();
assert_eq!(receipt["operations"][0]["cache_hit"], "true");
assert_eq!(
receipt["operations"][0]["matched"]["match_kind"],
"exact-key"
);
}
#[test]
fn restore_key_falls_back_to_default_branch_prefix() {
let dir = tempdir().unwrap();
let main_workspace = dir.path().join("main");
fs::create_dir_all(main_workspace.join("deps")).unwrap();
fs::write(main_workspace.join("deps/lock.txt"), "from main").unwrap();
let store = dir.path().join("store");
Command::cargo_bin("gha-cache-proof")
.unwrap()
.args([
"save",
"--store",
store.to_str().unwrap(),
"--workspace",
main_workspace.to_str().unwrap(),
"--reference",
"refs/heads/main",
"--key",
"Linux-deps-abc",
"--path",
"deps",
])
.assert()
.success();
let feature_workspace = dir.path().join("feature");
fs::create_dir_all(&feature_workspace).unwrap();
let output = Command::cargo_bin("gha-cache-proof")
.unwrap()
.args([
"restore",
"--store",
store.to_str().unwrap(),
"--workspace",
feature_workspace.to_str().unwrap(),
"--reference",
"refs/heads/feature",
"--default-branch",
"main",
"--key",
"Linux-deps-new",
"--restore-key",
"Linux-deps-",
"--path",
"deps",
"--format",
"json",
])
.assert()
.success()
.get_output()
.stdout
.clone();
let receipt: Value = serde_json::from_slice(&output).unwrap();
assert_eq!(receipt["operations"][0]["cache_hit"], "false");
assert_eq!(
receipt["operations"][0]["matched"]["scope"],
"refs/heads/main"
);
assert_eq!(
receipt["operations"][0]["matched"]["match_kind"],
"restore-prefix"
);
}
#[test]
fn fail_on_cache_miss_exits_nonzero() {
let dir = tempdir().unwrap();
fs::create_dir_all(dir.path().join("workspace")).unwrap();
Command::cargo_bin("gha-cache-proof")
.unwrap()
.args([
"restore",
"--store",
dir.path().join("store").to_str().unwrap(),
"--workspace",
dir.path().join("workspace").to_str().unwrap(),
"--key",
"missing",
"--path",
"deps",
"--fail-on-cache-miss",
])
.assert()
.failure()
.stdout(predicate::str::contains("cache miss"));
}
#[test]
fn workflow_check_evaluates_cache_templates() {
let dir = tempdir().unwrap();
let repo = dir.path().join("repo");
fs::create_dir_all(repo.join(".github/workflows")).unwrap();
fs::create_dir_all(repo.join("node_modules")).unwrap();
fs::write(repo.join("package-lock.json"), "{\"lockfileVersion\":3}").unwrap();
fs::write(repo.join("node_modules/dep.txt"), "dep").unwrap();
fs::write(
repo.join(".github/workflows/ci.yml"),
r#"
name: CI
on: push
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/cache@v5
id: npm-cache
with:
path: node_modules
key: ${{ runner.os }}-npm-${{ hashFiles('package-lock.json') }}
restore-keys: |
${{ runner.os }}-npm-
"#,
)
.unwrap();
let output = Command::cargo_bin("gha-cache-proof")
.unwrap()
.args([
"check-workflow",
"--repo",
repo.to_str().unwrap(),
"--workspace",
repo.to_str().unwrap(),
"--store",
dir.path().join("store").to_str().unwrap(),
"--format",
"json",
])
.assert()
.success()
.get_output()
.stdout
.clone();
let receipt: Value = serde_json::from_slice(&output).unwrap();
let step = &receipt["workflows"][0]["cache_steps"][0];
assert_eq!(step["uses"], "actions/cache@v5");
assert!(step["key"].as_str().unwrap().starts_with("Linux-npm-"));
assert_eq!(step["restore_keys"][0], "Linux-npm-");
assert!(step["expression_receipts"].as_array().unwrap().len() >= 2);
}
#[test]
fn restore_skips_absolute_path_entries_and_records_count() {
let dir = tempdir().unwrap();
let workspace = dir.path().join("workspace");
let outside = dir.path().join("outside-of-workspace");
fs::create_dir_all(workspace.join("deps")).unwrap();
fs::create_dir_all(&outside).unwrap();
fs::write(workspace.join("deps/in-ws.txt"), "in workspace").unwrap();
fs::write(outside.join("abs.txt"), "outside workspace").unwrap();
let store = dir.path().join("store");
Command::cargo_bin("gha-cache-proof")
.unwrap()
.args([
"save",
"--store",
store.to_str().unwrap(),
"--workspace",
workspace.to_str().unwrap(),
"--key",
"Linux-mixed-1",
"--path",
"deps",
"--path",
outside.join("abs.txt").to_str().unwrap(),
])
.assert()
.success();
fs::remove_dir_all(workspace.join("deps")).unwrap();
let output = Command::cargo_bin("gha-cache-proof")
.unwrap()
.args([
"restore",
"--store",
store.to_str().unwrap(),
"--workspace",
workspace.to_str().unwrap(),
"--key",
"Linux-mixed-1",
"--path",
"deps",
"--path",
outside.join("abs.txt").to_str().unwrap(),
"--format",
"json",
])
.assert()
.success()
.get_output()
.stdout
.clone();
assert_eq!(
fs::read_to_string(workspace.join("deps/in-ws.txt")).unwrap(),
"in workspace",
"workspace-relative file should be restored normally"
);
assert!(
!workspace.join("absolute").exists(),
"absolute/ sentinel directory must not be created inside the workspace"
);
let receipt: Value = serde_json::from_slice(&output).unwrap();
let op = &receipt["operations"][0];
assert_eq!(op["cache_hit"], "true");
assert_eq!(
op["restored_files"], 1,
"only the in-workspace file restores"
);
assert_eq!(
op["skipped_absolute_files"], 1,
"the out-of-workspace file should be counted as skipped"
);
let warn = op["checks"]
.as_array()
.unwrap()
.iter()
.find(|c| c["id"] == "cache.restore.absolute_skipped");
assert!(warn.is_some(), "expected an absolute_skipped warning check");
}
#[test]
fn markdown_output_has_receipt_heading() {
let dir = tempdir().unwrap();
fs::create_dir_all(dir.path().join("workspace")).unwrap();
Command::cargo_bin("gha-cache-proof")
.unwrap()
.args([
"restore",
"--store",
dir.path().join("store").to_str().unwrap(),
"--workspace",
dir.path().join("workspace").to_str().unwrap(),
"--key",
"missing",
"--path",
"deps",
"--format",
"markdown",
])
.assert()
.success()
.stdout(predicate::str::contains("# gha-cache-proof Receipt"));
}