Skip to content
Closed
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
77 changes: 77 additions & 0 deletions .github/workflows/hotpath-slice-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# Hotpath coverage for the sessions/lcm/capture/temporal-query slice.
#
# Proves this slice's hotpath instrumentation contract on every pull
# request: feature-off builds keep every hotpath macro a no-op (no metrics
# listener, no report file, hotpath absent from default features), while
# `--features hotpath` builds compile, run the same suites, and capture real
# measured sites in a guard report (see each crate's
# `tests/hotpath_coverage.rs`).
#
# This complements `.github/workflows/hotpath-profile.yml` — the index-bench
# profiling lane owned by the query/index slice — and must not replace it.
# Benches for this slice are deliberately deferred; this lane is tests only.
name: hotpath-slice-tests

on:
pull_request:
workflow_dispatch:

concurrency:
group: hotpath-slice-tests-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

permissions:
contents: read

env:
# Feature-on test binaries must never bind the live metrics listener in CI;
# the coverage tests also set this themselves before building a guard.
HOTPATH_METRICS_SERVER_OFF: "1"
SLICE_PACKAGES: >-
-p tracedecay-sessions
-p tracedecay-session-memory
-p tracedecay-session-runtime
-p tracedecay-session-temporal-store
-p tracedecay-lcm
-p tracedecay-capture
-p tracedecay-temporal-query

jobs:
slice-tests:
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- uses: actions/checkout@v4

- uses: dtolnay/rust-toolchain@stable

- uses: Swatinem/rust-cache@v2
with:
shared-key: hotpath-slice-tests
cache-on-failure: true

- name: kache compiler cache
uses: actions/cache@v4
with:
path: |
~/.cargo/bin/kache
~/.cache/kache
key: kache-${{ runner.os }}-${{ hashFiles('**/Cargo.lock') }}
restore-keys: kache-${{ runner.os }}-
- name: Enable kache
shell: bash
run: |
command -v kache >/dev/null 2>&1 || cargo install kache --locked
kache init --no-service
echo "RUSTC_WRAPPER=kache" >> "$GITHUB_ENV"

# Feature-off first: this is the production shape. The per-crate
# hotpath_coverage tests assert the no-op contract (no listener on
# 6770/6771, no report file, hotpath not in default features).
- name: Slice tests (hotpath feature off)
run: cargo test ${{ env.SLICE_PACKAGES }} --locked

# Feature-on second: the same suites plus the guard-report tests, which
# fail if the slice's instrumented sites stop being hit.
- name: Slice tests (hotpath feature on)
run: cargo test ${{ env.SLICE_PACKAGES }} --features hotpath --locked
153 changes: 153 additions & 0 deletions crates/tracedecay-capture/tests/hotpath_coverage.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
//! Hotpath coverage contract for `tracedecay-capture`.
//!
//! Feature-off (default build): every hotpath macro must be a no-op — no
//! metrics listener on 6770/6771, no report file even when the report
//! environment is set, and `hotpath` must stay out of the crate's default
//! features.
//!
//! Feature-on (`--features hotpath`): a process-boundary guard must capture
//! this crate's measured parse sites in a functions-timing report, proving
//! the instrumentation is real rather than dead configuration.

use serde_json::json;
use tracedecay_capture::parse_claude_record_v1;
use tracedecay_domain::ClaudeByteRangeV1;

/// Deterministic, daemon-free workload that reaches this crate's measured
/// parse path (`capture.parse.record` and `capture.parse.record_digest`).
fn run_capture_parse_workload() -> usize {
let record = serde_json::to_vec(&json!({
"type": "assistant",
"message": { "content": "hotpath coverage fixture" },
}))
.expect("serialize claude record fixture");
let range = ClaudeByteRangeV1::new(0, record.len() as u64).expect("valid fixture byte range");
let parsed = parse_claude_record_v1(&record, range).expect("parse claude record fixture");
assert_eq!(parsed.encoded_len(), record.len());
assert_eq!(
parsed.value()["message"]["content"],
"hotpath coverage fixture"
);
parsed.encoded_len()
}

