Skip to content

Commit e893e9a

Browse files
committed
Merge branch 'main' into feature/dedupe-git-test-helpers
2 parents 1c7031b + db35448 commit e893e9a

12 files changed

Lines changed: 224 additions & 91 deletions

.beads/issues.jsonl

Lines changed: 67 additions & 67 deletions
Large diffs are not rendered by default.

crates/gossip-scanner-runtime/src/distributed/integration_tests.rs

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1730,6 +1730,97 @@ fn run_git_repo_worker_treats_cursor_covered_target_as_exhausted_empty() {
17301730
);
17311731
}
17321732

1733+
/// Multi-commit secret-bearing histories must complete successfully without
1734+
/// triggering shard parking. History depth alone is not a parking condition;
1735+
/// only error-class conditions (permanent failures, repeated transient errors,
1736+
/// poisoned state) qualify.
1737+
#[test]
1738+
fn run_git_repo_worker_completes_multi_commit_secret_history() {
1739+
const COMMIT_COUNT: usize = 16;
1740+
let repo = create_git_repo_fixture_with_secret_history(COMMIT_COUNT);
1741+
let mirror_root = tempdir().expect("mirror root");
1742+
let mut mirrors = LocalMirrorManager::new(mirror_root.path()).expect("mirror manager");
1743+
let backend = TestGitBackend::default();
1744+
let findings_sink = InMemoryFindingsSink::new();
1745+
let done_ledger = InMemoryDoneLedger::new();
1746+
let mut coordinator =
1747+
setup_coordinator_with_git_shard(repo.path(), CoordCursorUpdate::initial(), 120_000);
1748+
1749+
let report = run_git_repo_worker(
1750+
&mut coordinator,
1751+
&mut mirrors,
1752+
git_worker_identity(repo.path()),
1753+
backend.clone(),
1754+
DistributedPersistence::new(findings_sink.clone(), done_ledger.clone()),
1755+
DistributedRuntimeConfig::default(),
1756+
)
1757+
.expect("multi-commit secret-bearing history should scan successfully");
1758+
1759+
assert_eq!(report.leases_seen, 1);
1760+
assert_eq!(report.shards_scanned, 1);
1761+
assert_eq!(run_progress(&coordinator).done(), 1);
1762+
1763+
let summaries = shard_summaries(&coordinator);
1764+
assert_eq!(summaries.len(), 1);
1765+
assert_ne!(
1766+
summaries[0].status(),
1767+
ShardStatus::Parked,
1768+
"history depth alone must not park the shard"
1769+
);
1770+
assert_eq!(
1771+
summaries[0].status(),
1772+
ShardStatus::Done,
1773+
"history depth alone must not strand the shard"
1774+
);
1775+
1776+
let expected_key = git_repo_key(repo.path());
1777+
assert_eq!(
1778+
summaries[0]
1779+
.last_key()
1780+
.expect("completed shard should have a last_key"),
1781+
expected_key.as_bytes(),
1782+
"shard cursor last_key should match the singleton repo key"
1783+
);
1784+
1785+
assert!(
1786+
backend.batch_call_count() > 0,
1787+
"git repo worker must durably persist repo state before advancing the shard"
1788+
);
1789+
assert!(
1790+
!backend.stored_keys().is_empty(),
1791+
"persistence backend should contain durable state after a complete scan"
1792+
);
1793+
1794+
let persisted = findings_sink
1795+
.findings_snapshot()
1796+
.expect("findings snapshot");
1797+
assert!(
1798+
persisted.len() >= COMMIT_COUNT,
1799+
"each of the {COMMIT_COUNT} secret-bearing commits should produce at least one \
1800+
persisted finding (got {})",
1801+
persisted.len()
1802+
);
1803+
1804+
let rows = done_ledger.snapshot().expect("done-ledger snapshot");
1805+
assert_eq!(
1806+
rows.len(),
1807+
1,
1808+
"singleton repo shard produces one done-ledger row"
1809+
);
1810+
assert_eq!(
1811+
rows[0].status(),
1812+
DoneLedgerStatus::ScannedWithFindings,
1813+
"secret-bearing history should produce a findings-bearing done-ledger row"
1814+
);
1815+
// Single repo-frontier shard: the done-ledger's per-item findings_count
1816+
// equals total persisted finding rows because there is exactly one item.
1817+
assert_eq!(
1818+
rows[0].findings_count(),
1819+
u32::try_from(persisted.len()).expect("findings count fits u32"),
1820+
"done-ledger findings_count must match persisted findings"
1821+
);
1822+
}
1823+
17331824
/// Git repo-frontier shards require `CursorSemantics::Completed` so the
17341825
/// checkpoint cursor represents fully-processed and durable progress.
17351826
/// `Dispatched` semantics are rejected before any scan work begins.

