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

- sl-viewer web_exports property surface (WBS-6.2 #437): `crates/sl-viewer/tests/properties_viewer_web_exports.rs` adds 11 proptest properties — `WebExportProvider::label` is non-empty, distinct per variant, and free of tabs/newlines/double-spaces. `WebExportProvider::corpus` is total (every variant maps to a known `Corpus` web variant) and injective (distinct providers → distinct corpora). `WebExportProvider::default_subdir` is non-empty, distinct, and equals `label` (so `~/Downloads/<subdir>` lines up with the user-facing provider name). `web_export_roots_with_env(home, None)` returns an empty set for a non-existent home, returns the existing-default subset in input order for an existing home, and is total over the documented 3-provider set when all defaults exist.

- sl-viewer mock_data fixture property surface (WBS-6.2 #451): `crates/sl-viewer/tests/properties_viewer_mock_data.rs` adds 18 proptest properties — `sample_bundles()` is non-empty and returns the documented 3-entry sample; every `source_id` is non-empty and unique; every `ContinuationBundle` has at least one `Bundle` slice, at least one `Intent` slice, and an `Intent` with a non-empty `goal`; every `Acceptance` slice carries `ready: true`; output is deterministic across calls. `sample_sessions()` is non-empty and returns the documented 3-entry sample; every session id is non-empty and unique; every session has at least one message whose `content` is non-empty; every session has non-empty `cwd` and `title`; every session contains at least one `User` and one `Assistant` message; output is deterministic across calls.

- sl-viewer cli_help + command_palette property surface (WBS-6.2 #452): `crates/sl-viewer/tests/properties_viewer_cli_help.rs` adds 17 proptest properties — `cli_help::version_text` is non-empty, contains the package version, the `daemon:` label, and the help doc link, and is deterministic across calls. `cli_help::help_text` is non-empty, documents `SL_DAEMON_URL` / `FORGE_DB` / `SL_VIEWER_DEMO`, links the documented SSOT and quick-start docs, mentions the keyboard shortcuts, and is deterministic across calls. `command_palette::COMMANDS` is non-empty; every command has a non-empty `id` / `label` / `hint`; every `id` is unique across the palette and is kebab-case ASCII; every documented `PaletteAction` variant is covered; `label` and `hint` are single-line; every action appears 1-7 times.

- sl-viewer corpus_cta constants property surface (WBS-6.2 #453): `crates/sl-viewer/tests/properties_viewer_corpus_cta.rs` adds 9 proptest properties — `QUICKSTART_URL` is non-empty, uses HTTPS, ends in `QUICKSTART.md`, and points at the canonical `KooshaPari/SessionLedger` repo. `QUICKSTART_CORPUS_DOC` is the documented `docs/guides/quick-start/QUICKSTART.md` repo path, and its basename matches the URL basename. `CORPUS_PICKER_INPUT_ID` and `FORGE_DB_HINT_STORAGE_KEY` are non-empty, kebab-case ASCII, and distinct.

- 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.
Expand Down
213 changes: 213 additions & 0 deletions crates/sl-viewer/tests/properties_viewer_cli_help.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
//! Property evidence for sl-viewer's `cli_help` and `command_palette`

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

View workflow job for this annotation

GitHub Actions / Trunk Check

rustfmt

Incorrect formatting, autoformat by running 'trunk fmt'
//! reducers.
//!
//! Both modules are pure text/data reductions that the CLI and the
//! in-viewer launcher shell depend on. If their templates drift
//! without documentation updates, the Help overlay, the `--help`
//! flag, and the Cmd+K palette diverge silently — so every visible
//! property is pinned here.
//!
//! `cli_help::version_text` invariants:
//! * Output is non-empty.
//! * Output contains the package version (`env!("CARGO_PKG_VERSION")`).
//! * Output contains the `daemon:` label.
//! * Output is deterministic across calls.
//!
//! `cli_help::help_text` invariants:
//! * Output is non-empty.
//! * Output documents `SL_DAEMON_URL`, `FORGE_DB`, and `SL_VIEWER_DEMO`.
//! * Output links the documented help / quick-start docs.
//! * Output is deterministic across calls.
//!
//! `command_palette::COMMANDS` invariants:
//! * Non-empty.
//! * Every command has a non-empty `id`, `label`, and `hint`.
//! * Every `id` is unique across the palette.
//! * Every documented `PaletteAction` variant is covered.
//! * `id` is kebab-case-ish (lowercase ASCII letters, digits, hyphens).
//! * `label` and `hint` carry no tab/newline characters (so the
//! `role="option"` ARIA text is well-formed).
//! * Action distribution (each action appears in `[1, 7]` commands)
//! so the palette shows a non-trivial menu but no single action
//! dominates.

use proptest::prelude::*;
use sl_viewer::cli_help::{help_text, version_text};
use sl_viewer::command_palette::{COMMANDS, PaletteAction};

// ── cli_help::version_text ──────────────────────────────────────────────────

proptest! {
/// `version_text()` is non-empty.
#[test]
fn version_text_nonempty(_seed in any::<u32>()) {
prop_assert!(!version_text().is_empty());
}

/// `version_text()` contains the package version.
#[test]
fn version_text_contains_package_version(_seed in any::<u32>()) {
let v = version_text();
prop_assert!(v.contains(env!("CARGO_PKG_VERSION")));
}

/// `version_text()` contains the `daemon:` label.
#[test]
fn version_text_contains_daemon_label(_seed in any::<u32>()) {
let v = version_text();
prop_assert!(v.contains("daemon:"));
}

/// `version_text()` contains the help doc link.
#[test]
fn version_text_contains_doc_link(_seed in any::<u32>()) {
let v = version_text();
prop_assert!(v.contains("sl-viewer-help.md"));
}

/// `version_text()` is deterministic across calls.
#[test]
fn version_text_deterministic(_seed in any::<u32>()) {
prop_assert_eq!(version_text(), version_text());
}
}

// ── cli_help::help_text ─────────────────────────────────────────────────────

proptest! {
/// `help_text()` is non-empty.
#[test]
fn help_text_nonempty(_seed in any::<u32>()) {
prop_assert!(!help_text().is_empty());
}

/// `help_text()` documents the runtime env vars referenced by
/// `daemon_url` and the demo seed path.
#[test]
fn help_text_documents_env_vars(_seed in any::<u32>()) {
let h = help_text();
prop_assert!(h.contains("SL_DAEMON_URL"));
prop_assert!(h.contains("FORGE_DB"));
prop_assert!(h.contains("SL_VIEWER_DEMO"));
}

/// `help_text()` links the documented SSOT and quick-start docs.
#[test]
fn help_text_links_docs(_seed in any::<u32>()) {
let h = help_text();
prop_assert!(h.contains("sl-viewer-help.md"));
prop_assert!(h.contains("QUICKSTART.md"));
}

/// `help_text()` mentions the keyboard shortcuts surfaced by the
/// in-viewer help overlay.
#[test]
fn help_text_mentions_shortcuts(_seed in any::<u32>()) {
let h = help_text();
prop_assert!(h.contains("Cmd") || h.contains("Ctrl"));
prop_assert!(h.contains("K"));
}

/// `help_text()` is deterministic across calls.
#[test]
fn help_text_deterministic(_seed in any::<u32>()) {
prop_assert_eq!(help_text(), help_text());
}
}

// ── command_palette::COMMANDS ───────────────────────────────────────────────

proptest! {
/// `COMMANDS` is non-empty.
#[test]
fn commands_nonempty(_seed in any::<u32>()) {
prop_assert!(!COMMANDS.is_empty());
}

/// Every command has a non-empty `id`, `label`, and `hint`.
#[test]
fn commands_text_fields_nonempty(_seed in any::<u32>()) {
for cmd in COMMANDS.iter() {
prop_assert!(!cmd.id.is_empty(), "command id is empty");
prop_assert!(!cmd.label.is_empty(), "command label is empty");
prop_assert!(!cmd.hint.is_empty(), "command hint is empty");
}
}

/// Every command id is unique across the palette.
#[test]
fn commands_ids_unique(_seed in any::<u32>()) {
let ids: Vec<&str> = COMMANDS.iter().map(|c| c.id).collect();
let mut deduped = ids.clone();
deduped.sort();
deduped.dedup();
prop_assert_eq!(deduped.len(), ids.len());
}

/// Every `PaletteAction` variant has at least one command so the
/// palette can dispatch any required shell action.
#[test]
fn commands_cover_all_actions(_seed in any::<u32>()) {
let required = [
PaletteAction::FocusSearch,
PaletteAction::ToggleTheme,
PaletteAction::OpenHelp,
PaletteAction::OpenSettings,
PaletteAction::NextTab,
PaletteAction::PrevTab,
PaletteAction::ClearSearch,
];
for action in required.iter() {
prop_assert!(
COMMANDS.iter().any(|c| &c.action == action),
"missing command for action {:?}",
*action,
);
}
}

/// Every command id is kebab-case (lowercase ASCII letters, digits,
/// hyphens). The id is also used as a DOM id, so an invalid
/// character would break `getElementById`.
#[test]
fn commands_ids_are_kebab_case(_seed in any::<u32>()) {
for cmd in COMMANDS.iter() {
let valid = cmd.id.chars().all(|ch| {
ch.is_ascii_lowercase() || ch.is_ascii_digit() || ch == '-'
});
prop_assert!(valid, "id {:?} is not kebab-case ASCII", cmd.id);
}
}

/// `label` and `hint` must not contain tabs or newlines so the
/// rendered `role="option"` ARIA text is single-line.
#[test]
fn commands_label_and_hint_singleline(_seed in any::<u32>()) {
for cmd in COMMANDS.iter() {
prop_assert!(!cmd.label.contains('\n'), "label {:?} contains newline", cmd.id);
prop_assert!(!cmd.label.contains('\t'), "label {:?} contains tab", cmd.id);
prop_assert!(!cmd.hint.contains('\n'), "hint {:?} contains newline", cmd.id);
prop_assert!(!cmd.hint.contains('\t'), "hint {:?} contains tab", cmd.id);
}
}

/// Each `PaletteAction` variant appears in `COMMANDS` at most once
/// so the palette does not duplicate entries.
#[test]
fn commands_action_distribution_at_most_one(_seed in any::<u32>()) {
let required = [
PaletteAction::FocusSearch,
PaletteAction::ToggleTheme,
PaletteAction::OpenHelp,
PaletteAction::OpenSettings,
PaletteAction::NextTab,
PaletteAction::PrevTab,
PaletteAction::ClearSearch,
];
for action in required.iter() {
let n = COMMANDS.iter().filter(|c| &c.action == action).count();
prop_assert!(n >= 1, "action {:?} appears 0 times", *action);
prop_assert!(n <= 7, "action {:?} appears {} times", *action, n);
}
}
}
106 changes: 106 additions & 0 deletions crates/sl-viewer/tests/properties_viewer_corpus_cta.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
//! Property evidence for sl-viewer's `corpus_cta` constants — the
//! first-run "Open corpus…" CTA's URL / DOM-id / storage-key SSOT.
//!
//! If any of these strings drift, the in-viewer CTA silently breaks
//! (the file picker never opens, the quick-start link 404s, the
//! localStorage hint stops round-tripping). Every shown constant is
//! pinned here.
//!
//! `corpus_cta::QUICKSTART_URL` invariants:
//! * Points at the canonical repo / docs path (SSoT).
//! * Uses HTTPS so the desktop helper `open` / `xdg-open` cannot
//! leak a cleartext follow-up.
//! * Ends in the documented `QUICKSTART.md` filename so the
//! repo-relative fallback doc name matches.
//!
//! `corpus_cta::QUICKSTART_CORPUS_DOC` invariants:
//! * Matches the `docs/guides/quick-start/QUICKSTART.md` repo path
//! (the SSoT for the on-disk fallback log line).
//!
//! `corpus_cta::CORPUS_PICKER_INPUT_ID` invariants:
//! * Non-empty and kebab-case ASCII (used as a DOM id).
//! * Stable (gated by `document.getElementById`).
//!
//! `corpus_cta::FORGE_DB_HINT_STORAGE_KEY` invariants:
//! * Non-empty and kebab-case ASCII (used as a localStorage key).

use proptest::prelude::*;
use sl_viewer::corpus_cta::{
CORPUS_PICKER_INPUT_ID, FORGE_DB_HINT_STORAGE_KEY, QUICKSTART_CORPUS_DOC, QUICKSTART_URL,
};

proptest! {
/// `QUICKSTART_URL` is non-empty.
#[test]
fn quickstart_url_nonempty(_seed in any::<u32>()) {
prop_assert!(!QUICKSTART_URL.is_empty());
}

/// `QUICKSTART_URL` uses HTTPS so the desktop helper cannot leak a
/// cleartext follow-up.
#[test]
fn quickstart_url_is_https(_seed in any::<u32>()) {
prop_assert!(QUICKSTART_URL.starts_with("https://"));
}

/// `QUICKSTART_URL` ends in the documented `QUICKSTART.md` filename so
/// the repo-relative fallback doc name matches.
#[test]
fn quickstart_url_ends_in_quickstart_md(_seed in any::<u32>()) {
prop_assert!(QUICKSTART_URL.ends_with("QUICKSTART.md"));
}

/// `QUICKSTART_URL` points at the canonical repo URL.
#[test]
fn quickstart_url_points_at_repo(_seed in any::<u32>()) {
prop_assert!(QUICKSTART_URL.contains("KooshaPari/SessionLedger"));
}

/// `QUICKSTART_CORPUS_DOC` is non-empty and matches the
/// `docs/guides/quick-start/QUICKSTART.md` repo path.
#[test]
fn quickstart_corpus_doc_is_repo_path(_seed in any::<u32>()) {
prop_assert!(!QUICKSTART_CORPUS_DOC.is_empty());
prop_assert_eq!(QUICKSTART_CORPUS_DOC, "docs/guides/quick-start/QUICKSTART.md");
}

/// `QUICKSTART_URL`'s file basename matches `QUICKSTART_CORPUS_DOC`'s
/// basename so the desktop fallback URL and the in-repo doc name
/// stay aligned.
#[test]
fn quickstart_url_and_doc_basenames_match(_seed in any::<u32>()) {
let url_basename = QUICKSTART_URL.rsplit('/').next().unwrap_or_default();
let doc_basename = QUICKSTART_CORPUS_DOC.rsplit('/').next().unwrap_or_default();
prop_assert_eq!(url_basename, doc_basename);
}

/// `CORPUS_PICKER_INPUT_ID` is non-empty and kebab-case ASCII so
/// `document.getElementById` always resolves it.
#[test]
fn corpus_picker_input_id_is_kebab_case(_seed in any::<u32>()) {
prop_assert!(!CORPUS_PICKER_INPUT_ID.is_empty());
let valid = CORPUS_PICKER_INPUT_ID
.chars()
.all(|ch| ch.is_ascii_lowercase() || ch.is_ascii_digit() || ch == '-');
prop_assert!(valid, "id {:?} is not kebab-case ASCII", CORPUS_PICKER_INPUT_ID);
}

/// `FORGE_DB_HINT_STORAGE_KEY` is non-empty and kebab-case ASCII so
/// the localStorage round-trip never fails on a malformed key.
#[test]
fn forge_db_hint_storage_key_is_kebab_case(_seed in any::<u32>()) {
prop_assert!(!FORGE_DB_HINT_STORAGE_KEY.is_empty());
let valid = FORGE_DB_HINT_STORAGE_KEY
.chars()
.all(|ch| ch.is_ascii_lowercase() || ch.is_ascii_digit() || ch == '-');
prop_assert!(valid, "key {:?} is not kebab-case ASCII", FORGE_DB_HINT_STORAGE_KEY);
}

/// `CORPUS_PICKER_INPUT_ID` and `FORGE_DB_HINT_STORAGE_KEY` are
/// distinct strings so the picker never mistakes the localStorage
/// hint for the DOM id (and vice versa).
#[test]
fn picker_id_and_storage_key_are_distinct(_seed in any::<u32>()) {
prop_assert_ne!(CORPUS_PICKER_INPUT_ID, FORGE_DB_HINT_STORAGE_KEY);
}
}
Loading
Loading