alma 0.1.1

A Bevy-native modal text editor with Vim-style navigation.
Documentation
use std::{collections::BTreeSet, fs, path::PathBuf};

use alma::{
    config::AppConfig,
    plugin::{
        CURRENT_WIT_WORLD, MAX_PLUGIN_FUEL_PER_UPDATE, MAX_PLUGIN_INTENT_BATCHES,
        MAX_PLUGIN_MEMORY_BYTES, MAX_PLUGIN_MESSAGE_BYTES, MAX_PLUGIN_TIMEOUT_MS, PluginManifest,
    },
};
use serde_json::Value;

const VALID_CONFIG_FIXTURES: &[&str] = &[
    "config-minimal-valid.json",
    "config-maximal-explicit.json",
    "config-disabled.json",
    "config-whole-workspace-grant.json",
];

const INVALID_CONFIG_FIXTURES: &[&str] = &[
    "config-unknown-plugin-field.json",
    "config-invalid-runtime-limit.json",
    "config-invalid-intent-queue-limit.json",
    "config-invalid-proposal-policy.json",
];

const VALID_MANIFEST_FIXTURES: &[&str] = &["manifest-valid.json"];

const INVALID_MANIFEST_FIXTURES: &[&str] = &[
    "manifest-mismatched-world.json",
    "manifest-unknown-capability.json",
    "manifest-unknown-field.json",
];

const REQUIRED_PLUGIN_GOLDEN_COVERAGE: &[&str] = &[
    "plugin.config.schema.checked-in",
    "plugin.config.schema.current-wit-world",
    "plugin.config.schema.runtime-limit-caps",
    "plugin.config.schema.valid-minimal",
    "plugin.config.schema.valid-maximal",
    "plugin.config.schema.disabled",
    "plugin.config.schema.whole-workspace-grant",
    "plugin.config.schema.unknown-field",
    "plugin.config.schema.invalid-runtime-limit",
    "plugin.config.schema.invalid-intent-queue-limit",
    "plugin.config.schema.invalid-proposal-policy",
    "plugin.config.schema.proposal-policy-vocabulary",
    "plugin.manifest.schema.checked-in",
    "plugin.manifest.schema.current-wit-world",
    "plugin.manifest.schema.valid",
    "plugin.manifest.schema.mismatched-world",
    "plugin.manifest.schema.unknown-capability",
    "plugin.manifest.schema.unknown-field",
    "plugin.capability.schema.reviewed-vocabulary",
];

const PLUGIN_GOLDEN_COVERAGE: &[(&str, &[&str])] = &[
    (
        "config-minimal-valid.json",
        &["plugin.config.schema.valid-minimal"],
    ),
    (
        "config-maximal-explicit.json",
        &["plugin.config.schema.valid-maximal"],
    ),
    ("config-disabled.json", &["plugin.config.schema.disabled"]),
    (
        "config-whole-workspace-grant.json",
        &["plugin.config.schema.whole-workspace-grant"],
    ),
    (
        "config-unknown-plugin-field.json",
        &["plugin.config.schema.unknown-field"],
    ),
    (
        "config-invalid-runtime-limit.json",
        &["plugin.config.schema.invalid-runtime-limit"],
    ),
    (
        "config-invalid-intent-queue-limit.json",
        &["plugin.config.schema.invalid-intent-queue-limit"],
    ),
    (
        "config-invalid-proposal-policy.json",
        &["plugin.config.schema.invalid-proposal-policy"],
    ),
    ("manifest-valid.json", &["plugin.manifest.schema.valid"]),
    (
        "manifest-mismatched-world.json",
        &["plugin.manifest.schema.mismatched-world"],
    ),
    (
        "manifest-unknown-capability.json",
        &["plugin.manifest.schema.unknown-capability"],
    ),
    (
        "manifest-unknown-field.json",
        &["plugin.manifest.schema.unknown-field"],
    ),
    (
        "plugin-config.schema.json",
        &[
            "plugin.config.schema.checked-in",
            "plugin.config.schema.current-wit-world",
            "plugin.config.schema.runtime-limit-caps",
            "plugin.config.schema.proposal-policy-vocabulary",
            "plugin.capability.schema.reviewed-vocabulary",
        ],
    ),
    (
        "plugin-manifest.schema.json",
        &[
            "plugin.manifest.schema.checked-in",
            "plugin.manifest.schema.current-wit-world",
            "plugin.capability.schema.reviewed-vocabulary",
        ],
    ),
];

