Files
figcraft/crates/engine/src/invite_links_tests.rs
T
BFlatandClaude Opus 5.5 435415770f M9: live sessions in the browser; join links and dialog polish (#106)
- Browser client (step 4): the web build joins sessions a desktop app hosts
  through the browser's WebSocket API (web-sys, binary frames, the same
  message encoding). Its connection logic is a target-independent state
  machine (collab-net driver.rs): hello on open, changes and presence held
  until the welcome, throttled presence, keep-alive pings, a silent host
  counted as dropped, reconnect with backoff, final refusals. Tested natively
  against a scripted socket and a real host over loopback. Hosting stays
  desktop-only (LiveLink::can_host; collab.peers canHost). https pages are
  told up front that they can only reach ws:// sessions on this computer.
- Shared join logic (join.rs: GuestConfig, outcomes, taking in host messages)
  and transport limits (limits.rs) used by the native guest and the browser.
- Invite links: `figcraft figcraft://join?...` joins on start (a bad link
  opens the Join dialog with it); file:// arguments are paths. Windows
  registers a per-user figcraft:// opener (HKCU\Software\Classes\figcraft)
  behind a Registry trait (tests use a fake): collab.registerLinks {on?} and a
  Preferences checkbox, default on for installed builds, never for builds
  from source. macOS Info.plist declares CFBundleURLTypes; the Linux desktop
  entry declares x-scheme-handler/figcraft and passes %U.
- Join dialog: Escape closes it and no longer reaches the canvas (selection
  kept); its field has the keyboard when it opens.
- Docs: collab-protocol (browser client, links, mixed content), mcp,
  control-protocol, README, web hosting notes, collaboration wording in the
  progress docs (numbers unchanged).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 09:10:59 +07:00

124 lines
4.0 KiB
Rust

//! `collab.registerLinks` and the startup registration, against a fake registrar (tests never
//! touch the real system).
use std::sync::{Arc, Mutex};
use serde_json::json;
use crate::invite_links::LinkRegistrar;
use crate::{EngineError, Session};
#[derive(Default)]
struct State {
registered: bool,
calls: Vec<bool>,
fail: bool,
}
struct Fake {
installed: bool,
state: Arc<Mutex<State>>,
}
impl LinkRegistrar for Fake {
fn registered(&self) -> bool {
self.state.lock().unwrap().registered
}
fn installed(&self) -> bool {
self.installed
}
fn set(&mut self, on: bool) -> Result<(), String> {
let mut s = self.state.lock().unwrap();
if s.fail {
return Err("access denied".into());
}
s.calls.push(on);
s.registered = on;
Ok(())
}
}
fn session(installed: bool, registered: bool) -> (Session, Arc<Mutex<State>>) {
let state = Arc::new(Mutex::new(State { registered, ..State::default() }));
let mut s = Session::new();
s.live.links = Some(Box::new(Fake { installed, state: state.clone() }));
(s, state)
}
#[test]
fn without_a_registrar_the_command_is_disabled() {
let mut s = Session::new();
assert_eq!(s.invite_links_on(), None);
s.invite_links_startup();
let e = s.execute("collab.registerLinks", &json!({"on": true})).unwrap_err();
assert!(matches!(e, EngineError::Disabled(..)), "{e}");
assert_eq!(s.prefs.invite_links, None, "the preference is untouched");
}
#[test]
fn installed_builds_register_on_first_run() {
let (mut s, state) = session(true, false);
assert_eq!(s.invite_links_on(), Some(true));
s.invite_links_startup();
assert_eq!(state.lock().unwrap().calls, vec![true]);
// Already this copy: nothing written again.
s.invite_links_startup();
assert_eq!(state.lock().unwrap().calls, vec![true]);
}
#[test]
fn development_builds_leave_the_registration_alone() {
// Not registered, or another copy's registration: a build from source changes neither.
for registered in [false, true] {
let (mut s, state) = session(false, registered);
assert_eq!(s.invite_links_on(), Some(false));
s.invite_links_startup();
assert!(state.lock().unwrap().calls.is_empty());
}
}
#[test]
fn the_preference_wins_at_startup() {
let (mut s, state) = session(false, false);
s.prefs.invite_links = Some(true);
s.invite_links_startup();
assert_eq!(state.lock().unwrap().calls, vec![true]);
let (mut s, state) = session(true, true);
s.prefs.invite_links = Some(false);
s.invite_links_startup();
assert_eq!(state.lock().unwrap().calls, vec![false], "turned off: this copy's registration is removed");
}
#[test]
fn the_command_switches_and_remembers() {
let (mut s, state) = session(true, false);
// Without `on` it switches the effective setting (on for an installed build) off.
let r = s.execute("collab.registerLinks", &json!({})).unwrap();
assert_eq!(r, json!({"on": false, "registered": false}));
assert_eq!(s.prefs.invite_links, Some(false));
let r = s.execute("collab.registerLinks", &json!({"on": true})).unwrap();
assert_eq!(r, json!({"on": true, "registered": true}));
assert_eq!(s.prefs.invite_links, Some(true));
assert_eq!(s.invite_links_on(), Some(true));
assert_eq!(state.lock().unwrap().calls, vec![false, true]);
}
#[test]
fn a_failed_registration_keeps_the_preference() {
let (mut s, state) = session(true, false);
state.lock().unwrap().fail = true;
let e = s.execute("collab.registerLinks", &json!({"on": true})).unwrap_err();
assert!(e.to_string().contains("access denied"), "{e}");
assert_eq!(s.prefs.invite_links, None);
// Startup logs the failure and carries on.
s.invite_links_startup();
}
#[test]
fn junk_parameters_never_panic() {
let (mut s, _) = session(true, false);
for p in [json!(null), json!([]), json!({"on": "yes"}), json!({"on": 3}), json!({"on": null})] {
let _ = s.execute("collab.registerLinks", &p);
}
}