From 3abfe6a79bad4c8b8dbaba4b25358467723cd1e2 Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Thu, 5 Feb 2026 11:11:49 +0300 Subject: [PATCH] pinakes-plugin-api: expand test coverage; fix merge conflicts Signed-off-by: NotAShelf Change-Id: I34e7c6d382ab7f4b6cf98ede9b7116056a6a6964 --- Cargo.lock | 1 + Cargo.toml | 1 + crates/pinakes-plugin-api/Cargo.toml | 3 + crates/pinakes-plugin-api/src/lib.rs | 1 + crates/pinakes-plugin-api/tests/api.rs | 556 ++++++++++++++++++ .../{validate_examples.rs => validate.rs} | 0 examples/plugins/heif-support/plugin.toml | 33 -- .../plugins/markdown-metadata/plugin.toml | 30 +- 8 files changed, 564 insertions(+), 61 deletions(-) create mode 100644 crates/pinakes-plugin-api/tests/api.rs rename crates/pinakes-plugin-api/tests/{validate_examples.rs => validate.rs} (100%) diff --git a/Cargo.lock b/Cargo.lock index 06d6458..67e3f3f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4904,6 +4904,7 @@ dependencies = [ "serde", "serde_json", "thiserror 2.0.18", + "tokio", "toml 0.9.11+spec-1.1.0", "uuid", "wit-bindgen 0.39.0", diff --git a/Cargo.toml b/Cargo.toml index b33a61d..c1923f8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,7 @@ resolver = "3" edition = "2024" version = "0.1.0" license = "MIT" +readme = true [workspace.dependencies] # Async runtime diff --git a/crates/pinakes-plugin-api/Cargo.toml b/crates/pinakes-plugin-api/Cargo.toml index b33befe..b761eb2 100644 --- a/crates/pinakes-plugin-api/Cargo.toml +++ b/crates/pinakes-plugin-api/Cargo.toml @@ -25,3 +25,6 @@ wit-bindgen = { workspace = true, optional = true } [features] default = [] wasm = ["wit-bindgen"] + +[dev-dependencies] +tokio = { workspace = true, features = ["rt", "rt-multi-thread", "macros"] } diff --git a/crates/pinakes-plugin-api/src/lib.rs b/crates/pinakes-plugin-api/src/lib.rs index d1e1534..abfa474 100644 --- a/crates/pinakes-plugin-api/src/lib.rs +++ b/crates/pinakes-plugin-api/src/lib.rs @@ -15,6 +15,7 @@ pub mod wasm; pub use manifest::PluginManifest; pub use types::*; +pub use wasm::host_functions; /// Plugin API version - plugins must match this version pub const PLUGIN_API_VERSION: &str = "1.0"; diff --git a/crates/pinakes-plugin-api/tests/api.rs b/crates/pinakes-plugin-api/tests/api.rs new file mode 100644 index 0000000..a1d8460 --- /dev/null +++ b/crates/pinakes-plugin-api/tests/api.rs @@ -0,0 +1,556 @@ +use pinakes_plugin_api::{ + Capabilities, EnvironmentCapability, Event, EventType, + ExtractedMetadata, FilesystemCapability, HealthStatus, + MediaTypeDefinition, NetworkCapability, Plugin, PluginContext, + PluginError, PluginMetadata, PluginResult, SearchIndexItem, SearchQuery, + SearchResult, SearchStats, ThumbnailFormat, ThumbnailInfo, ThumbnailOptions, +}; +use pinakes_plugin_api::wasm::{HttpRequest, HttpResponse, LogLevel, LogMessage}; +use std::collections::HashMap; +use std::path::PathBuf; +use async_trait::async_trait; + +struct TestPlugin { + initialized: bool, + shutdown: bool, + health_status: HealthStatus, + metadata: PluginMetadata, +} + +impl TestPlugin { + fn new() -> Self { + Self { + initialized: false, + shutdown: false, + health_status: HealthStatus { + healthy: true, + message: Some("OK".to_string()), + metrics: HashMap::new(), + }, + metadata: PluginMetadata { + id: "test-plugin".to_string(), + name: "Test Plugin".to_string(), + version: "1.0.0".to_string(), + author: "Test Author".to_string(), + description: "A test plugin".to_string(), + api_version: "1.0".to_string(), + capabilities_required: Capabilities::default(), + }, + } + } +} + +#[async_trait] +impl Plugin for TestPlugin { + fn metadata(&self) -> &PluginMetadata { + &self.metadata + } + + async fn initialize(&mut self, _context: PluginContext) -> PluginResult<()> { + self.initialized = true; + Ok(()) + } + + async fn shutdown(&mut self) -> PluginResult<()> { + self.shutdown = true; + Ok(()) + } + + async fn health_check(&self) -> PluginResult { + Ok(self.health_status.clone()) + } +} + +#[tokio::test] +async fn test_plugin_context_creation() { + let context = PluginContext { + data_dir: PathBuf::from("/data/test-plugin"), + cache_dir: PathBuf::from("/cache/test-plugin"), + config: HashMap::from([ + ("enabled".to_string(), serde_json::json!(true)), + ("max_items".to_string(), serde_json::json!(100)), + ]), + capabilities: Capabilities { + filesystem: FilesystemCapability { + read: vec![PathBuf::from("/data")], + write: vec![PathBuf::from("/data")], + }, + network: NetworkCapability { + enabled: true, + allowed_domains: Some(vec!["api.example.com".to_string()]), + }, + environment: EnvironmentCapability { + enabled: true, + allowed_vars: Some(vec!["API_KEY".to_string()]), + }, + max_memory_bytes: Some(256 * 1024 * 1024), + max_cpu_time_ms: Some(30000), + }, + }; + + assert_eq!(context.data_dir, PathBuf::from("/data/test-plugin")); + assert_eq!(context.cache_dir, PathBuf::from("/cache/test-plugin")); + assert_eq!(context.config.get("enabled").unwrap(), &serde_json::json!(true)); + assert!(context.capabilities.network.enabled); + assert_eq!( + context.capabilities.max_memory_bytes, + Some(256 * 1024 * 1024) + ); +} + +#[tokio::test] +async fn test_plugin_context_fields() { + let context = PluginContext { + data_dir: PathBuf::from("/custom/data"), + cache_dir: PathBuf::from("/custom/cache"), + config: HashMap::new(), + capabilities: Capabilities::default(), + }; + + assert_eq!(context.data_dir, PathBuf::from("/custom/data")); + assert_eq!(context.cache_dir, PathBuf::from("/custom/cache")); +} + +#[tokio::test] +async fn test_plugin_lifecycle() { + let mut plugin = TestPlugin::new(); + + assert!(!plugin.initialized); + assert!(!plugin.shutdown); + + let context = PluginContext { + data_dir: PathBuf::from("/data"), + cache_dir: PathBuf::from("/cache"), + config: HashMap::new(), + capabilities: Capabilities::default(), + }; + plugin.initialize(context).await.unwrap(); + assert!(plugin.initialized); + + let health = plugin.health_check().await.unwrap(); + assert!(health.healthy); + assert_eq!(health.message, Some("OK".to_string())); + + plugin.shutdown().await.unwrap(); + assert!(plugin.shutdown); +} + +#[tokio::test] +async fn test_extracted_metadata_structure() { + let metadata = ExtractedMetadata { + title: Some("Test Document".to_string()), + description: Some("A test document".to_string()), + author: Some("John Doe".to_string()), + created_at: Some("2024-01-15T10:30:00Z".to_string()), + duration_secs: Some(120.5), + width: Some(1920), + height: Some(1080), + file_size_bytes: Some(1_500_000), + codec: Some("h264".to_string()), + bitrate_kbps: Some(5000), + custom_fields: HashMap::from([ + ("color_space".to_string(), serde_json::json!("sRGB")), + ("orientation".to_string(), serde_json::json!(90)), + ]), + tags: vec!["test".to_string(), "document".to_string()], + }; + + assert_eq!(metadata.title, Some("Test Document".to_string())); + assert_eq!(metadata.width, Some(1920)); + assert_eq!(metadata.height, Some(1080)); + assert_eq!(metadata.tags.len(), 2); + assert_eq!(metadata.custom_fields.get("color_space").unwrap(), "sRGB"); +} + +#[tokio::test] +async fn test_search_query_serialization() { + let query = SearchQuery { + query_text: "nature landscape".to_string(), + filters: HashMap::from([ + ("type".to_string(), serde_json::json!("image")), + ("year".to_string(), serde_json::json!(2023)), + ]), + limit: 50, + offset: 0, + }; + + let serialized = serde_json::to_string(&query).unwrap(); + let deserialized: SearchQuery = serde_json::from_str(&serialized).unwrap(); + + assert_eq!(deserialized.query_text, "nature landscape"); + assert_eq!(deserialized.limit, 50); + assert_eq!(deserialized.offset, 0); + assert_eq!(deserialized.filters.get("type").unwrap(), "image"); +} + +#[tokio::test] +async fn test_search_result_serialization() { + let result = SearchResult { + id: "media-123".to_string(), + score: 0.95, + highlights: vec!["matched content".to_string()], + }; + + let serialized = serde_json::to_string(&result).unwrap(); + let deserialized: SearchResult = serde_json::from_str(&serialized).unwrap(); + + assert_eq!(deserialized.id, "media-123"); + assert!((deserialized.score - 0.95).abs() < 0.001); + assert_eq!(deserialized.highlights.len(), 1); +} + +#[tokio::test] +async fn test_search_stats_serialization() { + let stats = SearchStats { + total_indexed: 10_000, + index_size_bytes: 500_000_000, + last_update: Some("2024-01-15T12:00:00Z".to_string()), + }; + + let serialized = serde_json::to_string(&stats).unwrap(); + let deserialized: SearchStats = serde_json::from_str(&serialized).unwrap(); + + assert_eq!(deserialized.total_indexed, 10_000); + assert_eq!(deserialized.index_size_bytes, 500_000_000); + assert!(deserialized.last_update.is_some()); +} + +#[tokio::test] +async fn test_thumbnail_options_serialization() { + let options = ThumbnailOptions { + width: 320, + height: 240, + quality: 85, + format: ThumbnailFormat::Jpeg, + }; + + let serialized = serde_json::to_string(&options).unwrap(); + let deserialized: ThumbnailOptions = serde_json::from_str(&serialized).unwrap(); + + assert_eq!(deserialized.width, 320); + assert_eq!(deserialized.height, 240); + assert_eq!(deserialized.quality, 85); + assert!(matches!(deserialized.format, ThumbnailFormat::Jpeg)); +} + +#[tokio::test] +async fn test_thumbnail_format_variants() { + for format in [ThumbnailFormat::Jpeg, ThumbnailFormat::Png, ThumbnailFormat::WebP] { + let options = ThumbnailOptions { + width: 100, + height: 100, + quality: 90, + format, + }; + let serialized = serde_json::to_string(&options).unwrap(); + let deserialized: ThumbnailOptions = serde_json::from_str(&serialized).unwrap(); + assert!(matches!(deserialized.format, _)); + } +} + +#[tokio::test] +async fn test_thumbnail_info_structure() { + let info = ThumbnailInfo { + path: PathBuf::from("/thumbnails/test.jpg"), + width: 320, + height: 240, + file_size_bytes: 45_000, + }; + + assert_eq!(info.path, PathBuf::from("/thumbnails/test.jpg")); + assert_eq!(info.width, 320); + assert_eq!(info.height, 240); + assert_eq!(info.file_size_bytes, 45_000); +} + +#[tokio::test] +async fn test_media_type_definition() { + let media_type = MediaTypeDefinition { + id: "custom-image format".to_string(), + name: "Custom Image Format".to_string(), + category: "image".to_string(), + extensions: vec!["cif".to_string(), "custom-img".to_string()], + mime_types: vec!["image/x-custom".to_string()], + icon: Some("image-cif".to_string()), + }; + + assert_eq!(media_type.id, "custom-image format"); + assert_eq!(media_type.category, "image"); + assert_eq!(media_type.extensions.len(), 2); + assert!(media_type.icon.is_some()); +} + +#[tokio::test] +async fn test_event_type_variants() { + let variants: Vec = vec![ + EventType::MediaImported, + EventType::MediaUpdated, + EventType::MediaDeleted, + EventType::MediaTagged, + EventType::MediaUntagged, + EventType::CollectionCreated, + EventType::CollectionUpdated, + EventType::CollectionDeleted, + EventType::ScanStarted, + EventType::ScanCompleted, + EventType::Custom("custom".to_string()), + ]; + + for event_type in &variants { + let serialized = serde_json::to_string(event_type).unwrap(); + let _deserialized: EventType = serde_json::from_str(&serialized).unwrap(); + } +} + +#[tokio::test] +async fn test_event_serialization() { + let event = Event { + event_type: EventType::MediaImported, + timestamp: "2024-01-15T10:00:00Z".to_string(), + data: HashMap::from([ + ("path".to_string(), serde_json::json!("/media/test.jpg")), + ("size".to_string(), serde_json::json!(1024)), + ]), + }; + + let serialized = serde_json::to_string(&event).unwrap(); + let deserialized: Event = serde_json::from_str(&serialized).unwrap(); + + assert!(matches!(deserialized.event_type, EventType::MediaImported)); + assert_eq!(deserialized.timestamp, "2024-01-15T10:00:00Z"); +} + +#[tokio::test] +async fn test_http_request_serialization() { + let request = HttpRequest { + method: "GET".to_string(), + url: "https://api.example.com/data".to_string(), + headers: HashMap::from([ + ("Authorization".to_string(), "Bearer token".to_string()), + ("Content-Type".to_string(), "application/json".to_string()), + ]), + body: None, + }; + + let serialized = serde_json::to_string(&request).unwrap(); + let deserialized: HttpRequest = serde_json::from_str(&serialized).unwrap(); + + assert_eq!(deserialized.method, "GET"); + assert_eq!(deserialized.url, "https://api.example.com/data"); + assert!(deserialized.body.is_none()); +} + +#[tokio::test] +async fn test_http_response_serialization() { + let response = HttpResponse { + status: 200, + headers: HashMap::from([("Content-Type".to_string(), "application/json".to_string())]), + body: b"{\"success\": true}".to_vec(), + }; + + let serialized = serde_json::to_string(&response).unwrap(); + let deserialized: HttpResponse = serde_json::from_str(&serialized).unwrap(); + + assert_eq!(deserialized.status, 200); + assert_eq!(deserialized.body, b"{\"success\": true}"); +} + +#[tokio::test] +async fn test_log_message_serialization() { + let message = LogMessage { + level: LogLevel::Info, + target: "plugin::metadata".to_string(), + message: "Metadata extraction complete".to_string(), + fields: HashMap::from([ + ("file_count".to_string(), "42".to_string()), + ("duration_ms".to_string(), "150".to_string()), + ]), + }; + + let serialized = serde_json::to_string(&message).unwrap(); + let deserialized: LogMessage = serde_json::from_str(&serialized).unwrap(); + + assert!(matches!(deserialized.level, LogLevel::Info)); + assert_eq!(deserialized.target, "plugin::metadata"); + assert_eq!(deserialized.message, "Metadata extraction complete"); +} + +#[tokio::test] +async fn test_log_level_variants() { + let levels = [LogLevel::Trace, LogLevel::Debug, LogLevel::Info, LogLevel::Warn, LogLevel::Error]; + + for level in levels { + let serialized = serde_json::to_string(&level).unwrap(); + let _deserialized: LogLevel = serde_json::from_str(&serialized).unwrap(); + } +} + +#[tokio::test] +async fn test_plugin_error_variants() { + let errors: Vec = vec![ + PluginError::InitializationFailed("WASM load failed".to_string()), + PluginError::UnsupportedOperation("Custom search not implemented".to_string()), + PluginError::InvalidInput("Invalid file path".to_string()), + PluginError::IoError("File not found".to_string()), + PluginError::MetadataExtractionFailed("Parse error".to_string()), + PluginError::ThumbnailGenerationFailed("Format not supported".to_string()), + PluginError::SearchBackendError("Index corrupted".to_string()), + PluginError::PermissionDenied("Access denied to /data".to_string()), + PluginError::ResourceLimitExceeded("Memory limit exceeded".to_string()), + PluginError::Other("Unknown error".to_string()), + ]; + + for error in errors { + let serialized = serde_json::to_string(&error).unwrap(); + let deserialized: PluginError = serde_json::from_str(&serialized).unwrap(); + assert_eq!(format!("{}", error), format!("{}", deserialized)); + } +} + +#[tokio::test] +async fn test_search_index_item_serialization() { + let item = SearchIndexItem { + id: "media-456".to_string(), + title: Some("Summer Vacation".to_string()), + description: Some("Photos from summer vacation 2023".to_string()), + content: None, + tags: vec!["vacation".to_string(), "summer".to_string(), "photos".to_string()], + media_type: "image/jpeg".to_string(), + metadata: HashMap::from([ + ("camera".to_string(), serde_json::json!("Canon EOS R5")), + ("location".to_string(), serde_json::json!("Beach")), + ]), + }; + + let serialized = serde_json::to_string(&item).unwrap(); + let deserialized: SearchIndexItem = serde_json::from_str(&serialized).unwrap(); + + assert_eq!(deserialized.id, "media-456"); + assert_eq!(deserialized.title, Some("Summer Vacation".to_string())); + assert_eq!(deserialized.tags.len(), 3); + assert_eq!(deserialized.media_type, "image/jpeg"); +} + +#[tokio::test] +async fn test_health_status_variants() { + let healthy = HealthStatus { + healthy: true, + message: Some("All systems operational".to_string()), + metrics: HashMap::from([ + ("items_processed".to_string(), 1000.0), + ("avg_process_time_ms".to_string(), 45.5), + ]), + }; + assert!(healthy.healthy); + + let unhealthy = HealthStatus { + healthy: false, + message: Some("Database connection failed".to_string()), + metrics: HashMap::new(), + }; + assert!(!unhealthy.healthy); + assert_eq!(unhealthy.message, Some("Database connection failed".to_string())); +} + +#[tokio::test] +async fn test_capabilities_merge() { + let mut caps = Capabilities::default(); + assert!(!caps.network.enabled); + + caps.network.enabled = true; + caps.max_memory_bytes = Some(512 * 1024 * 1024); + + assert!(caps.network.enabled); + assert_eq!(caps.max_memory_bytes, Some(512 * 1024 * 1024)); +} + +#[tokio::test] +async fn test_filesystem_capability_paths() { + let caps = FilesystemCapability { + read: vec![ + PathBuf::from("/data"), + PathBuf::from("/media"), + PathBuf::from("/home/user/uploads"), + ], + write: vec![PathBuf::from("/tmp/pinakes")], + }; + + assert_eq!(caps.read.len(), 3); + assert_eq!(caps.write.len(), 1); + assert!(caps.read.contains(&PathBuf::from("/data"))); + assert!(caps.write.contains(&PathBuf::from("/tmp/pinakes"))); +} + +#[tokio::test] +async fn test_plugin_metadata_structure() { + let metadata = PluginMetadata { + id: "test-plugin".to_string(), + name: "Test Plugin".to_string(), + version: "1.0.0".to_string(), + author: "Test Author".to_string(), + description: "A test plugin for unit testing".to_string(), + api_version: "1.0".to_string(), + capabilities_required: Capabilities::default(), + }; + + assert_eq!(metadata.id, "test-plugin"); + assert_eq!(metadata.version, "1.0.0"); + assert_eq!(metadata.api_version, "1.0"); +} + +#[tokio::test] +async fn test_network_capability_defaults() { + let network = NetworkCapability::default(); + assert!(!network.enabled); + assert!(network.allowed_domains.is_none()); +} + +#[tokio::test] +async fn test_environment_capability_defaults() { + let env = EnvironmentCapability::default(); + assert!(!env.enabled); + assert!(env.allowed_vars.is_none()); +} + +#[tokio::test] +async fn test_extracted_metadata_default() { + let metadata = ExtractedMetadata::default(); + + assert!(metadata.title.is_none()); + assert!(metadata.description.is_none()); + assert!(metadata.author.is_none()); + assert!(metadata.duration_secs.is_none()); + assert!(metadata.width.is_none()); + assert!(metadata.height.is_none()); + assert!(metadata.custom_fields.is_empty()); + assert!(metadata.tags.is_empty()); +} + +#[tokio::test] +async fn test_search_query_structure() { + let query = SearchQuery { + query_text: "test query".to_string(), + filters: HashMap::new(), + limit: 10, + offset: 0, + }; + + assert!(!query.query_text.is_empty()); + assert_eq!(query.limit, 10); + assert_eq!(query.offset, 0); +} + +#[tokio::test] +async fn test_thumbnail_options_structure() { + let options = ThumbnailOptions { + width: 640, + height: 480, + quality: 75, + format: ThumbnailFormat::Png, + }; + + assert_eq!(options.width, 640); + assert_eq!(options.height, 480); + assert_eq!(options.quality, 75); + assert!(matches!(options.format, ThumbnailFormat::Png)); +} diff --git a/crates/pinakes-plugin-api/tests/validate_examples.rs b/crates/pinakes-plugin-api/tests/validate.rs similarity index 100% rename from crates/pinakes-plugin-api/tests/validate_examples.rs rename to crates/pinakes-plugin-api/tests/validate.rs diff --git a/examples/plugins/heif-support/plugin.toml b/examples/plugins/heif-support/plugin.toml index b60fecf..6f30eb1 100644 --- a/examples/plugins/heif-support/plugin.toml +++ b/examples/plugins/heif-support/plugin.toml @@ -2,31 +2,6 @@ name = "heif-support" version = "1.0.0" api_version = "1.0" -author = "Pinakes Team" -description = "HEIF/HEIC image format support with metadata extraction and thumbnail generation" -homepage = "https://github.com/pinakes/pinakes" -license = "MIT OR Apache-2.0" -kind = ["media_type", "metadata_extractor", "thumbnail_generator"] - -[plugin.binary] -wasm = "heif_support.wasm" - -[capabilities] -max_memory_mb = 256 -max_cpu_time_secs = 30 - -[capabilities.filesystem] -read = ["/tmp/pinakes-input"] -write = ["/tmp/pinakes-output"] - -[config] -extract_exif = { type = "boolean", default = true, description = "Extract EXIF metadata from HEIF images" } -generate_thumbnails = { type = "boolean", default = true, description = "Generate thumbnails for HEIF images" } -thumbnail_quality = { type = "integer", default = 85, description = "JPEG quality for thumbnails (1-100)" } -[plugin] -name = "heif-support" -version = "1.0.0" -api_version = "1.0" author = "Pinakes Contributors" description = "HEIF/HEIC image format support for Pinakes" homepage = "https://github.com/notashelf/pinakes" @@ -42,21 +17,13 @@ max_memory_mb = 256 max_cpu_time_secs = 30 [capabilities.filesystem] -# Read access for processing images (use specific paths in production) read = ["/media"] -# Write access for thumbnail generation write = ["/tmp/pinakes"] -# Plugin configuration [config] -# Enable EXIF metadata extraction extract_exif = true -# Enable thumbnail generation generate_thumbnails = true -# Thumbnail quality (1-100) thumbnail_quality = 85 -# Thumbnail format (jpeg, png, webp) thumbnail_format = "jpeg" -# Maximum image dimensions to process max_width = 8192 max_height = 8192 diff --git a/examples/plugins/markdown-metadata/plugin.toml b/examples/plugins/markdown-metadata/plugin.toml index acfc4e8..a558095 100644 --- a/examples/plugins/markdown-metadata/plugin.toml +++ b/examples/plugins/markdown-metadata/plugin.toml @@ -2,29 +2,8 @@ name = "markdown-metadata" version = "1.0.0" api_version = "1.0" -author = "Pinakes Team" -description = "Extract metadata from Markdown files with YAML frontmatter" -homepage = "https://github.com/pinakes/pinakes" -license = "MIT OR Apache-2.0" -kind = ["metadata_extractor"] - -[plugin.binary] -wasm = "markdown_metadata.wasm" - -[capabilities] -# No filesystem or network access needed -# Plugin operates on provided content - -[config] -extract_tags = { type = "boolean", default = true, description = "Extract tags from YAML frontmatter" } -parse_yaml = { type = "boolean", default = true, description = "Parse YAML frontmatter" } -max_file_size = { type = "integer", default = 10485760, description = "Maximum file size in bytes (10MB)" } -[plugin] -name = "markdown-metadata" -version = "1.0.0" -api_version = "1.0" author = "Pinakes Contributors" -description = "Enhanced Markdown metadata extractor with frontmatter parsing" +description = "Extract metadata from Markdown files with YAML frontmatter" homepage = "https://github.com/notashelf/pinakes" license = "MIT" kind = ["metadata_extractor"] @@ -39,13 +18,8 @@ network = false read = [] write = [] -# Plugin configuration [config] -# Extract frontmatter tags as media tags extract_tags = true -# Parse YAML frontmatter parse_yaml = true -# Parse TOML frontmatter parse_toml = true -# Maximum file size to process (in bytes) -max_file_size = 10485760 # 10MB +max_file_size = 10485760