#[test]
fn plugin_goldens_cover_required_behavior() {
    let required = REQUIRED_PLUGIN_GOLDEN_COVERAGE
        .iter()
        .copied()
        .collect::<BTreeSet<_>>();
    let mut covered = BTreeSet::new();
    let mut fixture_names = BTreeSet::<String>::new();

    for (name, labels) in PLUGIN_GOLDEN_COVERAGE {
        assert!(
            fixture_names.insert((*name).to_owned()),
            "duplicate plugin golden coverage entry for {name}"
        );
        for label in *labels {
            assert!(
                required.contains(label),
                "{name} declares unknown plugin golden coverage label {label:?}"
            );
            covered.insert(*label);
        }
    }

    let missing = required.difference(&covered).copied().collect::<Vec<_>>();
    assert!(
        missing.is_empty(),
        "missing required plugin golden coverage labels: {missing:?}"
    );

    let orphan_fixtures = plugin_golden_fixture_names()
        .difference(&fixture_names)
        .cloned()
        .collect::<Vec<_>>();
    assert!(
        orphan_fixtures.is_empty(),
        "plugin golden fixtures without coverage entries: {orphan_fixtures:?}"
    );
}

#[test]
fn checked_in_plugin_config_schema_accepts_valid_fixtures() {
    let schema = load_schema("plugin-config.schema.json");

    for name in VALID_CONFIG_FIXTURES {
        let value = load_json(name);
        assert_schema_accepts(&schema, &value, name);
        let _config = AppConfig::from_json_slice(
            &serde_json::to_vec(&value).expect("fixture JSON should serialize"),
            &fixture_path(name),
        )
        .unwrap_or_else(|error| panic!("{name} should load through typed config: {error}"));
    }
}

#[test]
fn checked_in_plugin_config_schema_rejects_invalid_fixtures() {
    let schema = load_schema("plugin-config.schema.json");

    for name in INVALID_CONFIG_FIXTURES {
        let value = load_json(name);
        assert_schema_rejects(&schema, &value, name);
        assert!(
            AppConfig::from_json_slice(
                &serde_json::to_vec(&value).expect("fixture JSON should serialize"),
                &fixture_path(name),
            )
            .is_err(),
            "{name} should fail typed config loading"
        );
    }
}

#[test]
fn checked_in_plugin_manifest_schema_accepts_valid_fixtures() {
    let schema = load_schema("plugin-manifest.schema.json");

    for name in VALID_MANIFEST_FIXTURES {
        let value = load_json(name);
        assert_schema_accepts(&schema, &value, name);
        let _manifest = PluginManifest::from_json_slice(
            &serde_json::to_vec(&value).expect("fixture JSON should serialize"),
            Some(&fixture_path(name)),
        )
        .unwrap_or_else(|error| panic!("{name} should load through typed manifest: {error}"));
    }
}

#[test]
fn checked_in_plugin_manifest_schema_rejects_invalid_fixtures() {
    let schema = load_schema("plugin-manifest.schema.json");

    for name in INVALID_MANIFEST_FIXTURES {
        let value = load_json(name);
        assert_schema_rejects(&schema, &value, name);
        let parsed = PluginManifest::from_json_slice(
            &serde_json::to_vec(&value).expect("fixture JSON should serialize"),
            Some(&fixture_path(name)),
        );
        if *name == "manifest-mismatched-world.json" {
            assert!(
                parsed.is_ok(),
                "{name} should parse as untrusted manifest data before authorization"
            );
        } else {
            assert!(parsed.is_err(), "{name} should fail typed manifest loading");
        }
    }
}

