feat(pack): add uninstall subcommand and full install/uninstall tests

weft-pack:
- uninstall <app_id>: validates app ID, resolves store root, removes
  the installed package directory. Fails with an error if the package
  is not present or the app ID is malformed.
- Extracted install_package_to(dir, root) and
  uninstall_package_from(app_id, root) inner functions so tests can
  drive them directly without touching process env vars (avoids parallel
  test env-var races).
- install_package / uninstall_package remain the CLI-facing wrappers
  that call resolve_install_root().

Tests added (2):
- install_package_copies_to_store: writes a valid temp package, calls
  install_package_to, verifies all files are present, confirms a second
  install fails.
- uninstall_package_removes_directory: installs then uninstalls,
  verifies directory is removed, confirms a second uninstall fails.
Both tests use process-ID-derived paths to avoid cross-test collisions.
This commit is contained in:
Marco Allegretti 2026-03-11 09:54:39 +01:00
parent 265868bf67
commit b2bb76125f

View file

@ -47,11 +47,16 @@ fn main() -> anyhow::Result<()> {
let dir = args.get(2).context("usage: weft-pack install <dir>")?;
install_package(Path::new(dir))?;
}
Some("uninstall") => {
let app_id = args.get(2).context("usage: weft-pack uninstall <app_id>")?;
uninstall_package(app_id)?;
}
_ => {
eprintln!("usage:");
eprintln!(" weft-pack check <dir> validate a package directory");
eprintln!(" weft-pack info <dir> print package metadata");
eprintln!(" weft-pack install <dir> install package to app store");
eprintln!(" weft-pack uninstall <app_id> remove installed package");
std::process::exit(1);
}
}
@ -160,13 +165,15 @@ fn resolve_install_root() -> anyhow::Result<PathBuf> {
}
fn install_package(dir: &Path) -> anyhow::Result<()> {
let root = resolve_install_root()?;
install_package_to(dir, &root)
}
fn install_package_to(dir: &Path, store_root: &Path) -> anyhow::Result<()> {
check_package(dir)?;
let manifest = load_manifest(dir)?;
let app_id = &manifest.package.id;
let store_root = resolve_install_root()?;
let dest = store_root.join(app_id);
if dest.exists() {
anyhow::bail!(
"package '{}' is already installed at {}; remove it first",
@ -174,14 +181,34 @@ fn install_package(dir: &Path) -> anyhow::Result<()> {
dest.display()
);
}
copy_dir(dir, &dest)
.with_context(|| format!("copy {} -> {}", dir.display(), dest.display()))?;
println!("installed {} -> {}", app_id, dest.display());
Ok(())
}
fn uninstall_package(app_id: &str) -> anyhow::Result<()> {
let root = resolve_install_root()?;
uninstall_package_from(app_id, &root)
}
fn uninstall_package_from(app_id: &str, store_root: &Path) -> anyhow::Result<()> {
if !is_valid_app_id(app_id) {
anyhow::bail!("'{}' is not a valid app ID", app_id);
}
let target = store_root.join(app_id);
if !target.exists() {
anyhow::bail!(
"package '{}' is not installed at {}",
app_id,
target.display()
);
}
std::fs::remove_dir_all(&target).with_context(|| format!("remove {}", target.display()))?;
println!("uninstalled {}", app_id);
Ok(())
}
fn copy_dir(src: &Path, dst: &Path) -> anyhow::Result<()> {
std::fs::create_dir_all(dst)?;
for entry in std::fs::read_dir(src)? {
@ -260,6 +287,64 @@ entry = "ui/index.html"
let _ = fs::remove_dir_all(&tmp);
}
#[test]
fn install_package_copies_to_store() {
use std::fs;
let id = format!("weft.pack.install{}", std::process::id());
let src = std::env::temp_dir().join(format!("weft_pack_install_src_{}", id));
let store = std::env::temp_dir().join(format!("weft_pack_install_store_{}", id));
let ui_dir = src.join("ui");
let _ = fs::create_dir_all(&ui_dir);
fs::write(src.join("app.wasm"), b"\0asm").unwrap();
fs::write(ui_dir.join("index.html"), b"<!DOCTYPE html>").unwrap();
let app_id = format!("com.example.t{}", std::process::id());
fs::write(
src.join("wapp.toml"),
format!(
"[package]\nid = \"{app_id}\"\nname = \"Test\"\nversion = \"1.0.0\"\n\n\
[runtime]\nmodule = \"app.wasm\"\n\n[ui]\nentry = \"ui/index.html\"\n"
),
)
.unwrap();
let result = install_package_to(&src, &store);
assert!(result.is_ok(), "{result:?}");
assert!(store.join(&app_id).join("app.wasm").exists());
assert!(store.join(&app_id).join("wapp.toml").exists());
assert!(store.join(&app_id).join("ui").join("index.html").exists());
assert!(install_package_to(&src, &store).is_err());
let _ = fs::remove_dir_all(&src);
let _ = fs::remove_dir_all(&store);
}
#[test]
fn uninstall_package_removes_directory() {
use std::fs;
let id = format!("weft.pack.uninstall{}", std::process::id());
let src = std::env::temp_dir().join(format!("weft_pack_uninstall_src_{}", id));
let store = std::env::temp_dir().join(format!("weft_pack_uninstall_store_{}", id));
let ui_dir = src.join("ui");
let _ = fs::create_dir_all(&ui_dir);
fs::write(src.join("app.wasm"), b"\0asm").unwrap();
fs::write(ui_dir.join("index.html"), b"").unwrap();
let app_id = format!("com.example.u{}", std::process::id());
fs::write(
src.join("wapp.toml"),
format!(
"[package]\nid = \"{app_id}\"\nname = \"U\"\nversion = \"1.0.0\"\n\n\
[runtime]\nmodule = \"app.wasm\"\n\n[ui]\nentry = \"ui/index.html\"\n"
),
)
.unwrap();
install_package_to(&src, &store).unwrap();
assert!(store.join(&app_id).exists());
let result = uninstall_package_from(&app_id, &store);
assert!(result.is_ok(), "{result:?}");
assert!(!store.join(&app_id).exists());
assert!(uninstall_package_from(&app_id, &store).is_err());
let _ = fs::remove_dir_all(&src);
let _ = fs::remove_dir_all(&store);
}
#[test]
fn check_package_invalid_app_id() {
use std::fs;