KeiSeiKit-1.0/_primitives/_rust/kei-prune/tests/prune_integration.rs
Parfii-bot a4e667de10 KeiSeiKit-public — clean state
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.
2026-05-01 12:09:03 +08:00

196 lines
6.9 KiB
Rust

//! Integration tests for kei-prune. In-memory SQLite with a minimal
//! `agents` table mirroring kei-ledger's shape; sidecar installed via
//! `ensure_schema`. No kei-ledger dep — synthetic rows inserted directly.
use kei_prune::{candidates, ensure_schema, mark_retired, stats, PruneError};
use rusqlite::{params, Connection};
/// Seconds per day — must match `prune.rs`.
const SECONDS_PER_DAY: i64 = 86_400;
/// Fixed "now" used by every test so age arithmetic is deterministic.
/// 2026-04-23 00:00 UTC-ish — any constant works; only deltas matter.
const FIXED_NOW: i64 = 1_745_366_400;
/// Minimal `agents` DDL — the column set kei-prune actually reads.
/// No kei-ledger CHECK on `status` — we test the library filter.
const AGENTS_DDL: &str = "\
CREATE TABLE agents (
id TEXT PRIMARY KEY,
branch TEXT NOT NULL,
parent_branch TEXT,
spec_sha TEXT NOT NULL,
status TEXT NOT NULL,
started_ts INTEGER NOT NULL,
finished_ts INTEGER,
summary TEXT,
worktree_path TEXT,
dna TEXT
);
";
/// Build an in-memory DB with the ledger shape + sidecar installed.
fn setup() -> Connection {
let conn = Connection::open_in_memory().expect("open in-memory");
conn.execute_batch(AGENTS_DDL).expect("agents DDL");
ensure_schema(&conn).expect("ensure_schema");
conn
}
/// Helper: insert a synthetic agent row.
fn insert_agent(
conn: &Connection,
id: &str,
status: &str,
started_ts: i64,
finished_ts: Option<i64>,
dna: Option<&str>,
) {
conn.execute(
"INSERT INTO agents (id, branch, spec_sha, status, started_ts, finished_ts, dna)
VALUES (?, ?, ?, ?, ?, ?, ?)",
params![
id,
format!("agent/{id}"),
"deadbeef",
status,
started_ts,
finished_ts,
dna,
],
)
.expect("insert agent");
}
/// Helper: timestamp `days` days before FIXED_NOW.
fn days_ago(days: i64) -> i64 {
FIXED_NOW - days * SECONDS_PER_DAY
}
// --- tests ------------------------------------------------------------
#[test]
fn candidates_returns_empty_on_fresh_db() {
let conn = setup();
let out = candidates(&conn, FIXED_NOW, 90).expect("candidates");
assert!(out.is_empty(), "fresh DB must yield zero candidates");
}
#[test]
fn candidates_excludes_active_rows() {
let conn = setup();
// Row started 3 days ago — far below 90-day threshold.
insert_agent(&conn, "a1", "running", days_ago(3), None, Some("dna-a1"));
let out = candidates(&conn, FIXED_NOW, 90).expect("candidates");
assert!(out.is_empty(), "recent row must not be a candidate");
}
#[test]
fn candidates_returns_idle_over_threshold() {
let conn = setup();
insert_agent(&conn, "old1", "done", days_ago(120), Some(days_ago(119)), Some("dna-old1"));
insert_agent(&conn, "young", "running", days_ago(5), None, Some("dna-young"));
let out = candidates(&conn, FIXED_NOW, 90).expect("candidates");
assert_eq!(out.len(), 1, "only the idle row should surface");
assert_eq!(out[0].id, "old1");
assert_eq!(out[0].dna, "dna-old1");
assert_eq!(out[0].age_days, 120);
assert_eq!(out[0].last_used_ts, days_ago(119));
}
#[test]
fn candidates_respects_min_idle_days() {
let conn = setup();
// Row age exactly 90 days — boundary inclusive per spec (>=).
insert_agent(&conn, "edge", "merged", days_ago(90), Some(days_ago(90)), None);
// Row age 89 days — below threshold.
insert_agent(&conn, "below", "done", days_ago(89), Some(days_ago(89)), None);
let out_90 = candidates(&conn, FIXED_NOW, 90).expect("at 90");
assert_eq!(out_90.len(), 1, "90d-old row must match at threshold=90");
assert_eq!(out_90[0].id, "edge");
let out_91 = candidates(&conn, FIXED_NOW, 91).expect("at 91");
assert!(out_91.is_empty(), "90d-old row must NOT match at threshold=91");
}
#[test]
fn mark_retired_inserts_sidecar_row() {
let conn = setup();
insert_agent(&conn, "victim", "done", days_ago(200), Some(days_ago(199)), None);
mark_retired(&conn, "victim", FIXED_NOW).expect("mark_retired");
let count: i64 = conn
.query_row(
"SELECT COUNT(*) FROM prune_retirements WHERE agent_id = ?",
params!["victim"],
|r| r.get(0),
)
.unwrap();
assert_eq!(count, 1, "retirement row must be present exactly once");
}
#[test]
fn mark_retired_idempotent() {
let conn = setup();
insert_agent(&conn, "idempot", "done", days_ago(200), None, None);
mark_retired(&conn, "idempot", FIXED_NOW).expect("first mark");
let first_ts: i64 = conn
.query_row(
"SELECT retired_ts FROM prune_retirements WHERE agent_id = ?",
params!["idempot"],
|r| r.get(0),
)
.unwrap();
// Second call at a later "now" — must NOT overwrite.
mark_retired(&conn, "idempot", FIXED_NOW + 10_000).expect("second mark");
let second_ts: i64 = conn
.query_row(
"SELECT retired_ts FROM prune_retirements WHERE agent_id = ?",
params!["idempot"],
|r| r.get(0),
)
.unwrap();
assert_eq!(first_ts, second_ts, "repeat mark must preserve original ts");
let count: i64 = conn
.query_row("SELECT COUNT(*) FROM prune_retirements", [], |r| r.get(0))
.unwrap();
assert_eq!(count, 1, "no duplicate rows");
}
#[test]
fn mark_retired_rejects_unknown_agent() {
let conn = setup();
let err = mark_retired(&conn, "ghost", FIXED_NOW).expect_err("unknown id must error");
match err {
PruneError::UnknownAgent(id) => assert_eq!(id, "ghost"),
other => panic!("expected UnknownAgent, got {other:?}"),
}
}
#[test]
fn stats_counts_buckets() {
let conn = setup();
// 2 active (running/done not yet retired) + 1 to-be-retired + 1 failed.
insert_agent(&conn, "act1", "running", days_ago(1), None, None);
insert_agent(&conn, "act2", "done", days_ago(10), Some(days_ago(9)), None);
insert_agent(&conn, "tired", "merged", days_ago(300), Some(days_ago(299)), None);
insert_agent(&conn, "fail1", "failed", days_ago(5), Some(days_ago(4)), None);
mark_retired(&conn, "tired", FIXED_NOW).expect("mark tired");
let s = stats(&conn).expect("stats");
assert_eq!(s.total, 4, "total = all rows");
assert_eq!(s.active, 2, "active = running/done/merged not retired");
assert_eq!(s.retired, 1, "retired = sidecar row count");
assert_eq!(s.idle, 0, "idle bucket is placeholder (candidates() is authoritative)");
}
#[test]
fn retired_rows_excluded_from_candidates() {
let conn = setup();
insert_agent(&conn, "stillhere", "done", days_ago(200), Some(days_ago(199)), None);
insert_agent(&conn, "gone", "done", days_ago(300), Some(days_ago(299)), None);
mark_retired(&conn, "gone", FIXED_NOW).expect("mark gone");
let out = candidates(&conn, FIXED_NOW, 90).expect("candidates");
assert_eq!(out.len(), 1, "only the non-retired row surfaces");
assert_eq!(out[0].id, "stillhere");
}