AkurAI Build
Menu

AkurAI-Build

public

Latest change 8aa1f8919f21ee292d04c845bb1abfb506bafb85 - Merge fc/test-explicit-empty-needs by AkurAI Build

use std::collections::{BTreeMap, BTreeSet};
use std::path::{Component, Path};

use anyhow::{Context, Result, bail, ensure};
use serde::{Deserialize, Serialize};

const MAX_PIPELINE_BYTES: usize = 256 * 1024;
const MAX_JOBS: usize = 64;
const MAX_VARIANTS: usize = 32;
const MAX_EXPANDED_JOBS: usize = 256;
const MAX_COMMAND_BYTES: usize = 64 * 1024;

#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
struct PipelineInput {
    version: u8,
    jobs: Vec<JobInput>,
}

#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
struct JobInput {
    name: String,
    needs: Option<Vec<String>>,
    #[serde(default = "docker")]
    executor: String,
    image: Option<String>,
    shell: Option<String>,
    run: Commands,
    #[serde(default)]
    matrix: BTreeMap<String, Vec<String>>,
    #[serde(default)]
    artifacts: Vec<String>,
    #[serde(default)]
    cache: Vec<String>,
    #[serde(default)]
    network: bool,
    #[serde(default)]
    secrets: Vec<String>,
    #[serde(default)]
    installation_id: Option<String>,
    #[serde(default)]
    installation_revision: Option<i64>,
    #[serde(default)]
    rollback_of_deployment_id: Option<i64>,
    #[serde(default)]
    recovery_of_deployment_id: Option<i64>,
    environment: Option<String>,
    #[serde(default)]
    approval: bool,
    #[serde(default)]
    branches: Vec<String>,
    #[serde(default = "default_timeout")]
    timeout: u64,
    #[serde(default)]
    profile: Option<ProfileInput>,
}

#[derive(Debug, Deserialize)]
#[serde(untagged)]
enum Commands {
    One(String),
    Many(Vec<String>),
}

#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ProfileSpec {
    pub workload: String,
    #[serde(default = "default_profile_samples")]
    pub samples: u32,
}

type ProfileInput = ProfileSpec;

const fn default_profile_samples() -> u32 {
    5
}

#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct JobSpec {
    pub base_name: String,
    pub name: String,
    pub needs: Vec<String>,
    pub executor: String,
    pub image: Option<String>,
    pub shell: Option<String>,
    pub command: String,
    pub matrix: BTreeMap<String, String>,
    pub artifacts: Vec<String>,
    pub cache: Vec<String>,
    pub network: bool,
    pub secrets: Vec<String>,
    #[serde(default)]
    pub installation_id: Option<String>,
    #[serde(default)]
    pub installation_revision: Option<i64>,
    #[serde(default)]
    pub rollback_of_deployment_id: Option<i64>,
    #[serde(default)]
    pub recovery_of_deployment_id: Option<i64>,
    pub environment: Option<String>,
    pub approval: bool,
    pub branches: Vec<String>,
    pub timeout: u64,
    #[serde(default)]
    pub profile: Option<ProfileSpec>,
}

fn docker() -> String {
    "docker".into()
}

const fn default_timeout() -> u64 {
    1800
}

