mirror of
https://github.com/storytold/effectcraft.git
synced 2026-11-10 13:34:15 +00:00
* G8: plug-in library Community Plug-in Library for WebAssembly effect plug-ins (#376): one reviewed catalog (index.json), install / update / remove from Settings > Plug-in Library (also Effect > Plug-in Library...), library.* commands, start-up loading of installed packages with SHA-256 checks. Downloads go through the system curl (https, GitHub hosts), as trained models do. Preview before installing (library.preview): the release is downloaded, checked like an install, and rendered in the sandbox on a built-in sample frame at default settings (2 s at 12 fps) with a Before/After split, plus the effect's parameters, "Open on GitHub" and an optional author video link (catalog field preview.video). Authors need to do nothing for it. Hardening: a module that fails to load leaves the installed copy alone, Update All updates every package in one job, minApp also applies to an explicit version, curl ignores ~/.curlrc and URL globbing, unique temp files, percent-decoded file URLs. Plug-in effect slots grow from 64 to 256 with a clearer overflow error. Docs: plugins.md "Plug-in Library", publishing-plugins.md (author guide from zero to listed), gaps.md; examples/plugin-catalog is the starting content of the catalog repository (schema, validation workflow). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * contributors: credit iseemhlyfta * G8: plug-in library: doc comments on the library's functions Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * G8: plug-in catalog check: require one object with schema 1 and a packages array An empty file or several JSON values passed jq empty, and with SKIP_SCHEMA=1 nothing else checked the catalog's shape. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * G8: plug-in library: one preview texture set; catalog schema requires effects The preview kept a texture set per package version previewed (about 6 MB each) for the whole session; it now keeps one, replaced when another package is previewed. Releases in the catalog schema must list at least one effect id, as the author guide says. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * G8: plug-in catalog schema: effect ids follow the manifest's id rules Letters, digits, '.', '_' and '-', and not the reserved 'ec.' prefix, so library.missing can match them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * G8: refuse Plug-in Library jobs while automation file access is confined Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * G8: split the Plug-in Library page into toolbar, card, links and actions Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * G8: show Plug-in Library toasts without a Roto job; cache preview frame checks Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * G8: log Plug-in Library packages that fail to load at start-up Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * G8: check an update before replacing a loaded package; Update All past failures - An update to a package that is already loaded is now compiled and its manifest checked (through the previewer, without registering it) before the new file is written and the old version's folder deleted. Before, a release with the right SHA-256 that didn't load replaced the working version and failed at the next start. - Package ids that differ only in case, or end in a dot, would share one folder on Windows and macOS: the catalog keeps the first of them, and ids can't end in a dot (schema too). - Update All: a package that can't be picked or installed no longer stops the others; the result names what was updated and what wasn't. - Downloads: at most 5 redirects, and one that ends off GitHub's hosts is refused, as the module docs say. - A job that panics no longer leaves the library busy; poisoned status locks are used, not dropped. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * G8: rustfmt the update test Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
348 lines
20 KiB
Rust
348 lines
20 KiB
Rust
//! The Plug-in Library in a full session, against a local catalog (`file://`): install, update,
|
||
//! remove and start-up loading of WebAssembly packages, checksum and URL rules.
|
||
|
||
use std::path::{Path, PathBuf};
|
||
|
||
use effectcraft_engine::Session;
|
||
use effectcraft_engine::project::{GroupKind, LayerId, Node};
|
||
use effectcraft_engine::segment::sha256;
|
||
use serde_json::{Value, json};
|
||
|
||
use super::plugins::wat_plugin;
|
||
|
||
/// A fresh folder for one test: the plug-ins folder and the catalog's files.
|
||
fn tmp(name: &str) -> PathBuf {
|
||
let d = std::env::temp_dir().join(format!("effectcraft-library-{name}-{}", std::process::id()));
|
||
let _ = std::fs::remove_dir_all(&d);
|
||
std::fs::create_dir_all(d.join("served")).unwrap();
|
||
d
|
||
}
|
||
|
||
/// `p` as a `file://` URL (`file:///C:/…` on Windows).
|
||
fn file_url(p: &Path) -> String {
|
||
let s = p.to_string_lossy().replace('\\', "/");
|
||
if s.starts_with('/') { format!("file://{s}") } else { format!("file:///{s}") }
|
||
}
|
||
|
||
/// A host session that installs into `dir`.
|
||
fn session(dir: &Path) -> Session {
|
||
let mut s = effectcraft_host::session();
|
||
s.plugins_dir = Some(dir.join("Plug-ins"));
|
||
s
|
||
}
|
||
|
||
/// A served module for `effect`; returns (url, sha256).
|
||
fn serve(dir: &Path, file: &str, effect: &str) -> (String, String) {
|
||
let bytes = wat_plugin(effect, 1, "i32.const 0").into_bytes();
|
||
let path = dir.join("served").join(file);
|
||
std::fs::write(&path, &bytes).unwrap();
|
||
(file_url(&path), sha256::hex(&bytes))
|
||
}
|
||
|
||
/// Write the catalog (`packages`) and return its URL.
|
||
fn write_catalog(dir: &Path, packages: Value) -> String {
|
||
let path = dir.join("served").join("index.json");
|
||
std::fs::write(&path, json!({"schema": 1, "packages": packages}).to_string()).unwrap();
|
||
file_url(&path)
|
||
}
|
||
|
||
/// A catalog release entry for a served module.
|
||
fn release(version: &str, (url, sha): &(String, String), effect: &str) -> Value {
|
||
json!({"version": version, "api": 1, "url": url, "sha256": sha, "effects": [effect]})
|
||
}
|
||
|
||
/// The package `id` in a `library.list` reply.
|
||
fn package<'a>(list: &'a Value, id: &str) -> &'a Value {
|
||
list["packages"].as_array().unwrap().iter().find(|p| p["id"] == id).unwrap_or_else(|| panic!("no {id} in {list}"))
|
||
}
|
||
|
||
/// The effect ids registered in the process.
|
||
fn plugin_ids(s: &mut Session) -> Vec<String> {
|
||
let l = s.execute("effect.plugins.list", json!({})).unwrap();
|
||
l["plugins"].as_array().unwrap().iter().filter_map(|p| p["id"].as_str().map(str::to_string)).collect()
|
||
}
|
||
|
||
#[test]
|
||
fn install_update_and_remove_a_package() {
|
||
let dir = tmp("cycle");
|
||
let fx = "org.test.library.cycle";
|
||
let v1 = serve(&dir, "cycle-1.wasm", fx);
|
||
let catalog = write_catalog(
|
||
&dir,
|
||
json!([{"id": "org.test.cycle", "name": "Cycle", "author": "Tester", "license": "MIT", "tags": ["test"], "versions": [release("1.0.0", &v1, fx)]}]),
|
||
);
|
||
let mut s = session(&dir);
|
||
// Before a catalog: nothing listed, installing is refused.
|
||
assert_eq!(s.execute("library.list", json!({})).unwrap()["packages"], json!([]));
|
||
assert!(s.execute("library.install", json!({"id": "org.test.cycle", "wait": true})).unwrap_err().to_string().contains("library.refresh"));
|
||
assert_eq!(s.execute("library.refresh", json!({"catalog": catalog, "wait": true})).unwrap()["packages"], 1);
|
||
let l = s.execute("library.list", json!({"query": "tester"})).unwrap();
|
||
let p = package(&l, "org.test.cycle");
|
||
assert_eq!((p["latest"].as_str(), p["installed"].as_str(), p["effects"][0].as_str()), (Some("1.0.0"), None, Some(fx)), "{l}");
|
||
assert_eq!(l["tags"], json!(["test"]));
|
||
assert_eq!(s.execute("library.list", json!({"query": "no such thing"})).unwrap()["packages"], json!([]));
|
||
|
||
let r = s.execute("library.install", json!({"id": "org.test.cycle", "wait": true})).unwrap();
|
||
assert_eq!((r["installed"].as_bool(), r["restartNeeded"].as_bool()), (Some(true), Some(false)), "{r}");
|
||
assert!(plugin_ids(&mut s).iter().any(|i| i == fx), "loaded right away");
|
||
let lib = dir.join("Plug-ins").join("Library");
|
||
assert!(lib.join("org.test.cycle").join("1.0.0").join("org.test.cycle.wasm").is_file());
|
||
let installed: Value = serde_json::from_slice(&std::fs::read(lib.join("installed.json")).unwrap()).unwrap();
|
||
assert_eq!(installed["packages"]["org.test.cycle"]["version"], "1.0.0");
|
||
// The effect applies like any other.
|
||
s.execute("comp.new", json!({"name": "L", "width": 8, "height": 8, "duration": 1})).unwrap();
|
||
let layer = s.execute("layer.newSolid", json!({"color": [1, 0, 0], "width": 8, "height": 8})).unwrap()["layer"].clone();
|
||
s.execute("effect.apply", json!({"layer": layer, "effect": fx})).unwrap();
|
||
assert_eq!(s.execute("library.missing", json!({})).unwrap()["effects"], json!([]));
|
||
|
||
// Version 2: an update; the loaded version stays until the next start.
|
||
let v2 = serve(&dir, "cycle-2.wasm", fx);
|
||
let catalog = write_catalog(&dir, json!([{"id": "org.test.cycle", "name": "Cycle", "versions": [release("1.0.0", &v1, fx), release("1.10.0", &v2, fx)]}]));
|
||
s.execute("library.refresh", json!({"catalog": catalog, "wait": true})).unwrap();
|
||
assert_eq!(package(&s.execute("library.list", json!({})).unwrap(), "org.test.cycle")["updateAvailable"], true);
|
||
let r = s.execute("library.update", json!({"wait": true})).unwrap();
|
||
assert_eq!(r["updating"], json!(["org.test.cycle"]), "{r}");
|
||
let p = s.execute("library.list", json!({"installed": true})).unwrap();
|
||
let p = package(&p, "org.test.cycle");
|
||
assert_eq!((p["installed"].as_str(), p["updateAvailable"].as_bool(), p["restartNeeded"].as_bool()), (Some("1.10.0"), Some(false), Some(true)), "{p}");
|
||
assert!(!lib.join("org.test.cycle").join("1.0.0").exists(), "the old version is removed");
|
||
|
||
let r = s.execute("library.uninstall", json!({"id": "org.test.cycle"})).unwrap();
|
||
assert_eq!((r["removed"].as_bool(), r["restartNeeded"].as_bool()), (Some(true), Some(true)));
|
||
assert!(!lib.join("org.test.cycle").exists());
|
||
assert_eq!(s.execute("library.uninstall", json!({"id": "org.test.cycle"})).unwrap()["removed"], false);
|
||
// Still listed by the catalog, no longer installed.
|
||
assert!(package(&s.execute("library.list", json!({})).unwrap(), "org.test.cycle")["installed"].is_null());
|
||
let _ = std::fs::remove_dir_all(&dir);
|
||
}
|
||
|
||
#[test]
|
||
fn a_wrong_checksum_or_a_bad_module_installs_nothing() {
|
||
let dir = tmp("checks");
|
||
let (url, _) = serve(&dir, "a.wasm", "org.test.library.checks.a");
|
||
let garbage = dir.join("served").join("garbage.wasm");
|
||
std::fs::write(&garbage, b"\0asm\x01\0\0\0garbage").unwrap();
|
||
let garbage_sha = sha256::hex(&std::fs::read(&garbage).unwrap());
|
||
let catalog = write_catalog(
|
||
&dir,
|
||
json!([
|
||
{"id": "org.test.badsum", "name": "Bad Sum", "versions": [{"version": "1", "api": 1, "url": url, "sha256": "0".repeat(64)}]},
|
||
{"id": "org.test.garbage", "name": "Garbage", "versions": [{"version": "1", "api": 1, "url": file_url(&garbage), "sha256": garbage_sha}]},
|
||
{"id": "org.test.web", "name": "Web", "versions": [{"version": "1", "api": 1, "url": "https://example.com/x.wasm", "sha256": "0".repeat(64)}]},
|
||
{"id": "org.test.future", "name": "Future", "versions": [{"version": "1", "api": 99, "url": url, "sha256": "0".repeat(64)}]}
|
||
]),
|
||
);
|
||
let mut s = session(&dir);
|
||
s.execute("library.refresh", json!({"catalog": catalog, "wait": true})).unwrap();
|
||
let e = s.execute("library.install", json!({"id": "org.test.badsum", "wait": true})).unwrap_err().to_string();
|
||
assert!(e.contains("SHA-256"), "{e}");
|
||
let e = s.execute("library.install", json!({"id": "org.test.garbage", "wait": true})).unwrap_err().to_string();
|
||
assert!(e.contains("didn't load"), "{e}");
|
||
let e = s.execute("library.install", json!({"id": "org.test.web", "wait": true})).unwrap_err().to_string();
|
||
assert!(e.contains("isn't on GitHub"), "{e}");
|
||
let e = s.execute("library.install", json!({"id": "org.test.future", "wait": true})).unwrap_err().to_string();
|
||
assert!(e.contains("no release for this EffectCraft"), "{e}");
|
||
assert_eq!(package(&s.execute("library.list", json!({})).unwrap(), "org.test.future")["needsNewerApp"], true);
|
||
let lib = dir.join("Plug-ins").join("Library");
|
||
for id in ["org.test.badsum", "org.test.garbage", "org.test.web"] {
|
||
assert!(!lib.join(id).exists(), "{id}");
|
||
}
|
||
assert!(!lib.join("installed.json").exists());
|
||
// Remote catalogs must be on GitHub too; the list still works from the cached copy.
|
||
assert!(s.execute("library.refresh", json!({"catalog": "https://example.com/index.json"})).unwrap_err().to_string().contains("isn't on GitHub"));
|
||
assert!(s.execute("library.refresh", json!({"catalog": "http://raw.githubusercontent.com/x"})).is_err());
|
||
let mut fresh = session(&dir);
|
||
assert_eq!(fresh.execute("library.list", json!({})).unwrap()["packages"].as_array().unwrap().len(), 4, "read from catalog.json");
|
||
let _ = std::fs::remove_dir_all(&dir);
|
||
}
|
||
|
||
#[test]
|
||
fn installed_packages_load_at_start_and_changed_files_are_refused() {
|
||
let dir = tmp("startup");
|
||
let lib = dir.join("Plug-ins").join("Library");
|
||
let mut entries = serde_json::Map::new();
|
||
for (id, effect, tamper) in
|
||
[("org.test.start.ok", "org.test.library.start.ok", false), ("org.test.start.tampered", "org.test.library.start.tampered", true)]
|
||
{
|
||
let bytes = wat_plugin(effect, 1, "i32.const 0").into_bytes();
|
||
let file = format!("{id}/1.0.0/{id}.wasm");
|
||
let path = lib.join(id).join("1.0.0");
|
||
std::fs::create_dir_all(&path).unwrap();
|
||
let written = if tamper { wat_plugin(effect, 1, "i32.const 1").into_bytes() } else { bytes.clone() };
|
||
std::fs::write(path.join(format!("{id}.wasm")), written).unwrap();
|
||
entries.insert(id.into(), json!({"version": "1.0.0", "file": file, "sha256": sha256::hex(&bytes), "effects": [effect]}));
|
||
}
|
||
entries.insert("org.test.start.escape".into(), json!({"version": "1", "file": "../../outside.wasm", "sha256": "0".repeat(64)}));
|
||
std::fs::write(lib.join("installed.json"), json!({"packages": entries}).to_string()).unwrap();
|
||
let mut s = session(&dir);
|
||
let r = s.execute("library.loadInstalled", json!({})).unwrap();
|
||
let loaded: Vec<&str> = r["loaded"].as_array().unwrap().iter().filter_map(|v| v["package"].as_str()).collect();
|
||
assert_eq!(loaded, ["org.test.start.ok"], "{r}");
|
||
let errors = r["errors"].to_string();
|
||
assert!(errors.contains("org.test.start.tampered") && errors.contains("SHA-256"), "{r}");
|
||
assert!(errors.contains("bad module path"), "{r}");
|
||
assert!(plugin_ids(&mut s).iter().any(|i| i == "org.test.library.start.ok"));
|
||
// Running it again loads nothing twice.
|
||
assert_eq!(s.execute("library.loadInstalled", json!({})).unwrap()["loaded"], json!([]));
|
||
// Without a catalog the installed packages are still listed.
|
||
let l = s.execute("library.list", json!({})).unwrap();
|
||
let p = package(&l, "org.test.start.ok");
|
||
assert_eq!((p["unlisted"].as_bool(), p["loaded"].as_str()), (Some(true), Some("1.0.0")), "{l}");
|
||
// A corrupt installed.json is an empty library, not a failure.
|
||
std::fs::write(lib.join("installed.json"), b"{not json").unwrap();
|
||
assert_eq!(session(&dir).execute("library.loadInstalled", json!({})).unwrap()["loaded"], json!([]));
|
||
let _ = std::fs::remove_dir_all(&dir);
|
||
}
|
||
|
||
#[test]
|
||
fn missing_effects_name_the_packages_that_provide_them() {
|
||
let dir = tmp("missing");
|
||
let fx = "org.test.library.missing.fx";
|
||
let v = serve(&dir, "m.wasm", fx);
|
||
let catalog = write_catalog(&dir, json!([{"id": "org.test.missing", "name": "Missing", "versions": [release("1.0.0", &v, fx)]}]));
|
||
let mut s = session(&dir);
|
||
s.execute("library.refresh", json!({"catalog": catalog, "wait": true})).unwrap();
|
||
// A project whose effects include one this session doesn't have (as when a project from
|
||
// someone with the plug-in opens here).
|
||
s.execute("comp.new", json!({"name": "M", "width": 8, "height": 8, "duration": 1})).unwrap();
|
||
let layer = s.execute("layer.newSolid", json!({"color": [1, 0, 0], "width": 8, "height": 8})).unwrap()["layer"].as_u64().unwrap();
|
||
s.execute("effect.apply", json!({"layer": layer, "effect": "ec.blur.gaussian"})).unwrap();
|
||
s.execute("effect.apply", json!({"layer": layer, "effect": "ec.blur.gaussian"})).unwrap();
|
||
let cid = s.active_comp_id().unwrap();
|
||
let proj = std::sync::Arc::make_mut(&mut s.project);
|
||
let l = proj.comp_mut(cid).unwrap().layer_mut(LayerId(layer)).unwrap();
|
||
let first = l.props.sub_mut("effects").unwrap().children.iter_mut().find_map(|n| match n {
|
||
Node::Group(g) => Some(g),
|
||
_ => None,
|
||
});
|
||
first.unwrap().kind = GroupKind::Effect { effect: fx.into() };
|
||
let r = s.execute("library.missing", json!({})).unwrap();
|
||
assert_eq!(r["effects"], json!([{"effect": fx, "packages": ["org.test.missing"]}]), "{r}");
|
||
let _ = std::fs::remove_dir_all(&dir);
|
||
}
|
||
|
||
#[test]
|
||
fn preview_renders_a_package_without_installing_it() {
|
||
let dir = tmp("preview");
|
||
let fx = "org.test.library.preview.fx";
|
||
// The preview frame (320 × 180 RGBA f32) needs more than the test module's two pages.
|
||
let bytes = wat_plugin(fx, 1, "i32.const 0").replace("(memory (export \"memory\") 2)", "(memory (export \"memory\") 20)").into_bytes();
|
||
let path = dir.join("served").join("p.wasm");
|
||
std::fs::write(&path, &bytes).unwrap();
|
||
let v = (file_url(&path), sha256::hex(&bytes));
|
||
let mut bad = release("1.0.0", &v, fx);
|
||
bad["sha256"] = json!("0".repeat(64));
|
||
let catalog = write_catalog(
|
||
&dir,
|
||
json!([
|
||
{"id": "org.test.preview", "name": "Preview", "preview": {"video": "https://www.youtube.com/watch?v=x"}, "versions": [release("1.0.0", &v, fx)]},
|
||
{"id": "org.test.preview.bad", "name": "Bad", "preview": {"video": "javascript:alert(1)"}, "versions": [bad]}
|
||
]),
|
||
);
|
||
let mut s = session(&dir);
|
||
s.execute("library.refresh", json!({"catalog": catalog, "wait": true})).unwrap();
|
||
let r = s.execute("library.preview", json!({"id": "org.test.preview", "wait": true})).unwrap();
|
||
assert_eq!((r["frames"].as_u64(), r["width"].as_u64(), r["effect"]["id"].as_str()), (Some(24), Some(320), Some(fx)), "{r}");
|
||
let st = s.library.status();
|
||
let pv = st.preview.as_ref().unwrap();
|
||
assert_eq!((pv.before.len(), pv.frames[0].len()), (320 * 180 * 4, 320 * 180 * 4));
|
||
// Previewing installs and registers nothing.
|
||
assert!(!dir.join("Plug-ins").join("Library").join("org.test.preview").exists());
|
||
assert!(!plugin_ids(&mut s).iter().any(|i| i == fx));
|
||
let l = s.execute("library.list", json!({})).unwrap();
|
||
let p = package(&l, "org.test.preview");
|
||
assert_eq!((p["previewed"].as_bool(), p["video"].as_str()), (Some(true), Some("https://www.youtube.com/watch?v=x")), "{l}");
|
||
assert!(package(&l, "org.test.preview.bad")["video"].is_null(), "only https:// videos are kept");
|
||
// Asking again answers from memory; a wrong checksum previews nothing.
|
||
assert_eq!(s.execute("library.preview", json!({"id": "org.test.preview"})).unwrap()["frames"], 24);
|
||
let e = s.execute("library.preview", json!({"id": "org.test.preview.bad", "wait": true})).unwrap_err().to_string();
|
||
assert!(e.contains("SHA-256"), "{e}");
|
||
let _ = std::fs::remove_dir_all(&dir);
|
||
}
|
||
|
||
#[test]
|
||
fn a_reinstall_that_fails_to_load_keeps_the_installed_copy() {
|
||
let dir = tmp("reinstall");
|
||
let fx = "org.test.library.reinstall.fx";
|
||
let v = serve(&dir, "r.wasm", fx);
|
||
let catalog = write_catalog(&dir, json!([{"id": "org.test.reinstall", "name": "Reinstall", "versions": [release("1.0.0", &v, fx)]}]));
|
||
let mut s = session(&dir);
|
||
s.execute("library.refresh", json!({"catalog": catalog, "wait": true})).unwrap();
|
||
s.execute("library.install", json!({"id": "org.test.reinstall", "wait": true})).unwrap();
|
||
// A second session in this process: the effect is registered already, so loading fails.
|
||
let mut other = session(&dir);
|
||
let e = other.execute("library.install", json!({"id": "org.test.reinstall", "wait": true})).unwrap_err().to_string();
|
||
assert!(e.contains("didn't load"), "{e}");
|
||
let lib = dir.join("Plug-ins").join("Library");
|
||
assert!(lib.join("org.test.reinstall").join("1.0.0").join("org.test.reinstall.wasm").is_file());
|
||
let installed: Value = serde_json::from_slice(&std::fs::read(lib.join("installed.json")).unwrap()).unwrap();
|
||
assert_eq!(installed["packages"]["org.test.reinstall"]["version"], "1.0.0");
|
||
let _ = std::fs::remove_dir_all(&dir);
|
||
}
|
||
|
||
#[test]
|
||
fn an_update_that_would_not_load_keeps_the_loaded_version() {
|
||
let dir = tmp("badupdate");
|
||
let fx = "org.test.library.badupdate.fx";
|
||
let v1 = serve(&dir, "u1.wasm", fx);
|
||
// The right checksum for a file that isn't a plug-in.
|
||
let junk = b"not a WebAssembly module".to_vec();
|
||
let path = dir.join("served").join("u2.wasm");
|
||
std::fs::write(&path, &junk).unwrap();
|
||
let v2 = (file_url(&path), sha256::hex(&junk));
|
||
let catalog = write_catalog(&dir, json!([{"id": "org.test.badupdate", "name": "Bad Update", "versions": [release("1.0.0", &v1, fx)]}]));
|
||
let mut s = session(&dir);
|
||
s.execute("library.refresh", json!({"catalog": catalog, "wait": true})).unwrap();
|
||
s.execute("library.install", json!({"id": "org.test.badupdate", "wait": true})).unwrap();
|
||
let catalog =
|
||
write_catalog(&dir, json!([{"id": "org.test.badupdate", "name": "Bad Update", "versions": [release("1.0.0", &v1, fx), release("2.0.0", &v2, fx)]}]));
|
||
s.execute("library.refresh", json!({"catalog": catalog, "wait": true})).unwrap();
|
||
// Version 1 is loaded in this session, so version 2 would only load at the next start.
|
||
let e = s.execute("library.update", json!({"id": "org.test.badupdate", "wait": true})).unwrap_err().to_string();
|
||
assert!(e.contains("didn't load"), "{e}");
|
||
let lib = dir.join("Plug-ins").join("Library");
|
||
assert!(lib.join("org.test.badupdate").join("1.0.0").join("org.test.badupdate.wasm").is_file());
|
||
assert!(!lib.join("org.test.badupdate").join("2.0.0").exists());
|
||
let installed: Value = serde_json::from_slice(&std::fs::read(lib.join("installed.json")).unwrap()).unwrap();
|
||
assert_eq!(installed["packages"]["org.test.badupdate"]["version"], "1.0.0");
|
||
let _ = std::fs::remove_dir_all(&dir);
|
||
}
|
||
|
||
#[test]
|
||
fn update_all_updates_every_package_and_min_app_is_enforced() {
|
||
let dir = tmp("updateall");
|
||
let (fa, fb) = ("org.test.library.updateall.a", "org.test.library.updateall.b");
|
||
let (a1, b1) = (serve(&dir, "a1.wasm", fa), serve(&dir, "b1.wasm", fb));
|
||
let catalog = write_catalog(
|
||
&dir,
|
||
json!([
|
||
{"id": "org.test.ua", "name": "A", "versions": [release("1.0.0", &a1, fa)]},
|
||
{"id": "org.test.ub", "name": "B", "versions": [release("1.0.0", &b1, fb)]}
|
||
]),
|
||
);
|
||
let mut s = session(&dir);
|
||
s.execute("library.refresh", json!({"catalog": catalog, "wait": true})).unwrap();
|
||
for id in ["org.test.ua", "org.test.ub"] {
|
||
s.execute("library.install", json!({"id": id, "wait": true})).unwrap();
|
||
}
|
||
let mut future = release("3.0.0", &a1, fa);
|
||
future["minApp"] = json!("999.0");
|
||
let catalog = write_catalog(
|
||
&dir,
|
||
json!([
|
||
{"id": "org.test.ua", "name": "A", "versions": [release("1.0.0", &a1, fa), release("2.0.0", &a1, fa), future]},
|
||
{"id": "org.test.ub", "name": "B", "versions": [release("1.0.0", &b1, fb), release("2.0.0", &b1, fb)]}
|
||
]),
|
||
);
|
||
s.execute("library.refresh", json!({"catalog": catalog, "wait": true})).unwrap();
|
||
let e = s.execute("library.install", json!({"id": "org.test.ua", "version": "3.0.0", "wait": true})).unwrap_err().to_string();
|
||
assert!(e.contains("999.0"), "{e}");
|
||
let r = s.execute("library.update", json!({"wait": true})).unwrap();
|
||
assert_eq!(r["updating"], json!(["org.test.ua", "org.test.ub"]), "{r}");
|
||
let l = s.execute("library.list", json!({"installed": true})).unwrap();
|
||
for id in ["org.test.ua", "org.test.ub"] {
|
||
assert_eq!(package(&l, id)["installed"], "2.0.0", "{l}");
|
||
}
|
||
let _ = std::fs::remove_dir_all(&dir);
|
||
}
|