From 3cf2c621667e398d411d934773b4b7470d8bfb96 Mon Sep 17 00:00:00 2001 From: Will Washburn Date: Wed, 6 May 2026 23:37:25 -0400 Subject: [PATCH] relayburn-sdk: gate gap-writer override behind test-utils feature Move `set_ingest_gap_writer` / `restore_ingest_gap_writer` (and their `pub use` re-exports) behind `cfg(any(test, feature = "test-utils"))`. The functions are test-only API for capturing stderr in unit tests; today they ship in the SDK's default public surface, where embedders can hijack the global gap-warning writer for the whole process. - Add `test-utils` feature to relayburn-sdk/Cargo.toml. - Gate the two function definitions in `ingest/gap.rs`. - Split the `pub use` block in `ingest.rs` so the writer hooks only re-export under the gate; non-writer items (`emit_gap_warning`, `record_session_gap`, `reset_ingest_gap_warnings`, etc.) stay public. `reset_ingest_gap_warnings` is left ungated because its doc explicitly calls out that it's safe to invoke from production code. Closes #338. Co-Authored-By: Claude Opus 4.7 (1M context) --- crates/relayburn-sdk/Cargo.toml | 8 ++++++++ crates/relayburn-sdk/src/ingest.rs | 9 +++++++-- crates/relayburn-sdk/src/ingest/gap.rs | 9 +++++++++ 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/crates/relayburn-sdk/Cargo.toml b/crates/relayburn-sdk/Cargo.toml index 4e8e305a..d5e28dd3 100644 --- a/crates/relayburn-sdk/Cargo.toml +++ b/crates/relayburn-sdk/Cargo.toml @@ -7,6 +7,14 @@ license.workspace = true repository.workspace = true description = "Embedding API for relayburn — published to crates.io as the supported Rust surface." +[features] +# Opt-in feature for downstream integration tests that need the +# test-only API (`set_ingest_gap_writer` / `restore_ingest_gap_writer`). +# In-crate unit tests reach the same items via `cfg(test)`; external +# crates wanting the same hooks enable this feature explicitly so the +# items are NOT part of the SDK's default public surface. +test-utils = [] + [dependencies] # Deps absorbed from the four former lower crates # (`relayburn-{reader,ledger,analyze,ingest}`) when the monolith diff --git a/crates/relayburn-sdk/src/ingest.rs b/crates/relayburn-sdk/src/ingest.rs index 00264dcb..5ae13ada 100644 --- a/crates/relayburn-sdk/src/ingest.rs +++ b/crates/relayburn-sdk/src/ingest.rs @@ -66,9 +66,14 @@ pub use cursors::{ }; pub use gap::{ count_new_tool_calls, count_new_tool_results, count_tool_call_gaps, emit_gap_warning, - record_session_gap, reset_ingest_gap_warnings, restore_ingest_gap_writer, - set_ingest_gap_writer, AdapterName, ToolCallGapCounts, + record_session_gap, reset_ingest_gap_warnings, AdapterName, ToolCallGapCounts, }; +// Test-only writer-override hooks. Gated to `cfg(test)` for in-crate +// tests and to the `test-utils` feature for downstream integration +// tests; deliberately NOT part of the default SDK surface so embedders +// can't hijack the global gap-warning writer for the whole process. +#[cfg(any(test, feature = "test-utils"))] +pub use gap::{restore_ingest_gap_writer, set_ingest_gap_writer}; pub use ingest::{ ingest_all, ingest_claude_projects, ingest_claude_session, ingest_codex_sessions, ingest_opencode_sessions, IngestOptions, IngestReport, IngestRoots, diff --git a/crates/relayburn-sdk/src/ingest/gap.rs b/crates/relayburn-sdk/src/ingest/gap.rs index b3224002..7d50dcc5 100644 --- a/crates/relayburn-sdk/src/ingest/gap.rs +++ b/crates/relayburn-sdk/src/ingest/gap.rs @@ -135,6 +135,12 @@ pub fn reset_ingest_gap_warnings() { /// back to `set_ingest_gap_writer` in a `defer`-equivalent so a panicking /// test leaves the global state restored on the next test that pulls the /// guard. +/// +/// Gated to `cfg(test)` for in-crate tests and to the `test-utils` +/// feature for downstream integration tests so the hook is NOT part of +/// the SDK's default public surface — otherwise an embedder could +/// hijack stderr writes for the whole process. +#[cfg(any(test, feature = "test-utils"))] pub fn set_ingest_gap_writer(write: F) -> WriterFn where F: Fn(&str) + Send + Sync + 'static, @@ -146,6 +152,9 @@ where /// Restore a previously captured sink. Convenience for the /// `let prev = set_ingest_gap_writer(...); ...; restore_ingest_gap_writer(prev);` /// idiom — equivalent to `setIngestGapWriter(prev)` in TS. +/// +/// Gated alongside `set_ingest_gap_writer` — see that function. +#[cfg(any(test, feature = "test-utils"))] pub fn restore_ingest_gap_writer(write: WriterFn) { state_lock().write = write; }