KeiSeiKit-1.0/_primitives/_rust/kei-agent-runtime/tests/prepare_smoke.rs
Parfii-bot d72ae51f16 feat(agent-substrate/wrapper): kei-agent-runtime prepare — orchestrator ergonomics
Single-command "prepare spawn" that emits everything orchestrator needs
to invoke the Agent tool: composed prompt, subagent_type (from role's
new claude-subagent-type field), isolation mode, verify command,
ledger row.

Before this: orchestrator ran compose + read prompt + manually
constructed Agent tool call + manually built verify command. 4 steps.

After: `kei-agent-runtime prepare <task.toml> --format=human` outputs
a single copy-paste-ready block. Orchestrator pastes into Agent tool
and records the verify command for return.

Files:
- src/prepare.rs (170 LOC) — prepare() returns AgentInvocation struct
  (agent_id, prompt, subagent_type, isolation, description,
  verify_command, ledger_row)
- src/main.rs (+39 LOC) — Prepare subcommand with --format=human|json|toml
- src/lib.rs (+2 LOC — pub mod prepare)
- _roles/*.toml (5 files) — new optional claude-subagent-type field:
  - edit-local / edit-shared → "code-implementer"
  - read-only → "critic" (default; "architect" override possible)
  - explorer → "Explore"
  - git-ops → "NOT-SPAWNABLE" (refused by prepare with RULE 0.13)
- tests/prepare_smoke.rs (3 tests) — happy path, unknown role, non-spawnable refusal
- docs/AGENT-SUBSTRATE-SCHEMA.md (+ ## Orchestrator ergonomics section)

Tests: 40/40 (was 37, +3 prepare_smoke). Same path exercised in tempfile
fixtures that the real CLI would hit end-to-end.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 03:25:14 +08:00

120 lines
3.8 KiB
Rust

//! Prepare smoke — validates orchestrator-facing wrapper.
//!
//! Three fixtures per task spec:
//! 1. Happy path — valid task.toml → AgentInvocation fully populated
//! 2. Unknown role → clear error (role lookup fails)
//! 3. Non-spawnable role (git-ops) → explicit refusal + RULE 0.13 pointer
use kei_agent_runtime::capability::TaskSpec;
use kei_agent_runtime::prepare::{prepare, render_human};
use tempfile::TempDir;
fn write_capability(root: &std::path::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: &std::path::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();
}
#[test]
fn happy_path_yields_full_invocation() {
let tmp = TempDir::new().unwrap();
let root = tmp.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"]
"#,
);
let mut task = TaskSpec::default();
task.task.role = "edit-local".into();
task.task.agent_id = "edit-local-forge-abc123".into();
task.body.text = "Port kei-forge templating to pure-Rust.".into();
let inv = prepare(&task, root).expect("prepare should succeed");
assert_eq!(inv.agent_id, "edit-local-forge-abc123");
assert_eq!(inv.role, "edit-local");
assert_eq!(inv.subagent_type, "code-implementer");
assert_eq!(inv.isolation.as_deref(), Some("worktree"));
assert!(inv.prompt.contains("Never git"));
assert!(inv.prompt.contains("Report fields"));
assert!(inv.prompt.contains("Port kei-forge templating"));
assert!(inv.verify_command.contains("kei-agent-runtime verify"));
assert!(inv.verify_command.contains("edit-local-forge-abc123"));
assert!(inv.ledger_row.contains("running"));
assert!(inv.ledger_row.contains("edit-local"));
assert!(inv.ledger_row.contains("parent=none"));
let human = render_human(&inv);
assert!(human.contains("=== AGENT SUBSTRATE v1"));
assert!(human.contains("--- PROMPT"));
assert!(human.contains("--- END PROMPT"));
assert!(human.contains("subagent_type: code-implementer"));
}
#[test]
fn unknown_role_errors_clearly() {
let tmp = TempDir::new().unwrap();
let root = tmp.path();
let mut task = TaskSpec::default();
task.task.role = "does-not-exist".into();
task.task.agent_id = "x-1".into();
let err = prepare(&task, root).expect_err("unknown role must fail");
let msg = format!("{err:#}");
assert!(
msg.contains("does-not-exist") || msg.contains("role"),
"error should mention the role or the word 'role': got {msg}"
);
}
#[test]
fn non_spawnable_role_refused_with_rule_013_pointer() {
let tmp = TempDir::new().unwrap();
let root = tmp.path();
write_role(
root,
"git-ops",
r#"
[role]
name = "git-ops"
spawnable = false
claude-subagent-type = "NOT-SPAWNABLE"
[capabilities]
required = []
"#,
);
let mut task = TaskSpec::default();
task.task.role = "git-ops".into();
task.task.agent_id = "orchestrator-only-1".into();
let err = prepare(&task, root).expect_err("git-ops must be refused");
let msg = format!("{err:#}");
assert!(
msg.contains("RULE 0.13"),
"refusal must cite RULE 0.13: got {msg}"
);
assert!(
msg.contains("spawnable") || msg.contains("orchestrator"),
"refusal message should mention spawnable/orchestrator: got {msg}"
);
}