KeiSeiKit-1.0/_primitives/_rust/kei-ledger/tests/integration.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

162 lines
5.6 KiB
Rust

//! Integration tests for kei-ledger.
//!
//! Constructor Pattern: each test = one scenario, one assertion target.
//! Uses tempfile for per-test isolated sqlite file. Loads source modules
//! via `#[path]` so we don't need to expose a library crate surface.
#[path = "../src/schema.rs"]
mod schema;
#[path = "../src/ledger.rs"]
mod ledger;
use rusqlite::Connection;
use std::fs;
use std::path::{Path, PathBuf};
use tempfile::TempDir;
fn open_tmp() -> (TempDir, Connection) {
let dir = tempfile::tempdir().unwrap();
let db = dir.path().join("ledger.sqlite");
let conn = ledger::open(&db).unwrap();
(dir, conn)
}
fn write_artefacts(root: &Path, agent_id: &str, which: &[&str]) -> PathBuf {
let base = root.join(".claude/agents").join(agent_id);
fs::create_dir_all(&base).unwrap();
for f in which {
fs::write(base.join(f), b"x").unwrap();
}
base
}
#[test]
fn fork_then_done_marks_terminal() {
let (_d, conn) = open_tmp();
ledger::fork(&conn, "a1", "agent/a1", None, "deadbeef", None, None).unwrap();
let running = ledger::list(&conn, Some("running")).unwrap();
assert_eq!(running.len(), 1);
assert_eq!(running[0].id, "a1");
let updated = ledger::done(&conn, "a1", "shipped").unwrap();
assert_eq!(updated, 1);
let done = ledger::list(&conn, Some("done")).unwrap();
assert_eq!(done.len(), 1);
assert_eq!(done[0].summary.as_deref(), Some("shipped"));
}
#[test]
fn fail_flow_sets_reason_and_finished_ts() {
let (_d, conn) = open_tmp();
ledger::fork(&conn, "b1", "agent/b1", Some("main"), "cafebabe", None, None).unwrap();
let updated = ledger::fail(&conn, "b1", "cargo build failed").unwrap();
assert_eq!(updated, 1);
let failed = ledger::list(&conn, Some("failed")).unwrap();
assert_eq!(failed.len(), 1);
assert!(failed[0].finished_ts.is_some());
assert_eq!(failed[0].summary.as_deref(), Some("cargo build failed"));
}
#[test]
fn tree_walks_parent_child_chain() {
let (_d, conn) = open_tmp();
ledger::fork(&conn, "root", "agent/root", Some("main"), "aa", None, None).unwrap();
ledger::fork(&conn, "c1", "agent/c1", Some("agent/root"), "bb", None, None).unwrap();
ledger::fork(&conn, "c2", "agent/c2", Some("agent/root"), "cc", None, None).unwrap();
ledger::fork(&conn, "g1", "agent/g1", Some("agent/c1"), "dd", None, None).unwrap();
let t = ledger::tree(&conn, "root").unwrap();
let ids: Vec<_> = t.iter().map(|a| a.id.as_str()).collect();
assert!(ids.contains(&"root"));
assert!(ids.contains(&"c1"));
assert!(ids.contains(&"c2"));
assert!(ids.contains(&"g1"));
assert_eq!(ids[0], "root");
assert_eq!(ids.len(), 4);
}
#[test]
fn list_filter_status_excludes_others() {
let (_d, conn) = open_tmp();
ledger::fork(&conn, "r1", "br-r1", None, "s1", None, None).unwrap();
ledger::fork(&conn, "r2", "br-r2", None, "s2", None, None).unwrap();
ledger::done(&conn, "r1", "ok").unwrap();
let running = ledger::list(&conn, Some("running")).unwrap();
assert_eq!(running.len(), 1);
assert_eq!(running[0].id, "r2");
let all = ledger::list(&conn, None).unwrap();
assert_eq!(all.len(), 2);
}
#[test]
fn validate_detects_missing_artefacts() {
let (d, _conn) = open_tmp();
write_artefacts(d.path(), "v1", &["spec.md", "plan.md"]);
let missing = ledger::validate(d.path(), "v1");
assert_eq!(missing.len(), 4);
assert!(missing.contains(&"progress.json".to_string()));
assert!(missing.contains(&"review.md".to_string()));
}
#[test]
fn validate_ok_when_all_six_present() {
let (d, _conn) = open_tmp();
write_artefacts(
d.path(),
"v2",
&[
"spec.md",
"plan.md",
"progress.json",
"chatlog.md",
"handoffs.md",
"review.md",
],
);
let missing = ledger::validate(d.path(), "v2");
assert!(missing.is_empty(), "got missing {missing:?}");
}
#[test]
fn duplicate_fork_id_rejected() {
let (_d, conn) = open_tmp();
ledger::fork(&conn, "dup", "br1", None, "x", None, None).unwrap();
let err = ledger::fork(&conn, "dup", "br2", None, "y", None, None);
assert!(err.is_err(), "duplicate id must fail");
}
#[test]
fn done_on_already_done_agent_is_noop() {
let (_d, conn) = open_tmp();
ledger::fork(&conn, "n1", "br-n1", None, "h", None, None).unwrap();
assert_eq!(ledger::done(&conn, "n1", "first").unwrap(), 1);
assert_eq!(ledger::done(&conn, "n1", "second").unwrap(), 0);
let row = &ledger::list(&conn, None).unwrap()[0];
assert_eq!(row.summary.as_deref(), Some("first"));
}
#[test]
fn fork_with_dna_roundtrips_through_list() {
let (_d, conn) = open_tmp();
let dna = "edit-local::NG-FW-FD-CP-CG-TG-ND-RF::A7B2::C9F1-xa7c";
ledger::fork(&conn, "dna1", "agent/dna1", None, "spec", None, Some(dna)).unwrap();
let rows = ledger::list(&conn, None).unwrap();
assert_eq!(rows.len(), 1);
assert_eq!(rows[0].dna.as_deref(), Some(dna));
ledger::fork(&conn, "legacy1", "agent/legacy1", None, "spec2", None, None).unwrap();
let rows = ledger::list(&conn, None).unwrap();
let legacy = rows.iter().find(|r| r.id == "legacy1").unwrap();
assert!(legacy.dna.is_none(), "legacy fork should leave dna NULL");
}
#[test]
fn merged_after_done_transitions_status() {
let (_d, conn) = open_tmp();
ledger::fork(&conn, "m1", "br-m1", None, "h", None, None).unwrap();
ledger::done(&conn, "m1", "ready").unwrap();
assert_eq!(ledger::merged(&conn, "m1").unwrap(), 1);
let merged = ledger::list(&conn, Some("merged")).unwrap();
assert_eq!(merged.len(), 1);
assert_eq!(merged[0].summary.as_deref(), Some("ready"));
}