provenance-cli 0.2.3

Command-line interface for Provenance requirements traceability
use std::{
    env, fs,
    path::{Path, PathBuf},
    process::Command,
};

use serde_json::Value;

const CRATE_ORDER: [&str; 9] = [
    "provenance-macros",
    "provenance-core",
    "provenance-scanner",
    "provenance-ste100",
    "provenance-store",
    "provenance-sdk",
    "provenance-http-client",
    "provenance-transport",
    "provenance-cli",
];

#[test]
fn rust_release_packages_have_registry_metadata_and_versioned_dependencies() {
    let workspace = workspace_root();
    let output = Command::new(env!("CARGO"))
        .args(["metadata", "--no-deps", "--format-version", "1"])
        .current_dir(&workspace)
        .output()
        .expect("run cargo metadata");
    assert!(output.status.success(), "cargo metadata failed");
    let metadata: Value = serde_json::from_slice(&output.stdout).expect("parse cargo metadata");
    let packages = metadata["packages"]
        .as_array()
        .expect("metadata packages are an array");

    for crate_name in CRATE_ORDER {
        let package = packages
            .iter()
            .find(|package| package["name"] == crate_name)
            .unwrap_or_else(|| panic!("metadata omitted {crate_name}"));
        let version = package["version"]
            .as_str()
            .expect("package version is text");

        assert_eq!(
            package["license"], "BUSL-1.1",
            "{crate_name} has no release license"
        );
        assert_eq!(
            package["repository"], "https://github.com/quality-sh/provenance",
            "{crate_name} has no canonical repository",
        );
        assert_eq!(
            package["homepage"], "https://github.com/quality-sh/provenance",
            "{crate_name} has no canonical homepage",
        );
        assert!(
            package["description"]
                .as_str()
                .is_some_and(|value| !value.is_empty()),
            "{crate_name} has no description",
        );
        assert!(package["readme"].is_string(), "{crate_name} has no README");
        assert_eq!(
            package["publish"],
            serde_json::json!(["crates-io"]),
            "{crate_name} must publish only to crates.io",
        );

        for dependency in package["dependencies"]
            .as_array()
            .expect("package dependencies are an array")
        {
            let Some(dependency_name) = dependency["name"].as_str() else {
                continue;
            };
            if !CRATE_ORDER.contains(&dependency_name) {
                continue;
            }
            assert_eq!(
                dependency["req"],
                format!("^{version}"),
                "{crate_name} must give {dependency_name} the shared registry version",
            );
        }
    }
}

#[test]
fn release_workflow_publishes_rust_crates_in_dependency_order() {
    let workspace = workspace_root();
    let workflow = fs::read_to_string(workspace.join(".github/workflows/release.yml"))
        .expect("read release workflow");
    let mut previous = 0;
    for crate_name in CRATE_ORDER {
        let offset = workflow[previous..]
            .find(crate_name)
            .unwrap_or_else(|| panic!("release workflow omits {crate_name}"));
        previous += offset + crate_name.len();
    }
    assert!(workflow.contains("name: Publish Rust crates"));
    assert!(workflow.contains("environment: crates-io"));
    assert!(workflow.contains("publish-cargo-if-missing.sh"));
    let package = workflow
        .find("cargo package --workspace --locked --allow-dirty")
        .unwrap();
    assert!(workflow.find("git diff --exit-code HEAD --").unwrap() < package);
    assert!(
        workflow
            .find("git ls-files --others --exclude-standard")
            .unwrap()
            < package
    );
    assert!(workflow.contains("toolchain: 1.98.0"));
    assert!(!workflow.contains("run: cargo publish"));
}

#[test]
fn release_workflow_uses_crates_io_trusted_publishing() {
    let workspace = workspace_root();
    let workflow = fs::read_to_string(workspace.join(".github/workflows/release.yml"))
        .expect("read release workflow");
    let crates_job = workflow
        .split_once("  publish-crates:")
        .expect("release workflow has a Rust publish job")
        .1
        .split_once("\n  publish-npm:")
        .expect("npm publish job follows the Rust publish job")
        .0;

    assert!(crates_job.contains("id-token: write"));
    assert!(crates_job
        .contains("rust-lang/crates-io-auth-action@c6f97d42243bad5fab37ca0427f495c86d5b1a18"));
    assert!(crates_job.contains("CARGO_REGISTRY_TOKEN: ${{ steps.crates-auth.outputs.token }}"));
    assert!(!workflow.contains("CRATES_IO_BOOTSTRAP_TOKEN"));
}

