use ito_config::ConfigContext;
use ito_core::templates::{
SchemaSource, WorkflowError, export_embedded_schemas, resolve_instructions, resolve_schema,
resolve_templates,
};
#[test]
fn resolve_schema_uses_embedded_when_no_overrides_exist() {
let project = tempfile::tempdir().expect("tempdir should succeed");
let ctx = ConfigContext {
project_dir: Some(project.path().to_path_buf()),
..Default::default()
};
let resolved = resolve_schema(Some("spec-driven"), &ctx).expect("schema should resolve");
assert_eq!(resolved.source, SchemaSource::Embedded);
assert_eq!(resolved.schema.name, "spec-driven");
}
#[test]
fn resolve_schema_prefers_project_over_user_override() {
let root = tempfile::tempdir().expect("tempdir should succeed");
let project = root.path().join("project");
let home = root.path().join("home");
std::fs::create_dir_all(project.join(".ito/templates/schemas/spec-driven"))
.expect("project schema dir");
std::fs::create_dir_all(home.join(".local/share/ito/schemas/spec-driven"))
.expect("user schema dir");
std::fs::write(
project.join(".ito/templates/schemas/spec-driven/schema.yaml"),
"name: spec-driven\nversion: 1\ndescription: project\nartifacts: []\n",
)
.expect("write project schema");
std::fs::write(
home.join(".local/share/ito/schemas/spec-driven/schema.yaml"),
"name: spec-driven\nversion: 1\ndescription: user\nartifacts: []\n",
)
.expect("write user schema");
let ctx = ConfigContext {
project_dir: Some(project),
home_dir: Some(home),
..Default::default()
};
let resolved = resolve_schema(Some("spec-driven"), &ctx).expect("schema should resolve");
assert_eq!(resolved.source, SchemaSource::Project);
assert_eq!(resolved.schema.description.as_deref(), Some("project"));
}
#[test]
fn resolve_instructions_reads_embedded_templates() {
let root = tempfile::tempdir().expect("tempdir should succeed");
let ito_path = root.path().join(".ito");
std::fs::create_dir_all(ito_path.join("changes/demo-change")).expect("create change dir");
let ctx = ConfigContext {
project_dir: Some(root.path().to_path_buf()),
..Default::default()
};
let out = resolve_instructions(
&ito_path,
"demo-change",
Some("spec-driven"),
"proposal",
&ctx,
)
.expect("instructions should resolve");
assert!(out.template.contains("## Why"));
}
#[test]
fn resolve_instructions_exposes_enhanced_spec_driven_templates() {
let root = tempfile::tempdir().expect("tempdir should succeed");
let ito_path = root.path().join(".ito");
std::fs::create_dir_all(ito_path.join("changes/demo-change")).expect("create change dir");
let ctx = ConfigContext {
project_dir: Some(root.path().to_path_buf()),
..Default::default()
};
let proposal = resolve_instructions(
&ito_path,
"demo-change",
Some("spec-driven"),
"proposal",
&ctx,
)
.expect("proposal template should resolve");
assert!(proposal.template.contains("## Change Shape"));
assert!(proposal.template.contains("- **Type**:"));
assert!(proposal.template.contains("- **Public Contract**: none"));
let spec = resolve_instructions(&ito_path, "demo-change", Some("spec-driven"), "specs", &ctx)
.expect("spec template should resolve");
assert!(spec.template.contains("- **Tags**:"));
assert!(spec.template.contains("- **Contract Refs**:"));
assert!(spec.template.contains("#### Rules / Invariants"));
assert!(spec.template.contains("#### State Transitions"));
let design = resolve_instructions(
&ito_path,
"demo-change",
Some("spec-driven"),
"design",
&ctx,
)
.expect("design template should resolve");
assert!(design.template.contains("## Approach"));
assert!(design.template.contains("## Contracts / Interfaces"));
assert!(design.template.contains("## Verification Strategy"));
assert!(design.template.contains("## Migration / Rollback"));
assert!(
design
.template
.contains("Do not include full code examples"),
"design template should discourage over-prescriptive code samples"
);
}
#[test]
fn export_embedded_schemas_writes_then_skips_without_force() {
let root = tempfile::tempdir().expect("tempdir should succeed");
let out_dir = root.path().join("schemas-out");
let first = export_embedded_schemas(&out_dir, false).expect("first export should succeed");
assert!(first.written > 0);
assert_eq!(first.skipped, 0);
let second = export_embedded_schemas(&out_dir, false).expect("second export should succeed");
assert!(second.skipped > 0);
let forced = export_embedded_schemas(&out_dir, true).expect("forced export should succeed");
assert!(forced.written > 0);
assert_eq!(forced.skipped, 0);
}
#[test]
fn resolve_schema_rejects_path_traversal_name() {
let ctx = ConfigContext::default();
let err = resolve_schema(Some("../spec-driven"), &ctx).expect_err("should reject traversal");
match err {
WorkflowError::SchemaNotFound(name) => assert_eq!(name, "../spec-driven"),
other => panic!("unexpected error: {other:?}"),
}
}
#[test]
fn resolve_schema_rejects_absolute_and_backslash_names() {
let ctx = ConfigContext::default();
let absolute = resolve_schema(Some("/etc"), &ctx).expect_err("should reject absolute path");
match absolute {
WorkflowError::SchemaNotFound(name) => assert_eq!(name, "/etc"),
other => panic!("unexpected error: {other:?}"),
}
let backslash =
resolve_schema(Some("..\\spec-driven"), &ctx).expect_err("should reject backslashes");
match backslash {
WorkflowError::SchemaNotFound(name) => assert_eq!(name, "..\\spec-driven"),
other => panic!("unexpected error: {other:?}"),
}
}
#[test]
fn resolve_templates_rejects_traversal_template_path() {
let root = tempfile::tempdir().expect("tempdir should succeed");
let project = root.path().join("project");
std::fs::create_dir_all(project.join(".ito/templates/schemas/spec-driven/templates"))
.expect("schema dir should exist");
std::fs::write(
project.join(".ito/templates/schemas/spec-driven/schema.yaml"),
"name: spec-driven\nversion: 1\nartifacts:\n - id: proposal\n generates: proposal.md\n template: ../escape.md\n",
)
.expect("schema write should succeed");
let ctx = ConfigContext {
project_dir: Some(project),
..Default::default()
};
let err = resolve_templates(Some("spec-driven"), &ctx).expect_err("should reject traversal");
match err {
WorkflowError::Io(io_err) => assert_eq!(io_err.kind(), std::io::ErrorKind::InvalidInput),
other => panic!("unexpected error: {other:?}"),
}
}
#[test]
fn resolve_instructions_rejects_traversal_template_path() {
let root = tempfile::tempdir().expect("tempdir should succeed");
let ito_path = root.path().join(".ito");
let change_dir = ito_path.join("changes/demo-change");
std::fs::create_dir_all(&change_dir).expect("change dir should exist");
let project = root.path().join("project");
std::fs::create_dir_all(project.join(".ito/templates/schemas/spec-driven/templates"))
.expect("schema dir should exist");
std::fs::write(
project.join(".ito/templates/schemas/spec-driven/schema.yaml"),
"name: spec-driven\nversion: 1\nartifacts:\n - id: proposal\n generates: proposal.md\n template: ../../secret.md\n",
)
.expect("schema write should succeed");
let ctx = ConfigContext {
project_dir: Some(project),
..Default::default()
};
let err = resolve_instructions(
&ito_path,
"demo-change",
Some("spec-driven"),
"proposal",
&ctx,
)
.expect_err("should reject traversal");
match err {
WorkflowError::Io(io_err) => assert_eq!(io_err.kind(), std::io::ErrorKind::InvalidInput),
other => panic!("unexpected error: {other:?}"),
}
}