use super::*; use oa_core::source_project::*; use std::collections::BTreeMap; fn authored() -> SourceBundle { let bundle = SourceBundle { entry: "harbor.arcadia".into(), files: BTreeMap::from([ ("harbor.arcadia".into(), "// Harbor engineering rationale 航海\r\narcadia 3; project harbor \"Harbor rescue\" {\r\n include \"team/first.arcadia\";\r\n include \"team/second.arcadia\";\r\n include \"contracts.arcadia\";\r\n include \"work.arcadia\";\r\n}\r\n".into()), ("team/first.arcadia".into(), "// First payload rationale\r\nsa_exchange_item first \"First payload\" { description \"Reviewed first\"; }\r\n".into()), ("team/second.arcadia".into(), "// Second payload rationale\r\nsa_exchange_item second \"Second payload\" {}\r\n".into()), ("contracts.arcadia".into(), "// Contract ownership stays here\r\nsa_interface contract \"Contract\" { items [\r\n @first // first reference rationale\r\n ,\r\n @second // second reference rationale\r\n ]; }\r\n".into()), ("work.arcadia".into(), "// Operational rationale\r\noa_activity assess \"Assess assistance\" {}\r\n".into()), ]), }; compile_bundle(&bundle, ProjectLimits::default()) .unwrap() .bundle } fn id_for(model: &Project, alias: &str) -> Id { model .identities() .into_iter() .find(|(_, name)| *name == alias) .unwrap() .0 } fn assert_pair(stored: &StoredProject) -> CompiledSourceProject { let compiled = compile_bundle(&stored.source_project, ProjectLimits::default()).unwrap(); assert_eq!(compiled.decoded.project, stored.model); assert_eq!( compiled.bundle, stored.source_project, "Stored IDs already stabilized" ); compiled } async fn import(server: &TestServer, bundle: &SourceBundle) -> StoredProject { let response = server .client .post(server.url("/projects/import-source-project")) .json(bundle) .send() .await .unwrap(); let status = response.status(); let body: Value = response.json().await.unwrap(); assert_eq!(status, StatusCode::CREATED, "{body}"); server .get(body["resource_id"].as_str().unwrap().parse().unwrap()) .await } async fn put(server: &TestServer, current: &StoredProject, bundle: &SourceBundle) -> Value { let response = server .client .put(server.url(&format!("/projects/{}/source-project", current.resource_id))) .header("if-match", current.etag()) .json(bundle) .send() .await .unwrap(); let status = response.status(); let body: Value = response.json().await.unwrap(); assert_eq!(status, StatusCode::OK, "{body}"); body } async fn transact( server: &TestServer, current: &StoredProject, commands: &[Command], dry_run: bool, ) -> Value { let response = server .client .post(server.url(&format!("/projects/{}/transactions", current.resource_id))) .header("if-match", current.etag()) .json(&json!({"commands":commands,"dry_run":dry_run})) .send() .await .unwrap(); let status = response.status(); let body: Value = response.json().await.unwrap(); assert_eq!(status, StatusCode::OK, "{body}"); body } #[tokio::test] async fn source_projects_preserve_exact_files_comments_and_comment_only_revisions() { let directory = tempfile::tempdir().unwrap(); let server = TestServer::start(directory.path(), None).await; let source = authored(); let original = import(&server, &source).await; let id = original.resource_id; assert_eq!(original.source_project, source); assert_pair(&original); let url = server.url(&format!("/projects/{id}/source-project")); let exported = server.client.get(&url).send().await.unwrap(); assert_eq!( exported.headers()["etag"].to_str().unwrap(), original.etag() ); assert_eq!(exported.json::().await.unwrap(), source); assert_eq!( server .client .get(&url) .header("if-none-match", original.etag()) .send() .await .unwrap() .status(), StatusCode::NOT_MODIFIED ); let mut edited = source.clone(); edited .files .get_mut("team/first.arcadia") .unwrap() .push_str("// Reviewed through automation\r\n"); let response = put(&server, &original, &edited).await; assert_eq!(response["changed"], true); assert_eq!( response["source_project"], serde_json::to_value(&edited).unwrap() ); let current = server.get(id).await; assert_eq!(current.revision, 2); assert_eq!(current.model, original.model); assert_eq!(current.source_project, edited); let stale = server .client .post(server.url(&format!("/projects/{id}/transactions"))) .header("if-match", original.etag()) .json(&json!({"commands":[Command::Rename(current.model.id,"Stale change".into())]})) .send() .await .unwrap(); assert_eq!(stale.status(), StatusCode::PRECONDITION_FAILED); let filename = directory.path().join(format!("{id}.json")); let saved_bytes = std::fs::read(&filename).unwrap(); let no_op = put(&server, ¤t, &edited).await; assert_eq!(no_op["changed"], false); assert_eq!(no_op["revision"], 2); assert_eq!(std::fs::read(&filename).unwrap(), saved_bytes); let mut renamed = edited; let text = renamed.files.remove(&renamed.entry).unwrap(); renamed.entry = "rescue.arcadia".into(); renamed.files.insert(renamed.entry.clone(), text); assert_eq!(put(&server, ¤t, &renamed).await["changed"], true); let current = server.get(id).await; assert_eq!(current.revision, 3); assert_eq!(current.model, original.model); server.stop().await; let server = TestServer::start(directory.path(), None).await; let restored = server.get(id).await; assert_eq!(restored.source_project, renamed); assert_eq!(restored.etag(), current.etag()); assert_pair(&restored); server.stop().await; } #[tokio::test] async fn source_projects_failed_atomic_comment_write_preserves_both_representations() { let directory = tempfile::tempdir().unwrap(); let server = TestServer::start(directory.path(), None).await; let current = import(&server, &authored()).await; let id = current.resource_id; let envelope = directory.path().join(format!("{id}.json")); let bytes = std::fs::read(&envelope).unwrap(); let mut edited = current.source_project.clone(); edited .files .get_mut("work.arcadia") .unwrap() .push_str("// Must not be adopted\r\n"); std::fs::remove_file(&envelope).unwrap(); std::fs::create_dir(&envelope).unwrap(); let failed = server .client .put(server.url(&format!("/projects/{id}/source-project"))) .header("if-match", current.etag()) .json(&edited) .send() .await .unwrap(); assert_eq!(failed.status(), StatusCode::SERVICE_UNAVAILABLE); let unchanged = server.get(id).await; assert_eq!(unchanged.etag(), current.etag()); assert_eq!(unchanged.model, current.model); assert_eq!(unchanged.source_project, current.source_project); std::fs::remove_dir(&envelope).unwrap(); std::fs::write(&envelope, &bytes).unwrap(); assert!(!std::fs::read_dir(directory.path()).unwrap().any(|entry| { entry .unwrap() .file_name() .to_string_lossy() .starts_with(".arcadia-write-") })); server.stop().await; let server = TestServer::start(directory.path(), None).await; assert_eq!(server.get(id).await.source_project, current.source_project); server.stop().await; } #[test] fn source_projects_openapi_describes_the_coupled_envelope_and_source_diagnostics() { let spec: Value = serde_json::from_str(include_str!("../../../../docs/openapi.json")).unwrap(); let schemas = &spec["components"]["schemas"]; assert!( schemas["StoredProject"]["required"] .as_array() .unwrap() .contains(&json!("source_project")) ); assert_eq!( schemas["SourceBundle"]["required"], json!(["entry", "files"]) ); assert_eq!(schemas["SourceBundle"]["additionalProperties"], false); assert_eq!( schemas["SourceBundle"]["properties"]["files"]["maxProperties"], 256 ); for property in ["path", "span", "related"] { assert!(schemas["Issue"]["properties"].get(property).is_some()); } assert_eq!( schemas["TransactionResult"]["properties"]["source_project"]["$ref"], "#/components/schemas/SourceBundle" ); assert!( schemas["ValidationRequest"]["oneOf"] .as_array() .unwrap() .iter() .any(|candidate| candidate["properties"]["format"]["const"] == "source-project") ); let routes = &spec["paths"]; assert_eq!( routes["/api/v1/projects/import-source-project"]["post"]["requestBody"]["content"]["application/json"] ["schema"]["$ref"], "#/components/schemas/SourceBundle" ); assert_eq!( routes["/api/v1/projects/{id}/source-project"]["get"]["responses"]["200"]["content"]["application/json"] ["schema"]["$ref"], "#/components/schemas/SourceBundle" ); assert_eq!( routes["/api/v1/projects/{id}/source-project"]["put"]["responses"]["200"]["content"]["application/json"] ["schema"]["$ref"], "#/components/schemas/SourceProjectMutationResult" ); } #[tokio::test] async fn source_projects_command_dry_runs_and_identity_swaps_preserve_lexical_ownership() { let directory = tempfile::tempdir().unwrap(); let server = TestServer::start(directory.path(), None).await; let original = import(&server, &authored()).await; let first = id_for(&original.model, "first"); let second = id_for(&original.model, "second"); let changed_id = Id::new_v4(); let mut element = original.model.elements[&first].clone(); element.id = changed_id; element.alias = "revised_first".into(); element.name = "Reviewed payload".into(); let commands = vec![Command::EditElement(first, element)]; let filename = directory .path() .join(format!("{}.json", original.resource_id)); let before_bytes = std::fs::read(&filename).unwrap(); let dry_run = transact(&server, &original, &commands, true).await; let proposed: SourceBundle = serde_json::from_value(dry_run["source_project"].clone()).unwrap(); assert!(proposed.files["team/first.arcadia"].contains(&changed_id.to_string())); assert!(proposed.files["team/first.arcadia"].starts_with("// First payload rationale\r\n")); assert_eq!( proposed.files["team/second.arcadia"], original.source_project.files["team/second.arcadia"] ); assert!( proposed.files["contracts.arcadia"].contains("@revised_first // first reference rationale") ); assert_eq!( server.get(original.resource_id).await.source_project, original.source_project ); assert_eq!(std::fs::read(&filename).unwrap(), before_bytes); transact(&server, &original, &commands, false).await; let current = server.get(original.resource_id).await; assert_eq!(current.source_project, proposed); assert_eq!( assert_pair(¤t).index.declarations[&changed_id].path, "team/first.arcadia" ); let temporary = Id::new_v4(); let swaps = vec![Command::Batch(vec![ Command::Reidentify(changed_id, temporary), Command::Reidentify(second, changed_id), Command::Reidentify(temporary, second), ])]; transact(&server, ¤t, &swaps, false).await; let swapped = server.get(current.resource_id).await; let compiled = assert_pair(&swapped); assert_eq!( compiled.index.declarations[&second].path, "team/first.arcadia" ); assert_eq!( compiled.index.declarations[&changed_id].path, "team/second.arcadia" ); assert!( swapped.source_project.files["team/first.arcadia"] .starts_with("// First payload rationale\r\n") ); assert!( swapped.source_project.files["contracts.arcadia"] .contains("@revised_first // first reference rationale") ); // Removing and reusing a UUID does not attach the old authored declaration // and its rationale to an unrelated new record. let mut replacement = swapped.model.elements[&second].clone(); replacement.alias = "new_payload".into(); replacement.name = "Independent new payload".into(); replacement.description.clear(); transact( &server, &swapped, &[Command::Delete(second), Command::CreateElement(replacement)], false, ) .await; let replaced = server.get(swapped.resource_id).await; let compiled = assert_pair(&replaced); assert_ne!( compiled.index.declarations[&second].path, "team/first.arcadia" ); assert!( replaced.source_project.files["team/first.arcadia"].contains("First payload rationale") ); assert!(!replaced.source_project.files["team/first.arcadia"].contains("new_payload")); server.stop().await; let server = TestServer::start(directory.path(), None).await; assert_eq!( server.get(replaced.resource_id).await.source_project, replaced.source_project ); server.stop().await; } #[tokio::test] async fn source_projects_equal_models_still_apply_changed_identity_provenance() { let directory = tempfile::tempdir().unwrap(); let server = TestServer::start(directory.path(), None).await; let original = import(&server, &authored()).await; let work = id_for(&original.model, "assess"); let commands = [ Command::Delete(work), Command::CreateElement(original.model.elements[&work].clone()), ]; let prepared = oa_core::prepare_command(&original.model, &Command::Batch(commands.to_vec())).unwrap(); assert_eq!(prepared.project, original.model); assert!(!prepared.identity_map.contains_key(&work)); let dry_run = transact(&server, &original, &commands, true).await; assert_eq!( dry_run["changed"], true, "Recreated record must not inherit the deleted record's declaration ownership" ); assert_eq!( dry_run["model"], serde_json::to_value(&original.model).unwrap() ); let response = transact(&server, &original, &commands, false).await; assert_eq!(response["changed"], true); let changed = server.get(original.resource_id).await; assert_eq!(changed.revision, original.revision + 1); assert_eq!(changed.model, original.model); let compiled = assert_pair(&changed); assert_ne!(compiled.index.declarations[&work].path, "work.arcadia"); assert!(changed.source_project.files["work.arcadia"].contains("Operational rationale")); assert!(!changed.source_project.files["work.arcadia"].contains("oa_activity")); server.stop().await; } #[tokio::test] async fn source_projects_model_interchange_and_transitions_keep_authored_modules() { let directory = tempfile::tempdir().unwrap(); let server = TestServer::start(directory.path(), None).await; let original = import(&server, &authored()).await; let id = original.resource_id; let work = id_for(&original.model, "assess"); let mut model = original.model.clone(); model.elements.get_mut(&work).unwrap().name = "Review assistance".into(); let response = server .client .put(server.url(&format!("/projects/{id}/model"))) .header("if-match", original.etag()) .json(&model) .send() .await .unwrap(); assert_eq!( response.status(), StatusCode::OK, "{}", response.text().await.unwrap() ); let current = server.get(id).await; assert_eq!(current.source_project.entry, "harbor.arcadia"); assert_eq!( current.source_project.files["team/first.arcadia"], original.source_project.files["team/first.arcadia"] ); assert!( current.source_project.files["work.arcadia"].starts_with("// Operational rationale\r\n") ); model.elements.get_mut(&work).unwrap().description = "Changed through canonical interchange".into(); let response = server .client .put(server.url(&format!("/projects/{id}/source"))) .header("if-match", current.etag()) .header("content-type", "text/plain") .body(oa_core::language::format(&model)) .send() .await .unwrap(); assert_eq!( response.status(), StatusCode::OK, "{}", response.text().await.unwrap() ); let current = server.get(id).await; assert_pair(¤t); let request = oa_core::transitions::TransitionRequest::new( oa_core::transitions::TransitionDirection::OaToSa, vec![work], ); let response = server .client .post(server.url(&format!("/projects/{id}/transitions/preview"))) .header("if-match", current.etag()) .json(&request) .send() .await .unwrap(); let status = response.status(); let preview: Value = response.json().await.unwrap(); assert_eq!(status, StatusCode::OK, "{preview}"); assert_eq!(server.get(id).await.source_project, current.source_project); let response = server .client .post(server.url(&format!("/projects/{id}/transitions/apply"))) .header("if-match", current.etag()) .json(&preview) .send() .await .unwrap(); assert_eq!( response.status(), StatusCode::OK, "{}", response.text().await.unwrap() ); let final_state = server.get(id).await; assert_pair(&final_state); assert!( final_state .model .elements .values() .any(|e| e.kind() == Kind::Function) ); assert_eq!(final_state.model.traces.len(), 1); for path in [ "team/first.arcadia", "team/second.arcadia", "contracts.arcadia", ] { assert_eq!( final_state.source_project.files[path], original.source_project.files[path] ); } server.stop().await; } #[tokio::test] async fn source_projects_invalid_dependencies_and_duplicate_paths_never_change_the_head() { let directory = tempfile::tempdir().unwrap(); let server = TestServer::start(directory.path(), None).await; let current = import(&server, &authored()).await; let id = current.resource_id; let filename = directory.path().join(format!("{id}.json")); let original_bytes = std::fs::read(&filename).unwrap(); // A same-named server file cannot satisfy a logical source include. std::fs::write( directory.path().join("server_only.arcadia"), "oa_activity external \"Must not load\" {}\n", ) .unwrap(); let mut cases = vec![]; let mut missing = current.source_project.clone(); missing .files .get_mut("work.arcadia") .unwrap() .push_str("include \"server_only.arcadia\";\n"); cases.push((missing, "missing_include")); let mut duplicate = current.source_project.clone(); let first = duplicate.files["team/first.arcadia"].clone(); duplicate .files .get_mut("team/second.arcadia") .unwrap() .push_str(&first); cases.push((duplicate, "duplicate_declaration")); let mut invalid = current.source_project.clone(); invalid .files .get_mut("team/second.arcadia") .unwrap() .push('?'); cases.push((invalid, "")); let mut cycle = current.source_project.clone(); cycle .files .get_mut("work.arcadia") .unwrap() .push_str("include \"harbor.arcadia\";\n"); cases.push((cycle, "include_cycle")); let mut orphan = current.source_project.clone(); orphan .files .insert("orphan.arcadia".into(), "// not included\n".into()); cases.push((orphan, "unreachable_file")); let mut escaped = current.source_project.clone(); escaped .files .insert("../outside.arcadia".into(), String::new()); cases.push((escaped, "invalid_path")); let mut excessive = current.source_project.clone(); for n in 0..256 { excessive .files .insert(format!("empty{n}.arcadia"), String::new()); } cases.push((excessive, "project_limit")); for (bundle, code) in cases { let response = server .client .put(server.url(&format!("/projects/{id}/source-project"))) .header("if-match", current.etag()) .json(&bundle) .send() .await .unwrap(); assert_eq!(response.status(), StatusCode::UNPROCESSABLE_ENTITY); let error: Value = response.json().await.unwrap(); let issue = &error["issues"][0]; if !code.is_empty() { assert_eq!(issue["code"], code, "{error}"); } assert!(issue["path"].is_string(), "{error}"); assert!(issue["span"]["start"].is_number(), "{error}"); assert!(issue["line"].as_u64().unwrap() >= 1); if code == "duplicate_declaration" { assert_eq!(issue["path"], "team/second.arcadia"); assert_eq!(issue["related"][0]["path"], "team/first.arcadia"); } let response = server .client .post(server.url(&format!("/projects/{id}/validation"))) .json(&json!({"format":"source-project","source_project":bundle})) .send() .await .unwrap(); assert_eq!(response.status(), StatusCode::OK); let validation: Value = response.json().await.unwrap(); assert_eq!(validation["valid"], false, "{validation}"); assert_eq!(validation["issues"][0]["path"], issue["path"]); assert_eq!(server.get(id).await.source_project, current.source_project); assert_eq!(std::fs::read(&filename).unwrap(), original_bytes); } let body = "{\"entry\":\"a.arcadia\",\"files\":{\"a.arcadia\":\"one\",\"a.arcadia\":\"two\"}}"; let duplicate_keys = server .client .put(server.url(&format!("/projects/{id}/source-project"))) .header("if-match", current.etag()) .header("content-type", "application/json") .body(body) .send() .await .unwrap(); assert_eq!(duplicate_keys.status(), StatusCode::BAD_REQUEST); assert_eq!(std::fs::read(&filename).unwrap(), original_bytes); let missing_precondition = server .client .put(server.url(&format!("/projects/{id}/source-project"))) .json(¤t.source_project) .send() .await .unwrap(); assert_eq!( missing_precondition.status(), StatusCode::PRECONDITION_REQUIRED ); server.stop().await; } #[tokio::test] async fn source_projects_stabilize_once_and_enforce_size_after_insertions() { let directory = tempfile::tempdir().unwrap(); let server = TestServer::start(directory.path(), None).await; let bundle = SourceBundle { entry: "entry.arcadia".into(), files: BTreeMap::from([ ( "entry.arcadia".into(), "// Header rationale\narcadia 3; project p \"P\" { include \"work.arcadia\"; }\n" .into(), ), ( "work.arcadia".into(), "// Work rationale\noa_activity work \"Work\" {}\n".into(), ), ]), }; let current = import(&server, &bundle).await; assert_ne!(current.source_project, bundle); assert!(current.source_project.files["work.arcadia"].starts_with("// Work rationale\n")); assert_pair(¤t); assert_eq!( put(&server, ¤t, ¤t.source_project).await["changed"], false ); let mut large = SourceBundle { entry: "entry.arcadia".into(), files: BTreeMap::from([( "entry.arcadia".into(), "arcadia 3; project p \"P\" {}\n// ".into(), )]), }; let base = serde_json::to_vec(&large).unwrap().len(); large .files .get_mut("entry.arcadia") .unwrap() .push_str(&"x".repeat(oa_server::store::MAX_SOURCE_PROJECT_BYTES - base - 8)); assert!(serde_json::to_vec(&large).unwrap().len() < oa_server::store::MAX_MODEL_BYTES); let response = server .client .put(server.url(&format!("/projects/{}/source-project", current.resource_id))) .header("if-match", current.etag()) .json(&large) .send() .await .unwrap(); assert_eq!(response.status(), StatusCode::PAYLOAD_TOO_LARGE); let error: Value = response.json().await.unwrap(); assert_eq!(error["code"], "source_project_too_large"); assert_eq!( server.get(current.resource_id).await.source_project, current.source_project ); server.stop().await; let server = TestServer::start(directory.path(), None).await; let restored = server.get(current.resource_id).await; assert_eq!(restored.source_project, current.source_project); assert_eq!(restored.etag(), current.etag()); server.stop().await; } #[tokio::test] async fn source_projects_concurrent_comment_and_semantic_writes_share_one_revision() { let directory = tempfile::tempdir().unwrap(); let server = TestServer::start(directory.path(), None).await; let original = import(&server, &authored()).await; let id = original.resource_id; let mut bundle = original.source_project.clone(); bundle .files .get_mut("work.arcadia") .unwrap() .push_str("// Comment writer\r\n"); let comment = server .client .put(server.url(&format!("/projects/{id}/source-project"))) .header("if-match", original.etag()) .json(&bundle) .send(); let semantic = server .client .patch(server.url(&format!("/projects/{id}"))) .header("if-match", original.etag()) .json(&json!({"name":"Semantic writer"})) .send(); let (comment, semantic) = tokio::join!(comment, semantic); let statuses = [comment.unwrap().status(), semantic.unwrap().status()]; assert_eq!(statuses.iter().filter(|s| **s == StatusCode::OK).count(), 1); assert_eq!( statuses .iter() .filter(|s| **s == StatusCode::PRECONDITION_FAILED) .count(), 1 ); let accepted = server.get(id).await; assert_eq!(accepted.revision, 2); assert_pair(&accepted); if accepted.model.name == "Semantic writer" { assert_eq!( accepted.source_project.files["work.arcadia"], original.source_project.files["work.arcadia"] ); } else { assert_eq!(accepted.model, original.model); assert_eq!(accepted.source_project, bundle); } server.stop().await; let server = TestServer::start(directory.path(), None).await; assert_eq!(server.get(id).await.source_project, accepted.source_project); server.stop().await; } #[tokio::test] async fn source_projects_upgrade_old_envelopes_without_writes_and_reject_inconsistent_pairs() { let directory = tempfile::tempdir().unwrap(); let resource = Id::new_v4(); let model = graph(); let filename = directory.path().join(format!("{resource}.json")); let original = serde_json::to_vec(&json!({"resource_id":resource,"revision":9,"model":model})).unwrap(); std::fs::write(&filename, &original).unwrap(); let server = TestServer::start(directory.path(), None).await; let current = server.get(resource).await; assert_eq!(current.model, model); assert_eq!(current.revision, 9); assert_pair(¤t); assert_eq!(std::fs::read(&filename).unwrap(), original); let response = server .client .put(server.url(&format!("/projects/{resource}/model"))) .header("if-match", current.etag()) .json(&model) .send() .await .unwrap(); assert_eq!(response.status(), StatusCode::OK); assert_eq!(response.json::().await.unwrap()["changed"], false); assert_eq!(std::fs::read(&filename).unwrap(), original); let mut bundle = current.source_project.clone(); bundle .files .get_mut(&bundle.entry) .unwrap() .push_str("// Reviewed source upgrade\n"); put(&server, ¤t, &bundle).await; let accepted = server.get(resource).await; assert_eq!(accepted.revision, 10); server.stop().await; let saved = std::fs::read(&filename).unwrap(); let mut envelope: Value = serde_json::from_slice(&saved).unwrap(); assert!(envelope.get("source_project").is_some()); envelope["model"]["name"] = json!("Corrupted cache"); std::fs::write(&filename, serde_json::to_vec(&envelope).unwrap()).unwrap(); assert!(Store::open(directory.path()).is_err()); let mut envelope: Value = serde_json::from_slice(&saved).unwrap(); envelope["source_project"] = Value::Null; std::fs::write(&filename, serde_json::to_vec(&envelope).unwrap()).unwrap(); assert!( Store::open(directory.path()).is_err(), "Present null source is not a legacy envelope" ); let mut envelope: Value = serde_json::from_slice(&saved).unwrap(); let entry = envelope["source_project"]["entry"] .as_str() .unwrap() .to_owned(); let header_id = format!(" id \"{}\"", accepted.model.id); let source = envelope["source_project"]["files"][&entry] .as_str() .unwrap(); assert!(source.contains(&header_id)); envelope["source_project"]["files"][&entry] = json!(source.replacen(&header_id, "", 1)); let unstable_bytes = serde_json::to_vec(&envelope).unwrap(); std::fs::write(&filename, &unstable_bytes).unwrap(); assert!( Store::open(directory.path()).is_err(), "Accepted sources must not silently acquire new IDs on restart" ); assert_eq!(std::fs::read(&filename).unwrap(), unstable_bytes); std::fs::write(&filename, &saved).unwrap(); let server = TestServer::start(directory.path(), None).await; assert_eq!(server.get(resource).await.source_project, bundle); server.stop().await; }