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.
271 lines
9.5 KiB
Rust
271 lines
9.5 KiB
Rust
//! Integration tests for the R1–R19 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"));
|
||
}
|