1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
//! Imperative integration test: `aristo init --hook` installs the (deprecated,
//! opt-in) pre-commit hook that runs `aristo stamp` + `aristo lint --check`
//! (per `[lint] pre_commit` default — J6) against the staged content of a
//! fresh `git init` repo. Default `init` no longer installs the hook.
//!
//! Source: `../aretta-sdk/docs/diagrams/01-lifecycle.mmd` § "2 · Daily authoring
//! loop", `L → l2` ("git commit triggers pre-commit hook → aristo stamp + lint").
//!
//! Why imperative (not trycmd): the test must drive a real `git init` + add +
//! commit cycle in a temp directory, observe the hook firing, and assert that
//! `.aristo/index.toml` is updated and lint findings cause the commit to abort.
//! That sequence isn't a single CLI invocation, so it doesn't fit a `console`-
//! fenced trycmd file.
//!
//! Un-ignored in slice 21 (the pre-commit hook implementation) per
//! CLAUDE.md §12A — promoted from `#[ignore]` to active in the same commit
//! that lands the real hook content. Counts as the imperative-side
//! equivalent of moving a `_pending/` `.md` scenario to `active/`.
use assert_cmd::Command;
use std::path::Path;
#[test]
fn pre_commit_hook_runs_stamp_and_lint() {
let tmp = tempfile::tempdir().expect("tempdir");
let repo = tmp.path();
// The hook script runs `aristo ...` and needs to find it on PATH. In
// tests, the cargo-built binary lives next to its dependencies in
// target/debug/. Prepend that directory to PATH for every git
// invocation so the hook subprocess inherits it.
let aristo_bin = assert_cmd::cargo::cargo_bin("aristo");
let aristo_dir = aristo_bin.parent().expect("cargo bin has a parent dir");
let new_path = match std::env::var_os("PATH") {
Some(existing) => {
let mut paths = vec![aristo_dir.to_path_buf()];
paths.extend(std::env::split_paths(&existing));
std::env::join_paths(paths).expect("join PATH entries")
}
None => aristo_dir.as_os_str().to_owned(),
};
let git = |dir: &Path| {
let mut cmd = Command::new("git");
cmd.current_dir(dir).env("PATH", &new_path);
cmd
};
// 1. fresh git repo
git(repo).args(["init", "--quiet"]).assert().success();
git(repo)
.args(["config", "user.email", "test@aretta.dev"])
.assert()
.success();
git(repo)
.args(["config", "user.name", "Test"])
.assert()
.success();
git(repo)
.args(["config", "commit.gpgsign", "false"])
.assert()
.success();
// 2. minimal Cargo project so `aristo init` recognizes it
std::fs::write(
repo.join("Cargo.toml"),
r#"[package]
name = "hook-test"
version = "0.0.0"
edition = "2021"
"#,
)
.unwrap();
std::fs::create_dir(repo.join("src")).unwrap();
std::fs::write(repo.join("src/lib.rs"), "").unwrap();
// 3. aristo init --hook — opt into the (deprecated) pre-commit hook and
// write aristo.toml + .aristo/. Default init no longer installs it.
Command::cargo_bin("aristo")
.unwrap()
.arg("init")
.arg("--hook")
.current_dir(repo)
.assert()
.success();
assert!(
repo.join(".git/hooks/pre-commit").exists(),
"expected aristo init --hook to install .git/hooks/pre-commit"
);
// 4. add a well-formed annotation and commit — hook runs stamp; commit succeeds
std::fs::write(
repo.join("src/lib.rs"),
r#"use aristo::intent;
#[intent("the function returns a stable hash of its input")]
pub fn stable_hash(_x: &[u8]) -> u64 { 0 }
"#,
)
.unwrap();
git(repo).args(["add", "."]).assert().success();
let commit = git(repo)
.args(["commit", "-m", "feat: stable_hash"])
.assert()
.success();
let stderr = String::from_utf8_lossy(&commit.get_output().stderr).to_string();
assert!(
stderr.contains("aristo stamp") || stderr.contains("annotations stamped"),
"expected pre-commit hook to invoke `aristo stamp`; got stderr:\n{stderr}"
);
assert!(
repo.join(".aristo/index.toml").exists(),
"expected `aristo stamp` to populate .aristo/index.toml during commit"
);
// 5. add an empty-text annotation (lint violation) — hook's `aristo lint --check`
// should fail the commit with a non-zero exit
std::fs::write(
repo.join("src/lib.rs"),
r#"use aristo::intent;
#[intent("the function returns a stable hash of its input")]
pub fn stable_hash(_x: &[u8]) -> u64 { 0 }
#[intent("")]
pub fn empty_text() {}
"#,
)
.unwrap();
git(repo).args(["add", "."]).assert().success();
let blocked = git(repo)
.args(["commit", "-m", "feat: empty_text"])
.assert()
.failure();
let stderr = String::from_utf8_lossy(&blocked.get_output().stderr).to_string();
assert!(
stderr.contains("empty_text") || stderr.contains("lint"),
"expected pre-commit hook's `aristo lint --check` to abort the commit; got stderr:\n{stderr}"
);
}