crates/gossip-scanner-runtime/src/distributed/test_support.rs

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -430,17 +430,40 @@ pub(super) fn successor_bytes(bytes: &[u8]) -> Vec<u8> {
430430
next
431431
}
432432

433-
/// Git repo fixture seeded with a secret so scans produce at least one finding.
433+
/// Single-commit git repo fixture seeded with a secret so scans produce at least one finding.
434434
pub(super) fn create_git_repo_fixture_with_secrets() -> tempfile::TempDir {
435+
create_git_repo_fixture_with_secret_history(1)
436+
}
437+
438+
/// Git repo fixture where each commit introduces a secret in a distinct file.
439+
///
440+
/// Every commit adds a new `secret-{N}.txt`, so each commit's diff independently
441+
/// contains a detectable secret regardless of whether the scanner operates on
442+
/// full blobs or diffs.
443+
pub(super) fn create_git_repo_fixture_with_secret_history(
444+
commit_count: usize,
445+
) -> tempfile::TempDir {
446+
assert!(
447+
commit_count > 0,
448+
"secret-history fixture requires at least one commit"
449+
);
450+
435451
let dir = tempdir().expect("tempdir");
436452
init_git_repo(
437453
dir.path(),
438454
"distributed-runtime-tests@example.com",
439455
"Distributed Runtime Tests",
440456
);
441-
fs::write(dir.path().join("secret.txt"), secret_fixture()).expect("write fixture");
442-
run_git(dir.path(), &["add", "."]);
443-
run_git(dir.path(), &["commit", "-q", "-m", "fixture"]);
457+
458+
for commit in 0..commit_count {
459+
let filename = format!("secret-{commit}.txt");
460+
let contents = format!("{}\ncommit-{commit}\n", secret_fixture());
461+
fs::write(dir.path().join(&filename), contents).expect("write fixture");
462+
run_git(dir.path(), &["add", "."]);
463+
let message = format!("fixture-{commit}");
464+
run_git(dir.path(), &["commit", "-q", "-m", message.as_str()]);
465+
}
466+
444467
dir
445468
}
446469

docs/gossip-contracts/boundary-5-persistence.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ Non-negotiables (project-wide):
5959

6060
| Trait | Purpose | Key methods |
6161
|-------|---------|-------------|
62-
| `PersistenceFinding` | Unified finding-identity surface consumed by persistence translation, regardless of source family | `rule_id(&self) -> u32`, `norm_hash(&self) -> NormHash`, `span_start(&self) -> u64`, `span_end(&self) -> u64`, `span_len(&self) -> u64` |
62+
| `PersistenceFinding` | Unified finding-identity surface consumed by persistence translation, regardless of source family | `rule_id(&self) -> u32`, `norm_hash(&self) -> NormHash`, `blob_offset_start(&self) -> u64`, `blob_offset_end(&self) -> u64`, `blob_offset_len(&self) -> u64` |
6363
| `DoneLedger` | Dedupe index: "was this object-version scanned under this policy?" | `batch_get(&self, TenantId, PolicyHash, &[OvidHash]) -> Result<Vec<Option<DoneLedgerRecord>>, Self::Error>`, `list_done_hashes(&self, TenantId, PolicyHash) -> Result<Vec<OvidHash>, Self::Error>`, `batch_upsert(&self, &[DoneLedgerRecord]) -> Result<Self::CommitHandle, Self::Error>` |
6464
| `FindingsSink` | Triage/query plane: findings + occurrences + observations persistence | `upsert_batch(&self, FindingsUpsertBatch<'_>) -> Result<Self::CommitHandle, Self::Error>` |
6565
| `CommitHandle` | Durable acknowledgement handle; `wait()` consumes self and returns a receipt | `wait(self) -> Result<Self::Receipt, Self::Error>` |