#[test]
fn plugin_schemas_name_only_reviewed_capabilities() {
    for name in ["plugin-config.schema.json", "plugin-manifest.schema.json"] {
        let schema = load_schema(name);
        let capability_properties = schema
            .pointer("/$defs/capabilities/properties")
            .and_then(Value::as_object)
            .expect("capability schema should expose reviewed properties");

        assert_eq!(
            capability_properties.keys().collect::<Vec<_>>(),
            [
                "buffer.observe",
                "buffer.propose_edit",
                "status.publish",
                "workspace.artifact_write",
                "workspace.observe",
            ],
            "{name} capability vocabulary drifted"
        );
    }
}

#[test]
fn plugin_schemas_match_current_wit_world() {
    let config_schema = load_schema("plugin-config.schema.json");
    assert_eq!(
        config_schema
            .pointer("/$defs/plugin/properties/wit_world/const")
            .and_then(Value::as_str),
        Some(CURRENT_WIT_WORLD)
    );

    let manifest_schema = load_schema("plugin-manifest.schema.json");
    assert_eq!(
        manifest_schema
            .pointer("/properties/wit_world/const")
            .and_then(Value::as_str),
        Some(CURRENT_WIT_WORLD)
    );
}

#[test]
fn plugin_config_schema_matches_runtime_limit_caps() {
    let schema = load_schema("plugin-config.schema.json");
    let limits = [
        ("max_memory_bytes", MAX_PLUGIN_MEMORY_BYTES),
        ("max_message_bytes", MAX_PLUGIN_MESSAGE_BYTES),
        ("fuel_per_update", MAX_PLUGIN_FUEL_PER_UPDATE),
        ("timeout_ms", MAX_PLUGIN_TIMEOUT_MS),
        ("max_intent_batches", MAX_PLUGIN_INTENT_BATCHES),
    ];

    for (field, max) in limits {
        let limit = schema
            .pointer(&format!("/$defs/runtime_limits/properties/{field}"))
            .unwrap_or_else(|| panic!("runtime limit schema missing {field}"));
        assert_eq!(
            limit.pointer("/minimum").and_then(Value::as_u64),
            Some(1),
            "{field} schema minimum drifted"
        );
        assert_eq!(
            limit.pointer("/maximum").and_then(Value::as_u64),
            Some(max),
            "{field} schema maximum drifted"
        );
    }
}

#[test]
fn plugin_config_schema_names_only_reviewed_proposal_modes() {
    let schema = load_schema("plugin-config.schema.json");
    let modes = schema
        .pointer("/$defs/proposal_review_mode/enum")
        .and_then(Value::as_array)
        .expect("proposal review mode schema should expose reviewed enum values");

    let expected = [Value::from("auto_apply"), Value::from("manual_review")];
    assert_eq!(modes.as_slice(), expected);
}

fn load_schema(name: &str) -> Value {
    load_json(name)
}

fn load_json(name: &str) -> Value {
    let path = fixture_path(name);
    serde_json::from_slice(&fs::read(&path).expect("fixture should read"))
        .unwrap_or_else(|error| panic!("{} should parse as JSON: {error}", path.display()))
}

fn assert_schema_accepts(schema: &Value, value: &Value, name: &str) {
    let validator = jsonschema::validator_for(schema).expect("schema should compile");
    if let Err(error) = validator.validate(value) {
        panic!("{name} should satisfy checked-in schema: {error}");
    }
}

fn assert_schema_rejects(schema: &Value, value: &Value, name: &str) {
    let validator = jsonschema::validator_for(schema).expect("schema should compile");
    assert!(
        validator.validate(value).is_err(),
        "{name} should fail checked-in schema"
    );
}

fn fixture_path(name: &str) -> PathBuf {
    PathBuf::from(env!("CARGO_MANIFEST_DIR"))
        .join("tests/goldens/plugin")
        .join(name)
}

fn plugin_golden_fixture_names() -> BTreeSet<String> {
    let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/goldens/plugin");
    fs::read_dir(root)
        .expect("plugin golden directory should read")
        .filter_map(|entry| {
            let path = entry.expect("plugin golden entry should read").path();
            path.extension()
                .is_some_and(|extension| extension == "json")
                .then(|| {
                    path.file_name()
                        .and_then(|name| name.to_str())
                        .expect("plugin golden fixture name should be UTF-8")
                        .to_owned()
                })
        })
        .collect()
}