From f7d03f2dd62b4629d28ee038cbd9116d7162f198 Mon Sep 17 00:00:00 2001 From: Ralf Anton Beier Date: Mon, 9 Mar 2026 07:43:20 +0100 Subject: [PATCH 1/2] docs: add phase 2.5 features, roadmap, and CLI/schema tests Co-Authored-By: Claude Opus 4.6 --- Cargo.lock | 1 + artifacts/features.yaml | 129 ++++++++ docs/roadmap.md | 26 ++ rivet-cli/Cargo.toml | 4 + rivet-cli/tests/cli_commands.rs | 506 ++++++++++++++++++++++++++++++++ rivet-core/tests/docs_schema.rs | 209 +++++++++++++ 6 files changed, 875 insertions(+) create mode 100644 rivet-cli/tests/cli_commands.rs create mode 100644 rivet-core/tests/docs_schema.rs diff --git a/Cargo.lock b/Cargo.lock index b815d5b..9dfb9a5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2359,6 +2359,7 @@ dependencies = [ "serde", "serde_json", "serde_yaml", + "tempfile", "tokio", "tower-http", "urlencoding", diff --git a/artifacts/features.yaml b/artifacts/features.yaml index 54994fd..36d7c1e 100644 --- a/artifacts/features.yaml +++ b/artifacts/features.yaml @@ -312,3 +312,132 @@ artifacts: target: REQ-007 fields: phase: phase-3 + + - id: FEAT-021 + type: feature + title: Embedded schemas + status: approved + description: > + Schemas compiled into the binary via include_str! with fallback + loading when the schemas/ directory is not found on disk. Enables + zero-configuration operation without requiring schema files to be + shipped alongside the binary. + tags: [schema, distribution, phase-2] + links: + - type: satisfies + target: REQ-010 + fields: + phase: phase-2 + + - id: FEAT-022 + type: feature + title: "rivet docs command" + status: approved + description: > + Built-in searchable documentation system with topic-based browsing, + grep-style search across all docs, and --format json output for + machine consumption. Provides offline-first help without external + documentation sites. + tags: [cli, docs, phase-2] + links: + - type: satisfies + target: REQ-007 + fields: + phase: phase-2 + + - id: FEAT-023 + type: feature + title: "rivet schema command" + status: approved + description: > + Schema introspection commands (list, show, links, rules) that let + users explore loaded schemas from the CLI. Supports --format json + for agent and tooling integration. + tags: [cli, schema, phase-2] + links: + - type: satisfies + target: REQ-007 + - type: satisfies + target: REQ-010 + fields: + phase: phase-2 + + - id: FEAT-024 + type: feature + title: "rivet context command" + status: approved + description: > + Generates .rivet/agent-context.md containing project state, coverage + summaries, active validation rules, and example artifact IDs. Designed + to give AI coding agents the context they need to work effectively + with rivet-managed projects. + tags: [cli, agent, phase-2] + links: + - type: satisfies + target: REQ-007 + fields: + phase: phase-2 + + - id: FEAT-025 + type: feature + title: "--format json on all commands" + status: approved + description: > + Machine-readable JSON output envelope available on all CLI commands + (validate, list, stats, matrix, diff, coverage, schema, docs). + Enables agents and CI pipelines to consume rivet output + programmatically. + tags: [cli, agent, phase-2] + links: + - type: satisfies + target: REQ-007 + fields: + phase: phase-2 + + - id: FEAT-026 + type: feature + title: "rivet init --preset command" + status: approved + description: > + Schema-aware project scaffolding with presets (dev, aspice, stpa, + cybersecurity, aadl). Generates rivet.yaml, directories, and example + artifacts matching the chosen domain schema. + tags: [cli, schema, phase-2] + links: + - type: satisfies + target: REQ-007 + - type: satisfies + target: REQ-010 + fields: + phase: phase-2 + + - id: FEAT-027 + type: feature + title: Syntax highlighting in dashboard + status: approved + description: > + Server-side YAML and bash syntax highlighting in the source viewer + and documentation pages. Rendered as annotated HTML spans without + requiring client-side JavaScript highlighting libraries. + tags: [ui, dashboard, phase-2] + links: + - type: satisfies + target: REQ-007 + fields: + phase: phase-2 + + - id: FEAT-028 + type: feature + title: Help and Docs dashboard section + status: approved + description: > + Dashboard section providing a schema browser, link type reference, + validation rules summary, and doc topic viewer. Gives users a + graphical interface for exploring schemas and documentation + alongside their artifacts. + tags: [ui, dashboard, docs, phase-2] + links: + - type: satisfies + target: REQ-007 + fields: + phase: phase-2 diff --git a/docs/roadmap.md b/docs/roadmap.md index 13f4db6..3c0f9d2 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -75,6 +75,32 @@ The `arch/` directory contains AADL models for rivet's own architecture: (extensible adapter subsystem + WASM runtime), and `RivetDashboard` (axum/HTMX serve handler with view renderers and graph visualizer). +## Phase 2.5 — Documentation & Agent Support (Complete) + +Phase 2.5 added built-in documentation, schema introspection, agent context +generation, and machine-readable output — making rivet self-describing and +consumable by both humans and AI agents. + +### Embedded Schemas & Scaffolding + +- [[FEAT-021]] — Schemas compiled into binary (include_str! with disk fallback) +- [[FEAT-026]] — `rivet init --preset` for schema-aware project scaffolding + +### CLI Introspection Commands + +- [[FEAT-022]] — `rivet docs` with topic browsing, grep search, --format json +- [[FEAT-023]] — `rivet schema` introspection (list, show, links, rules) +- [[FEAT-024]] — `rivet context` generates .rivet/agent-context.md for AI agents + +### Machine-Readable Output + +- [[FEAT-025]] — `--format json` envelope on all CLI commands + +### Dashboard Enhancements + +- [[FEAT-027]] — Server-side syntax highlighting in source viewer and docs +- [[FEAT-028]] — Help & Docs dashboard section (schema browser, link types, rules) + ## Phase 3 — Sync & Extensibility (Planned) Phase 3 enables bidirectional synchronization with external ALM tools and diff --git a/rivet-cli/Cargo.toml b/rivet-cli/Cargo.toml index 357f919..a272dea 100644 --- a/rivet-cli/Cargo.toml +++ b/rivet-cli/Cargo.toml @@ -30,3 +30,7 @@ tower-http = { workspace = true } etch = { path = "../etch" } petgraph = { workspace = true } urlencoding = { workspace = true } + +[dev-dependencies] +serde_json = { workspace = true } +tempfile = "3" diff --git a/rivet-cli/tests/cli_commands.rs b/rivet-cli/tests/cli_commands.rs new file mode 100644 index 0000000..8e4a886 --- /dev/null +++ b/rivet-cli/tests/cli_commands.rs @@ -0,0 +1,506 @@ +//! CLI integration tests — exercise the `rivet` binary end-to-end. +//! +//! Uses `std::process::Command` to invoke the built binary and verify +//! stdout/stderr content and exit codes. + +use std::process::Command; + +/// Locate the `rivet` binary built by cargo. +fn rivet_bin() -> std::path::PathBuf { + // `cargo test` sets CARGO_BIN_EXE_rivet` when the binary is declared + // in Cargo.toml. Fall back to constructing the path manually. + if let Ok(bin) = std::env::var("CARGO_BIN_EXE_rivet") { + return std::path::PathBuf::from(bin); + } + // Construct path from CARGO_MANIFEST_DIR -> workspace target directory + let manifest = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")); + let workspace_root = manifest.parent().expect("workspace root"); + let target_dir = workspace_root.join("target").join("debug").join("rivet"); + target_dir +} + +/// Project root (one level up from rivet-cli/). +fn project_root() -> std::path::PathBuf { + std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .parent() + .expect("workspace root") + .to_path_buf() +} + +// ── rivet docs ────────────────────────────────────────────────────────── + +/// `rivet docs` (no args) lists all available topics. +#[test] +fn docs_list_topics() { + let output = Command::new(rivet_bin()) + .args(["docs"]) + .output() + .expect("failed to execute rivet docs"); + + assert!(output.status.success(), "rivet docs must exit 0"); + + let stdout = String::from_utf8_lossy(&output.stdout); + assert!( + stdout.contains("artifact-format"), + "topic list must include 'artifact-format', got:\n{stdout}" + ); + assert!( + stdout.contains("rivet-yaml"), + "topic list must include 'rivet-yaml', got:\n{stdout}" + ); +} + +/// `rivet docs artifact-format` shows the topic content. +#[test] +fn docs_show_topic() { + let output = Command::new(rivet_bin()) + .args(["docs", "artifact-format"]) + .output() + .expect("failed to execute rivet docs artifact-format"); + + assert!(output.status.success(), "rivet docs artifact-format must exit 0"); + + let stdout = String::from_utf8_lossy(&output.stdout); + assert!( + stdout.contains("Artifact YAML Format"), + "topic content must include 'Artifact YAML Format', got:\n{stdout}" + ); +} + +/// `rivet docs --grep verification` finds matches across documentation. +#[test] +fn docs_grep_finds_matches() { + let output = Command::new(rivet_bin()) + .args(["docs", "--grep", "verification"]) + .output() + .expect("failed to execute rivet docs --grep"); + + assert!(output.status.success(), "rivet docs --grep must exit 0"); + + let stdout = String::from_utf8_lossy(&output.stdout); + assert!( + stdout.contains("verification") || stdout.contains("Verification"), + "grep output must contain 'verification', got:\n{stdout}" + ); + // Should show match counts or individual matches + assert!( + stdout.contains("match"), + "grep output must mention matches, got:\n{stdout}" + ); +} + +/// `rivet docs --format json` produces valid JSON output. +#[test] +fn docs_list_json() { + let output = Command::new(rivet_bin()) + .args(["docs", "--format", "json"]) + .output() + .expect("failed to execute rivet docs --format json"); + + assert!(output.status.success(), "rivet docs --format json must exit 0"); + + let stdout = String::from_utf8_lossy(&output.stdout); + let parsed: serde_json::Value = + serde_json::from_str(&stdout).expect("docs list JSON must be valid"); + + assert_eq!( + parsed.get("command").and_then(|v| v.as_str()), + Some("docs-list"), + "JSON envelope must have command 'docs-list'" + ); + assert!( + parsed.get("topics").and_then(|v| v.as_array()).is_some(), + "JSON must contain a 'topics' array" + ); +} + +/// `rivet docs --grep verification --format json` produces valid JSON with matches. +#[test] +fn docs_grep_json() { + let output = Command::new(rivet_bin()) + .args(["docs", "--grep", "verification", "--format", "json"]) + .output() + .expect("failed to execute rivet docs --grep --format json"); + + assert!(output.status.success()); + + let stdout = String::from_utf8_lossy(&output.stdout); + let parsed: serde_json::Value = + serde_json::from_str(&stdout).expect("grep JSON must be valid"); + + assert_eq!( + parsed.get("command").and_then(|v| v.as_str()), + Some("docs-grep"), + ); + assert!( + parsed + .get("match_count") + .and_then(|v| v.as_u64()) + .unwrap_or(0) + > 0, + "grep must find at least one match for 'verification'" + ); +} + +// ── rivet schema ──────────────────────────────────────────────────────── + +/// `rivet schema list` (run against the project) lists artifact types. +#[test] +fn schema_list() { + let output = Command::new(rivet_bin()) + .args(["--project", project_root().to_str().unwrap(), "schema", "list"]) + .output() + .expect("failed to execute rivet schema list"); + + assert!( + output.status.success(), + "rivet schema list must exit 0. stderr: {}", + String::from_utf8_lossy(&output.stderr) + ); + + let stdout = String::from_utf8_lossy(&output.stdout); + assert!( + stdout.contains("Artifact types"), + "schema list must contain 'Artifact types', got:\n{stdout}" + ); +} + +/// `rivet schema list --format json` produces valid JSON with artifact_types. +#[test] +fn schema_list_json() { + let output = Command::new(rivet_bin()) + .args([ + "--project", + project_root().to_str().unwrap(), + "schema", + "list", + "--format", + "json", + ]) + .output() + .expect("failed to execute rivet schema list --format json"); + + assert!( + output.status.success(), + "rivet schema list --format json must exit 0. stderr: {}", + String::from_utf8_lossy(&output.stderr) + ); + + let stdout = String::from_utf8_lossy(&output.stdout); + let parsed: serde_json::Value = + serde_json::from_str(&stdout).expect("schema list JSON must be valid"); + + assert_eq!( + parsed.get("command").and_then(|v| v.as_str()), + Some("schema-list"), + ); + assert!( + parsed + .get("artifact_types") + .and_then(|v| v.as_array()) + .is_some(), + "JSON must contain 'artifact_types' array" + ); + assert!( + parsed.get("count").and_then(|v| v.as_u64()).unwrap_or(0) > 0, + "schema list must report at least one type" + ); +} + +// ── rivet init ────────────────────────────────────────────────────────── + +/// `rivet init --preset stpa` creates rivet.yaml and artifacts in a temp dir. +#[test] +fn init_stpa_preset() { + let tmp = tempfile::tempdir().expect("create temp dir"); + let dir = tmp.path(); + + let output = Command::new(rivet_bin()) + .args(["init", "--preset", "stpa", "--dir", dir.to_str().unwrap()]) + .output() + .expect("failed to execute rivet init"); + + assert!( + output.status.success(), + "rivet init --preset stpa must exit 0. stderr: {}", + String::from_utf8_lossy(&output.stderr) + ); + + // rivet.yaml must exist + let config_path = dir.join("rivet.yaml"); + assert!(config_path.exists(), "rivet.yaml must be created"); + + // Read and verify config content + let config_content = + std::fs::read_to_string(&config_path).expect("read rivet.yaml"); + assert!( + config_content.contains("stpa"), + "rivet.yaml must reference 'stpa' schema, got:\n{config_content}" + ); + assert!( + config_content.contains("common"), + "rivet.yaml must reference 'common' schema" + ); + + // artifacts/ directory must exist with sample file + let artifacts_dir = dir.join("artifacts"); + assert!( + artifacts_dir.exists(), + "artifacts/ directory must be created" + ); + + // Should have a safety.yaml sample file (STPA preset creates safety.yaml) + let safety_path = artifacts_dir.join("safety.yaml"); + assert!( + safety_path.exists(), + "artifacts/safety.yaml must be created for stpa preset" + ); + + // docs/ directory should exist + let docs_dir = dir.join("docs"); + assert!(docs_dir.exists(), "docs/ directory must be created"); +} + +/// `rivet init` with default preset creates a dev project. +#[test] +fn init_dev_preset() { + let tmp = tempfile::tempdir().expect("create temp dir"); + let dir = tmp.path(); + + let output = Command::new(rivet_bin()) + .args(["init", "--dir", dir.to_str().unwrap()]) + .output() + .expect("failed to execute rivet init"); + + assert!( + output.status.success(), + "rivet init must exit 0. stderr: {}", + String::from_utf8_lossy(&output.stderr) + ); + + let config_path = dir.join("rivet.yaml"); + assert!(config_path.exists(), "rivet.yaml must be created"); + + let config_content = std::fs::read_to_string(&config_path).expect("read rivet.yaml"); + assert!( + config_content.contains("dev"), + "default rivet.yaml must reference 'dev' schema" + ); + + // Should have requirements.yaml sample + let req_path = dir.join("artifacts").join("requirements.yaml"); + assert!( + req_path.exists(), + "artifacts/requirements.yaml must be created for dev preset" + ); +} + +// ── rivet validate ────────────────────────────────────────────────────── + +/// `rivet validate --format json` produces valid JSON with "command":"validate". +#[test] +fn validate_json() { + let output = Command::new(rivet_bin()) + .args([ + "--project", + project_root().to_str().unwrap(), + "validate", + "--format", + "json", + ]) + .output() + .expect("failed to execute rivet validate --format json"); + + // validate may exit non-zero if there are errors, but JSON output should + // still be valid. + let stdout = String::from_utf8_lossy(&output.stdout); + let parsed: serde_json::Value = + serde_json::from_str(&stdout).expect("validate JSON must be valid"); + + assert_eq!( + parsed.get("command").and_then(|v| v.as_str()), + Some("validate"), + "JSON envelope must have command 'validate'" + ); + assert!( + parsed.get("diagnostics").and_then(|v| v.as_array()).is_some(), + "JSON must contain a 'diagnostics' array" + ); + // errors/warnings fields should be present + assert!( + parsed.get("errors").is_some(), + "JSON must contain 'errors' count" + ); + assert!( + parsed.get("warnings").is_some(), + "JSON must contain 'warnings' count" + ); +} + +// ── rivet stats ───────────────────────────────────────────────────────── + +/// `rivet stats --format json` produces valid JSON with total count. +#[test] +fn stats_json() { + let output = Command::new(rivet_bin()) + .args([ + "--project", + project_root().to_str().unwrap(), + "stats", + "--format", + "json", + ]) + .output() + .expect("failed to execute rivet stats --format json"); + + assert!( + output.status.success(), + "rivet stats --format json must exit 0. stderr: {}", + String::from_utf8_lossy(&output.stderr) + ); + + let stdout = String::from_utf8_lossy(&output.stdout); + let parsed: serde_json::Value = + serde_json::from_str(&stdout).expect("stats JSON must be valid"); + + assert_eq!( + parsed.get("command").and_then(|v| v.as_str()), + Some("stats"), + ); + assert!( + parsed.get("total").and_then(|v| v.as_u64()).unwrap_or(0) > 0, + "stats must report at least one artifact" + ); + assert!( + parsed.get("types").is_some(), + "stats JSON must contain 'types' breakdown" + ); +} + +// ── rivet list ────────────────────────────────────────────────────────── + +/// `rivet list --format json` produces valid JSON with artifacts array. +#[test] +fn list_json() { + let output = Command::new(rivet_bin()) + .args([ + "--project", + project_root().to_str().unwrap(), + "list", + "--format", + "json", + ]) + .output() + .expect("failed to execute rivet list --format json"); + + assert!( + output.status.success(), + "rivet list --format json must exit 0. stderr: {}", + String::from_utf8_lossy(&output.stderr) + ); + + let stdout = String::from_utf8_lossy(&output.stdout); + let parsed: serde_json::Value = + serde_json::from_str(&stdout).expect("list JSON must be valid"); + + assert_eq!( + parsed.get("command").and_then(|v| v.as_str()), + Some("list"), + ); + assert!( + parsed + .get("artifacts") + .and_then(|v| v.as_array()) + .is_some(), + "list JSON must contain 'artifacts' array" + ); + assert!( + parsed.get("count").and_then(|v| v.as_u64()).unwrap_or(0) > 0, + "list must report at least one artifact" + ); +} + +/// `rivet list --format json` artifacts have expected fields. +#[test] +fn list_json_artifact_fields() { + let output = Command::new(rivet_bin()) + .args([ + "--project", + project_root().to_str().unwrap(), + "list", + "--format", + "json", + ]) + .output() + .expect("failed to execute rivet list --format json"); + + assert!(output.status.success()); + + let stdout = String::from_utf8_lossy(&output.stdout); + let parsed: serde_json::Value = serde_json::from_str(&stdout).expect("valid JSON"); + + let artifacts = parsed + .get("artifacts") + .and_then(|v| v.as_array()) + .expect("artifacts array"); + assert!(!artifacts.is_empty(), "must have at least one artifact"); + + // Every artifact should have id, type, title + for artifact in artifacts { + assert!( + artifact.get("id").and_then(|v| v.as_str()).is_some(), + "artifact must have 'id'" + ); + assert!( + artifact.get("type").and_then(|v| v.as_str()).is_some(), + "artifact must have 'type'" + ); + assert!( + artifact.get("title").and_then(|v| v.as_str()).is_some(), + "artifact must have 'title'" + ); + } +} + +// ── rivet init then validate roundtrip ────────────────────────────────── + +/// Initialize a project, then validate it — the sample artifacts should pass. +#[test] +fn init_then_validate_roundtrip() { + let tmp = tempfile::tempdir().expect("create temp dir"); + let dir = tmp.path(); + + // Init + let init_out = Command::new(rivet_bin()) + .args(["init", "--preset", "dev", "--dir", dir.to_str().unwrap()]) + .output() + .expect("failed to execute rivet init"); + assert!(init_out.status.success(), "init must succeed"); + + // Validate the newly initialized project + let validate_out = Command::new(rivet_bin()) + .args([ + "--project", + dir.to_str().unwrap(), + "validate", + "--format", + "json", + ]) + .output() + .expect("failed to execute rivet validate"); + + let stdout = String::from_utf8_lossy(&validate_out.stdout); + let parsed: serde_json::Value = + serde_json::from_str(&stdout).expect("validate JSON must be valid"); + + assert_eq!( + parsed.get("command").and_then(|v| v.as_str()), + Some("validate"), + ); + // Sample artifacts should have at most warnings, no errors + let errors = parsed.get("errors").and_then(|v| v.as_u64()).unwrap_or(999); + assert_eq!( + errors, 0, + "freshly-initialized project should have 0 validation errors, got {errors}" + ); +} diff --git a/rivet-core/tests/docs_schema.rs b/rivet-core/tests/docs_schema.rs new file mode 100644 index 0000000..2ec7396 --- /dev/null +++ b/rivet-core/tests/docs_schema.rs @@ -0,0 +1,209 @@ +//! Integration tests for embedded schema loading, fallback, and content. +//! +//! These tests verify that schemas compiled into the binary via `include_str!` +//! are accessible, parseable, and usable as fallbacks when disk files are absent. + +use std::path::PathBuf; + +// ── Embedded schema loading ────────────────────────────────────────────── + +/// `load_embedded_schema("common")` parses successfully and has the expected +/// schema name. +#[test] +fn embedded_schema_common_loads() { + let schema_file = rivet_core::embedded::load_embedded_schema("common") + .expect("common schema must load"); + assert_eq!(schema_file.schema.name, "common"); +} + +/// `load_embedded_schema("dev")` parses successfully. +#[test] +fn embedded_schema_dev_loads() { + let schema_file = rivet_core::embedded::load_embedded_schema("dev") + .expect("dev schema must load"); + assert_eq!(schema_file.schema.name, "dev"); + assert!( + !schema_file.artifact_types.is_empty(), + "dev schema must define artifact types" + ); +} + +/// All known embedded schemas load successfully. +#[test] +fn all_embedded_schemas_load() { + for name in rivet_core::embedded::SCHEMA_NAMES { + let schema_file = rivet_core::embedded::load_embedded_schema(name) + .unwrap_or_else(|e| panic!("embedded schema '{name}' must load: {e}")); + assert_eq!( + &schema_file.schema.name, name, + "schema name field must match the lookup key" + ); + } +} + +/// Unknown schema names return an error from `load_embedded_schema`. +#[test] +fn embedded_schema_unknown_returns_err() { + let result = rivet_core::embedded::load_embedded_schema("nonexistent-schema"); + assert!( + result.is_err(), + "unknown schema name must return Err" + ); + let err_msg = format!("{}", result.unwrap_err()); + assert!( + err_msg.contains("unknown built-in schema"), + "error message should mention 'unknown built-in schema', got: {err_msg}" + ); +} + +/// `embedded_schema()` returns `None` for unknown names. +#[test] +fn embedded_schema_lookup_none_for_unknown() { + assert!(rivet_core::embedded::embedded_schema("does-not-exist").is_none()); +} + +/// `embedded_schema()` returns `Some` for all known names. +#[test] +fn embedded_schema_lookup_some_for_known() { + for name in rivet_core::embedded::SCHEMA_NAMES { + assert!( + rivet_core::embedded::embedded_schema(name).is_some(), + "embedded_schema(\"{name}\") must return Some" + ); + } +} + +// ── Schema fallback ────────────────────────────────────────────────────── + +/// When the schemas directory does not contain the requested files, +/// `load_schemas_with_fallback` falls back to the embedded copies. +#[test] +fn schema_fallback_uses_embedded_when_dir_missing() { + // Point at a directory that definitely does not contain schema YAML files. + let fake_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests"); + + let names: Vec = vec!["common".into(), "dev".into()]; + let schema = rivet_core::embedded::load_schemas_with_fallback(&names, &fake_dir) + .expect("fallback must succeed"); + + // The merged schema should contain dev types (requirement, design-decision, feature). + assert!( + schema.artifact_type("requirement").is_some(), + "fallback-loaded schema must contain 'requirement' type" + ); + assert!( + schema.artifact_type("design-decision").is_some(), + "fallback-loaded schema must contain 'design-decision' type" + ); + assert!( + schema.artifact_type("feature").is_some(), + "fallback-loaded schema must contain 'feature' type" + ); +} + +/// Fallback with STPA schemas produces a schema containing STPA types. +#[test] +fn schema_fallback_stpa() { + let fake_dir = PathBuf::from("/tmp/rivet-test-nonexistent-dir"); + + let names: Vec = vec!["common".into(), "stpa".into()]; + let schema = rivet_core::embedded::load_schemas_with_fallback(&names, &fake_dir) + .expect("fallback must succeed for stpa"); + + assert!(schema.artifact_type("loss").is_some()); + assert!(schema.artifact_type("hazard").is_some()); + assert!(schema.artifact_type("uca").is_some()); + assert!(schema.link_type("leads-to-loss").is_some()); +} + +/// Fallback ignores completely unknown schema names (logs a warning but +/// does not error). The resulting merged schema is still valid. +#[test] +fn schema_fallback_unknown_name_ignored() { + let fake_dir = PathBuf::from("/tmp/rivet-test-nonexistent-dir"); + + let names: Vec = vec!["common".into(), "totally-unknown-name".into()]; + let schema = rivet_core::embedded::load_schemas_with_fallback(&names, &fake_dir) + .expect("fallback must not error on unknown names"); + + // Common link types should still be present from the "common" schema. + assert!(schema.link_type("satisfies").is_some()); +} + +// ── Embedded schema content ────────────────────────────────────────────── + +/// The embedded SCHEMA_COMMON constant is non-empty and contains expected content. +#[test] +fn schema_common_content_non_empty() { + assert!( + !rivet_core::embedded::SCHEMA_COMMON.is_empty(), + "SCHEMA_COMMON must not be empty" + ); + assert!( + rivet_core::embedded::SCHEMA_COMMON.contains("common"), + "SCHEMA_COMMON must contain 'common'" + ); +} + +/// The embedded SCHEMA_DEV constant is non-empty and mentions 'requirement'. +#[test] +fn schema_dev_content_non_empty() { + assert!( + !rivet_core::embedded::SCHEMA_DEV.is_empty(), + "SCHEMA_DEV must not be empty" + ); + assert!( + rivet_core::embedded::SCHEMA_DEV.contains("requirement"), + "SCHEMA_DEV must mention 'requirement' type" + ); +} + +/// The embedded SCHEMA_STPA constant is non-empty and mentions 'loss'. +#[test] +fn schema_stpa_content_non_empty() { + assert!(!rivet_core::embedded::SCHEMA_STPA.is_empty()); + assert!(rivet_core::embedded::SCHEMA_STPA.contains("loss")); +} + +/// The embedded SCHEMA_ASPICE constant is non-empty and mentions 'sw-req'. +#[test] +fn schema_aspice_content_non_empty() { + assert!(!rivet_core::embedded::SCHEMA_ASPICE.is_empty()); + assert!(rivet_core::embedded::SCHEMA_ASPICE.contains("sw-req")); +} + +/// The embedded SCHEMA_CYBERSECURITY constant is non-empty. +#[test] +fn schema_cybersecurity_content_non_empty() { + assert!(!rivet_core::embedded::SCHEMA_CYBERSECURITY.is_empty()); + assert!(rivet_core::embedded::SCHEMA_CYBERSECURITY.contains("threat-scenario")); +} + +/// The embedded SCHEMA_AADL constant is non-empty. +#[test] +fn schema_aadl_content_non_empty() { + assert!(!rivet_core::embedded::SCHEMA_AADL.is_empty()); + assert!(rivet_core::embedded::SCHEMA_AADL.contains("aadl")); +} + +/// All embedded schema constants are valid YAML that can be parsed into SchemaFile. +#[test] +fn all_embedded_constants_parse_as_yaml() { + let all: &[(&str, &str)] = &[ + ("common", rivet_core::embedded::SCHEMA_COMMON), + ("dev", rivet_core::embedded::SCHEMA_DEV), + ("stpa", rivet_core::embedded::SCHEMA_STPA), + ("aspice", rivet_core::embedded::SCHEMA_ASPICE), + ("cybersecurity", rivet_core::embedded::SCHEMA_CYBERSECURITY), + ("aadl", rivet_core::embedded::SCHEMA_AADL), + ]; + + for (name, content) in all { + let parsed: Result = serde_yaml::from_str(content); + assert!( + parsed.is_ok(), + "embedded schema constant for '{name}' must be valid YAML: {:?}", + parsed.err() + ); + } +} From 2944afb60f14044c94ef1eba5d26d3060f1aa2ac Mon Sep 17 00:00:00 2001 From: Ralf Anton Beier Date: Mon, 9 Mar 2026 07:47:25 +0100 Subject: [PATCH 2/2] style: fix rustfmt and clippy let-and-return Co-Authored-By: Claude Opus 4.6 --- rivet-cli/tests/cli_commands.rs | 44 ++++++++++++++++++--------------- rivet-core/tests/docs_schema.rs | 13 ++++------ 2 files changed, 29 insertions(+), 28 deletions(-) diff --git a/rivet-cli/tests/cli_commands.rs b/rivet-cli/tests/cli_commands.rs index 8e4a886..0c91e92 100644 --- a/rivet-cli/tests/cli_commands.rs +++ b/rivet-cli/tests/cli_commands.rs @@ -15,8 +15,7 @@ fn rivet_bin() -> std::path::PathBuf { // Construct path from CARGO_MANIFEST_DIR -> workspace target directory let manifest = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")); let workspace_root = manifest.parent().expect("workspace root"); - let target_dir = workspace_root.join("target").join("debug").join("rivet"); - target_dir + workspace_root.join("target").join("debug").join("rivet") } /// Project root (one level up from rivet-cli/). @@ -58,7 +57,10 @@ fn docs_show_topic() { .output() .expect("failed to execute rivet docs artifact-format"); - assert!(output.status.success(), "rivet docs artifact-format must exit 0"); + assert!( + output.status.success(), + "rivet docs artifact-format must exit 0" + ); let stdout = String::from_utf8_lossy(&output.stdout); assert!( @@ -97,7 +99,10 @@ fn docs_list_json() { .output() .expect("failed to execute rivet docs --format json"); - assert!(output.status.success(), "rivet docs --format json must exit 0"); + assert!( + output.status.success(), + "rivet docs --format json must exit 0" + ); let stdout = String::from_utf8_lossy(&output.stdout); let parsed: serde_json::Value = @@ -125,8 +130,7 @@ fn docs_grep_json() { assert!(output.status.success()); let stdout = String::from_utf8_lossy(&output.stdout); - let parsed: serde_json::Value = - serde_json::from_str(&stdout).expect("grep JSON must be valid"); + let parsed: serde_json::Value = serde_json::from_str(&stdout).expect("grep JSON must be valid"); assert_eq!( parsed.get("command").and_then(|v| v.as_str()), @@ -148,7 +152,12 @@ fn docs_grep_json() { #[test] fn schema_list() { let output = Command::new(rivet_bin()) - .args(["--project", project_root().to_str().unwrap(), "schema", "list"]) + .args([ + "--project", + project_root().to_str().unwrap(), + "schema", + "list", + ]) .output() .expect("failed to execute rivet schema list"); @@ -231,8 +240,7 @@ fn init_stpa_preset() { assert!(config_path.exists(), "rivet.yaml must be created"); // Read and verify config content - let config_content = - std::fs::read_to_string(&config_path).expect("read rivet.yaml"); + let config_content = std::fs::read_to_string(&config_path).expect("read rivet.yaml"); assert!( config_content.contains("stpa"), "rivet.yaml must reference 'stpa' schema, got:\n{config_content}" @@ -323,7 +331,10 @@ fn validate_json() { "JSON envelope must have command 'validate'" ); assert!( - parsed.get("diagnostics").and_then(|v| v.as_array()).is_some(), + parsed + .get("diagnostics") + .and_then(|v| v.as_array()) + .is_some(), "JSON must contain a 'diagnostics' array" ); // errors/warnings fields should be present @@ -400,18 +411,11 @@ fn list_json() { ); let stdout = String::from_utf8_lossy(&output.stdout); - let parsed: serde_json::Value = - serde_json::from_str(&stdout).expect("list JSON must be valid"); + let parsed: serde_json::Value = serde_json::from_str(&stdout).expect("list JSON must be valid"); - assert_eq!( - parsed.get("command").and_then(|v| v.as_str()), - Some("list"), - ); + assert_eq!(parsed.get("command").and_then(|v| v.as_str()), Some("list"),); assert!( - parsed - .get("artifacts") - .and_then(|v| v.as_array()) - .is_some(), + parsed.get("artifacts").and_then(|v| v.as_array()).is_some(), "list JSON must contain 'artifacts' array" ); assert!( diff --git a/rivet-core/tests/docs_schema.rs b/rivet-core/tests/docs_schema.rs index 2ec7396..b8827dc 100644 --- a/rivet-core/tests/docs_schema.rs +++ b/rivet-core/tests/docs_schema.rs @@ -11,16 +11,16 @@ use std::path::PathBuf; /// schema name. #[test] fn embedded_schema_common_loads() { - let schema_file = rivet_core::embedded::load_embedded_schema("common") - .expect("common schema must load"); + let schema_file = + rivet_core::embedded::load_embedded_schema("common").expect("common schema must load"); assert_eq!(schema_file.schema.name, "common"); } /// `load_embedded_schema("dev")` parses successfully. #[test] fn embedded_schema_dev_loads() { - let schema_file = rivet_core::embedded::load_embedded_schema("dev") - .expect("dev schema must load"); + let schema_file = + rivet_core::embedded::load_embedded_schema("dev").expect("dev schema must load"); assert_eq!(schema_file.schema.name, "dev"); assert!( !schema_file.artifact_types.is_empty(), @@ -45,10 +45,7 @@ fn all_embedded_schemas_load() { #[test] fn embedded_schema_unknown_returns_err() { let result = rivet_core::embedded::load_embedded_schema("nonexistent-schema"); - assert!( - result.is_err(), - "unknown schema name must return Err" - ); + assert!(result.is_err(), "unknown schema name must return Err"); let err_msg = format!("{}", result.unwrap_err()); assert!( err_msg.contains("unknown built-in schema"),