Skip to content

Commit 68981ee

Browse files
staging-devin-ai-integration[bot]streamkit-devinstreamer45
authored
feat(plugins): wire asset unregistration, fix restart persistence, add bundle convention docs (#256)
* feat: plugin-driven asset registration system Replace hardcoded slint asset type with a generic plugin asset registration mechanism. Plugins declare asset types in their plugin.yml manifests, and the server provides generic CRUD endpoints parameterized by type_id. Backend: - Extend PluginManifest with optional assets section (PluginAssetSpec) - Add PluginAssetRegistry with thread-safe registration/lookup - Generic handlers: list, upload, delete, serve, update for plugin assets - Discovery endpoint GET /api/v1/asset-types returns core + plugin types - Add PluginAsset, AssetContentKind, AssetTypeInfo to streamkit-api - Register slint plugin assets via plugins/native/slint/plugin.yml - Move slint files to samples/slint/system/ and update pipeline refs - Update permissions to include plugin asset paths Frontend: - Add useAssetTypes() hook for dynamic type discovery - Add generic usePluginAssets(typeId) CRUD hooks - Add React Query hooks to imageAssets.ts and fontAssets.ts - New AssetCard component with discriminated UnifiedAsset union - New data-driven AssetLibrary replacing AudioAssetLibrary - Update DesignView drag-drop for plugin asset node creation - Update ControlPane to use unified AssetLibrary Signed-off-by: Devin AI <devin@cognition.ai> Signed-off-by: StreamKit Devin <devin@streamkit.dev> Co-Authored-By: Claudio Costa <cstcld91@gmail.com> * fix: remove unused files and exports flagged by knip - Delete AudioAssetCard.tsx and AudioAssetLibrary.tsx (replaced by AssetCard/AssetLibrary) - Remove unused getAssetId export and AssetType type export from AssetCard - Remove unused fetchPluginAssetContent and updatePluginAssetContent (Phase 2) Signed-off-by: Devin AI <devin@cognition.ai> Signed-off-by: StreamKit Devin <devin@streamkit.dev> Co-Authored-By: Claudio Costa <cstcld91@gmail.com> * style: format AssetCard.tsx with prettier Signed-off-by: Devin AI <devin@cognition.ai> Signed-off-by: StreamKit Devin <devin@streamkit.dev> Co-Authored-By: Claudio Costa <cstcld91@gmail.com> * fix: remove unused AssetType and getId definitions Flagged by ESLint no-unused-vars after removing their exports. Signed-off-by: Devin AI <devin@cognition.ai> Signed-off-by: StreamKit Devin <devin@streamkit.dev> Co-Authored-By: Claudio Costa <cstcld91@gmail.com> * fix: address review findings — bundle optional, drag guard, path traversal - Make PluginManifest.bundle Optional so local plugin.yml files (which lack a bundle section) can be deserialized correctly. - Prevent drag-drop for image/font assets that don't have drop handlers yet, avoiding broken node creation on the canvas. - Add path traversal guard (../ check) to delete_handler for defense-in-depth consistency with serve_handler and update_handler. Signed-off-by: Devin AI <devin@cognition.ai> Signed-off-by: StreamKit Devin <devin@streamkit.dev> Co-Authored-By: Claudio Costa <cstcld91@gmail.com> * test: add manifest deserialization tests for bundle optionality Regression tests for the bug where local plugin manifests (without a bundle section) failed to deserialize. Tests both with and without the bundle field to prevent recurrence. Signed-off-by: Devin AI <devin@cognition.ai> Signed-off-by: StreamKit Devin <devin@streamkit.dev> Co-Authored-By: Claudio Costa <cstcld91@gmail.com> * fix: enforce max_size_bytes limit in update_handler The PUT endpoint for in-place text file updates was missing the per-asset-type size check, allowing writes up to the 100 MiB outer body limit. Now rejects bodies exceeding max_size_bytes before writing. Signed-off-by: Devin AI <devin@cognition.ai> Signed-off-by: StreamKit Devin <devin@streamkit.dev> Co-Authored-By: Claudio Costa <cstcld91@gmail.com> * fix: address review findings — security, correctness, and cleanup Security: - Add validate_file_in_directory to serve_handler (symlink bypass). - Validate type_id is [a-zA-Z0-9_-]+ and system_dir has no '..' at registration time to prevent path injection from plugin manifests. - Reject '.' as a filename in validate_filename. - Enforce max_size_bytes in update_handler (PUT bypass of per-type limit). Correctness: - Fix plugin delete from 'All' view: call deletePluginAsset directly with the asset's own type_id instead of pre-parameterized hook that captures empty string when no plugin type is selected. - Upload handler returns 201 Created instead of 200. - copy-plugins-native now copies plugin.yml alongside .so files so read_local_plugin_manifest can find manifests in the flat layout. - read_local_plugin_manifest searches for {stem}.plugin.yml in addition to plugin.yml in the same directory. - Add viewer role access to system plugin assets (samples/*/system/*). Cleanup: - Remove unused AssetContentKind from API types (naming inconsistency with backend AssetContentType; editable bool already covers the need). - Replace 'as never' cast with type-safe appendIfMatches helper. - Expand permissions.rs glob comment explaining intentional breadth. - Skip core asset queries when filtered to a different type (enabled flag). - Remove unused useDeletePluginAsset hook. - Add TODO comment for multi-plugin 'All' view limitation. Signed-off-by: Devin AI <devin@cognition.ai> Signed-off-by: StreamKit Devin <devin@streamkit.dev> Co-Authored-By: Claudio Costa <cstcld91@gmail.com> * fix: address review findings — security, correctness, and cleanup Security: - Add validate_file_in_directory to serve_handler (symlink bypass). - Validate type_id is [a-zA-Z0-9_-]+ and system_dir has no '..' at registration time to prevent path injection from plugin manifests. - Reject '.' as a filename in validate_filename. - Enforce max_size_bytes in update_handler (PUT bypass of per-type limit). Correctness: - Fix plugin delete from 'All' view: call deletePluginAsset directly with the asset's own type_id instead of pre-parameterized hook that captures empty string when no plugin type is selected. - Upload handler returns 201 Created instead of 200. - copy-plugins-native now copies plugin.yml alongside .so files so read_local_plugin_manifest can find manifests in the flat layout. - read_local_plugin_manifest searches for {stem}.plugin.yml in addition to plugin.yml in the same directory. - Add viewer role access to system plugin assets (samples/*/system/*). Cleanup: - Remove unused AssetContentKind from API types (naming inconsistency with backend AssetContentType; editable bool already covers the need). - Replace 'as never' cast with type-safe appendIfMatches helper. - Expand permissions.rs glob comment explaining intentional breadth. - Skip core asset queries when filtered to a different type (enabled flag). - Remove unused useDeletePluginAsset hook. - Add TODO comment for multi-plugin 'All' view limitation. Signed-off-by: Devin AI <devin@cognition.ai> Signed-off-by: StreamKit Devin <devin@streamkit.dev> Co-Authored-By: Claudio Costa <cstcld91@gmail.com> * style: format TypeScript files Signed-off-by: Devin AI <devin@cognition.ai> Signed-off-by: StreamKit Devin <devin@streamkit.dev> Co-Authored-By: Claudio Costa <cstcld91@gmail.com> * fix: reject empty type_id and update TS bundle type to allow null - Add is_empty() check before Iterator::all to prevent vacuous truth accepting empty type_id at registration time. - Update TypeScript PluginManifest.bundle to PluginBundle | null to match the Rust Option<PluginBundle> change. - Add optional chaining to bundle.size_bytes access in MarketplacePanels. Signed-off-by: Devin AI <devin@cognition.ai> Signed-off-by: StreamKit Devin <devin@streamkit.dev> Co-Authored-By: Claudio Costa <cstcld91@gmail.com> * fix: harden plugin asset security, wire marketplace registration, multi-plugin All view Security: - Reject absolute paths and path-traversal in system_dir using Path::components() instead of substring matching (C1, S3). - Add validate_file_in_directory to upload_handler for defense-in-depth (C2). - Post-sanitization guard in sanitize_filename rejects '.' and '..' (S2). - Change scan_directory param from &PathBuf to &Path (clippy lint). Correctness: - Wire PluginAssetRegistry into marketplace installer so plugins installed at runtime register their asset types immediately (not only at startup). - Use useQueries to fetch all plugin types in the 'All' view, not just the first one (S4). - Remove unused usePluginAssets hook after useQueries migration. - Downgrade pluginAssets.ts fetch logging from info to debug. Testing: - Add 29 unit tests covering sanitize_filename, validate_filename, validate_file_in_directory, and register validation (empty type_id, traversal type_id, absolute system_dir, dotdot system_dir, dotdot substring in dirname, valid spec). Signed-off-by: Devin AI <devin@cognition.ai> Signed-off-by: StreamKit Devin <devin@streamkit.dev> Co-Authored-By: Claudio Costa <cstcld91@gmail.com> * fix: TOCTOU in delete/serve/update, upload validation, node_kind, memoization Security: - Eliminate TOCTOU race in delete/serve/update handlers: drop exists() pre-check, map io::ErrorKind::NotFound inside validate_file_in_directory. - Fix upload_handler: validate parent directory (canonicalize user_dir) instead of non-existent file_path (canonicalize fails on missing files). - serve_handler now validates file extension against registered asset type. - sanitize_filename('') now returns '_invalid_' instead of empty string. - Document icon_hint Option<String> vs String asymmetry. Correctness: - Marketplace installer uses expected_kind (from manifest) instead of hardcoded 'plugin::native::' prefix for asset registration node_kind. Performance: - Stabilize pluginQueries dependency in AssetLibrary via useRef shallow comparison — prevents cascading useMemo recomputation every render. - useUploadPluginAsset short-circuits with rejection when typeId is empty. Logging: - assetTypes.ts: logger.info → logger.debug on fetch calls. Signed-off-by: Devin AI <devin@cognition.ai> Signed-off-by: StreamKit Devin <devin@streamkit.dev> Co-Authored-By: Claudio Costa <cstcld91@gmail.com> * fix: clippy CI — allow unwrap_used in tests, fix redundant closure Signed-off-by: Devin AI <devin@cognition.ai> Signed-off-by: StreamKit Devin <devin@streamkit.dev> Co-Authored-By: Claudio Costa <cstcld91@gmail.com> * fix: lower update_handler DefaultBodyLimit from 100 MiB to 10 MiB The PUT route for text asset updates buffers body: String in memory before checking per-type max_size_bytes (typically 1 MiB). The 100 MiB outer limit allowed authenticated users to force large allocations that would be immediately rejected. Lower to 10 MiB to bound memory while still being generous for text content editing. Signed-off-by: Devin AI <devin@cognition.ai> Signed-off-by: StreamKit Devin <devin@streamkit.dev> Co-Authored-By: Claudio Costa <cstcld91@gmail.com> * feat(plugins): wire unregister_plugin into plugin unload/delete path Call plugin_asset_registry.unregister_plugin() when a plugin is deleted via the HTTP API (delete_plugin_handler) and when an existing plugin is unloaded before marketplace re-installation (load_plugin). This ensures CRUD endpoints stop serving stale asset types after a plugin is removed. User-uploaded files are intentionally left in place for potential re-install. Remove the #[allow(dead_code)] annotation from unregister_plugin now that it is wired in. Add unit tests for register-then-unregister flows: - unregister_removes_all_types_for_plugin - unregister_leaves_other_plugins_intact Add tests for write_manifest_yml: - write_manifest_yml_creates_yaml_next_to_entrypoint - write_manifest_yml_nested_entrypoint Closes #255 Signed-off-by: StreamKit Devin <devin@streamkit.dev> Co-Authored-By: Claudio Costa <cstcld91@gmail.com> * docs(plugins): add plugin bundle convention design doc Add architecture/plugin-bundle-convention.md proposing a unified directory layout for plugins (local and marketplace). The doc covers: - Current state (bare .so vs extracted bundle directories) - Proposed convention (plugin.yml + entrypoint + optional extras) - Incremental migration path (backward-compatible, no breaking changes) - Already-done steps and future work items Update publishing-plugins.md to reference the new convention and mention that plugin.yml is written automatically during marketplace installation. Add the new doc to the sidebar in astro.config.mjs. Ref #254 Signed-off-by: StreamKit Devin <devin@streamkit.dev> Co-Authored-By: Claudio Costa <cstcld91@gmail.com> * fix(plugins): use active record plugin_id for asset unregistration For marketplace plugins, use record.plugin_id (which matches manifest.id used during registration) instead of summary.original_kind. This avoids a silent mismatch if a plugin author sets a manifest id that differs from the binary's reported kind. For non-marketplace plugins, fall back to original_kind which by convention equals the manifest id. Signed-off-by: StreamKit Devin <devin@streamkit.dev> Co-Authored-By: Claudio Costa <cstcld91@gmail.com> * style: format unregister_id binding Signed-off-by: StreamKit Devin <devin@streamkit.dev> Co-Authored-By: Claudio Costa <cstcld91@gmail.com> * feat(plugins): add install-plugin recipe for single-plugin build+install Adds `just install-plugin <name>` which builds a single native plugin and copies the .so + plugin.yml to .plugins/native/. Idempotent — safe to re-run after code changes. Usage: just install-plugin slint just install-plugin pocket-tts Signed-off-by: StreamKit Devin <devin@streamkit.dev> Co-Authored-By: Claudio Costa <cstcld91@gmail.com> * chore(plugins): bump all native plugin versions to 0.2.0 Minor version bump for all 11 official native plugins (Cargo.toml and plugin.yml) to trigger marketplace rebuilds. Regenerate marketplace/official-plugins.json with updated versions. Signed-off-by: StreamKit Devin <devin@streamkit.dev> Co-Authored-By: Claudio Costa <cstcld91@gmail.com> * refactor(api): replace stringly-typed AssetTypeInfo.source with enum Introduce AssetTypeSource enum with Core and Plugin variants instead of magic "core"/"plugin" string values. Updated all server usages and regenerated TypeScript types. Also normalise core type_id naming to singular form (images→image, fonts→font) for consistency with plugin type_ids. Adds unit tests for type_id collision detection, max_size_bytes cap, and registered_type_ids sync cache. Signed-off-by: StreamKit Devin <devin@streamkit.dev> Co-Authored-By: Claudio Costa <cstcld91@gmail.com> * refactor(api): replace stringly-typed AssetTypeInfo.source with enum Introduce AssetTypeSource enum with Core and Plugin variants instead of magic "core"/"plugin" string values. Updated all server usages and regenerated TypeScript types. Also normalise core type_id naming to singular form (images→image, fonts→font) for consistency with plugin type_ids. Adds unit tests for type_id collision detection, max_size_bytes cap, and registered_type_ids sync cache. Signed-off-by: StreamKit Devin <devin@streamkit.dev> Co-Authored-By: Claudio Costa <cstcld91@gmail.com> * fix(permissions): replace broad plugin asset wildcards with dynamic patterns Remove the overly broad `samples/*/system/*` and `samples/*/user/*` wildcards from default role permissions. Instead, dynamically augment permissions at request time with patterns scoped to actually-registered plugin type IDs via PluginAssetRegistry::registered_type_ids(). This prevents theoretical access to unregistered type directories while maintaining full access to registered plugin asset types. Adds unit test verifying default roles contain no broad wildcards. Signed-off-by: StreamKit Devin <devin@streamkit.dev> Co-Authored-By: Claudio Costa <cstcld91@gmail.com> * fix(marketplace): guard against empty entrypoint in write_manifest_yml Add anyhow::ensure! check that manifest.entrypoint is non-empty before writing plugin.yml. An empty entrypoint would cause .parent() to traverse above the bundle directory. Signed-off-by: StreamKit Devin <devin@streamkit.dev> Co-Authored-By: Claudio Costa <cstcld91@gmail.com> * feat(plugins): use bundle directory layout for install-plugin recipe install-plugin now creates .plugins/native/<name>/ directories with the compiled library and plugin.yml manifest inside, matching the marketplace bundle convention. Also updates the plugin loader to scan one level of subdirectories for bundle-layout plugins while maintaining backward compatibility with flat .so files at the top level. Signed-off-by: StreamKit Devin <devin@streamkit.dev> Co-Authored-By: Claudio Costa <cstcld91@gmail.com> * fix(ui): improve AssetLibrary robustness and dynamic type filters - Aggregate plugin query loading/error states into UI indicators - Add typeFilter check for plugin entries in buildUnifiedItems - Normalise type filter values to match singular type_ids (image, font) - Generate type filter buttons dynamically from asset-types API response - Make format dropdown show counts per format from actual asset data - Extract aggregateQueryState helper to reduce hook complexity Signed-off-by: StreamKit Devin <devin@streamkit.dev> Co-Authored-By: Claudio Costa <cstcld91@gmail.com> * fix(permissions): derive plugin asset patterns from actual paths Address three Devin Review findings: - Permission patterns now use the actual system_dir/user_dir from the registry instead of hardcoded samples/{type_id}/ convention. This ensures correct behaviour when a plugin uses a custom directory layout. - Extract shared helpers (resolve_role_name, augment_plugin_asset_permissions) so both get_permissions and get_role_and_permissions apply the same plugin asset augmentation consistently. - copy-plugins-native recipe now also copies pocket-tts plugin.yml for consistency with the main plugin loop. Adds unit test verifying custom system_dir produces correct permission patterns. Signed-off-by: StreamKit Devin <devin@streamkit.dev> Co-Authored-By: Claudio Costa <cstcld91@gmail.com> * fix(permissions): derive plugin asset patterns from actual paths Address three Devin Review findings: - Permission patterns now use the actual system_dir/user_dir from the registry instead of hardcoded samples/{type_id}/ convention. This ensures correct behaviour when a plugin uses a custom directory layout. - Extract shared helpers (resolve_role_name, augment_plugin_asset_permissions) so both get_permissions and get_role_and_permissions apply the same plugin asset augmentation consistently. - copy-plugins-native recipe now also copies pocket-tts plugin.yml for consistency with the main plugin loop. Adds unit test verifying custom system_dir produces correct permission patterns. Signed-off-by: StreamKit Devin <devin@streamkit.dev> Co-Authored-By: Claudio Costa <cstcld91@gmail.com> * fix(ui): reset format filter on type switch, fix drag affordance on non-draggable assets - Reset formatFilter to 'all' when switching asset type tabs so stale format selections from a previous type don't hide all results. - Make AssetCard drag cursor conditional: only show grab cursor and set draggable when the item actually supports drag-and-drop (audio and plugin assets). Image/font cards no longer misleadingly appear draggable. - Thread isDraggable predicate through AssetLibrary → AssetCard. - Fix user_dir derivation edge case: when a plugin sets a single-component system_dir (e.g. 'system'), parent() returns an empty path; now falls back to the default samples/{type_id}/user instead of creating a bare 'user' directory at the project root. - Add unit test for the single-component system_dir edge case. Signed-off-by: StreamKit Devin <devin@streamkit.dev> Co-Authored-By: Claudio Costa <cstcld91@gmail.com> * fix(ui): reset format filter on type switch, fix drag affordance on non-draggable assets - Reset formatFilter to 'all' when switching asset type tabs so stale format selections from a previous type don't hide all results. - Make AssetCard drag cursor conditional: only show grab cursor and set draggable when the item actually supports drag-and-drop (audio and plugin assets). Image/font cards no longer misleadingly appear draggable. - Thread isDraggable predicate through AssetLibrary → AssetCard. - Fix user_dir derivation edge case: when a plugin sets a single-component system_dir (e.g. 'system'), parent() returns an empty path; now falls back to the default samples/{type_id}/user instead of creating a bare 'user' directory at the project root. - Add unit test for the single-component system_dir edge case. Signed-off-by: StreamKit Devin <devin@streamkit.dev> Co-Authored-By: Claudio Costa <cstcld91@gmail.com> --------- Signed-off-by: Devin AI <devin@cognition.ai> Signed-off-by: StreamKit Devin <devin@streamkit.dev> Co-authored-by: StreamKit Devin <devin@streamkit.dev> Co-authored-by: Claudio Costa <cstcld91@gmail.com>
1 parent 5020a9c commit 68981ee

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

58 files changed

+3608
-666
lines changed

apps/skit/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ pub mod marketplace_security;
1616
pub mod moq_gateway;
1717
pub mod mse_gateway;
1818
pub mod permissions;
19+
pub mod plugin_assets;
1920
pub mod plugin_paths;
2021
pub mod plugin_records;
2122
pub mod plugins;

apps/skit/src/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ mod marketplace_security;
4444
mod moq_gateway;
4545
mod mse_gateway;
4646
mod permissions;
47+
mod plugin_assets;
4748
mod plugin_paths;
4849
mod plugin_records;
4950
mod plugins;

apps/skit/src/marketplace.rs

Lines changed: 109 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -225,10 +225,64 @@ pub struct PluginManifest {
225225
pub homepage: Option<String>,
226226
pub repository: Option<String>,
227227
pub entrypoint: String,
228-
pub bundle: PluginBundle,
228+
/// Marketplace bundle info. Required for marketplace-distributed plugins;
229+
/// absent for local-only plugins that ship alongside their `.so`.
230+
#[serde(default)]
231+
pub bundle: Option<PluginBundle>,
229232
pub compatibility: Option<PluginCompatibility>,
230233
#[serde(default)]
231234
pub models: Vec<ModelSpec>,
235+
/// Asset types registered by this plugin.
236+
///
237+
/// When a plugin declares asset types, the server creates generic CRUD
238+
/// endpoints under `/api/v1/assets/plugin/{type_id}` and includes them
239+
/// in the `GET /api/v1/asset-types` discovery response.
240+
#[serde(default)]
241+
pub assets: Vec<PluginAssetSpec>,
242+
}
243+
244+
/// An asset type declared by a plugin in its manifest.
245+
///
246+
/// Each spec causes the server to register generic CRUD endpoints and
247+
/// include the type in the asset-type discovery API.
248+
#[derive(Debug, Clone, Serialize, Deserialize)]
249+
pub struct PluginAssetSpec {
250+
/// URL-safe identifier, unique per plugin (e.g. `slint`).
251+
pub type_id: String,
252+
/// Human-readable label for the UI (e.g. "Slint Files").
253+
pub label: String,
254+
/// Allowed file extensions (e.g. `["slint"]`).
255+
pub extensions: Vec<String>,
256+
/// Maximum upload size in bytes (default: 1 MiB).
257+
#[serde(default = "default_max_asset_size")]
258+
pub max_size_bytes: usize,
259+
/// Whether the file content is text (editable) or binary.
260+
#[serde(default = "default_asset_content_type")]
261+
pub content_type: AssetContentType,
262+
/// UI icon hint (e.g. `code`, `music`, `image`, `type`, `file`).
263+
pub icon_hint: Option<String>,
264+
/// Which node parameter references this asset on drag-drop
265+
/// (e.g. `slint_file`).
266+
pub node_param: Option<String>,
267+
/// Directory (relative to server CWD) containing bundled system assets.
268+
/// User uploads go to a sibling `user/` directory derived from this path.
269+
pub system_dir: Option<String>,
270+
}
271+
272+
/// Whether a plugin asset is text (editable) or binary.
273+
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
274+
#[serde(rename_all = "lowercase")]
275+
pub enum AssetContentType {
276+
Text,
277+
Binary,
278+
}
279+
280+
const fn default_max_asset_size() -> usize {
281+
1_048_576 // 1 MiB
282+
}
283+
284+
const fn default_asset_content_type() -> AssetContentType {
285+
AssetContentType::Binary
232286
}
233287

234288
#[derive(Debug, Clone, Serialize, Deserialize)]
@@ -855,4 +909,58 @@ abcd";
855909
let err = decode_base64_line("!!!").unwrap_err();
856910
assert!(err.to_string().contains("Base64 decode failed"));
857911
}
912+
913+
// ==================== PluginManifest Deserialization Tests ====================
914+
915+
/// Local plugin manifests (e.g. `plugins/native/slint/plugin.yml`) omit
916+
/// the `bundle` section because they aren't distributed via marketplace.
917+
/// Ensure deserialization succeeds with `bundle` absent.
918+
#[test]
919+
fn deserialize_manifest_without_bundle() {
920+
let yaml = r#"
921+
schema_version: 1
922+
id: slint
923+
version: "0.1.0"
924+
node_kind: "plugin::native::slint"
925+
kind: native
926+
entrypoint: libslint_plugin.so
927+
assets:
928+
- type_id: slint
929+
label: "Slint Files"
930+
extensions: [slint]
931+
max_size_bytes: 1048576
932+
content_type: text
933+
icon_hint: code
934+
node_param: slint_file
935+
system_dir: samples/slint/system
936+
"#;
937+
let manifest: PluginManifest = serde_saphyr::from_str(yaml).unwrap();
938+
assert_eq!(manifest.id, "slint");
939+
assert!(manifest.bundle.is_none());
940+
assert_eq!(manifest.assets.len(), 1);
941+
assert_eq!(manifest.assets[0].type_id, "slint");
942+
assert_eq!(manifest.assets[0].content_type, AssetContentType::Text);
943+
}
944+
945+
/// Marketplace manifests include `bundle`; deserialization should populate it.
946+
#[test]
947+
fn deserialize_manifest_with_bundle() {
948+
let yaml = r#"
949+
schema_version: 1
950+
id: test-plugin
951+
version: "1.0.0"
952+
node_kind: "plugin::native::test"
953+
kind: native
954+
entrypoint: libtest.so
955+
bundle:
956+
url: "https://example.com/bundle.tar.zst"
957+
sha256: "abc123"
958+
"#;
959+
let manifest: PluginManifest = serde_saphyr::from_str(yaml).unwrap();
960+
assert_eq!(manifest.id, "test-plugin");
961+
let bundle = manifest.bundle.unwrap();
962+
assert_eq!(bundle.url, "https://example.com/bundle.tar.zst");
963+
assert_eq!(bundle.sha256, "abc123");
964+
assert!(manifest.assets.is_empty());
965+
}
858966
}

0 commit comments

Comments
 (0)