#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
use std::path::PathBuf;
use serde_json::json;
use speccoder_core::snapshot::Severity;
use speccoder_runtime::tools::policy::{Decision, Policy};
const SUPPORTED: [&str; 6] = ["0.13.4", "0.15.2", "0.16.5", "1.0.13", "1.1.3", "1.2.3"];
fn versions() -> Vec<String> {
let mut v: Vec<String> = SUPPORTED.iter().map(|s| (*s).to_owned()).collect();
if let Ok(extra) = std::env::var("SPECCODER_COMPAT_VERSIONS") {
v.extend(
extra
.split(',')
.map(|s| s.trim().to_owned())
.filter(|s| !s.is_empty()),
);
}
v.sort();
v.dedup();
v
}
fn fixture(version: &str) -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../speccoder-speckit/tests/fixtures/compat")
.join(version)
}
#[test]
fn every_supported_release_has_a_fixture() {
for v in versions() {
let v = v.as_str();
let f = fixture(v);
assert_eq!(
std::fs::read_to_string(f.join("VERSION")).unwrap().trim(),
v
);
let opts = std::fs::read_to_string(f.join(".specify/init-options.json")).unwrap();
assert!(
opts.contains(&format!("\"speckit_version\": \"{v}\"")),
"{v}: {opts}"
);
}
}
#[test]
fn inspect_reads_each_releases_project_and_its_own_task_template() {
for v in versions() {
let v = v.as_str();
let snap =
speccoder_speckit::inspect(&fixture(v), None).unwrap_or_else(|e| panic!("{v}: {e:?}"));
let errors: Vec<_> = snap
.diagnostics
.iter()
.filter(|d| d.severity == Severity::Error)
.map(|d| d.code.clone())
.collect();
assert!(errors.is_empty(), "{v}: {errors:?}");
let f = snap
.feature
.as_ref()
.unwrap_or_else(|| panic!("{v}: no feature selected"));
assert_eq!(f.feature.name, "001-sample", "{v}");
let tasks = f
.tasks
.as_ref()
.unwrap_or_else(|| panic!("{v}: no tasks.md"));
assert!(
tasks.tasks.len() >= 5,
"{v}: only {} tasks parsed from the template",
tasks.tasks.len()
);
assert!(
tasks.tasks.iter().any(|t| t.story.is_some()),
"{v}: no [USn] labels parsed"
);
assert!(
f.schedule.as_ref().is_some_and(|s| s
.tasks
.iter()
.any(|t| t.state == speccoder_core::snapshot::TaskState::Ready)),
"{v}: nothing ready"
);
}
}
#[test]
fn each_releases_skills_are_hosted_and_their_scripts_allowed() {
let planning = Policy::new(&[], &[]).authoring();
for v in versions() {
let v = v.as_str();
let root = fixture(v);
let skills = speccoder_runtime::skills::discover(&root);
for name in [
"speckit-specify",
"speckit-plan",
"speckit-tasks",
"speckit-clarify",
"speckit-analyze",
] {
let s = skills
.iter()
.find(|s| s.name == name)
.unwrap_or_else(|| panic!("{v}: no {name}"));
assert!(s.refused.is_none(), "{v}: {name} refused");
let body = speccoder_runtime::skills::instruction(&root, s, "ARGS-HERE").unwrap();
assert!(
body.contains("ARGS-HERE") && !body.contains("$ARGUMENTS"),
"{v}: {name} has no $ARGUMENTS slot"
);
}
for name in ["speckit-implement", "speckit-converge"] {
assert!(
skills
.iter()
.find(|s| s.name == name)
.is_some_and(|s| s.refused.is_some()),
"{v}: {name} hosted"
);
}
for script in ["check-prerequisites.sh", "setup-plan.sh", "common.sh"] {
let rel = format!(".specify/scripts/bash/{script}");
assert!(root.join(&rel).is_file(), "{v}: no {rel}");
assert_eq!(
planning.classify(
"run_command",
&json!({ "argv": ["bash", rel, "--json"] }),
None
),
Decision::Allow,
"{v}"
);
}
for s in &skills {
let text = std::fs::read_to_string(root.join(&s.path)).unwrap();
for word in text.split(|c: char| c.is_whitespace() || c == '`' || c == '"') {
if let Some(rel) = word
.strip_prefix(".specify/scripts/bash/")
.filter(|w| w.ends_with(".sh"))
{
assert!(
root.join(".specify/scripts/bash").join(rel).is_file(),
"{v}: {} names a missing script {rel}",
s.name
);
}
}
}
}
}