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
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
name: CI
on:
push:
branches:
pull_request:
branches:
workflow_dispatch:
permissions:
contents: read
actions: write
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
CARGO_TERM_COLOR: always
jobs:
check:
name: Check
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- run: cargo check --all-targets --locked
test:
name: Test
runs-on: ${{ matrix.os }}
timeout-minutes: 45
env:
RUST_BACKTRACE: 1
strategy:
fail-fast: false
matrix:
os:
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- run: cargo test --locked --no-fail-fast
doc:
name: Doc
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- env:
RUSTDOCFLAGS: -D warnings
run: cargo doc --no-deps --locked
corpus:
name: Corpus tests
runs-on: ${{ matrix.os }}
timeout-minutes: 15
strategy:
fail-fast: false
matrix:
os:
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- run: cargo build --release --locked
- name: Run zshrs corpus tests
run: bash test_corpus/run_corpus.sh
env:
ZSHRS: ${{ github.workspace }}/target/release/zshrs
- name: Upload corpus failure log
if: always()
uses: actions/upload-artifact@v4
with:
name: corpus-failures-${{ matrix.os }}
path: test_corpus/corpus_failures.log
if-no-files-found: ignore
retention-days: 30
# ztst job removed: the per-block fresh-spawn wrapper at tests/ztst_runner.rs
# was a fake-pass (counted failures, never asserted). The corpus exercises
# interactive ZLE/completion/job-control/signal-trap paths that need a real
# pty + per-file persistent shell to drive correctly. Replacement is the
# PTY harness designed in docs/ZTST_PTY_HARNESS.md; not CI-gated until that
# lands. Local manual runs only via `cargo run --bin ztst-runner` once built.
recorder-harness:
# Plugin-Framework-Agnostic State-Modification Recorder (PFA-SMR) ——
# see docs/RECORDER.md. The harness in tests/recorder_harness.rs runs
# `zshrs-recorder --no-daemon --file PATH` against tests/recorder_corpus/
# (synthetic per-kind scripts + verbatim zinit upstream `bin/*.zsh`) and
# asserts the per-kind event counts the recorder must produce. A
# regression that misses a dispatcher or double-fires one fails the
# relevant test with a kind-by-kind diff.
#
# Hermetic: --no-daemon skips the end-of-run IPC bundle, so the
# zshrs-daemon never starts and ~/.cache/zshrs/ is not written.
name: Recorder harness
runs-on: ${{ matrix.os }}
timeout-minutes: 20
env:
RUST_BACKTRACE: 1
strategy:
fail-fast: false
matrix:
os:
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- name: Build with --features recorder
run: cargo build --locked --features recorder --bin zshrs-recorder
- name: Verify Rule 1 — zero recorder code in default zshrs binary
# docs/RECORDER.md §"Zero-overhead default binary": the daily-driver
# `zshrs` must contain ZERO `zsh::recorder::*` module symbols.
#
# Match shape: Rust mangles module paths as <byte-length><name>, so
# the recorder module appears in symbols as `8recorder` (the 8 = byte
# length of "recorder"). This is precise — it matches the AOP
# runtime module specifically, not arbitrary functions whose names
# happen to contain the substring "recorder" (e.g. shell-side
# `latest_recorder_shard()` which only WALKS THE FILESYSTEM for
# `*-recorder.rkyv` shards the daemon's recorder produced — not
# recorder runtime code).
#
# Whitelist: the daemon-side `op_recorder_ingest` ingest endpoint
# always ships in the daemon (different concern; not the
# shell-runtime AOP layer); `sqlite3RecordError*` from rusqlite is
# not affected by the precise grep but kept in the whitelist for
# belt-and-suspenders robustness.
run: |
cargo build --locked --bin zshrs
set -e
hits=$(nm target/debug/zshrs 2>/dev/null \
| grep -E '\b8recorder[0-9a-zA-Z_]' \
| grep -v sqlite3RecordError \
| grep -v zshrs_daemon \
| wc -l \
| tr -d ' ')
echo "shell-runtime zsh::recorder module symbols in default zshrs: $hits"
if [ "$hits" -ne 0 ]; then
echo "FAIL: Rule 1 broken — zsh::recorder code leaked into default zshrs"
nm target/debug/zshrs 2>/dev/null | grep -E '\b8recorder[0-9a-zA-Z_]' \
| grep -v sqlite3RecordError | grep -v zshrs_daemon
exit 1
fi
- name: Run recorder harness
run: cargo test --locked --features recorder --test recorder_harness -- --nocapture
daemon-http:
# zshrs-daemon HTTP listener — see daemon/http.rs and
# docs/DAEMON_AS_SERVICE.md. Spawns the daemon with an isolated
# XDG_CACHE_HOME + XDG_CONFIG_HOME (so each test owns its own
# cache / socket / config), points it at a kernel-allocated free
# port (127.0.0.1:0 → real port via TcpListener probe), and drives
# `GET /health`, `GET /ops`, `POST /op/ping`, `POST /op/<unknown>`,
# plus the bearer-token auth path. Hermetic: every test owns a
# tempdir, no shared state across tests, no external network.
name: Daemon HTTP
runs-on: ${{ matrix.os }}
timeout-minutes: 15
env:
RUST_BACKTRACE: 1
strategy:
fail-fast: false
matrix:
os:
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
# The integration test spawns `target/debug/zshrs-daemon` (per
# tests/daemon_http.rs::zshrs_daemon_binary which falls back from
# CARGO_BIN_EXE_zshrs-daemon to manifest/target/debug). The daemon
# binary lives in the workspace's `daemon` crate (separate package
# from `zsh`), so `cargo test -p zsh` does NOT auto-build it. Build
# it explicitly here so each test's spawn isn't racing.
- name: Build zshrs-daemon binary the test will spawn
run: cargo build --locked -p zshrs-daemon --bin zshrs-daemon
- name: Run daemon HTTP integration tests
# --test-threads=1 because each test binds its own port + spawns
# its own daemon; sequential gives clearer per-test diagnostics
# and avoids the chance of two daemons racing the same XDG_CACHE
# tempdir cleanup on slow CI runners.
run: cargo test --locked --test daemon_http -- --test-threads=1 --nocapture
release-build:
name: Release Build
needs:
runs-on: ${{ matrix.os }}
timeout-minutes: 60
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
target: x86_64-unknown-linux-gnu
- os: macos-latest
target: x86_64-apple-darwin
- os: macos-latest
target: aarch64-apple-darwin
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- uses: Swatinem/rust-cache@v2
- run: cargo build --release --locked --target ${{ matrix.target }}
- uses: actions/upload-artifact@v4
with:
name: zshrs-${{ matrix.target }}
path: target/${{ matrix.target }}/release/zshrs