pkgwatch/src/state.rs
Austin Schaefer 3e87282b21
All checks were successful
CI / build (pull_request) Successful in 48s
CI / test (pull_request) Successful in 2m1s
CI / audit (pull_request) Successful in 9s
CI / coverage (pull_request) Successful in 3m53s
Move high-level docs into docs/, matching doubleo7's convention
SPEC.md and ARCHITECTURE.md were sitting at the repo root alongside
Cargo.toml/Makefile.toml/packages.d — moved both into docs/ (doubleo7
already does this for its own supplementary docs, so this matches an
existing convention in the fleet rather than inventing a new one).

Updated every doc-comment cross-reference across src/*.rs and
Makefile.toml (23 references) to the new docs/SPEC.md / docs/
ARCHITECTURE.md paths. The two files' own cross-references to each other
didn't need changing — they're still same-directory relative references.

Also updated the project reference memory pointing at ARCHITECTURE.md's
location, so it doesn't go stale pointing at a path that no longer exists.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-17 13:22:40 +02:00

136 lines
4.8 KiB
Rust

//! Persists two independent per-package facts as plain files: the last
//! published version, and any version currently pending human review.
//! The only module that touches `state/` on disk.
use anyhow::Result;
use std::path::Path;
/// Last-known-published version per package, so re-runs don't re-flag a
/// version already handled. Deliberately just one file per package for
/// now — this is where a real review-queue persistence layer plugs in
/// later (see docs/SPEC.md > Architecture > Reviewer queue).
pub fn load_last_version(state_dir: &Path, name: &str) -> Option<String> {
std::fs::read_to_string(state_dir.join(format!("{name}.version")))
.ok()
.map(|s| s.trim().to_string())
}
pub fn save_last_version(state_dir: &Path, name: &str, version: &str) -> Result<()> {
std::fs::create_dir_all(state_dir)?;
std::fs::write(state_dir.join(format!("{name}.version")), version)?;
Ok(())
}
/// Tag currently awaiting human review for a tier 4-6 package (see
/// docs/SPEC.md > Architecture > Reviewer queue), if any. Separate from
/// `load_last_version`/`save_last_version`: approving a review doesn't
/// mean future versions auto-publish, so the two must be tracked
/// independently.
pub fn load_pending_version(state_dir: &Path, name: &str) -> Option<String> {
std::fs::read_to_string(state_dir.join(format!("{name}.pending")))
.ok()
.map(|s| s.trim().to_string())
}
pub fn save_pending_version(state_dir: &Path, name: &str, version: &str) -> Result<()> {
std::fs::create_dir_all(state_dir)?;
std::fs::write(state_dir.join(format!("{name}.pending")), version)?;
Ok(())
}
/// Clears a pending review, e.g. once it's been approved and published.
/// Not an error if there was nothing pending.
pub fn clear_pending_version(state_dir: &Path, name: &str) -> Result<()> {
match std::fs::remove_file(state_dir.join(format!("{name}.pending"))) {
Ok(()) => Ok(()),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()),
Err(e) => Err(e.into()),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn load_last_version_missing_file_returns_none() {
let dir = tempfile::tempdir().unwrap();
assert_eq!(load_last_version(dir.path(), "nonexistent"), None);
}
#[test]
fn save_then_load_roundtrips() {
let dir = tempfile::tempdir().unwrap();
save_last_version(dir.path(), "uv", "0.12.15").unwrap();
assert_eq!(
load_last_version(dir.path(), "uv"),
Some("0.12.15".to_string())
);
}
#[test]
fn load_last_version_trims_trailing_whitespace() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(dir.path().join("uv.version"), "0.12.15\n").unwrap();
assert_eq!(
load_last_version(dir.path(), "uv"),
Some("0.12.15".to_string())
);
}
#[test]
fn save_last_version_creates_state_dir_if_missing() {
let dir = tempfile::tempdir().unwrap();
let nested = dir.path().join("nested/state");
save_last_version(&nested, "uv", "0.12.15").unwrap();
assert_eq!(
load_last_version(&nested, "uv"),
Some("0.12.15".to_string())
);
}
#[test]
fn load_pending_version_missing_file_returns_none() {
let dir = tempfile::tempdir().unwrap();
assert_eq!(load_pending_version(dir.path(), "scaleway-cli"), None);
}
#[test]
fn save_then_load_pending_roundtrips() {
let dir = tempfile::tempdir().unwrap();
save_pending_version(dir.path(), "scaleway-cli", "v2.62.0").unwrap();
assert_eq!(
load_pending_version(dir.path(), "scaleway-cli"),
Some("v2.62.0".to_string())
);
}
#[test]
fn clear_pending_version_removes_it() {
let dir = tempfile::tempdir().unwrap();
save_pending_version(dir.path(), "scaleway-cli", "v2.62.0").unwrap();
clear_pending_version(dir.path(), "scaleway-cli").unwrap();
assert_eq!(load_pending_version(dir.path(), "scaleway-cli"), None);
}
#[test]
fn clear_pending_version_is_a_noop_when_nothing_pending() {
let dir = tempfile::tempdir().unwrap();
assert!(clear_pending_version(dir.path(), "scaleway-cli").is_ok());
}
#[test]
fn pending_and_last_version_are_tracked_independently() {
let dir = tempfile::tempdir().unwrap();
save_last_version(dir.path(), "scaleway-cli", "v2.61.0").unwrap();
save_pending_version(dir.path(), "scaleway-cli", "v2.62.0").unwrap();
assert_eq!(
load_last_version(dir.path(), "scaleway-cli"),
Some("v2.61.0".to_string())
);
assert_eq!(
load_pending_version(dir.path(), "scaleway-cli"),
Some("v2.62.0".to_string())
);
}
}