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

- sl-viewer search/memory property surface (WBS-6.2 #435): `crates/sl-viewer/tests/properties_viewer_search_memory.rs` adds 12 proptest properties — `search_view::build_query` trims each field, emits a field iff its post-trim value is non-empty, encodes the documented break-character set (` `, `,`, `#`, `&`, `=`, `+`), and always emits `limit=` whose value is the parsed input or the documented `"50"` fallback. `advanced_filter_active_count` counts `min_tokens`/`tags` non-empty fields, treats `"50"` as the default for `limit`, and is trim-invariant. `memory_tab::to_wiki_page` carries `session_id` and `title` through unchanged and is deterministic across calls; `all_wiki_pages_from_sessions` produces exactly one page per input session, in input order.

- 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.
//!
//! 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!(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

SUGGESTION: Manual temp-directory cleanup is fragile under panic

The four proptest cases use std::env::temp_dir() + fs::remove_dir_all for cleanup. If a prop_assert! panics, the directory is left behind. The existing unit tests in corpus_paths.rs use tempfile::tempdir() for RAII cleanup. Consider switching to tempfile::TempDir here for consistency and automatic cleanup on panic.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

"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 @@ -312,6 +312,7 @@
"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_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`; `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; 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_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 corpus_paths round-trip properties #446; full loom/shuttle unpaid |

## audit-v38 waves

Expand Down
Loading