KeiSeiKit-1.0/_primitives/_rust/kei-pet/tests/reflect_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

163 lines
4.9 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! Hermetic tests for `kei_pet::reflect::propose_tune`.
//!
//! Each test builds an in-memory `PetManifest` (no disk, no TOML parsing)
//! so the logic is tested in isolation from schema serialization.
use kei_pet::reflect::{propose_tune, CorrectionSignal, ProposedChange};
use kei_pet::schema::{
Addressing, Directness, Edge, Forbidden, HumorFrequency, HumorStyle,
Identity, Initiative, Meta, PetManifest, Profanity, Tone, Voice,
};
fn base_manifest() -> PetManifest {
PetManifest {
schema: 1,
identity: Identity {
pet_name: "Kei".into(),
user_name: "Alex".into(),
addressing: Addressing::ByName,
languages: vec!["en".into()],
},
voice: Voice {
tone_primary: Tone::Neutral,
tone_secondary: vec![],
humor_style: HumorStyle::None,
humor_frequency: HumorFrequency::Rare,
},
edge: Edge {
profanity: Profanity::Never,
profanity_languages: vec![],
directness: Directness::Balanced,
initiative: Initiative::Wait,
},
appearance: None,
room: None,
privacy: None,
interests: vec![],
routines: vec![],
forbidden: Forbidden {
topics: vec![],
tone_patterns: vec![],
},
meta: Meta {
schema_version_written_by: "kei-pet 0.1.0".into(),
created_at: "2026-04-23T12:00:00Z".into(),
last_tuned: "2026-04-23T12:00:00Z".into(),
tune_count: 0,
},
}
}
fn sig(topic: &str, ts: i64) -> CorrectionSignal {
CorrectionSignal {
timestamp: ts,
topic: topic.into(),
severity: 5,
note: None,
}
}
#[test]
fn propose_tune_empty_signals_returns_empty() {
let m = base_manifest();
let out = propose_tune(&m, &[]);
assert!(out.is_empty(), "empty signals → no proposals, got {out:?}");
}
#[test]
fn propose_tune_threshold_too_verbose_3() {
let m = base_manifest();
let signals = vec![
sig("too_verbose", 100),
sig("too_verbose", 101),
sig("too_verbose", 102),
];
let out = propose_tune(&m, &signals);
assert!(
out.contains(&ProposedChange::SetDirectness("direct".into())),
"3× too_verbose on balanced manifest must emit SetDirectness(direct); got {out:?}"
);
}
#[test]
fn propose_tune_below_threshold_too_verbose_2() {
let m = base_manifest();
let signals = vec![
sig("too_verbose", 100),
sig("too_verbose", 101),
];
let out = propose_tune(&m, &signals);
assert!(
!out.contains(&ProposedChange::SetDirectness("direct".into())),
"2× too_verbose is below threshold; got {out:?}"
);
}
#[test]
fn propose_tune_threshold_forbidden_2() {
let m = base_manifest();
let signals = vec![
sig("forbidden_topic:diagnosis", 100),
sig("forbidden_topic:diagnosis", 101),
];
let out = propose_tune(&m, &signals);
assert!(
out.contains(&ProposedChange::AddForbiddenTopic("diagnosis".into())),
"2× forbidden_topic:diagnosis on clean manifest must emit AddForbiddenTopic(diagnosis); got {out:?}"
);
}
#[test]
fn propose_tune_idempotent_directness_hard() {
let mut m = base_manifest();
m.edge.directness = Directness::Hard;
let signals = vec![
sig("too_verbose", 100),
sig("too_verbose", 101),
sig("too_verbose", 102),
sig("too_verbose", 103),
];
let out = propose_tune(&m, &signals);
assert!(
!out.iter().any(|c| matches!(c, ProposedChange::SetDirectness(_))),
"manifest already Hard → no SetDirectness proposal; got {out:?}"
);
}
#[test]
fn propose_tune_idempotent_forbidden_already_listed() {
let mut m = base_manifest();
m.forbidden.topics = vec!["diagnosis".into()];
let signals = vec![
sig("forbidden_topic:diagnosis", 100),
sig("forbidden_topic:diagnosis", 101),
sig("forbidden_topic:diagnosis", 102),
];
let out = propose_tune(&m, &signals);
assert!(
!out.iter().any(|c| matches!(c, ProposedChange::AddForbiddenTopic(_))),
"diagnosis already in forbidden list → no AddForbiddenTopic proposal; got {out:?}"
);
}
#[test]
fn propose_tune_initiative_and_tone_thresholds() {
let m = base_manifest();
let signals = vec![
sig("not_proactive_enough", 100),
sig("not_proactive_enough", 101),
sig("not_proactive_enough", 102),
sig("too_formal", 200),
sig("too_formal", 201),
sig("too_formal", 202),
];
let out = propose_tune(&m, &signals);
assert!(
out.contains(&ProposedChange::SetInitiative("proactive".into())),
"3× not_proactive_enough must emit SetInitiative(proactive); got {out:?}"
);
assert!(
out.contains(&ProposedChange::SetTonePrimary("warm".into())),
"3× too_formal must emit SetTonePrimary(warm); got {out:?}"
);
}