use super::*; use dever_cli::libs::sumdb::{self, ChecksumDatabase, Evidence, Verifier}; use ring::signature::{Ed25519KeyPair, KeyPair}; fn key() -> Ed25519KeyPair { Ed25519KeyPair::from_seed_unchecked(&[47; 32]).unwrap() } pub(super) fn verifier() -> Verifier { let mut encoded = vec![1]; encoded.extend(key().public_key().as_ref()); let mut hash = Sha256::new(); hash.update(b"dever-sumdb-test\n"); hash.update(&encoded); let id = hash.finalize(); Verifier::new(&format!( "dever-sumdb-test+{}+{}", id[..4] .iter() .map(|byte| format!("{byte:02x}")) .collect::(), base64::engine::general_purpose::STANDARD.encode(encoded) )) .unwrap() } fn signed(size: usize, root: [u8; 32]) -> String { let text = format!( "go.sum database tree\n{size}\n{}\n", base64::engine::general_purpose::STANDARD.encode(root) ); let mut hash = Sha256::new(); hash.update(b"dever-sumdb-test\n"); hash.update([1]); hash.update(key().public_key().as_ref()); let mut signature = hash.finalize()[..4].to_vec(); signature.extend(key().sign(text.as_bytes()).as_ref()); format!( "{text}\n— dever-sumdb-test {}\n", base64::engine::general_purpose::STANDARD.encode(signature) ) } fn leaf(record: &[u8]) -> [u8; 32] { let mut hash = Sha256::new(); hash.update([0]); hash.update(record); hash.finalize().into() } fn root(hashes: &[[u8; 32]]) -> [u8; 32] { if hashes.is_empty() { return Sha256::digest([]).into(); } if hashes.len() == 1 { return hashes[0]; } let split = hashes.len().next_power_of_two() / 2; let mut hash = Sha256::new(); hash.update([1]); hash.update(root(&hashes[..split])); hash.update(root(&hashes[split..])); hash.finalize().into() } fn unescape(path: &str) -> String { let mut chars = path.chars(); let mut output = String::new(); while let Some(ch) = chars.next() { output.push(if ch == '!' { chars.next().unwrap().to_ascii_uppercase() } else { ch }); } output } pub(super) fn seal(registry: &mut LocalRegistry) { registry .0 .retain(|(_, path), _| !path.starts_with("https://sum.golang.org/")); let records = registry .0 .iter() .filter_map(|((ecosystem, path), bytes)| { if *ecosystem != Ecosystem::Go || !path.ends_with(".mod") { return None; } let (module, version) = path.strip_prefix('/')?.split_once("/@v/v")?; let version = version.strip_suffix(".mod")?; let module = unescape(module); let archive = registry.0.get(&( Ecosystem::Go, format!("{}zip", path.strip_suffix("mod").unwrap()), ))?; let record = format!( "{module} v{version} {}\n{module} v{version}/go.mod {}\n", sumdb::zip_hash(&module, version, archive).unwrap(), sumdb::mod_hash(bytes) ); Some(( format!( "/lookup/{}@v{version}", path.strip_prefix('/') .unwrap() .split("/@v/") .next() .unwrap() ), record, )) }) .collect::>(); let hashes = records .iter() .map(|(_, record)| leaf(record.as_bytes())) .collect::>(); let checkpoint = signed(records.len(), root(&hashes)); registry.0.insert( (Ecosystem::Go, "https://sum.golang.org/latest".into()), checkpoint.as_bytes().to_vec(), ); for (id, (path, record)) in records.iter().enumerate() { registry.0.insert( (Ecosystem::Go, format!("https://sum.golang.org{path}")), format!("{id}\n{record}\n{checkpoint}").into_bytes(), ); } // Fixtures deliberately remain below one tile. Larger trees use official // captured vectors, not a second production tile-address implementation. assert!(hashes.len() < 256); if !hashes.is_empty() { registry.0.insert( ( Ecosystem::Go, format!("https://sum.golang.org/tile/8/0/000.p/{}", hashes.len()), ), hashes.iter().flatten().copied().collect(), ); } } pub(super) struct Transport<'a> { pub registry: &'a dyn RegistryTransport, pub checksums: &'a LocalRegistry, } impl RegistryTransport for Transport<'_> { fn get(&self, ecosystem: Ecosystem, path: &str, limit: usize) -> Result, String> { if path.starts_with("https://sum.golang.org/") { self.checksums.get(ecosystem, path, limit) } else { self.registry.get(ecosystem, path, limit) } } } fn resolved( registry: &LocalRegistry, previous: Option, roots: &[&str], ) -> Result { let cache = FixtureArtifactStore::default(); let mut resolver = local_resolver(registry, &cache); resolver.go_sumdb = ChecksumDatabase::new(verifier(), previous)?; dever_cli::libs::LibResolver::resolve( &resolver, &roots .iter() .map(|root| root.parse().unwrap()) .collect::>(), ) } #[test] fn authenticated_modules_replay_offline_and_reject_content_or_proof_drift() { let registry = local_registry(); let lock = resolved(®istry, None, &["go:example.com/a@1.0.0"]).unwrap(); let evidence = &lock.go_sumdb[0]; let checked = evidence.verify(&verifier()).unwrap(); let archive = ®istry.0[&(Ecosystem::Go, "/example.com/a/@v/v1.0.0.zip".into())]; checked .verify_zip("example.com/a", "1.0.0", archive) .unwrap(); assert!( evidence .verify(&Verifier::official()) .unwrap_err() .contains("trusted signature") ); let mut changed = evidence.clone(); changed.records[0].go_mod.push_str("// changed\n"); assert!(changed.verify(&verifier()).unwrap_err().contains("go.mod")); let mut changed = evidence.clone(); changed.records[0].module = "wrong.example/a".into(); assert!( changed .verify(&verifier()) .unwrap_err() .contains("different module/version") ); let mut changed = evidence.clone(); changed.records[0].inclusion[0] = base64::engine::general_purpose::STANDARD.encode([0; 32]); assert!( changed .verify(&verifier()) .unwrap_err() .contains("inclusion proof") ); let mut changed = evidence.clone(); changed.checkpoint = changed .checkpoint .replace("go.sum database tree", "go.sum database free"); assert!( changed .verify(&verifier()) .unwrap_err() .contains("signature verification") ); assert!( checked .verify_zip( "example.com/a", "1.0.0", &zip_file("example.com/a@v1.0.0/changed.go") ) .unwrap_err() .contains("sumdb h1") ); } #[test] fn checkpoint_updates_require_consistency_and_never_move_backwards() { let mut first = local_registry(); first .0 .remove(&(Ecosystem::Go, "/example.com/b/@v/v1.2.0.mod".into())); first.0.insert( (Ecosystem::Go, "/example.com/a/@v/v1.0.0.mod".into()), b"module example.com/a\n".to_vec(), ); seal(&mut first); let old = resolved(&first, None, &["go:example.com/a@1.0.0"]).unwrap(); let checkpoint = old.go_sumdb[0].verify(&verifier()).unwrap().checkpoint; let mut next = first; next.0.insert( (Ecosystem::Go, "/example.com/z/@v/v1.0.0.mod".into()), b"module example.com/z\n".to_vec(), ); next.0.insert( (Ecosystem::Go, "/example.com/z/@v/v1.0.0.zip".into()), zip_file("example.com/z@v1.0.0/z.go"), ); seal(&mut next); let new = resolved(&next, Some(checkpoint.clone()), &["go:example.com/z@1.0.0"]).unwrap(); let current = new.go_sumdb[0].verify(&verifier()).unwrap().checkpoint; assert!(resolved(&next, Some(current.clone()), &["go:example.com/z@1.0.0"]).is_ok()); next.0.insert( (Ecosystem::Go, "https://sum.golang.org/latest".into()), checkpoint.into_bytes(), ); assert!( resolved(&next, Some(current), &["go:example.com/a@1.0.0"]) .unwrap_err() .contains("rollback") ); let mut fork = new.go_sumdb[0].clone(); fork.consistency[0] = base64::engine::general_purpose::STANDARD.encode([0; 32]); assert!( fork.verify(&verifier()) .unwrap_err() .contains("conflicting history") ); } #[test] fn sumdb_verifier_key_and_upstream_signed_note_vectors() { let note = "go.sum database tree\n63512409\nF7eHRPqN/cqPtqti14NKdGGaI1Giw8coWP3KxihFZbg=\n\n— sum.golang.org Az3gri1OBHKT87Xj8UXU8zjoHGy/X+7Nl94f8TZkELVaasolmGApUsWqwVG9vE6hgqYb/ph/w2vdkLIVRH7bMB/FPQ4=\n"; let evidence = Evidence { previous: None, checkpoint: note.into(), consistency: vec![], records: vec![], }; evidence.verify(&Verifier::official()).unwrap(); assert!(evidence.verify(&verifier()).is_err()); assert!(Verifier::new(&sumdb::OFFICIAL_VERIFIER.replace("033de0ae", "033de0af")).is_err()); } #[test] fn official_large_tree_inclusion_and_consistency_vectors_verify_offline() { // Captured from sum.golang.org/lookup/github.com/google/uuid@v1.6.0 // and its immutable height-8 tiles on 2026-10-02. Hashes were assembled // independently of this Rust implementation. Both notes retain Google's // original Ed25519 signatures; no fixture signer can mint these vectors. let evidence: Evidence = serde_json::from_str(include_str!("sumdb_uuid.json")).unwrap(); let trusted = Verifier::official(); let verified = evidence.verify(&trusted).unwrap(); assert!(verified.contains("github.com/google/uuid", "1.6.0")); assert_eq!( sumdb::mod_hash(b"module github.com/google/uuid\n"), "h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=" ); for index in 0..evidence.records[0].inclusion.len() { let mut changed = evidence.clone(); changed.records[0].inclusion[index] = base64::engine::general_purpose::STANDARD.encode([0; 32]); assert!( changed.verify(&trusted).is_err(), "accepted corrupt inclusion hash {index}" ); } for index in 0..evidence.consistency.len() { let mut changed = evidence.clone(); changed.consistency[index] = base64::engine::general_purpose::STANDARD.encode([0; 32]); assert!( changed.verify(&trusted).is_err(), "accepted corrupt consistency hash {index}" ); } } #[test] fn go_h1_ignores_zip_order_and_compression_and_preserves_module_case() { let archive = |reverse, compression| { let mut writer = zip::ZipWriter::new(Cursor::new(Vec::new())); let mut files = vec![ ("a.go", b"package a\n".as_slice()), ("LICENSE", b"fixture license\n".as_slice()), ]; if reverse { files.reverse(); } for (name, content) in files { writer .start_file( format!("example.com/Mixed@v1.0.0/{name}"), zip::write::SimpleFileOptions::default().compression_method(compression), ) .unwrap(); writer.write_all(content).unwrap(); } writer.finish().unwrap().into_inner() }; let first = archive(false, zip::CompressionMethod::Stored); let second = archive(true, zip::CompressionMethod::Deflated); assert_ne!(digest(&first), digest(&second)); assert_eq!( sumdb::zip_hash("example.com/Mixed", "1.0.0", &first).unwrap(), sumdb::zip_hash("example.com/Mixed", "1.0.0", &second).unwrap() ); let mut registry = LocalRegistry(BTreeMap::from([ ( (Ecosystem::Go, "/example.com/!mixed/@v/v1.0.0.mod".into()), b"module example.com/Mixed\n".to_vec(), ), ( (Ecosystem::Go, "/example.com/!mixed/@v/v1.0.0.zip".into()), first, ), ])); seal(&mut registry); let lock = resolved(®istry, None, &["go:example.com/Mixed@1.0.0"]).unwrap(); assert_eq!(lock.libs[0].spec.name, "example.com/Mixed"); assert!( lock.go_sumdb[0] .verify(&verifier()) .unwrap() .contains("example.com/Mixed", "1.0.0") ); } #[test] fn every_go_mod_consulted_by_mvs_is_authenticated_even_when_not_selected() { let mut registry = LocalRegistry(BTreeMap::new()); for (name, version, source) in [ ( "a", "1.0.0", "module example.com/a\nrequire (\nexample.com/b v1.0.0\nexample.com/c v1.0.0\n)\n", ), ("b", "1.0.0", "module example.com/b\n"), ("b", "2.0.0", "module example.com/b\n"), ( "c", "1.0.0", "module example.com/c\nrequire example.com/b v2.0.0\n", ), ] { registry.0.insert( ( Ecosystem::Go, format!("/example.com/{name}/@v/v{version}.mod"), ), source.as_bytes().to_vec(), ); registry.0.insert( ( Ecosystem::Go, format!("/example.com/{name}/@v/v{version}.zip"), ), zip_file(&format!("example.com/{name}@v{version}/{name}.go")), ); } seal(&mut registry); let lock = resolved(®istry, None, &["go:example.com/a@1.0.0"]).unwrap(); assert!( !lock .libs .iter() .any(|lib| lib.spec.key() == "go:example.com/b@1.0.0") ); assert!( lock.go_sumdb[0] .records .iter() .any(|record| record.module == "example.com/b" && record.version == "1.0.0") ); registry .0 .get_mut(&(Ecosystem::Go, "/example.com/b/@v/v1.0.0.mod".into())) .unwrap() .extend(b"// modified discarded metadata\n"); let cache = FixtureArtifactStore::default(); let resolver = local_resolver(®istry, &cache); assert!( dever_cli::libs::LibResolver::resolve( &resolver, &["go:example.com/a@1.0.0".parse().unwrap()] ) .unwrap_err() .contains("go.mod does not match") ); for ((_, path), bytes) in ®istry.0 { if path.ends_with(".zip") { assert!(!cache.has(&digest(bytes), "linux-x86_64").unwrap()); } } } #[test] fn go_hashzip_includes_empty_directory_entries() { let archive = |directory| { let mut writer = zip::ZipWriter::new(Cursor::new(Vec::new())); writer .start_file( "example.com/a@v1.0.0/a.go", zip::write::SimpleFileOptions::default(), ) .unwrap(); writer.write_all(b"package a\n").unwrap(); if directory { writer .add_directory( "example.com/a@v1.0.0/empty/", zip::write::SimpleFileOptions::default(), ) .unwrap(); } writer.finish().unwrap().into_inner() }; // Independent Hash1 vectors: sorted "sha256(content) filename\n" records, // including the directory's trailing slash and SHA-256 of empty content. assert_eq!( sumdb::zip_hash("example.com/a", "1.0.0", &archive(false)).unwrap(), "h1:VEUcYKYLbKBYq0eA/VEJ3mMQBzi4jjWpusjuduGJcBQ=" ); assert_eq!( sumdb::zip_hash("example.com/a", "1.0.0", &archive(true)).unwrap(), "h1:y9ZWKWCmVsKnLxdFkIVoxr4MyXyHgBUvp1hgBzZ+IfE=" ); } #[test] fn project_updates_and_removing_last_lib_preserve_official_checkpoint_anchor() { let root = TemporaryDirectory::new(); let evidence: Evidence = serde_json::from_str(include_str!("sumdb_uuid.json")).unwrap(); let mut lock = LockFile::new(Vec::new()).unwrap(); lock.go_sumdb.push(evidence.clone()); lock.write_atomic(root.path()).unwrap(); let spec = vec!["pip:fixture@1.0.0".into()]; for command in ["add", "update", "remove"] { dever_cli::libs::execute(command, root.path(), &spec).unwrap(); let lock = LockFile::decode(&fs::read(root.path().join("dever.lock")).unwrap()).unwrap(); assert_eq!(lock.go_sumdb, vec![evidence.clone()]); let current = sumdb::verify_chain(&lock.go_sumdb, &Verifier::official()).unwrap(); let previous = evidence.previous.clone().unwrap(); // A subsequent online resolution cannot accept the older official tree. let mut registry = local_registry(); registry.0.insert( (Ecosystem::Go, "https://sum.golang.org/latest".into()), previous.into_bytes(), ); let cache = FixtureArtifactStore::default(); let mut resolver = local_resolver(®istry, &cache); resolver.go_sumdb = ChecksumDatabase::official(Some(current.checkpoint)).unwrap(); assert!( dever_cli::libs::LibResolver::resolve( &resolver, &["go:example.com/a@1.0.0".parse().unwrap()] ) .unwrap_err() .contains("rollback") ); } } #[test] fn lock_publication_rejects_offline_byte_and_element_overflow_before_replacement() { let root = TemporaryDirectory::new(); let original = LockFile::new(Vec::new()).unwrap(); original.write_atomic(root.path()).unwrap(); let path = root.path().join("dever.lock"); let before = fs::read(&path).unwrap(); let receipt = Evidence { previous: None, checkpoint: "x".into(), consistency: Vec::new(), records: Vec::new(), }; for overflow in [true, false] { let mut lock = original.clone(); let mut receipt = receipt.clone(); if overflow { receipt.checkpoint = "x".repeat(dever_runtime::wire::MAX_BYTES); } else { receipt.consistency = vec!["x".into(); dever_runtime::wire::MAX_ELEMENTS]; } lock.go_sumdb.push(receipt); assert!(lock.encode().unwrap_err().contains("invalid dever.lock")); assert!(lock.write_atomic(root.path()).is_err()); assert_eq!(fs::read(&path).unwrap(), before); assert_eq!(fs::read_dir(root.path()).unwrap().count(), 1); } } #[test] fn truncated_surplus_and_out_of_bounds_sumdb_proofs_are_rejected() { let original: Evidence = serde_json::from_str(include_str!("sumdb_uuid.json")).unwrap(); let trusted = Verifier::official(); let mut changed = original.clone(); changed.records[0].inclusion.pop(); assert!(changed.verify(&trusted).is_err()); let mut changed = original.clone(); changed.records[0] .inclusion .push(base64::engine::general_purpose::STANDARD.encode([0; 32])); assert!(changed.verify(&trusted).is_err()); for id in ["66321505", "09223372036854775807", "9223372036854775808"] { let mut changed = original.clone(); let (_, suffix) = changed.records[0].lookup.split_once('\n').unwrap(); changed.records[0].lookup = format!("{id}\n{suffix}"); assert!(changed.verify(&trusted).is_err()); } let mut changed = original.clone(); changed.checkpoint.pop(); assert!(changed.verify(&trusted).is_err()); let mut disconnected = original.clone(); disconnected.previous = None; disconnected.consistency.clear(); assert!( sumdb::verify_chain(&[original, disconnected], &trusted) .unwrap_err() .contains("not connected") ); } #[cfg(unix)] #[test] fn lib_and_package_mutations_share_a_stable_nonblocking_project_lock() { use std::os::unix::fs::OpenOptionsExt; let root = TemporaryDirectory::new(); let requested = vec!["pip:fixture@1.0.0".into()]; dever_cli::libs::execute("add", root.path(), &requested).unwrap(); let lock_path = root.path().join("dever.lock"); let config_path = root.path().join("config/setting.json"); let before_lock = fs::read(&lock_path).unwrap(); let before_config = fs::read(&config_path).unwrap(); let guard = fs::OpenOptions::new() .read(true) .write(true) .mode(0o600) .open(root.path().join(".dever-project.lock")) .unwrap(); fs2::FileExt::try_lock_exclusive(&guard).unwrap(); for command in ["add", "update", "remove"] { assert!( dever_cli::libs::execute(command, root.path(), &requested) .unwrap_err() .contains("mutation is busy") ); assert!( dever_cli::packages::execute(command, root.path(), &[]) .unwrap_err() .contains("mutation is busy") ); struct UnusedTransport; impl dever_cli::packages::PackageTransport for UnusedTransport { fn get(&self, _: &str, _: usize) -> Result, String> { panic!("busy mutation must not contact a registry") } } assert!( dever_cli::packages::execute_with( command, root.path(), &[], &UnusedTransport, &FixtureArtifactStore::default() ) .unwrap_err() .contains("mutation is busy") ); } assert_eq!(fs::read(&lock_path).unwrap(), before_lock); assert_eq!(fs::read(&config_path).unwrap(), before_config); drop(guard); dever_cli::libs::execute("update", root.path(), &requested).unwrap(); assert_eq!(fs::read(&lock_path).unwrap(), before_lock); } #[cfg(unix)] #[test] fn project_mutation_rejects_linked_or_unprotected_lock_paths() { use std::os::unix::fs::{PermissionsExt, symlink}; let root = TemporaryDirectory::new(); let outside = TemporaryDirectory::new(); let target = outside.path().join("untouched"); fs::write(&target, "keep").unwrap(); let path = root.path().join(".dever-project.lock"); symlink(&target, &path).unwrap(); let requested = vec!["pip:fixture@1.0.0".into()]; assert!( dever_cli::libs::execute("add", root.path(), &requested) .unwrap_err() .contains("without symlinks") ); assert_eq!(fs::read(&target).unwrap(), b"keep"); fs::remove_file(&path).unwrap(); fs::hard_link(&target, &path).unwrap(); assert!( dever_cli::libs::execute("add", root.path(), &requested) .unwrap_err() .contains("private and unlinked") ); fs::remove_file(&path).unwrap(); fs::write(&path, "").unwrap(); fs::set_permissions(&path, fs::Permissions::from_mode(0o666)).unwrap(); assert!( dever_cli::libs::execute("add", root.path(), &requested) .unwrap_err() .contains("private and unlinked") ); fs::set_permissions(root.path(), fs::Permissions::from_mode(0o777)).unwrap(); assert!( dever_cli::libs::execute("add", root.path(), &requested) .unwrap_err() .contains("not group/other writable") ); } #[test] fn independent_go_workers_merge_connected_offline_evidence() { use dever_cli::libs::{LockedWorker, resolve_workers}; let registry = local_registry(); let cache = FixtureArtifactStore::default(); let resolver = local_resolver(®istry, &cache); let worker = |adapter: &str, spec: &str| LockedWorker { port: "sample.worker".into(), adapter: adapter.into(), ecosystem: "go".into(), runtime: Some(registry_runtime(Ecosystem::Go).pack), entry: format!("worker/{adapter}.go"), schema: "checked-port".into(), capabilities: vec![], operations: vec!["read".into()], libs: vec![spec.parse().unwrap()], }; let lock = resolve_workers( &resolver, &[], vec![ worker("first", "go:example.com/a@1.0.0"), worker("second", "go:example.com/b@1.2.0"), ], ) .unwrap(); assert_eq!(lock.go_sumdb.len(), 2); assert_eq!( lock.go_sumdb[1].previous.as_ref(), Some(&lock.go_sumdb[0].verify(&verifier()).unwrap().checkpoint) ); let decoded = LockFile::decode(&lock.encode().unwrap()).unwrap(); let verified = sumdb::verify_chain(&decoded.go_sumdb, &verifier()).unwrap(); assert!(verified.contains("example.com/a", "1.0.0")); assert!(verified.contains("example.com/b", "1.2.0")); verify_locked_artifacts(&decoded, &cache, "linux-x86_64").unwrap(); } #[test] #[ignore = "author operation: explicitly fetches official Go proxy/sumdb; requires target/go-managed/config/setting.json and runtime.pack"] fn prepare_official_go_dependency_fixture() { #[derive(serde::Deserialize)] #[serde(deny_unknown_fields)] struct Settings { runtime: RegistryRuntime, } let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../target/go-managed"); let settings: Settings = serde_json::from_slice( &fs::read(root.join("config/setting.json")) .expect("prepare the explicit fixture settings first"), ) .unwrap(); let pack = fs::read(root.join("runtime.pack")).expect("prepare the explicit Go runtime pack first"); assert_eq!( settings.runtime.pack.sha256, digest(&pack), "configured runtime does not match runtime.pack" ); assert_eq!( settings.runtime.target, dever_cli::toolchain::platform_identity() ); let output = root.join("dependency"); let lock_path = output.join("dever.lock"); let previous = if lock_path.exists() { let lock = LockFile::decode(&fs::read(&lock_path).unwrap()).unwrap(); dever_cli::libs::doctor(&lock).unwrap(); let verified = sumdb::verify_chain(&lock.go_sumdb, &Verifier::official()).unwrap(); (!verified.checkpoint.is_empty()).then_some(verified.checkpoint) } else { None }; let target = settings.runtime.target.clone(); let store = FixtureArtifactStore::default(); let transport = dever_cli::libs::HttpRegistry::official(); let resolver = RegistryResolver { build: None, transport: &transport, store: &store, runtimes: BTreeMap::from([(Ecosystem::Go, settings.runtime)]), go_sumdb: ChecksumDatabase::official(previous).unwrap(), }; let lock = dever_cli::libs::LibResolver::resolve( &resolver, &["go:github.com/google/uuid@1.6.0".parse().unwrap()], ) .unwrap(); // The same official verifier and offline artifact checks protect normal builds. dever_cli::libs::doctor(&lock).unwrap(); dever_cli::libs::verify_locked_artifacts(&lock, &store, &target).unwrap(); publish_fixture_artifacts(&output, &lock, &store); }