Something went wrong. Try again.
a tiny pet with a GPT-2 brain, living in your browser
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849use axum::{ body::{Body, to_bytes}, http::{Request, StatusCode},};use serde_json::{Value, json};use std::sync::{ Arc, Mutex, atomic::{AtomicBool, AtomicUsize, Ordering},};use tokenling_server::{ brain::{Generated, Generation}, host::{Generator, Host, Worker}, simulation::{self, Command, HOUR, HostedPet, MIN}, store::Store, types::PetState,};use tower::ServiceExt;
fn pet() -> PetState { let mut value: Value = serde_json::from_str(include_str!("../../test/fixtures/save-v1.json")).unwrap(); value["v"] = json!(2); value["hosting"] = json!({"mode":"local","brainId":"test-brain","timeZone":"UTC"}); value["lastTick"] = json!(simulation::now()); value["nextPoop"] = json!(simulation::now() + 10.0 * HOUR); serde_json::from_value(value).unwrap()}fn activate(row: &mut HostedPet) { row.authority = "remote".into(); row.epoch += 1; row.state.hosting.mode = "remote".into(); row.pending = Some("react".into());}
#[test]fn handoffs_fence_old_replies_and_keep_the_complete_save() { let directory = tempfile::tempdir().unwrap(); let mut db = Store::open(&directory.path().join("state.sqlite")).unwrap(); let id = uuid::Uuid::new_v4().to_string(); let key = "a".repeat(64); let original = pet(); db.create( &id, &key, HostedPet::new(original.clone(), simulation::now()), 64, ) .unwrap(); let operation = uuid::Uuid::new_v4().to_string(); let remote = db .operation(&id, &key, 0, &operation, "activate", |r| { activate(r); Ok(()) }) .unwrap(); let captured = (remote.epoch, remote.context_revision); let local = db .operation( &id, &key, 1, &uuid::Uuid::new_v4().to_string(), "local", |r| { r.authority = "local".into(); r.epoch += 1; r.state.hosting.mode = "local".into(); Ok(()) }, ) .unwrap(); assert!( !db.commit(&id, captured.0, captured.1, |r| r .state .shelf .push("flower".into())) .unwrap() ); assert_eq!(local.state.chat, original.chat); assert_eq!(local.state.memories, original.memories); assert_eq!( serde_json::to_value(&local.state.diary).unwrap(), serde_json::to_value(original.diary).unwrap() ); assert_eq!(db.get(&id, &key).unwrap().state.shelf, original.shelf); assert!( db.operation( &id, &key, 1, &uuid::Uuid::new_v4().to_string(), "stale", |_| Ok(()) ) .is_err() );}
#[test]fn a_host_never_runs_hosted_pets_on_a_different_brain() { let directory = tempfile::tempdir().unwrap(); let mut db = Store::open(&directory.path().join("state.sqlite")).unwrap(); let (id, key) = (uuid::Uuid::new_v4().to_string(), "c".repeat(64)); let mut state = pet(); state.hosting.brain_id = Some("test-brain".into()); db.create(&id, &key, HostedPet::new(state, simulation::now()), 64) .unwrap(); // A prepared or parked save is checked again when it is activated. assert_eq!(db.foreign_brains("newer-brain").unwrap(), 0); db.operation( &id, &key, 0, &uuid::Uuid::new_v4().to_string(), "activate", |r| { activate(r); Ok(()) }, ) .unwrap(); assert_eq!(db.foreign_brains("test-brain").unwrap(), 0); assert_eq!(db.foreign_brains("newer-brain").unwrap(), 1);}
#[test]fn retries_do_not_repeat_care_or_overwrite_a_running_pet() { let directory = tempfile::tempdir().unwrap(); let path = directory.path().join("state.sqlite"); let mut db = Store::open(&path).unwrap(); let id = uuid::Uuid::new_v4().to_string(); let key = "b".repeat(64); let state = pet(); db.create( &id, &key, HostedPet::new(state.clone(), simulation::now()), 64, ) .unwrap(); db.operation( &id, &key, 0, &uuid::Uuid::new_v4().to_string(), "activate", |r| { activate(r); Ok(()) }, ) .unwrap(); let op = uuid::Uuid::new_v4().to_string(); let t = simulation::now(); let count = db.get(&id, &key).unwrap().state.counts.pats; for _ in 0..2 { db.operation(&id, &key, 1, &op, "pat", |r| { r.command( Command::Care { kind: "pat".into(), item: None, }, t, "", ) }) .unwrap(); } assert_eq!(db.get(&id, &key).unwrap().state.counts.pats, count + 1); assert!( db.operation(&id, &key, 1, &op, "different payload", |_| Ok(())) .is_err() ); let retried = db .create(&id, &key, HostedPet::new(state, simulation::now()), 64) .unwrap(); assert_eq!(retried.state.counts.pats, count + 1); assert_eq!(retried.authority, "remote"); drop(db); let reopened = Store::open(&path).unwrap(); assert_eq!( reopened.get(&id, &key).unwrap().state.counts.pats, count + 1 ); assert!( !std::fs::read(&path) .unwrap() .windows(64) .any(|w| w == key.as_bytes()) );}
#[test]fn sleeping_offers_are_brain_decisions_and_unknown_tags_have_no_effect() { let t = simulation::now(); let mut row = HostedPet::new(pet(), t); activate(&mut row); row.state.asleep = true; row.state.stats.energy = 50.0; row.command( Command::Care { kind: "feed".into(), item: Some("cookie".into()), }, t, "", ) .unwrap(); assert!(row.state.asleep); let tummy = row.state.stats.tummy; row.apply_line("[dream:cookie] [no] [gift:flower]", t, ""); assert!(row.state.asleep); assert_eq!(row.state.stats.tummy, tummy); assert!(row.state.offer.is_none()); row.command( Command::Care { kind: "feed".into(), item: Some("cookie".into()), }, t, "", ) .unwrap(); row.apply_line("yes [yes] [teleport:kitchen]", t, ""); assert!(!row.state.asleep); assert_eq!(row.state.stats.tummy, (tummy + 22.0).min(100.0)); let shelf = row.state.shelf.clone(); row.apply_line("[gift:spaceship] [walk:up] [dream:spaceship]", t, ""); assert_eq!(row.state.shelf, shelf);}
#[test]fn body_rates_and_wake_protection_match_the_browser() { use chrono::TimeZone; let t = chrono::Utc .with_ymd_and_hms(2026, 10, 1, 12, 0, 0) .unwrap() .timestamp_millis() as f64; let mut awake = pet(); awake.stats = tokenling_server::types::Stats { tummy: 70.0, fun: 70.0, energy: 80.0, clean: 90.0, }; awake.next_poop = t + 10.0 * HOUR; let mut sleeping = awake.clone(); sleeping.asleep = true; sleeping.stats.energy = 40.0; simulation::simulate_body(&mut awake, t, t + HOUR, false, &mut || 0.5); simulation::simulate_body(&mut sleeping, t, t + HOUR, false, &mut || 0.5); assert_eq!( serde_json::to_value(&awake.stats).unwrap(), json!({"tummy":62.0,"fun":64.0,"energy":75.0,"clean":87.0}) ); assert_eq!( serde_json::to_value(&sleeping.stats).unwrap(), json!({"tummy":68.0,"fun":70.0,"energy":62.0,"clean":89.5}) ); awake.stats.energy = 5.0; awake.woke_at = t; simulation::simulate_body(&mut awake, t, t + MIN, false, &mut || 0.5); assert!(!awake.asleep); let events = simulation::simulate_body(&mut awake, t + MIN, t + 6.0 * MIN, false, &mut || 0.5); assert!(awake.asleep); assert!(events.contains(&"fell asleep"));}
struct Scripted { calls: Arc<AtomicUsize>,}struct BrokenGpu;struct FilteredDiaryBrain;impl Generator for FilteredDiaryBrain { fn generate(&mut self, g: &Generation, _: &AtomicBool) -> anyhow::Result<Generated> { Ok(Generated { text: tokenling_server::world::NOT_CUTE.as_str().into(), prompt: g.header.clone() + &g.prefix, reason: "end".into(), prompt_tokens: 100, prefill_ms: 1.0, tps: 100.0, generated_tokens: 5, total_ms: 1.0, }) }}impl Generator for BrokenGpu { fn generate(&mut self, _: &Generation, _: &AtomicBool) -> anyhow::Result<Generated> { anyhow::bail!("GPU unavailable") }}#[tokio::test]async fn failed_gpu_requeues_the_turn_on_cpu_without_applying_it_twice() { let directory = tempfile::tempdir().unwrap(); let calls = Arc::new(AtomicUsize::new(0)); let mut db = Store::open(&directory.path().join("state.sqlite")).unwrap(); let id = uuid::Uuid::new_v4().to_string(); let key = "e".repeat(64); let mut row = HostedPet::new(pet(), simulation::now()); activate(&mut row); db.create(&id, &key, row, 64).unwrap(); let host = Arc::new(Host { db: Mutex::new(db), model_id: "test-brain".into(), features: vec![], max_pets: 64, weather_seed: String::new(), cpu: Worker::new( "cpu", Scripted { calls: calls.clone(), }, ), gpu: Some(Worker::new("gpu", BrokenGpu)), jobs: Mutex::new(Default::default()), notify: tokio::sync::Notify::new(), queue: Mutex::new(Default::default()), }); let scheduler = tokio::spawn(host.clone().run()); for _ in 0..100 { if host .db .lock() .unwrap() .get(&id, &key) .unwrap() .state .chat .iter() .any(|s| s.contains("[clock]{")) { break; } tokio::time::sleep(std::time::Duration::from_millis(10)).await; } assert!(!host.gpu.as_ref().unwrap().available.load(Ordering::Relaxed)); assert!(calls.load(Ordering::Relaxed) >= 2); let row = host.db.lock().unwrap().get(&id, &key).unwrap(); assert_eq!( row.state .chat .iter() .filter(|s| s.contains("[clock]{")) .count(), 1 ); scheduler.abort();}
#[tokio::test]async fn a_filtered_diary_retries_its_pet_without_disabling_a_healthy_gpu() { let temporary = tempfile::tempdir().unwrap(); let mut db = Store::open(&temporary.path().join("pets.sqlite")).unwrap(); let id = uuid::Uuid::new_v4().to_string(); let key = "a".repeat(64); let t = simulation::now(); let mut state = pet(); state.last_active = t - 60000.0; state.history.insert( (chrono::Utc::now() - chrono::Duration::days(1)) .format("%Y-%m-%d") .to_string(), tokenling_server::types::DayLog { log: vec!["had a cozy day".into(), "watched flowers".into()], weather: None, day: Some(1), }, ); let mut row = HostedPet::new(state, t); activate(&mut row); row.pending = None; row.next_idle = t + HOUR; db.create(&id, &key, row, 64).unwrap(); let host = Arc::new(Host { db: Mutex::new(db), model_id: "test-brain".into(), features: vec![], max_pets: 64, weather_seed: String::new(), cpu: Worker::new("cpu", FilteredDiaryBrain), gpu: Some(Worker::new("gpu", FilteredDiaryBrain)), jobs: Mutex::new(Default::default()), notify: tokio::sync::Notify::new(), queue: Mutex::new(Default::default()), }); host.cpu.available.store(false, Ordering::Relaxed); let scheduler = tokio::spawn(host.clone().run()); for _ in 0..100 { if host .db .lock() .unwrap() .get(&id, &key) .unwrap() .last_error .is_some() { break; } tokio::time::sleep(std::time::Duration::from_millis(5)).await; } assert!( host.db .lock() .unwrap() .get(&id, &key) .unwrap() .last_error .is_some() ); assert!(host.gpu.as_ref().unwrap().available.load(Ordering::Relaxed)); scheduler.abort();}
#[test]fn forgetting_a_memory_cannot_delete_a_different_fact_after_eviction() { let t = simulation::now(); let mut row = HostedPet::new(pet(), t); row.state.memories = vec!["Sam likes frogs".into(), "Sam likes flowers".into()]; activate(&mut row); row.state.memories.remove(0); assert!( row.command( Command::ForgetMemory { index: 0, fact: "Sam likes frogs".into() }, t, "" ) .is_err() ); assert_eq!(row.state.memories, vec!["Sam likes flowers"]); row.command( Command::ForgetMemory { index: 0, fact: "Sam likes flowers".into(), }, t, "", ) .unwrap(); assert!(row.state.memories.is_empty());}impl Generator for Scripted { fn generate(&mut self, g: &Generation, _: &AtomicBool) -> anyhow::Result<Generated> { self.calls.fetch_add(1, Ordering::Relaxed); let (text, reason) = if g.prefix.contains("[clock]{") { (" hello! [emote:happy]", "line") } else { (" [clock]", "lookup") }; Ok(Generated { text: text.into(), prompt: g.header.clone() + &g.prefix, reason: reason.into(), generated_tokens: 0, total_ms: 0.0, prompt_tokens: 100, prefill_ms: 1.0, tps: 100.0, }) }}
struct Gated { backend: &'static str, started: Arc<Mutex<Vec<(&'static str, String)>>>, released: Arc<AtomicBool>,}impl Generator for Gated { fn generate(&mut self, g: &Generation, cancel: &AtomicBool) -> anyhow::Result<Generated> { self.started .lock() .unwrap() .push((self.backend, g.prefix.clone())); let deadline = std::time::Instant::now() + std::time::Duration::from_secs(5); while !self.released.load(Ordering::Relaxed) && !cancel.load(Ordering::Relaxed) { anyhow::ensure!(std::time::Instant::now() < deadline, "test gate timed out"); std::thread::sleep(std::time::Duration::from_millis(1)); } anyhow::ensure!(!cancel.load(Ordering::Relaxed), "generation cancelled"); Ok(Generated { text: " [emote:happy]".into(), prompt: g.header.clone() + &g.prefix, reason: "line".into(), prompt_tokens: 100, prefill_ms: 1.0, tps: 100.0, generated_tokens: 5, total_ms: 1.0, }) }}
#[tokio::test]async fn mixed_workers_prefer_reactive_gpu_and_background_cpu_and_serialize_each_lane() { let temporary = tempfile::tempdir().unwrap(); let mut db = Store::open(&temporary.path().join("pets.sqlite")).unwrap(); let t = simulation::now(); for (name, reactive) in [("Mochi", true), ("Bean", false), ("Pip", true)] { let mut state = pet(); state.pet.as_mut().unwrap().name = name.into(); state.today.date = chrono::Utc::now().format("%Y-%m-%d").to_string(); state.today.log.log.clear(); state.history.clear(); state.hosting.time_zone = "UTC".into(); let mut row = HostedPet::new(state, t); activate(&mut row); row.pending = reactive.then(|| "react".into()); row.next_idle = t - 1.0; // Put the second reactive request in a later burst so the first free // CPU lane can choose existing background work. if name == "Pip" { row.pending = None; row.next_idle = t + HOUR; } db.create(&uuid::Uuid::new_v4().to_string(), &"f".repeat(64), row, 64) .unwrap(); } let started = Arc::new(Mutex::new(Vec::new())); let released = Arc::new(AtomicBool::new(false)); let host = Arc::new(Host { db: Mutex::new(db), model_id: "test-brain".into(), features: vec![], max_pets: 64, weather_seed: String::new(), cpu: Worker::new( "cpu", Gated { backend: "cpu", started: started.clone(), released: released.clone(), }, ), gpu: Some(Worker::new( "gpu", Gated { backend: "gpu", started: started.clone(), released: released.clone(), }, )), jobs: Mutex::new(Default::default()), notify: tokio::sync::Notify::new(), queue: Mutex::new(Default::default()), }); let scheduler = tokio::spawn(host.clone().run()); for _ in 0..100 { if started.lock().unwrap().len() == 2 { break; } tokio::time::sleep(std::time::Duration::from_millis(5)).await; } let first = started.lock().unwrap().clone(); assert_eq!(first.len(), 2); assert!(first.contains(&("gpu", "Mochi:".into()))); assert!(first.contains(&("cpu", "Bean:".into()))); assert_eq!(host.cpu.slot.available_permits(), 0); assert_eq!(host.gpu.as_ref().unwrap().slot.available_permits(), 0); let rows = host.db.lock().unwrap().advance(t, "").unwrap(); let (id, row) = rows .iter() .find(|(_, r)| r.state.pet.as_ref().unwrap().name == "Pip") .unwrap(); host.db .lock() .unwrap() .commit(id, row.epoch, row.context_revision, |r| { r.pending = Some("react".into()) }) .unwrap(); host.notify.notify_one(); tokio::time::sleep(std::time::Duration::from_millis(25)).await; assert_eq!( started.lock().unwrap().len(), 2, "a busy session must not accept another turn" ); released.store(true, Ordering::Relaxed); for _ in 0..100 { if started .lock() .unwrap() .iter() .any(|(_, prefix)| prefix == "Pip:") { break; } tokio::time::sleep(std::time::Duration::from_millis(5)).await; } assert!( started .lock() .unwrap() .iter() .any(|(_, prefix)| prefix == "Pip:") ); assert!(host.gpu.as_ref().unwrap().available.load(Ordering::Relaxed)); scheduler.abort();}
#[tokio::test]async fn measured_small_queue_waits_for_busy_gpu_instead_of_creating_a_cpu_tail() { use tokenling_server::routing::{Kind, Work}; let temporary = tempfile::tempdir().unwrap(); let mut db = Store::open(&temporary.path().join("pets.sqlite")).unwrap(); for name in ["Mochi", "Bean", "Pip"] { let mut state = pet(); state.pet.as_mut().unwrap().name = name.into(); let mut row = HostedPet::new(state, simulation::now()); activate(&mut row); db.create(&uuid::Uuid::new_v4().to_string(), &"a".repeat(64), row, 64) .unwrap(); } let started = Arc::new(Mutex::new(Vec::new())); let released = Arc::new(AtomicBool::new(false)); let host = Arc::new(Host { db: Mutex::new(db), model_id: "test-brain".into(), features: vec![], max_pets: 64, weather_seed: String::new(), cpu: Worker::new( "cpu", Gated { backend: "cpu", started: started.clone(), released: released.clone(), }, ), gpu: Some(Worker::new( "gpu", Gated { backend: "gpu", started: started.clone(), released: released.clone(), }, )), jobs: Mutex::new(Default::default()), notify: tokio::sync::Notify::new(), queue: Mutex::new(Default::default()), }); let work = Work::new(Kind::Reactive, 100); host.cpu.costs.lock().unwrap().observe(work, 140.0); host.gpu .as_ref() .unwrap() .costs .lock() .unwrap() .observe(work, 30.0); let scheduler = tokio::spawn(host.clone().run()); for _ in 0..100 { if started.lock().unwrap().len() == 1 { break; } tokio::time::sleep(std::time::Duration::from_millis(5)).await; } tokio::time::sleep(std::time::Duration::from_millis(25)).await; assert_eq!(started.lock().unwrap().len(), 1); assert_eq!(started.lock().unwrap()[0].0, "gpu"); assert_eq!( host.cpu.slot.available_permits(), 1, "CPU stays free for this measured small queue" ); released.store(true, Ordering::Relaxed); for _ in 0..100 { if started.lock().unwrap().len() == 3 { break; } tokio::time::sleep(std::time::Duration::from_millis(5)).await; } assert_eq!(started.lock().unwrap().len(), 3); assert!(started.lock().unwrap().iter().all(|r| r.0 == "gpu")); assert_eq!( host.cpu.costs.lock().unwrap().sample_count(work), 1, "seeded timing must not claim an unrun CPU turn" ); scheduler.abort();}async fn request( app: axum::Router, method: &str, path: &str, key: Option<&str>, body: Value,) -> (StatusCode, Value) { let mut req = Request::builder() .method(method) .uri(path) .header("content-type", "application/json"); if let Some(key) = key { req = req.header("authorization", format!("Bearer {key}")); } let res = app .oneshot( req.body(Body::from(serde_json::to_vec(&body).unwrap())) .unwrap(), ) .await .unwrap(); let status = res.status(); let body = to_bytes(res.into_body(), 8 * 1024 * 1024).await.unwrap(); (status, serde_json::from_slice(&body).unwrap())}
#[tokio::test]async fn private_api_runs_background_turns_and_supports_both_transfer_directions() { let directory = tempfile::tempdir().unwrap(); let calls = Arc::new(AtomicUsize::new(0)); let host = Arc::new(Host { db: Mutex::new(Store::open(&directory.path().join("state.sqlite")).unwrap()), model_id: "test-brain".into(), features: vec![], max_pets: 64, weather_seed: String::new(), cpu: Worker::new( "cpu", Scripted { calls: calls.clone(), }, ), gpu: None, jobs: Mutex::new(Default::default()), notify: tokio::sync::Notify::new(), queue: Mutex::new(Default::default()), }); let app = host.clone().router(); let id = uuid::Uuid::new_v4().to_string(); let key = "c".repeat(64); let device = uuid::Uuid::new_v4().to_string(); let state = pet(); let prepared_id = uuid::Uuid::new_v4().to_string(); let prepared_key = "e".repeat(64); assert_eq!( request( app.clone(), "POST", "/api/pets", Some(&prepared_key), json!({"id":prepared_id,"state":state,"modelId":"test-brain"}) ) .await .0, StatusCode::OK ); let (status, fenced) = request(app.clone(), "POST", &format!("/api/pets/{prepared_id}/transfer"), Some(&prepared_key), json!({"epoch":0,"operationId":uuid::Uuid::new_v4().to_string(),"mode":"local","deviceId":device,"state":null})).await; assert_eq!(status, StatusCode::OK); assert_eq!(fenced["epoch"], 1); assert_eq!(fenced["authority"], "local"); assert_eq!(request(app.clone(), "POST", &format!("/api/pets/{prepared_id}/transfer"), Some(&prepared_key), json!({"epoch":0,"operationId":uuid::Uuid::new_v4().to_string(),"mode":"remote","deviceId":device,"state":state})).await.0, StatusCode::CONFLICT, "a late activation cannot take ownership back after a prepared record is parked"); assert_eq!(request(app.clone(), "POST", &format!("/api/pets/{prepared_id}/transfer"), Some(&prepared_key), json!({"epoch":1,"operationId":uuid::Uuid::new_v4().to_string(),"mode":"remote","deviceId":uuid::Uuid::new_v4().to_string(),"state":state})).await.0, StatusCode::CONFLICT, "only the handoff destination can reactivate a parked local copy"); assert_eq!( request( app.clone(), "POST", "/api/pets", None, json!({"id":id,"state":state,"modelId":"test-brain"}) ) .await .0, StatusCode::UNAUTHORIZED ); assert_eq!( request( app.clone(), "POST", "/api/pets", Some(&key), json!({"id":id,"state":state,"modelId":"test-brain"}) ) .await .0, StatusCode::OK ); assert_eq!( request( app.clone(), "GET", &format!("/api/pets/{id}"), Some(&"d".repeat(64)), Value::Null ) .await .0, StatusCode::UNAUTHORIZED ); let (status,remote)=request(app.clone(),"POST",&format!("/api/pets/{id}/transfer"),Some(&key),json!({"epoch":0,"operationId":uuid::Uuid::new_v4().to_string(),"mode":"remote","deviceId":device,"state":state})).await; assert_eq!(status, StatusCode::OK); assert_eq!(remote["epoch"], 1); let scheduler = tokio::spawn(host.clone().run()); for _ in 0..50 { if host .db .lock() .unwrap() .get(&id, &key) .unwrap() .state .chat .iter() .any(|l| l.contains("[clock]{")) { break; } tokio::time::sleep(std::time::Duration::from_millis(10)).await; } assert!( calls.load(Ordering::Relaxed) >= 2, "the brain must run without a browser polling" ); let (_, snapshot) = request( app.clone(), "GET", &format!("/api/pets/{id}"), Some(&key), Value::Null, ) .await; assert!( snapshot["state"]["chat"] .as_array() .unwrap() .iter() .any(|l| l.as_str().unwrap().contains("[clock]{")), "lookup result is supplied by the host" ); let (status,local)=request(app.clone(),"POST",&format!("/api/pets/{id}/transfer"),Some(&key),json!({"epoch":1,"operationId":uuid::Uuid::new_v4().to_string(),"mode":"local","deviceId":device,"state":null})).await; assert_eq!(status, StatusCode::OK); assert_eq!(local["authority"], "local"); assert_eq!(local["localDevice"], device); let (status,resumed)=request(app.clone(),"POST",&format!("/api/pets/{id}/transfer"),Some(&key),json!({"epoch":2,"operationId":uuid::Uuid::new_v4().to_string(),"mode":"remote","deviceId":device,"state":local["state"]})).await; assert_eq!(status, StatusCode::OK); assert_eq!(resumed["epoch"], 3); assert_eq!(resumed["authority"], "remote"); scheduler.abort();}