Files
Marcos Paulo cac6b0ed39
ci / ci (macos-latest) (push) Has been cancelled
ci / ci (ubuntu-latest) (push) Has been cancelled
ci / ci (windows-latest) (push) Has been cancelled
feat: registry consumers (grammar, theme, pack), seed reader, wasm plugin host with approval, wanted scan, STIX and Lottie store lookups, plugin auto-disable persistence
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-21 18:35:47 -03:00

144 lines
6.2 KiB
Rust

//! State file, per-vault file and store layout of the add-on registry.
mod common;
use notescraft_engine::addons::{Installed, Kind, Source, State, Store, VaultFile, Version, key};
fn v(major: u16, minor: u16, patch: u16) -> Version {
Version { major, minor, patch }
}
fn row(version: Version, previous: Option<Version>) -> Installed {
Installed { version, previous, source: Source::Registry, installed_at: "2026-10-21T12:00:00Z".into(), sha256_of_files: "e".repeat(64) }
}
fn sample() -> State {
let mut state = State::default();
state.installed.insert(key(Kind::Grammar, "twig"), row(v(1, 1, 0), Some(v(1, 0, 0))));
state.installed.insert(key(Kind::Plugin, "dice"), row(v(0, 1, 0), None));
state
}
#[test]
fn state_round_trip() {
let state = sample();
assert_eq!(State::parse(state.to_json().unwrap().as_bytes()).unwrap(), state);
}
#[test]
fn state_rejections() {
assert!(State::parse(b"nope").is_err());
assert!(State::parse(br#"{"version":2,"installed":{}}"#).is_err());
assert!(State::parse(br#"{"installed":{}}"#).is_err());
let good = |k: &str| {
format!(r#"{{"version":1,"installed":{{"{k}":{{"version":"1.0.0","source":"seed","installed_at":"t","sha256_of_files":"{}"}}}}}}"#, "e".repeat(64))
};
assert!(State::parse(good("grammar:twig").as_bytes()).is_ok());
for bad in ["twig", "exe:twig", "grammar:../x", "grammar:Twig", "grammar:"] {
assert!(State::parse(good(bad).as_bytes()).is_err(), "{bad}");
}
assert!(State::parse(good("grammar:a").replace("1.0.0", "1.0").as_bytes()).is_err());
assert!(State::parse(good("grammar:a").replace("seed", "magic").as_bytes()).is_err());
let mut big = State::default();
for i in 0..513 {
big.installed.insert(key(Kind::Theme, &format!("t{i}")), row(v(1, 0, 0), None));
}
assert!(big.to_json().is_err());
let rows: Vec<String> =
(0..513).map(|i| format!(r#""theme:t{i}":{{"version":"1.0.0","source":"seed","installed_at":"t","sha256_of_files":"{}"}}"#, "e".repeat(64))).collect();
assert!(State::parse(format!(r#"{{"version":1,"installed":{{{}}}}}"#, rows.join(",")).as_bytes()).is_err());
}
#[test]
fn vault_file_round_trip_and_caps() {
let file = VaultFile { enabled: vec!["plugin:dice".into()], wanted: vec!["grammar:twig".into(), "theme:x".into()], ..VaultFile::default() };
assert_eq!(VaultFile::parse(file.to_json().unwrap().as_bytes()).unwrap(), file);
assert!(VaultFile::parse(br#"{"version":2}"#).is_err());
assert!(VaultFile::parse(br#"{"version":1,"enabled":["grammar:twig"]}"#).is_err());
assert!(VaultFile::parse(br#"{"version":1,"wanted":["../x"]}"#).is_err());
let many: Vec<String> = (0..257).map(|i| format!("grammar:g{i}")).collect();
assert!(VaultFile { wanted: many, ..VaultFile::default() }.to_json().is_err());
assert_eq!(VaultFile::parse(br#"{"version":1}"#).unwrap(), VaultFile::default());
}
#[test]
fn commit_and_load() {
let dir = common::temp_config("addons-commit");
let store = Store::open(dir.path()).unwrap();
assert_eq!(store.load().unwrap(), State::default());
let state = sample();
store.commit(&state).unwrap();
assert_eq!(store.load().unwrap(), state);
assert!(store.root().join("state.json").is_file());
}
#[test]
fn corrupt_state_errs_and_file_is_untouched() {
let dir = common::temp_config("addons-corrupt");
let store = Store::open(dir.path()).unwrap();
let path = store.root().join("state.json");
std::fs::write(&path, b"{ not json").unwrap();
assert!(store.load().is_err());
assert!(store.gc(&State::default()).is_ok());
assert_eq!(std::fs::read(&path).unwrap(), b"{ not json");
}
#[test]
fn version_dir_uses_validated_parts() {
let dir = common::temp_config("addons-vdir");
let store = Store::open(dir.path()).unwrap();
let path = store.version_dir(Kind::Grammar, "twig", v(1, 2, 3)).unwrap();
assert!(path.starts_with(store.root()));
assert!(path.ends_with("store/grammar/twig/1.2.3"));
assert!(store.version_dir(Kind::Grammar, "../x", v(1, 0, 0)).is_err());
assert!(store.version_dir(Kind::Grammar, "a/b", v(1, 0, 0)).is_err());
assert!(store.version_dir(Kind::Grammar, "", v(1, 0, 0)).is_err());
}
#[test]
fn gc_keeps_version_and_previous_only() {
let dir = common::temp_config("addons-gc");
let store = Store::open(dir.path()).unwrap();
let state = sample();
let touch = |kind, id, ver| {
let d = store.version_dir(kind, id, ver).unwrap();
std::fs::create_dir_all(&d).unwrap();
std::fs::write(d.join("f"), b"x").unwrap();
d
};
let active = touch(Kind::Grammar, "twig", v(1, 1, 0));
let previous = touch(Kind::Grammar, "twig", v(1, 0, 0));
let old = touch(Kind::Grammar, "twig", v(0, 9, 0));
let orphan = touch(Kind::Theme, "gone", v(1, 0, 0));
let plugin = touch(Kind::Plugin, "dice", v(0, 1, 0));
std::fs::create_dir_all(store.staging_dir().join("abc")).unwrap();
std::fs::write(store.staging_dir().join("abc/part"), b"x").unwrap();
std::fs::write(store.staging_dir().join("loose"), b"x").unwrap();
let outside = dir.path().join("outside");
std::fs::create_dir_all(&outside).unwrap();
std::fs::write(outside.join("keep"), b"x").unwrap();
assert_eq!(store.gc(&state).unwrap(), 2);
assert!(active.is_dir() && previous.is_dir() && plugin.is_dir());
assert!(!old.exists() && !orphan.exists());
assert!(!orphan.parent().unwrap().exists(), "empty parents are pruned");
assert_eq!(std::fs::read_dir(store.staging_dir()).unwrap().count(), 0);
assert!(outside.join("keep").is_file());
}
#[cfg(unix)]
#[test]
fn gc_does_not_follow_links() {
let dir = common::temp_config("addons-link");
let store = Store::open(dir.path()).unwrap();
let outside = dir.path().join("outside");
std::fs::create_dir_all(&outside).unwrap();
std::fs::write(outside.join("keep"), b"x").unwrap();
let parent = store.version_dir(Kind::Theme, "t", v(1, 0, 0)).unwrap().parent().unwrap().to_path_buf();
std::fs::create_dir_all(&parent).unwrap();
std::os::unix::fs::symlink(&outside, parent.join("2.0.0")).unwrap();
store.gc(&State::default()).unwrap();
assert!(outside.join("keep").is_file());
assert!(!parent.join("2.0.0").exists());
}