Files
lightcraft/crates/engine/src/tests_segment.rs
T
francoisgnandClaude Opus 5.5 85947ca069 Round 3a: the failing-download test no longer races the mirror's 404
tests_segment::a_failing_download_ends_with_an_error_and_never_blocks
checks that a second download request while one runs starts nothing. Its
local mirror answered 404 at once, so on a loaded runner the first
download could end before the second request, which then rightly started
a new one: the assertion failed (upstream's gate on #2689, 5223c8095). The
mirror's first answer now waits until the second request is made. A
300 ms pause between the two requests failed the test before and passes
now. Test code only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 00:10:12 +03:00

316 lines
15 KiB
Rust

//! AI masks without the SAM 3 model (it is never required): every request is a clear error that
//! comes back at once, nothing waits on the network or the model, a damaged model is an error
//! (not a crash or a stuck "busy"), and the download command needs consent and fails cleanly.
//! No test touches the internet: downloads go to a local server.
use std::time::{Duration, Instant};
use serde_json::json;
use crate::Session;
use crate::segment::{NOT_INSTALLED, Segmenter};
fn tmp(name: &str) -> std::path::PathBuf {
let d = std::env::temp_dir().join(format!("lc-sam3-engine-{name}-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&d);
d
}
/// A folder that looks installed but holds no usable model.
fn damaged_model(name: &str) -> std::path::PathBuf {
let d = tmp(name);
std::fs::create_dir_all(&d).unwrap();
std::fs::write(d.join("model.safetensors"), b"\x10\x00\x00\x00\x00\x00\x00\x00{not json at all}").unwrap();
std::fs::write(d.join("vocab.json"), b"{}").unwrap();
std::fs::write(d.join("merges.txt"), b"#version: 0.2\n").unwrap();
d
}
/// Poll until the worker is idle (or `limit` passes), collecting messages.
fn settle(s: &mut Session, limit: Duration) -> Vec<String> {
let t = Instant::now();
let mut messages = Vec::new();
loop {
let p = s.segment_poll();
messages.extend(p.messages);
if !s.segmenter.busy() && !s.segmenter.detail_busy() {
messages.extend(s.segment_poll().messages);
return messages;
}
assert!(t.elapsed() < limit, "the worker never finished");
std::thread::sleep(Duration::from_millis(10));
}
}
#[test]
fn status_and_model_commands_without_the_model() {
let mut s = Session::with_demo();
let st = s.execute("segment.model.status", &json!({})).unwrap();
assert_eq!(st["available"], Segmenter::AVAILABLE);
assert_eq!(st["installed"], false);
assert_eq!(st["download"]["running"], false);
assert!(st["license"].as_str().unwrap().contains("SAM License"));
// downloading needs the user's consent, and names the size and the licence
let e = s.execute("segment.model.download", &json!({})).unwrap_err().to_string();
assert!(e.contains("acknowledged") && e.contains("SAM License") && e.contains("GB"), "{e}");
let e = s.execute("segment.model.download", &json!({"acknowledged": "yes"})).unwrap_err().to_string();
assert!(e.contains("acknowledged"), "{e}");
assert_eq!(s.execute("segment.model.cancel", &json!({})).unwrap()["cancelled"], false);
if !Segmenter::AVAILABLE {
assert!(s.execute("segment.model.download", &json!({"acknowledged": true})).is_err());
return;
}
// no folder, then no download location: clear errors, nothing started
let e = s.execute("segment.model.download", &json!({"acknowledged": true})).unwrap_err().to_string();
assert!(e.contains("no folder"), "{e}");
let dir = tmp("nomirrors");
s.segmenter.dir = Some(dir.clone());
s.segmenter.mirrors_file = Some(dir.join("none.txt"));
if s.segmenter.mirrors().is_empty() {
let e = s.execute("segment.model.download", &json!({"acknowledged": true})).unwrap_err().to_string();
assert!(e.contains("LIGHTCRAFT_SAM3_MIRRORS"), "{e}");
}
assert!(!s.segmenter.download_status().running);
}
#[cfg(feature = "sam")]
#[test]
fn a_failing_download_ends_with_an_error_and_never_blocks() {
use std::io::{BufRead, BufReader, Write};
// a local mirror that has nothing; its first answer waits until the test has made its second
// request, so the download is still running then however the threads are scheduled (a 404 at
// once could end it before, and the second request would rightly start a new one)
let l = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
let base = format!("http://{}/sam3", l.local_addr().unwrap());
let (release, released) = std::sync::mpsc::channel::<()>();
std::thread::spawn(move || {
let mut first = Some(released);
for mut c in l.incoming().flatten() {
let mut r = BufReader::new(c.try_clone().unwrap());
let mut line = String::new();
while r.read_line(&mut line).unwrap_or(0) > 2 {
line.clear();
}
if let Some(released) = first.take() {
let _ = released.recv_timeout(Duration::from_secs(10));
}
let _ = c.write_all(b"HTTP/1.1 404 Not Found\r\nContent-Length: 13\r\nConnection: close\r\n\r\n<h1>404</h1>\n");
}
});
let dir = tmp("download404");
let mirrors = dir.with_extension("mirrors.txt");
std::fs::write(&mirrors, format!("# test mirror\n{base}\n")).unwrap();
let mut s = Session::with_demo();
s.segmenter.dir = Some(dir.clone());
s.segmenter.mirrors_file = Some(mirrors.clone());
let t = Instant::now();
let r = s.execute("segment.model.download", &json!({"acknowledged": true})).unwrap();
assert_eq!(r["started"], true);
assert!(t.elapsed() < Duration::from_secs(1), "starting a download returns at once");
// a second request while it runs starts nothing
let again = s.execute("segment.model.download", &json!({"acknowledged": true})).unwrap();
assert!(again["started"] == false || again["downloading"] == false);
let _ = release.send(());
let t = Instant::now();
while s.segmenter.download_status().running {
assert!(t.elapsed() < Duration::from_secs(30), "the download never ended");
// AI mask requests meanwhile: an error at once, never a wait
let q = Instant::now();
let e = s.execute("mask.add", &json!({"kind": "prompt", "text": "sky"})).unwrap_err().to_string();
assert!(e.contains(NOT_INSTALLED), "{e}");
assert!(q.elapsed() < Duration::from_millis(500));
std::thread::sleep(Duration::from_millis(20));
}
let st = s.segmenter.download_status();
assert!(!st.finished);
assert!(!st.cancelled, "a failure is not a cancel");
let e = st.error.unwrap();
assert!(e.contains("not found"), "{e}");
assert!(!s.segmenter.installed());
// nothing was left behind under a real name, and no error page was saved
for f in ["model.safetensors", "vocab.json", "merges.txt", "vocab.json.part"] {
assert!(!dir.join(f).exists(), "{f}");
}
let _ = std::fs::remove_dir_all(&dir);
let _ = std::fs::remove_file(&mirrors);
}
/// The app (background mode) never waits: with the model missing every request fails at once;
/// with a damaged model the requests are queued, fail on the worker, and come back as messages.
#[test]
fn background_requests_never_wait_and_failures_come_back_as_messages() {
let mut s = Session::with_demo();
s.segmenter.background = true;
let id = s.active().unwrap();
s.segmenter.dir = Some(tmp("missing"));
for (cmd, p) in [("mask.add", json!({"kind": "object"})), ("mask.add", json!({"kind": "prompt", "text": "sky"})), ("segment.prepare", json!({}))]
{
let t = Instant::now();
let e = s.execute(cmd, &p).unwrap_err().to_string();
assert!(t.elapsed() < Duration::from_millis(500), "{cmd}");
if Segmenter::AVAILABLE {
assert!(e.starts_with(NOT_INSTALLED), "{cmd}: {e}");
} else {
assert!(e.contains("not available"), "{cmd}: {e}");
}
}
assert!(s.develop_of(id).unwrap().masks.is_empty());
if !Segmenter::AVAILABLE {
return;
}
// a model folder whose weights are damaged
let dir = damaged_model("damaged");
s.segmenter.dir = Some(dir.clone());
assert!(s.segmenter.installed());
let t = Instant::now();
s.execute("mask.add", &json!({"kind": "object"})).unwrap();
let r = s.execute("mask.objectPoint", &json!({"x": 0.5, "y": 0.5})).unwrap();
assert_eq!(r["pending"], true);
assert!(s.segmenter.pending_clicks().is_some_and(|p| p.hint.len() == 1));
// a second click while the first is on its way builds on it
let r = s.execute("mask.objectPoint", &json!({"x": 0.6, "y": 0.5, "exclude": true})).unwrap();
assert_eq!((r["include"].as_u64(), r["exclude"].as_u64()), (Some(1), Some(1)));
let r = s.execute("mask.add", &json!({"kind": "prompt", "text": "the sky"})).unwrap();
assert_eq!(r["pending"], true);
assert!(t.elapsed() < Duration::from_secs(1), "nothing waited for the model: {:?}", t.elapsed());
let messages = settle(&mut s, Duration::from_secs(60));
assert!(!messages.is_empty() && messages.iter().all(|m| !m.is_empty()), "{messages:?}");
assert!(messages.iter().any(|m| m.contains("model.safetensors") || m.contains("SAM 3")), "{messages:?}");
assert!(s.segmenter.pending_clicks().is_none());
// the Object mask is there, without a selection; no Describe mask was made from nothing
let d = s.develop_of(id).unwrap();
assert_eq!(d.masks.len(), 1);
assert!(matches!(&d.masks[0].components[0].shape, lightcraft_develop::MaskShape::Object { seg: None, .. }));
assert!(!s.segmenter.busy() && !s.segmenter.loaded());
let _ = std::fs::remove_dir_all(&dir);
}
/// CLI / MCP (waiting mode): a damaged model is an error from the command, quickly.
#[test]
fn waiting_requests_with_a_damaged_model_are_errors() {
if !Segmenter::AVAILABLE {
return;
}
let mut s = Session::with_demo();
let id = s.active().unwrap();
let dir = damaged_model("damaged-wait");
s.segmenter.dir = Some(dir.clone());
let e = s.execute("mask.add", &json!({"kind": "prompt", "text": "sky"})).unwrap_err().to_string();
assert!(e.contains("model.safetensors") || e.contains("SAM 3"), "{e}");
let e = s.execute("mask.add", &json!({"kind": "object", "points": [[0.5, 0.5]]})).unwrap_err().to_string();
assert!(!e.is_empty());
assert!(s.develop_of(id).unwrap().masks.is_empty());
assert!(!s.segmenter.busy());
// a stored segmentation needs no model at all (replayed or pasted masks)
let seg = serde_json::to_value(lightcraft_develop::SegMask::from_logits(4, &[5.0; 16])).unwrap();
s.segmenter.dir = None;
s.execute("mask.add", &json!({"kind": "prompt", "text": "sky", "seg": seg})).unwrap();
assert_eq!(s.develop_of(id).unwrap().masks.len(), 1);
let _ = std::fs::remove_dir_all(&dir);
}
/// Too many clicks on one Object selection are refused, not sent to the model.
#[test]
fn object_clicks_are_capped() {
if !Segmenter::AVAILABLE {
return;
}
let mut s = Session::with_demo();
s.segmenter.background = true;
let dir = damaged_model("clicks");
s.segmenter.dir = Some(dir.clone());
let id = s.active().unwrap();
let mut d = (*s.develop_of(id).unwrap()).clone();
let many = vec![lightcraft_geom::Point::new(0.5, 0.5); crate::segment::MAX_CLICKS];
d.masks.push(lightcraft_develop::Mask {
id: 1,
components: vec![lightcraft_develop::MaskComponent {
name: None,
op: lightcraft_develop::MaskOp::Add,
invert: false,
shape: lightcraft_develop::MaskShape::Object { hint: many, exclude: vec![], seg: None, detail: vec![], edge: 0.0 },
}],
..Default::default()
});
s.set_develop(id, d, "test").unwrap();
s.active_mask = Some(1);
let e = s.execute("mask.objectPoint", &json!({"x": 0.5, "y": 0.5})).unwrap_err().to_string();
assert!(e.contains("at most"), "{e}");
assert!(!s.segmenter.busy());
let _ = std::fs::remove_dir_all(&dir);
}
#[cfg(feature = "sam")]
#[test]
fn a_running_model_download_shows_in_activity_and_cancels() {
use std::io::{BufRead, BufReader, Write};
// a local mirror that sends its files slowly, so the download is still running when it is cancelled
let l = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
let base = format!("http://{}/sam3", l.local_addr().unwrap());
std::thread::spawn(move || {
for mut c in l.incoming().flatten() {
let mut r = BufReader::new(c.try_clone().unwrap());
let mut line = String::new();
while r.read_line(&mut line).unwrap_or(0) > 2 {
line.clear();
}
let _ = c.write_all(b"HTTP/1.1 200 OK\r\nContent-Length: 1000000\r\nConnection: close\r\n\r\n");
for _ in 0..250 {
if c.write_all(&[b' '; 4000]).is_err() {
break;
}
std::thread::sleep(Duration::from_millis(20));
}
}
});
let dir = tmp("download-activity");
let mirrors = dir.with_extension("activity-mirrors.txt");
std::fs::write(&mirrors, format!("# test mirror\n{base}\n")).unwrap();
let mut s = Session::with_demo();
s.segmenter.dir = Some(dir.clone());
s.segmenter.mirrors_file = Some(mirrors.clone());
assert_eq!(s.execute("segment.model.download", &json!({"acknowledged": true})).unwrap()["started"], true);
let rows = s.activity.list();
assert_eq!(rows.len(), 1);
assert_eq!((rows[0].kind, rows[0].label.as_str(), rows[0].detail.as_str()), ("download", "Downloading model", "SAM 3"));
assert_eq!(rows[0].unit, crate::activity::Unit::Bytes);
assert!(rows[0].cancellable);
s.execute("activity.cancel", &json!({"id": rows[0].id})).unwrap();
let t = Instant::now();
while s.segmenter.download_status().running || !s.activity.list().is_empty() {
assert!(t.elapsed() < Duration::from_secs(10), "the download never stopped");
std::thread::sleep(Duration::from_millis(10));
}
let st = s.segmenter.download_status();
assert!(st.cancelled, "the app tells a cancel by the flag (#1046): {st:?}");
let e = st.error.unwrap_or_default();
assert!(e.contains("cancelled"), "{e}");
assert!(!s.segmenter.installed());
let _ = std::fs::remove_dir_all(&dir);
let _ = std::fs::remove_file(&mirrors);
}
/// Frontends decide on the error's kind, not its words (#1046): a missing model is
/// `ModelMissing(Sam3)` whether the app queues requests (background) or a command waits (CLI, MCP);
/// a build without AI masks is not a missing model (there is nothing to download).
#[test]
fn a_missing_model_is_model_missing_by_kind() {
use crate::{AiModel, ErrorKind};
for background in [true, false] {
let mut s = Session::with_demo();
s.segmenter.background = background;
s.segmenter.dir = Some(tmp(&format!("kind-{background}")));
for (cmd, p) in
[("mask.add", json!({"kind": "object"})), ("mask.add", json!({"kind": "prompt", "text": "sky"})), ("segment.prepare", json!({}))]
{
let e = s.execute(cmd, &p).unwrap_err();
if Segmenter::AVAILABLE {
assert_eq!(e.kind(), ErrorKind::ModelMissing(AiModel::Sam3), "{cmd} (background {background}): {e}");
assert!(e.to_string().starts_with(NOT_INSTALLED), "the message is unchanged: {e}");
} else {
assert_eq!(e.kind(), ErrorKind::Other, "{cmd}: {e}");
}
}
}
}