use std::collections::HashMap;
use std::path::{Path, PathBuf};
use super::{
GIT_EXTRA_ENABLED, format_git_extra, format_prompt_path, insert_artifact_paths,
insert_current_state, insert_formatted, resolve_nonexistent_path, resolve_path_against_base,
resolve_user_brief_path, workflow_context_paths_only,
};
use crate::prompt_stratification::WorkflowRenderContext;
fn resolve_path_against_base_resolves_relative_plan_path() {
let tmp = tempfile::tempdir().expect("tempdir");
let base = tmp.path().canonicalize().expect("base");
let resolved = resolve_path_against_base(Path::new("plan.md"), &base);
assert!(resolved.ends_with("plan.md"));
}
fn resolve_path_against_base_resolves_absolute_missing_file_under_base() {
let tmp = tempfile::tempdir().expect("tempdir");
let base = tmp.path().canonicalize().expect("base");
let abs = tmp.path().join("src/foo.rs");
let resolved = resolve_path_against_base(&abs, &base);
assert!(
resolved.starts_with(&base),
"expected resolved path under base, got {}",
resolved.display()
);
assert!(resolved.ends_with("src/foo.rs"));
}
fn resolve_nonexistent_path_cases() {
let _ = resolve_nonexistent_path;
assert_eq!(resolve_nonexistent_path(Path::new("")), PathBuf::from(""));
let tmp = tempfile::tempdir().expect("tempdir");
let base = tmp.path().canonicalize().expect("base");
let missing = tmp.path().join("nested/missing.md");
let resolved = resolve_nonexistent_path(&missing);
assert!(resolved.starts_with(&base));
assert!(resolved.ends_with("nested/missing.md"));
let deep = tmp.path().join("a/b/c/d.md");
let deep_resolved = resolve_nonexistent_path(&deep);
assert!(deep_resolved.starts_with(&base));
assert!(deep_resolved.ends_with("a/b/c/d.md"));
}
fn format_prompt_path_fallback_uses_resolved_path_display() {
let tmp = tempfile::tempdir().expect("tempdir");
let outside = std::env::temp_dir().join(format!("malvin_outside_{}", std::process::id()));
std::fs::create_dir_all(&outside).expect("outside dir");
let missing = outside.join("missing.md");
let formatted = format_prompt_path(&missing, tmp.path());
assert!(
!formatted.starts_with("./"),
"outside path must not be relativized: {formatted}"
);
assert!(
formatted.contains("missing.md"),
"fallback must name the file: {formatted}"
);
let _ = std::fs::remove_dir_all(&outside);
}
fn insert_quality_gates_log_paths_sets_alias() {
let tmp = tempfile::tempdir().expect("tempdir");
let plan = tmp.path().join("plan.md");
std::fs::write(&plan, "p").expect("write");
let artifacts =
crate::artifacts::create_run_artifacts(&plan, Some(tmp.path())).expect("artifacts");
let mut ctx = HashMap::new();
super::insert_quality_gates_log_paths(&mut ctx, &artifacts, tmp.path());
assert_eq!(
ctx.get("quality_gates_path").map(String::as_str),
ctx.get("quality_gates_log").map(String::as_str),
);
assert!(
ctx.get("quality_gates_log")
.expect("log")
.ends_with("quality_gates.log")
);
}
fn insert_artifact_paths_sets_logs_dir_to_home_bucket() {
let tmp = tempfile::tempdir().expect("tempdir");
let plan = tmp.path().join("plan.md");
std::fs::write(&plan, "p").expect("write");
let artifacts =
crate::artifacts::create_run_artifacts(&plan, Some(tmp.path())).expect("artifacts");
let mut ctx = HashMap::new();
insert_artifact_paths(&mut ctx, &artifacts);
let logs_dir = ctx.get("logs_dir").expect("logs_dir");
let expected = format_prompt_path(&crate::malvin_logs_root(tmp.path()), tmp.path());
assert_eq!(logs_dir, &expected);
assert!(
logs_dir.contains(".malvin_home/logs"),
"logs_dir should point at home logs bucket, got {logs_dir:?}"
);
}
fn insert_artifact_paths_populates_expected_keys() {
let tmp = tempfile::tempdir().expect("tempdir");
let plan = tmp.path().join("plan.md");
std::fs::write(&plan, "p").expect("write");
let artifacts =
crate::artifacts::create_run_artifacts(&plan, Some(tmp.path())).expect("artifacts");
let mut ctx = HashMap::new();
insert_artifact_paths(&mut ctx, &artifacts);
assert!(ctx.contains_key("result_path"));
assert!(ctx.contains_key("review_prep_path"));
assert!(ctx.contains_key("advice_path"));
assert!(ctx.contains_key("logs_dir"));
assert!(ctx.contains_key("workspace_dir"));
assert!(ctx.contains_key("user_request_path"));
assert_eq!(
ctx.get("user_request_path").map(String::as_str),
ctx.get("plan_path").map(String::as_str),
);
assert_eq!(
ctx.get("workspace_dir").map(String::as_str),
ctx.get("malvin_output_path").map(String::as_str),
);
}
fn workflow_context_paths_only_includes_current_state() {
let tmp = tempfile::tempdir().expect("tempdir");
let plan = tmp.path().join("plan.md");
std::fs::write(&plan, "p").expect("write");
let artifacts =
crate::artifacts::create_run_artifacts(&plan, Some(tmp.path())).expect("artifacts");
let ctx = workflow_context_paths_only(&artifacts, crate::config::DEFAULT_CLI_MODEL, false);
assert!(ctx.contains_key("current_state"));
assert!(ctx.get("current_state").expect("state").contains("User:"));
}
fn workflow_context_paths_only_sets_git_extra_from_flag() {
let tmp = tempfile::tempdir().expect("tempdir");
let plan = tmp.path().join("plan.md");
std::fs::write(&plan, "p").expect("write");
let artifacts =
crate::artifacts::create_run_artifacts(&plan, Some(tmp.path())).expect("artifacts");
let off = workflow_context_paths_only(&artifacts, crate::config::DEFAULT_CLI_MODEL, false);
assert_eq!(off.get("git_extra").map(String::as_str), Some(""));
let on = workflow_context_paths_only(&artifacts, crate::config::DEFAULT_CLI_MODEL, true);
assert_eq!(
on.get("git_extra").map(String::as_str),
Some(GIT_EXTRA_ENABLED)
);
assert_eq!(format_git_extra(false), "");
assert_eq!(format_git_extra(true), GIT_EXTRA_ENABLED);
}
fn insert_current_state_populates_key() {
let tmp = tempfile::tempdir().expect("tempdir");
let plan = tmp.path().join("plan.md");
std::fs::write(&plan, "p").expect("write");
let artifacts =
crate::artifacts::create_run_artifacts(&plan, Some(tmp.path())).expect("artifacts");
let mut ctx = HashMap::new();
insert_current_state(&mut ctx, &artifacts, tmp.path());
assert!(
ctx.get("current_state")
.expect("state")
.contains("Sandbox memory:")
);
}
fn insert_formatted_stores_workflow_relative_path() {
let tmp = tempfile::tempdir().expect("tempdir");
let plan = tmp.path().join("plan.md");
std::fs::write(&plan, "p").expect("write");
let mut ctx = HashMap::new();
insert_formatted(&mut ctx, "plan_path", &plan, tmp.path());
assert_eq!(ctx.get("plan_path").map(String::as_str), Some("./plan.md"));
}
fn resolve_user_brief_path_uses_context_override() {
let tmp = tempfile::tempdir().expect("tempdir");
let plan = tmp.path().join("plan.md");
std::fs::write(&plan, "p").expect("write");
let artifacts =
crate::artifacts::create_run_artifacts(&plan, Some(tmp.path())).expect("artifacts");
let override_path = tmp.path().join("user_request.md");
std::fs::write(&override_path, "u").expect("write");
let mut ctx = HashMap::new();
insert_formatted(&mut ctx, "user_request_path", &override_path, tmp.path());
let resolved = resolve_user_brief_path(&artifacts, &WorkflowRenderContext::from(ctx));
assert_eq!(
resolved,
override_path.canonicalize().expect("canonicalize")
);
}
fn resolve_user_brief_path_falls_back_to_plan_path() {
let tmp = tempfile::tempdir().expect("tempdir");
let plan = tmp.path().join("plan.md");
std::fs::write(&plan, "p").expect("write");
let artifacts =
crate::artifacts::create_run_artifacts(&plan, Some(tmp.path())).expect("artifacts");
let resolved = resolve_user_brief_path(&artifacts, &WorkflowRenderContext::default());
assert_eq!(
resolved,
artifacts
.plan_path
.canonicalize()
.unwrap_or_else(|_| artifacts.plan_path.clone())
);
}
fn workflow_context_returns_plan_path_and_quality_gates() {
let tmp = tempfile::tempdir().expect("tempdir");
let plan_path = tmp.path().join("plan.md");
std::fs::write(&plan_path, "plan\n").expect("write plan");
crate::seed_malvin_checks(tmp.path(), "true\n");
let artifacts =
crate::artifacts::create_run_artifacts(&plan_path, Some(tmp.path())).expect("artifacts");
let store = crate::prompts::PromptStore::default_store();
store.ensure_defaults().expect("defaults");
let ctx = super::workflow_context(&artifacts, &store, crate::config::DEFAULT_CLI_MODEL)
.expect("context");
assert!(ctx.contains_key("plan_path"));
assert!(ctx.contains_key("quality_gates"));
}
#[test]
fn kiss_bundled_workflow_context_tests() {
resolve_path_against_base_resolves_relative_plan_path();
resolve_path_against_base_resolves_absolute_missing_file_under_base();
resolve_nonexistent_path_cases();
format_prompt_path_fallback_uses_resolved_path_display();
insert_quality_gates_log_paths_sets_alias();
insert_artifact_paths_sets_logs_dir_to_home_bucket();
insert_artifact_paths_populates_expected_keys();
workflow_context_paths_only_includes_current_state();
workflow_context_paths_only_sets_git_extra_from_flag();
insert_current_state_populates_key();
insert_formatted_stores_workflow_relative_path();
resolve_user_brief_path_uses_context_override();
resolve_user_brief_path_falls_back_to_plan_path();
workflow_context_returns_plan_path_and_quality_gates();
}