use super::{ Fixture, fixture::{archive, hash, signed}, }; use dever::compiler::project::dependency::{ Lock, Store, library::{ Spec, lock::{Artifact, Library, Runtime}, npm::Environment, }, }; use serde::{Deserialize, Serialize}; use serde_json::{Value, json}; use std::collections::{BTreeMap, BTreeSet}; // Fixture 按既有 v6 协议编码,独立于实现中的私有 Receipt 类型。 #[derive(Deserialize, Serialize)] struct Inputs { format: String, toolchain: String, libs: Vec, workers: Vec, npm: Vec, } #[derive(Deserialize, Serialize)] struct Tools { format: String, ecosystem: String, target: String, runtime_sha256: String, python_runtime_sha256: Option, sha256: String, } #[derive(Serialize)] struct Receipt { ecosystem: String, roots: Vec, sources: Vec, graph: Option, runtime: Runtime, auxiliary_runtime: Option, native_entries: BTreeSet, native_rejections: BTreeMap, python_markers: Option, tools: Tools, frontend: String, python: Option, dynamic_requires: Vec, inputs: Inputs, config_settings: BTreeMap, output: Artifact, } fn prepare(fixture: &mut Fixture, addons: bool) -> Receipt { let node_pack = archive(&BTreeMap::from([( "fixture/node.txt".into(), b"author fixture runtime".to_vec(), )])); let python_pack = archive(&BTreeMap::from([( "fixture/python.txt".into(), b"author fixture auxiliary runtime".to_vec(), )])); let build_pack = archive(&BTreeMap::from([( "fixture/tools.txt".into(), b"already prepared output; no hooks run".to_vec(), )])); fixture.metadata.pack.sha256 = hash(&node_pack); let mut extra = BTreeMap::new(); if addons { extra.insert( "package/foreign.node".into(), b"prebuilt foreign format".to_vec(), ); } fixture.package( json!({"name":"built","version":"1.0.0","scripts":{"install":"must never run"}}), extra, ); fixture.resolve(&["npm:built@1.0.0"]).unwrap(); let graph = fixture.graph(); let mut output = BTreeMap::from([ ( "node_modules/built/package.json".into(), serde_json::to_vec( &json!({"name":"built","version":"1.0.0","scripts":{"install":"must never run"}}), ) .unwrap(), ), ( "node_modules/built/generated.js".into(), b"module.exports=42".to_vec(), ), ]); if addons { output.insert( "node_modules/built/foreign.node".into(), b"prebuilt foreign format".to_vec(), ); } let bytes = archive(&output); fixture.put(&bytes).unwrap(); let python = Runtime { name: "python".into(), version: "3.12.1".into(), sha256: hash(&python_pack), }; let tools = Tools { format: "dever-registry-build-v1".into(), ecosystem: "npm".into(), target: fixture.target.name().into(), runtime_sha256: fixture.metadata.pack.sha256.clone(), python_runtime_sha256: Some(python.sha256.clone()), sha256: hash(&build_pack), }; let target = fixture.target.name(); let files=BTreeMap::from([ (format!("runtime/npm/{target}/runtime.pack"),node_pack), (format!("runtime/npm/{target}/manifest.json"),serde_json::to_vec(&json!({"format":"dever-registry-runtime-v1","runtime":{"pack":fixture.metadata.pack,"target":target,"python_markers":null,"python_wheel_tags":[],"npm_libc":"glibc"}})).unwrap()), (format!("runtime/pip/{target}/runtime.pack"),python_pack), (format!("runtime/pip/{target}/manifest.json"),serde_json::to_vec(&json!({"format":"dever-registry-runtime-v1","runtime":{"pack":python,"target":target,"python_markers":{"implementation_name":"cpython","implementation_version":"3.12.1","os_name":"posix","platform_machine":"x86_64","platform_python_implementation":"CPython","platform_release":"","platform_system":"Linux","platform_version":"","python_full_version":"3.12.1","python_version":"3.12","sys_platform":"linux"},"python_wheel_tags":["py3-none-any"]}})).unwrap()), (format!("build/npm/{target}/build.pack"),build_pack), (format!("build/npm/{target}/manifest.json"),serde_json::to_vec(&tools).unwrap()), ]); fixture.signed = signed(&fixture.directory, &files); Receipt { ecosystem: "npm".into(), roots: graph.roots.clone(), sources: fixture .lock .libs .iter() .flat_map(|lib| lib.artifacts.clone()) .collect(), graph: Some(graph), runtime: fixture.metadata.pack.clone(), auxiliary_runtime: Some(python), native_entries: BTreeSet::new(), native_rejections: if addons { BTreeMap::from([("node_modules/built/foreign.node".into(), "format".into())]) } else { BTreeMap::new() }, python_markers: None, tools, frontend: "cfe67ebbc939762cd4ba7328943bf97a72d06a4b79071f056ebd8e5ab566a721".into(), python: None, dynamic_requires: vec![], inputs: serde_json::from_value(serde_json::to_value(&fixture.lock).unwrap()).unwrap(), config_settings: BTreeMap::new(), output: Artifact { target: target.into(), path: format!("build/npm/{}/installation.tgz", hash(&bytes)), bytes: bytes.len() as u64, sha256: hash(&bytes), source: None, }, } } fn attach(fixture: &mut Fixture, receipt: &Receipt) { fixture.lock.npm[0]["build"] = json!(hash(&serde_json::to_vec(receipt).unwrap())); fixture.lock.builds = vec![serde_json::to_value(receipt).unwrap()]; } #[test] fn npm_lifecycle_receipt_consumes_signed_inputs_without_network_or_hooks() { let mut fixture = Fixture::new(); let receipt = prepare(&mut fixture, false); attach(&mut fixture, &receipt); fixture.documents.clear(); let calls = fixture.calls.borrow().len(); let files = fixture.install().unwrap(); assert_eq!( files["node_modules/built/generated.js"], b"module.exports=42" ); assert_eq!(fixture.calls.borrow().len(), calls); let text = fixture.lock.encode().unwrap(); let reparsed = Lock::decode(&text).unwrap(); fixture.lock = reparsed; fixture.install().unwrap(); fixture.lock.builds[0]["frontend"] = json!("0".repeat(64)); assert!(fixture.install().unwrap_err().contains("receipt")); } #[test] fn npm_native_receipt_accounts_for_all_addons_and_never_admits_failed_build() { let mut fixture = Fixture::new(); let mut receipt = prepare(&mut fixture, true); attach(&mut fixture, &receipt); assert!( !fixture .install() .unwrap() .keys() .any(|path| path.ends_with(".node")) ); receipt.native_rejections.clear(); attach(&mut fixture, &receipt); assert!(fixture.install().unwrap_err().contains("admission")); receipt .native_entries .insert("node_modules/built/foreign.node".into()); attach(&mut fixture, &receipt); assert!(fixture.install().unwrap_err().contains("ELF")); receipt.native_entries.clear(); receipt .native_rejections .insert("node_modules/built/new.node".into(), "unloadable".into()); attach(&mut fixture, &receipt); assert!(fixture.install().unwrap_err().contains("admission")); } #[test] fn npm_replay_rejects_malformed_binding_before_fetching_or_executing() { let mut fixture = Fixture::new(); let mut receipt = prepare(&mut fixture, false); receipt.tools.runtime_sha256 = "0".repeat(64); attach(&mut fixture, &receipt); fixture.cache.borrow_mut().clear(); let calls = fixture.calls.borrow().len(); let error = dever::compiler::project::dependency::library::npm::restore( &fixture.lock, &fixture.graph(), fixture.target, &fixture.signed, &fixture, &fixture, ) .unwrap_err(); assert!(error.contains("binding differs"), "{error}"); assert_eq!(fixture.calls.borrow().len(), calls); assert!(fixture.cache.borrow().is_empty()); } #[test] fn npm_cached_receipt_rejects_tampering_but_needs_no_build_tools() { let mut fixture = Fixture::new(); let receipt = prepare(&mut fixture, false); attach(&mut fixture, &receipt); let original = fixture.cache.borrow()[&receipt.output.sha256].clone(); fixture .cache .borrow_mut() .insert(receipt.output.sha256.clone(), b"tampered".to_vec()); assert!(fixture.install().unwrap_err().contains("digest")); fixture .cache .borrow_mut() .insert(receipt.output.sha256.clone(), original); std::fs::remove_file(fixture.directory.0.join(format!( "signed/versions/0.0.1/build/npm/{}/build.pack", fixture.target.name() ))) .unwrap(); fixture.install().unwrap(); }