docs/gossip-scanner-runtime.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@ and validation, and Git connector mode uses the direct path.
4242
| `src/distributed/execution.rs` | Lease execution functions (`run_filesystem_lease`, `run_git_repo_lease`) and worker entry points (`run_worker`, `run_git_repo_worker`) |
4343
| `src/distributed/commit_bridge.rs` | `ReceiptCommitSink` adapter, receipt-driven checkpoint building, `drain_commit_stage` |
4444
| `src/distributed/lease_ops.rs` | Lease lifecycle: `advance_shard`, `ArmedLeaseDeadline`, `watch_lease_deadline`, `LeaseUncertaintySignal` |
45+
| `src/distributed/integration_tests.rs` | Distributed lease orchestration and coordination integration tests |
46+
| `src/distributed/test_support.rs` | Test helpers, builders, and mock fixtures for distributed execution tests |
47+
| `src/distributed/unit_tests.rs` | Unit tests for lease lifecycle, commit bridging, and distributed types |
4548
| `src/event_sink.rs` | JSONL, text, JSON, and SARIF event sinks |
4649
| `src/git_discovery.rs` | Static single-target Git repository discovery source for payload-backed repo-frontier shards |
4750
| `src/git_executor.rs` | Contract-level adapter that implements `GitRepoExecutor` for mirror-backed repo scans by translating `GitSelection` + `GitExecutionLimits` into `scanner-git` config, propagating repo/policy identity into persistence-aware runs, and reusing the shared runtime runner |

docs/scanner-engine-integration-tests.md

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,12 @@ Each category is a separate test binary gated behind a Cargo feature:
3434

3535
| Binary | Path | Feature Gate | Tests |
3636
| --------------- | -------------------------- | -------------------- | ----: |
37-
| `integration` | `tests/integration/main.rs`| `integration-tests` | ~162 |
37+
| `integration` | `tests/integration/main.rs`| `integration-tests` | ~176 |
3838
| `property` | `tests/property/main.rs` | `property-tests` | ~130 |
39-
| `simulation` | `tests/simulation/main.rs` | various (see below) | ~41 |
39+
| `simulation` | `tests/simulation/main.rs` | various (see below) | ~42 |
4040
| `diagnostic` | `tests/diagnostic/main.rs` | `diagnostic-tests` | 2 |
4141
| `smoke` | `tests/smoke/main.rs` | `smoke-tests` | 0 |
42-
| *(standalone)* | `tests/chunked_file_scans.rs` | *(none)* | 2 |
42+
| *(standalone)* | `tests/chunked_file_scans.rs` | *(none)* | 3 |
4343

4444
## Feature Gates
4545

@@ -105,23 +105,24 @@ scanner-git boundaries.
105105
| -------------------------- | ----: | -------------------------------------------------- |
106106
| `anchor_optimization` | 14 | Anchor derivation and optimization |
107107
| `archive_scanning` | 49 | Archive expansion, virtual paths, budget limits |
108-
| `bench_guards` | 0 | Guards against benchmark execution without the benchmark feature gate (no active tests) |
108+
| `bench_guards` | 1 | Guards against benchmark execution without the benchmark feature gate |
109109
| `binary_awareness` | 10 | Binary file detection |
110110
| `finding_json` | 4 | JSONL finding parsing helpers used by integration assertions |
111-
| `git_commit_walk` | 6 | Commit graph traversal |
111+
| `git_commit_walk` | 8 | Commit graph traversal |
112112
| `git_engine_adapter` | 1 | Git-to-engine adapter |
113113
| `git_inmem_artifacts` | 13 | In-memory git artifact handling |
114114
| `git_mapping_bridge` | 3 | MIDX mapping bridge |
115115
| `git_pack_exec` | 1 | Pack execution |
116116
| `git_pack_inflate` | 4 | Pack inflation/decompression |
117117
| `git_pack_inflate_corpus` | 5 | Pathological zlib regression corpus |
118118
| `git_pack_plan` | 14 | Pack plan computation |
119-
| `git_persist` | 3 | Git persistence |
119+
| `git_persist` | 4 | Git persistence |
120120
| `git_preflight` | 4 | Git preflight checks |
121121
| `git_repo_open` | 4 | Repository opening |
122122
| `git_run_format` | 1 | Run format validation |
123-
| `git_scan_validation` | 10 | Git scan validation |
124-
| `git_seen_unique` | 2 | Deduplication of seen objects |
123+
| `git_scan_validation` | 15 | Git scan validation |
124+
| `git_seen_crash_recovery` | 3 | Seen-bitmap crash recovery |
125+
| `git_seen_unique` | 4 | Deduplication of seen objects |
125126
| `git_snapshot` | 1 | Snapshot testing |
126127
| `git_tree_diff` | 10 | Tree diff computation |
127128
| `manual_anchors` | 3 | Manual anchor specification |
@@ -148,7 +149,7 @@ contains both deterministic `#[test]` assertions and `proptest!` fuzz runs.
148149
| `git_spill_dedupe` | 3 | 2 | Spill deduplication |
149150
| `git_tree_diff` | 2 | 2 | Tree diff properties |
150151
| `path_policy_soundness` | 4 | 1 | Path allow/deny soundness |
151-
| `proptest_support` | 2 | 0 | Shared proptest helpers and shrinker guards |
152+
| `proptest_support` | 2 | 1 | Shared proptest helpers and shrinker guards |
152153
| `regex2anchor_soundness` | 26 | 2 | Regex-to-anchor derivation soundness |
153154
| `secret_bytes_safelist_soundness` | 3 | 1 | Safelist soundness |
154155
| `value_suppressor_soundness` | 2 | 1 | Value suppression soundness |

