KeiSeiKit-1.0/_primitives/_rust/kei-agent-runtime/tests/role_expression_smoke.rs
Parfii-bot 84319efcb6 feat(convergence/p3): Role expression (extends/relaxes) + DNA identity
Layer E + G. Role TOML gains extends/relaxes for parent-role
composition; agent spawn gets self-describing DNA identity alongside
UUID.

Role expression:
- _roles/*.toml gain optional `extends = "<parent>"` + `relaxes = [...]`
- compose.rs + verify.rs delegate to new role::resolve_role() with
  recursive extends-chain resolution + cycle detection
- explorer.toml: 28→18 LOC (extends read-only)
- edit-shared.toml: 31→23 LOC (extends edit-local, relaxes
  scope::files-whitelist for task-param override)

DNA identity:
- new dna.rs (159 LOC) — compose/render/parse round-trip
- AgentInvocation carries dna field (prepare.rs)
- Format: <role>::<caps-bitmap>::<sha4-scope>::<sha4-body>-<hex4-nonce>
- ≤ 80 chars total, greppable, parseable
- 11 capability codes in CAP_CODES table: NG, FW, FD, CP, CG, TG, ND,
  RF, SG, DT, BA

kei-ledger schema v2:
- ADD COLUMN dna TEXT + prefix index
- `kei-ledger fork --dna <string>` optional flag
- AgentRow.dna: Option<String>
- Backward compat: schema migration detects + applies on open

Docs: AGENT-SUBSTRATE-SCHEMA.md Layer E + Layer G sections + CAP_CODES table.

New deps: sha2 (workspace), rand 0.8.

Tests: kei-agent-runtime 50 (was 41, +9: 4 role + 5 DNA), kei-ledger
10 (was 9, +1 DNA roundtrip).

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

149 lines
3 KiB
Rust

//! Layer E — role expression resolver smoke tests.
//!
//! Fixtures built in tempdir; each test writes the role files it needs,
//! runs `resolve_role`, asserts the flattened required list.
use kei_agent_runtime::role::resolve_role;
use std::fs;
use std::path::Path;
use tempfile::TempDir;
fn write_role(root: &Path, name: &str, body: &str) {
let dir = root.join("_roles");
fs::create_dir_all(&dir).unwrap();
fs::write(dir.join(format!("{name}.toml")), body).unwrap();
}
#[test]
fn extends_chain_merges_parent_plus_local() {
let tmp = TempDir::new().unwrap();
let root = tmp.path();
write_role(
root,
"base",
r#"
[role]
name = "base"
[capabilities]
required = ["tools::deny-tools", "output::report-format"]
"#,
);
write_role(
root,
"child",
r#"
[role]
name = "child"
[capabilities]
extends = "base"
required = ["tools::bash-allowlist"]
"#,
);
let r = resolve_role(root, "child").unwrap();
assert_eq!(
r.required,
vec![
"tools::deny-tools".to_string(),
"output::report-format".to_string(),
"tools::bash-allowlist".to_string(),
],
"child should inherit parent ordering then append local"
);
}
#[test]
fn cycle_detection_errors_with_path() {
let tmp = TempDir::new().unwrap();
let root = tmp.path();
write_role(
root,
"a",
r#"
[role]
name = "a"
[capabilities]
extends = "b"
"#,
);
write_role(
root,
"b",
r#"
[role]
name = "b"
[capabilities]
extends = "a"
"#,
);
let err = resolve_role(root, "a").unwrap_err();
let msg = format!("{err:#}");
assert!(
msg.contains("cycle"),
"error should mention cycle: got {msg}"
);
}
#[test]
fn relaxes_drops_inherited_capability() {
let tmp = TempDir::new().unwrap();
let root = tmp.path();
write_role(
root,
"parent",
r#"
[role]
name = "parent"
[capabilities]
required = ["scope::files-whitelist", "quality::cargo-check-green", "output::report-format"]
"#,
);
write_role(
root,
"relaxed",
r#"
[role]
name = "relaxed"
[capabilities]
extends = "parent"
relaxes = ["scope::files-whitelist"]
"#,
);
let r = resolve_role(root, "relaxed").unwrap();
assert!(
!r.required.iter().any(|c| c == "scope::files-whitelist"),
"relaxed cap must be removed from the inherited list"
);
assert!(r.required.iter().any(|c| c == "quality::cargo-check-green"));
assert!(r.required.iter().any(|c| c == "output::report-format"));
}
#[test]
fn flat_role_without_extends_still_works() {
let tmp = TempDir::new().unwrap();
let root = tmp.path();
write_role(
root,
"flat",
r#"
[role]
name = "flat"
[capabilities]
required = ["policy::no-git-ops", "output::report-format"]
"#,
);
let r = resolve_role(root, "flat").unwrap();
assert_eq!(r.required.len(), 2);
assert_eq!(r.required[0], "policy::no-git-ops");
assert_eq!(r.required[1], "output::report-format");
}