47 crates, 801 tests green (up from 771 at v0.34.0). Wave 18 audit
found 8 HIGH findings across architect/critic/security/validator. All
closed. Three-role pipeline REBUILT after validator discovered Wave 16
commit was a half-commit (files claimed but never tracked).
## A. Three-role pipeline (REBUILD — was missing from v0.33.0 despite
CHANGELOG claim)
Files validator flagged absent: _roles/auditor.toml + merger.toml,
4 _capabilities/{policy/git-ops-scope,output/verdict,output/merge-result,
verify/fork-audit}/text.md, kei-spawn/src/{pipeline,precedent}.rs,
pipeline_smoke.rs + pipeline_unit.rs tests. ALL NOW REAL (verified by
git log --all and `ls`).
- auditor role: claude-subagent-type=critic, handoff=[merger]
- merger role: git-ops scope, claude-subagent-type=infra-implementer,
leaf (empty handoff)
- 5 capability text.md (+ capability.toml for each) defining contracts
- kei-spawn pipeline.rs (171 LOC): pipeline_from_role, derive_steps,
emit_pipeline_json, scaffold_downstream_tasks
- kei-spawn precedent.rs (118 LOC): env-gated advisory shell-out
- --pipeline flag on spawn subcommand
- +11 tests (pipeline_smoke + pipeline_unit)
## B. kei-fork — 4 HIGH fixes (Critic F1+F7a, Security #3+#4)
- `git add -A` → explicit path list from ls-untracked + ls-modified,
with exclusion filter for .DONE / .KEI_FORK_META.toml / _archive/ /
_forks/. No more merge bleed. +1 regression test.
- create() rollback: on write_meta or ledger_fork failure, worktree
+ branch cleaned. +1 test via KEI_FORK_FORCE_LEDGER_FAIL=1.
- worktree_add arg injection: added `--` sentinel + is_safe_refname()
validator (refuses dash-leading, NUL, ..). +3 tests.
- PATH hijack: KEI_FORK_GIT_BIN env override for all Command::new(git).
+1 test.
## C. kei-spawn — 2 HIGH fixes (Security #1+#2)
- HTTP body unbounded DoS: MAX_BODY_BYTES=10MiB + content-length
pre-check + streamed cap (io::Read::take) for chunked encoding.
+2 feature-gated tests.
- PATH hijack: KEI_LEDGER_BIN env override already existed at
ledger_sh.rs:15; documented precedence + added 4 regression tests
locking the 3-tier lookup order.
## D. kei-ledger-sign — 1 HIGH fix (Security #2)
- save_keypair atomic POSIX open(2) O_CREAT|O_EXCL + mode 0o600 +
rename(2) into place. No race window where key is world-readable.
+2 tests.
## E. spawn_from_task rollback (Critic F7b)
- register_in_ledger helper: on ledger fork failure, rollback_task_dir
before error propagation. +1 test spawn_rolls_back_task_dir_on_ledger_fail.
## Audit summary
- architect: GO conditional (taxonomy 19% — defer)
- critic: HIGH closed, MEDIUM debt logged
- security: 4 HIGH closed; MEDIUM (tar symlink, watcher symlink) tracked
- validator: CHANGELOG no longer lies — three-role pipeline is real
- patent-compliance: GO / LOW risk unchanged
All 8 HIGH blockers from Wave 18 consolidated audit → GREEN.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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");
|
|
}
|