pub fn parse(source: &str) -> Result<Vec<JobSpec>> {
    ensure!(source.len() <= MAX_PIPELINE_BYTES, "pipeline is too large");
    let pipeline: PipelineInput = serde_yaml::from_str(source).context("parse .akurai.yml")?;
    ensure!(pipeline.version == 1, "pipeline version must be 1");
    ensure!(
        !pipeline.jobs.is_empty() && pipeline.jobs.len() <= MAX_JOBS,
        "pipeline must contain 1..={MAX_JOBS} jobs"
    );

    let mut names = BTreeSet::new();
    for job in &pipeline.jobs {
        validate_name("job", &job.name)?;
        ensure!(
            names.insert(job.name.clone()),
            "duplicate job: {}",
            job.name
        );
    }

    let mut expanded = Vec::new();
    let mut graph = BTreeMap::new();
    let mut previous: Option<String> = None;
    for job in pipeline.jobs {
        validate_job(&job)?;
        let needs = job
            .needs
            .clone()
            .unwrap_or_else(|| previous.iter().cloned().collect());
        for need in &needs {
            ensure!(
                names.contains(need),
                "job {} needs unknown job {need}",
                job.name
            );
            ensure!(need != &job.name, "job {} cannot need itself", job.name);
        }
        graph.insert(job.name.clone(), needs.clone());
        let command = commands(&job.run)?;
        let variants = expand_matrix(&job.matrix)?;
        for matrix in variants {
            let suffix = matrix
                .iter()
                .map(|(key, value)| format!("{key}={value}"))
                .collect::<Vec<_>>()
                .join(",");
            ensure!(
                expanded.len() < MAX_EXPANDED_JOBS,
                "pipeline expands beyond {MAX_EXPANDED_JOBS} jobs"
            );
            expanded.push(JobSpec {
                base_name: job.name.clone(),
                name: if suffix.is_empty() {
                    job.name.clone()
                } else {
                    format!("{}[{suffix}]", job.name)
                },
                needs: needs.clone(),
                executor: job.executor.clone(),
                image: job.image.clone(),
                shell: job.shell.clone(),
                command: command.clone(),
                matrix,
                artifacts: job.artifacts.clone(),
                cache: job.cache.clone(),
                network: job.network,
                installation_id: job.installation_id.clone(),
                installation_revision: job.installation_revision,
                rollback_of_deployment_id: job.rollback_of_deployment_id,
                recovery_of_deployment_id: job.recovery_of_deployment_id,
                environment: job.environment.clone(),
                secrets: job.secrets.clone(),
                approval: job.approval,
                branches: job.branches.clone(),
                timeout: job.timeout,
                profile: job.profile.clone(),
            });
        }
        previous = Some(job.name);
    }
    validate_dag(&graph)?;
    Ok(expanded)
}

fn validate_job(job: &JobInput) -> Result<()> {
    ensure!(
        matches!(job.executor.as_str(), "docker" | "native"),
        "job {} executor must be docker or native",
        job.name
    );
    if job.executor == "docker" {
        let image = job.image.as_deref().context("docker job requires image")?;
        ensure!(
            !image.starts_with('-') && !image.chars().any(char::is_whitespace),
            "invalid Docker image"
        );
        ensure!(image.len() <= 256, "Docker image is too long");
    } else {
        ensure!(job.image.is_none(), "native job cannot set image");
    }
    if job.installation_id.is_some() || job.installation_revision.is_some() {
        ensure!(
            job.environment.is_some(),
            "managed installation provenance requires an environment"
        );
        ensure!(
            job.installation_id.is_some() && job.installation_revision.is_some(),
            "managed installation provenance requires installation_id and installation_revision"
        );
        ensure!(
            job.installation_revision
                .is_some_and(|revision| revision > 0),
            "installation_revision must be positive"
        );
        ensure!(
            job.installation_id
                .as_ref()
                .is_some_and(|id| !id.is_empty() && id.len() <= 128),
            "installation_id must be 1..=128 bytes"
        );
    }
    if let Some(profile) = &job.profile {
        ensure!(
            !profile.workload.trim().is_empty(),
            "profile workload must not be empty"
        );
        ensure!(
            (1..=100).contains(&profile.samples),
            "profile samples must be 1..=100"
        );
    }
    for (name, id) in [
        ("rollback_of_deployment_id", job.rollback_of_deployment_id),
        ("recovery_of_deployment_id", job.recovery_of_deployment_id),
    ] {
        ensure!(id.is_none_or(|value| value > 0), "{name} must be positive");
    }
    ensure!(
        (job.rollback_of_deployment_id.is_none() && job.recovery_of_deployment_id.is_none())
            || (job.installation_id.is_some() && job.installation_revision.is_some()),
        "rollback/recovery lineage requires managed installation provenance"
    );
    ensure!(
        !job.approval || job.environment.is_some(),
        "approval requires an environment"
    );
    if let Some(environment) = &job.environment {
        validate_name("environment", environment)?;
    }
    if let Some(shell) = &job.shell {
        ensure!(
            matches!(shell.as_str(), "sh" | "cmd" | "powershell"),
            "unsupported shell {shell}"
        );
    }
    if let Some(platforms) = job.matrix.get("platform") {
        for platform in platforms {
            validate_platform(platform)?;
        }
    }
    ensure!(
        (1..=14_400).contains(&job.timeout),
        "timeout must be 1..=14400 seconds"
    );
    ensure!(job.artifacts.len() <= 64, "too many artifact patterns");
    ensure!(job.cache.len() <= 16, "too many cache paths");
    for path in &job.artifacts {
        validate_relative_path(path, true)?;
    }
    for path in &job.cache {
        validate_relative_path(path, false)?;
    }
    for secret in &job.secrets {
        ensure!(valid_env_name(secret), "invalid secret name: {secret}");
    }
    for branch in &job.branches {
        validate_ref(branch)?;
    }
    commands(&job.run)?;
    Ok(())
}

