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.
179 lines
5.3 KiB
Rust
179 lines
5.3 KiB
Rust
//! pipeline_smoke — integration tests for `spawn --pipeline` end-to-end.
|
|
//!
|
|
//! Same pattern as spawn_smoke.rs: minimal tempdir kit, role + capability
|
|
//! fixtures, then call the library surface and assert on-disk artefacts.
|
|
//! `KEI_SPAWN_LEDGER_NOOP=1` keeps the ledger subprocess a no-op so tests
|
|
//! do not depend on a real kei-ledger binary.
|
|
|
|
use kei_spawn::{
|
|
derive_chain_from_role, pipeline_from_role, spawn_with_pipeline, PipelineChain,
|
|
};
|
|
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 write_task(root: &Path, toml: &str) -> std::path::PathBuf {
|
|
let path = root.join("task.toml");
|
|
std::fs::write(&path, toml).unwrap();
|
|
path
|
|
}
|
|
|
|
fn minimal_kit_with_handoff(root: &Path) {
|
|
write_capability(root, "policy", "no-git-ops", "## Never git.\n");
|
|
write_capability(root, "output", "report-format", "## Report fields.\n");
|
|
write_capability(root, "scope", "read-only", "## Read-only.\n");
|
|
write_capability(root, "output", "verdict", "## Verdict.\n");
|
|
write_capability(root, "verify", "fork-audit", "## Fork audit.\n");
|
|
write_capability(root, "policy", "git-ops-scope", "## Git ops scope.\n");
|
|
write_capability(root, "output", "merge-result", "## Merge result.\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"]
|
|
|
|
[pipeline]
|
|
handoff = ["auditor"]
|
|
"#,
|
|
);
|
|
write_role(
|
|
root,
|
|
"auditor",
|
|
r#"
|
|
[role]
|
|
name = "auditor"
|
|
spawnable = true
|
|
claude-subagent-type = "critic"
|
|
|
|
[capabilities]
|
|
required = ["policy::no-git-ops", "scope::read-only", "verify::fork-audit", "output::verdict"]
|
|
|
|
[tools]
|
|
allowed = ["Read", "Glob", "Grep", "Bash"]
|
|
bash-patterns-allowed = ['^cargo( |$)','^git diff','^git log','^git show']
|
|
|
|
[pipeline]
|
|
handoff = ["merger"]
|
|
"#,
|
|
);
|
|
write_role(
|
|
root,
|
|
"merger",
|
|
r#"
|
|
[role]
|
|
name = "merger"
|
|
spawnable = true
|
|
claude-subagent-type = "infra-implementer"
|
|
|
|
[capabilities]
|
|
required = ["policy::git-ops-scope", "output::merge-result"]
|
|
|
|
[tools]
|
|
allowed = ["Read", "Bash"]
|
|
bash-patterns-allowed = ['^git( |$)','^kei-fork( |$)','^kei-ledger( |$)']
|
|
|
|
[pipeline]
|
|
handoff = []
|
|
"#,
|
|
);
|
|
}
|
|
|
|
fn set_noop() {
|
|
std::env::set_var("KEI_SPAWN_LEDGER_NOOP", "1");
|
|
}
|
|
|
|
#[test]
|
|
fn auditor_role_resolves() {
|
|
let tmp = TempDir::new().unwrap();
|
|
let root = tmp.path();
|
|
minimal_kit_with_handoff(root);
|
|
let handoff = pipeline_from_role(root, "auditor").expect("auditor handoff");
|
|
assert_eq!(handoff, vec!["merger".to_string()]);
|
|
}
|
|
|
|
#[test]
|
|
fn merger_role_resolves() {
|
|
let tmp = TempDir::new().unwrap();
|
|
let root = tmp.path();
|
|
minimal_kit_with_handoff(root);
|
|
let handoff = pipeline_from_role(root, "merger").expect("merger handoff");
|
|
assert!(handoff.is_empty(), "merger is the terminal step: {handoff:?}");
|
|
}
|
|
|
|
#[test]
|
|
fn pipeline_handoff_produces_chain() {
|
|
let tmp = TempDir::new().unwrap();
|
|
let root = tmp.path();
|
|
minimal_kit_with_handoff(root);
|
|
let chain: PipelineChain =
|
|
derive_chain_from_role(root, "edit-local", "ag-edit-local-test-001").unwrap();
|
|
assert_eq!(chain.steps.len(), 1);
|
|
assert_eq!(chain.steps[0].role, "auditor");
|
|
assert_eq!(chain.steps[0].agent_id, "ag-edit-local-test-001-auditor");
|
|
}
|
|
|
|
#[test]
|
|
fn pipeline_missing_role_returns_empty() {
|
|
let tmp = TempDir::new().unwrap();
|
|
let root = tmp.path();
|
|
minimal_kit_with_handoff(root);
|
|
// merger exists but has empty handoff — empty Vec, not error.
|
|
let chain = derive_chain_from_role(root, "merger", "ag-merger-test-001").unwrap();
|
|
assert!(chain.steps.is_empty());
|
|
|
|
// unknown role — must error cleanly.
|
|
let err = derive_chain_from_role(root, "does-not-exist", "ag-x-001")
|
|
.expect_err("unknown role must error");
|
|
let msg = format!("{err:#}");
|
|
assert!(msg.contains("does-not-exist"), "msg: {msg}");
|
|
}
|
|
|
|
#[test]
|
|
fn spawn_with_pipeline_scaffolds_downstream_stubs() {
|
|
set_noop();
|
|
let tmp = TempDir::new().unwrap();
|
|
let root = tmp.path();
|
|
minimal_kit_with_handoff(root);
|
|
|
|
let task_path = write_task(
|
|
root,
|
|
r#"
|
|
[task]
|
|
role = "edit-local"
|
|
|
|
[body]
|
|
text = "Writer body that hands off to auditor."
|
|
"#,
|
|
);
|
|
|
|
let (out, chain) = spawn_with_pipeline(&task_path, root).expect("spawn_with_pipeline");
|
|
assert_eq!(chain.steps.len(), 1);
|
|
assert_eq!(chain.steps[0].role, "auditor");
|
|
assert_eq!(chain.steps[0].agent_id, format!("{}-auditor", out.agent_id));
|
|
|
|
let writer_dir = root.join("tasks").join(&out.agent_id);
|
|
assert!(writer_dir.join("pipeline.json").is_file());
|
|
|
|
let auditor_dir = root.join("tasks").join(format!("{}-auditor", out.agent_id));
|
|
assert!(auditor_dir.join("task.stub.toml").is_file());
|
|
let stub_text = std::fs::read_to_string(auditor_dir.join("task.stub.toml")).unwrap();
|
|
assert!(stub_text.contains(r#"role = "auditor""#));
|
|
assert!(stub_text.contains(&format!(r#"parent-agent = "{}""#, out.agent_id)));
|
|
}
|