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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ Follows [Keep a Changelog](https://keepachangelog.com/); versioning is [SemVer](

- sl-viewer bundle-diff property surface (WBS-6.2 #434): `crates/sl-viewer/tests/properties_viewer_bundle_diff.rs` adds 10 proptest properties — `diff_fields` returns the documented field set in stable order, is reflexive on `a == a`, idempotent on `a == a.clone()`, value-flipped symmetric (`diff_fields(b, a)` swaps `value_a`/`value_b` while `differs` matches), `differs` matches `value_a != value_b`, and `Option<String>` fields render the em-dash fallback when both sides are `None`. `OkfBundle::from_bundle` properties pin the reduction: `message_count` matches slice count, `has_acceptance`/`has_contract` reflect kind presence, `token_count` falls back to 0 when no Intent slice carries numeric `user_turn_count`, `source_id` carries through unchanged.

- sl-viewer corpus_paths round-trip property surface (WBS-6.2 #446): `crates/sl-viewer/tests/properties_viewer_corpus_paths.rs` adds 10 proptest properties — `CorpusPathConfig::empty()` produces a config with zero custom paths; `Default::default()` equals `empty()`; `is_empty()` is true iff `custom_paths` is empty. JSON round-trip preserves `custom_paths` exactly (order-sensitive), is idempotent, and preserves length. `save_config_to(c, p); load_config_from(p)` round-trips equal configs; missing files yield `Ok(empty())`; junk JSON surfaces `Err`; `save_config_to` creates missing parent directories.

- Wave-44 plan landed: `WAVE44_SCOPE.md` + `docs/ops/WAVE44_PERT.md` enumerate 6 close-out lanes (3 machine, 3 human-gated) for the 6 unpaid residuals from Wave-43 (396/402 → 402/402 target). Theme: stack-stability closure + i18n migration + eval coverage + supply-chain signing.
- Wave-44 reaudit (Wave-44-D): `audit/SCORECARD.md` refresh at commit `13c974f7` (machine-w44-reaudit); `docs/ops/TRACEABILITY.json` overall_audit wave=Wave-44 commit=13c974f7 (conservative hold at 396/402); `docs/ops/GAP_QA_MATRIX.md` C00 + C08 + PLAN-W8-B rows reflect Wave-44 closure (#368 W44-B6 corpus / #372 W44-B1 loom / #373 PERT correction). 2 of 3 machine lanes shipped 2026-07-24; remaining 6 raw pts across C04 L36 / C08 L76 / C11 L110.

Expand Down
208 changes: 208 additions & 0 deletions crates/sl-viewer/tests/properties_viewer_corpus_paths.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
//! Property evidence for sl-viewer's `corpus_paths` module.

Check failure on line 1 in crates/sl-viewer/tests/properties_viewer_corpus_paths.rs

View workflow job for this annotation

GitHub Actions / Trunk Check

rustfmt

Incorrect formatting, autoformat by running 'trunk fmt'
//!
//! Integration tests. The unit tests in `corpus_paths.rs` pin specific
//! values; these properties pin invariants over the full shape of
//! inputs the helpers can receive.
//!
//! `CorpusPathConfig` invariants:
//! * `empty()` produces a config with zero custom paths.
//! * `is_empty()` is true iff `custom_paths.is_empty()`.
//! * `Default::default()` equals `empty()`.
//! * JSON round-trip preserves `custom_paths` exactly (order-sensitive).
//!
//! `save_config_to` / `load_config_from` invariants:
//! * Round-trip: `save_config_to(c, p); load_config_from(p) == c`.
//! * Missing file yields `Ok(empty())` (no error surfaced).
//! * Junk JSON surfaces an `Err` (never silently drops the file).
//! * `save_config_to` creates missing parent directories.
//!
//! proptest is added to `sl-viewer/[dev-dependencies]` (mirroring the
//! workspace root); see PR #425 for the initial wiring.

use std::fs;
use std::path::PathBuf;

use proptest::prelude::*;
use sl_viewer::corpus_paths::{
load_config_from, save_config_to, CorpusPathConfig,
};

// ── strategies ──────────────────────────────────────────────────────────────

/// Strategy for a list of relative / absolute path-like strings.
fn path_strategy() -> impl Strategy<Value = PathBuf> {
prop::string::string_regex("[/a-zA-Z0-9._-]{1,40}")
.expect("valid regex")
.prop_map(PathBuf::from)
}

/// Strategy for a `CorpusPathConfig` with 0..6 paths.
fn config_strategy() -> impl Strategy<Value = CorpusPathConfig> {
prop::collection::vec(path_strategy(), 0..6).prop_map(|paths| CorpusPathConfig {
custom_paths: paths,
})
}

/// Strategy for junk JSON content that is *not* valid `CorpusPathConfig`.
fn junk_json_strategy() -> impl Strategy<Value = String> {
prop::sample::select(vec![
// Plain garbage.
"not json at all".to_owned(),
// Empty string.
String::new(),
// Truncated object.
r#"{"custom_paths":["#.to_owned(),
// Wrong shape — `custom_paths` as a number.
r#"{"custom_paths": 42}"#.to_owned(),
// Wrong shape — `custom_paths` as an object.
r#"{"custom_paths": {"k": "v"}}"#.to_owned(),
// Trailing junk.
r#"{"custom_paths": []} trailing junk"#.to_owned(),
])
}

// ── CorpusPathConfig pure reductions ────────────────────────────────────────

proptest! {
/// Property: `empty()` returns a config with zero `custom_paths`.
#[test]
fn empty_has_no_custom_paths(_i in 0u8..4) {
let config = CorpusPathConfig::empty();
prop_assert!(config.custom_paths.is_empty());
prop_assert!(config.is_empty());
}

/// Property: `Default::default()` equals `empty()`.
#[test]
fn default_equals_empty(_i in 0u8..4) {
let a: CorpusPathConfig = CorpusPathConfig::default();
let b: CorpusPathConfig = CorpusPathConfig::empty();
prop_assert_eq!(a, b);
}

/// Property: `is_empty()` is true iff `custom_paths` is empty.
#[test]
fn is_empty_iff_no_paths(config in config_strategy()) {
let expected = config.custom_paths.is_empty();
prop_assert_eq!(config.is_empty(), expected);
}

/// Property: JSON round-trip preserves `custom_paths` exactly
/// (order-sensitive — the on-disk contract is `Vec<PathBuf>`).
#[test]
fn json_round_trip_preserves_paths(config in config_strategy()) {
let json = serde_json::to_string(&config).expect("serialize");
let restored: CorpusPathConfig = serde_json::from_str(&json).expect("parse");
prop_assert_eq!(restored, config);
}

/// Property: JSON round-trip is idempotent — round-tripping a
/// restored config yields the same JSON bytes.
#[test]
fn json_round_trip_idempotent(config in config_strategy()) {
let json1 = serde_json::to_string(&config).expect("serialize 1");
let restored: CorpusPathConfig = serde_json::from_str(&json1).expect("parse 1");
let json2 = serde_json::to_string(&restored).expect("serialize 2");
prop_assert_eq!(json1, json2);
}

/// Property: `len(custom_paths)` is preserved through JSON
/// round-trip (catches drift where the round-trip drops / dedups
/// path entries).
#[test]
fn json_round_trip_preserves_len(config in config_strategy()) {
let json = serde_json::to_string(&config).expect("serialize");
let restored: CorpusPathConfig = serde_json::from_str(&json).expect("parse");
prop_assert_eq!(restored.custom_paths.len(), config.custom_paths.len());
}
}

// ── save_config_to / load_config_from ───────────────────────────────────────

proptest! {
/// Property: `save_config_to` followed by `load_config_from` yields
/// an equal config (round-trip). This is the contract the viewer's
/// "user picks a folder" → "viewer reads it back" flow depends on.
#[test]
fn save_load_round_trip(config in config_strategy(), i in 0u8..3) {
let dir = std::env::temp_dir().join(format!(
"sessionledger-corpus-paths-roundtrip-{}-{}",
std::process::id(),
i,
));
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(&dir).expect("mkdir");
let path = dir.join("corpus_paths.json");

save_config_to(&config, &path).expect("save");
let restored = load_config_from(&path).expect("load");

prop_assert_eq!(restored, config);

let _ = fs::remove_dir_all(&dir);
}

/// Property: `load_config_from(<missing>)` returns `Ok(empty())`
/// — the viewer's first launch on a new machine must not fail
/// just because the user hasn't picked anything yet.
#[test]
fn missing_file_yields_empty_config(i in 0u8..4) {
let dir = std::env::temp_dir().join(format!(
"sessionledger-corpus-paths-missing-{}-{}",
std::process::id(),
i,
));
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(&dir).expect("mkdir");
let path = dir.join("does-not-exist.json");

let result = load_config_from(&path);
prop_assert!(result.is_ok(), "missing file must yield Ok, got {:?}", result.err());
let config = result.unwrap();
prop_assert!(config.is_empty());

let _ = fs::remove_dir_all(&dir);
}

/// Property: `load_config_from(<junk>)` surfaces an `Err` — the
/// viewer must never silently drop the user's picks on a
/// malformed file.
#[test]
fn junk_json_surfaces_error(junk in junk_json_strategy(), i in 0u8..3) {
let dir = std::env::temp_dir().join(format!(
"sessionledger-corpus-paths-junk-{}-{}",
std::process::id(),
i,
));
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(&dir).expect("mkdir");
let path = dir.join("corpus_paths.json");
fs::write(&path, junk.as_bytes()).expect("write junk");

let result = load_config_from(&path);
prop_assert!(result.is_err(), "junk JSON must surface as Err, got {result:?}");

let _ = fs::remove_dir_all(&dir);
}

/// Property: `save_config_to` creates missing parent directories
/// (the viewer may save into a fresh `~/.../SessionLedger/` that
/// doesn't exist yet).
#[test]
fn save_creates_parent_directories(config in config_strategy(), i in 0u8..3) {
let dir = std::env::temp_dir().join(format!(
"sessionledger-corpus-paths-nested-{}-{}",
std::process::id(),
i,
));
let _ = fs::remove_dir_all(&dir);
let nested = dir.join("a").join("b").join("c").join("corpus_paths.json");
prop_assert!(!nested.parent().expect("parent").exists());

save_config_to(&config, &nested).expect("save nested");

prop_assert!(nested.exists());

let _ = fs::remove_dir_all(&dir);
}
}
1 change: 1 addition & 0 deletions docs/ops/TRACEABILITY.json
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,7 @@
"crates/sl-viewer/tests/properties_viewer_corpus_cta.rs",
"crates/sl-viewer/tests/properties_viewer_theme.rs",
"crates/sl-viewer/tests/properties_viewer_settings.rs",
"crates/sl-viewer/tests/properties_viewer_corpus_paths.rs",
"fuzz/fuzz_targets/okf_roundtrip.rs",
"fuzz/fuzz_targets/jsonl_ingest.rs",
".github/workflows/ci.yml",
Expand Down
2 changes: 1 addition & 1 deletion docs/ops/WBS.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ without a new audit.
| WBS-4.2 | P4 FTS recall via context-mode and explicit TUI decision | partial | human | `docs/DESIGN.md` §3, §7; `crates/sl-viewer/` | DESIGN P4 residual; C00, C11 |
| WBS-5.1 | P5 deterministic dedup merge and crash/lost-work recovery E2E | done | machine | `src/domain/merge.rs`; `src/domain/worklog.rs`; `tests/merge_recovery.rs` | FR-011; T-024, T-035; C03 |
| WBS-6.1 | P6 85% coverage gate and deterministic golden corpus | done | machine | `.github/workflows/ci.yml`; `tests/okf_golden.rs`; `tests/fixtures/okf/` | T-037, T-038; C01, C08 |
| WBS-6.2 | P6 property tests, fuzzing, race checks, and enforced performance budgets | partial | machine | `tests/properties.rs`; `crates/sl-viewer/tests/properties_viewer.rs`; `crates/sl-viewer/tests/properties_viewer_theme_url.rs`; `crates/sl-viewer/tests/properties_viewer_unfinished_tab.rs`; `crates/sl-viewer/tests/properties_viewer_timeline.rs`; `crates/sl-viewer/tests/properties_viewer_search_memory.rs`; `crates/sl-viewer/tests/properties_viewer_history.rs`; `crates/sl-viewer/tests/properties_viewer_web_exports.rs`; `crates/sl-viewer/tests/properties_viewer_bundle_detail.rs`; `crates/sl-viewer/tests/properties_viewer_bundle_diff.rs`; `crates/sl-viewer/tests/properties_viewer_mock_data.rs`; `crates/sl-viewer/tests/properties_viewer_cli_help.rs`; `crates/sl-viewer/tests/properties_viewer_corpus_cta.rs`; `crates/sl-viewer/tests/properties_viewer_theme.rs`; `crates/sl-viewer/tests/properties_viewer_settings.rs`; `fuzz/fuzz_targets/okf_roundtrip.rs`; `fuzz/fuzz_targets/jsonl_ingest.rs`; `.github/workflows/ci.yml`; `.github/workflows/bench-gate.yml`; `docs/ops/perf-baseline.json`; `scripts/bench-gate.ps1`; `benches/pipeline.rs`; `tests/loom_model.rs` | DESIGN P6 residual; C00 L6-L8; C07 L66-L68; C08 L74; perf-budget enforced Wave-26 #223; p95 latency enforced Wave-30 #256; FSM properties Wave-31 #261; soft loom Wave-31 #264; viewer corpus_paths/parquet/settings properties #425; viewer theme + daemon_url properties #427; viewer unfinished_tab properties + fuzz/rootless CI drift fixes #428; viewer bundle_diff + timeline properties + web_exports/hmetic-pin cleanups #432; viewer bundle_diff properties #434; viewer search/memory properties #435; viewer history_tab properties #444; viewer web_exports properties #437; viewer bundle_list + detail_pane properties #436; viewer mock_data fixture properties #451; viewer cli_help / command_palette properties #452; viewer corpus_cta constants properties #453; viewer theme + settings properties #454; full loom/shuttle unpaid |
| WBS-6.2 | P6 property tests, fuzzing, race checks, and enforced performance budgets | partial | machine | `tests/properties.rs`; `crates/sl-viewer/tests/properties_viewer.rs`; `crates/sl-viewer/tests/properties_viewer_theme_url.rs`; `crates/sl-viewer/tests/properties_viewer_unfinished_tab.rs`; `crates/sl-viewer/tests/properties_viewer_timeline.rs`; `crates/sl-viewer/tests/properties_viewer_search_memory.rs`; `crates/sl-viewer/tests/properties_viewer_history.rs`; `crates/sl-viewer/tests/properties_viewer_web_exports.rs`; `crates/sl-viewer/tests/properties_viewer_bundle_detail.rs`; `crates/sl-viewer/tests/properties_viewer_bundle_diff.rs`; `crates/sl-viewer/tests/properties_viewer_mock_data.rs`; `crates/sl-viewer/tests/properties_viewer_cli_help.rs`; `crates/sl-viewer/tests/properties_viewer_corpus_cta.rs`; `crates/sl-viewer/tests/properties_viewer_theme.rs`; `crates/sl-viewer/tests/properties_viewer_settings.rs`; `crates/sl-viewer/tests/properties_viewer_corpus_paths.rs`; `fuzz/fuzz_targets/okf_roundtrip.rs`; `fuzz/fuzz_targets/jsonl_ingest.rs`; `.github/workflows/ci.yml`; `.github/workflows/bench-gate.yml`; `docs/ops/perf-baseline.json`; `scripts/bench-gate.ps1`; `benches/pipeline.rs`; `tests/loom_model.rs` | DESIGN P6 residual; C00 L6-L8; C07 L66-L68; C08 L74; perf-budget enforced Wave-26 #223; p95 latency enforced Wave-30 #256; FSM properties Wave-31 #261; soft loom Wave-31 #264; viewer corpus_paths/parquet/settings properties #425; viewer theme + daemon_url properties #427; viewer unfinished_tab properties + fuzz/rootless CI drift fixes #428; viewer bundle_diff + timeline properties + web_exports/hmetic-pin cleanups #432; viewer bundle_diff properties #434; viewer search/memory properties #435; viewer history_tab properties #444; viewer web_exports properties #437; viewer bundle_list + detail_pane properties #436; viewer mock_data fixture properties #451; viewer cli_help / command_palette properties #452; viewer corpus_cta constants properties #453; viewer theme + settings properties #454; viewer corpus_paths round-trip properties #446; full loom/shuttle unpaid |

## audit-v38 waves

Expand Down
Loading