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.
188 lines
8.3 KiB
Rust
188 lines
8.3 KiB
Rust
//! Tests for the encyclopedia subcommand rendering.
|
|
//!
|
|
//! Six test cases: mixed-type markdown, JSON roundtrip, --type filter,
|
|
//! empty registry, supersede chain rendering, and section-count accuracy.
|
|
|
|
use kei_registry::encyclopedia::{render_json, render_markdown, to_entries, EncyclopediaEntry};
|
|
use kei_registry::{open_db, register, BlockType};
|
|
use tempfile::tempdir;
|
|
|
|
// ── helpers ────────────────────────────────────────────────────────────────
|
|
|
|
fn seed_db(conn: &rusqlite::Connection) {
|
|
register(conn, BlockType::Primitive, "kei-cache", "/tmp/kei-cache/Cargo.toml", b"body-p1", "").unwrap();
|
|
register(conn, BlockType::Skill, "deploy-kit", "/tmp/skills/deploy-kit/SKILL.md", b"body-s1", "deploy").unwrap();
|
|
register(conn, BlockType::Rule, "code-style-1", "/tmp/rules/code-style.md", b"body-r1", "").unwrap();
|
|
register(conn, BlockType::Hook, "disk-guard", "/tmp/hooks/disk-guard.sh", b"body-h1", "PreToolUse:Bash").unwrap();
|
|
register(conn, BlockType::Atom, "atom-one", "/tmp/atoms/atom-one.md", b"body-a1", "").unwrap();
|
|
}
|
|
|
|
// ── test 1: markdown contains all 5 names and correct counts table ─────────
|
|
|
|
#[test]
|
|
fn markdown_contains_all_names_and_counts() {
|
|
let tmp = tempdir().unwrap();
|
|
let conn = open_db(tmp.path().join("r.sqlite")).unwrap();
|
|
seed_db(&conn);
|
|
|
|
let active = kei_registry::list(&conn, false, 1000).unwrap();
|
|
let all = kei_registry::list(&conn, true, 1000).unwrap();
|
|
let entries = to_entries(&active);
|
|
let md = render_markdown(&entries, &all);
|
|
|
|
assert!(md.contains("kei-cache"), "primitive name present");
|
|
assert!(md.contains("deploy-kit"), "skill name present");
|
|
assert!(md.contains("code-style-1"), "rule name present");
|
|
assert!(md.contains("disk-guard"), "hook name present");
|
|
assert!(md.contains("atom-one"), "atom name present");
|
|
|
|
assert!(md.contains("Total blocks: 5"), "total count in header");
|
|
assert!(md.contains("| primitive | 1 |"), "primitive count row");
|
|
assert!(md.contains("| skill | 1 |"), "skill count row");
|
|
assert!(md.contains("| rule | 1 |"), "rule count row");
|
|
assert!(md.contains("| hook | 1 |"), "hook count row");
|
|
assert!(md.contains("| atom | 1 |"), "atom count row");
|
|
}
|
|
|
|
// ── test 2: JSON roundtrip ─────────────────────────────────────────────────
|
|
|
|
#[test]
|
|
fn json_roundtrips_through_serde() {
|
|
let tmp = tempdir().unwrap();
|
|
let conn = open_db(tmp.path().join("r.sqlite")).unwrap();
|
|
seed_db(&conn);
|
|
|
|
let active = kei_registry::list(&conn, false, 1000).unwrap();
|
|
let entries = to_entries(&active);
|
|
let json_str = render_json(&entries).unwrap();
|
|
|
|
let parsed: kei_registry::encyclopedia::Encyclopedia =
|
|
serde_json::from_str(&json_str).expect("valid JSON");
|
|
assert_eq!(parsed.total_blocks, 5);
|
|
assert_eq!(parsed.blocks.len(), 5);
|
|
assert!(parsed.counts.contains_key("primitive"));
|
|
assert!(parsed.counts.contains_key("hook"));
|
|
}
|
|
|
|
// ── test 3: --type filter returns only matching rows ──────────────────────
|
|
|
|
#[test]
|
|
fn type_filter_returns_only_matching_type() {
|
|
let tmp = tempdir().unwrap();
|
|
let conn = open_db(tmp.path().join("r.sqlite")).unwrap();
|
|
seed_db(&conn);
|
|
|
|
let rule_blocks = kei_registry::list_by_type(&conn, BlockType::Rule).unwrap();
|
|
let all = kei_registry::list(&conn, true, 1000).unwrap();
|
|
let entries = to_entries(&rule_blocks);
|
|
let md = render_markdown(&entries, &all);
|
|
|
|
assert!(md.contains("code-style-1"), "rule name present");
|
|
assert!(!md.contains("kei-cache"), "primitive absent from rule-filtered output");
|
|
assert!(!md.contains("deploy-kit"), "skill absent from rule-filtered output");
|
|
// Counts table reflects only the filtered type.
|
|
assert!(md.contains("Total blocks: 1"), "total = 1 for rule filter");
|
|
}
|
|
|
|
// ── test 4: empty registry renders without panic ──────────────────────────
|
|
|
|
#[test]
|
|
fn empty_registry_renders_without_panic() {
|
|
let tmp = tempdir().unwrap();
|
|
let conn = open_db(tmp.path().join("r.sqlite")).unwrap();
|
|
|
|
let active = kei_registry::list(&conn, false, 1000).unwrap();
|
|
let all = kei_registry::list(&conn, true, 1000).unwrap();
|
|
let entries = to_entries(&active);
|
|
let md = render_markdown(&entries, &all);
|
|
|
|
assert!(md.contains("Total blocks: 0"), "zero total shown");
|
|
assert!(!md.is_empty(), "output is not blank");
|
|
|
|
let json_str = render_json(&entries).unwrap();
|
|
let parsed: kei_registry::encyclopedia::Encyclopedia =
|
|
serde_json::from_str(&json_str).unwrap();
|
|
assert_eq!(parsed.total_blocks, 0);
|
|
assert!(parsed.blocks.is_empty());
|
|
}
|
|
|
|
// ── test 5: supersede chain renders correctly ─────────────────────────────
|
|
|
|
#[test]
|
|
fn supersede_chain_renders_in_markdown() {
|
|
let tmp = tempdir().unwrap();
|
|
let conn = open_db(tmp.path().join("r.sqlite")).unwrap();
|
|
|
|
let path = "/tmp/evolving-block.md";
|
|
let _v1 = register(&conn, BlockType::Rule, "evolving", path, b"body v1", "").unwrap();
|
|
let _v2 = register(&conn, BlockType::Rule, "evolving", path, b"body v2 changed", "").unwrap();
|
|
|
|
let active = kei_registry::list(&conn, false, 1000).unwrap();
|
|
let all = kei_registry::list(&conn, true, 1000).unwrap();
|
|
let entries = to_entries(&active);
|
|
let md = render_markdown(&entries, &all);
|
|
|
|
// Active section: only 1 row (the newest).
|
|
assert!(md.contains("Total blocks: 1"), "only active counted");
|
|
// Supersede chain section must appear with 2 versions.
|
|
assert!(md.contains("Supersede chains"), "chain section present");
|
|
assert!(md.contains("`evolving`"), "name in chain section");
|
|
assert!(md.contains("2 versions"), "two versions noted");
|
|
}
|
|
|
|
// ── test 6: section headers only appear for non-empty types ───────────────
|
|
|
|
#[test]
|
|
fn section_headers_only_for_non_empty_types() {
|
|
let tmp = tempdir().unwrap();
|
|
let conn = open_db(tmp.path().join("r.sqlite")).unwrap();
|
|
|
|
// Insert only a primitive and a hook.
|
|
register(&conn, BlockType::Primitive, "kei-log", "/tmp/kei-log/Cargo.toml", b"body", "").unwrap();
|
|
register(&conn, BlockType::Hook, "stop-verify", "/tmp/hooks/stop-verify.sh", b"body", "Stop").unwrap();
|
|
|
|
let active = kei_registry::list(&conn, false, 1000).unwrap();
|
|
let all = kei_registry::list(&conn, true, 1000).unwrap();
|
|
let entries = to_entries(&active);
|
|
let md = render_markdown(&entries, &all);
|
|
|
|
assert!(md.contains("## Primitive"), "primitive section present");
|
|
assert!(md.contains("## Hook"), "hook section present");
|
|
// Types with zero rows must NOT appear.
|
|
assert!(!md.contains("## Skill"), "skill section absent");
|
|
assert!(!md.contains("## Rule"), "rule section absent");
|
|
assert!(!md.contains("## Atom"), "atom section absent");
|
|
}
|
|
|
|
// ── test 7: to_entries ordering is stable (alpha by type then name) ────────
|
|
|
|
#[test]
|
|
fn entries_sorted_by_type_then_name() {
|
|
let tmp = tempdir().unwrap();
|
|
let conn = open_db(tmp.path().join("r.sqlite")).unwrap();
|
|
|
|
register(&conn, BlockType::Atom, "z-atom", "/tmp/z.md", b"z", "").unwrap();
|
|
register(&conn, BlockType::Atom, "a-atom", "/tmp/a.md", b"a", "").unwrap();
|
|
register(&conn, BlockType::Primitive, "mid-prim", "/tmp/mid/Cargo.toml", b"m", "").unwrap();
|
|
|
|
let active = kei_registry::list(&conn, false, 1000).unwrap();
|
|
let entries = to_entries(&active);
|
|
|
|
// "atom" sorts before "primitive" alphabetically.
|
|
let types: Vec<&str> = entries.iter().map(|e| e.block_type.as_str()).collect();
|
|
let prim_idx = types.iter().position(|&t| t == "primitive").unwrap();
|
|
let first_atom = types.iter().position(|&t| t == "atom").unwrap();
|
|
assert!(first_atom < prim_idx, "atom before primitive (alphabetical)");
|
|
|
|
// Within atom, a-atom before z-atom.
|
|
let atom_names: Vec<&str> = entries
|
|
.iter()
|
|
.filter(|e| e.block_type == "atom")
|
|
.map(|e| e.name.as_str())
|
|
.collect();
|
|
assert_eq!(atom_names, vec!["a-atom", "z-atom"]);
|
|
}
|
|
|
|
// suppress unused import warning in certain build configs
|
|
#[allow(dead_code)]
|
|
fn _use_entry_type(_: EncyclopediaEntry) {}
|