Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .github/scripts/check-skill-version.sh
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,28 @@ dep_version="$(sed -n 's/^termlens = .*version = "\([0-9][0-9.]*\)".*/\1/p;s/^te
exit 1
}
dep_minor="$(echo "$dep_version" | cut -d. -f1,2)"
msrv="$(sed -n 's/^rust-version = "\([0-9][0-9.]*\)"/\1/p' "$manifest" | head -1)"
[ -n "$msrv" ] || { echo "::error::no rust-version found in $manifest"; exit 1; }

if [ "$skill_minor" != "$dep_minor" ]; then
echo "::error::the vendored termlens skill is written against ${skill_version} but this workspace depends on ${dep_version}."
echo "::error::Refresh it: cp ../termlens/skills/termlens/SKILL.md ${skill}"
exit 1
fi

for document in docs/SPEC.md docs/dependencies.md; do
[ -f "$document" ] || { echo "::error::$document does not exist"; exit 1; }
# Matches every spelling these two documents use: the docs.rs URL
# (termlens/0.11), the table cell (`termlens` | 0.11), the manifest
# excerpt (termlens = { version = "0.11" }) and running prose.
grep -Eq "termlens[^0-9]*${dep_minor//./\\.}" "$document" || {
echo "::error::$document does not name termlens ${dep_minor}"
exit 1
}
grep -Fq "$msrv" "$document" || {
echo "::error::$document does not name rust-version ${msrv}"
exit 1
}
done

echo "the vendored termlens skill (${skill_version}) matches the dependency (${dep_version})"
12 changes: 11 additions & 1 deletion .github/scripts/test_ci.py
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,12 @@ def run_check(self, skill_version, dep_line):
skill.parent.mkdir(parents=True)
header = f"Written against **termlens {skill_version}**.\n" if skill_version else ""
skill.write_text("# Testing terminal programs with termlens\n\n" + header)
(root / "Cargo.toml").write_text(f"[workspace.dependencies]\n{dep_line}\n")
(root / "Cargo.toml").write_text(
f'[workspace.package]\nrust-version = "1.94.1"\n[workspace.dependencies]\n{dep_line}\n'
)
(root / "docs").mkdir()
for document in ("SPEC.md", "dependencies.md"):
(root / "docs" / document).write_text("termlens 0.10\nrust-version 1.94.1\n")
return subprocess.run(["/bin/bash", str(script)], text=True, capture_output=True, cwd=directory)

def test_a_matching_skill_passes_and_a_patch_release_is_not_drift(self):
Expand All @@ -259,6 +264,11 @@ def test_an_unreadable_claim_is_a_failure_not_a_pass(self):
self.assertNotEqual(self.run_check("", 'termlens = "0.10"').returncode, 0)
self.assertNotEqual(self.run_check("0.10.1", "insta = \"1\"").returncode, 0)

def test_stale_documented_dependency_version_fails(self):
run = self.run_check("0.11.0", 'termlens = "0.11"')
self.assertNotEqual(run.returncode, 0)
self.assertIn("docs/SPEC.md does not name termlens 0.11", run.stdout)

