mirror of
https://github.com/storytold/figcraft.git
synced 2026-10-28 12:26:52 -06:00
- 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>
124 lines
4.0 KiB
Rust
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);
|
|
}
|
|
}
|