use super::*;
use serde::Serialize;
#[test]
fn render_template_str_renders_from_serialize_ctx() {
#[derive(Serialize)]
struct Ctx {
name: &'static str,
}
let out = render_template_str("hello {{ name }}", &Ctx { name: "world" }).unwrap();
assert_eq!(out, "hello world");
}
#[test]
fn render_template_str_is_strict_on_undefined() {
#[derive(Serialize)]
struct Ctx {}
let err = render_template_str("hello {{ missing }}", &Ctx {}).unwrap_err();
assert_eq!(err.kind(), minijinja::ErrorKind::UndefinedError);
}
#[test]
fn render_instruction_template_str_trims_block_whitespace() {
#[derive(Serialize)]
struct Ctx {
tasks: Vec<&'static str>,
}
let template = "## Tasks\n{% if tasks %}\n### Pending\n{% for task in tasks %}\n- {{ task }}\n{% endfor %}\n{% endif %}\n## Done\n";
let out = render_instruction_template_str(
template,
&Ctx {
tasks: vec!["Write test", "Fix renderer"],
},
)
.unwrap();
assert_eq!(
out,
"## Tasks\n### Pending\n- Write test\n- Fix renderer\n## Done\n"
);
}
#[test]
fn render_template_str_preserves_trailing_newline() {
#[derive(Serialize)]
struct Ctx {
name: &'static str,
}
let out = render_template_str("hello {{ name }}\n", &Ctx { name: "world" }).unwrap();
assert_eq!(out, "hello world\n");
}
#[test]
fn list_instruction_templates_is_sorted_and_non_empty() {
let templates = list_instruction_templates();
assert!(!templates.is_empty());
let mut sorted = templates.clone();
sorted.sort_unstable();
assert_eq!(templates, sorted);
}
#[test]
fn template_fetchers_work_for_known_and_unknown_paths() {
let templates = list_instruction_templates();
let known = *templates
.first()
.expect("expected at least one embedded instruction template");
let bytes = get_instruction_template_bytes(known);
assert!(bytes.is_some());
let text = get_instruction_template(known);
assert!(text.is_some());
assert_eq!(get_instruction_template_bytes("missing/template.md"), None);
assert_eq!(get_instruction_template("missing/template.md"), None);
}
#[test]
fn render_instruction_template_returns_not_found_for_missing_template() {
#[derive(Serialize)]
struct Ctx {}
let err = render_instruction_template("missing/template.md", &Ctx {}).unwrap_err();
assert_eq!(err.kind(), minijinja::ErrorKind::TemplateNotFound);
}
#[test]
fn review_template_renders_conditional_sections() {
#[derive(Serialize)]
struct Artifact {
id: &'static str,
path: &'static str,
present: bool,
}
#[derive(Serialize)]
struct ValidationIssue {
level: &'static str,
path: &'static str,
message: &'static str,
line: Option<u32>,
column: Option<u32>,
}
#[derive(Serialize)]
struct TaskSummary {
total: usize,
complete: usize,
in_progress: usize,
pending: usize,
shelved: usize,
wave_count: usize,
}
#[derive(Serialize)]
struct AffectedSpec {
spec_id: &'static str,
operation: &'static str,
description: &'static str,
}
#[derive(Serialize)]
struct TestingPolicy {
tdd_workflow: &'static str,
coverage_target_percent: u64,
}
#[derive(Serialize)]
struct Ctx {
change_name: &'static str,
change_dir: &'static str,
schema_name: &'static str,
module_id: &'static str,
module_name: &'static str,
artifacts: Vec<Artifact>,
validation_issues: Vec<ValidationIssue>,
validation_passed: bool,
task_summary: TaskSummary,
affected_specs: Vec<AffectedSpec>,
user_guidance: &'static str,
testing_policy: TestingPolicy,
generated_at: &'static str,
}
let ctx = Ctx {
change_name: "000-01_test-change",
change_dir: "/tmp/.ito/changes/000-01_test-change",
schema_name: "spec-driven",
module_id: "000_ungrouped",
module_name: "Ungrouped",
artifacts: vec![
Artifact {
id: "proposal",
path: "/tmp/.ito/changes/000-01_test-change/proposal.md",
present: true,
},
Artifact {
id: "design",
path: "/tmp/.ito/changes/000-01_test-change/design.md",
present: false,
},
Artifact {
id: "tasks",
path: "/tmp/.ito/changes/000-01_test-change/tasks.md",
present: true,
},
Artifact {
id: "specs",
path: "/tmp/.ito/changes/000-01_test-change/specs",
present: true,
},
],
validation_issues: vec![ValidationIssue {
level: "warning",
path: ".ito/changes/000-01_test-change/tasks.md",
message: "sample warning",
line: Some(3),
column: Some(1),
}],
validation_passed: false,
task_summary: TaskSummary {
total: 4,
complete: 1,
in_progress: 1,
pending: 2,
shelved: 0,
wave_count: 2,
},
affected_specs: vec![AffectedSpec {
spec_id: "agent-instructions",
operation: "MODIFIED",
description: "Review routing",
}],
user_guidance: "Follow strict review format.",
testing_policy: TestingPolicy {
tdd_workflow: "red-green-refactor",
coverage_target_percent: 80,
},
generated_at: "2026-02-19T00:00:00Z",
};
let out = render_instruction_template("agent/review.md.j2", &ctx).unwrap();
assert!(out.contains("Peer Review"));
assert!(out.contains("## Proposal Review"));
assert!(out.contains("## Spec Review"));
assert!(out.contains("## Task Review"));
assert!(!out.contains("## Design Review"));
assert!(out.contains("## Testing Policy"));
assert!(out.contains("<user_guidance>"));
assert!(out.contains("## Output Format"));
assert!(out.contains("Verdict: needs-discussion"));
}
#[test]
fn worktrees_template_bare_control_siblings_branches_from_default_branch() {
#[derive(Serialize)]
struct WorktreeCtx {
enabled: bool,
strategy: &'static str,
layout_dir_name: &'static str,
default_branch: &'static str,
integration_mode: &'static str,
ito_root: &'static str,
project_root: &'static str,
worktree_root: &'static str,
}
#[derive(Serialize)]
struct Ctx {
worktree: WorktreeCtx,
loaded_from: Vec<&'static str>,
ito_dir_name: &'static str,
}
let ctx = Ctx {
worktree: WorktreeCtx {
enabled: true,
strategy: "bare_control_siblings",
layout_dir_name: "ito-worktrees",
default_branch: "develop",
integration_mode: "commit_pr",
ito_root: "/repo/main/.ito",
project_root: "/repo",
worktree_root: "/repo/main",
},
loaded_from: Vec::new(),
ito_dir_name: ".ito",
};
let out = render_instruction_template("agent/worktrees.md.j2", &ctx).unwrap();
assert!(out.contains(
"git -C \"$PROJECT_ROOT\" worktree add \"$WORKTREES_ROOT/${BRANCH_NAME}\" -b \"${BRANCH_NAME}\" \"develop\""
));
}
#[test]
fn schemas_template_includes_fix_and_platform_guidance() {
#[derive(Serialize)]
struct Schema<'a> {
name: &'a str,
description: &'a str,
artifacts: Vec<&'a str>,
source: &'a str,
}
#[derive(Serialize)]
struct Ctx<'a> {
schemas: Vec<Schema<'a>>,
recommended_default: &'a str,
}
let ctx = Ctx {
schemas: vec![
Schema {
name: "minimalist",
description: "Lightweight workflow for small changes",
artifacts: vec!["specs", "tasks"],
source: "embedded",
},
Schema {
name: "spec-driven",
description: "Proposal-driven workflow",
artifacts: vec!["proposal", "specs", "design", "tasks"],
source: "embedded",
},
Schema {
name: "tdd",
description: "Test-first workflow",
artifacts: vec!["spec", "tests", "implementation", "docs"],
source: "embedded",
},
],
recommended_default: "spec-driven",
};
let out = render_instruction_template("agent/schemas.md.j2", &ctx).unwrap();
assert!(out.contains("bounded bug fixes or regression-oriented corrections"));
assert!(out.contains("supporting platform or infrastructure"));
assert!(out.contains("When in doubt, start from `ito-proposal`"));
}
#[test]
fn apply_template_bare_control_siblings_branches_from_default_branch() {
#[derive(Serialize)]
struct InstructionsCtx {
#[serde(rename = "changeName")]
change_name: &'static str,
#[serde(rename = "schemaName")]
schema_name: &'static str,
state: &'static str,
#[serde(rename = "missingArtifacts")]
missing_artifacts: Vec<&'static str>,
instruction: &'static str,
#[serde(rename = "tracksFile")]
tracks_file: bool,
#[serde(rename = "tracksPath")]
tracks_path: &'static str,
#[serde(rename = "tracksFormat")]
tracks_format: &'static str,
progress: ProgressCtx,
tasks: Vec<&'static str>,
}
#[derive(Serialize)]
struct ProgressCtx {
total: usize,
complete: usize,
}
#[derive(Serialize)]
struct WorktreeCtx {
enabled: bool,
apply_enabled: bool,
strategy: &'static str,
default_branch: &'static str,
layout_dir_name: &'static str,
integration_mode: &'static str,
copy_from_main: Vec<&'static str>,
setup_commands: Vec<&'static str>,
}
#[derive(Serialize)]
struct TestingPolicyCtx {
tdd_workflow: &'static str,
coverage_target_percent: u64,
}
#[derive(Serialize)]
struct Ctx {
instructions: InstructionsCtx,
context_files: Vec<&'static str>,
worktree: WorktreeCtx,
tracking_errors: Vec<&'static str>,
tracking_warnings: Vec<&'static str>,
testing_policy: TestingPolicyCtx,
user_guidance: &'static str,
}
let ctx = Ctx {
instructions: InstructionsCtx {
change_name: "000-01_test-change",
schema_name: "spec-driven",
state: "ready",
missing_artifacts: Vec::new(),
instruction: "Implement the change.",
tracks_file: false,
tracks_path: "",
tracks_format: "",
progress: ProgressCtx {
total: 0,
complete: 0,
},
tasks: Vec::new(),
},
context_files: Vec::new(),
worktree: WorktreeCtx {
enabled: true,
apply_enabled: true,
strategy: "bare_control_siblings",
default_branch: "develop",
layout_dir_name: "ito-worktrees",
integration_mode: "commit_pr",
copy_from_main: Vec::new(),
setup_commands: Vec::new(),
},
tracking_errors: Vec::new(),
tracking_warnings: Vec::new(),
testing_policy: TestingPolicyCtx {
tdd_workflow: "red-green-refactor",
coverage_target_percent: 80,
},
user_guidance: "",
};
let out = render_instruction_template("agent/apply.md.j2", &ctx).unwrap();
assert!(out.contains(
"git -C \"$PROJECT_ROOT\" worktree add \"$CHANGE_DIR\" -b \"$CHANGE_NAME\" \"develop\""
));
}
#[test]
fn repo_sweep_template_renders() {
#[derive(Serialize)]
struct Ctx {}
let rendered = render_instruction_template("agent/repo-sweep.md.j2", &Ctx {}).unwrap();
assert!(rendered.contains("Sub-Module"));
assert!(rendered.contains("NNN.SS-NN_name"));
}
#[test]
fn archive_template_renders_generic_guidance_without_change() {
#[derive(Serialize)]
struct Ctx {
change: Option<String>,
available_changes: Vec<String>,
}
let out = render_instruction_template(
"agent/archive.md.j2",
&Ctx {
change: None,
available_changes: vec![],
},
)
.unwrap();
assert!(out.contains("ito archive"));
assert!(out.contains("ito audit reconcile"));
assert!(!out.contains("Archive:"));
}
#[test]
fn archive_template_renders_targeted_instruction_with_change() {
#[derive(Serialize)]
struct Ctx {
change: Option<String>,
available_changes: Vec<String>,
}
let out = render_instruction_template(
"agent/archive.md.j2",
&Ctx {
change: Some("009-02_event-sourced-audit-log".to_string()),
available_changes: vec![],
},
)
.unwrap();
assert!(out.contains("009-02_event-sourced-audit-log"));
assert!(out.contains("ito archive 009-02_event-sourced-audit-log --yes"));
assert!(out.contains("ito audit reconcile --change 009-02_event-sourced-audit-log"));
}
#[test]
fn archive_template_lists_available_changes_in_generic_mode() {
#[derive(Serialize)]
struct Ctx {
change: Option<String>,
available_changes: Vec<String>,
}
let out = render_instruction_template(
"agent/archive.md.j2",
&Ctx {
change: None,
available_changes: vec!["001-01_init".to_string(), "002-03_cleanup".to_string()],
},
)
.unwrap();
assert!(out.contains("001-01_init"));
assert!(out.contains("002-03_cleanup"));
}