fn commands(value: &Commands) -> Result<String> {
    let values = match value {
        Commands::One(value) => vec![value.as_str()],
        Commands::Many(values) => values.iter().map(String::as_str).collect(),
    };
    ensure!(!values.is_empty(), "run must not be empty");
    ensure!(
        values.iter().all(|value| !value.trim().is_empty()),
        "run command must not be empty"
    );
    let joined = values.join("\n");
    ensure!(
        joined.len() <= MAX_COMMAND_BYTES,
        "run command is too large"
    );
    Ok(joined)
}

fn expand_matrix(matrix: &BTreeMap<String, Vec<String>>) -> Result<Vec<BTreeMap<String, String>>> {
    let mut variants = vec![BTreeMap::new()];
    for (key, values) in matrix {
        validate_name("matrix key", key)?;
        ensure!(!values.is_empty(), "matrix axis {key} is empty");
        ensure!(
            values.len() <= MAX_VARIANTS,
            "matrix axis {key} is too large"
        );
        let mut next = Vec::new();
        for variant in &variants {
            for value in values {
                ensure!(
                    !value.is_empty() && value.len() <= 128,
                    "invalid matrix value"
                );
                let mut item = variant.clone();
                item.insert(key.clone(), value.clone());
                next.push(item);
                ensure!(
                    next.len() <= MAX_VARIANTS,
                    "matrix expands beyond {MAX_VARIANTS} jobs"
                );
            }
        }
        variants = next;
    }
    Ok(variants)
}

fn validate_dag(graph: &BTreeMap<String, Vec<String>>) -> Result<()> {
    let mut done = BTreeSet::new();
    while done.len() < graph.len() {
        let before = done.len();
        for (name, needs) in graph {
            if needs.iter().all(|need| done.contains(need)) {
                done.insert(name.clone());
            }
        }
        if done.len() == before {
            bail!("pipeline contains a dependency cycle");
        }
    }
    Ok(())
}

pub fn validate_ref(value: &str) -> Result<()> {
    ensure!(!value.is_empty() && value.len() <= 200, "invalid Git ref");
    ensure!(
        !value.starts_with('-') && !value.starts_with('/') && !value.ends_with('/'),
        "invalid Git ref"
    );
    ensure!(
        value != "@"
            && !value.contains("..")
            && !value.contains("@{")
            && !value.contains("//")
            && !value.contains('\\')
            && !value.ends_with('.'),
        "invalid Git ref"
    );
    ensure!(
        value.split('/').all(|component| {
            !component.starts_with('.')
                && !component.ends_with(".lock")
                && component
                    .bytes()
                    .all(|byte| byte.is_ascii_alphanumeric() || b"._-".contains(&byte))
        }),
        "invalid Git ref"
    );
    Ok(())
}

pub fn validate_repo_name(name: &str) -> Result<()> {
    ensure!(
        !name.is_empty()
            && name.len() <= 64
            && name
                .bytes()
                .all(|byte| byte.is_ascii_alphanumeric() || b"._-".contains(&byte)),
        "invalid repository name"
    );
    Ok(())
}

pub fn validate_repository(name: &str, url: &str, branch: &str) -> Result<()> {
    validate_repo_name(name)?;
    ensure!(
        !url.starts_with('-') && url.len() <= 2048,
        "invalid repository URL"
    );
    ensure!(
        Path::new(url).is_absolute()
            || url.starts_with("https://")
            || url.starts_with("ssh://")
            || url.starts_with("git@"),
        "repository must use HTTPS, SSH, or an absolute local path"
    );
    validate_ref(branch)
}

