main-spex 0.26.0

AI-first development framework that turns specifications into verified code
use std::fs;
use std::path::PathBuf;

use tempfile::TempDir;

use crate::common::{assert_fails_mentioning, dummy_project_fixture, run_spex, temp_project};

fn spex_dir() -> (TempDir, PathBuf) {
    let (temp_dir, project) = temp_project(&dummy_project_fixture());
    let spex = project.join("dummy-project-spex");
    (temp_dir, spex)
}

const BAD_ID_SPEC: &str = "id: Bad_ID
title: Bad spec
description: A spec whose id breaks the sibling spec-id schema.
acceptance_criteria:
  - id: valid-criterion
    must: do something
packages:
  - api
contract: none
";

const TITLE_ONLY_SPEC: &str = "title: Only a title\n";
const VALID_JSON_RULE: &str = r#"{"id":"json-rule","title":"Json rule","description":"A rule written as JSON.","acceptance_criteria":["MUST do a thing"]}"#;
const INCOMPLETE_JSON_RULE: &str = r#"{"id":"json-rule"}"#;

#[test]
fn check_schema_accepts_valid_files_without_output() {
    let (_temp_dir, spex) = spex_dir();

    let check = run_spex(
        &spex,
        &[
            "check-schema",
            "--schema",
            "schemas/spec.schema.json",
            "--files",
            "specs/planned-one.yaml",
            "specs/active-with-gap.yaml",
        ],
        "",
    );

    assert!(check.status.success(), "check-schema should exit 0");
    assert!(check.stdout.is_empty());
    assert!(
        check.stderr.is_empty(),
        "stderr:\n{}",
        String::from_utf8_lossy(&check.stderr)
    );
}

#[test]
fn check_schema_reports_each_violation_per_file_and_exits_non_zero() {
    let (_temp_dir, spex) = spex_dir();
    fs::write(spex.join("bad-id.yaml"), BAD_ID_SPEC).expect("write bad-id spec");
    fs::write(spex.join("missing-fields.yaml"), TITLE_ONLY_SPEC).expect("write title-only spec");

    let check = run_spex(
        &spex,
        &[
            "check-schema",
            "--schema",
            "schemas/spec.schema.json",
            "--files",
            "specs/planned-one.yaml",
            "bad-id.yaml",
            "missing-fields.yaml",
        ],
        "",
    );

    assert_eq!(check.status.code(), Some(1));
    assert!(check.stdout.is_empty());
    assert_eq!(
        String::from_utf8_lossy(&check.stderr),
        concat!(
            "bad-id.yaml: /id: \"Bad_ID\" does not match \"^[a-z0-9]+(-[a-z0-9]+)*$\"\n",
            "missing-fields.yaml: \"acceptance_criteria\" is a required property\n",
            "missing-fields.yaml: \"contract\" is a required property\n",
            "missing-fields.yaml: \"description\" is a required property\n",
            "missing-fields.yaml: \"id\" is a required property\n",
            "missing-fields.yaml: \"packages\" is a required property\n",
        ),
    );
}

#[test]
fn check_schema_validates_json_files() {
    let (_temp_dir, spex) = spex_dir();
    fs::write(spex.join("valid-rule.json"), VALID_JSON_RULE).expect("write valid json");
    fs::write(spex.join("incomplete-rule.json"), INCOMPLETE_JSON_RULE).expect("write invalid json");

    let valid = run_spex(
        &spex,
        &[
            "check-schema",
            "--schema",
            "schemas/rule.schema.json",
            "--files",
            "valid-rule.json",
        ],
        "",
    );
    let invalid = run_spex(
        &spex,
        &[
            "check-schema",
            "--schema",
            "schemas/rule.schema.json",
            "--files",
            "incomplete-rule.json",
        ],
        "",
    );

    assert!(valid.status.success());
    assert!(valid.stderr.is_empty());
    assert_eq!(invalid.status.code(), Some(1));
    assert_eq!(
        String::from_utf8_lossy(&invalid.stderr),
        concat!(
            "incomplete-rule.json: \"acceptance_criteria\" is a required property\n",
            "incomplete-rule.json: \"description\" is a required property\n",
            "incomplete-rule.json: \"title\" is a required property\n",
        ),
    );
}

#[test]
fn check_schema_fails_when_a_file_or_the_schema_cannot_be_read() {
    let (_temp_dir, spex) = spex_dir();

    let missing_file = run_spex(
        &spex,
        &[
            "check-schema",
            "--schema",
            "schemas/spec.schema.json",
            "--files",
            "nope.yaml",
        ],
        "",
    );
    let missing_schema = run_spex(
        &spex,
        &[
            "check-schema",
            "--schema",
            "schemas/nope.json",
            "--files",
            "specs/planned-one.yaml",
        ],
        "",
    );

    assert_fails_mentioning(&missing_file, "could not read `nope.yaml`");
    assert_fails_mentioning(&missing_schema, "could not read `schemas/nope.json`");
}

#[test]
fn check_schema_requires_a_schema_and_files() {
    let (_temp_dir, spex) = spex_dir();

    let check = run_spex(&spex, &["check-schema"], "");

    assert_eq!(check.status.code(), Some(2));
    assert_fails_mentioning(&check, "--schema <SCHEMA>");
    assert_fails_mentioning(&check, "--files <FILES>...");
}