#[test]
fn release_workflow_uses_npm_trusted_publishing_without_a_token() {
    let workspace = workspace_root();
    let workflow = fs::read_to_string(workspace.join(".github/workflows/release.yml"))
        .expect("read release workflow");
    let npm_job = workflow
        .split_once("  publish-npm:")
        .expect("release workflow has an npm publish job")
        .1;

    assert!(npm_job.contains("id-token: write"));
    assert!(
        npm_job.contains("runs-on: ubuntu-latest"),
        "npm provenance requires a GitHub-hosted publisher"
    );
    assert!(npm_job.contains("npm install --global npm@^11.5.1"));
    assert!(!workflow.contains("NPM_TOKEN"));
    assert!(!workflow.contains("NODE_AUTH_TOKEN"));
}

#[test]
fn cli_package_contains_its_embedded_skills() {
    let workspace = workspace_root();
    let output = Command::new(env!("CARGO"))
        .args([
            "package",
            "--locked",
            "--no-verify",
            "--allow-dirty",
            "--package",
            "provenance-cli",
            "--list",
        ])
        .current_dir(workspace)
        .output()
        .expect("list the CLI package");
    assert!(
        output.status.success(),
        "cargo package failed: {}",
        String::from_utf8_lossy(&output.stderr),
    );
    let files = String::from_utf8(output.stdout).expect("package file list is UTF-8");
    for file in [
        "build/review_assets.rs",
        "review-assets/index.html",
        "src/review.rs",
    ] {
        assert!(
            files.lines().any(|entry| entry == file),
            "CLI package omits {file}"
        );
    }
    for skill in [
        "provenance-fork-tournament",
        "provenance-grounded-writing",
        "provenance-shaping",
        "provenance-swarm-backtrace",
    ] {
        assert!(
            files
                .lines()
                .any(|file| file == format!("skills/{skill}/SKILL.md")),
            "CLI package omits {skill}",
        );
    }
}

#[test]
fn crate_local_embedded_skills_are_checked_out_with_lf() {
    let workspace = workspace_root();
    for skill in [
        "provenance-fork-tournament",
        "provenance-grounded-writing",
        "provenance-shaping",
        "provenance-swarm-backtrace",
    ] {
        let path = format!("crates/provenance-cli/skills/{skill}/SKILL.md");
        let output = Command::new("git")
            .args(["check-attr", "eol", "--", &path])
            .current_dir(&workspace)
            .output()
            .expect("read Git attributes");
        assert!(output.status.success(), "git check-attr failed for {path}");
        assert!(
            String::from_utf8(output.stdout)
                .expect("git check-attr output is UTF-8")
                .trim_end()
                .ends_with("eol: lf"),
            "{path} must use LF so include_str! embeds the same bytes on every platform",
        );
    }
}

#[cfg(unix)]
mod publish_helper {
    use super::*;
    use std::os::unix::fs::PermissionsExt;

    const LOCAL_CHECKSUM: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";

    #[test]
    fn an_existing_crate_version_is_not_published_again() {
        let result = invoke("200", LOCAL_CHECKSUM, "false");
        assert!(
            result.status.success(),
            "{}",
            String::from_utf8_lossy(&result.stderr)
        );
        let calls = fs::read_to_string(result.call_log).expect("read helper calls");
        assert!(calls.contains("curl "));
        assert!(calls.contains("/provenance-core/0.2.0"));
        assert!(!calls.contains("cargo "), "{calls}");
    }

    #[test]
    fn a_missing_crate_version_is_published() {
        let result = invoke("404", "unused-checksum", "false");
        assert!(
            result.status.success(),
            "{}",
            String::from_utf8_lossy(&result.stderr)
        );
        let calls = fs::read_to_string(result.call_log).expect("read helper calls");
        assert!(calls.contains("/provenance-core/0.2.0"));
        assert!(
            calls.contains(
                "cargo token=unset publish --registry crates-io --dry-run --locked --allow-dirty --package provenance-core",
            ),
            "{calls}",
        );
        assert!(
            calls.contains(
                "cargo token=test-token publish --registry crates-io --no-verify --locked --allow-dirty --package provenance-core",
            ),
            "{calls}",
        );
        assert!(
            calls.contains("cargo token=unset info --registry crates-io provenance-core@0.2.0",),
            "{calls}",
        );
    }

    #[test]
    fn an_unexpected_registry_response_fails_closed() {
        let result = invoke("503", "unused-checksum", "false");
        assert!(!result.status.success());
        let calls = fs::read_to_string(result.call_log).expect("read helper calls");
        assert!(!calls.contains("cargo "), "{calls}");
    }