pub fn validate_commit(value: &str) -> Result<()> {
    ensure!(
        value.len() == 40 || value.len() == 64,
        "commit must be a full hexadecimal object id"
    );
    ensure!(
        value.bytes().all(|byte| byte.is_ascii_hexdigit()),
        "invalid commit"
    );
    Ok(())
}

fn validate_relative_path(value: &str, allow_glob: bool) -> Result<()> {
    ensure!(
        !value.is_empty() && value.len() <= 512,
        "invalid relative path"
    );
    ensure!(
        !value.contains(['\\', ':', '\0']) && (allow_glob || !value.contains(['*', '?', '[', ']'])),
        "invalid path syntax"
    );
    let plain = value.replace(['*', '?', '[', ']'], "x");
    ensure!(
        Path::new(&plain)
            .components()
            .all(|component| matches!(component, Component::Normal(_))),
        "path must stay inside the workspace"
    );
    Ok(())
}

fn validate_platform(value: &str) -> Result<()> {
    let mut parts = value.split('/');
    let os = parts.next().unwrap_or_default();
    let architecture = parts.next().unwrap_or_default();
    ensure!(
        os == "linux"
            && !architecture.is_empty()
            && parts.next().is_none()
            && [os, architecture].into_iter().all(|part| {
                part.bytes().all(|byte| {
                    byte.is_ascii_lowercase()
                        || byte.is_ascii_digit()
                        || matches!(byte, b'_' | b'-')
                })
            }),
        "invalid Docker platform: {value}"
    );
    Ok(())
}

fn validate_name(label: &str, value: &str) -> Result<()> {
    ensure!(!value.is_empty() && value.len() <= 64, "invalid {label}");
    ensure!(
        value
            .bytes()
            .all(|byte| byte.is_ascii_alphanumeric() || b"._-".contains(&byte)),
        "invalid {label}: {value}"
    );
    Ok(())
}

fn valid_env_name(value: &str) -> bool {
    let mut bytes = value.bytes();
    matches!(bytes.next(), Some(b'A'..=b'Z') | Some(b'_'))
        && bytes.all(|byte| byte.is_ascii_uppercase() || byte.is_ascii_digit() || byte == b'_')
}

#[cfg(test)]
mod tests {
    use super::*;

    // ── parse ────────────────────────────────────────────────────────

    const MINIMAL: &str =
        "version: 1\njobs:\n  - name: build\n    image: alpine:3\n    run: echo ok\n";

    #[test]
    fn parses_minimal_valid_pipeline() -> Result<()> {
        let jobs = parse(MINIMAL)?;
        assert_eq!(jobs.len(), 1);
        let job = &jobs[0];
        assert_eq!(job.name, "build");
        assert_eq!(job.executor, "docker");
        assert_eq!(job.image.as_deref(), Some("alpine:3"));
        assert_eq!(job.command, "echo ok");
        assert!(job.cache.is_empty());
        assert!(job.artifacts.is_empty());
        Ok(())
    }

    #[test]
    fn parses_native_executor() -> Result<()> {
        let jobs = parse(
            "version: 1\njobs:\n  - name: lint\n    executor: native\n    run: cargo fmt --check\n",
        )?;
        assert_eq!(jobs[0].executor, "native");
        assert!(jobs[0].image.is_none());
        Ok(())
    }

    #[test]
    fn expands_matrix_and_defaults_to_linear_dependencies() -> Result<()> {
        let jobs = parse(
            "version: 1\njobs:\n  - name: test\n    image: rust:1\n    run: cargo test\n    matrix:\n      platform: [linux/amd64, linux/arm64]\n  - name: package\n    image: rust:1\n    run: cargo build\n",
        )?;
        assert_eq!(jobs.len(), 3);
        assert_eq!(jobs[2].needs, ["test"]);
        Ok(())
    }

    #[test]
    fn explicit_empty_needs_overrides_linear_default() -> Result<()> {
        let jobs = parse(
            "version: 1\njobs:\n  - name: build\n    image: rust:1\n    run: cargo build\n  - name: lint\n    needs: []\n    image: rust:1\n    run: cargo fmt --check\n",
        )?;
        assert!(jobs[1].needs.is_empty());
        Ok(())
    }

