KeiSeiKit-1.0/_primitives/_rust/kei-pet/tests/fleet_tests.rs
Parfii-bot 07eb0b83ea feat(wave19): kei-pet Day 2 — 8 pet gaps closed via substrate dogfood
48 crates, 859 tests green (+58 kei-pet tests, was 801 at v0.35.0).

Full substrate pipeline test: all 8 agents launched via kei-agent-runtime
prepare → composed capability-fragment prompts → Agent tool invocations.
Zero file conflicts across disjoint scopes. Every agent self-verified
and landed files direct to main.

## A. memory (4 tests) — persistent conversations
- src/memory.rs — (user_id, pet_name)-scoped conversation log
- SQLite via rusqlite, index (user_id, pet_name, ts DESC)
- record_interaction / recent / search with LIKE-escape

## B. evolution (3 tests) — version diff + fork chain
- src/evolution.rs — PersonaVersion { version, parent_version, manifest }
- diff(old, new) → Vec<Change> (tone / directness / initiative / forbidden / humor)
- fork_version increments + links parent

## C. wizard (5 markdown phases) — /pet-init skill
- skills/pet-init/SKILL.md + 4 phases (identity / voice / edge / emit)
- AskUserQuestion-driven, no TOML editing for end users
- Writes ~/.claude/pet/<user_id>.toml + calls kei-pet keygen if needed

## D. templates (3 tests + 5 presets) — role-based personas
- templates/{friend,tutor,coach,therapist-companion,productivity-partner}.toml
- src/templates.rs — PetTemplate enum + load_template + list_templates
- Schema-enum mapping documented (dry→engineering-meta, etc) — schema.rs
  expansion is future work

## E. bridge (3 tests) — /spawn-agent pet overlay
- src/bridge.rs — compose_prompt_with_pet(base + persona overlay + task)
- skills/spawn-agent/phase-3-pet-overlay.md — interactive pet selector

## F. recall (4 tests) — "have we discussed this before?"
- src/recall.rs — wraps kei_dna_index::precedent with body_sha8()
- SHA-256 first 4 bytes → 8 hex lowercase (matches kei_shared width)
- Fetches started_ts per hit for honest sort-by-recency

## G. reflect (7 tests) — self-reflection threshold proposals
- src/reflect.rs — CorrectionSignal + ProposedChange
- Thresholds: 3× too_verbose → SetDirectness, 2× forbidden_topic → AddForbidden, etc
- Idempotent: no-op if manifest already in desired state

## H. fleet (6 tests) — multi-pet per user
- src/fleet.rs — PetFleet { user_id, pets, active_pet }
- add_pet / switch_active / load_fleet with toml persistence
- shared_memory_key vs per_pet_memory_key — one user scopes multiple pets

## Known follow-ups (not blockers)

- Phase-4-emit of /spawn-agent should read PET_MANIFEST_PATH from new
  phase-3-pet-overlay and pass to kei-spawn (wiring next wave)
- SKILL.md for spawn-agent should list new pet-overlay phase
- Schema enum expansion: humor_style "dry/witty", directness "direct/
  gentle/blunt", initiative "proactive/nudge" as first-class variants

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

83 lines
2.7 KiB
Rust

//! Hermetic tests for the multi-pet fleet module.
//!
//! Every test uses a fresh `tempfile::TempDir` as the fleet_root, so no
//! test touches real user state and no test depends on another's side
//! effects.
use std::path::PathBuf;
use kei_pet::fleet::{
add_pet, load_fleet, per_pet_memory_key, shared_memory_key, switch_active, PetHandle,
};
fn mk_handle(name: &str, role: &str) -> PetHandle {
PetHandle {
pet_name: name.to_string(),
role: role.to_string(),
manifest_path: PathBuf::from(format!("/tmp/{name}.toml")),
last_active: 0,
}
}
#[test]
fn load_fleet_empty_returns_zero_pets() {
let dir = tempfile::TempDir::new().expect("tempdir");
let fleet = load_fleet("user-alpha", dir.path()).expect("load empty");
assert_eq!(fleet.user_id, "user-alpha");
assert!(fleet.pets.is_empty());
assert!(fleet.active_pet.is_none());
}
#[test]
fn add_pet_persists_to_disk() {
let dir = tempfile::TempDir::new().expect("tempdir");
let handle = mk_handle("mira", "friend");
add_pet("user-alpha", handle, dir.path()).expect("add");
let fleet = load_fleet("user-alpha", dir.path()).expect("reload");
assert_eq!(fleet.pets.len(), 1);
assert_eq!(fleet.pets[0].pet_name, "mira");
assert_eq!(fleet.pets[0].role, "friend");
// First add should seed active_pet.
assert_eq!(fleet.active_pet.as_deref(), Some("mira"));
}
#[test]
fn switch_active_updates_file() {
let dir = tempfile::TempDir::new().expect("tempdir");
add_pet("user-alpha", mk_handle("mira", "friend"), dir.path()).expect("add 1");
add_pet("user-alpha", mk_handle("nova", "tutor"), dir.path()).expect("add 2");
switch_active("user-alpha", "nova", dir.path()).expect("switch");
let fleet = load_fleet("user-alpha", dir.path()).expect("reload");
assert_eq!(fleet.pets.len(), 2);
assert_eq!(fleet.active_pet.as_deref(), Some("nova"));
}
#[test]
fn memory_keys_differ_per_pet_same_user() {
let a = per_pet_memory_key("user-alpha", "mira");
let b = per_pet_memory_key("user-alpha", "nova");
assert_ne!(a, b);
assert!(a.contains("user-alpha"));
assert!(a.contains("mira"));
assert!(b.contains("nova"));
}
#[test]
fn shared_memory_key_stable() {
let k1 = shared_memory_key("user-alpha");
let k2 = shared_memory_key("user-alpha");
assert_eq!(k1, k2);
assert_ne!(k1, shared_memory_key("user-beta"));
}
#[test]
fn switch_active_errors_when_pet_absent() {
let dir = tempfile::TempDir::new().expect("tempdir");
add_pet("user-alpha", mk_handle("mira", "friend"), dir.path()).expect("add");
let err = switch_active("user-alpha", "ghost", dir.path()).unwrap_err();
assert!(matches!(err, kei_pet::fleet::FleetError::PetNotInFleet(_)));
}