def test_the_committed_skill_matches_the_committed_manifest(self):
run = subprocess.run(["/bin/bash", str(SCRIPTS / "check-skill-version.sh")],
text=True, capture_output=True, cwd=ROOT)
Expand Down
10 changes: 5 additions & 5 deletions docs/SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ Verified facts about the development machine (2026-08-25): Linux 7.0 x86_64, 20
- Every public item has a doc comment; every crate has `//!` docs stating its role and which internal crates it may depend on.
- Observability via `tracing` spans/events (operator execution, allocations, spills, plan submission). No `println!` outside the CLI's user-facing output.
- Tests: colocated unit tests plus `tests/` integration tests; snapshot tests via `insta` (commit the `.snap` files). Deterministic: seed every RNG; no wall-clock dependence in rendered output.
- Toolchain: **stable Rust**, pinned in `rust-toolchain.toml` (`channel = "1.98.0"`, `components = ["rustfmt", "clippy"]`); `workspace.package.rust-version = "1.88"` (the MSRV of Ballista 54, DataFusion 54 and ratatui 0.30 — the highest in the dependency graph). Do **not** use nightly `std::simd`: CPU vectorization comes from Arrow/DataFusion compute kernels plus `rayon` data parallelism and auto-vectorization. (If profiling later justifies explicit SIMD, the `wide` crate is the stable escape hatch — do not add it now.)
- Toolchain: **stable Rust**, pinned in `rust-toolchain.toml` (`channel = "1.98.0"`, `components = ["rustfmt", "clippy"]`); `workspace.package.rust-version = "1.94.1"` (the lockfile's floor, verified by `make msrv`). Do **not** use nightly `std::simd`: CPU vectorization comes from Arrow/DataFusion compute kernels plus `rayon` data parallelism and auto-vectorization. (If profiling later justifies explicit SIMD, the `wide` crate is the stable escape hatch — do not add it now.)
- Workspace lints (every crate sets `[lints] workspace = true`; test modules may locally `#[allow(clippy::unwrap_used, clippy::expect_used)]`):

```toml
Expand Down Expand Up @@ -144,7 +144,7 @@ There is no private file format (ADR-0014). `oxidelake-storage` owns configurati
- A deterministic demo binary `oxidelake-tui-demo` (`[[bin]]` inside `oxidelake-tui`) renders the dashboard from a fixed synthetic telemetry snapshot — no clock, no animation — so PTY tests can spawn it via `env!("CARGO_BIN_EXE_oxidelake-tui-demo")` (`termlens::bin!`).
- **Headless tests, two layers:**
1. `crates/oxidelake-tui/tests/tui_render_test.rs` — in-process: `ratatui::backend::TestBackend` + `insta::assert_snapshot!` of the rendered buffer at 80×24 and 120×40; state transitions (`↓`/`↑` selection, `Tab`, `q` → quit) asserted on `AppState`.
2. `crates/oxidelake-tui/tests/tui_pty_test.rs` — end-to-end with **`termlens`** (dev-dependency; its default feature enables `insta` integration): `termlens::bin!("oxidelake-tui-demo", size(80, 24), timeout(..))`, then `wait_frame(|s| s.contains("OxideLake"))`, `termlens::assert_screen_snapshot!(&first, styles = false)`, `send(Key::Down)` and re-snapshot, `resize(120, 40)` and re-snapshot, `send(Key::Char('q'))` then `wait_exit()?.success()`. Beside it, `tests/emulation.rs` pins what the emulator dropped (`Screen::unsupported`), that no terminal mode was left set and that a screen survives the snapshot text format and JSON; `tests/termlens_cli.rs` drives the committed screens through `termlens-cli` (`#[ignore]`d — it installs the tool); and `crates/oxidelake-runtime/tests/oxide_tui_pty.rs` does the same for the shipped `oxide tui`. Read <https://docs.rs/termlens/0.10.1> before writing these — the API is newer than your training data, and the vendored `.claude/skills/termlens/SKILL.md` is the cheat sheet for the version this workspace depends on. Never `sleep`; always use the `wait_*` methods. termlens is a real-PTY harness, so no physical TTY is needed.
2. `crates/oxidelake-tui/tests/tui_pty_test.rs` — end-to-end with **`termlens`** (dev-dependency; its default feature enables `insta` integration): `termlens::bin!("oxidelake-tui-demo", size(80, 24), timeout(..))`, then `wait_frame(|s| s.contains("OxideLake"))`, `termlens::assert_screen_snapshot!(&first, styles = false)`, `send(Key::Down)` and re-snapshot, `resize(120, 40)` and re-snapshot, `send(Key::Char('q'))` then `wait_exit()?.success()`. Beside it, `tests/emulation.rs` pins what the emulator dropped (`Screen::unsupported`), that no terminal mode was left set and that a screen survives the snapshot text format and JSON; `tests/termlens_cli.rs` drives the committed screens through `termlens-cli` (`#[ignore]`d — it installs the tool); and `crates/oxidelake-runtime/tests/oxide_tui_pty.rs` does the same for the shipped `oxide tui`. Read <https://docs.rs/termlens/0.11> before writing these — the API is newer than your training data, and the vendored `.claude/skills/termlens/SKILL.md` is the cheat sheet for the version this workspace depends on. Never `sleep`; always use the `wait_*` methods. termlens is a real-PTY harness, so no physical TTY is needed.

### 2.8 API — `oxidelake-api`

Expand Down Expand Up @@ -204,7 +204,7 @@ Rules:

1. Every external dependency is declared once in `[workspace.dependencies]`; members use `{ workspace = true }`.
2. **Version coherence has one root: Ballista.** Its docs say "Make sure the version of `datafusion` is the same as `ballista`'s!" — so `ballista*` fixes the `datafusion` / `datafusion-proto` major, DataFusion fixes the `arrow` / `parquet` / `object_store` majors, and `arrow-flight` / `tonic` / `prost` arrive transitively through Ballista (not declared directly). Never depend on other `datafusion-*` sub-crates; `datafusion-proto` is the single exception (needed for the plan codec) and is pinned to the identical version (ADR-0009). Gate: `cargo tree --workspace -d -e normal` shows **no duplicate `arrow-*`, `parquet`, `datafusion*`, `object_store`, `tonic` or `prost` majors**.
3. Evidence for the set below (live registry): ballista / ballista-core / ballista-scheduler / ballista-executor 54.1.0 (2026-08-09) require `datafusion ^54`, `datafusion-proto ^54`, `arrow-flight ^58.3`, `object_store ^0.13.2`, `tonic ^0.14`, `prost ^0.14`; datafusion 54.1.0 requires `arrow ^58.3`, `parquet ^58.3`, `object_store ^0.13.2`, `tokio ^1.52`; arrow-flight 58.4 requires `tonic` / `prost ^0.14.1`; ratatui 0.30.2 pairs with crossterm 0.29 through `crossterm_0_29`; cudarc 0.19.9 ships `dynamic-loading`, `nvrtc` and `cuda-*`; objc2-metal 0.3 requires objc2 0.6 and objc2-foundation 0.3; termlens 0.10.1 defaults to `insta` and keeps `serde` / `regex` behind features (MSRV 1.85, far under this workspace's floor). MSRVs: Ballista, DataFusion 54 and ratatui 1.88 → `rust-version = "1.88"`. DataFusion 55 exists but Ballista is on 54 — Ballista wins. When Ballista moves to a new DataFusion major, re-derive the whole chain; never bump one crate alone.
3. Evidence for the set below (live registry): ballista / ballista-core / ballista-scheduler / ballista-executor 54.1.0 (2026-08-09) require `datafusion ^54`, `datafusion-proto ^54`, `arrow-flight ^58.3`, `object_store ^0.13.2`, `tonic ^0.14`, `prost ^0.14`; datafusion 54.1.0 requires `arrow ^58.3`, `parquet ^58.3`, `object_store ^0.13.2`, `tokio ^1.52`; arrow-flight 58.4 requires `tonic` / `prost ^0.14.1`; ratatui 0.30.2 pairs with crossterm 0.29 through `crossterm_0_29`; cudarc 0.19.9 ships `dynamic-loading`, `nvrtc` and `cuda-*`; objc2-metal 0.3 requires objc2 0.6 and objc2-foundation 0.3; termlens 0.11 defaults to `insta` and keeps `serde` / `regex` behind features (MSRV 1.85, far under this workspace's floor). MSRVs: the locked dependency graph requires `rust-version = "1.94.1"`. DataFusion 55 exists but Ballista is on 54 — Ballista wins. When Ballista moves to a new DataFusion major, re-derive the whole chain; never bump one crate alone.

```toml
[workspace]
Expand All @@ -214,7 +214,7 @@ members = ["crates/*"]
[workspace.package]
version = "0.1.0"
edition = "2024"
rust-version = "1.88"
rust-version = "1.94.1"
license = "Apache-2.0"
authors = ["OxideLake Contributors"]

Expand Down Expand Up @@ -257,7 +257,7 @@ crossterm = "0.29"
clap = { version = "4", features = ["derive"] }
# test-only
insta = "1"
termlens = { version = "0.10", features = ["serde"] }
termlens = { version = "0.11", features = ["serde"] }
assert_cmd = "2"
tempfile = "3"
rand = "0.10"
Expand Down
19 changes: 19 additions & 0 deletions docs/decisions/ADR-0011-tui-testing-layers.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,22 @@ The two layers stand; the dependency moved 0.9 → 0.10.1 and the PTY layer grew
- `tests/termlens_cli.rs` reads the committed `.snap` files back with `termlens-cli`. It is `#[ignore]`d: these crates are published, and `cargo test` must not install a tool behind a contributor's back. CI runs it (`make test-termlens-cli`).

The vendored agent skill is checked against the dependency by `make skill-version`, because it had already drifted two releases behind.

## Update — 2026-09-11 (termlens 0.11)

The dependency moved 0.10.1 → 0.11, termlens's stability candidate: from it
no promised item changes incompatibly before its 1.0, so this requirement
should hold for a while. Neither layer changed shape.

Its one breaking change simplified the invariant the PTY layer rests on.
`Screen::unsupported()` returns a view instead of a slice of `Arc<str>` and
`unsupported_overflow()` folds into it, so the pinned list and "the record
is not truncated" became one assertion in both suites — the dashboard's
(`oxidelake-tui/tests/emulation.rs`) and the shipped binary's
(`oxidelake-runtime/tests/oxide_tui_pty.rs`). The view compares equal to a
slice only when the retained shapes match *and* nothing overflowed the
bound, so a truncated record can no longer pass as a shorter list.

`make skill-version` now also holds `docs/SPEC.md` and `docs/dependencies.md`
to the dependency's major.minor and to the workspace `rust-version`, because
both had drifted behind this bump.
2 changes: 1 addition & 1 deletion docs/dependencies.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ The pins live in [SPEC.md §4](SPEC.md). This page records *why* those numbers,
| `objc2-metal` | 0.3 | `objc2 >=0.6.2, <0.8`, `objc2-foundation ^0.3.2` | macOS target-gated, behind `metal` |
| `objc2` / `objc2-foundation` | 0.6 / 0.3 | — | |
| `io-uring` | 0.7 | — | Linux target-gated, behind `io-uring`; chosen over `tokio-uring` 0.5, which pins `io-uring ^0.6` and needs its own runtime ([ADR-0007](decisions/ADR-0007-io-uring-crate-dedicated-thread.md)) |
| `termlens` | 0.10 (dev) | default feature `insta`; feature `serde` | real-PTY harness driving `oxidelake-tui-demo` and `oxide tui`; MSRV 1.85; `serde` is what `TERMLENS_ARTIFACT_DIR` writes in CI and what `tests/emulation.rs` round-trips; read <https://docs.rs/termlens/0.10.1> before use, and refresh `.claude/skills/termlens/SKILL.md` with the dependency (`make skill-version`) |
| `termlens` | 0.11 (dev) | default feature `insta`; feature `serde` | real-PTY harness driving `oxidelake-tui-demo` and `oxide tui`; MSRV 1.85; `serde` is what `TERMLENS_ARTIFACT_DIR` writes in CI and what `tests/emulation.rs` round-trips; read <https://docs.rs/termlens/0.11> before use, and refresh `.claude/skills/termlens/SKILL.md` with the dependency (`make skill-version`) |
| `insta` | 1 (dev) | — | snapshot files are committed |
| `assert_cmd` | 2 (dev) | — | CLI end-to-end tests, including the spawned scheduler + worker; it captures pipes, so anything about a *terminal* is a termlens test |
| `serde_json` | 1 (dev) | — | reads a serialized termlens `Screen` back in `oxidelake-tui/tests/emulation.rs`; already in the lockfile through DataFusion |
Expand Down
Loading