Single-commit clean baseline after security scrub of niche-tells, project codenames, internal jargon, and contributor-email leaks. Contents: - 100 Rust crates (_primitives/_rust/) - 37 agent manifests (_manifests/) + generated specs (_generated/) - 67 user-invocable skills (skills/) - 33 hooks (hooks/) - Composition blocks (_blocks/) - Documentation (docs/, README.md) - TS adapter packages (_ts_packages/) - Assembler (_assembler/) - Roles (_roles/) - Templates (_templates/) - Forgejo CI (.forgejo/) Author: Denis Parfionovich <info@greendragon.info> License: see LICENSE.
139 lines
4.3 KiB
Rust
139 lines
4.3 KiB
Rust
//! pipeline_unit — fine-grained coverage for pipeline helpers + the
|
|
//! spawn-rollback-on-ledger-failure contract.
|
|
//!
|
|
//! Complements pipeline_smoke.rs (end-to-end) with unit-level assertions
|
|
//! that don't need the full spawn pipeline.
|
|
|
|
use kei_spawn::{
|
|
derive_steps, emit_pipeline_json, pipeline_json_path, spawn_from_task, PipelineChain,
|
|
PipelineStep,
|
|
};
|
|
use std::path::Path;
|
|
use tempfile::TempDir;
|
|
|
|
fn write_capability(root: &Path, cat: &str, slug: &str, body: &str) {
|
|
let dir = root.join("_capabilities").join(cat).join(slug);
|
|
std::fs::create_dir_all(&dir).unwrap();
|
|
std::fs::write(dir.join("text.md"), body).unwrap();
|
|
}
|
|
|
|
fn write_role(root: &Path, name: &str, toml: &str) {
|
|
std::fs::create_dir_all(root.join("_roles")).unwrap();
|
|
std::fs::write(root.join("_roles").join(format!("{name}.toml")), toml).unwrap();
|
|
}
|
|
|
|
fn minimal_kit(root: &Path) {
|
|
write_capability(root, "policy", "no-git-ops", "## Never git.\n");
|
|
write_capability(root, "output", "report-format", "## Report fields.\n");
|
|
write_role(
|
|
root,
|
|
"edit-local",
|
|
r#"
|
|
[role]
|
|
name = "edit-local"
|
|
spawnable = true
|
|
claude-subagent-type = "code-implementer"
|
|
|
|
[capabilities]
|
|
required = ["policy::no-git-ops", "output::report-format"]
|
|
"#,
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn derive_steps_child_ids_distinct() {
|
|
let roles = vec!["auditor".to_string(), "merger".to_string()];
|
|
let steps = derive_steps("ag-edit-local-zzz", &roles);
|
|
assert_eq!(steps.len(), 2);
|
|
assert_eq!(steps[0].agent_id, "ag-edit-local-zzz-auditor");
|
|
assert_eq!(steps[1].agent_id, "ag-edit-local-zzz-merger");
|
|
assert_ne!(steps[0].agent_id, steps[1].agent_id);
|
|
}
|
|
|
|
#[test]
|
|
fn derive_steps_skips_empty_role_names() {
|
|
let roles = vec![
|
|
"auditor".to_string(),
|
|
" ".to_string(),
|
|
"".to_string(),
|
|
"merger".to_string(),
|
|
];
|
|
let steps = derive_steps("ag-writer-001", &roles);
|
|
assert_eq!(steps.len(), 2);
|
|
assert_eq!(steps[0].role, "auditor");
|
|
assert_eq!(steps[1].role, "merger");
|
|
}
|
|
|
|
#[test]
|
|
fn emit_pipeline_json_creates_parent_dir() {
|
|
let tmp = TempDir::new().unwrap();
|
|
let nested = tmp.path().join("a").join("b").join("pipeline.json");
|
|
let chain = PipelineChain {
|
|
steps: vec![PipelineStep {
|
|
role: "auditor".into(),
|
|
agent_id: "ag-x-auditor".into(),
|
|
}],
|
|
};
|
|
emit_pipeline_json(&nested, &chain).expect("emit");
|
|
assert!(nested.is_file(), "{} should exist", nested.display());
|
|
let body = std::fs::read_to_string(&nested).unwrap();
|
|
assert!(body.contains("\"auditor\""), "json: {body}");
|
|
assert!(body.contains("\"ag-x-auditor\""), "json: {body}");
|
|
}
|
|
|
|
#[test]
|
|
fn pipeline_json_path_uses_convention() {
|
|
let root = Path::new("/tmp/kit");
|
|
let path = pipeline_json_path(root, "ag-writer-42");
|
|
assert_eq!(
|
|
path,
|
|
Path::new("/tmp/kit/tasks/ag-writer-42/pipeline.json")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn precedent_check_env_gated_off_silent() {
|
|
// Ensure env flag absent → run_advisory returns Ok(0) without shelling out.
|
|
std::env::remove_var("KEI_SPAWN_PRECEDENT_CHECK");
|
|
let n = kei_spawn::precedent::run_advisory("00".repeat(32).as_str()).unwrap();
|
|
assert_eq!(n, 0);
|
|
}
|
|
|
|
#[test]
|
|
fn spawn_rolls_back_task_dir_on_ledger_fail() {
|
|
// Force ledger failure by pointing at a bogus binary AND clearing the
|
|
// noop escape hatch so the subprocess actually runs (and fails).
|
|
std::env::remove_var("KEI_SPAWN_LEDGER_NOOP");
|
|
std::env::set_var("KEI_LEDGER_BIN", "/nonexistent/kei-ledger-rollback-test");
|
|
|
|
let tmp = TempDir::new().unwrap();
|
|
let root = tmp.path();
|
|
minimal_kit(root);
|
|
let task_path = root.join("task.toml");
|
|
std::fs::write(
|
|
&task_path,
|
|
r#"
|
|
[task]
|
|
role = "edit-local"
|
|
agent-id = "ag-edit-local-rollback-001"
|
|
|
|
[body]
|
|
text = "Rollback test."
|
|
"#,
|
|
)
|
|
.unwrap();
|
|
|
|
let result = spawn_from_task(&task_path, root);
|
|
assert!(result.is_err(), "ledger fail must propagate");
|
|
|
|
let agent_dir = root.join("tasks").join("ag-edit-local-rollback-001");
|
|
assert!(
|
|
!agent_dir.exists(),
|
|
"task dir must be cleaned up after ledger failure; still exists at {}",
|
|
agent_dir.display()
|
|
);
|
|
|
|
// Restore ledger env so other tests in the crate don't see the bogus path.
|
|
std::env::remove_var("KEI_LEDGER_BIN");
|
|
std::env::set_var("KEI_SPAWN_LEDGER_NOOP", "1");
|
|
}
|