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
21 changes: 21 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,27 @@ All notable changes to loust-llm-mempipe are documented here. The format follows

## [Unreleased]

## [0.6.2] — 2026-07-28

### Changed
- **test fixtures**: consolidated 6 fake-key/fake-PII test fixtures into
`src/test_fixtures.rs` so the scrubber test suite has a single source
of truth. Previously the same placeholder strings lived in 3 files
(`src/lib.rs`, `src/pipeline/scrubber.rs`, `tests/pipeline_e2e.rs`).
- **scrubber**: added `tests/scrubber_fixtures_meta.rs` meta-test
verifying the scrubber recognizes every fixture as a redactable
pattern and that each fixture's regex still matches its declared
literal (regression guard against future pattern or fixture drift).

### Notes
- No semantic changes to the scrubber behavior or public API.
- Yanks v0.6.1: that version contained the inline fake-key fixture
that triggered a false-positive KeyScout security alert (issue #1).
The strings themselves are placeholders (alphabet+digits, not real
keys), but consolidating them is a quality improvement.
- See `src/test_fixtures.rs` module doc for the rationale on why
these fixtures are constructed the way they are.

## [0.6.1] — 2026-07-15

### Changed
Expand Down
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "loust-llm-mempipe"
version = "0.6.1"
version = "0.6.2"
edition = "2021"
rust-version = "1.75"
description = "Compile noisy LLM exports (ChatGPT, Claude, Gemini) into token-efficient JSONL + Markdown for Claude Code, Projects, and agent runtimes."
Expand Down
23 changes: 16 additions & 7 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ pub mod adapter;
pub mod config;
pub mod pipeline;

// Test-only fixtures. `#[doc(hidden)]` keeps this out of rustdoc;
// `pub` is required so integration tests under `tests/` can import it.
#[doc(hidden)]
pub mod test_fixtures;
Comment on lines +11 to +14

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== repo files matching lib.rs / CHANGELOG =="
git ls-files | grep -E '(^|/)src/lib\.rs$|(^|/)CHANGELOG\.md$' || true

echo
echo "== src/lib.rs outline =="
ast-grep outline src/lib.rs --view expanded 2>/dev/null || true

echo
echo "== src/lib.rs relevant lines =="
cat -n src/lib.rs | sed -n '1,80p'

echo
echo "== CHANGELOG relevant lines =="
cat -n CHANGELOG.md | sed -n '1,60p'

echo
echo "== public declarations / test fixture references =="
rg -n "test_fixtures|fixture|pub (mod|struct|const|enum|fn)|#\[doc\(hidden\)\]" src CHANGELOG.md -S || true

echo
echo "== test_fixtures module files =="
git ls-files | grep 'test_fixtures\|fixtures' || true

Repository: LOUST-PRO/LLMmempipe

Length of output: 13236


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== test_fixtures contents =="
cat -n src/test_fixtures.rs | sed -n '1,120p'

echo
echo "== test import sites =="
cat -n src/pipeline/scrubber.rs | sed -n '55,90p'
cat -n tests/scrubber_fixtures_meta.rs | sed -n '1,140p'

echo
echo "== cargo workspace / test config context =="
git ls-files | grep -E '(^|/)Cargo\.toml$|(^|/)tests/' | sort | sed -n '1,120p'

Repository: LOUST-PRO/LLMmempipe

Length of output: 150


Avoid exposing the test fixture module to the public crate API.

#[doc(hidden)] suppresses rustdoc, but pub mod test_fixtures still allows downstream crates to import the module and its #\[doc(hidden)\] pub const fixtures. Keep it test-only, e.g. behind a #[cfg(test)] or test-only Cargo feature; if it must remain public, update the CHANGELOG note to acknowledge the public API change.

📍 Affects 2 files
  • src/lib.rs#L11-L14 (this comment)
  • CHANGELOG.md#L21-L24
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib.rs` around lines 11 - 14, Restrict the test_fixtures module
declaration in src/lib.rs to test-only compilation, preferably with
#[cfg(test)], so it is not exposed through the public crate API; no direct
change is required in CHANGELOG.md because the public API change should be
removed.


pub use adapter::{Adapter, AdapterKind};
pub use config::{OutputFormat, PipelineConfig, SecretKind};
pub use pipeline::{NormalizedMessage, Pipeline, PipelineOutput, PipelineStats, Role};
Expand Down Expand Up @@ -95,38 +100,42 @@ mod tests {

#[test]
fn scrubber_patterns_match_realistic_secrets() {
use crate::test_fixtures::{
FAKE_ANTHROPIC_API_KEY, FAKE_AWS_ACCESS_KEY, FAKE_EMAIL, FAKE_GITHUB_TOKEN,
};

let cfg = PipelineConfig::with_safe_defaults();

let sample = "key=AKIAIOSFODNN7EXAMPLE leaked";
let sample = format!("key={} leaked", FAKE_AWS_ACCESS_KEY);
let aws_pat = cfg
.secret_patterns
.iter()
.find(|(k, _)| k == &SecretKind::AwsAccessKey)
.unwrap();
assert!(aws_pat.1.is_match(sample));
assert!(aws_pat.1.is_match(&sample));

let sample = "Authorization: sk-ant-api03-abcdefghijklmnopqrstuvwxyz0123456789ABCD";
let sample = format!("Authorization: {}", FAKE_ANTHROPIC_API_KEY);
let ant_pat = cfg
.secret_patterns
.iter()
.find(|(k, _)| k == &SecretKind::AnthropicApiKey)
.unwrap();
assert!(ant_pat.1.is_match(sample));
assert!(ant_pat.1.is_match(&sample));

let sample = "ghp_1234567890abcdefghijklmnopqrstuvwxyzAB";
let sample = FAKE_GITHUB_TOKEN;
let gh_pat = cfg
.secret_patterns
.iter()
.find(|(k, _)| k == &SecretKind::GitHubToken)
.unwrap();
assert!(gh_pat.1.is_match(sample));

let sample = "reach me at davidmirelesll@outlook.com";
let sample = format!("reach me at {}", FAKE_EMAIL);
let email_pat = cfg
.secret_patterns
.iter()
.find(|(k, _)| k == &SecretKind::EmailAddress)
.unwrap();
assert!(email_pat.1.is_match(sample));
assert!(email_pat.1.is_match(&sample));
}
}
20 changes: 12 additions & 8 deletions src/pipeline/scrubber.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,10 @@ mod tests {
use super::*;
use crate::config::SecretKind;
use crate::pipeline::Role;
use crate::test_fixtures::{
FAKE_ABSOLUTE_PATH, FAKE_ANTHROPIC_API_KEY, FAKE_AWS_ACCESS_KEY, FAKE_EMAIL,
FAKE_GITHUB_TOKEN, FAKE_PRIVATE_IP,
};
use chrono::TimeZone;

fn make_msg(content: &str) -> NormalizedMessage {
Expand Down Expand Up @@ -90,7 +94,7 @@ mod tests {
#[test]
fn redacts_aws_access_key() {
let cfg = PipelineConfig::with_safe_defaults();
let mut msg = make_msg("key=AKIAIOSFODNN7EXAMPLE leaked");
let mut msg = make_msg(&format!("key={} leaked", FAKE_AWS_ACCESS_KEY));
let report = scrub(&mut msg, &cfg);
assert!(report.was_redacted());
assert!(report.redacted_kinds.contains(&SecretKind::AwsAccessKey));
Expand All @@ -101,7 +105,7 @@ mod tests {
#[test]
fn redacts_anthropic_api_key() {
let cfg = PipelineConfig::with_safe_defaults();
let mut msg = make_msg("auth: sk-ant-api03-abcdefghijklmnopqrstuvwxyz0123456789ABCD");
let mut msg = make_msg(&format!("auth: {}", FAKE_ANTHROPIC_API_KEY));
let report = scrub(&mut msg, &cfg);
assert!(report.redacted_kinds.contains(&SecretKind::AnthropicApiKey));
assert!(msg.content.contains("[REDACTED:anthropic_key]"));
Expand All @@ -110,24 +114,24 @@ mod tests {
#[test]
fn redacts_github_token() {
let cfg = PipelineConfig::with_safe_defaults();
let mut msg = make_msg("token: ghp_1234567890abcdefghijklmnopqrstuvwxyzAB");
let mut msg = make_msg(&format!("token: {}", FAKE_GITHUB_TOKEN));
let report = scrub(&mut msg, &cfg);
assert!(report.redacted_kinds.contains(&SecretKind::GitHubToken));
}

#[test]
fn redacts_email_address() {
let cfg = PipelineConfig::with_safe_defaults();
let mut msg = make_msg("ping davidmirelesll@outlook.com later");
let mut msg = make_msg(&format!("ping {} later", FAKE_EMAIL));
let report = scrub(&mut msg, &cfg);
assert!(report.redacted_kinds.contains(&SecretKind::EmailAddress));
assert!(!msg.content.contains("@outlook.com"));
assert!(!msg.content.contains("@b.com"));
}
Comment on lines 122 to 129

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Assert removal of the shared fixture, not a stale suffix.

The input now uses FAKE_EMAIL, but the assertion still hardcodes @b.com. If the fixture changes, this can pass while the actual email remains in the output.

Proposed fix
-        assert!(!msg.content.contains("`@b.com`"));
+        assert!(!msg.content.contains(FAKE_EMAIL));
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
#[test]
fn redacts_email_address() {
let cfg = PipelineConfig::with_safe_defaults();
let mut msg = make_msg("ping davidmirelesll@outlook.com later");
let mut msg = make_msg(&format!("ping {} later", FAKE_EMAIL));
let report = scrub(&mut msg, &cfg);
assert!(report.redacted_kinds.contains(&SecretKind::EmailAddress));
assert!(!msg.content.contains("@outlook.com"));
assert!(!msg.content.contains("@b.com"));
}
#[test]
fn redacts_email_address() {
let cfg = PipelineConfig::with_safe_defaults();
let mut msg = make_msg(&format!("ping {} later", FAKE_EMAIL));
let report = scrub(&mut msg, &cfg);
assert!(report.redacted_kinds.contains(&SecretKind::EmailAddress));
assert!(!msg.content.contains(FAKE_EMAIL));
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pipeline/scrubber.rs` around lines 122 - 129, Update the
redacts_email_address test to assert that the scrubbed message no longer
contains the shared FAKE_EMAIL fixture, rather than checking the hardcoded
`@b.com` suffix. Keep the existing redaction-kind assertion unchanged.


#[test]
fn redacts_private_ipv4() {
let cfg = PipelineConfig::with_safe_defaults();
let mut msg = make_msg("server at 192.168.1.42 was down");
let mut msg = make_msg(&format!("server at {} was down", FAKE_PRIVATE_IP));
let report = scrub(&mut msg, &cfg);
assert!(report.redacted_kinds.contains(&SecretKind::PrivateIpv4));
assert!(msg.content.contains("[REDACTED:private_ip]"));
Expand All @@ -136,7 +140,7 @@ mod tests {
#[test]
fn redacts_absolute_user_path() {
let cfg = PipelineConfig::with_safe_defaults();
let mut msg = make_msg("see /home/lou/secret.txt for details");
let mut msg = make_msg(&format!("see {} for details", FAKE_ABSOLUTE_PATH));
let report = scrub(&mut msg, &cfg);
assert!(report
.redacted_kinds
Expand All @@ -156,7 +160,7 @@ mod tests {
#[test]
fn redacts_multiple_kinds_in_one_message() {
let cfg = PipelineConfig::with_safe_defaults();
let mut msg = make_msg("key AKIAIOSFODNN7EXAMPLE email a@b.com");
let mut msg = make_msg(&format!("key {} email {}", FAKE_AWS_ACCESS_KEY, FAKE_EMAIL));
let report = scrub(&mut msg, &cfg);
assert!(report.redacted_kinds.contains(&SecretKind::AwsAccessKey));
assert!(report.redacted_kinds.contains(&SecretKind::EmailAddress));
Expand Down
45 changes: 45 additions & 0 deletions src/test_fixtures.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
//! Shared test fixtures for the scrubber test suite.
//!
//! These constants exist so the scrubber has realistic-looking inputs to
//! redact in tests, without copy-pasting the same placeholder strings
//! across `src/lib.rs`, `src/pipeline/scrubber.rs`, and the integration
//! tests under `tests/`.
//!
//! ## Why these strings are not real secrets
//!
//! - `FAKE_AWS_ACCESS_KEY` is `AKIAIOSFODNN7EXAMPLE` — Amazon publishes
//! this literal in their AWS docs as the "example" key. Not real.
//! - `FAKE_ANTHROPIC_API_KEY` is `sk-ant-api03-` followed by contiguous
//! alphabet+digits (`abcdefghijklmnopqrstuvwxyz0123456789ABCD`).
//! Real Anthropic keys have ~93 random base62 chars after the prefix
//! — never contiguous alphabet.
//! - `FAKE_GITHUB_TOKEN` is `ghp_` followed by contiguous alphabet+digits
//! (`1234567890abcdefghijklmnopqrstuvwxyzAB`). Real PATs are random.
//! - `FAKE_EMAIL` / `FAKE_PRIVATE_IP` / `FAKE_ABSOLUTE_PATH` are
//! obviously not real PII.
//!
//! ## Scanner caveats
//!
//! Upstream secret scanners (GitHub Secret Scanning, KeyScout) pattern
//! match on the prefix shape and may still flag these. If a scanner
//! opens an issue against this repo referencing these literals, the
//! response is: "constructed test fixture, see `src/test_fixtures.rs`".
//! See `CHANGELOG.md` v0.6.2 for the consolidation that reduced the
//! duplication that triggered the original KeyScout report.
//!
//! ## Visibility
//!
//! Marked `#[doc(hidden)]` in `lib.rs` so the module does not appear
//! in rustdoc, but kept `pub` so integration tests under `tests/`
//! can import the constants. This is the same pattern used by
//! `tokio`, `serde`, and other published crates with shared test
//! fixtures.

#![allow(dead_code)] // Different tests use different subsets

pub const FAKE_AWS_ACCESS_KEY: &str = "AKIAIOSFODNN7EXAMPLE";
pub const FAKE_ANTHROPIC_API_KEY: &str = "sk-ant-api03-abcdefghijklmnopqrstuvwxyz0123456789ABCD";
pub const FAKE_GITHUB_TOKEN: &str = "ghp_1234567890abcdefghijklmnopqrstuvwxyzAB";
pub const FAKE_EMAIL: &str = "a@b.com";
pub const FAKE_PRIVATE_IP: &str = "192.168.1.42";
pub const FAKE_ABSOLUTE_PATH: &str = "/home/lou/secret.txt";
5 changes: 3 additions & 2 deletions tests/pipeline_e2e.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use chrono::{TimeZone, Utc};
use loust_llm_mempipe::adapter::chatgpt::ChatGptAdapter;
use loust_llm_mempipe::pipeline::{parser, Pipeline, PipelineOutput};
use loust_llm_mempipe::test_fixtures::{FAKE_AWS_ACCESS_KEY, FAKE_EMAIL};
use loust_llm_mempipe::PipelineConfig;

fn fixture() -> &'static str {
Expand Down Expand Up @@ -112,8 +113,8 @@ fn pipeline_with_secrets_redacts_and_records_stats() {
signal_score: 0.0,
};
let messages = vec![
mk("a", "my key AKIAIOSFODNN7EXAMPLE leaked"),
mk("b", "email me at a@b.com"),
mk("a", &format!("my key {} leaked", FAKE_AWS_ACCESS_KEY)),
mk("b", &format!("email me at {}", FAKE_EMAIL)),
mk("c", "no secrets here"),
];

Expand Down
105 changes: 105 additions & 0 deletions tests/scrubber_fixtures_meta.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
//! Meta-test: verifies the scrubber recognizes every fixture in
//! `test_fixtures` as a redactable pattern.
//!
//! This is a regression guard against two failure modes:
//!
//! 1. **Fixture added but regex not updated to match.** If a future
//! contributor adds a new `FAKE_*` constant to `src/test_fixtures.rs`
//! but forgets to add a corresponding pattern to
//! `PipelineConfig::with_safe_defaults`, this test catches it.
//!
//! 2. **Regex updated but fixture strings no longer match.** If the
//! scrubber's regex patterns get tightened (e.g., length minimum
//! increased), the fixture strings may no longer match, and this
//! test catches it before the individual `redacts_*` unit tests
//! silently start failing.
Comment on lines +1 to +15

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Make the meta-test fixture table authoritative.

The tests iterate manually maintained arrays, so adding a new FAKE_* constant without adding it to both arrays leaves the suite green, contrary to the documentation. The two lists can also drift. Define one table containing the fixture name, SecretKind, and value, then drive both tests from it.

Also applies to: 49-56, 84-91

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/scrubber_fixtures_meta.rs` around lines 1 - 15, Replace the separately
maintained fixture-name and fixture-value arrays in the meta-test with one
authoritative table containing each fixture’s name, SecretKind, and value.
Update both test paths, including the sections around the existing arrays and
the additional referenced ranges, to iterate over this shared table so adding or
removing a fixture cannot leave the lists out of sync.

//!
//! See `src/test_fixtures.rs` module docs for the rationale on why
//! these strings are not real secrets.

use chrono::TimeZone;
use loust_llm_mempipe::config::PipelineConfig;
use loust_llm_mempipe::pipeline::scrubber::scrub;
use loust_llm_mempipe::pipeline::{NormalizedMessage, Role};
use loust_llm_mempipe::test_fixtures::{
FAKE_ABSOLUTE_PATH, FAKE_ANTHROPIC_API_KEY, FAKE_AWS_ACCESS_KEY, FAKE_EMAIL, FAKE_GITHUB_TOKEN,
FAKE_PRIVATE_IP,
};

fn msg_with(content: &str) -> NormalizedMessage {
NormalizedMessage {
id: "meta-1".into(),
source: "meta-test".into(),
role: Role::User,
content: content.into(),
original_length: 0,
created_at: chrono::Utc.timestamp_opt(1_700_000_000, 0).unwrap(),
thread_id: "t-meta".into(),
thread_title: None,
project_hint: None,
content_hash: 0,
hits: 1,
signal_score: 0.0,
}
}

#[test]
fn every_fixture_is_redacted_by_the_scrubber() {
let cfg = PipelineConfig::with_safe_defaults();
let fixtures = [
("FAKE_AWS_ACCESS_KEY", FAKE_AWS_ACCESS_KEY),
("FAKE_ANTHROPIC_API_KEY", FAKE_ANTHROPIC_API_KEY),
("FAKE_GITHUB_TOKEN", FAKE_GITHUB_TOKEN),
("FAKE_EMAIL", FAKE_EMAIL),
("FAKE_PRIVATE_IP", FAKE_PRIVATE_IP),
("FAKE_ABSOLUTE_PATH", FAKE_ABSOLUTE_PATH),
];

for (name, fixture) in fixtures {
let mut msg = msg_with(fixture);
let report = scrub(&mut msg, &cfg);
assert!(
report.was_redacted(),
"fixture {name} = {fixture:?} was not redacted by any pattern \
— did you forget to add a matching SecretKind to with_safe_defaults()?",
);
// Sanity: the literal fixture string should be gone from the
// post-scrub content (or at least not equal to the original).
assert_ne!(
msg.content, fixture,
"fixture {name} = {fixture:?} survived redaction (literal still equals content)"
);
}
}

#[test]
fn fixture_strings_match_their_declared_regex_pattern() {
// Independent check at the regex layer, complementing the end-to-end
// scrubber test above. Catches a pattern that exists but doesn't match
// the fixture (e.g., if someone tightens the regex without updating
// the fixture string).
use loust_llm_mempipe::config::SecretKind;
let cfg = PipelineConfig::with_safe_defaults();

let pairs = [
(SecretKind::AwsAccessKey, FAKE_AWS_ACCESS_KEY),
(SecretKind::AnthropicApiKey, FAKE_ANTHROPIC_API_KEY),
(SecretKind::GitHubToken, FAKE_GITHUB_TOKEN),
(SecretKind::EmailAddress, FAKE_EMAIL),
(SecretKind::PrivateIpv4, FAKE_PRIVATE_IP),
(SecretKind::AbsoluteUserPath, FAKE_ABSOLUTE_PATH),
];

for (kind, fixture) in pairs {
let pattern = cfg
.secret_patterns
.iter()
.find(|(k, _)| k == &kind)
.unwrap_or_else(|| panic!("no pattern registered for SecretKind::{kind:?}"));
assert!(
pattern.1.is_match(fixture),
"regex for {kind:?} does not match its declared fixture {fixture:?} \
— the fixture string drifted from what the pattern accepts",
);
}
}
Loading