Files
pbakaus_impeccable/crates/context/src/component_review/tests.rs
T
Paul Bakaus ded752a5c7 Capture original review images and bind the measured inventory
Support static WebP and JPEG without conversion-cache evidence. Bind the measured spec centrally, require complete semantic component previews, and present the initial kit before automatic gate-driven repairs.

AI assistance: implemented and validated with OpenAI Codex.
2026-09-13 21:21:06 -07:00

492 lines
20 KiB
Rust

use super::{manifest, server, store};
use serde_json::{Value, json};
use std::{
fs,
path::PathBuf,
sync::atomic::{AtomicUsize, Ordering},
};
static NEXT: AtomicUsize = AtomicUsize::new(0);
struct Fixture {
root: PathBuf,
project: PathBuf,
store: PathBuf,
}
impl Fixture {
fn new() -> Self {
let root = std::env::temp_dir().join(format!(
"impeccable-component-review-{}-{}",
std::process::id(),
NEXT.fetch_add(1, Ordering::Relaxed)
));
let project = root.join("project");
let store = root.join("store");
fs::create_dir_all(&project).unwrap();
for (name, body) in [
("comp.png", b"comp".as_slice()),
("art.png", b"art"),
("control.html", b"<button>Go</button>"),
("shared.css", b"button{color:red}"),
] {
fs::write(project.join(name), body).unwrap();
}
Self {
root,
project,
store,
}
}
fn manifest(&self) -> Value {
json!({"schemaVersion":1,"id":"hero","title":"Hero review","comp":{"path":"comp.png","width":100,"height":100},"components":[{"id":"art","name":"Art","medium":"Raster","note":"Illustration","box":{"x":0,"y":0,"w":0.5,"h":1},"preview":{"kind":"image","path":"art.png"},"dependencies":[]},{"id":"control","name":"Control","medium":"HTML","note":"Semantic control","box":{"x":0.5,"y":0,"w":0.5,"h":1},"preview":{"kind":"page","path":"control.html"},"dependencies":["shared.css"]}]})
}
fn prepare(&self) -> PathBuf {
store::prepare(&self.store, &self.project, &self.manifest()).unwrap()
}
}
impl Drop for Fixture {
fn drop(&mut self) {
let _ = fs::remove_dir_all(&self.root);
}
}
fn approve(state: &Value) -> Value {
let mut decisions = serde_json::Map::new();
for c in state["packet"]["components"].as_array().unwrap() {
decisions.insert(
c["id"].as_str().unwrap().into(),
json!({"revision":c["revision"],"action":"approve","feedback":"","split":false}),
);
}
json!({"schemaVersion":1,"requestId":state["packet"]["id"],"packetRevision":state["packet"]["revision"],"decisions":decisions,"missing":[],"inventoryConfirmed":true})
}
#[test]
fn immutable_snapshots_and_idempotent_feedback_survive_reload() {
let f = Fixture::new();
let dir = f.prepare();
let state = store::read(&dir.join("current.json")).unwrap();
let body = approve(&state);
let receipt = store::submit(&dir, &body).unwrap();
assert_eq!(store::submit(&dir, &body).unwrap(), receipt);
assert_eq!(
store::read(&dir.join("current.json")).unwrap()["receipt"],
receipt
);
assert_eq!(receipt["reviewer"], "local-browser");
assert_eq!(receipt["captureVerified"], false);
let mut conflict = body;
conflict["decisions"]["art"]["feedback"] = json!("different");
assert!(
store::submit(&dir, &conflict)
.unwrap_err()
.contains("already")
);
}
#[test]
fn source_change_rejects_pending_approval_and_invalidates_only_affected_components() {
let f = Fixture::new();
let dir = f.prepare();
let first = store::read(&dir.join("current.json")).unwrap();
store::submit(&dir, &approve(&first)).unwrap();
fs::write(f.project.join("art.png"), b"new art").unwrap();
let dir = f.prepare();
let next = store::read(&dir.join("current.json")).unwrap();
assert_ne!(first["packet"]["revision"], next["packet"]["revision"]);
assert!(next["draft"]["decisions"]["art"].is_null());
assert_eq!(next["draft"]["decisions"]["control"]["action"], "approve");
assert_eq!(next["draft"]["inventoryConfirmed"], false);
assert!(
store::submit(&dir, &approve(&first))
.unwrap_err()
.contains("stale")
);
fs::write(f.project.join("shared.css"), b"changed again").unwrap();
assert!(
store::submit(&dir, &approve(&next))
.unwrap_err()
.contains("stale")
);
}
#[test]
fn missing_regions_persist_across_rounds_and_old_receipts_are_preserved() {
let f = Fixture::new();
let dir = f.prepare();
let first = store::read(&dir.join("current.json")).unwrap();
let mut body = approve(&first);
body["missing"] = json!([{"id":"missing-1","name":"Brushwork","feedback":"Restore it","box":{"x":0.2,"y":0.2,"w":0.1,"h":0.1}}]);
body["inventoryConfirmed"] = json!(false);
let receipt = store::submit(&dir, &body).unwrap();
fs::write(f.project.join("art.png"), b"repair").unwrap();
f.prepare();
let next = store::read(&dir.join("current.json")).unwrap();
assert_eq!(next["draft"]["missing"], body["missing"]);
let history = store::read(&dir.join(format!(
"revisions/{}.json",
first["packet"]["revision"].as_str().unwrap()
)))
.unwrap();
assert_eq!(history["receipt"], receipt);
}
#[test]
fn refusal_paths_cannot_be_turned_into_approval() {
let f = Fixture::new();
let dir = f.prepare();
let state = store::read(&dir.join("current.json")).unwrap();
let mut body = approve(&state);
body["inventoryConfirmed"] = json!(false);
assert!(store::submit(&dir, &body).is_err());
body["inventoryConfirmed"] = json!(true);
body["decisions"]["art"]["action"] = json!("skip");
assert!(store::submit(&dir, &body).is_err());
body["decisions"]["art"]["action"] = json!("approve");
body["decisions"]["art"]["revision"] = json!("invented");
assert!(store::submit(&dir, &body).is_err());
assert!(store::read(&dir.join("current.json")).unwrap()["receipt"].is_null());
}
#[test]
fn files_are_confined_and_store_is_outside_project() {
let f = Fixture::new();
let mut manifest = f.manifest();
manifest["components"][0]["preview"]["path"] = json!("../outside.png");
assert!(manifest::freeze(&f.project, &manifest).is_err());
assert!(
store::prepare(
&f.project.join("forged-approvals"),
&f.project,
&f.manifest()
)
.is_err()
);
#[cfg(unix)]
{
std::os::unix::fs::symlink(&f.root, f.project.join("outside")).unwrap();
manifest["components"][0]["preview"]["path"] = json!("outside/project/art.png");
let frozen = manifest::freeze(&f.project, &manifest);
assert!(frozen.is_ok());
std::os::unix::fs::symlink("/etc/hosts", f.project.join("leak")).unwrap();
manifest["components"][0]["preview"]["path"] = json!("leak");
assert!(manifest::freeze(&f.project, &manifest).is_err());
}
}
#[test]
fn malformed_maps_and_cross_origin_posts_are_rejected() {
let f = Fixture::new();
let mut manifest = f.manifest();
manifest["components"][0]["box"]["w"] = json!(0);
assert!(manifest::freeze(&f.project, &manifest).is_err());
assert!(server::authorized(
"POST",
Some("127.0.0.1:4321"),
Some("http://127.0.0.1:4321"),
Some("same-origin"),
4321
));
for origin in [None, Some("null"), Some("https://evil.example")] {
assert!(!server::authorized(
"POST",
Some("127.0.0.1:4321"),
origin,
Some("same-origin"),
4321
));
}
assert!(!server::authorized(
"GET",
Some("evil.example"),
None,
None,
4321
));
assert!(super::decode_path("a%20b.png").is_ok());
assert!(super::decode_path("%00").is_err());
}
#[test]
fn prepare_cli_reports_an_existing_receipt_instead_of_requesting_review_again() {
let f = Fixture::new();
fs::write(
f.project.join("review.json"),
serde_json::to_vec(&f.manifest()).unwrap(),
)
.unwrap();
let args = vec![
"prepare".into(),
"--manifest".into(),
"review.json".into(),
"--store".into(),
f.store.to_string_lossy().into_owned(),
];
let invoke = || {
let (mut io, captured) =
impeccable_common::Io::captured("", f.project.clone(), Default::default());
assert_eq!(super::run(&args, &mut io), 0);
let result = serde_json::from_slice::<Value>(&captured.stdout.borrow()).unwrap();
result
};
let initial = invoke();
assert_eq!(initial["status"], "awaiting-review");
let dir = f.store.join(initial["session"].as_str().unwrap());
let first = store::read(&dir.join("current.json")).unwrap();
store::submit(&dir, &approve(&first)).unwrap();
assert_eq!(invoke()["status"], "approved");
}
#[test]
fn repair_history_records_feedback_changes_and_removed_components() {
let f = Fixture::new();
let dir = f.prepare();
let first = store::read(&dir.join("current.json")).unwrap();
let mut body = approve(&first);
body["decisions"]["art"]["action"] = json!("revise");
body["decisions"]["art"]["feedback"] = json!("Preserve the motif");
store::submit(&dir, &body).unwrap();
fs::write(f.project.join("art.png"), b"repair").unwrap();
f.prepare();
let next = store::read(&dir.join("current.json")).unwrap();
assert_eq!(next["history"]["packet"]["round"], 1);
assert_eq!(
next["history"]["draft"]["decisions"]["art"]["feedback"],
"Preserve the motif"
);
assert_eq!(
next["history"]["changes"]["art"]["files"],
json!(["art.png"])
);
assert_eq!(next["history"]["changes"]["control"]["kind"], "unchanged");
let mut manifest = f.manifest();
manifest["components"].as_array_mut().unwrap().remove(1);
store::prepare(&f.store, &f.project, &manifest).unwrap();
let removed = store::read(&dir.join("current.json")).unwrap();
assert_eq!(removed["history"]["removed"][0]["id"], "control");
assert_eq!(removed["draft"]["inventoryConfirmed"], false);
}
#[test]
fn reverting_to_old_content_cannot_reuse_an_old_round_or_approval() {
let f = Fixture::new();
let dir = f.prepare();
let first = store::read(&dir.join("current.json")).unwrap();
store::submit(&dir, &approve(&first)).unwrap();
fs::write(f.project.join("art.png"), b"repair").unwrap();
f.prepare();
fs::write(f.project.join("art.png"), b"art").unwrap();
f.prepare();
let third = store::read(&dir.join("current.json")).unwrap();
assert_eq!(third["packet"]["round"], 3);
assert_ne!(first["packet"]["revision"], third["packet"]["revision"]);
assert!(store::submit(&dir, &approve(&first)).is_err());
assert!(third["draft"]["decisions"]["art"].is_null());
let archive = store::read(&dir.join(format!(
"revisions/{}.json",
first["packet"]["revision"].as_str().unwrap()
)))
.unwrap();
assert_eq!(archive["packet"]["round"], 1);
assert_eq!(archive["receipt"]["visualDecision"], "approved");
}
#[test]
fn preparing_again_before_a_reply_preserves_outstanding_feedback_and_carried_approvals() {
let f = Fixture::new();
let dir = f.prepare();
let first = store::read(&dir.join("current.json")).unwrap();
let mut body = approve(&first);
body["decisions"]["art"]["action"] = json!("revise");
body["decisions"]["art"]["feedback"] = json!("Keep the motif");
store::submit(&dir, &body).unwrap();
fs::write(f.project.join("art.png"), b"repair one").unwrap();
f.prepare();
fs::write(f.project.join("art.png"), b"repair two").unwrap();
f.prepare();
let third = store::read(&dir.join("current.json")).unwrap();
assert_eq!(
third["history"]["feedback"]["art"]["decision"]["feedback"],
"Keep the motif"
);
assert_eq!(third["history"]["feedback"]["art"]["round"], 1);
assert_eq!(third["history"]["changes"]["control"]["carried"], true);
store::submit(&dir, &approve(&third)).unwrap();
fs::write(f.project.join("art.png"), b"another version").unwrap();
f.prepare();
let fourth = store::read(&dir.join("current.json")).unwrap();
assert!(fourth["history"]["feedback"]["art"].is_null());
}
#[test]
fn failed_native_capture_does_not_replace_the_current_review() {
struct Refuse;
impl super::capture::ComponentCapturer for Refuse {
fn capture(
&mut self,
_: &mut Value,
_: &std::collections::BTreeMap<String, Vec<u8>>,
) -> Result<super::capture::CapturedPreviews, String> {
Err("missing stylesheet".into())
}
}
let f = Fixture::new();
let dir = f.prepare();
let before = fs::read(dir.join("current.json")).unwrap();
assert!(
store::prepare_captured(&f.store, &f.project, &f.manifest(), Some(&mut Refuse))
.unwrap_err()
.contains("missing stylesheet")
);
assert_eq!(fs::read(dir.join("current.json")).unwrap(), before);
}
#[test]
fn producer_capture_claims_are_never_authority() {
let f = Fixture::new();
let mut input = f.manifest();
input["captureVerified"] = json!(true);
input["capture"] = json!({"schema":"native-component-previews-v1"});
input["components"][0]["capture"] = json!({"verified":true});
input["components"][0]["preview"]["sourceKind"] = json!("page");
let dir = store::prepare(&f.store, &f.project, &input).unwrap();
let state = store::read(&dir.join("current.json")).unwrap();
assert!(state["packet"]["capture"].is_null());
assert!(state["packet"]["components"][0]["capture"].is_null());
assert!(state["packet"]["components"][0]["preview"]["sourceKind"].is_null());
assert_eq!(
store::submit(&dir, &approve(&state)).unwrap()["captureVerified"],
false
);
}
#[test]
fn native_capture_outputs_are_immutable_and_source_changes_invalidate_approval() {
struct Renderer;
impl super::capture::ComponentCapturer for Renderer {
fn capture(
&mut self,
packet: &mut Value,
_: &std::collections::BTreeMap<String, Vec<u8>>,
) -> Result<super::capture::CapturedPreviews, String> {
// A trusted in-process renderer double, never a producer JSON claim.
packet["components"][1]["preview"] = json!({"kind":"image","url":"/files/_review_captures/control.png","sourceKind":"page"});
Ok(super::capture::CapturedPreviews {
files: std::collections::BTreeMap::from([(
"_review_captures/control.png".into(),
b"native pixels".to_vec(),
)]),
evidence: json!({"schema":"native-component-previews-v1","components":[{"id":"art"},{"id":"control"}]}),
})
}
}
let f = Fixture::new();
let dir =
store::prepare_captured(&f.store, &f.project, &f.manifest(), Some(&mut Renderer)).unwrap();
let state = store::read(&dir.join("current.json")).unwrap();
store::sources_current(&state).unwrap();
assert!(
state["sources"]
.get("_review_captures/control.png")
.is_none()
);
let receipt = store::submit(&dir, &approve(&state)).unwrap();
assert_eq!(receipt["captureVerified"], true);
assert_eq!(receipt["reviewer"], "local-browser");
fs::write(f.project.join("art.png"), b"changed").unwrap();
assert!(store::sources_current(&state).is_err());
store::prepare_captured(&f.store, &f.project, &f.manifest(), Some(&mut Renderer)).unwrap();
let next = store::read(&dir.join("current.json")).unwrap();
assert!(next["draft"]["decisions"]["art"].is_null());
assert_eq!(next["draft"]["decisions"]["control"]["action"], "approve");
assert_ne!(state["packet"]["revision"], next["packet"]["revision"]);
assert_eq!(
store::submit(&dir, &approve(&next)).unwrap()["captureVerified"],
true
);
}
#[test]
fn changing_manifest_without_prepare_rejects_review_submission() {
let f = Fixture::new();
let input = f.manifest();
let file = f.project.join("review.json");
fs::write(&file, serde_json::to_vec(&input).unwrap()).unwrap();
let dir = store::prepare_file(&f.store, &f.project, "review.json", None).unwrap();
let state = store::read(&dir.join("current.json")).unwrap();
let mut changed = input;
changed["components"][0]["box"]["w"] = json!(0.4);
fs::write(&file, serde_json::to_vec(&changed).unwrap()).unwrap();
assert!(
store::submit(&dir, &approve(&state))
.unwrap_err()
.contains("stale")
);
}
#[test]
fn versioned_packet_keeps_submitted_round_when_current_advances() {
let f = Fixture::new();
let dir = f.prepare();
let state = store::read(&dir.join("current.json")).unwrap();
let revision = state["packet"]["revision"].as_str().unwrap();
let receipt = store::submit(&dir, &approve(&state)).unwrap();
assert_eq!(
server::packet_state(&dir, Some(revision)).unwrap()["receipt"],
receipt
);
fs::write(f.project.join("art.png"), b"new artwork").unwrap();
f.prepare();
let old = server::packet_state(&dir, Some(revision)).unwrap();
assert_eq!(old["packet"], state["packet"]);
assert_eq!(old["receipt"], receipt);
assert_eq!(old["historical"], true);
assert!(old["sourceStatus"].is_null());
assert_ne!(
server::packet_state(&dir, None).unwrap()["packet"]["revision"],
revision
);
assert!(server::packet_state(&dir, Some("../../current")).is_err());
assert!(store::submit(&dir, &approve(&state)).is_err());
}
#[test]
fn verify_requires_native_approval_and_current_manifest_and_dependencies() {
struct Renderer;
impl super::capture::ComponentCapturer for Renderer {
fn capture(&mut self, packet: &mut Value, _: &std::collections::BTreeMap<String, Vec<u8>>) -> Result<super::capture::CapturedPreviews, String> {
packet["components"][1]["preview"] = json!({"kind":"image","url":"/files/_review_captures/control.png","sourceKind":"page"});
Ok(super::capture::CapturedPreviews {
files: std::collections::BTreeMap::from([("_review_captures/control.png".into(), b"native pixels".to_vec())]),
evidence: json!({"schema":"native-component-previews-v1","components":[{"id":"art"},{"id":"control"}]}),
})
}
}
let f = Fixture::new();
fs::write(f.project.join("review.json"), f.manifest().to_string()).unwrap();
let dir = store::prepare_file(&f.store,&f.project,"review.json",Some(&mut Renderer)).unwrap();
assert!(super::verify::approved(&f.store,&f.project,"review.json").is_err());
let state = store::read(&dir.join("current.json")).unwrap();
let mut needs_work = approve(&state);
needs_work["decisions"]["art"]["action"] = json!("revise");
needs_work["decisions"]["art"]["feedback"] = json!("Wrong shape");
store::submit(&dir,&needs_work).unwrap();
assert!(super::verify::approved(&f.store,&f.project,"review.json").is_err());
fs::write(f.project.join("art.png"), b"repaired art").unwrap();
let dir = store::prepare_file(&f.store,&f.project,"review.json",Some(&mut Renderer)).unwrap();
let state = store::read(&dir.join("current.json")).unwrap();
store::submit(&dir,&approve(&state)).unwrap();
assert_eq!(super::verify::approved(&f.store,&f.project,"review.json").unwrap()["visualDecision"],"approved");
fs::write(f.project.join("other.json"), f.manifest().to_string()).unwrap();
assert!(super::verify::approved(&f.store,&f.project,"other.json").unwrap_err().contains("bind"));
fs::write(f.project.join("shared.css"), b"changed after approval").unwrap();
assert!(super::verify::approved(&f.store,&f.project,"review.json").unwrap_err().contains("changed"));
}
#[test]
fn measured_inventory_is_bound_without_repeated_author_dependencies() {
let f = Fixture::new();
fs::create_dir_all(f.project.join(".impeccable/build")).unwrap();
let path = f.project.join(".impeccable/build/spec.json");
fs::write(&path, br#"{"regions":[{"id":"art","kind":"plate"},{"id":"control","kind":"control"}]}"#).unwrap();
let mut input = f.manifest(); input["stage"] = json!("components");
let dir = store::prepare(&f.store, &f.project, &input).unwrap();
let state = store::read(&dir.join("current.json")).unwrap();
assert!(state["sources"][".impeccable/build/spec.json"].is_string());
let mut missing = input.clone(); missing["components"].as_array_mut().unwrap().pop();
assert!(store::prepare(&f.store, &f.project, &missing).unwrap_err().contains("omitted measured region"));
let mut flattened = input; flattened["components"][1]["preview"] = json!({"kind":"image","path":"art.png"});
assert!(store::prepare(&f.store, &f.project, &flattened).unwrap_err().contains("rendered code preview"));
fs::write(path, br#"{"regions":[]}"#).unwrap();
assert!(store::sources_current(&state).is_err());
}