KeiSeiKit-1.0/_primitives/_rust/kei-pet/tests/validation_tests.rs
Parfii-bot 0be354a920 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

271 lines
9.5 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.

//! Integration tests for the R1R19 validator.
//!
//! Each `reject_*` test asserts a specific rule fires with a specific variant.
//! Accepting cases parse `examples/minimal.toml` and `examples/full.toml`
//! unmodified.
use kei_pet::{parse, validate};
use kei_pet::schema::*;
use kei_pet::validate::ValidationError;
const MINIMAL: &str = include_str!("../examples/minimal.toml");
const FULL: &str = include_str!("../examples/full.toml");
fn base() -> PetManifest {
parse(MINIMAL).expect("minimal.toml must validate")
}
#[test]
fn accept_minimal_example() {
let m = parse(MINIMAL).unwrap();
assert_eq!(m.schema, 1);
assert_eq!(m.identity.pet_name, "Kei");
}
#[test]
fn accept_full_example() {
let m = parse(FULL).unwrap();
assert_eq!(m.interests.len(), 2);
assert_eq!(m.routines.len(), 4);
assert!(m.appearance.is_some());
assert!(m.room.is_some());
assert!(m.privacy.is_some());
}
// ─────────────────────────── R1 schema version ───────────────────────────
#[test]
fn r1_wrong_schema() {
let mut m = base();
m.schema = 99;
let errs = validate(&m).unwrap_err();
assert!(errs.iter().any(|e| matches!(e, ValidationError::SchemaVersion(99))));
}
// ──────────────────────────── R2 name bounds ─────────────────────────────
#[test]
fn r2_empty_pet_name() {
let mut m = base();
m.identity.pet_name.clear();
assert!(validate(&m).unwrap_err().contains(&ValidationError::PetNameInvalid));
}
#[test]
fn r2_pet_name_too_long() {
let mut m = base();
m.identity.pet_name = "a".repeat(25);
assert!(validate(&m).unwrap_err().contains(&ValidationError::PetNameInvalid));
}
#[test]
fn r2_empty_user_name() {
let mut m = base();
m.identity.user_name.clear();
assert!(validate(&m).unwrap_err().contains(&ValidationError::UserNameInvalid));
}
// ───────────────────────────── R4 languages ──────────────────────────────
#[test]
fn r4_empty_languages() {
let mut m = base();
m.identity.languages.clear();
assert!(validate(&m).unwrap_err().contains(&ValidationError::LanguagesEmpty));
}
#[test]
fn r4_non_iso_language() {
let mut m = base();
m.identity.languages = vec!["english".to_string()];
let errs = validate(&m).unwrap_err();
assert!(errs.iter().any(|e| matches!(e, ValidationError::LanguageNotIso(0, s) if s == "english")));
}
// ─────────────────────────────── R6 tones ────────────────────────────────
#[test]
fn r6_too_many_secondary_tones() {
let mut m = base();
m.voice.tone_secondary = vec![Tone::Warm, Tone::Sarcastic, Tone::Supportive];
let errs = validate(&m).unwrap_err();
assert!(errs.iter().any(|e| matches!(e, ValidationError::ToneSecondaryTooMany(3))));
}
#[test]
fn r6_primary_duplicated_in_secondary() {
let mut m = base();
m.voice.tone_primary = Tone::Warm;
m.voice.tone_secondary = vec![Tone::Warm];
let errs = validate(&m).unwrap_err();
assert!(errs.iter().any(|e| matches!(e, ValidationError::ToneSecondaryDuplicatePrimary(Tone::Warm))));
}
// ───────────────────────── R10 profanity/languages ───────────────────────
#[test]
fn r10_never_with_language_list_populated() {
let mut m = base();
m.edge.profanity = Profanity::Never;
m.edge.profanity_languages = vec!["en".to_string()];
assert!(validate(&m).unwrap_err().contains(&ValidationError::ProfanityLanguagesWhenNever));
}
#[test]
fn r10_profanity_language_not_in_identity() {
let mut m = base();
m.edge.profanity = Profanity::MirrorUser;
m.edge.profanity_languages = vec!["de".to_string()];
let errs = validate(&m).unwrap_err();
assert!(errs.iter().any(|e| matches!(e, ValidationError::ProfanityLanguageNotDeclared(s) if s == "de")));
}
// ──────────────────────────── R12 slug-safe ──────────────────────────────
#[test]
fn r12_non_slug_topic() {
let mut m = base();
m.interests = vec![Interest {
topic: "Distributed Systems".into(),
depth: Depth::Expert,
freshness: Freshness::Weekly,
vault_path: String::new(),
last_refresh: String::new(),
}];
let errs = validate(&m).unwrap_err();
assert!(errs.iter().any(|e| matches!(e, ValidationError::InterestTopicNotSlug(0, _))));
}
#[test]
fn r12_leading_dash() {
let mut m = base();
m.interests = vec![Interest {
topic: "-bad".into(),
depth: Depth::Expert,
freshness: Freshness::Weekly,
vault_path: String::new(),
last_refresh: String::new(),
}];
assert!(validate(&m).is_err());
}
// ──────────────────────── R14 interest/forbidden overlap ─────────────────
#[test]
fn r14_interest_in_forbidden() {
let mut m = base();
m.interests = vec![Interest {
topic: "ai-hype".into(),
depth: Depth::Shallow,
freshness: Freshness::OnDemand,
vault_path: String::new(),
last_refresh: String::new(),
}];
m.forbidden.topics = vec!["ai-hype".into()];
let errs = validate(&m).unwrap_err();
assert!(errs.iter().any(|e| matches!(e, ValidationError::InterestForbiddenContradiction(0, _))));
}
// ────────────────────────────── R16 schedules ────────────────────────────
#[test]
fn r16_valid_schedules_accepted() {
let mut m = base();
for sched in &[
"09:00", "23:59", "00:00",
"sun-10:00", "mon-08:30",
"every-4h", "no-commit-for-3h",
"3-errors-in-20-calls",
] {
m.routines = vec![Routine {
kind: RoutineKind::Custom,
schedule: (*sched).to_string(),
template: "pet-routine-morning".to_string(),
enabled: true,
}];
validate(&m).unwrap_or_else(|e| panic!("schedule '{sched}' rejected: {e:?}"));
}
}
#[test]
fn r16_invalid_schedule() {
let mut m = base();
m.routines = vec![Routine {
kind: RoutineKind::Custom,
schedule: "whenever".into(),
template: "pet-routine-morning".into(),
enabled: true,
}];
let errs = validate(&m).unwrap_err();
assert!(errs.iter().any(|e| matches!(e, ValidationError::RoutineScheduleInvalid(0, s) if s == "whenever")));
}
#[test]
fn r16_invalid_hour() {
let mut m = base();
m.routines = vec![Routine {
kind: RoutineKind::Custom,
schedule: "25:00".into(),
template: "x".into(),
enabled: true,
}];
assert!(validate(&m).is_err());
}
// ────────────────────────────── R18 empty strings ────────────────────────
#[test]
fn r18_empty_forbidden_topic() {
let mut m = base();
m.forbidden.topics = vec![" ".into()];
let errs = validate(&m).unwrap_err();
assert!(errs.iter().any(|e| matches!(e, ValidationError::ForbiddenTopicEmpty(0))));
}
// ──────────────────────────────── R19 ISO-8601 ───────────────────────────
#[test]
fn r19_bad_timestamp() {
let mut m = base();
m.meta.created_at = "yesterday".into();
let errs = validate(&m).unwrap_err();
assert!(errs.iter().any(|e| matches!(e, ValidationError::MetaTimestampInvalid("created_at", _))));
}
// ─────────────────────────────── hex colours ─────────────────────────────
#[test]
fn hex_color_invalid() {
let mut m = parse(FULL).unwrap();
if let Some(ref mut app) = m.appearance {
app.color_primary = "brown".into();
}
let errs = validate(&m).unwrap_err();
assert!(errs.iter().any(|e| matches!(e, ValidationError::HexColorInvalid(s) if s == "brown")));
}
// ─────────────────────── multiple errors accumulate ──────────────────────
#[test]
fn errors_accumulate_not_fail_fast() {
let mut m = base();
m.schema = 99; // R1
m.identity.pet_name.clear(); // R2
m.identity.languages.clear(); // R4
let errs = validate(&m).unwrap_err();
// Ensure we got ≥3 distinct errors, proving we accumulated rather than
// short-circuited on the first.
assert!(errs.len() >= 3, "expected ≥3 accumulated errors, got {}: {errs:?}", errs.len());
}
// ─────────────────────────────── overlay smoke ───────────────────────────
#[test]
fn overlay_contains_names() {
let m = parse(FULL).unwrap();
let overlay = kei_pet::system_prompt(&m);
assert!(overlay.contains("Kei"));
assert!(overlay.contains("Denis"));
assert!(overlay.contains("distributed-systems"));
assert!(overlay.contains("politics"));
}