/// Collects the entries of one feature array (for example `default`) from
/// this crate's manifest, tolerating multi-line arrays. Returns `None` when
/// the feature is not declared at all.
fn manifest_feature_array(manifest: &str, feature: &str) -> Option<String> {
let mut in_features = false;
let mut collecting = false;
let mut collected = String::new();
for line in manifest.lines() {
let trimmed = line.trim();
if trimmed.starts_with('[') {
in_features = trimmed == "[features]";
continue;
}
if collecting {
collected.push_str(trimmed);
if trimmed.contains(']') {
return Some(collected);
}
continue;
}
if in_features
&& let Some(rest) = trimmed.strip_prefix(feature)
&& let Some(array) = rest.trim_start().strip_prefix('=')
{
collected.push_str(array.trim());
if collected.contains(']') {
return Some(collected);
}
collecting = true;
}
}
None
}

/// The profiling features must remain opt-in: neither `default` nor any
/// production-shaped feature set of this crate may pull in hotpath.
#[test]
fn hotpath_stays_out_of_default_and_production_features() {
let manifest = std::fs::read_to_string(concat!(env!("CARGO_MANIFEST_DIR"), "/Cargo.toml"))
.expect("read crate manifest");
for gate in ["default", "production"] {
if let Some(entries) = manifest_feature_array(&manifest, gate) {
assert!(
!entries.contains("hotpath"),
"feature `{gate}` must never enable hotpath, found: {entries}"
);
}
}
}

#[cfg(not(feature = "hotpath"))]
mod feature_off {
use std::net::TcpStream;
use std::path::Path;

/// With the feature off the macros expand to their primary expression:
/// the workload behaves identically, the report environment is ignored,
/// and no metrics listener appears.
#[test]
fn workload_is_a_no_op_for_profiling() {
let report = Path::new(env!("CARGO_TARGET_TMPDIR")).join("capture-hotpath-off.json");
let _ = std::fs::remove_file(&report);
// SAFETY: single-threaded with respect to readers — the feature-off
// build contains no hotpath runtime and nothing else in this test
// binary reads these variables.
unsafe {
std::env::set_var("HOTPATH_OUTPUT_FORMAT", "json");
std::env::set_var("HOTPATH_OUTPUT_PATH", &report);
}

assert!(super::run_capture_parse_workload() > 0);

assert!(
!report.exists(),
"feature-off build must never write a hotpath report"
);
for port in [6770u16, 6771] {
assert!(
TcpStream::connect(("127.0.0.1", port)).is_err(),
"feature-off build must not expose a hotpath listener on port {port} \
(a listener here means another process on this machine is serving it)"
);
}
}
}

#[cfg(feature = "hotpath")]
mod feature_on {
use std::path::Path;

/// A guard-scoped run of the same workload must record this crate's
/// measured sites, proving `--features hotpath` produces live
/// instrumentation and not an empty report.
#[test]
fn guard_report_captures_measured_parse_sites() {
// SAFETY: set before the first guard build in this process, which is
// the only reader; the metrics listener must stay off in tests.
unsafe { std::env::set_var("HOTPATH_METRICS_SERVER_OFF", "1") };
let report = Path::new(env!("CARGO_TARGET_TMPDIR")).join("capture-hotpath-on.json");
let _ = std::fs::remove_file(&report);

{
let _guard = hotpath::HotpathGuardBuilder::new("capture-hotpath-coverage")
.format(hotpath::Format::Json)
.output_path(&report)
.report("functions-timing")
.build();
assert!(super::run_capture_parse_workload() > 0);
}

let report_text =
std::fs::read_to_string(&report).expect("feature-on guard drop must write a report");
for label in ["capture.parse.record", "capture.parse.record_digest"] {
assert!(
report_text.contains(label),
"hotpath report must capture measured site `{label}`: {report_text}"
);
}
}
}
Loading
Loading