    #[test]
    fn repository_pipeline_is_valid() -> Result<()> {
        let jobs = parse(include_str!("../.akurai.yml"))?;
        assert_eq!(jobs.len(), 3);
        let deploy = &jobs[2];
        assert_eq!(deploy.name, "deploy-production");
        assert_eq!(deploy.needs, ["package"]);
        assert_eq!(deploy.environment.as_deref(), Some("production"));
        assert!(
            !deploy.approval,
            "deploy-production runs unattended once verify+package pass"
        );
        Ok(())
    }

    #[test]
    fn parse_rejects_missing_version() {
        assert!(parse("jobs:\n  - name: build\n    image: alpine:3\n    run: echo ok\n").is_err());
    }

    #[test]
    fn parse_rejects_wrong_version() {
        assert!(
            parse("version: 2\njobs:\n  - name: build\n    image: alpine:3\n    run: echo ok\n")
                .is_err()
        );
    }

    #[test]
    fn parse_rejects_empty_jobs() {
        assert!(parse("version: 1\njobs: []\n").is_err());
        assert!(parse("version: 1\n").is_err());
    }

    #[test]
    fn parse_rejects_too_many_jobs() {
        let mut yaml = String::from("version: 1\njobs:\n");
        for i in 0..65 {
            yaml.push_str(&format!(
                "  - name: job{i}\n    image: alpine:3\n    run: echo ok\n"
            ));
        }
        assert!(parse(&yaml).is_err());
    }

    #[test]
    fn parse_rejects_missing_job_name() {
        assert!(parse("version: 1\njobs:\n  - image: alpine:3\n    run: echo ok\n").is_err());
    }

    #[test]
    fn parse_rejects_missing_run() {
        assert!(parse("version: 1\njobs:\n  - name: build\n    image: alpine:3\n").is_err());
    }

    #[test]
    fn parse_rejects_empty_run() {
        assert!(
            parse("version: 1\njobs:\n  - name: build\n    image: alpine:3\n    run: \"\"\n")
                .is_err()
        );
        assert!(
            parse(
                "version: 1\njobs:\n  - name: build\n    image: alpine:3\n    run:\n      - \"\"\n"
            )
            .is_err()
        );
    }

    #[test]
    fn parse_rejects_duplicate_job_names() {
        let source = "version: 1\njobs:\n  - name: build\n    image: alpine:3\n    run: echo a\n  - name: build\n    image: alpine:3\n    run: echo b\n";
        assert!(parse(source).is_err());
    }

    #[test]
    fn parse_rejects_self_dependency() {
        let source = "version: 1\njobs:\n  - name: build\n    image: alpine:3\n    run: echo a\n    needs: [build]\n";
        assert!(parse(source).is_err());
    }

    #[test]
    fn parse_rejects_unknown_dependency() {
        let source = "version: 1\njobs:\n  - name: build\n    image: alpine:3\n    run: echo a\n    needs: [nonexistent]\n";
        assert!(parse(source).is_err());
    }

    #[test]
    fn parse_rejects_invalid_executor() {
        let source = "version: 1\njobs:\n  - name: build\n    executor: podman\n    image: alpine:3\n    run: echo ok\n";
        assert!(parse(source).is_err());
    }

    #[test]
    fn parse_rejects_native_with_image() {
        let source = "version: 1\njobs:\n  - name: build\n    executor: native\n    image: alpine:3\n    run: echo ok\n";
        assert!(parse(source).is_err());
    }

    #[test]
    fn parse_rejects_docker_without_image() {
        let source = "version: 1\njobs:\n  - name: build\n    executor: docker\n    run: echo ok\n";
        assert!(parse(source).is_err());
    }

    #[test]
    fn parse_rejects_approval_without_environment() {
        let source = "version: 1\njobs:\n  - name: build\n    image: alpine:3\n    run: echo ok\n    approval: true\n";
        assert!(parse(source).is_err());
    }

    #[test]
    fn parse_accepts_approval_with_environment() -> Result<()> {
        let source = "version: 1\njobs:\n  - name: deploy\n    image: alpine:3\n    run: echo ok\n    approval: true\n    environment: production\n";
        let jobs = parse(source)?;
        assert!(jobs[0].approval);
        assert_eq!(jobs[0].environment.as_deref(), Some("production"));
        Ok(())
    }

