use crate::archetype::detect_archetype;
use tokmd_types::{ChildIncludeMode, ExportData, FileKind, FileRow};
fn export_with_paths(paths: &[&str]) -> ExportData {
let rows = paths
.iter()
.map(|p| FileRow {
path: (*p).to_string(),
module: "(root)".to_string(),
lang: "Rust".to_string(),
kind: FileKind::Parent,
code: 1,
comments: 0,
blanks: 0,
lines: 1,
bytes: 10,
tokens: 2,
})
.collect();
ExportData {
rows,
module_roots: vec![],
module_depth: 2,
children: ChildIncludeMode::Separate,
}
}
#[test]
fn given_empty_repo_then_no_archetype() {
let export = export_with_paths(&[]);
assert!(detect_archetype(&export).is_none());
}
#[test]
fn given_only_readme_then_no_archetype() {
let export = export_with_paths(&["README.md"]);
assert!(detect_archetype(&export).is_none());
}
#[test]
fn given_generic_source_files_then_no_archetype() {
let export = export_with_paths(&["src/lib.rs", "src/utils.rs", "Makefile"]);
assert!(detect_archetype(&export).is_none());
}
#[test]
fn given_cargo_toml_and_crates_dir_then_rust_workspace() {
let export = export_with_paths(&["Cargo.toml", "crates/core/src/lib.rs"]);
let a = detect_archetype(&export).unwrap();
assert_eq!(a.kind, "Rust workspace");
assert!(a.evidence.contains(&"Cargo.toml".to_string()));
}
#[test]
fn given_cargo_toml_and_packages_dir_then_rust_workspace() {
let export = export_with_paths(&["Cargo.toml", "packages/util/src/lib.rs"]);
let a = detect_archetype(&export).unwrap();
assert_eq!(a.kind, "Rust workspace");
assert!(a.evidence.iter().any(|e| e.starts_with("packages/")));
}
#[test]
fn given_rust_workspace_with_main_rs_then_cli_variant() {
let export = export_with_paths(&["Cargo.toml", "crates/foo/src/lib.rs", "src/main.rs"]);
let a = detect_archetype(&export).unwrap();
assert_eq!(a.kind, "Rust workspace (CLI)");
}
#[test]
fn given_rust_workspace_with_bin_dir_then_cli_variant() {
let export = export_with_paths(&[
"Cargo.toml",
"crates/foo/src/lib.rs",
"crates/foo/src/bin/runner.rs",
]);
let a = detect_archetype(&export).unwrap();
assert_eq!(a.kind, "Rust workspace (CLI)");
}
#[test]
fn given_cargo_toml_without_workspace_dir_then_no_rust_workspace() {
let export = export_with_paths(&["Cargo.toml", "src/lib.rs"]);
assert!(
detect_archetype(&export).is_none(),
"plain Cargo.toml without crates/ or packages/ must not match Rust workspace"
);
}
#[test]
fn given_package_json_and_next_config_js_then_nextjs() {
let export = export_with_paths(&["package.json", "next.config.js"]);
let a = detect_archetype(&export).unwrap();
assert_eq!(a.kind, "Next.js app");
assert!(a.evidence.contains(&"package.json".to_string()));
}
#[test]
fn given_package_json_and_next_config_mjs_then_nextjs() {
let export = export_with_paths(&["package.json", "next.config.mjs"]);
let a = detect_archetype(&export).unwrap();
assert_eq!(a.kind, "Next.js app");
}
#[test]
fn given_package_json_and_next_config_ts_then_nextjs() {
let export = export_with_paths(&["package.json", "next.config.ts"]);
let a = detect_archetype(&export).unwrap();
assert_eq!(a.kind, "Next.js app");
}
#[test]
fn given_nested_next_config_then_nextjs() {
let export = export_with_paths(&["package.json", "apps/web/next.config.js"]);
let a = detect_archetype(&export).unwrap();
assert_eq!(a.kind, "Next.js app");
assert!(a.evidence.iter().any(|e| e == "apps/web/next.config.js"));
}
#[test]
fn given_next_config_without_package_json_then_no_nextjs() {
let export = export_with_paths(&["next.config.js"]);
assert!(detect_archetype(&export).is_none());
}
#[test]
fn given_dockerfile_and_k8s_dir_then_containerized_service() {
let export = export_with_paths(&["Dockerfile", "k8s/deployment.yaml"]);
let a = detect_archetype(&export).unwrap();
assert_eq!(a.kind, "Containerized service");
assert!(a.evidence.contains(&"Dockerfile".to_string()));
}
#[test]
fn given_dockerfile_and_kubernetes_dir_then_containerized_service() {
let export = export_with_paths(&["Dockerfile", "kubernetes/pod.yaml"]);
let a = detect_archetype(&export).unwrap();
assert_eq!(a.kind, "Containerized service");
}
#[test]
fn given_dockerfile_without_k8s_then_no_containerized_service() {
let export = export_with_paths(&["Dockerfile", "src/main.rs"]);
assert!(detect_archetype(&export).is_none());
}
#[test]
fn given_k8s_without_dockerfile_then_no_containerized_service() {
let export = export_with_paths(&["k8s/deployment.yaml"]);
assert!(detect_archetype(&export).is_none());
}
#[test]
fn given_tf_file_then_iac() {
let export = export_with_paths(&["main.tf", "variables.tf"]);
let a = detect_archetype(&export).unwrap();
assert_eq!(a.kind, "Infrastructure as code");
}
#[test]
fn given_terraform_dir_then_iac() {
let export = export_with_paths(&["terraform/main.tf"]);
let a = detect_archetype(&export).unwrap();
assert_eq!(a.kind, "Infrastructure as code");
}
#[test]
fn given_no_tf_files_then_no_iac() {
let export = export_with_paths(&["cloudformation.yaml"]);
assert!(detect_archetype(&export).is_none());
}
#[test]
fn given_pyproject_toml_then_python_package() {
let export = export_with_paths(&["pyproject.toml", "src/main.py"]);
let a = detect_archetype(&export).unwrap();
assert_eq!(a.kind, "Python package");
assert!(a.evidence.contains(&"pyproject.toml".to_string()));
}
#[test]
fn given_setup_py_without_pyproject_then_no_python_package() {
let export = export_with_paths(&["setup.py", "src/main.py"]);
assert!(detect_archetype(&export).is_none());
}
#[test]
fn given_only_package_json_then_node_package() {
let export = export_with_paths(&["package.json", "src/index.js"]);
let a = detect_archetype(&export).unwrap();
assert_eq!(a.kind, "Node package");
assert!(a.evidence.contains(&"package.json".to_string()));
}
#[test]
fn rust_workspace_beats_node_package() {
let export = export_with_paths(&["Cargo.toml", "crates/core/src/lib.rs", "package.json"]);
let a = detect_archetype(&export).unwrap();
assert!(
a.kind.starts_with("Rust workspace"),
"Rust workspace should take priority over Node package, got: {}",
a.kind
);
}
#[test]
fn rust_workspace_beats_containerized_service() {
let export = export_with_paths(&[
"Cargo.toml",
"crates/core/src/lib.rs",
"Dockerfile",
"k8s/deploy.yaml",
]);
let a = detect_archetype(&export).unwrap();
assert!(a.kind.starts_with("Rust workspace"));
}
#[test]
fn nextjs_beats_plain_node_package() {
let export = export_with_paths(&["package.json", "next.config.js", "src/index.ts"]);
let a = detect_archetype(&export).unwrap();
assert_eq!(a.kind, "Next.js app");
}
#[test]
fn containerized_beats_iac_when_both_present() {
let export = export_with_paths(&["Dockerfile", "k8s/pod.yaml", "main.tf"]);
let a = detect_archetype(&export).unwrap();
assert_eq!(a.kind, "Containerized service");
}
#[test]
fn iac_beats_python_when_both_present() {
let export = export_with_paths(&["main.tf", "pyproject.toml"]);
let a = detect_archetype(&export).unwrap();
assert_eq!(a.kind, "Infrastructure as code");
}
#[test]
fn python_beats_node_when_both_present() {
let export = export_with_paths(&["pyproject.toml", "package.json"]);
let a = detect_archetype(&export).unwrap();
assert_eq!(a.kind, "Python package");
}
#[test]
fn given_child_rows_only_then_no_archetype() {
let rows = vec![FileRow {
path: "Cargo.toml".to_string(),
module: "(root)".to_string(),
lang: "TOML".to_string(),
kind: FileKind::Child,
code: 10,
comments: 0,
blanks: 0,
lines: 10,
bytes: 100,
tokens: 20,
}];
let export = ExportData {
rows,
module_roots: vec![],
module_depth: 2,
children: ChildIncludeMode::Separate,
};
assert!(
detect_archetype(&export).is_none(),
"Child FileKind rows should be filtered out"
);
}
#[test]
fn given_mix_of_parent_and_child_rows_then_only_parents_considered() {
let rows = vec![
FileRow {
path: "Cargo.toml".to_string(),
module: "(root)".to_string(),
lang: "TOML".to_string(),
kind: FileKind::Parent,
code: 10,
comments: 0,
blanks: 0,
lines: 10,
bytes: 100,
tokens: 20,
},
FileRow {
path: "crates/core/src/lib.rs".to_string(),
module: "core".to_string(),
lang: "Rust".to_string(),
kind: FileKind::Child,
code: 50,
comments: 5,
blanks: 3,
lines: 58,
bytes: 500,
tokens: 100,
},
];
let export = ExportData {
rows,
module_roots: vec![],
module_depth: 2,
children: ChildIncludeMode::Separate,
};
assert!(detect_archetype(&export).is_none());
}
#[test]
fn given_backslash_paths_then_normalized_to_forward_slash() {
let rows = vec![
FileRow {
path: "Cargo.toml".to_string(),
module: "(root)".to_string(),
lang: "TOML".to_string(),
kind: FileKind::Parent,
code: 1,
comments: 0,
blanks: 0,
lines: 1,
bytes: 10,
tokens: 2,
},
FileRow {
path: "crates\\core\\src\\lib.rs".to_string(),
module: "core".to_string(),
lang: "Rust".to_string(),
kind: FileKind::Parent,
code: 1,
comments: 0,
blanks: 0,
lines: 1,
bytes: 10,
tokens: 2,
},
];
let export = ExportData {
rows,
module_roots: vec![],
module_depth: 2,
children: ChildIncludeMode::Separate,
};
let a = detect_archetype(&export).unwrap();
assert!(
a.kind.starts_with("Rust workspace"),
"backslash paths should be normalized; got: {:?}",
a.kind
);
}
#[test]
fn evidence_is_never_empty_when_archetype_detected() {
let cases: Vec<Vec<&str>> = vec![
vec!["Cargo.toml", "crates/a/src/lib.rs"],
vec!["package.json", "next.config.js"],
vec!["Dockerfile", "k8s/pod.yaml"],
vec!["main.tf"],
vec!["pyproject.toml"],
vec!["package.json"],
];
for paths in &cases {
let export = export_with_paths(paths);
if let Some(a) = detect_archetype(&export) {
assert!(
!a.evidence.is_empty(),
"evidence must not be empty for kind={:?} with paths={:?}",
a.kind,
paths
);
}
}
}
#[test]
fn kind_is_always_a_known_archetype() {
let known = [
"Rust workspace",
"Rust workspace (CLI)",
"Next.js app",
"Containerized service",
"Infrastructure as code",
"Python package",
"Node package",
];
let cases: Vec<Vec<&str>> = vec![
vec!["Cargo.toml", "crates/a/src/lib.rs"],
vec!["Cargo.toml", "crates/a/src/lib.rs", "src/main.rs"],
vec!["package.json", "next.config.js"],
vec!["Dockerfile", "k8s/pod.yaml"],
vec!["main.tf"],
vec!["pyproject.toml"],
vec!["package.json"],
];
for paths in &cases {
let export = export_with_paths(paths);
if let Some(a) = detect_archetype(&export) {
assert!(
known.contains(&a.kind.as_str()),
"unexpected kind {:?} for paths={:?}",
a.kind,
paths
);
}
}
}
#[test]
fn given_same_input_when_detect_called_twice_then_same_result() {
let export = export_with_paths(&["Cargo.toml", "crates/core/src/lib.rs", "src/main.rs"]);
let a1 = detect_archetype(&export);
let a2 = detect_archetype(&export);
assert_eq!(a1.as_ref().map(|a| &a.kind), a2.as_ref().map(|a| &a.kind));
assert_eq!(
a1.as_ref().map(|a| &a.evidence),
a2.as_ref().map(|a| &a.evidence)
);
}
#[test]
fn given_only_dockerfile_then_no_archetype() {
let export = export_with_paths(&["Dockerfile"]);
assert!(
detect_archetype(&export).is_none(),
"Dockerfile alone should not match any archetype"
);
}
#[test]
fn given_single_tf_file_then_iac_detected() {
let export = export_with_paths(&["deploy.tf"]);
let a = detect_archetype(&export).unwrap();
assert_eq!(a.kind, "Infrastructure as code");
}
#[test]
fn given_rust_workspace_with_both_main_rs_and_bin_dir_then_cli_variant() {
let export = export_with_paths(&[
"Cargo.toml",
"crates/foo/src/lib.rs",
"src/main.rs",
"crates/foo/src/bin/tool.rs",
]);
let a = detect_archetype(&export).unwrap();
assert_eq!(a.kind, "Rust workspace (CLI)");
}
#[test]
fn given_any_archetype_evidence_paths_have_no_backslashes() {
let cases: Vec<Vec<&str>> = vec![
vec!["Cargo.toml", "crates/a/src/lib.rs"],
vec!["package.json", "next.config.js"],
vec!["Dockerfile", "k8s/pod.yaml"],
vec!["main.tf"],
vec!["pyproject.toml"],
vec!["package.json"],
];
for paths in &cases {
let export = export_with_paths(paths);
if let Some(a) = detect_archetype(&export) {
for ev in &a.evidence {
assert!(
!ev.contains('\\'),
"evidence path should not contain backslashes: {:?} for kind={:?}",
ev,
a.kind
);
}
}
}
}
#[test]
fn given_detected_archetype_kind_is_non_empty() {
let cases: Vec<Vec<&str>> = vec![
vec!["Cargo.toml", "crates/a/src/lib.rs"],
vec!["package.json", "next.config.mjs"],
vec!["Dockerfile", "kubernetes/deploy.yaml"],
vec!["terraform/main.tf"],
vec!["pyproject.toml"],
vec!["package.json", "src/index.js"],
];
for paths in &cases {
let export = export_with_paths(paths);
if let Some(a) = detect_archetype(&export) {
assert!(
!a.kind.is_empty(),
"kind must not be empty for paths={:?}",
paths
);
}
}
}
#[test]
fn given_all_markers_present_then_rust_workspace_wins() {
let export = export_with_paths(&[
"Cargo.toml",
"crates/core/src/lib.rs",
"package.json",
"next.config.js",
"Dockerfile",
"k8s/pod.yaml",
"main.tf",
"pyproject.toml",
]);
let a = detect_archetype(&export).unwrap();
assert!(
a.kind.starts_with("Rust workspace"),
"Rust workspace should win over all others, got: {}",
a.kind
);
}