use ito_core::change_repository::FsChangeRepository;
use ito_core::module_repository::FsModuleRepository;
use ito_core::validate::{
validate_change, validate_module, validate_spec_markdown, validate_tasks_file,
};
use std::path::Path;
fn write(path: &Path, contents: &str) {
let Some(parent) = path.parent() else {
panic!("path has no parent: {}", path.display());
};
std::fs::create_dir_all(parent).unwrap();
std::fs::write(path, contents).unwrap();
}
#[test]
fn validate_spec_markdown_reports_missing_purpose_and_requirements() {
let md = r#"
## Purpose
## Requirements
"#;
let r = validate_spec_markdown(md, false);
assert!(!r.valid);
assert!(r.summary.errors >= 1);
}
#[test]
fn validate_spec_markdown_strict_treats_warnings_as_invalid() {
let md = r#"
## Purpose
Too short.
## Requirements
### Requirement: R
The system SHALL do it.
#### Scenario: S
ok
"#;
let non_strict = validate_spec_markdown(md, false);
assert!(non_strict.valid);
assert!(non_strict.summary.warnings >= 1);
let strict = validate_spec_markdown(md, true);
assert!(!strict.valid);
assert!(strict.summary.warnings >= 1);
}
#[test]
fn validate_spec_markdown_warns_on_delta_headings_in_main_specs() {
let md = r#"
# Auth
## Purpose
This current-truth spec describes authentication behavior for users.
## Requirements
### Requirement: Login
The system SHALL let users log in.
#### Scenario: Login succeeds
- **WHEN** valid credentials are provided
- **THEN** the user is authenticated
## ADDED Requirements
## MODIFIED Requirements
## REMOVED Requirements
## RENAMED Requirements
"#;
let expected_lines: Vec<u32> = md
.lines()
.enumerate()
.filter_map(|(idx, line)| {
matches!(
line.trim(),
"## ADDED Requirements"
| "## MODIFIED Requirements"
| "## REMOVED Requirements"
| "## RENAMED Requirements"
)
.then_some((idx + 1) as u32)
})
.collect();
let non_strict = validate_spec_markdown(md, false);
assert!(non_strict.valid);
let warning_lines: Vec<u32> = non_strict
.issues
.iter()
.filter(|issue| {
issue.level == "WARNING" && issue.message.contains("delta operation headings")
})
.filter_map(|issue| issue.line)
.collect();
assert_eq!(warning_lines, expected_lines);
let strict = validate_spec_markdown(md, true);
assert!(!strict.valid);
let error_lines: Vec<u32> = strict
.issues
.iter()
.filter(|issue| {
issue.level == "ERROR" && issue.message.contains("delta operation headings")
})
.filter_map(|issue| issue.line)
.collect();
assert_eq!(error_lines, expected_lines);
}
#[test]
fn validate_change_requires_at_least_one_delta() {
let td = tempfile::tempdir().unwrap();
let ito = td.path().join(".ito");
std::fs::create_dir_all(ito.join("changes").join("001-01_demo")).unwrap();
let change_repo = FsChangeRepository::new(&ito);
let r = validate_change(&change_repo, &ito, "001-01_demo", false).unwrap();
assert!(!r.valid);
assert!(
r.issues
.iter()
.any(|i| i.message.contains("at least one delta"))
);
}
#[test]
fn validate_change_requires_shall_or_must_in_requirement_text() {
let td = tempfile::tempdir().unwrap();
let ito = td.path().join(".ito");
let change_id = "001-01_demo";
let change_repo = FsChangeRepository::new(&ito);
write(
&ito.join("changes")
.join(change_id)
.join("specs")
.join("auth")
.join("spec.md"),
r#"
## ADDED Requirements
### Requirement: R
This requirement has no keywords.
#### Scenario: S
ok
"#,
);
let r = validate_change(&change_repo, &ito, change_id, false).unwrap();
assert!(!r.valid);
assert!(
r.issues
.iter()
.any(|i| i.message.contains("SHALL") || i.message.contains("MUST"))
);
}
#[test]
fn validate_change_with_unknown_schema_and_no_validation_yaml_does_not_require_deltas() {
let td = tempfile::tempdir().unwrap();
let project_root = td.path();
let ito = project_root.join(".ito");
let change_id = "001-01_demo";
write(
&project_root
.join(".ito")
.join("templates")
.join("schemas")
.join("unknown")
.join("schema.yaml"),
r#"
name: unknown
version: 1
artifacts:
- id: notes
generates: notes.md
template: notes.md
requires: []
apply:
requires: [notes]
"#,
);
write(
&ito.join("changes").join(change_id).join(".ito.yaml"),
"schema: unknown\n",
);
write(
&ito.join("changes").join(change_id).join("notes.md"),
"ok\n",
);
let change_repo = FsChangeRepository::new(&ito);
let r = validate_change(&change_repo, &ito, change_id, false).unwrap();
assert!(
!r.issues
.iter()
.any(|i| i.message.contains("at least one delta")),
"unknown schema should not require deltas, got issues: {:?}",
r.issues
);
assert!(
r.issues
.iter()
.any(|i| i.message.contains("manual validation required")),
"unknown schema should emit manual validation issue, got issues: {:?}",
r.issues
);
}
#[test]
fn validate_change_default_apply_requirements_ignore_optional_artifacts() {
let td = tempfile::tempdir().unwrap();
let project_root = td.path();
let ito = project_root.join(".ito");
let change_id = "001-01_demo";
write(
&project_root
.join(".ito")
.join("templates")
.join("schemas")
.join("demo")
.join("schema.yaml"),
r#"
name: demo
version: 1
artifacts:
- id: discovery
generates: domain-discovery.md
template: domain-discovery.md
optional: true
requires: []
- id: proposal
generates: proposal.md
template: proposal.md
requires: []
"#,
);
write(
&ito.join("changes").join(change_id).join(".ito.yaml"),
"schema: demo\n",
);
write(
&ito.join("changes").join(change_id).join("proposal.md"),
"# Proposal\n",
);
let change_repo = FsChangeRepository::new(&ito);
let r = validate_change(&change_repo, &ito, change_id, false).unwrap();
assert!(
!r.issues.iter().any(|i| {
i.path == "artifacts.discovery"
&& i.message
.contains("Apply-required artifact 'discovery' is missing")
}),
"optional artifact should not be apply-required by default, got issues: {:?}",
r.issues
);
}
#[test]
fn validate_change_uses_validation_yaml_delta_specs_validator_when_configured() {
let td = tempfile::tempdir().unwrap();
let project_root = td.path();
let ito = project_root.join(".ito");
let change_id = "001-01_demo";
write(
&project_root
.join(".ito")
.join("templates")
.join("schemas")
.join("delta")
.join("schema.yaml"),
r#"
name: delta
version: 1
artifacts:
- id: specs
generates: specs/**/*.md
template: specs/spec.md
requires: []
"#,
);
write(
&project_root
.join(".ito")
.join("templates")
.join("schemas")
.join("delta")
.join("validation.yaml"),
r#"
version: 1
artifacts:
specs:
required: true
validate_as: ito.delta-specs.v1
"#,
);
write(
&ito.join("changes").join(change_id).join(".ito.yaml"),
"schema: delta\n",
);
write(
&ito.join("changes")
.join(change_id)
.join("specs")
.join("auth")
.join("spec.md"),
r#"
## ADDED Requirements
### Requirement: R
No keywords here.
#### Scenario: S
ok
"#,
);
let change_repo = FsChangeRepository::new(&ito);
let r = validate_change(&change_repo, &ito, change_id, false).unwrap();
assert!(
r.issues
.iter()
.any(|i| i.message.contains("SHALL") || i.message.contains("MUST")),
"delta validator should run, got issues: {:?}",
r.issues
);
assert!(
!r.issues
.iter()
.any(|i| i.message.contains("manual validation required")),
"schema with validation.yaml should not emit manual-validation issue, got issues: {:?}",
r.issues
);
assert!(
r.issues
.iter()
.any(|i| i.message.contains("ito.delta-specs.v1")),
"expected delta spec issues to cite validator id, got issues: {:?}",
r.issues
);
let with_meta = r.issues.iter().any(|i| {
let Some(meta) = i.metadata.as_ref().and_then(|m| m.as_object()) else {
return false;
};
meta.get("validator_id").and_then(|v| v.as_str()) == Some("ito.delta-specs.v1")
&& meta.get("spec_path").and_then(|v| v.as_str())
== Some(".ito/specs/delta-specs/spec.md")
});
assert!(
with_meta,
"expected delta spec issues to include metadata references"
);
}
#[test]
fn validate_change_with_validation_yaml_and_no_delta_validator_does_not_require_deltas() {
let td = tempfile::tempdir().unwrap();
let project_root = td.path();
let ito = project_root.join(".ito");
let change_id = "001-01_demo";
write(
&project_root
.join(".ito")
.join("templates")
.join("schemas")
.join("nodeltas")
.join("schema.yaml"),
r#"
name: nodeltas
version: 1
artifacts:
- id: notes
generates: notes.md
template: notes.md
requires: []
"#,
);
write(
&project_root
.join(".ito")
.join("templates")
.join("schemas")
.join("nodeltas")
.join("validation.yaml"),
"version: 1\nmanual_semantic_validation_note: Schema semantics require manual validation.\n",
);
write(
&ito.join("changes").join(change_id).join(".ito.yaml"),
"schema: nodeltas\n",
);
let change_repo = FsChangeRepository::new(&ito);
let r = validate_change(&change_repo, &ito, change_id, false).unwrap();
assert!(
!r.issues
.iter()
.any(|i| i.message.contains("at least one delta")),
"schema validation without delta validator should not require deltas, got issues: {:?}",
r.issues
);
assert!(
r.issues.iter().any(|i| {
i.path == "schema.validation.manual"
&& i.message
.contains("Schema semantics require manual validation.")
}),
"schema validation should emit manual semantic validation note, got issues: {:?}",
r.issues
);
}
#[test]
fn validate_change_validates_apply_tracks_file_when_configured() {
let td = tempfile::tempdir().unwrap();
let project_root = td.path();
let ito = project_root.join(".ito");
let change_id = "001-01_demo";
write(
&project_root
.join(".ito")
.join("templates")
.join("schemas")
.join("tracks")
.join("schema.yaml"),
r#"
name: tracks
version: 1
artifacts:
- id: proposal
generates: proposal.md
template: proposal.md
requires: []
apply:
tracks: todo.md
"#,
);
write(
&project_root
.join(".ito")
.join("templates")
.join("schemas")
.join("tracks")
.join("validation.yaml"),
r#"
version: 1
tracking:
source: apply_tracks
required: true
validate_as: ito.tasks-tracking.v1
"#,
);
write(
&ito.join("changes").join(change_id).join(".ito.yaml"),
"schema: tracks\n",
);
write(
&ito.join("changes").join(change_id).join("todo.md"),
r#"
# Tasks for: 001-01_demo
## Wave 1
### Task 1.1: Demo
- **Dependencies**: Task 2.1
- **Status**: [ ] pending
"#,
);
let change_repo = FsChangeRepository::new(&ito);
let r = validate_change(&change_repo, &ito, change_id, false).unwrap();
assert!(
!r.issues.iter().any(|i| i.message.contains("tasks.md")),
"apply.tracks validation should not require tasks.md, got issues: {:?}",
r.issues
);
assert!(
r.issues
.iter()
.any(|i| i.path == "changes/001-01_demo/todo.md" && i.level == "ERROR"),
"apply.tracks validation should run and report todo.md diagnostics, got issues: {:?}",
r.issues
);
}
#[test]
fn validate_change_uses_apply_tracks_for_legacy_delta_schemas() {
let td = tempfile::tempdir().unwrap();
let project_root = td.path();
let ito = project_root.join(".ito");
let change_id = "001-01_demo";
write(
&project_root
.join(".ito")
.join("templates")
.join("schemas")
.join("spec-driven")
.join("schema.yaml"),
r#"
name: spec-driven
version: 1
artifacts: []
apply:
tracks: todo.md
"#,
);
write(
&ito.join("changes").join(change_id).join(".ito.yaml"),
"schema: spec-driven\n",
);
write(
&ito.join("changes")
.join(change_id)
.join("specs")
.join("a")
.join("spec.md"),
r#"
## ADDED Requirements
### Requirement: R
The system SHALL do it.
#### Scenario: S
- **WHEN** ok
- **THEN** ok
"#,
);
write(
&ito.join("changes").join(change_id).join("todo.md"),
r#"
## Wave 1
### Task 1.1: First
- **Status**: [ ] pending
- **Updated At**: 2026-02-25
### Task 1.1: Duplicate
- **Status**: [ ] pending
- **Updated At**: 2026-02-25
"#,
);
let change_repo = FsChangeRepository::new(&ito);
let r = validate_change(&change_repo, &ito, change_id, false).unwrap();
assert!(
!r.issues.iter().any(|i| i.path.contains("tasks.md")),
"apply.tracks should override legacy tasks.md validation, got issues: {:?}",
r.issues
);
assert!(
r.issues
.iter()
.any(|i| i.path == "changes/001-01_demo/todo.md"),
"expected validation to report issues against todo.md, got issues: {:?}",
r.issues
);
}
#[test]
fn validate_change_rejects_unsafe_apply_tracks_for_schema_validation_tracking() {
let td = tempfile::tempdir().unwrap();
let project_root = td.path();
let ito = project_root.join(".ito");
let change_id = "001-01_demo";
write(
&project_root
.join(".ito")
.join("templates")
.join("schemas")
.join("tracks")
.join("schema.yaml"),
r#"
name: tracks
version: 1
artifacts: []
apply:
tracks: ../todo.md
"#,
);
write(
&project_root
.join(".ito")
.join("templates")
.join("schemas")
.join("tracks")
.join("validation.yaml"),
r#"
version: 1
tracking:
source: apply_tracks
required: true
validate_as: ito.tasks-tracking.v1
"#,
);
write(
&ito.join("changes").join(change_id).join(".ito.yaml"),
"schema: tracks\n",
);
let change_repo = FsChangeRepository::new(&ito);
let r = validate_change(&change_repo, &ito, change_id, false).unwrap();
assert!(
r.issues
.iter()
.any(|i| i.path == "tracking" && i.message.contains("Invalid tracking file path")),
"expected unsafe apply.tracks to be rejected, got issues: {:?}",
r.issues
);
}
#[test]
fn validate_change_rejects_unsafe_apply_tracks_for_legacy_delta_schemas() {
let td = tempfile::tempdir().unwrap();
let project_root = td.path();
let ito = project_root.join(".ito");
let change_id = "001-01_demo";
write(
&project_root
.join(".ito")
.join("templates")
.join("schemas")
.join("spec-driven")
.join("schema.yaml"),
r#"
name: spec-driven
version: 1
artifacts: []
apply:
tracks: dir/todo.md
"#,
);
write(
&ito.join("changes").join(change_id).join(".ito.yaml"),
"schema: spec-driven\n",
);
write(
&ito.join("changes")
.join(change_id)
.join("specs")
.join("a")
.join("spec.md"),
r#"
## ADDED Requirements
### Requirement: R
The system SHALL do it.
#### Scenario: S
- **WHEN** ok
- **THEN** ok
"#,
);
let change_repo = FsChangeRepository::new(&ito);
let r = validate_change(&change_repo, &ito, change_id, false).unwrap();
assert!(
r.issues
.iter()
.any(|i| i.path == "tracking" && i.message.contains("Invalid tracking file path")),
"expected unsafe apply.tracks to be rejected, got issues: {:?}",
r.issues
);
}
#[test]
fn empty_tracking_file_is_warning_in_non_strict_and_error_in_strict() {
let td = tempfile::tempdir().unwrap();
let project_root = td.path();
let ito = project_root.join(".ito");
let change_id = "001-01_demo";
write(
&project_root
.join(".ito")
.join("templates")
.join("schemas")
.join("spec-driven")
.join("schema.yaml"),
r#"
name: spec-driven
version: 1
artifacts: []
"#,
);
write(
&ito.join("changes").join(change_id).join(".ito.yaml"),
"schema: spec-driven\n",
);
write(
&ito.join("changes")
.join(change_id)
.join("specs")
.join("a")
.join("spec.md"),
r#"
## ADDED Requirements
### Requirement: R
The system SHALL do it.
#### Scenario: S
- **WHEN** ok
- **THEN** ok
"#,
);
write(
&ito.join("changes").join(change_id).join("tasks.md"),
"# just notes\n",
);
let change_repo = FsChangeRepository::new(&ito);
let non_strict = validate_change(&change_repo, &ito, change_id, false).unwrap();
assert!(
non_strict
.issues
.iter()
.any(|i| i.path == "changes/001-01_demo/tasks.md" && i.level == "WARNING"),
"expected warning for empty tracking file, got issues: {:?}",
non_strict.issues
);
let strict = validate_change(&change_repo, &ito, change_id, true).unwrap();
assert!(
strict
.issues
.iter()
.any(|i| i.path == "changes/001-01_demo/tasks.md" && i.level == "ERROR"),
"expected error for empty tracking file in strict mode, got issues: {:?}",
strict.issues
);
}
#[test]
fn validate_change_skips_optional_validator_when_artifact_is_missing() {
let td = tempfile::tempdir().unwrap();
let project_root = td.path();
let ito = project_root.join(".ito");
let change_id = "001-01_demo";
write(
&project_root
.join(".ito")
.join("templates")
.join("schemas")
.join("optional")
.join("schema.yaml"),
r#"
name: optional
version: 1
artifacts:
- id: tasks
generates: tasks.md
template: tasks.md
requires: []
"#,
);
write(
&project_root
.join(".ito")
.join("templates")
.join("schemas")
.join("optional")
.join("validation.yaml"),
r#"
version: 1
artifacts:
tasks:
required: false
validate_as: ito.tasks-tracking.v1
"#,
);
write(
&ito.join("changes").join(change_id).join(".ito.yaml"),
"schema: optional\n",
);
let change_repo = FsChangeRepository::new(&ito);
let r = validate_change(&change_repo, &ito, change_id, false).unwrap();
assert!(
!r.issues
.iter()
.any(|i| i.message.contains("Failed to read") && i.path.contains("tasks.md")),
"optional missing artifact should skip validators, got issues: {:?}",
r.issues
);
}
#[test]
fn validate_module_reports_missing_scope_and_short_purpose() {
let td = tempfile::tempdir().unwrap();
let ito = td.path().join(".ito");
let module_repo = FsModuleRepository::new(&ito);
write(
&ito.join("modules").join("006_demo").join("module.md"),
r#"
## Purpose
Too short.
## Scope
"#,
);
let (_name, r) = validate_module(&module_repo, &ito, "006_demo", false).unwrap();
assert!(!r.valid);
assert!(r.summary.errors >= 1);
}
#[test]
fn validate_tasks_file_returns_empty_for_valid_tasks() {
let td = tempfile::tempdir().unwrap();
let ito = td.path().join(".ito");
let change_id = "001-01_demo";
let valid_tasks = "## 1. Implementation\n- [ ] 1.1 Do the thing\n- [x] 1.2 Done thing\n";
write(
&ito.join("changes").join(change_id).join("tasks.md"),
valid_tasks,
);
let issues =
validate_tasks_file(&ito, change_id, false).expect("validate_tasks_file should succeed");
assert!(
issues.is_empty(),
"valid tasks file should produce no issues, got: {issues:?}"
);
}
#[test]
fn validate_tasks_file_returns_error_for_missing_file() {
let td = tempfile::tempdir().unwrap();
let ito = td.path().join(".ito");
let issues = validate_tasks_file(&ito, "nonexistent-change", false)
.expect("validate_tasks_file should return Ok with error issues");
assert!(
!issues.is_empty(),
"missing tasks file should produce at least one issue"
);
assert!(
issues[0].level == "ERROR",
"issue should be an ERROR, got: {}",
issues[0].level
);
assert!(
issues[0].message.contains("tasks.md"),
"error message should mention tasks.md, got: {}",
issues[0].message
);
}
#[test]
fn validate_tasks_file_returns_diagnostics_for_malformed_content() {
let td = tempfile::tempdir().unwrap();
let ito = td.path().join(".ito");
let change_id = "001-01_broken";
let malformed_tasks = "\
## Wave 1
### Task 1.1: First task
- **Status**: [x] complete
### Task 1.1: Duplicate ID task
- **Status**: [ ] pending
";
write(
&ito.join("changes").join(change_id).join("tasks.md"),
malformed_tasks,
);
let issues =
validate_tasks_file(&ito, change_id, false).expect("validate_tasks_file should succeed");
assert!(
!issues.is_empty(),
"malformed tasks file should produce diagnostics, got empty"
);
}
#[test]
fn validate_tasks_file_issues_cite_tasks_tracking_validator_id() {
let td = tempfile::tempdir().unwrap();
let ito = td.path().join(".ito");
let change_id = "001-01_broken";
let malformed_tasks = "\
## Wave 1\n\n\
### Task 1.1: First task\n\
- **Status**: [x] complete\n\
- **Updated At**: 2026-02-25\n\n\
### Task 1.1: Duplicate ID task\n\
- **Status**: [ ] pending\n\
- **Updated At**: 2026-02-25\n";
write(
&ito.join("changes").join(change_id).join("tasks.md"),
malformed_tasks,
);
let issues =
validate_tasks_file(&ito, change_id, false).expect("validate_tasks_file should succeed");
assert!(
!issues.is_empty(),
"expected tasks diagnostics to be reported"
);
let some = issues
.iter()
.any(|i| i.message.contains("ito.tasks-tracking.v1"));
assert!(some, "expected tasks issues to cite validator id");
let with_meta = issues.iter().any(|i| {
let Some(meta) = i.metadata.as_ref().and_then(|m| m.as_object()) else {
return false;
};
meta.get("validator_id").and_then(|v| v.as_str()) == Some("ito.tasks-tracking.v1")
&& meta.get("spec_path").and_then(|v| v.as_str())
== Some(".ito/specs/tasks-tracking/spec.md")
});
assert!(
with_meta,
"expected tasks issues to include metadata references"
);
}
#[test]
fn validate_tasks_file_uses_apply_tracks_when_set() {
let td = tempfile::tempdir().unwrap();
let project_root = td.path();
let ito = project_root.join(".ito");
let change_id = "001-01_demo";
write(
&project_root
.join(".ito")
.join("templates")
.join("schemas")
.join("tracks")
.join("schema.yaml"),
r#"
name: tracks
version: 1
artifacts: []
apply:
tracks: todo.md
"#,
);
write(
&ito.join("changes").join(change_id).join(".ito.yaml"),
"schema: tracks\n",
);
write(
&ito.join("changes").join(change_id).join("todo.md"),
"## Wave 1\n\n### Task 1.1: First\n- **Status**: [x] complete\n- **Updated At**: 2026-02-25\n\n### Task 1.1: Duplicate\n- **Status**: [ ] pending\n- **Updated At**: 2026-02-25\n",
);
let issues =
validate_tasks_file(&ito, change_id, false).expect("validate_tasks_file should succeed");
assert!(
issues
.iter()
.any(|i| i.path == "changes/001-01_demo/todo.md"),
"expected issues to reference todo.md, got: {issues:?}"
);
assert!(
!issues
.iter()
.any(|i| i.message.contains("tasks.md") || i.path.contains("tasks.md")),
"apply.tracks should override tasks.md validation, got: {issues:?}"
);
}
#[test]
fn validate_module_passes_when_sub_modules_have_valid_module_md() {
let td = tempfile::tempdir().unwrap();
let ito = td.path().join(".ito");
let module_repo = FsModuleRepository::new(&ito);
write(
&ito.join("modules").join("024_backend").join("module.md"),
"# Backend\n\n## Purpose\nBackend module for the application.\n\n## Scope\n- *\n\n## Changes\n",
);
write(
&ito.join("modules")
.join("024_backend")
.join("sub")
.join("01_auth")
.join("module.md"),
"# Auth\n\n## Purpose\nAuthentication sub-module for the backend.\n\n## Scope\n- *\n\n## Changes\n",
);
let (_name, r) = validate_module(&module_repo, &ito, "024", false).unwrap();
assert!(r.valid, "should be valid; issues: {:?}", r.issues);
}
#[test]
fn validate_module_errors_when_sub_module_missing_module_md() {
let td = tempfile::tempdir().unwrap();
let ito = td.path().join(".ito");
let module_repo = FsModuleRepository::new(&ito);
write(
&ito.join("modules").join("024_backend").join("module.md"),
"# Backend\n\n## Purpose\nBackend module for the application.\n\n## Scope\n- *\n\n## Changes\n",
);
std::fs::create_dir_all(
ito.join("modules")
.join("024_backend")
.join("sub")
.join("01_auth"),
)
.unwrap();
let (_name, r) = validate_module(&module_repo, &ito, "024", false).unwrap();
assert!(
r.issues
.iter()
.any(|i| i.message.contains("missing module.md")),
"expected missing module.md warning; issues: {:?}",
r.issues
);
}
#[test]
fn validate_module_errors_when_sub_module_has_invalid_naming() {
let td = tempfile::tempdir().unwrap();
let ito = td.path().join(".ito");
let module_repo = FsModuleRepository::new(&ito);
write(
&ito.join("modules").join("024_backend").join("module.md"),
"# Backend\n\n## Purpose\nBackend module for the application.\n\n## Scope\n- *\n\n## Changes\n",
);
std::fs::create_dir_all(
ito.join("modules")
.join("024_backend")
.join("sub")
.join("badname"),
)
.unwrap();
let (_name, r) = validate_module(&module_repo, &ito, "024", false).unwrap();
assert!(
r.issues
.iter()
.any(|i| i.message.contains("SS_name convention")),
"expected naming convention error; issues: {:?}",
r.issues
);
}
#[test]
fn validate_module_warns_when_sub_module_purpose_too_short() {
let td = tempfile::tempdir().unwrap();
let ito = td.path().join(".ito");
let module_repo = FsModuleRepository::new(&ito);
write(
&ito.join("modules").join("024_backend").join("module.md"),
"# Backend\n\n## Purpose\nBackend module for the application.\n\n## Scope\n- *\n\n## Changes\n",
);
write(
&ito.join("modules")
.join("024_backend")
.join("sub")
.join("01_auth")
.join("module.md"),
"# Auth\n\n## Purpose\nShort.\n\n## Scope\n- *\n\n## Changes\n",
);
let (_name, r) = validate_module(&module_repo, &ito, "024", false).unwrap();
assert!(
r.issues
.iter()
.any(|i| i.level == "WARNING" && i.message.contains("too brief")),
"expected too-brief purpose warning; issues: {:?}",
r.issues
);
}