//! Authored Rust -> PDK -> validated Component -> fresh Wasmtime Store -> OwnerHost. //! The deterministic owner replaces storage/network, not the execution adapter; //! transaction/lease/live policy guarantees are tested by the domain owner suites. use async_trait::async_trait; use manifest::plugin::{self, PluginPackageLimits, PluginSourceKind}; use server_api::*; use std::{ collections::BTreeMap, fs, path::Path, process::Command, sync::{Arc, Mutex, OnceLock}, time::{Duration, SystemTime, UNIX_EPOCH}, }; use tokio::time::Instant; use yoi_plugin_runtime::{ host::{CancellationToken, Grants, InvocationContext}, owner::{OwnerHost, PluginHostOwner}, *, }; fn component() -> &'static [u8] { static COMPONENT: OnceLock> = OnceLock::new(); COMPONENT.get_or_init(|| { let target = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../target/plugin-service-fixture/guest"); let output = Command::new(env!("CARGO")) .args([ "build", "--locked", "--offline", "--release", "--target", "wasm32-unknown-unknown", "--manifest-path", ]) .arg(Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/service-guest/Cargo.toml")) .arg("--target-dir") .arg(&target) .output() .unwrap(); fs::create_dir_all(target.parent().unwrap()).unwrap(); fs::write( target.parent().unwrap().join("guest-build.log"), &output.stderr, ) .unwrap(); assert!( output.status.success(), "Service fixture requires wasm32 std + lld on PATH: {}", String::from_utf8_lossy(&output.stderr) ); let core = fs::read(target.join("wasm32-unknown-unknown/release/service_test_guest.wasm")) .unwrap(); let bytes = wit_component::ComponentEncoder::default() .module(&core) .unwrap() .validate(true) .encode() .unwrap(); fs::write( target.parent().unwrap().join("service.component.wasm"), &bytes, ) .unwrap(); bytes }) } const MANIFEST: &str = include_str!("service-guest/plugin.toml"); fn package(manifest: &str) -> Result { package_bytes(manifest, component()) } fn package_bytes( manifest: &str, bytes: &[u8], ) -> Result { let target = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../target/plugin-service-fixture"); fs::create_dir_all(&target).unwrap(); let dir = tempfile::tempdir_in(target).unwrap(); fs::write(dir.path().join("plugin.toml"), manifest).unwrap(); fs::write(dir.path().join("plugin.component.wasm"), bytes).unwrap(); let materialized = plugin::read_plugin_directory( dir.path(), PluginSourceKind::Explicit, &PluginPackageLimits::default(), )?; plugin::pack_plugin_package(&materialized, &PluginPackageLimits::default()) } fn compile(ex: &Executor) -> CompiledService { let p = package(MANIFEST).unwrap(); ex.compile_service( &p.bytes, &p.package_digest, &p.artifact_digest, p.artifact_size, ) .unwrap() } #[derive(Clone, Copy, Default)] enum Fault { #[default] None, HttpUnknown, DataUnknown, HttpWait, DataWait, LookupUnavailable, LookupExpired, ReadDenied, } #[derive(Default)] struct State { calls: Vec, writes: usize, sends: usize, receipts: BTreeMap, http: BTreeMap, } struct Owner { state: Mutex, fault: Fault, waiting: tokio::sync::Notify, } impl Owner { fn new(fault: Fault) -> Arc { Arc::new(Self { state: Mutex::new(State::default()), fault, waiting: tokio::sync::Notify::new(), }) } } fn rejected(code: PluginHostErrorCode, effect: PluginEffectObservation) -> PluginHostError { PluginHostError { code, effect, diagnostic_id: "test".into(), } } #[async_trait] impl PluginHostOwner for Owner { async fn call( &self, request: PluginHostRequest, ) -> Result { if (matches!(self.fault, Fault::HttpWait) && matches!(request, PluginHostRequest::Send { .. })) || (matches!(self.fault, Fault::DataWait) && matches!(request, PluginHostRequest::DataMutate { .. })) { self.waiting.notify_one(); return std::future::pending().await; } let mut s = self.state.lock().unwrap(); s.calls.push(request.clone()); Ok(match request { PluginHostRequest::Begin { .. } => PluginHostResponse::Invocation { invocation_id: "stable-service-invocation".into(), expires_at_ms: SystemTime::now() .duration_since(UNIX_EPOCH) .unwrap() .as_millis() as i64 + 60_000, }, PluginHostRequest::Authorize { .. } => PluginHostResponse::Authorized, PluginHostRequest::Log { .. } => PluginHostResponse::Logged, PluginHostRequest::ResolveTicket { ticket_ref, .. } => { assert_eq!(ticket_ref, "T-796"); PluginHostResponse::TicketResolved { ticket_id: "canonical-ticket".into(), } } PluginHostRequest::ReadTicket { .. } => { if matches!(self.fault, Fault::ReadDenied) { return Err(rejected( PluginHostErrorCode::Denied, PluginEffectObservation::NotDispatched, )); } PluginHostResponse::TicketRead { snapshot: PluginTicketSnapshot { title: "note".into(), case_state: PluginTicketCaseState::Open, content_digest: "digest".into(), body: "body".into(), truncated: false, }, } } PluginHostRequest::DataLookupMutation { operation_key, .. } => { if matches!(self.fault, Fault::LookupUnavailable) { return Err(rejected( PluginHostErrorCode::Unavailable, PluginEffectObservation::NotDispatched, )); } PluginHostResponse::DataMutationLookup { outcome: if matches!(self.fault, Fault::LookupExpired) { PluginDataMutationLookup::Expired } else { s.receipts .get(&operation_key) .cloned() .map(PluginDataMutationLookup::Committed) .unwrap_or(PluginDataMutationLookup::NotRecorded) }, } } PluginHostRequest::DataGet { .. } => PluginHostResponse::DataCell { cell: PluginDataCell::NeverWritten, }, PluginHostRequest::DataMutate { operation_key, .. } => { if matches!(self.fault, Fault::DataUnknown) { return Err(rejected( PluginHostErrorCode::Indeterminate, PluginEffectObservation::Indeterminate, )); } s.writes += 1; let receipt = PluginDataReceipt { mutation_id: "write-1".into(), result_digest: "result".into(), }; assert!(s.receipts.insert(operation_key, receipt.clone()).is_none()); PluginHostResponse::DataMutated { receipt } } PluginHostRequest::Open { .. } => PluginHostResponse::Opened, PluginHostRequest::Send { request, .. } => { if matches!(self.fault, Fault::HttpUnknown) { return Err(rejected( PluginHostErrorCode::Indeterminate, PluginEffectObservation::Indeterminate, )); } let response = if let Some(response) = s.http.get(&request.effect_key) { response.clone() } else { s.sends += 1; let response = PluginHttpResponse { status: 200, headers: vec![], body: b"ok".to_vec(), }; s.http.insert(request.effect_key, response.clone()); response }; PluginHostResponse::Response { response } } _ => { return Err(rejected( PluginHostErrorCode::Unsupported, PluginEffectObservation::NotDispatched, )); } }) } } fn context(c: &CompiledService) -> InvocationContext { let namespace = ("notes".into(), "note".into(), "1".into()); InvocationContext { workspace: "workspace".into(), installation: "installation".into(), plan: "plan".into(), subject: "worker".into(), invocation_id: "ignored-before-begin".into(), artifact: c.identity().clone(), tool_name: "ticket_events".into(), grants: Grants { logging: true, connection_slots: ["api".into()].into(), ticket_events: true, ticket_read: true, ticket_data_read: [namespace.clone()].into(), ticket_data_write: [namespace].into(), }, deadline: Instant::now() + Duration::from_secs(10), cancellation: CancellationToken::new(), } } async fn host(c: &CompiledService, owner: Arc) -> Arc { Arc::new( OwnerHost::begin( owner, context(c), PluginConsumer::Consumer { consumer_id: "consumer".into(), event_id: "event".into(), export_name: "ticket_events".into(), }, ) .await .unwrap(), ) } fn envelope() -> PluginTicketEventEnvelope { PluginTicketEventEnvelope { event_id: "event".into(), event_type: PluginTicketEventType::Changed, version: 1, occurred_at: "2026-10-30T00:00:00Z".into(), ticket_id: "T-796".into(), domain_event_ref: "domain-event".into(), content_digest: "digest".into(), correlation_id: "correlation".into(), causation_id: None, attempt: 1, } } #[tokio::test] async fn authored_service_recovers_with_receipt_lookup_before_rebuilding_a_mutation() { let ex = Executor::new(Limits::default()).unwrap(); let c = compile(&ex); let owner = Owner::new(Fault::None); let host = host(&c, owner.clone()).await; assert!(matches!( ex.service_start( &c, host.context().clone(), host.clone(), "ticket_events", r#"{"mode":"normal"}"# ) .await .outcome, Ok(ServiceOutput::Started) )); for attempt in 1..=2 { let mut e = envelope(); e.attempt = attempt; let result = ex .service_event( &c, host.context().clone(), host.clone(), "ticket_events", &e, r#"{"mode":"normal"}"#, ) .await; assert!( matches!( result.outcome, Ok(ServiceOutput::Event(PluginServiceEventResult::Ack)) ), "{result:?}" ); assert!(matches!( result.effect, host::errors::EffectObservation::ResponseObserved )); } assert!(matches!( ex.service_stop( &c, host.context().clone(), host.clone(), "ticket_events", "normal" ) .await .outcome, Ok(ServiceOutput::Stopped) )); let state = owner.state.lock().unwrap(); assert_eq!(state.writes, 1); assert_eq!(state.sends, 1); assert_eq!( state .calls .iter() .filter(|r| matches!(r, PluginHostRequest::DataGet { .. })) .count(), 1 ); assert_eq!( state .calls .iter() .filter(|r| matches!(r, PluginHostRequest::DataLookupMutation { .. })) .count(), 2 ); } #[tokio::test] async fn lifecycle_hooks_allow_logging_but_never_open_resolve_read_data_or_http() { let ex = Executor::new(Limits::default()).unwrap(); let c = compile(&ex); let owner = Owner::new(Fault::None); let host = host(&c, owner.clone()).await; for mode in ["open", "resolve", "read", "data", "http"] { let config = format!(r#"{{"mode":"{mode}"}}"#); let start = ex .service_start( &c, host.context().clone(), host.clone(), "ticket_events", &config, ) .await; let stop = ex .service_stop( &c, host.context().clone(), host.clone(), "ticket_events", mode, ) .await; assert!( matches!(start.outcome, Err(ExecutionError::Trap(_))), "{mode}: {start:?}" ); assert!( matches!(stop.outcome, Err(ExecutionError::Trap(_))), "{mode}: {stop:?}" ); } let s = owner.state.lock().unwrap(); assert!(s.calls.iter().all(|r| matches!( r, PluginHostRequest::Begin { .. } | PluginHostRequest::Authorize { .. } ))); } #[tokio::test] async fn guest_ack_cannot_hide_unknown_http_data_or_failed_receipt_lookup() { let ex = Executor::new(Limits::default()).unwrap(); let c = compile(&ex); for fault in [ Fault::HttpUnknown, Fault::DataUnknown, Fault::LookupUnavailable, Fault::LookupExpired, ] { let host = host(&c, Owner::new(fault)).await; let result = ex .service_event( &c, host.context().clone(), host.clone(), "ticket_events", &envelope(), r#"{"mode":"swallow-unknown"}"#, ) .await; assert!( matches!( result.outcome, Ok(ServiceOutput::Event(PluginServiceEventResult::Ack)) ), "{result:?}" ); assert!( matches!( result.effect, host::errors::EffectObservation::Indeterminate ), "{result:?}" ); } } #[tokio::test] async fn cancellation_during_effect_wait_preserves_unknown_observation() { let ex = Executor::new(Limits::default()).unwrap(); let c = compile(&ex); for fault in [Fault::HttpWait, Fault::DataWait] { let owner = Owner::new(fault); let host = host(&c, owner.clone()).await; let cancellation = host.context().cancellation.clone(); let e = envelope(); let (result, ()) = tokio::time::timeout(Duration::from_secs(5), async { tokio::join!( ex.service_event( &c, host.context().clone(), host.clone(), "ticket_events", &e, r#"{"mode":"normal"}"# ), async { owner.waiting.notified().await; cancellation.cancel(); } ) }) .await .expect("effect was not dispatched"); assert!( matches!(result.outcome, Err(ExecutionError::Cancelled)), "{result:?}" ); assert!( matches!( result.effect, host::errors::EffectObservation::Indeterminate ), "{result:?}" ); } } #[tokio::test] async fn deleted_metadata_never_resolves_a_ticket_and_live_read_denial_remains_a_failure() { let ex = Executor::new(Limits::default()).unwrap(); let c = compile(&ex); let owner = Owner::new(Fault::ReadDenied); let host = host(&c, owner.clone()).await; let result = ex .service_event( &c, host.context().clone(), host.clone(), "ticket_events", &envelope(), r#"{"mode":"normal"}"#, ) .await; assert!(matches!( result.outcome, Ok(ServiceOutput::Event( PluginServiceEventResult::RetryAfter { .. } )) )); let before = owner.state.lock().unwrap().calls.len(); let mut e = envelope(); e.event_type = PluginTicketEventType::Deleted; e.content_digest.clear(); let result = ex .service_event( &c, host.context().clone(), host, "ticket_events", &e, r#"{"mode":"normal"}"#, ) .await; assert!(matches!( result.outcome, Ok(ServiceOutput::Event(PluginServiceEventResult::Ack)) )); assert!( owner.state.lock().unwrap().calls[before..] .iter() .all(|r| matches!(r, PluginHostRequest::Authorize { .. })) ); } #[test] fn service_manifest_rejects_old_world_subscription_and_grant_mismatches() { package(MANIFEST).unwrap(); for bad in [ MANIFEST.replace("@2.2.0", "@2.1.0"), MANIFEST.replace("ticket_changed", "arbitrary_event"), MANIFEST.replace("kind = \"ticket_events\"", "kind = \"ticket_read\""), MANIFEST.replace( "ticket_changed\", \"ticket_deleted", "ticket_changed\", \"ticket_changed", ), MANIFEST.replace("surfaces = [\"service\"]", "surfaces = [\"tool\"]"), ] { assert!(package(&bad).is_err()); } } #[tokio::test] async fn service_host_and_guest_limits_fail_before_publication() { let ex = Executor::new(Limits::default()).unwrap(); let c = compile(&ex); let host = host(&c, Owner::new(Fault::None)).await; let invalid = ex .service_event( &c, host.context().clone(), host.clone(), "ticket_events", &envelope(), r#"{"mode":"oversize-retry"}"#, ) .await; assert!(matches!( invalid.outcome, Err(ExecutionError::InvalidOutput(_)) )); let mut ctx = host.context().clone(); ctx.grants.ticket_events = false; let denied = ex .service_event( &c, ctx, host.clone(), "ticket_events", &envelope(), r#"{"mode":"normal"}"#, ) .await; assert!(matches!( denied.outcome, Err(ExecutionError::InvalidInput(_)) )); let mut e = envelope(); e.version = 2; assert!(matches!( ex.service_event( &c, host.context().clone(), host.clone(), "ticket_events", &e, "{}" ) .await .outcome, Err(ExecutionError::InvalidInput(_)) )); e = envelope(); e.correlation_id = "x".repeat(65536); assert!(matches!( ex.service_event( &c, host.context().clone(), host.clone(), "ticket_events", &e, "{}" ) .await .outcome, Err(ExecutionError::InvalidInput(_)) )); let ctx = host.context().clone(); ctx.cancellation.cancel(); assert!(matches!( ex.service_start(&c, ctx, host.clone(), "ticket_events", "{}") .await .outcome, Err(ExecutionError::Cancelled) )); } fn startup_component(logging: bool) -> Vec { let mut resolve = wit_parser::Resolve::default(); let (package, _) = resolve .push_dir(Path::new(env!("CARGO_MANIFEST_DIR")).join("../../resources/plugin/wit-service")) .unwrap(); let world = resolve .select_world(&[package], Some("ticket-service-plugin")) .unwrap(); let core = wit_component::dummy_module(&resolve, world, wit_parser::ManglingAndAbi::Standard32); let wat = wasmprinter::print_bytes(core).unwrap(); let init = wat .lines() .find(|l| l.contains("(export \"cm32p2_initialize\"")) .unwrap(); let index = init .split("(func ") .nth(1) .unwrap() .split(')') .next() .unwrap(); let needle = wat .lines() .find(|l| l.contains(&format!("(func (;{index};)"))) .unwrap(); assert!(needle.trim_end().ends_with("))"), "{needle}"); let import = wat .lines() .find(|l| { l.contains(if logging { "yoi:host/logging@2" } else { "yoi:host/connections@2" }) && l.contains(if logging { "\"log\"" } else { "\"open\"" }) }) .unwrap(); let imported_index = import .split("(func (;") .nth(1) .unwrap() .split(';') .next() .unwrap(); let body = if logging { "i32.const 0 ".repeat(7) + &format!("i32.const 128 call {imported_index}") } else { format!("i32.const 0 i32.const 0 i32.const 128 call {imported_index}") }; let replacement = format!("{} {body})", needle.trim_end().strip_suffix(')').unwrap()); let wat = wat .replace(needle, &replacement) .replace("(memory (;0;) 0)", "(memory (;0;) 1)"); let mut core = wat::parse_str(wat).unwrap(); wit_component::embed_component_metadata( &mut core, &resolve, world, wit_component::StringEncoding::UTF8, ) .unwrap(); wit_component::ComponentEncoder::default() .module(&core) .unwrap() .validate(true) .encode() .unwrap() } #[tokio::test] async fn component_start_sections_never_receive_a_service_export_context() { let ex = Executor::new(Limits::default()).unwrap(); for logging in [false, true] { let p = package_bytes(MANIFEST, &startup_component(logging)).unwrap(); let c = ex .compile_service( &p.bytes, &p.package_digest, &p.artifact_digest, p.artifact_size, ) .unwrap(); let owner = Owner::new(Fault::None); let host = host(&c, owner.clone()).await; let result = ex .service_start( &c, host.context().clone(), host.clone(), "ticket_events", r#"{"mode":"normal"}"#, ) .await; assert!( matches!(result.outcome, Err(ExecutionError::Trap(_))), "{result:?}" ); assert!(owner.state.lock().unwrap().calls.iter().all(|r| matches!( r, PluginHostRequest::Begin { .. } | PluginHostRequest::Authorize { .. } ))); } } #[tokio::test] async fn artifact_delivery_binds_service_names_and_subscriptions_to_component_package() { use delivery::{ ArtifactCache, ArtifactExpectation, ArtifactSource, ArtifactStream, CacheLimits, DeliveryError, }; struct Source { bytes: Vec, metadata: PluginArtifactDeliveryMetadata, } #[async_trait] impl ArtifactSource for Source { fn server_authority_identity(&self) -> &str { "service-test-authority" } async fn authorize( &self, _: &PluginArtifactDeliveryRequest, ) -> Result { Ok(self.metadata.clone()) } async fn fetch( &self, _: &PluginArtifactDeliveryRequest, ) -> Result { Ok(Box::pin(std::io::Cursor::new(self.bytes.clone()))) } } fn metadata(value: &T) -> U { serde_json::from_value(serde_json::to_value(value).unwrap()).unwrap() } let artifact = package(MANIFEST).unwrap(); let verified = plugin::verify_plugin_artifact( &artifact.bytes, &artifact.package_digest, &artifact.artifact_digest, artifact.artifact_size, &PluginPackageLimits::default(), ) .unwrap(); let m = &verified.package.manifest; let schema = plugin::plugin_config_schema_identity(m); let descriptor = PluginArtifact { package_id: m.id.clone(), name: m.name.clone(), version: m.version.clone(), description: m.description.clone(), package_digest: artifact.package_digest.clone(), artifact_digest: artifact.artifact_digest.clone(), artifact_size: artifact.artifact_size.try_into().unwrap(), schema_version: m.schema_version, world: m.runtime.world.clone(), required_imports: m.runtime.required_imports.clone(), tools: metadata(&m.tools), services: metadata(&m.services), connections: metadata(&m.connections), requested_grants: metadata(&m.requested_grants), data_collections: metadata(&m.data_collections), config_schema: PluginSchemaContribution { identity: schema.identity, version: schema.version, digest: schema.digest, namespace: schema.namespace, dcdl: m.config_schema.clone(), }, revoked: false, revocation_reason: None, }; assert_eq!( descriptor.services[0].subscriptions, ["ticket_changed", "ticket_deleted"] ); let request = PluginArtifactDeliveryRequest { plan_id: "service-plan".into(), plan_fingerprint: "service-plan-fingerprint".into(), installation_id: "installation".into(), artifact_digest: artifact.artifact_digest.clone(), }; let directory = tempfile::tempdir().unwrap(); let cache = ArtifactCache::new(directory.path().join("cache"), CacheLimits::default()).unwrap(); let mut source = Source { bytes: artifact.bytes.clone(), metadata: PluginArtifactDeliveryMetadata { workspace_id: "workspace".into(), request: request.clone(), artifact: descriptor.clone(), }, }; let resolved = cache .resolve( &source, "workspace", &request, &ArtifactExpectation::new(descriptor.clone()), ) .await .unwrap(); assert_eq!(resolved.bytes(), artifact.bytes); let mut renamed = descriptor.clone(); renamed.services[0].name = "different_service".into(); let mut narrowed = descriptor.clone(); narrowed.services[0].subscriptions = vec!["ticket_changed".into()]; let mut unknown = descriptor.clone(); unknown.services[0].subscriptions = vec!["arbitrary_event".into()]; let mut missing = descriptor.clone(); missing.services.clear(); for tampered in [renamed, narrowed, unknown, missing] { // Source metadata and the trusted descriptor agree; only verification // against the actual package can detect this mismatch, including hits. source.metadata.artifact = tampered.clone(); let expected = ArtifactExpectation::new(tampered); assert!(matches!( cache .resolve(&source, "workspace", &request, &expected) .await, Err(DeliveryError::MetadataMismatch) )); } source.metadata.artifact = descriptor.clone(); assert_eq!( cache .resolve( &source, "workspace", &request, &ArtifactExpectation::new(descriptor) ) .await .unwrap() .bytes(), artifact.bytes ); }