docs/scanner-scheduler/scheduler-device-slots.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ The device slots system implements **per-device admission control**:
108108

109109
```rust
110110
pub struct DeviceSlotPermit {
111-
inner: CountPermit,
111+
_inner: CountPermit,
112112
device: DeviceId,
113113
}
114114
```

docs/scanner-scheduler/scheduler-global-resource-pool.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,7 @@ The spill permission uses an enum to distinguish three cases:
229229
enum SpillGrant {
230230
NotRequested, // Job didn't request spill
231231
Unlimited, // Spill enabled; no slot counting
232-
Limited(CountPermit), // Spill enabled; using 1 counted slot
232+
Limited { _permit: CountPermit }, // Spill enabled; using 1 counted slot
233233
}
234234
```
235235

docs/scanner-scheduler/scheduler-local-fs-uring.md

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1079,7 +1079,7 @@ struct ArchiveWork {
10791079
struct UringArchiveSink<'a, E: ScanEngine> {
10801080
engine: &'a E,
10811081
scratch: &'a mut E::Scratch,
1082-
pending: &'a mut Vec<Finding>,
1082+
pending: &'a mut Vec<<E::Scratch as EngineScratch>::Finding>,
10831083
event_sink: &'a dyn EventOutput,
10841084
display: Vec<u8>, // Current entry display path
10851085
container_file_id: FileId, // ID of the archive file itself
@@ -1099,13 +1099,14 @@ struct UringArchiveSink<'a, E: ScanEngine> {
10991099
// Main loop for I/O worker thread
11001100
fn io_worker_loop<E: ScanEngine>(
11011101
_wid: usize,
1102-
rx: chan::Receiver<FileWork>, // File queue
1103-
pool: Arc<FixedBufferPool>, // Shared buffer pool
1104-
cpu: ExecutorHandle<CpuTask>, // CPU executor handle
1102+
rx: chan::Receiver<FileWork>,
1103+
pool: Arc<FixedBufferPool>,
1104+
cpu: ExecutorHandle<CpuTask>,
11051105
engine: Arc<E>,
11061106
cfg: LocalFsUringConfig,
1107-
stop: Arc<AtomicBool>, // Graceful shutdown flag
1108-
archive_tx: Option<chan::Sender<ArchiveWork>>, // Archive routing channel
1107+
stop: Arc<AtomicBool>,
1108+
archive_tx: Option<chan::Sender<ArchiveWork>>,
1109+
extract_tx: Option<chan::Sender<ExtractWork>>,
11091110
) -> io::Result<UringIoStats>;
11101111

11111112
// Archive worker loop (blocking decompression + scan)

docs/scanner-scheduler/scheduler_test_harness_guide.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,9 @@ SCHEDULER_SIM_STRESS_SEEDS=100 cargo test --features scheduler-sim scheduler_sim
3636
- **Corpus**: `crates/scanner-engine-integration-tests/tests/simulation/corpus/*.json` - regression tests replayed on every run
3737
- **Failures**: `crates/scanner-engine-integration-tests/tests/failures/*.json` - where stress failures are written
3838

39-
## SimCase DSL Reference
39+
## ReproArtifact DSL Reference
4040

41-
A `SimCase` defines a complete simulation scenario in JSON. The harness loads the case, initializes the executor, and steps through actions until completion or failure.
41+
A `ReproArtifact` defines a complete simulation scenario in JSON. The harness loads the case, initializes the executor, and steps through actions until completion or failure.
4242

4343
### Top-Level Structure
4444

0 commit comments

Comments
 (0)