Something went wrong. Try again.
a tiny pet with a GPT-2 brain, living in your browser
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223//! Native checks of the shared rules. The browser build runs the same code;//! `server/tests/parity.mjs` compares the two builds step by step.use tokenling_core::{ body::{DAY, HOUR, MIN}, engine::{Command, Engine, Task}, save, world,};
const FIXTURE: &str = include_str!("../../test/fixtures/save-v1.json");// 2026-10-01 12:00 UTC.const NOON: f64 = 1_790_856_000_000.0;fn engine() -> Engine { let mut state = save::parse_save(FIXTURE, NOON, "UTC").unwrap(); state.last_tick = NOON; state.next_poop = NOON + 10.0 * HOUR; state.pet.as_mut().unwrap().hatched = NOON; Engine::new(state, NOON)}fn dice() -> impl FnMut() -> f64 { || 0.5}fn care(kind: &str, item: Option<&str>) -> Command { Command::Care { kind: kind.into(), item: item.map(Into::into), }}
#[test]fn older_saves_migrate_and_unreadable_ones_are_refused() { let state = save::parse_save(FIXTURE, NOON, "Europe/Berlin").unwrap(); assert_eq!(state.v, save::VERSION); assert_eq!(state.hosting.mode, "local"); assert_eq!(state.hosting.time_zone, "Europe/Berlin"); assert_eq!(state.pet.unwrap().name, "Mochi"); let newer = save::parse_save(r#"{"v":99}"#, NOON, "UTC").unwrap_err(); assert!(newer.newer); assert_eq!( save::parse_save("{", NOON, "UTC").unwrap_err().message, "it is damaged" ); assert!( save::parse_save(r#"{"v":1}"#, NOON, "UTC") .unwrap_err() .message .contains("missing") );}
#[test]fn stop_rules_and_context_fit_are_shared() { assert_eq!( world::stop("line", " hi\nSam:"), Some(("line", " hi".into())) ); assert_eq!( world::stop("line", " it is [clock]["), Some(("lookup", " it is [clock]".into())) ); assert_eq!(world::stop("line", " [recall]"), None); assert_eq!(world::stop("line", " [look:x]"), None); assert_eq!( world::stop("diary", " a day.\n## tokenling"), Some(("end", " a day.".into())) ); let lines: Vec<String> = (0..10).map(|i| format!("line {i}")).collect(); // One "token" per character: header 10 + prefix 6, budget 64 - 20 - 4 = 40. let prompt = world::fit("## header\n", &lines, "Mochi:", 20, 64, &mut |t| { Ok(t.chars().count()) }) .unwrap(); assert_eq!(prompt, "## header\nline 7\nline 8\nline 9\nMochi:"); assert!( world::fit("x".repeat(50).as_str(), &lines, "", 20, 64, &mut |t| Ok( t.len() )) .is_err() );}
#[test]fn a_line_continues_after_lookups_and_unsafe_lines_are_retried() { let e = engine(); let mut turn = e.chat_turn("react", NOON, ""); assert_eq!(turn.request().prefix, "Mochi:"); assert!(turn.feed(" hmm [recall:frogs]", "lookup")); assert!(turn.request().prefix.ends_with("[recall:frogs]{nothing}")); assert!( turn.feed(" shit", "line"), "a filtered line is generated again" ); assert_eq!(turn.request().prefix, "Mochi:"); assert!(!turn.feed(" ok then ", "line")); assert_eq!(turn.body(), Some("ok then")); let mut idle = e.chat_turn("idle", NOON, ""); assert_eq!((idle.request().max_new, idle.request().ctx), (40, 512)); for _ in 0..3 { idle.feed(" fuck", "line"); } assert_eq!(idle.body(), Some(""));}
#[test]fn offers_wait_for_the_brain_and_answers_apply_once() { let mut e = engine(); let mut d = dice(); e.command(care("feed", Some("cookie")), NOON, "", &mut d) .unwrap(); assert_eq!(e.pending.as_deref(), Some("react")); let refused = e.command(care("bath", None), NOON, "", &mut d).unwrap_err(); assert_eq!(refused.0, "Mochi is still deciding about a cookie."); let tummy = e.state.stats.tummy; e.commit_line("yum [yes]", NOON, "", &mut d); assert!(e.state.offer.is_none()); assert_eq!(e.state.stats.tummy, (tummy + 22.0).min(100.0)); assert!( e.effects .iter() .any(|x| x.kind == "eat" && x.arg == "cookie") ); e.command(care("medicine", None), NOON, "", &mut d).unwrap(); e.commit_line("...", NOON, "", &mut d); assert!(e.state.offer.is_none(), "a wordless line counts as no"); assert_eq!(e.state.today.log.log.last().unwrap(), "said no to medicine"); assert_eq!( e.command(care("clean", None), NOON, "", &mut d) .unwrap_err() .0, "All clean already." );}
#[test]fn turns_follow_reactions_then_bedtime_then_idle_then_missed_pages() { let mut e = engine(); let mut d = dice(); e.command( Command::Message { text: "hi [x]\n*there*".into(), }, NOON, "", &mut d, ) .unwrap(); assert_eq!(e.state.chat.last().unwrap(), "Sam: hi x there"); assert_eq!( e.choose_task(NOON, false), Some(Task::Chat { kind: "react".into() }) ); e.begin_turn(NOON, &mut d); assert_eq!(e.choose_task(NOON, false), None); assert_eq!( e.choose_task(e.next_idle, true), Some(Task::Chat { kind: "idle".into() }) ); // Night, asleep, with a day worth writing about: today's page first. let night = NOON + 10.0 * HOUR; e.state.asleep = true; e.state.today.log.log = vec!["ate a cookie".into(), "got patted".into()]; let today = e.state.today.date.clone(); assert_eq!( e.choose_task(night, true), Some(Task::Diary { date: today.clone() }) ); let turn = e.diary_turn(&today, night, "", false).unwrap(); assert_eq!(turn.request().prefix, "dear diary,"); assert!(turn.finish(" we had sex").is_none()); let page = turn .finish(" fun [doodle:heart] [doodle:robot]\n\nthe end [emote:happy]") .unwrap(); assert_eq!(page.text, "dear diary, fun\nthe end"); assert_eq!(page.doodles, vec!["heart"]); e.commit_page(page, false); assert!(e.diary_turn(&today, night, "", false).is_none()); // A day without a page waits in history and is written after 30 quiet seconds. e.state.asleep = false; let tomorrow = NOON + DAY; e.state.last_active = tomorrow; e.log("ate an apple".into(), tomorrow, ""); e.log("napped".into(), tomorrow, ""); let next = NOON + 2.0 * DAY; e.roll_day(next, ""); let missed = e.state.history.keys().next().unwrap().clone(); e.next_idle = next + HOUR; assert_eq!( e.choose_task(next + 31_000.0, true), Some(Task::Diary { date: missed }) );}
#[test]fn absences_catch_up_quietly_and_are_noticed() { let mut e = engine(); let mut d = dice(); e.advance(NOON + 3.0 * HOUR, "", true, &mut d); assert_eq!( e.state.chat.last().unwrap(), "* Sam came back after 3 hours *" ); assert_eq!(e.state.pet.as_ref().unwrap().stage, "baby"); e.advance(NOON + 13.0 * HOUR, "", false, &mut d); assert_eq!(e.state.pet.as_ref().unwrap().stage, "kid"); assert!(e.effects.iter().any(|x| x.kind == "grew")); // A week caps the simulation; ten days away do not overflow anything. e.advance(NOON + 10.0 * DAY + MIN, "", true, &mut d); assert_eq!(e.state.last_tick, NOON + 10.0 * DAY + MIN); assert!( e.state .chat .last() .unwrap() .starts_with("* Sam came back after") || e.state.chat.last().unwrap().ends_with("woke up *") );}