    #[test]
    fn an_existing_version_with_different_contents_fails_closed() {
        let result = invoke(
            "200",
            "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
            "false",
        );
        assert!(!result.status.success());
        assert!(
            String::from_utf8_lossy(&result.stderr).contains("checksum"),
            "{}",
            String::from_utf8_lossy(&result.stderr),
        );
        let calls = fs::read_to_string(result.call_log).expect("read helper calls");
        assert!(!calls.contains("cargo "), "{calls}");
    }

    #[test]
    fn a_yanked_existing_version_fails_closed() {
        let result = invoke("200", LOCAL_CHECKSUM, "true");
        assert!(!result.status.success());
        assert!(
            String::from_utf8_lossy(&result.stderr).contains("yanked"),
            "{}",
            String::from_utf8_lossy(&result.stderr),
        );
        let calls = fs::read_to_string(result.call_log).expect("read helper calls");
        assert!(!calls.contains("cargo "), "{calls}");
    }

    struct Invocation {
        status: std::process::ExitStatus,
        stderr: Vec<u8>,
        call_log: PathBuf,
        _temporary: tempfile::TempDir,
    }

    fn invoke(status_code: &str, registry_checksum: &str, yanked: &str) -> Invocation {
        let workspace = workspace_root();
        let temporary = tempfile::tempdir().expect("create helper directory");
        let call_log = temporary.path().join("calls.log");
        let archive = temporary.path().join("provenance-core-0.2.0.crate");
        fs::write(&archive, "test crate archive").expect("write fake crate archive");
        write_executable(
            &temporary.path().join("curl"),
            "#!/bin/sh\nprintf 'curl token=%s %s\\n' \"${CARGO_REGISTRY_TOKEN-unset}\" \"$*\" >> \"$PUBLISH_CALL_LOG\"\noutput=/dev/null\nwant_output=false\nfor argument in \"$@\"; do\n  if [ \"$want_output\" = true ]; then\n    output=$argument\n    want_output=false\n  elif [ \"$argument\" = --output ]; then\n    want_output=true\n  fi\ndone\nprintf '{\"version\":{\"checksum\":\"%s\",\"yanked\":%s}}' \"$FAKE_REGISTRY_CHECKSUM\" \"$FAKE_REGISTRY_YANKED\" > \"$output\"\nprintf '%s' \"$FAKE_CRATES_STATUS\"\n",
        );
        write_executable(
            &temporary.path().join("cargo"),
            "#!/bin/sh\nprintf 'cargo token=%s %s\\n' \"${CARGO_REGISTRY_TOKEN-unset}\" \"$*\" >> \"$PUBLISH_CALL_LOG\"\n",
        );
        write_executable(
            &temporary.path().join("sha256sum"),
            "#!/bin/sh\nprintf '%s  %s\\n' \"$FAKE_LOCAL_CHECKSUM\" \"$1\"\n",
        );
        let path = env::join_paths(
            std::iter::once(temporary.path().to_path_buf())
                .chain(env::split_paths(&env::var_os("PATH").expect("PATH is set"))),
        )
        .expect("build helper PATH");
        let output = Command::new("bash")
            .args([
                workspace
                    .join(".github/scripts/publish-cargo-if-missing.sh")
                    .to_str()
                    .expect("helper path is UTF-8"),
                "provenance-core",
                "0.2.0",
                archive.to_str().expect("archive path is UTF-8"),
            ])
            .env("PATH", path)
            .env("PUBLISH_CALL_LOG", &call_log)
            .env("FAKE_CRATES_STATUS", status_code)
            .env("FAKE_REGISTRY_CHECKSUM", registry_checksum)
            .env("FAKE_REGISTRY_YANKED", yanked)
            .env("FAKE_LOCAL_CHECKSUM", LOCAL_CHECKSUM)
            .env("CARGO_REGISTRY_TOKEN", "test-token")
            .current_dir(workspace)
            .output()
            .expect("run cargo publication helper");

        Invocation {
            status: output.status,
            stderr: output.stderr,
            call_log,
            _temporary: temporary,
        }
    }

    fn write_executable(path: &Path, contents: &str) {
        fs::write(path, contents).expect("write fake executable");
        let mut permissions = fs::metadata(path)
            .expect("read fake metadata")
            .permissions();
        permissions.set_mode(0o755);
        fs::set_permissions(path, permissions).expect("make fake executable runnable");
    }
}

fn workspace_root() -> PathBuf {
    PathBuf::from(env!("CARGO_MANIFEST_DIR"))
        .parent()
        .and_then(Path::parent)
        .expect("CLI crate is nested under the workspace root")
        .to_path_buf()
}