pushkin-core 0.2.1

Core envelope, manifest, pipeline, and waiver types for the pushkin write-gate
Documentation
//! Manifest parsing conformance (Phase 1 test plan — read-only once committed).

use pushkin_core::manifest::{Manifest, ManifestError};

const MINIMAL: &str = r#"
version = 1
canonical = "json-schema-2020-12"
authoring = "zod"

[[contracts]]
name = "user"
source = "contracts/user.zod.ts"
emit = ["zod", "pydantic", "rust", "sql"]

[[mappings]]
glob = "app/api/**/*.ts"
contracts = ["user"]
require = "boundary-validation"

[gates]
suppression_comments = "deny"
protected_paths = ["pushkin.toml", ".claude/settings.json", "schemas/**"]
"#;

#[test]
fn parses_minimal_manifest() {
    let manifest = Manifest::parse(MINIMAL).unwrap();
    assert_eq!(manifest.version, 1);
    assert_eq!(manifest.contracts.len(), 1);
    assert_eq!(manifest.contracts[0].name.as_str(), "user");
    assert_eq!(manifest.mappings.len(), 1);
    assert_eq!(manifest.gates.protected_paths.len(), 3);
}

#[test]
fn rejects_unknown_key_with_candidates() {
    let bad = MINIMAL.replace("suppression_comments", "supression_comments");
    let err = Manifest::parse(&bad).unwrap_err();
    let message = err.to_string();
    assert!(
        message.contains("supression_comments") || message.contains("unknown field"),
        "error must name the unknown key: {message}"
    );
    assert!(
        message.contains("suppression_comments"),
        "error must suggest the valid key (candidates, spec conduct 4.4): {message}"
    );
}

#[test]
fn rejects_unsupported_version() {
    let bad = MINIMAL.replace("version = 1", "version = 99");
    let err = Manifest::parse(&bad).unwrap_err();
    assert!(matches!(
        err,
        ManifestError::UnsupportedVersion { found: 99, .. }
    ));
}

#[test]
fn mapping_glob_resolves_to_contract() {
    let manifest = Manifest::parse(MINIMAL).unwrap();
    let hit = manifest.mapping_for("app/api/users/route.ts");
    assert!(hit.is_some());
    let mapping = hit.unwrap();
    assert_eq!(mapping.contracts[0].as_str(), "user");

    assert!(manifest.mapping_for("docs/notes.md").is_none());
}

#[test]
fn protected_paths_parsed() {
    let manifest = Manifest::parse(MINIMAL).unwrap();
    assert!(manifest.is_protected("pushkin.toml"));
    assert!(manifest.is_protected(".claude/settings.json"));
    assert!(manifest.is_protected("schemas/user.schema.json"));
    assert!(!manifest.is_protected("src/lib.rs"));
}

#[test]
fn rejects_mapping_to_undeclared_contract() {
    let bad = MINIMAL.replace(r#"contracts = ["user"]"#, r#"contracts = ["usr"]"#);
    let err = Manifest::parse(&bad).unwrap_err();
    let message = err.to_string();
    assert!(
        message.contains("usr") && message.contains("user"),
        "ambiguity/miss errors carry candidates (spec 7.1): {message}"
    );
}