    #[test]
    fn parse_rejects_cycles_traversal_and_unknown_fields() {
        for source in [
            "version: 1\njobs:\n- name: a\n  needs: [b]\n  image: x\n  run: x\n- name: b\n  needs: [a]\n  image: x\n  run: x\n",
            "version: 1\njobs:\n- name: a\n  image: x\n  run: x\n  artifacts: [../secret]\n",
            "version: 1\njobs:\n- name: a\n  image: x\n  run: x\n  magic: true\n",
            "version: 1\njobs:\n- name: a\n  image: x\n  run: x\n  approval: true\n",
            "version: 1\njobs:\n- name: a\n  image: x\n  run: x\n  cache: ['cache/**']\n",
            "version: 1\njobs:\n- name: a\n  image: x\n  run: x\n  matrix:\n    platform: ['--privileged']\n",
        ] {
            assert!(parse(source).is_err(), "expected error for: {source}");
        }
    }

    // ── validate_repo_name ───────────────────────────────────────────

    #[test]
    fn repo_name_accepts_valid() -> Result<()> {
        for name in [
            "app",
            "my-app",
            "app_v2",
            "my.repo",
            "a",
            "123",
            "_underscore",
        ] {
            validate_repo_name(name).with_context(|| format!("should accept {name:?}"))?;
        }
        Ok(())
    }

    #[test]
    fn repo_name_rejects_empty() {
        assert!(validate_repo_name("").is_err());
    }

    #[test]
    fn repo_name_rejects_path_traversal() {
        for name in ["../escape", "./hidden", "sub/dir", "/etc", "a\\b"] {
            assert!(validate_repo_name(name).is_err(), "should reject {name:?}");
        }
    }

    #[test]
    fn repo_name_rejects_control_chars() {
        for name in ["a\nb", "tab\there", "null\0byte"] {
            assert!(
                validate_repo_name(name).is_err(),
                "should reject control chars in {name:?}"
            );
        }
    }

    #[test]
    fn repo_name_rejects_over_length() {
        let long = "a".repeat(65);
        assert!(validate_repo_name(&long).is_err());
    }

    #[test]
    fn repo_name_rejects_shell_injection() {
        for name in ["$(whoami)", "`id`", "a;rm -rf /", "a|cat /etc/passwd"] {
            assert!(validate_repo_name(name).is_err(), "should reject {name:?}");
        }
    }

    // ── validate_ref ─────────────────────────────────────────────────

    #[test]
    fn ref_accepts_valid() -> Result<()> {
        for value in [
            "main",
            "refs/heads/main",
            "feature/branch",
            "v1.0.0",
            "fix-123",
            "a",
            "release/2024.01",
        ] {
            validate_ref(value).with_context(|| format!("should accept {value:?}"))?;
        }
        Ok(())
    }

    #[test]
    fn ref_rejects_empty() {
        assert!(validate_ref("").is_err());
    }

    #[test]
    fn ref_rejects_leading_slash() {
        assert!(validate_ref("/etc").is_err());
    }

    #[test]
    fn ref_rejects_leading_dash() {
        assert!(validate_ref("-f").is_err());
    }

    #[test]
    fn ref_rejects_trailing_slash() {
        assert!(validate_ref("branch/").is_err());
    }

    #[test]
    fn ref_rejects_dotdot() {
        assert!(validate_ref("a..b").is_err());
        assert!(validate_ref("../escape").is_err());
    }

    #[test]
    fn ref_rejects_at_brace() {
        assert!(validate_ref("ref@{upstream}").is_err());
    }

    #[test]
    fn ref_rejects_backslash() {
        assert!(validate_ref("a\\b").is_err());
    }

    #[test]
    fn ref_rejects_control_chars() {
        for value in ["a\nb", "tab\there"] {
            assert!(validate_ref(value).is_err(), "should reject {value:?}");
        }
    }

    #[test]
    fn ref_rejects_over_length() {
        let long = "a".repeat(201);
        assert!(validate_ref(&long).is_err());
    }

    #[test]
    fn ref_rejects_git_forbidden_forms() {
        for value in ["@", "a//b", ".hidden/main", "main.lock", "main."] {
            assert!(validate_ref(value).is_err(), "should reject {value:?}");
        }
    }

    #[test]
    fn ref_rejects_shell_injection() {
        for value in ["$(whoami)", "`id`", "a;rm -rf /"] {
            assert!(validate_ref(value).is_err(), "should reject {value:?}");
        }
    }
}