|
28 | 28 | # reader) — `src/version.rs` reads it via `option_env!("CLI_BUILD_STAMP")`. |
29 | 29 | # `self.shortRev`/`lastModified` are absent only for a dirty tree, where |
30 | 30 | # `dirtyShortRev` and the working-tree mtime stand in and `dirty` is true. |
| 31 | + sourceRev = self.shortRev or self.dirtyShortRev or "unknown"; |
| 32 | + sourceCommitUnix = self.lastModified or 0; |
| 33 | + sourceDirty = !(self ? rev); |
31 | 34 | buildStamp = builtins.toJSON { |
32 | 35 | type = "nix"; |
33 | 36 | inherit version; |
34 | | - rev = self.shortRev or self.dirtyShortRev or "unknown"; |
35 | | - commitTs = self.lastModified or 0; |
36 | | - dirty = !(self ? rev); |
| 37 | + rev = sourceRev; |
| 38 | + commitTs = sourceCommitUnix; |
| 39 | + dirty = sourceDirty; |
37 | 40 | }; |
38 | 41 |
|
39 | 42 | completionShells = [ |
|
59 | 62 | # src/version.rs). A derivation env var change rebuilds the crate. |
60 | 63 | CLI_BUILD_STAMP = buildStamp; |
61 | 64 |
|
62 | | - # `git` is present for the tests below (they init throwaway repos); |
63 | | - # `installShellFiles` provides `installShellCompletion`. |
| 65 | + # The hook integration test executes the shipped Bash scripts with |
| 66 | + # their real jq dependency. `git` is present for tests that initialize |
| 67 | + # throwaway repositories; `installShellFiles` provides |
| 68 | + # `installShellCompletion`. |
64 | 69 | nativeBuildInputs = [ |
| 70 | + pkgs.bash |
65 | 71 | pkgs.git |
66 | 72 | pkgs.installShellFiles |
| 73 | + pkgs.jq |
67 | 74 | ]; |
68 | 75 |
|
69 | 76 | # Completions are generated by the binary we just built (never |
|
80 | 87 | --fish completions-fish |
81 | 88 | ''; |
82 | 89 |
|
83 | | - # Run only the hermetic **unit** tests (`--lib --bins`). The integration |
84 | | - # tests in `tests/*.rs` each assume a real environment the Nix build |
85 | | - # sandbox deliberately lacks — `/bin/bash` + `jq` (the shipped Codex |
86 | | - # hooks), `/usr/bin/git` on a hardcoded `PATH` (materialize's |
87 | | - # git-worktree safety check), and live PTY backends or a systemd |
88 | | - # `--user` manager (the survival + render-neutrality gates). Chasing |
89 | | - # those with per-test skips is unbounded as the suite grows, so they run |
90 | | - # on native CI (real runner) while the flake proves the package here: |
91 | | - # it builds, its ~150 pure unit tests pass, and `--help`/completions |
92 | | - # smoke-test the wired binary below. |
| 90 | + # Run the hermetic unit tests plus the real lifecycle-hook integration |
| 91 | + # test. The remaining integration tests assume facilities the Nix build |
| 92 | + # sandbox deliberately lacks: `/usr/bin/git` on a hardcoded `PATH`, |
| 93 | + # live PTY backends, or a systemd `--user` manager. They remain native |
| 94 | + # gates, while the flake proves that its own packaged hooks execute. |
93 | 95 | cargoTestFlags = [ |
94 | 96 | "--lib" |
95 | 97 | "--bins" |
| 98 | + "--test" |
| 99 | + "codex_hooks" |
| 100 | + "--test" |
| 101 | + "hooks" |
96 | 102 | ]; |
97 | 103 |
|
98 | 104 | # A few unit tests write under $HOME; the sandbox HOME is not writable. |
|
105 | 111 | mainProgram = "st2"; |
106 | 112 | }; |
107 | 113 | }; |
| 114 | + |
| 115 | + hookSuccessorSource = pkgs.runCommand "st2-hook-successor-source" { } '' |
| 116 | + cp -R ${self} $out |
| 117 | + chmod -R u+w $out |
| 118 | + printf '\n# Nix hook replacement acceptance probe.\n' >> $out/hooks/codex-stop.sh |
| 119 | + ''; |
| 120 | + |
| 121 | + st2HookSuccessor = st2.overrideAttrs (_: { |
| 122 | + pname = "st2-hook-successor"; |
| 123 | + src = hookSuccessorSource; |
| 124 | + CLI_BUILD_STAMP = builtins.toJSON { |
| 125 | + type = "nix"; |
| 126 | + inherit version; |
| 127 | + rev = "hook-successor"; |
| 128 | + commitTs = sourceCommitUnix; |
| 129 | + dirty = false; |
| 130 | + }; |
| 131 | + }); |
108 | 132 | in |
109 | 133 | { |
110 | 134 | packages.st2 = st2; |
|
147 | 171 | touch $out |
148 | 172 | ''; |
149 | 173 |
|
| 174 | + # End-to-end receipt proof across two real Nix-built binaries. The |
| 175 | + # synthetic successor changes embedded hook bytes while deliberately |
| 176 | + # retaining the same source timestamp: replacement must be explicit, |
| 177 | + # and must not be mislabeled as a downgrade. |
| 178 | + checks.hooks-replacement = pkgs.runCommand "st2-hooks-replacement-${version}" { |
| 179 | + nativeBuildInputs = [ |
| 180 | + pkgs.git |
| 181 | + pkgs.jq |
| 182 | + ]; |
| 183 | + } '' |
| 184 | + export HOME=$(mktemp -d) |
| 185 | + export ST_HOOKS=$HOME/hooks |
| 186 | +
|
| 187 | + ${st2}/bin/st2 hooks install |
| 188 | + original_dir=$(jq -r '.directory' "$ST_HOOKS/current.json") |
| 189 | + jq -e \ |
| 190 | + --arg rev ${pkgs.lib.escapeShellArg sourceRev} \ |
| 191 | + --argjson commit ${toString sourceCommitUnix} \ |
| 192 | + --argjson dirty ${builtins.toJSON sourceDirty} \ |
| 193 | + '.st2GitSha == $rev and .sourceCommitUnix == $commit and .sourceDirty == $dirty' \ |
| 194 | + "$ST_HOOKS/current.json" >/dev/null |
| 195 | +
|
| 196 | + if ${st2HookSuccessor}/bin/st2 hooks install 2>replacement.err; then |
| 197 | + echo "same-order hook replacement unexpectedly succeeded" >&2 |
| 198 | + exit 1 |
| 199 | + fi |
| 200 | + grep -F -- '--replace' replacement.err >/dev/null |
| 201 | +
|
| 202 | + ${st2HookSuccessor}/bin/st2 hooks install --replace |
| 203 | + ${st2HookSuccessor}/bin/st2 hooks verify |
| 204 | + ${st2}/bin/st2 hooks verify-own |
| 205 | + jq -e \ |
| 206 | + --argjson commit ${toString sourceCommitUnix} \ |
| 207 | + '.st2GitSha == "hook-successor" and .sourceCommitUnix == $commit and .sourceDirty == false' \ |
| 208 | + "$ST_HOOKS/current.json" >/dev/null |
| 209 | +
|
| 210 | + # The old binary keeps using its own previously installed immutable |
| 211 | + # set even though the successor is now selected globally. |
| 212 | + mkdir -p "$HOME/catalog/agents/h/worker" "$HOME/workspace" |
| 213 | + git init -q "$HOME/workspace" |
| 214 | + cat > "$HOME/catalog/agents/h/worker/agent.kdl" <<EOF |
| 215 | + agent "worker" { |
| 216 | + host "h" |
| 217 | + workspace "$HOME/workspace" |
| 218 | + command "exec codex" |
| 219 | + render { file "hook-path" "\$ST_HOOKS/codex-stop.sh" } |
| 220 | + } |
| 221 | + EOF |
| 222 | + ${st2}/bin/st2 up "$HOME/catalog" --host h --materialize-only |
| 223 | + grep -Fx \ |
| 224 | + "$ST_HOOKS/$original_dir/codex-stop.sh" \ |
| 225 | + "$HOME/workspace/hook-path" >/dev/null |
| 226 | +
|
| 227 | + touch $out |
| 228 | + ''; |
| 229 | + |
150 | 230 | devShells.default = pkgs.mkShell { |
151 | 231 | packages = [ |
152 | 232 | pkgs.cargo |
|
0 commit comments