use std::{collections::HashMap, path::Path};
use crate::{
error::{MacroResult, err_call_site},
metadata::CollectedMetadata,
openapi_generator::{OpenApiSecurity, try_generate_openapi_doc_with_metadata},
route_impl::StoredRouteInfo,
router_codegen::ProcessedVesperaInput,
};
use proc_macro2::Span;
use syn::spanned::Spanned;
use super::{
cache::{MergeSpecCache, MergeSpecRead, path_fingerprint},
path_utils::{current_crate_tag, find_target_dir},
schema_merge::SchemaMergeGuard,
};
fn display_merge_path(path: &syn::Path) -> String {
path.segments
.iter()
.map(|segment| segment.ident.to_string())
.collect::<Vec<_>>()
.join("::")
}
#[derive(Debug)]
pub struct OpenApiWriteResult {
pub spec_json: Option<String>,
pub spec_pretty: Option<String>,
}
pub(super) struct EmbeddedSpecWrite {
pub(super) tokens: proc_macro2::TokenStream,
pub(super) fingerprint: Option<u64>,
}
pub(super) fn content_unchanged(path: &Path, content: &str) -> bool {
std::fs::metadata(path).is_ok_and(|m| m.len() == content.len() as u64)
&& std::fs::read_to_string(path).is_ok_and(|existing| existing == content)
}
pub(super) fn write_if_changed(path: &Path, content: &str) -> std::io::Result<Option<u64>> {
if !content_unchanged(path, content) {
std::fs::write(path, content)?;
}
Ok(path_fingerprint(path))
}
pub fn generate_and_write_openapi(
input: &ProcessedVesperaInput,
metadata: &CollectedMetadata,
file_asts: HashMap<String, syn::File>,
route_storage: &[StoredRouteInfo],
merge_specs: &mut MergeSpecCache,
) -> MacroResult<OpenApiWriteResult> {
if input.openapi_file_names.is_empty() && input.docs_url.is_none() && input.redoc_url.is_none()
{
return Ok(OpenApiWriteResult {
spec_json: None,
spec_pretty: None,
});
}
let mut openapi_doc = try_generate_openapi_doc_with_metadata(
input.title.clone(),
input.version.clone(),
input.servers.clone(),
Some(OpenApiSecurity {
security_schemes: input.security_schemes.clone(),
security: input.security.clone(),
tag_descriptions: input.tag_descriptions.clone(),
}),
metadata,
Some(file_asts),
route_storage,
)?;
if !input.merge.is_empty() {
let mut schema_guard = SchemaMergeGuard::new(&openapi_doc)?;
for merge_path in &input.merge {
if let Some((struct_name, spec_file)) = merge_specs.spec_file_for(merge_path) {
let spec_content = match merge_specs.read(&spec_file) {
MergeSpecRead::Present(content) => content,
MergeSpecRead::Error(e) => {
return Err(err_call_site(format!(
"OpenAPI merge: failed to read child spec for `{struct_name}` at '{}'. Error: {e}. Ensure the child crate containing `export_app!({struct_name})` is built before the parent app.",
spec_file.display()
)));
}
};
let child_spec = serde_json::from_str::<vespera_core::openapi::OpenApi>(spec_content).map_err(|e| err_call_site(format!("OpenAPI merge: failed to parse child spec for `{struct_name}` at '{}'. Error: {e}.", spec_file.display())))?;
let child_origin = format!("merged app `{}`", display_merge_path(merge_path));
schema_guard.check_child(&child_spec, &child_origin, merge_path.span())?;
openapi_doc.merge(child_spec);
}
}
}
let spec_pretty = if input.openapi_file_names.is_empty() {
None
} else {
let json_pretty = serde_json::to_string_pretty(&openapi_doc).map_err(|e| err_call_site(format!("OpenAPI generation: failed to serialize document to JSON. Error: {e}. Check that all schema types are serializable.")))?;
for openapi_file_name in &input.openapi_file_names {
let file_path = Path::new(openapi_file_name);
if let Some(parent) = file_path.parent() {
std::fs::create_dir_all(parent).map_err(|e| err_call_site(format!("OpenAPI output: failed to create directory '{}'. Error: {}. Ensure the path is valid and writable.", parent.display(), e)))?;
}
let should_write = !content_unchanged(file_path, &json_pretty);
if should_write {
std::fs::write(file_path, &json_pretty).map_err(|e| err_call_site(format!("OpenAPI output: failed to write file '{openapi_file_name}'. Error: {e}. Ensure the file path is writable.")))?;
}
}
Some(json_pretty)
};
let spec_json = if input.docs_url.is_some() || input.redoc_url.is_some() {
Some(serde_json::to_string(&openapi_doc).map_err(|e| err_call_site(format!("OpenAPI generation: failed to serialize document to JSON. Error: {e}. Check that all schema types are serializable.")))?)
} else {
None
};
Ok(OpenApiWriteResult {
spec_json,
spec_pretty,
})
}
pub fn ensure_openapi_files_from_cache(
openapi_file_names: &[String],
spec_pretty: Option<&str>,
) -> syn::Result<()> {
let Some(pretty) = spec_pretty else {
return Ok(());
};
for openapi_file_name in openapi_file_names {
let file_path = Path::new(openapi_file_name);
let should_write = !content_unchanged(file_path, pretty);
if should_write {
if let Some(parent) = file_path.parent() {
std::fs::create_dir_all(parent).map_err(|e| {
syn::Error::new(
Span::call_site(),
format!(
"OpenAPI output: failed to create directory '{}': {}",
parent.display(),
e
),
)
})?;
}
std::fs::write(file_path, pretty).map_err(|e| {
syn::Error::new(
Span::call_site(),
format!("OpenAPI output: failed to write file '{openapi_file_name}': {e}"),
)
})?;
}
}
Ok(())
}
pub(super) fn embed_spec_path() -> std::path::PathBuf {
let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap_or_default();
find_target_dir(Path::new(&manifest_dir))
.join("vespera")
.join(format!("vespera_spec-{}.json", current_crate_tag()))
}
pub(super) fn pretty_sidecar_path() -> std::path::PathBuf {
let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap_or_default();
find_target_dir(Path::new(&manifest_dir))
.join("vespera")
.join(format!("openapi_pretty-{}.json", current_crate_tag()))
}
fn embed_tokens(spec_file: &Path) -> proc_macro2::TokenStream {
let path_str = crate::file_utils::path_to_include_str_literal(spec_file);
quote::quote! { include_str!(#path_str) }
}
pub(super) struct SidecarSpecs {
pub(super) pretty: Option<String>,
pub(super) spec_tokens: Option<proc_macro2::TokenStream>,
}
pub(super) fn load_validated_sidecar_specs(
spec_json_hash: Option<u64>,
spec_pretty_hash: Option<u64>,
spec_json_fingerprint: Option<u64>,
spec_pretty_fingerprint: Option<u64>,
) -> Option<SidecarSpecs> {
let spec_tokens = match (spec_json_hash, spec_json_fingerprint) {
(None, _) => None,
(Some(expected_hash), Some(expected_fingerprint)) => {
let path = embed_spec_path();
if !super::cache::sidecar_matches(
&path,
Some(expected_hash),
Some(expected_fingerprint),
) {
return None;
}
Some(embed_tokens(&path))
}
(Some(_), None) => return None,
};
let pretty = match (spec_pretty_hash, spec_pretty_fingerprint) {
(None, _) => None,
(Some(expected_hash), Some(expected_fingerprint)) => {
let path = pretty_sidecar_path();
let content = std::fs::read_to_string(&path).ok()?;
if super::cache::path_fingerprint(&path) != Some(expected_fingerprint)
&& super::cache::hash_str(&content) != expected_hash
{
return None;
}
Some(content)
}
(Some(_), None) => return None,
};
Some(SidecarSpecs {
pretty,
spec_tokens,
})
}
pub(super) fn write_pretty_sidecar(spec_pretty: Option<&str>) -> Option<u64> {
let pretty = spec_pretty?;
let path = pretty_sidecar_path();
if let Some(parent) = path.parent() {
let _ = std::fs::create_dir_all(parent);
}
write_if_changed(&path, pretty).ok().flatten()
}
pub(super) fn write_spec_for_embedding(
spec_json: Option<String>,
) -> syn::Result<Option<EmbeddedSpecWrite>> {
let Some(json) = spec_json else {
return Ok(None);
};
let spec_file = embed_spec_path();
if let Some(parent) = spec_file.parent() {
std::fs::create_dir_all(parent).map_err(|e| {
syn::Error::new(
Span::call_site(),
format!(
"vespera! macro: failed to create directory '{}': {}",
parent.display(),
e
),
)
})?;
}
let fingerprint = write_if_changed(&spec_file, &json).map_err(|e| {
syn::Error::new(
Span::call_site(),
format!(
"vespera! macro: failed to write spec file '{}': {}",
spec_file.display(),
e
),
)
})?;
Ok(Some(EmbeddedSpecWrite {
tokens: embed_tokens(&spec_file),
fingerprint,
}))
}
#[cfg(test)]
mod tests {
use std::fs;
use tempfile::TempDir;
use super::*;
use crate::vespera_impl::cache::hash_str;
#[test]
fn test_generate_and_write_openapi_no_output() {
let processed = ProcessedVesperaInput {
folder_name: "routes".to_string(),
openapi_file_names: vec![],
title: None,
version: None,
docs_url: None,
redoc_url: None,
servers: None,
security_schemes: None,
security: None,
tag_descriptions: None,
merge: vec![],
};
let metadata = CollectedMetadata::new();
let result = generate_and_write_openapi(
&processed,
&metadata,
HashMap::new(),
&[],
&mut MergeSpecCache::new(),
);
assert!(result.is_ok());
let result = result.unwrap();
assert!(result.spec_json.is_none());
assert!(result.spec_pretty.is_none());
}
#[test]
fn test_generate_and_write_openapi_docs_only() {
let processed = ProcessedVesperaInput {
folder_name: "routes".to_string(),
openapi_file_names: vec![],
title: Some("Test API".to_string()),
version: Some("1.0.0".to_string()),
docs_url: Some("/docs".to_string()),
redoc_url: None,
servers: None,
security_schemes: None,
security: None,
tag_descriptions: None,
merge: vec![],
};
let metadata = CollectedMetadata::new();
let result = generate_and_write_openapi(
&processed,
&metadata,
HashMap::new(),
&[],
&mut MergeSpecCache::new(),
);
assert!(result.is_ok());
let result = result.unwrap();
assert!(result.spec_pretty.is_none());
assert!(result.spec_json.is_some());
let json = result.spec_json.unwrap();
assert!(json.contains("\"openapi\""));
assert!(json.contains("Test API"));
}
#[test]
fn test_generate_and_write_openapi_redoc_only() {
let processed = ProcessedVesperaInput {
folder_name: "routes".to_string(),
openapi_file_names: vec![],
title: None,
version: None,
docs_url: None,
redoc_url: Some("/redoc".to_string()),
servers: None,
security_schemes: None,
security: None,
tag_descriptions: None,
merge: vec![],
};
let metadata = CollectedMetadata::new();
let result = generate_and_write_openapi(
&processed,
&metadata,
HashMap::new(),
&[],
&mut MergeSpecCache::new(),
);
assert!(result.is_ok());
let result = result.unwrap();
assert!(result.spec_json.is_some());
assert!(result.spec_pretty.is_none());
}
#[test]
fn test_generate_and_write_openapi_both_docs() {
let processed = ProcessedVesperaInput {
folder_name: "routes".to_string(),
openapi_file_names: vec![],
title: None,
version: None,
docs_url: Some("/docs".to_string()),
redoc_url: Some("/redoc".to_string()),
servers: None,
security_schemes: None,
security: None,
tag_descriptions: None,
merge: vec![],
};
let metadata = CollectedMetadata::new();
let result = generate_and_write_openapi(
&processed,
&metadata,
HashMap::new(),
&[],
&mut MergeSpecCache::new(),
);
assert!(result.is_ok());
let result = result.unwrap();
assert!(result.spec_json.is_some());
assert!(result.spec_pretty.is_none());
}
#[test]
fn test_generate_and_write_openapi_file_output() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let output_path = temp_dir.path().join("test-openapi.json");
let processed = ProcessedVesperaInput {
folder_name: "routes".to_string(),
openapi_file_names: vec![output_path.to_string_lossy().to_string()],
title: Some("File Test".to_string()),
version: Some("2.0.0".to_string()),
docs_url: None,
redoc_url: None,
servers: None,
security_schemes: None,
security: None,
tag_descriptions: None,
merge: vec![],
};
let metadata = CollectedMetadata::new();
let result = generate_and_write_openapi(
&processed,
&metadata,
HashMap::new(),
&[],
&mut MergeSpecCache::new(),
);
assert!(result.is_ok());
assert!(output_path.exists());
let content = fs::read_to_string(&output_path).unwrap();
assert!(content.contains("\"openapi\""));
assert!(content.contains("File Test"));
assert!(content.contains("2.0.0"));
}
#[test]
fn test_generate_and_write_openapi_creates_directories() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let output_path = temp_dir.path().join("nested/dir/openapi.json");
let processed = ProcessedVesperaInput {
folder_name: "routes".to_string(),
openapi_file_names: vec![output_path.to_string_lossy().to_string()],
title: None,
version: None,
docs_url: None,
redoc_url: None,
servers: None,
security_schemes: None,
security: None,
tag_descriptions: None,
merge: vec![],
};
let metadata = CollectedMetadata::new();
let result = generate_and_write_openapi(
&processed,
&metadata,
HashMap::new(),
&[],
&mut MergeSpecCache::new(),
);
assert!(result.is_ok());
assert!(output_path.exists());
}
#[serial_test::serial]
#[test]
fn test_generate_and_write_openapi_with_merge_no_manifest_dir() {
let old_manifest_dir = std::env::var("CARGO_MANIFEST_DIR").ok();
unsafe { std::env::remove_var("CARGO_MANIFEST_DIR") };
let processed = ProcessedVesperaInput {
folder_name: "routes".to_string(),
openapi_file_names: vec![],
title: Some("Test".to_string()),
version: None,
docs_url: Some("/docs".to_string()),
redoc_url: None,
servers: None,
security_schemes: None,
security: None,
tag_descriptions: None,
merge: vec![syn::parse_quote!(app::TestApp)], };
let metadata = CollectedMetadata::new();
let result = generate_and_write_openapi(
&processed,
&metadata,
HashMap::new(),
&[],
&mut MergeSpecCache::new(),
);
if let Some(value) = old_manifest_dir {
unsafe { std::env::set_var("CARGO_MANIFEST_DIR", value) };
}
assert!(result.is_ok());
}
#[serial_test::serial]
#[test]
fn test_generate_and_write_openapi_with_merge_and_valid_spec() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let target_dir = temp_dir.path().join("target").join("vespera");
fs::create_dir_all(&target_dir).expect("Failed to create target/vespera dir");
let spec_content =
r#"{"openapi":"3.1.0","info":{"title":"Child API","version":"1.0.0"},"paths":{}}"#;
fs::write(target_dir.join("ChildApp.openapi.json"), spec_content)
.expect("Failed to write spec file");
let old_manifest_dir = std::env::var("CARGO_MANIFEST_DIR").ok();
unsafe { std::env::set_var("CARGO_MANIFEST_DIR", temp_dir.path()) };
let processed = ProcessedVesperaInput {
folder_name: "routes".to_string(),
openapi_file_names: vec![],
title: Some("Parent API".to_string()),
version: Some("2.0.0".to_string()),
docs_url: Some("/docs".to_string()),
redoc_url: None,
servers: None,
security_schemes: None,
security: None,
tag_descriptions: None,
merge: vec![syn::parse_quote!(child::ChildApp)],
};
let metadata = CollectedMetadata::new();
let result = generate_and_write_openapi(
&processed,
&metadata,
HashMap::new(),
&[],
&mut MergeSpecCache::new(),
);
if let Some(old_value) = old_manifest_dir {
unsafe { std::env::set_var("CARGO_MANIFEST_DIR", old_value) };
}
assert!(result.is_ok());
}
#[serial_test::serial]
#[test]
fn merged_child_schema_conflict_is_a_spanned_compile_error() {
let temp_dir = TempDir::new().unwrap();
let target_dir = temp_dir.path().join("target/vespera");
fs::create_dir_all(&target_dir).unwrap();
let spec = |property: &str, schema_type: &str| {
serde_json::json!({
"openapi": "3.1.0",
"info": { "title": "child", "version": "1.0.0" },
"paths": {},
"components": {
"schemas": {
"ExampleItem": {
"type": "object",
"properties": { (property): { "type": schema_type } }
}
}
}
})
.to_string()
};
fs::write(
target_dir.join("PluginA.openapi.json"),
spec("id", "string"),
)
.unwrap();
fs::write(
target_dir.join("PluginB.openapi.json"),
spec("collisionMarker", "boolean"),
)
.unwrap();
let _restore = RestoreManifest(std::env::var("CARGO_MANIFEST_DIR").ok());
unsafe { std::env::set_var("CARGO_MANIFEST_DIR", temp_dir.path()) };
let mut processed = merge_input(syn::parse_quote!(plugin_a::PluginA));
processed.merge.push(syn::parse_quote!(plugin_b::PluginB));
let error = generate_and_write_openapi(
&processed,
&CollectedMetadata::new(),
HashMap::new(),
&[],
&mut MergeSpecCache::new(),
)
.expect_err("different same-named child schemas must fail the build");
let message = error.to_string();
assert!(message.contains("Duplicate OpenAPI schema name 'ExampleItem'"));
assert!(message.contains("plugin_a::PluginA"));
assert!(message.contains("plugin_b::PluginB"));
}
#[test]
fn test_generate_and_write_openapi_file_write_error() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let output_path = temp_dir.path().join("openapi.json");
fs::create_dir(&output_path).expect("Failed to create directory");
let processed = ProcessedVesperaInput {
folder_name: "routes".to_string(),
openapi_file_names: vec![output_path.to_string_lossy().to_string()],
title: Some("Test API".to_string()),
version: Some("1.0.0".to_string()),
docs_url: None,
redoc_url: None,
servers: None,
security_schemes: None,
security: None,
tag_descriptions: None,
merge: vec![],
};
let metadata = CollectedMetadata::new();
let result = generate_and_write_openapi(
&processed,
&metadata,
HashMap::new(),
&[],
&mut MergeSpecCache::new(),
);
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(err.contains("failed to write file"));
}
#[test]
fn test_ensure_openapi_files_from_cache_none_spec() {
let result = ensure_openapi_files_from_cache(&["dummy.json".to_string()], None);
assert!(result.is_ok());
}
#[test]
fn test_ensure_openapi_files_from_cache_writes_file() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let output_path = temp_dir.path().join("api.json");
let spec = r#"{"openapi":"3.1.0"}"#;
let result = ensure_openapi_files_from_cache(
&[output_path.to_string_lossy().to_string()],
Some(spec),
);
assert!(result.is_ok());
assert_eq!(fs::read_to_string(&output_path).unwrap(), spec);
}
#[test]
fn test_ensure_openapi_files_from_cache_skip_unchanged() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let output_path = temp_dir.path().join("api.json");
let spec = r#"{"openapi":"3.1.0"}"#;
fs::write(&output_path, spec).unwrap();
let result = ensure_openapi_files_from_cache(
&[output_path.to_string_lossy().to_string()],
Some(spec),
);
assert!(result.is_ok());
assert_eq!(fs::read_to_string(&output_path).unwrap(), spec);
}
#[test]
fn test_ensure_openapi_files_from_cache_creates_parent_dirs() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let output_path = temp_dir.path().join("nested").join("dir").join("api.json");
let spec = r#"{"openapi":"3.1.0"}"#;
let result = ensure_openapi_files_from_cache(
&[output_path.to_string_lossy().to_string()],
Some(spec),
);
assert!(result.is_ok());
assert!(output_path.exists());
assert_eq!(fs::read_to_string(&output_path).unwrap(), spec);
}
#[test]
fn test_ensure_openapi_files_from_cache_write_error() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let output_path = temp_dir.path().join("api.json");
fs::create_dir(&output_path).unwrap();
let result = ensure_openapi_files_from_cache(
&[output_path.to_string_lossy().to_string()],
Some("spec"),
);
assert!(result.is_err());
}
#[test]
fn test_ensure_openapi_files_from_cache_multiple_files() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let path1 = temp_dir.path().join("api1.json");
let path2 = temp_dir.path().join("api2.json");
let spec = r#"{"openapi":"3.1.0"}"#;
let result = ensure_openapi_files_from_cache(
&[
path1.to_string_lossy().to_string(),
path2.to_string_lossy().to_string(),
],
Some(spec),
);
assert!(result.is_ok());
assert_eq!(fs::read_to_string(&path1).unwrap(), spec);
assert_eq!(fs::read_to_string(&path2).unwrap(), spec);
}
struct RestoreManifest(Option<String>);
impl Drop for RestoreManifest {
fn drop(&mut self) {
unsafe {
match self.0.take() {
Some(value) => std::env::set_var("CARGO_MANIFEST_DIR", value),
None => std::env::remove_var("CARGO_MANIFEST_DIR"),
}
}
}
}
fn merge_input(path: syn::Path) -> ProcessedVesperaInput {
ProcessedVesperaInput {
folder_name: "routes".to_string(),
openapi_file_names: Vec::new(),
title: None,
version: None,
docs_url: Some("/docs".to_string()),
redoc_url: None,
servers: None,
security_schemes: None,
security: None,
tag_descriptions: None,
merge: vec![path],
}
}
#[test]
#[serial_test::serial]
fn merge_reports_missing_and_malformed_child_specs() {
let temp = TempDir::new().unwrap();
let vespera_dir = temp.path().join("target/vespera");
fs::create_dir_all(&vespera_dir).unwrap();
let _restore = RestoreManifest(std::env::var("CARGO_MANIFEST_DIR").ok());
unsafe { std::env::set_var("CARGO_MANIFEST_DIR", temp.path()) };
let missing = generate_and_write_openapi(
&merge_input(syn::parse_quote!(child::MissingChild)),
&CollectedMetadata::new(),
HashMap::new(),
&[],
&mut MergeSpecCache::new(),
)
.expect_err("missing child sidecar must fail");
assert!(missing.to_string().contains("failed to read child spec"));
fs::write(vespera_dir.join("BrokenChild.openapi.json"), "not-json").unwrap();
let malformed = generate_and_write_openapi(
&merge_input(syn::parse_quote!(child::BrokenChild)),
&CollectedMetadata::new(),
HashMap::new(),
&[],
&mut MergeSpecCache::new(),
)
.expect_err("malformed child sidecar must fail");
assert!(malformed.to_string().contains("failed to parse child spec"));
}
#[test]
fn ensure_cached_openapi_reports_parent_creation_error() {
let temp = TempDir::new().unwrap();
let blocking_parent = temp.path().join("blocking");
fs::write(&blocking_parent, "file").unwrap();
let output = blocking_parent.join("openapi.json");
let error =
ensure_openapi_files_from_cache(&[output.to_string_lossy().into_owned()], Some("{}"))
.expect_err("a file cannot be created as a directory");
assert!(error.to_string().contains("failed to create directory"));
}
#[test]
#[serial_test::serial]
fn sidecar_validation_covers_missing_fingerprints_corruption_and_success() {
let temp = TempDir::new().unwrap();
let _restore = RestoreManifest(std::env::var("CARGO_MANIFEST_DIR").ok());
unsafe { std::env::set_var("CARGO_MANIFEST_DIR", temp.path()) };
let vespera_dir = temp.path().join("target/vespera");
fs::create_dir_all(&vespera_dir).unwrap();
let compact = embed_spec_path();
let pretty = pretty_sidecar_path();
fs::write(&compact, "compact").unwrap();
fs::write(&pretty, "pretty").unwrap();
let compact_fp = path_fingerprint(&compact).unwrap();
let pretty_fp = path_fingerprint(&pretty).unwrap();
assert!(
load_validated_sidecar_specs(Some(hash_str("compact")), None, None, None).is_none()
);
assert!(load_validated_sidecar_specs(None, Some(hash_str("pretty")), None, None).is_none());
assert!(
load_validated_sidecar_specs(
Some(hash_str("wrong")),
None,
Some(compact_fp.wrapping_add(1)),
None,
)
.is_none()
);
assert!(
load_validated_sidecar_specs(
None,
Some(hash_str("wrong")),
None,
Some(pretty_fp.wrapping_add(1)),
)
.is_none()
);
let loaded = load_validated_sidecar_specs(
Some(hash_str("compact")),
Some(hash_str("pretty")),
Some(compact_fp),
Some(pretty_fp),
)
.expect("matching sidecars load");
assert_eq!(loaded.pretty.as_deref(), Some("pretty"));
assert!(loaded.spec_tokens.is_some());
}
#[test]
fn sidecar_validation_accepts_cache_without_generated_specs() {
let loaded = load_validated_sidecar_specs(None, None, None, None)
.expect("a cache without configured specs needs no sidecar validation");
assert!(loaded.pretty.is_none());
assert!(loaded.spec_tokens.is_none());
}
#[test]
#[serial_test::serial]
fn embedding_reports_directory_creation_and_spec_write_errors() {
let create_temp = TempDir::new().unwrap();
let _restore = RestoreManifest(std::env::var("CARGO_MANIFEST_DIR").ok());
unsafe { std::env::set_var("CARGO_MANIFEST_DIR", create_temp.path()) };
fs::create_dir_all(create_temp.path().join("target")).unwrap();
fs::write(create_temp.path().join("target/vespera"), "blocking file").unwrap();
let Err(create_error) = write_spec_for_embedding(Some("{}".to_string())) else {
panic!("blocking parent must fail directory creation");
};
assert!(
create_error
.to_string()
.contains("failed to create directory")
);
let write_temp = TempDir::new().unwrap();
unsafe { std::env::set_var("CARGO_MANIFEST_DIR", write_temp.path()) };
let spec_path = embed_spec_path();
fs::create_dir_all(&spec_path).unwrap();
let Err(write_error) = write_spec_for_embedding(Some("{}".to_string())) else {
panic!("a directory cannot be overwritten as a spec file");
};
assert!(
write_error
.to_string()
.contains("failed to write spec file")
);
}
}