diff --git a/CHANGELOG.md b/CHANGELOG.md index 220c40ee..beb38f0b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -57,6 +57,8 @@ Follows [Keep a Changelog](https://keepachangelog.com/); versioning is [SemVer]( - sl-viewer menu id taxonomy property surface (WBS-6.2 #457): `crates/sl-viewer/tests/properties_viewer_menu.rs` adds 5 proptest properties — every documented menu id (9 of them: `ID_APP_ABOUT`, `ID_APP_SETTINGS`, `ID_FILE_RELOAD_DISCOVERY`, `ID_FILE_SETTINGS`, `ID_EDIT_FIND`, `ID_VIEW_RELOAD`, `ID_VIEW_TOGGLE_THEME`, `ID_VIEW_COMMAND_PALETTE`, `ID_HELP_TOGGLE`) is non-empty, kebab-case ASCII, carries the `sl-viewer.` prefix, and is unique across the set so a muda event resolves to one DOM action. The menu taxonomy has exactly 9 documented ids so the operator documentation can be re-aligned if it drifts. +- sl-viewer async_states SkeletonLayout property surface (WBS-6.2 #458): `crates/sl-viewer/tests/properties_viewer_async_states.rs` adds 7 proptest properties — `SkeletonLayout::default()` is `Bundles`, exposes exactly three variants (`Bundles`, `ListDetail`, `StreamFeed`), and every variant's `Debug` label is non-empty, single-line, and matches one of the documented names. `list_rows.clamp(3, 6)` lands in `[3, 6]` for every input, is monotonic non-decreasing, and has the documented fixed points (`0` / `2` → `3`, `6` / `usize::MAX` → `6`). + - Commit signing header scan (C04 L34): `commit-signing-check.ps1` reads bounded commit headers via line-scanner (no unbounded `git cat-file` buffers or `(?ms)` regex); `-SelfCheck` + `tests/commit_signing_check.rs`. - Loom permutation CI timeout (P0 stability): split blocking `loom-permutation.yml` into core + per-daemon `loom_model` jobs with `LOOM_MAX_PREEMPTIONS` on broadcast/pipeline/shutdown; mirror in soft `loom-smoke.yml` so Wave-40 tokio-shaped daemon graph tests no longer exceed single-job ceilings. diff --git a/crates/sl-viewer/tests/properties_viewer_async_states.rs b/crates/sl-viewer/tests/properties_viewer_async_states.rs new file mode 100644 index 00000000..812c14a0 --- /dev/null +++ b/crates/sl-viewer/tests/properties_viewer_async_states.rs @@ -0,0 +1,103 @@ +//! Property evidence for sl-viewer's `async_states::SkeletonLayout` +//! enum and the `clamp_rows` helper used by `ContentSkeleton`. +//! +//! `async_states::SkeletonLayout` invariants: +//! * `SkeletonLayout::default()` is `Bundles` so the most common +//! desktop surface (bundle list) is the first paint. +//! * Every variant has a stable, kebab-case-ish single-line label. +//! * The enum exposes exactly three variants so the +//! `match layout { Bundles | ListDetail | StreamFeed }` arms in +//! `ContentSkeleton` stay exhaustive. +//! +//! `list_rows` clamp invariants: +//! * `list_rows.clamp(3, 6)` is deterministic and lands in `[3, 6]` +//! for every input. +//! * The clamp is monotonic non-decreasing on the input range: +//! larger input never produces smaller output. + +use proptest::prelude::*; +use sl_viewer::async_states::SkeletonLayout; + +proptest! { + /// `SkeletonLayout::default()` is `Bundles`. + #[test] + fn skeleton_layout_default_is_bundles(_seed in any::()) { + prop_assert_eq!(SkeletonLayout::default(), SkeletonLayout::Bundles); + } + + /// The enum exposes exactly three variants — the number of + /// documented match arms in `ContentSkeleton`. + #[test] + fn skeleton_layout_has_three_variants(_seed in any::()) { + let variants = [ + SkeletonLayout::Bundles, + SkeletonLayout::ListDetail, + SkeletonLayout::StreamFeed, + ]; + // Round-trip through Debug to confirm each variant's name + // survives stable serialisation. + let mut seen = std::collections::HashSet::new(); + for v in variants { + let name = format!("{v:?}"); + prop_assert!(name.is_ascii(), "variant {name:?} is not ASCII"); + seen.insert(name); + } + prop_assert_eq!(seen.len(), 3, "variant count drifted"); + } + + /// Every variant's Debug label is non-empty, single-line, and + /// matches one of the documented variant names. + #[test] + fn skeleton_layout_labels_documented(variant in prop::sample::select(vec![ + SkeletonLayout::Bundles, + SkeletonLayout::ListDetail, + SkeletonLayout::StreamFeed, + ])) { + let label = format!("{variant:?}"); + prop_assert!(!label.is_empty()); + prop_assert!(!label.contains('\n')); + let valid = label == "Bundles" || label == "ListDetail" || label == "StreamFeed"; + prop_assert!(valid, "label {label:?} is not a documented variant name"); + } + + /// `SkeletonLayout::default()` matches the first arm in the + /// `match` block in `ContentSkeleton` so adding a new variant + /// forces a deliberate `default()` change. + #[test] + fn skeleton_layout_default_is_first_arm(_seed in any::()) { + let first = match () { + () => SkeletonLayout::Bundles, // mirrors the first match arm in ContentSkeleton + }; + prop_assert_eq!(SkeletonLayout::default(), first); + } + + /// `list_rows.clamp(3, 6)` lands in `[3, 6]` for every input. + #[test] + fn list_rows_clamp_in_range(input in any::()) { + let clamped = input.clamp(3, 6); + prop_assert!((3..=6).contains(&clamped), "clamp produced {clamped} for input {input}"); + } + + /// The clamp is monotonic non-decreasing. + #[test] + fn list_rows_clamp_monotonic( + a in any::(), + b in any::(), + ) { + let (lo, hi) = if a <= b { (a, b) } else { (b, a) }; + let c_lo = lo.clamp(3, 6); + let c_hi = hi.clamp(3, 6); + prop_assert!(c_lo <= c_hi, "clamp not monotonic: {lo}→{c_lo}, {hi}→{c_hi}"); + } + + /// The clamp has the documented fixed points: `0` and `2` clamp + /// to `3`; `6` and `u64::MAX` clamp to `6`. + #[test] + fn list_rows_clamp_fixed_points(_seed in any::()) { + prop_assert_eq!(0_usize.clamp(3, 6), 3); + prop_assert_eq!(2_usize.clamp(3, 6), 3); + prop_assert_eq!(3_usize.clamp(3, 6), 3); + prop_assert_eq!(6_usize.clamp(3, 6), 6); + prop_assert_eq!(usize::MAX.clamp(3, 6), 6); + } +} diff --git a/docs/ops/TRACEABILITY.json b/docs/ops/TRACEABILITY.json index 9ec60536..8ca77f38 100644 --- a/docs/ops/TRACEABILITY.json +++ b/docs/ops/TRACEABILITY.json @@ -325,6 +325,7 @@ "crates/sl-viewer/tests/properties_viewer_help_overlay.rs", "crates/sl-viewer/tests/properties_viewer_settings_tab.rs", "crates/sl-viewer/tests/properties_viewer_menu.rs", + "crates/sl-viewer/tests/properties_viewer_async_states.rs", "fuzz/fuzz_targets/okf_roundtrip.rs", "fuzz/fuzz_targets/jsonl_ingest.rs", ".github/workflows/ci.yml", diff --git a/docs/ops/WBS.md b/docs/ops/WBS.md index b032d0e6..9144a96c 100644 --- a/docs/ops/WBS.md +++ b/docs/ops/WBS.md @@ -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`; `crates/sl-viewer/tests/properties_viewer_corpus_paths.rs`; `crates/sl-viewer/tests/properties_viewer_help_overlay.rs`; `crates/sl-viewer/tests/properties_viewer_settings_tab.rs`; `crates/sl-viewer/tests/properties_viewer_menu.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; viewer help_overlay shortcuts properties #455; viewer settings_tab HealthStatus properties #456; viewer menu id taxonomy properties #457; 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`; `crates/sl-viewer/tests/properties_viewer_help_overlay.rs`; `crates/sl-viewer/tests/properties_viewer_settings_tab.rs`; `crates/sl-viewer/tests/properties_viewer_menu.rs`; `crates/sl-viewer/tests/properties_viewer_async_states.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; viewer help_overlay shortcuts properties #455; viewer settings_tab HealthStatus properties #456; viewer menu id taxonomy properties #457; viewer async_states SkeletonLayout properties #458; full loom/shuttle unpaid | ## audit-v38 waves