use super::{
PluginHostOwnerBatches, PluginWorkspaceObserveTaskResourceAuthority, WasmtimePluginInstance,
WasmtimePluginRuntime, WasmtimePluginRuntimeError, WasmtimePluginRuntimeFailure,
WasmtimePluginRuntimeFailureKind, WasmtimePluginRuntimePhase, WasmtimePluginScheduledUpdate,
WasmtimeStoreState, WasmtimeUpdateBufferResource, WasmtimeUpdateResources,
WasmtimeUpdateViewResource, WasmtimeWorkspaceObserveTaskEnqueueErrorKind,
WasmtimeWorkspaceObserveTaskResourceDrop, WasmtimeWorkspaceObserveTaskResourceLimit,
WasmtimeWorkspaceObserveTaskResourceRep, WasmtimeWorkspaceObserveTaskResources,
WasmtimeWorkspaceObserveTaskScheduleError, WasmtimeWorkspaceObserveTasks,
};
use crate::{
ecs::{
BufferPlugin, EditorCorePlugin, InitialEditorBuffer, PluginIntentQueue, PluginOwnerQueues,
components::{
buffer::{BufferEntity, BufferText, EditorBuffer, EditorView, ViewEntity},
vim::VimModalState,
},
events::edit::{BufferEditRejected, BufferEdited},
},
fs_utils::EscapedDisplayText,
plugin::host::{
PluginHostImportSnapshot, handles::PluginBufferSnapshot,
workspace_io::PendingPluginWorkspaceObserveTaskBatch,
},
plugin::policy::PluginWitWorld,
plugin::{
AuthorizedPluginComponent, PendingPluginUpdateBatch, PendingPluginWorkspaceIoBatch,
PluginCapabilities, PluginEffectBatchLimit, PluginHostContext, PluginIdentity,
PluginInstanceState, PluginOperationalExport, PluginOperationalImport,
PluginResourceHandleRejectionReason, PluginRevocationReason, PluginRuntimeLimitField,
PluginRuntimeLimits, PluginRuntimeSpec, PluginRuntimeTraceEvent, PluginUnloadPolicy,
PluginWorkspaceIoBudget, PluginWorkspaceIoWorkerQueue, PluginWorkspaceIoWorkerQueueLimit,
PluginWorkspaceObserveTaskHandle, PluginWorkspaceObserveTaskId,
PluginWorkspaceObserveTaskQueue, PluginWorkspaceObserveTaskQueueError,
PluginWorkspaceObserveTaskQueueErrorKind, PluginWorkspaceObserveTaskQueueLimit,
PluginWorkspaceObserveTaskReservationMismatch, SealedPluginEffectBatch,
SealedPluginWorkspaceIoBatch, SealedPluginWorkspaceObserveTaskBatch, WitHostImport,
WorkspacePath, WorkspacePathError, WorkspacePathGrant, WorkspacePathRef,
},
text_stream::{TextByteStream, TextRevision},
};
use bevy::prelude::{App, Entity, MessageReader, Resource, Update, With};
use std::{
any::Any,
fs,
num::NonZeroUsize,
panic::{self, AssertUnwindSafe},
time::{Duration, Instant, SystemTime, UNIX_EPOCH},
};
use wasm_encoder::{
BlockType, CodeSection, ConstExpr, EntityType, ExportKind, ExportSection, Function,
FunctionSection, GlobalSection, GlobalType, ImportSection, Instruction, MemorySection,
MemoryType, Module, TypeSection, ValType,
};
use wasmtime::StoreLimitsBuilder;
use wit_component::{ComponentEncoder, StringEncoding, embed_component_metadata};
use wit_parser::{
Function as WitFunction, ManglingAndAbi, Resolve, ResourceIntrinsic, WasmExport,
WasmExportKind, WasmImport, WorldItem, WorldKey,
abi::{AbiVariant, WasmSignature, WasmType},
};
use crate::plugin::abi::bindings::alma::editor::host::{
BufferEdit as WitBufferEdit, BufferInsert as WitBufferInsert, Host, HostWorkspaceObserveTask,
WorkspaceObserveOutcomeShape as WitWorkspaceObserveOutcomeShape,
WorkspaceObserveTaskPoll as WitWorkspaceObserveTaskPoll,
WorkspaceObserveTaskTake as WitWorkspaceObserveTaskTake,
};
#[derive(Default, Resource)]
struct RuntimeOwnerResults {
edited: Vec<BufferEdited>,
edit_rejected: Vec<BufferEditRejected>,
}
const UPDATE_VIEW_REP: u32 = 0;
const UPDATE_BUFFER_REP: u32 = 1;
#[test]
fn wasmtime_observe_task_resource_limit_uses_nonzero_queue_proof() {
let limit = PluginWorkspaceObserveTaskQueueLimit::try_new(1, 2).expect("limit");
let resource_limit = WasmtimeWorkspaceObserveTaskResourceLimit::from_queue_limit(limit);
assert_eq!(resource_limit.get(), 3);
let saturated_queue_limit =
PluginWorkspaceObserveTaskQueueLimit::try_new(usize::MAX, usize::MAX)
.expect("non-zero queue limits should validate");
let saturated_resource_limit =
WasmtimeWorkspaceObserveTaskResourceLimit::from_queue_limit(saturated_queue_limit);
assert_eq!(saturated_resource_limit.get(), usize::MAX);
}
#[test]
fn wasmtime_observe_task_resources_use_queue_limit() {
let limit = PluginWorkspaceObserveTaskQueueLimit::try_new(1, 1).expect("limit");
let mut tasks = WasmtimeWorkspaceObserveTasks::new(limit);
let mut table = wasmtime::component::ResourceTable::new();
let identity = PluginIdentity::try_new("runtime").expect("identity");
let _first = tasks
.push_resource_started_in_update(
&mut table,
observe_task_resource_authority(&identity, 1, "docs/first.txt"),
)
.expect("first resource should fit");
let _second = tasks
.push_resource_started_in_update(
&mut table,
observe_task_resource_authority(&identity, 2, "docs/second.txt"),
)
.expect("second resource should fit");
let error = tasks
.push_resource_started_in_update(
&mut table,
observe_task_resource_authority(&identity, 3, "docs/third.txt"),
)
.expect_err("resource cap should match queue pending plus retention caps");
assert_eq!(tasks.live_resource_len(), 2);
assert!(matches!(
error,
crate::plugin::PluginHostImportError::ResourceHandle {
kind: crate::plugin::PluginHostResourceKind::WorkspaceObserveTask,
reason: PluginResourceHandleRejectionReason::Unavailable,
}
));
}
#[test]
fn wasmtime_observe_task_resource_push_rejects_reused_table_rep() {
let limit = PluginWorkspaceObserveTaskQueueLimit::try_new(1, 1).expect("limit");
let mut resources = WasmtimeWorkspaceObserveTaskResources::new(limit);
let mut table = wasmtime::component::ResourceTable::new();
let identity = PluginIdentity::try_new("runtime").expect("identity");
let first = resources
.push(
&mut table,
observe_task_handle(&identity, 1),
workspace_path("docs/first.txt"),
)
.expect("task resource should fit");
let rep = first.rep();
let _handle = table
.delete(first)
.expect("table task resource should delete");
let error = resources
.push(
&mut table,
observe_task_handle(&identity, 2),
workspace_path("docs/second.txt"),
)
.expect_err("reused table rep should fail closed");
assert!(matches!(
error,
crate::plugin::PluginHostImportError::ResourceHandle {
kind: crate::plugin::PluginHostResourceKind::WorkspaceObserveTask,
reason: PluginResourceHandleRejectionReason::Unavailable,
}
));
assert_eq!(resources.live_len(), 0);
assert_eq!(resources.resources.len(), 1);
assert!(matches!(
table.get(&wasmtime::component::Resource::<
PluginWorkspaceObserveTaskHandle,
>::new_borrow(rep)),
Err(wasmtime::component::ResourceTableError::NotPresent)
));
}
#[test]
fn wasmtime_observe_task_resource_resolution_rejects_table_tracker_drift() {
let limit = PluginWorkspaceObserveTaskQueueLimit::try_new(1, 1).expect("limit");
let mut resources = WasmtimeWorkspaceObserveTaskResources::new(limit);
let mut table = wasmtime::component::ResourceTable::new();
let identity = PluginIdentity::try_new("runtime").expect("identity");
let stored_handle = observe_task_handle(&identity, 1);
let table_resource = resources
.push(
&mut table,
stored_handle.clone(),
workspace_path("docs/input.txt"),
)
.expect("task resource should fit");
let session = workspace_observe_task_session(&identity);
resources.resources[0].handle = observe_task_handle(&identity, 2);
let error = resources
.resolve_authorized(
&table,
&session,
&wasmtime::component::Resource::new_borrow(table_resource.rep()),
)
.expect_err("table/tracker drift should fail closed");
assert!(matches!(
error,
crate::plugin::PluginHostImportError::ResourceHandle {
kind: crate::plugin::PluginHostResourceKind::WorkspaceObserveTask,
reason: PluginResourceHandleRejectionReason::Unavailable,
}
));
assert_eq!(resources.live_len(), 0);
assert_eq!(resources.resources.len(), 1);
assert_eq!(
table
.get(&table_resource)
.expect("table resource remains guest-owned"),
&stored_handle
);
}
#[test]
fn wasmtime_observe_task_resource_resolution_tombstones_table_lookup_error() {
let limit = PluginWorkspaceObserveTaskQueueLimit::try_new(1, 1).expect("limit");
let mut resources = WasmtimeWorkspaceObserveTaskResources::new(limit);
let mut table = wasmtime::component::ResourceTable::new();
let identity = PluginIdentity::try_new("runtime").expect("identity");
let table_resource = resources
.push(
&mut table,
observe_task_handle(&identity, 1),
workspace_path("docs/input.txt"),
)
.expect("task resource should fit");
let rep = table_resource.rep();
let _handle = table
.delete(table_resource)
.expect("table task resource should delete");
let wrong_resource = table
.push(String::from("wrong task resource type"))
.expect("wrong resource should fit");
assert_eq!(wrong_resource.rep(), rep);
let session = workspace_observe_task_session(&identity);
let error = resources
.resolve_authorized(
&table,
&session,
&wasmtime::component::Resource::new_borrow(rep),
)
.expect_err("wrong table type should fail closed");
assert!(matches!(
error,
crate::plugin::PluginHostImportError::ResourceHandle {
kind: crate::plugin::PluginHostResourceKind::WorkspaceObserveTask,
reason: PluginResourceHandleRejectionReason::WrongType,
}
));
assert_eq!(resources.live_len(), 0);
assert_eq!(resources.resources.len(), 1);
}
#[test]
fn wasmtime_observe_task_resource_drop_rejects_table_tracker_drift() {
let limit = PluginWorkspaceObserveTaskQueueLimit::try_new(1, 1).expect("limit");
let mut resources = WasmtimeWorkspaceObserveTaskResources::new(limit);
let mut table = wasmtime::component::ResourceTable::new();
let identity = PluginIdentity::try_new("runtime").expect("identity");
let table_resource = resources
.push(
&mut table,
observe_task_handle(&identity, 1),
workspace_path("docs/input.txt"),
)
.expect("task resource should fit");
resources.resources[0].handle = observe_task_handle(&identity, 2);
let error = resources
.delete(&mut table, table_resource)
.expect_err("drop drift should fail closed");
assert!(matches!(
error,
crate::plugin::PluginHostImportError::ResourceHandle {
kind: crate::plugin::PluginHostResourceKind::WorkspaceObserveTask,
reason: PluginResourceHandleRejectionReason::Unavailable,
}
));
assert_eq!(resources.resources.len(), 0);
}
#[test]
fn wasmtime_observe_task_resource_drop_tombstones_table_delete_error() {
let limit = PluginWorkspaceObserveTaskQueueLimit::try_new(1, 1).expect("limit");
let mut resources = WasmtimeWorkspaceObserveTaskResources::new(limit);
let mut table = wasmtime::component::ResourceTable::new();
let identity = PluginIdentity::try_new("runtime").expect("identity");
let table_resource = resources
.push(
&mut table,
observe_task_handle(&identity, 1),
workspace_path("docs/input.txt"),
)
.expect("task resource should fit");
let rep = table_resource.rep();
let _handle = table
.delete(table_resource)
.expect("table task resource should delete");
let wrong_resource = table
.push(String::from("wrong task resource type"))
.expect("wrong resource should fit");
assert_eq!(wrong_resource.rep(), rep);
let error = resources
.delete(&mut table, wasmtime::component::Resource::new_own(rep))
.expect_err("wrong table type should fail closed");
assert!(matches!(
error,
crate::plugin::PluginHostImportError::ResourceHandle {
kind: crate::plugin::PluginHostResourceKind::WorkspaceObserveTask,
reason: PluginResourceHandleRejectionReason::WrongType,
}
));
assert_eq!(resources.live_len(), 0);
assert_eq!(resources.resources.len(), 1);
}
#[test]
fn wasmtime_observe_task_resource_drop_rejects_missing_tracker_entry() {
let mut resources = WasmtimeWorkspaceObserveTaskResources::new(
PluginWorkspaceObserveTaskQueueLimit::try_new(1, 1).expect("limit"),
);
let mut table = wasmtime::component::ResourceTable::new();
let identity = PluginIdentity::try_new("runtime").expect("identity");
let table_resource = table
.push(observe_task_handle(&identity, 1))
.expect("table resource should fit");
let rep = table_resource.rep();
let error = resources
.delete(&mut table, table_resource)
.expect_err("tracked task resource drop should require a tracker entry");
assert!(matches!(
error,
crate::plugin::PluginHostImportError::ResourceHandle {
kind: crate::plugin::PluginHostResourceKind::WorkspaceObserveTask,
reason: PluginResourceHandleRejectionReason::Unavailable,
}
));
assert!(matches!(
table.get(&wasmtime::component::Resource::<
PluginWorkspaceObserveTaskHandle,
>::new_borrow(rep)),
Err(wasmtime::component::ResourceTableError::NotPresent)
));
}
#[test]
fn wasmtime_observe_task_resource_drop_reports_inactive_after_invalidation() {
let limit = PluginWorkspaceObserveTaskQueueLimit::try_new(1, 1).expect("limit");
let mut resources = WasmtimeWorkspaceObserveTaskResources::new(limit);
let mut table = wasmtime::component::ResourceTable::new();
let identity = PluginIdentity::try_new("runtime").expect("identity");
let table_resource = resources
.push(
&mut table,
observe_task_handle(&identity, 1),
workspace_path("docs/input.txt"),
)
.expect("task resource should fit");
let _invalidated = resources.resources[0].invalidate();
let dropped = resources
.delete(&mut table, table_resource)
.expect("invalidated resource drop should clean up");
assert_eq!(dropped, WasmtimeWorkspaceObserveTaskResourceDrop::Inactive);
assert_eq!(resources.resources.len(), 0);
assert_eq!(resources.live_len(), 0);
}
fn observe_task_handle(
identity: &PluginIdentity,
task_id: u64,
) -> PluginWorkspaceObserveTaskHandle {
PluginWorkspaceObserveTaskHandle::new(
identity.clone(),
PluginWorkspaceObserveTaskId::try_new(task_id).expect("task id"),
)
}
fn observe_task_resource_authority(
identity: &PluginIdentity,
task_id: u64,
path: &str,
) -> PluginWorkspaceObserveTaskResourceAuthority {
PluginWorkspaceObserveTaskResourceAuthority::for_test(
observe_task_handle(identity, task_id),
workspace_path(path),
)
}
fn workspace_path(path: &str) -> WorkspacePath {
WorkspacePath::from_ref(WorkspacePathRef::try_from(path).expect("workspace path"))
}
fn workspace_observe_task_session(
identity: &PluginIdentity,
) -> crate::plugin::PluginHostImportSession {
let host = crate::plugin::PluginHostContext::for_test(
identity.as_str(),
PluginCapabilities {
workspace_observe: vec![WorkspacePathGrant::new("docs")],
..PluginCapabilities::default()
},
);
let handles = crate::plugin::PluginHandleStore::new(identity.clone());
crate::plugin::PluginHostImportSession::new(
&host,
&handles,
PluginEffectBatchLimit::default(),
PluginWorkspaceIoBudget::try_new(1, 64, 64).expect("budget should validate"),
)
.expect("session should validate")
}
#[test]
fn invalid_component_bytes_fail_validation_without_echoing_bytes() {
let runtime = WasmtimePluginRuntime::new().expect("runtime");
let spec = PluginRuntimeSpec::new(
AuthorizedPluginComponent::for_test(
PluginIdentity::try_new("bad-component").expect("identity"),
EscapedDisplayText::from_display_text("plugins/bad.wasm"),
b"secret component bytes".to_vec(),
),
PluginWitWorld::current(),
PluginCapabilities::default(),
PluginRuntimeLimits::default().validate().expect("limits"),
);
let error = runtime
.instantiate(&spec)
.expect_err("invalid component bytes should fail");
assert!(matches!(
&error,
WasmtimePluginRuntimeError::Component {
identity,
byte_len: 22,
..
} if identity.as_str() == "bad-component"
));
assert!(!format!("{error:?}").contains("secret"));
assert!(!error.to_string().contains("secret"));
}
#[test]
fn runtime_failure_detail_does_not_store_raw_sources() {
let source = String::from("secret guest trap");
let failure = WasmtimePluginRuntimeFailure::from_wasmtime(
WasmtimePluginRuntimeFailureKind::GuestTrap,
&source,
);
assert_eq!(failure.kind(), WasmtimePluginRuntimeFailureKind::GuestTrap);
assert_eq!(failure.kind().as_str(), "guest trap");
assert!(!format!("{failure:?}").contains("secret"));
assert!(!failure.to_string().contains("secret"));
}
#[test]
fn failed_update_discards_pending_work_without_filesystem_or_ecs_publication() {
let root = temp_dir("wasmtime-discard");
fs::create_dir(root.join("docs")).expect("docs directory");
let host = crate::plugin::PluginHostContext::for_test(
"runtime",
PluginCapabilities {
status_publish: true,
workspace_artifact_write: vec![WorkspacePathGrant::new("docs")],
..PluginCapabilities::default()
},
);
let mut handles = crate::plugin::PluginHandleStore::new(
PluginIdentity::try_new("runtime").expect("identity"),
);
let view = handles
.issue_view(crate::ecs::components::buffer::ViewEntity(
Entity::from_raw_u32(7).expect("entity"),
))
.expect("view handle");
let mut session = crate::plugin::PluginHostImportSession::new(
&host,
&handles,
crate::plugin::PluginEffectBatchLimit::default(),
crate::plugin::PluginWorkspaceIoBudget::try_new(4, 64, 64).expect("budget"),
)
.expect("session");
session
.push_status_text(view, "secret status")
.expect("status");
session
.queue_workspace_artifact_write(
WorkspacePathRef::try_from("docs/output.txt").expect("path"),
b"secret write".to_vec(),
)
.expect("workspace write");
let error = WasmtimePluginRuntimeError::GuestTrap {
identity: PluginIdentity::try_new("runtime").expect("identity"),
export: PluginOperationalExport::Update,
failure: WasmtimePluginRuntimeFailure::new(WasmtimePluginRuntimeFailureKind::GuestTrap),
}
.with_discard(session.discard());
assert!(matches!(
&error,
WasmtimePluginRuntimeError::UpdateFailed { discarded, .. }
if discarded.effects().discarded_effects() == 1
&& discarded.workspace_io().discarded_requests() == 1
));
assert!(!root.join("docs/output.txt").exists());
assert!(!format!("{error:?}").contains("secret"));
assert!(!error.to_string().contains("secret"));
let _cleanup = fs::remove_dir_all(root);
}
#[test]
fn timeout_error_emits_redacted_operational_event() {
let error = WasmtimePluginRuntimeError::Timeout {
identity: PluginIdentity::try_new("runtime").expect("identity"),
export: PluginOperationalExport::Update,
timeout_ms: 50,
};
let event = error.operational_event().expect("event");
assert_eq!(event.identity(), Some("runtime"));
assert_eq!(
event.to_string(),
"plugin \"runtime\" timed out after 50 ms"
);
}
#[test]
fn guest_trap_error_emits_redacted_operational_event() {
let error = WasmtimePluginRuntimeError::GuestTrap {
identity: PluginIdentity::try_new("runtime").expect("identity"),
export: PluginOperationalExport::Update,
failure: WasmtimePluginRuntimeFailure::new(WasmtimePluginRuntimeFailureKind::GuestTrap),
};
let event = error.operational_event().expect("event");
assert_eq!(event.identity(), Some("runtime"));
assert_eq!(
event.to_string(),
"plugin \"runtime\" export update trapped"
);
assert!(!format!("{event:?}").contains("secret"));
assert!(!event.to_string().contains("secret"));
}
#[test]
fn host_import_decode_error_emits_redacted_operational_event() {
let error = WasmtimePluginRuntimeError::HostImport {
identity: PluginIdentity::try_new("runtime").expect("identity"),
export: PluginOperationalExport::Update,
import: PluginOperationalImport::WorkspaceArtifactWrite,
source: crate::plugin::PluginHostImportError::Decode(
crate::plugin::GuestDecodeError::PayloadTooLarge {
field: crate::plugin::GuestDecodeField::WorkspaceArtifactWriteBytes,
size: 4097,
max: 4096,
},
),
};
let event = error.operational_event().expect("event");
assert_eq!(event.identity(), Some("runtime"));
assert_eq!(
event.kind(),
crate::plugin::PluginOperationalEventKind::ImportRejected {
import: PluginOperationalImport::WorkspaceArtifactWrite,
reason: crate::plugin::PluginOperationalImportRejection::Decode,
}
);
assert_eq!(
event.to_string(),
"plugin \"runtime\" rejected import workspace.artifact_write: decode"
);
assert!(!format!("{event:?}").contains("secret"));
assert!(!event.to_string().contains("secret"));
}
#[test]
fn update_setup_error_emits_redacted_operational_event() {
let error = WasmtimePluginRuntimeError::UpdateSetup {
identity: PluginIdentity::try_new("runtime").expect("identity"),
source: crate::plugin::PluginHostImportError::SessionAlreadyActive,
};
let event = error.operational_event().expect("event");
assert_eq!(event.identity(), Some("runtime"));
assert_eq!(
event.kind(),
crate::plugin::PluginOperationalEventKind::ImportRejected {
import: PluginOperationalImport::HostImport,
reason: crate::plugin::PluginOperationalImportRejection::SessionAlreadyActive,
}
);
assert_eq!(
event.to_string(),
"plugin \"runtime\" rejected import host-import: session-already-active"
);
}
#[test]
fn update_setup_host_state_mismatch_emits_redacted_operational_event() {
let error = WasmtimePluginRuntimeError::UpdateSetup {
identity: PluginIdentity::try_new("runtime").expect("identity"),
source: crate::plugin::PluginHostImportError::HostStateMismatch(
crate::plugin::PluginHostStateMismatch::new(
PluginIdentity::try_new("runtime").expect("identity"),
PluginIdentity::try_new("other").expect("identity"),
),
),
};
let event = error.operational_event().expect("event");
assert_eq!(event.identity(), Some("runtime"));
assert_eq!(
event.kind(),
crate::plugin::PluginOperationalEventKind::ImportRejected {
import: PluginOperationalImport::HostImport,
reason: crate::plugin::PluginOperationalImportRejection::HostStateMismatch,
}
);
assert_eq!(
event.to_string(),
"plugin \"runtime\" rejected import host-import: host-state-mismatch"
);
}
#[test]
fn scheduled_update_error_delegates_update_operational_event() {
let error =
super::WasmtimePluginUpdateScheduleError::from(WasmtimePluginRuntimeError::Timeout {
identity: PluginIdentity::try_new("scheduled-update").expect("identity"),
export: PluginOperationalExport::Update,
timeout_ms: 75,
});
let event = error.operational_event().expect("event");
assert_eq!(event.identity(), Some("scheduled-update"));
assert_eq!(
event.to_string(),
"plugin \"scheduled-update\" timed out after 75 ms"
);
}
#[test]
fn wasmtime_observe_task_enqueue_error_kinds_are_closed() {
let cases = [
(
WasmtimeWorkspaceObserveTaskEnqueueErrorKind::SchedulingDenied,
"scheduling-denied",
),
(
WasmtimeWorkspaceObserveTaskEnqueueErrorKind::InstanceStateMismatch,
"instance-state-mismatch",
),
(
WasmtimeWorkspaceObserveTaskEnqueueErrorKind::BatchIdentityMismatch,
"batch-identity-mismatch",
),
(WasmtimeWorkspaceObserveTaskEnqueueErrorKind::Queue, "queue"),
];
for (kind, expected) in cases {
assert_eq!(kind.as_str(), expected);
assert_eq!(kind.to_string(), expected);
assert_eq!(format!("{kind:?}"), expected);
}
}
#[test]
fn runtime_failures_emit_closed_operational_events() {
use crate::plugin::PluginOperationalRuntimeFailure;
let cases = [
(
RuntimeFailureCase::Engine,
None,
PluginOperationalRuntimeFailure::Engine,
),
(
RuntimeFailureCase::RuntimeTimer,
None,
PluginOperationalRuntimeFailure::RuntimeTimer,
),
(
RuntimeFailureCase::Component,
Some("runtime"),
PluginOperationalRuntimeFailure::Component,
),
(
RuntimeFailureCase::Limit,
Some("runtime"),
PluginOperationalRuntimeFailure::Limit,
),
(
RuntimeFailureCase::Linker,
Some("runtime"),
PluginOperationalRuntimeFailure::Linker,
),
(
RuntimeFailureCase::Instantiate,
Some("runtime"),
PluginOperationalRuntimeFailure::Instantiate,
),
(
RuntimeFailureCase::Export,
Some("runtime"),
PluginOperationalRuntimeFailure::Export,
),
(
RuntimeFailureCase::PostReturn,
Some("runtime"),
PluginOperationalRuntimeFailure::PostReturn,
),
(
RuntimeFailureCase::Fuel,
Some("runtime"),
PluginOperationalRuntimeFailure::Fuel,
),
(
RuntimeFailureCase::Timer,
Some("runtime"),
PluginOperationalRuntimeFailure::Timer,
),
];
for (case, expected_identity, expected_failure) in cases {
let event = runtime_failure_error(case)
.operational_event()
.expect("event");
assert_eq!(event.identity(), expected_identity);
assert_eq!(
event.kind(),
crate::plugin::PluginOperationalEventKind::RuntimeFailure {
failure: expected_failure,
}
);
assert!(!format!("{event:?}").contains("secret"));
assert!(!event.to_string().contains("secret"));
}
}
#[derive(Clone, Copy)]
enum RuntimeFailureCase {
Engine,
RuntimeTimer,
Component,
Limit,
Linker,
Instantiate,
Export,
PostReturn,
Fuel,
Timer,
}
fn runtime_failure_error(case: RuntimeFailureCase) -> WasmtimePluginRuntimeError {
let identity = || PluginIdentity::try_new("runtime").expect("identity");
match case {
RuntimeFailureCase::Engine => WasmtimePluginRuntimeError::Engine {
failure: WasmtimePluginRuntimeFailure::new(WasmtimePluginRuntimeFailureKind::Engine),
},
RuntimeFailureCase::RuntimeTimer => WasmtimePluginRuntimeError::RuntimeTimer {
failure: WasmtimePluginRuntimeFailure::new(
WasmtimePluginRuntimeFailureKind::RuntimeTimer,
),
},
RuntimeFailureCase::Component => WasmtimePluginRuntimeError::Component {
identity: identity(),
byte_len: 13,
failure: WasmtimePluginRuntimeFailure::new(WasmtimePluginRuntimeFailureKind::Component),
},
RuntimeFailureCase::Limit => WasmtimePluginRuntimeError::Limit {
identity: identity(),
field: PluginRuntimeLimitField::MaxMemoryBytes,
value: u64::MAX,
},
RuntimeFailureCase::Linker => WasmtimePluginRuntimeError::Instantiate {
identity: identity(),
failure: WasmtimePluginRuntimeFailure::new(WasmtimePluginRuntimeFailureKind::Linker),
},
RuntimeFailureCase::Instantiate => WasmtimePluginRuntimeError::Instantiate {
identity: identity(),
failure: WasmtimePluginRuntimeFailure::new(
WasmtimePluginRuntimeFailureKind::Instantiate,
),
},
RuntimeFailureCase::Export => WasmtimePluginRuntimeError::Export {
identity: identity(),
export: PluginOperationalExport::Load,
failure: WasmtimePluginRuntimeFailure::new(WasmtimePluginRuntimeFailureKind::Export),
},
RuntimeFailureCase::PostReturn => WasmtimePluginRuntimeError::GuestTrap {
identity: identity(),
export: PluginOperationalExport::Load,
failure: WasmtimePluginRuntimeFailure::new(
WasmtimePluginRuntimeFailureKind::PostReturn,
),
},
RuntimeFailureCase::Fuel => WasmtimePluginRuntimeError::Fuel {
identity: identity(),
failure: WasmtimePluginRuntimeFailure::new(WasmtimePluginRuntimeFailureKind::Fuel),
},
RuntimeFailureCase::Timer => WasmtimePluginRuntimeError::Timer {
identity: identity(),
failure: WasmtimePluginRuntimeFailure::new(
WasmtimePluginRuntimeFailureKind::RuntimeTimer,
),
},
}
}
#[test]
fn runtime_errors_expose_root_phase_and_discard_evidence() {
let host = crate::plugin::PluginHostContext::for_test(
"runtime",
PluginCapabilities {
status_publish: true,
..PluginCapabilities::default()
},
);
let mut handles = crate::plugin::PluginHandleStore::new(
PluginIdentity::try_new("runtime").expect("identity"),
);
let view = handles
.issue_view(crate::ecs::components::buffer::ViewEntity(test_entity(6)))
.expect("view handle");
let mut session = crate::plugin::PluginHostImportSession::new(
&host,
&handles,
crate::plugin::PluginEffectBatchLimit::default(),
crate::plugin::PluginWorkspaceIoBudget::try_new(4, 64, 64).expect("budget"),
)
.expect("session");
session
.push_status_text(view, "secret status")
.expect("status");
let error = WasmtimePluginRuntimeError::HostImport {
identity: PluginIdentity::try_new("runtime").expect("identity"),
export: PluginOperationalExport::Update,
import: PluginOperationalImport::StatusPublish,
source: crate::plugin::PluginHostImportError::SessionMissing,
}
.with_discard(session.discard());
assert_eq!(error.phase(), WasmtimePluginRuntimePhase::HostImport);
assert_eq!(
error
.discarded_update()
.expect("discard evidence")
.effects()
.discarded_effects(),
1
);
assert!(!format!("{error:?}").contains("secret"));
assert!(!error.to_string().contains("secret"));
}
#[test]
fn runtime_error_debug_uses_redacted_host_import_shapes() {
let host = crate::plugin::PluginHostContext::for_test(
"runtime",
PluginCapabilities {
status_publish: true,
..PluginCapabilities::default()
},
);
let mut handles = crate::plugin::PluginHandleStore::new(
PluginIdentity::try_new("runtime").expect("identity"),
);
let view = handles
.issue_view(crate::ecs::components::buffer::ViewEntity(test_entity(6)))
.expect("view handle");
let mut session = crate::plugin::PluginHostImportSession::new(
&host,
&handles,
crate::plugin::PluginEffectBatchLimit::default(),
crate::plugin::PluginWorkspaceIoBudget::try_new(4, 64, 64).expect("budget"),
)
.expect("session");
session
.push_status_text(view, "secret status")
.expect("status");
let error = WasmtimePluginRuntimeError::HostImport {
identity: PluginIdentity::try_new("runtime").expect("identity"),
export: PluginOperationalExport::Update,
import: PluginOperationalImport::WorkspaceArtifactWrite,
source: crate::plugin::PluginHostImportError::Decode(
crate::plugin::GuestDecodeError::InvalidWorkspacePath {
field: crate::plugin::GuestDecodeField::WorkspaceArtifactWritePath,
source: WorkspacePathError::DotComponent,
},
),
}
.with_discard(session.discard());
let debug = format!("{error:?}");
assert!(debug.contains("WasmtimePluginRuntimeError"));
assert!(debug.contains("phase"));
assert!(debug.contains("PluginHostImportError"));
assert!(debug.contains("workspace.artifact_write"));
assert!(!debug.contains("secret status"));
assert!(!debug.contains("docs/../secret.txt"));
}
#[test]
fn epoch_deadline_ticks_are_non_zero_ceiling_values() {
assert_eq!(
super::epoch_ticks_for(Duration::from_millis(1), Duration::from_millis(10)),
1
);
assert_eq!(
super::epoch_ticks_for(Duration::from_millis(20), Duration::from_millis(10)),
2
);
assert_eq!(
super::epoch_ticks_for(Duration::ZERO, Duration::from_millis(10)),
1
);
}
#[test]
fn epoch_interrupt_trap_is_typed_as_deadline_expiry() {
let deadline = super::GuestDeadline {
started: Instant::now(),
timeout: Duration::from_mins(1),
epoch_ticks: 1,
};
let interrupt: wasmtime::Error = wasmtime::Trap::Interrupt.into();
let out_of_fuel: wasmtime::Error = wasmtime::Trap::OutOfFuel.into();
assert!(deadline.trapped_by_deadline(&interrupt));
assert!(!deadline.trapped_by_deadline(&out_of_fuel));
}
#[test]
fn guest_execution_guard_preserves_export_context_when_classifying_traps() {
let identity = PluginIdentity::try_new("runtime").expect("identity");
let guard = super::GuestExecutionGuard {
export: PluginOperationalExport::Update,
timeout_ms: 50,
deadline: super::GuestDeadline {
started: Instant::now(),
timeout: Duration::from_mins(1),
epoch_ticks: 1,
},
};
let interrupt: wasmtime::Error = wasmtime::Trap::Interrupt.into();
let out_of_fuel: wasmtime::Error = wasmtime::Trap::OutOfFuel.into();
assert!(matches!(
guard.runtime_error(
&identity,
&interrupt,
WasmtimePluginRuntimeFailureKind::GuestTrap
),
WasmtimePluginRuntimeError::Timeout {
identity: actual_identity,
export: PluginOperationalExport::Update,
timeout_ms: 50,
} if actual_identity.as_str() == "runtime"
));
assert!(matches!(
guard.runtime_error(
&identity,
&out_of_fuel,
WasmtimePluginRuntimeFailureKind::GuestTrap
),
WasmtimePluginRuntimeError::GuestTrap {
identity: actual_identity,
export: PluginOperationalExport::Update,
failure,
} if actual_identity.as_str() == "runtime"
&& failure.kind() == WasmtimePluginRuntimeFailureKind::GuestTrap
));
}
#[test]
fn status_publish_host_import_queues_status_effect() {
let view = crate::ecs::components::buffer::ViewEntity(test_entity(7));
let host = crate::plugin::PluginHostContext::for_test(
"runtime",
PluginCapabilities {
status_publish: true,
..PluginCapabilities::default()
},
);
let mut handles = crate::plugin::PluginHandleStore::new(
PluginIdentity::try_new("runtime").expect("identity"),
);
let _view_handle = handles.issue_view(view).expect("view handle");
let _buffer_handle = handles
.issue_buffer(
crate::ecs::components::buffer::BufferEntity(test_entity(70)),
Some(TextRevision::from(0)),
Some(view),
)
.expect("buffer handle");
let mut state = store_state(PluginRuntimeLimits::default());
let resources = begin_test_update(&mut state, &host, &handles);
Host::status_publish(
&mut state,
resources.view_borrow(),
String::from("plugin ok"),
)
.expect("status import should queue");
let report = state
.end_update()
.expect("session")
.seal()
.into_parts()
.effects
.drain();
assert_eq!(report.status_messages().len(), 1);
assert_eq!(report.status_messages()[0].target, view);
assert!(report.buffer_edits().is_empty());
assert!(state.take_import_error().is_none());
}
#[test]
fn denied_status_publish_host_import_records_typed_rejection() {
let view = crate::ecs::components::buffer::ViewEntity(test_entity(8));
let host = crate::plugin::PluginHostContext::for_test("runtime", PluginCapabilities::default());
let mut handles = crate::plugin::PluginHandleStore::new(
PluginIdentity::try_new("runtime").expect("identity"),
);
let _view_handle = handles.issue_view(view).expect("view handle");
let _buffer_handle = handles
.issue_buffer(
crate::ecs::components::buffer::BufferEntity(test_entity(80)),
Some(TextRevision::from(0)),
Some(view),
)
.expect("buffer handle");
let mut state = store_state(PluginRuntimeLimits::default());
let resources = begin_test_update(&mut state, &host, &handles);
let error = Host::status_publish(
&mut state,
resources.view_borrow(),
String::from("secret status"),
)
.expect_err("status import should trap");
let import_error = state.take_import_error().expect("typed import error");
let discard = state.end_update().expect("session").discard();
assert!(error.to_string().contains("plugin host import rejected"));
assert!(matches!(
import_error,
crate::plugin::PluginHostImportError::Authorization(_)
));
assert_eq!(discard.effects().discarded_effects(), 0);
assert!(!format!("{error:?}").contains("secret"));
}
#[test]
fn store_state_retains_first_import_rejection_with_import_class() {
let mut state = store_state(PluginRuntimeLimits::default());
let _first = state.reject_import(
PluginOperationalImport::StatusPublish,
crate::plugin::PluginHostImportError::SessionMissing,
);
let _second = state.reject_import(
PluginOperationalImport::BufferObserve,
crate::plugin::PluginHostImportError::SessionAlreadyActive,
);
let rejection = state
.take_import_rejection()
.expect("first rejection should be retained");
assert_eq!(rejection.import, PluginOperationalImport::StatusPublish);
assert!(matches!(
rejection.source,
crate::plugin::PluginHostImportError::SessionMissing
));
assert!(state.take_import_rejection().is_none());
}
#[test]
fn oversized_status_publish_host_import_fails_before_queueing() {
let view = crate::ecs::components::buffer::ViewEntity(test_entity(9));
let host = crate::plugin::PluginHostContext::for_test(
"runtime",
PluginCapabilities {
status_publish: true,
..PluginCapabilities::default()
},
);
let mut handles = crate::plugin::PluginHandleStore::new(
PluginIdentity::try_new("runtime").expect("identity"),
);
let _view_handle = handles.issue_view(view).expect("view handle");
let _buffer_handle = handles
.issue_buffer(
crate::ecs::components::buffer::BufferEntity(test_entity(90)),
Some(TextRevision::from(0)),
Some(view),
)
.expect("buffer handle");
let mut state = store_state(PluginRuntimeLimits {
max_message_bytes: 4,
..PluginRuntimeLimits::default()
});
let resources = begin_test_update(&mut state, &host, &handles);
let error = Host::status_publish(
&mut state,
resources.view_borrow(),
String::from("secret status"),
)
.expect_err("oversized status should trap");
let import_error = state.take_import_error().expect("typed import error");
let discard = state.end_update().expect("session").discard();
assert!(matches!(
import_error,
crate::plugin::PluginHostImportError::Decode(_)
));
assert_eq!(discard.effects().discarded_effects(), 0);
assert!(!format!("{error:?}").contains("secret"));
assert!(!error.to_string().contains("secret"));
}
#[test]
fn end_update_removes_host_owned_update_resources() {
let view = crate::ecs::components::buffer::ViewEntity(test_entity(10));
let buffer = crate::ecs::components::buffer::BufferEntity(test_entity(100));
let host = crate::plugin::PluginHostContext::for_test("runtime", PluginCapabilities::default());
let mut handles = crate::plugin::PluginHandleStore::new(
PluginIdentity::try_new("runtime").expect("identity"),
);
let _view_handle = handles.issue_view(view).expect("view handle");
let _buffer_handle = handles
.issue_buffer(buffer, Some(TextRevision::from(0)), Some(view))
.expect("buffer handle");
let mut state = store_state(PluginRuntimeLimits::default());
let _resources = begin_test_update(&mut state, &host, &handles);
assert!(!state.resources.is_empty());
let _session = state.end_update().expect("session");
assert!(state.resources.is_empty());
assert!(state.take_import_error().is_none());
}
#[test]
fn update_resource_cleanup_attempts_buffer_after_view_failure() {
let mut table = wasmtime::component::ResourceTable::new();
let wrong_view = table
.push(String::from("wrong update resource type"))
.expect("wrong resource should fit");
let view_rep = wrong_view.rep();
let mut handles = crate::plugin::PluginHandleStore::new(
PluginIdentity::try_new("runtime").expect("identity"),
);
let view = crate::ecs::components::buffer::ViewEntity(test_entity(10));
let buffer = handles
.issue_buffer(
crate::ecs::components::buffer::BufferEntity(test_entity(100)),
Some(TextRevision::from(0)),
Some(view),
)
.expect("buffer handle");
let buffer_resource = table.push(buffer).expect("buffer resource should fit");
let buffer_rep = buffer_resource.rep();
let resources = WasmtimeUpdateResources::new(
WasmtimeUpdateViewResource { rep: view_rep },
WasmtimeUpdateBufferResource::from_owned(&buffer_resource),
);
let error = resources
.delete_from(&mut table)
.expect_err("wrong view resource type should fail cleanup");
assert!(matches!(
error,
crate::plugin::PluginHostImportError::ResourceHandle {
kind: crate::plugin::PluginHostResourceKind::ViewHandle,
reason: PluginResourceHandleRejectionReason::WrongType,
}
));
assert!(matches!(
table.get(&wasmtime::component::Resource::<
crate::plugin::PluginBufferHandle,
>::new_borrow(buffer_rep)),
Err(wasmtime::component::ResourceTableError::NotPresent)
));
}
#[test]
fn begin_update_rejects_reentrant_session_without_replacing_active_session() {
let view = crate::ecs::components::buffer::ViewEntity(test_entity(11));
let buffer = crate::ecs::components::buffer::BufferEntity(test_entity(110));
let host = crate::plugin::PluginHostContext::for_test("runtime", PluginCapabilities::default());
let mut handles = crate::plugin::PluginHandleStore::new(
PluginIdentity::try_new("runtime").expect("identity"),
);
let _view_handle = handles.issue_view(view).expect("view handle");
let _buffer_handle = handles
.issue_buffer(buffer, Some(TextRevision::from(0)), Some(view))
.expect("buffer handle");
let mut state = store_state(PluginRuntimeLimits::default());
let _resources = begin_test_update(&mut state, &host, &handles);
let error = state
.begin_update(
import_session(&host, &handles),
handles.first_view_handle().expect("view handle"),
handles.first_buffer_handle().expect("buffer handle"),
)
.expect_err("nested update should fail closed");
assert_eq!(
error,
crate::plugin::PluginHostImportError::SessionAlreadyActive
);
assert!(!state.resources.is_empty());
let _session = state.end_update().expect("original session");
assert!(state.resources.is_empty());
}
#[test]
fn update_resources_preserve_typed_view_and_buffer_reps() {
let resources = WasmtimeUpdateResources::new(
WasmtimeUpdateViewResource::from_owned(&wasmtime::component::Resource::<
crate::plugin::PluginViewHandle,
>::new_own(UPDATE_VIEW_REP)),
WasmtimeUpdateBufferResource::from_owned(&wasmtime::component::Resource::<
crate::plugin::PluginBufferHandle,
>::new_own(UPDATE_BUFFER_REP)),
);
let borrows = resources.borrows();
assert_eq!(borrows.view.rep(), UPDATE_VIEW_REP);
assert_eq!(borrows.buffer.rep(), UPDATE_BUFFER_REP);
}
#[test]
fn buffer_observe_host_import_returns_snapshot() {
let buffer = crate::ecs::components::buffer::BufferEntity(test_entity(12));
let host = crate::plugin::PluginHostContext::for_test(
"runtime",
PluginCapabilities {
buffer_observe: true,
..PluginCapabilities::default()
},
);
let mut handles = crate::plugin::PluginHandleStore::new(
PluginIdentity::try_new("runtime").expect("identity"),
);
let view = crate::ecs::components::buffer::ViewEntity(test_entity(120));
let _view_handle = handles.issue_view(view).expect("view handle");
let _buffer_handle = handles
.issue_observed_buffer(
buffer,
PluginBufferSnapshot::new(TextRevision::from(9), "secret snapshot"),
Some(view),
)
.expect("buffer handle");
let mut state = store_state(PluginRuntimeLimits::default());
let resources = begin_test_update(&mut state, &host, &handles);
let snapshot = Host::buffer_observe(&mut state, resources.buffer_borrow())
.expect("buffer observe import should return snapshot");
assert_eq!(snapshot.revision, 9);
assert_eq!(snapshot.text, "secret snapshot");
assert!(state.take_import_error().is_none());
}
#[test]
fn denied_buffer_observe_host_import_records_typed_rejection() {
let buffer = crate::ecs::components::buffer::BufferEntity(test_entity(12));
let host = crate::plugin::PluginHostContext::for_test("runtime", PluginCapabilities::default());
let mut handles = crate::plugin::PluginHandleStore::new(
PluginIdentity::try_new("runtime").expect("identity"),
);
let view = crate::ecs::components::buffer::ViewEntity(test_entity(121));
let _view_handle = handles.issue_view(view).expect("view handle");
let _buffer_handle = handles
.issue_observed_buffer(
buffer,
PluginBufferSnapshot::new(TextRevision::from(9), "secret snapshot"),
Some(view),
)
.expect("buffer handle");
let mut state = store_state(PluginRuntimeLimits::default());
let resources = begin_test_update(&mut state, &host, &handles);
let error = Host::buffer_observe(&mut state, resources.buffer_borrow())
.expect_err("buffer observe import should trap");
let import_error = state.take_import_error().expect("typed import error");
let discard = state.end_update().expect("session").discard();
assert!(error.to_string().contains("plugin host import rejected"));
assert!(matches!(
import_error,
crate::plugin::PluginHostImportError::Authorization(_)
));
assert_eq!(discard.effects().discarded_effects(), 0);
assert!(!format!("{error:?}").contains("secret"));
}
#[test]
fn oversized_buffer_observe_host_import_fails_before_disclosure() {
let buffer = crate::ecs::components::buffer::BufferEntity(test_entity(12));
let host = crate::plugin::PluginHostContext::for_test(
"runtime",
PluginCapabilities {
buffer_observe: true,
..PluginCapabilities::default()
},
);
let mut handles = crate::plugin::PluginHandleStore::new(
PluginIdentity::try_new("runtime").expect("identity"),
);
let view = crate::ecs::components::buffer::ViewEntity(test_entity(122));
let _view_handle = handles.issue_view(view).expect("view handle");
let _buffer_handle = handles
.issue_observed_buffer(
buffer,
PluginBufferSnapshot::new(TextRevision::from(9), "secret snapshot"),
Some(view),
)
.expect("buffer handle");
let mut state = store_state(PluginRuntimeLimits {
max_message_bytes: 4,
..PluginRuntimeLimits::default()
});
let resources = begin_test_update(&mut state, &host, &handles);
let error = Host::buffer_observe(&mut state, resources.buffer_borrow())
.expect_err("oversized observe should trap");
let import_error = state.take_import_error().expect("typed import error");
let discard = state.end_update().expect("session").discard();
assert!(matches!(
import_error,
crate::plugin::PluginHostImportError::Observation(_)
));
assert_eq!(discard.effects().discarded_effects(), 0);
assert!(!format!("{error:?}").contains("secret"));
assert!(!error.to_string().contains("secret"));
}
#[test]
fn buffer_propose_edit_host_import_queues_revision_guarded_effect() {
let buffer = crate::ecs::components::buffer::BufferEntity(test_entity(12));
let host = crate::plugin::PluginHostContext::for_test(
"runtime",
PluginCapabilities {
buffer_propose_edit: true,
..PluginCapabilities::default()
},
);
let mut handles = crate::plugin::PluginHandleStore::new(
PluginIdentity::try_new("runtime").expect("identity"),
);
let view = crate::ecs::components::buffer::ViewEntity(test_entity(123));
let _view_handle = handles.issue_view(view).expect("view handle");
let _buffer_handle = handles
.issue_buffer(
buffer,
Some(crate::text_stream::TextRevision::from(9)),
Some(view),
)
.expect("buffer handle");
let mut state = store_state(PluginRuntimeLimits::default());
let resources = begin_test_update(&mut state, &host, &handles);
Host::buffer_propose_edit(
&mut state,
resources.buffer_borrow(),
WitBufferEdit::Insert(WitBufferInsert {
byte_index: 0,
text: String::from("plugin edit"),
}),
)
.expect("buffer edit import should queue");
let report = state
.end_update()
.expect("session")
.seal()
.into_parts()
.effects
.drain();
assert_eq!(report.buffer_edits().len(), 1);
assert_eq!(report.buffer_edits()[0].target, buffer);
assert_eq!(
report.buffer_edits()[0].provenance.base_revision(),
crate::text_stream::TextRevision::from(9)
);
assert_eq!(
report.buffer_edits()[0].provenance.source_identity(),
"runtime"
);
assert_eq!(
crate::buffer::BufferEditShape::from_edit(&report.buffer_edits()[0].edit)
.replacement_byte_len(),
Some(11)
);
assert!(report.status_messages().is_empty());
assert!(state.take_import_error().is_none());
}
#[test]
fn denied_buffer_propose_edit_host_import_records_typed_rejection() {
let buffer = crate::ecs::components::buffer::BufferEntity(test_entity(13));
let host = crate::plugin::PluginHostContext::for_test("runtime", PluginCapabilities::default());
let mut handles = crate::plugin::PluginHandleStore::new(
PluginIdentity::try_new("runtime").expect("identity"),
);
let view = crate::ecs::components::buffer::ViewEntity(test_entity(124));
let _view_handle = handles.issue_view(view).expect("view handle");
let _buffer_handle = handles
.issue_buffer(
buffer,
Some(crate::text_stream::TextRevision::from(1)),
Some(view),
)
.expect("buffer handle");
let mut state = store_state(PluginRuntimeLimits::default());
let resources = begin_test_update(&mut state, &host, &handles);
let error = Host::buffer_propose_edit(
&mut state,
resources.buffer_borrow(),
WitBufferEdit::Insert(WitBufferInsert {
byte_index: 0,
text: String::from("secret edit"),
}),
)
.expect_err("buffer edit import should trap");
let import_error = state.take_import_error().expect("typed import error");
let discard = state.end_update().expect("session").discard();
assert!(error.to_string().contains("plugin host import rejected"));
assert!(matches!(
import_error,
crate::plugin::PluginHostImportError::Authorization(_)
));
assert_eq!(discard.effects().discarded_effects(), 0);
assert!(!format!("{error:?}").contains("secret"));
}
#[test]
fn oversized_buffer_propose_edit_host_import_fails_before_queueing() {
let buffer = crate::ecs::components::buffer::BufferEntity(test_entity(14));
let host = crate::plugin::PluginHostContext::for_test(
"runtime",
PluginCapabilities {
buffer_propose_edit: true,
..PluginCapabilities::default()
},
);
let mut handles = crate::plugin::PluginHandleStore::new(
PluginIdentity::try_new("runtime").expect("identity"),
);
let view = crate::ecs::components::buffer::ViewEntity(test_entity(125));
let _view_handle = handles.issue_view(view).expect("view handle");
let _buffer_handle = handles
.issue_buffer(
buffer,
Some(crate::text_stream::TextRevision::from(1)),
Some(view),
)
.expect("buffer handle");
let mut state = store_state(PluginRuntimeLimits {
max_message_bytes: 4,
..PluginRuntimeLimits::default()
});
let resources = begin_test_update(&mut state, &host, &handles);
let error = Host::buffer_propose_edit(
&mut state,
resources.buffer_borrow(),
WitBufferEdit::Insert(WitBufferInsert {
byte_index: 0,
text: String::from("secret edit"),
}),
)
.expect_err("oversized edit should trap");
let import_error = state.take_import_error().expect("typed import error");
let discard = state.end_update().expect("session").discard();
assert!(matches!(
import_error,
crate::plugin::PluginHostImportError::Decode(_)
));
assert_eq!(discard.effects().discarded_effects(), 0);
assert!(!format!("{error:?}").contains("secret"));
assert!(!error.to_string().contains("secret"));
}
#[test]
fn workspace_artifact_write_host_import_queues_deferred_io() {
let root = temp_dir("workspace-artifact-host");
fs::create_dir(root.join("docs")).expect("docs directory");
let filesystem =
crate::fs_utils::FilesystemConfig::from_workspace_root(&root).expect("filesystem");
let host = crate::plugin::PluginHostContext::for_test(
"runtime",
PluginCapabilities {
workspace_artifact_write: vec![WorkspacePathGrant::new("docs")],
..PluginCapabilities::default()
},
);
let mut handles = crate::plugin::PluginHandleStore::new(
PluginIdentity::try_new("runtime").expect("identity"),
);
let view = crate::ecs::components::buffer::ViewEntity(test_entity(15));
let _view_handle = handles.issue_view(view).expect("view handle");
let _buffer_handle = handles
.issue_buffer(
crate::ecs::components::buffer::BufferEntity(test_entity(150)),
Some(TextRevision::from(0)),
Some(view),
)
.expect("buffer handle");
let mut state = store_state(PluginRuntimeLimits::default());
let _resources = begin_test_update(&mut state, &host, &handles);
Host::workspace_artifact_write(
&mut state,
String::from("docs/output.txt"),
b"artifact bytes".to_vec(),
)
.expect("workspace write import should queue");
let batches = seal_successful_test_update(&mut state).into_parts();
let effect_report = batches.effects.drain();
let workspace_report = batches.workspace_io.execute_synchronous(&filesystem);
assert!(effect_report.status_messages().is_empty());
assert!(effect_report.buffer_edits().is_empty());
assert_eq!(workspace_report.identity(), "runtime");
assert_eq!(workspace_report.completions().len(), 1);
assert_eq!(
workspace_report.completions()[0].kind(),
crate::plugin::WorkspaceAccessKind::Write
);
assert_eq!(
workspace_report.completions()[0].workspace_relative_path(),
"docs/output.txt"
);
assert_eq!(
workspace_report.completions()[0].workspace_path().as_str(),
"docs/output.txt"
);
assert!(
workspace_report.completions()[0]
.as_write()
.expect("completion should be a write")
.outcome()
.is_ok()
);
assert_eq!(
fs::read(root.join("docs/output.txt")).expect("written artifact"),
b"artifact bytes"
);
assert!(state.take_import_error().is_none());
let _cleanup = fs::remove_dir_all(root);
}
#[test]
fn denied_workspace_artifact_write_host_import_records_typed_rejection() {
let host = crate::plugin::PluginHostContext::for_test("runtime", PluginCapabilities::default());
let mut handles = crate::plugin::PluginHandleStore::new(
PluginIdentity::try_new("runtime").expect("identity"),
);
let view = crate::ecs::components::buffer::ViewEntity(test_entity(16));
let _view_handle = handles.issue_view(view).expect("view handle");
let _buffer_handle = handles
.issue_buffer(
crate::ecs::components::buffer::BufferEntity(test_entity(160)),
Some(TextRevision::from(0)),
Some(view),
)
.expect("buffer handle");
let mut state = store_state(PluginRuntimeLimits::default());
let _resources = begin_test_update(&mut state, &host, &handles);
let error = Host::workspace_artifact_write(
&mut state,
String::from("docs/output.txt"),
b"secret artifact".to_vec(),
)
.expect_err("workspace write import should trap");
let import_error = state.take_import_error().expect("typed import error");
let discard = state.end_update().expect("session").discard();
assert!(error.to_string().contains("plugin host import rejected"));
assert!(matches!(
import_error,
crate::plugin::PluginHostImportError::Authorization(_)
));
assert_eq!(discard.workspace_io().discarded_requests(), 0);
assert!(!format!("{error:?}").contains("secret"));
}
#[test]
fn invalid_workspace_artifact_write_path_fails_before_queueing() {
let host = crate::plugin::PluginHostContext::for_test(
"runtime",
PluginCapabilities {
workspace_artifact_write: vec![WorkspacePathGrant::new("docs")],
..PluginCapabilities::default()
},
);
let mut handles = crate::plugin::PluginHandleStore::new(
PluginIdentity::try_new("runtime").expect("identity"),
);
let view = crate::ecs::components::buffer::ViewEntity(test_entity(17));
let _view_handle = handles.issue_view(view).expect("view handle");
let _buffer_handle = handles
.issue_buffer(
crate::ecs::components::buffer::BufferEntity(test_entity(170)),
Some(TextRevision::from(0)),
Some(view),
)
.expect("buffer handle");
let mut state = store_state(PluginRuntimeLimits::default());
let _resources = begin_test_update(&mut state, &host, &handles);
let error = Host::workspace_artifact_write(
&mut state,
String::from("docs/../secret.txt"),
b"secret artifact".to_vec(),
)
.expect_err("invalid path should trap");
let import_error = state.take_import_error().expect("typed import error");
let discard = state.end_update().expect("session").discard();
assert!(matches!(
import_error,
crate::plugin::PluginHostImportError::Decode(_)
));
assert_eq!(discard.workspace_io().discarded_requests(), 0);
assert!(!format!("{error:?}").contains("secret"));
assert!(!error.to_string().contains("secret"));
}
#[test]
fn oversized_workspace_artifact_write_payload_fails_before_queueing() {
let host = crate::plugin::PluginHostContext::for_test(
"runtime",
PluginCapabilities {
workspace_artifact_write: vec![WorkspacePathGrant::new("docs")],
..PluginCapabilities::default()
},
);
let mut handles = crate::plugin::PluginHandleStore::new(
PluginIdentity::try_new("runtime").expect("identity"),
);
let view = crate::ecs::components::buffer::ViewEntity(test_entity(18));
let _view_handle = handles.issue_view(view).expect("view handle");
let _buffer_handle = handles
.issue_buffer(
crate::ecs::components::buffer::BufferEntity(test_entity(180)),
Some(TextRevision::from(0)),
Some(view),
)
.expect("buffer handle");
let mut state = store_state(PluginRuntimeLimits {
max_message_bytes: 4,
..PluginRuntimeLimits::default()
});
let _resources = begin_test_update(&mut state, &host, &handles);
let error = Host::workspace_artifact_write(
&mut state,
String::from("docs"),
b"secret artifact".to_vec(),
)
.expect_err("oversized write should trap");
let import_error = state.take_import_error().expect("typed import error");
let discard = state.end_update().expect("session").discard();
assert!(matches!(
import_error,
crate::plugin::PluginHostImportError::Decode(_)
));
assert_eq!(discard.workspace_io().discarded_requests(), 0);
assert!(!format!("{error:?}").contains("secret"));
assert!(!error.to_string().contains("secret"));
}
#[test]
fn workspace_observe_task_host_imports_start_poll_and_take() {
let root = temp_dir("workspace-observe-task-host");
fs::create_dir(root.join("docs")).expect("docs directory");
fs::write(root.join("docs/input.txt"), b"observed bytes").expect("fixture");
let filesystem =
crate::fs_utils::FilesystemConfig::from_workspace_root(&root).expect("filesystem");
let host = crate::plugin::PluginHostContext::for_test(
"runtime",
PluginCapabilities {
workspace_observe: vec![WorkspacePathGrant::new("docs")],
..PluginCapabilities::default()
},
);
let mut handles = crate::plugin::PluginHandleStore::new(
PluginIdentity::try_new("runtime").expect("identity"),
);
let view = crate::ecs::components::buffer::ViewEntity(test_entity(31));
let _view_handle = handles.issue_view(view).expect("view handle");
let _buffer_handle = handles
.issue_buffer(
crate::ecs::components::buffer::BufferEntity(test_entity(310)),
Some(TextRevision::from(0)),
Some(view),
)
.expect("buffer handle");
let mut state = store_state(PluginRuntimeLimits::default());
let _resources = begin_test_update(&mut state, &host, &handles);
let task = Host::workspace_observe_start(&mut state, String::from("docs/input.txt"))
.expect("workspace observe start should return a task resource");
let task_borrow = wasmtime::component::Resource::<
crate::plugin::PluginWorkspaceObserveTaskHandle,
>::new_borrow(task.rep());
assert!(matches!(
Host::workspace_observe_poll(&mut state, task_borrow)
.expect("same-update poll should succeed"),
WitWorkspaceObserveTaskPoll::Pending
));
let batches = seal_successful_test_update(&mut state).into_parts();
let enqueue = state
.workspace_observe_tasks
.enqueue(batches.workspace_observe_tasks)
.expect("task batch should enqueue");
assert_eq!(enqueue.task_handles().len(), 1);
let completions = state.workspace_observe_tasks.execute_all(&filesystem);
assert_eq!(completions.len(), 1);
let _resources = begin_test_update(&mut state, &host, &handles);
let task_borrow = wasmtime::component::Resource::<
crate::plugin::PluginWorkspaceObserveTaskHandle,
>::new_borrow(task.rep());
assert!(matches!(
Host::workspace_observe_poll(&mut state, task_borrow)
.expect("completed poll should succeed"),
WitWorkspaceObserveTaskPoll::Completed(WitWorkspaceObserveOutcomeShape::Bytes(14))
));
let task_borrow = wasmtime::component::Resource::<
crate::plugin::PluginWorkspaceObserveTaskHandle,
>::new_borrow(task.rep());
assert!(matches!(
Host::workspace_observe_take(&mut state, task_borrow)
.expect("completed take should succeed"),
WitWorkspaceObserveTaskTake::Bytes(bytes) if bytes == b"observed bytes"
));
let task_borrow = wasmtime::component::Resource::<
crate::plugin::PluginWorkspaceObserveTaskHandle,
>::new_borrow(task.rep());
assert!(matches!(
Host::workspace_observe_take(&mut state, task_borrow)
.expect("second take should not expose bytes"),
WitWorkspaceObserveTaskTake::Unknown
));
HostWorkspaceObserveTask::drop(&mut state, task).expect("task resource should drop");
let _sealed = state.end_update().expect("session").seal();
assert!(state.take_import_error().is_none());
let _cleanup = fs::remove_dir_all(root);
}
#[test]
fn workspace_observe_task_drop_before_commit_removes_pending_request() {
let host = crate::plugin::PluginHostContext::for_test(
"runtime",
PluginCapabilities {
workspace_observe: vec![WorkspacePathGrant::new("docs")],
..PluginCapabilities::default()
},
);
let mut handles = crate::plugin::PluginHandleStore::new(
PluginIdentity::try_new("runtime").expect("identity"),
);
let view = crate::ecs::components::buffer::ViewEntity(test_entity(33));
let _view_handle = handles.issue_view(view).expect("view handle");
let _buffer_handle = handles
.issue_buffer(
crate::ecs::components::buffer::BufferEntity(test_entity(330)),
Some(TextRevision::from(0)),
Some(view),
)
.expect("buffer handle");
let mut state = store_state(PluginRuntimeLimits::default());
let _resources = begin_test_update(&mut state, &host, &handles);
let task = Host::workspace_observe_start(&mut state, String::from("docs/input.txt"))
.expect("workspace observe start should return a task resource");
assert_eq!(state.workspace_observe_tasks.live_resource_len(), 1);
assert_eq!(state.workspace_observe_tasks.reserved_pending_len(), 1);
HostWorkspaceObserveTask::drop(&mut state, task)
.expect("dropping an uncommitted task resource should cancel it");
assert_eq!(state.workspace_observe_tasks.live_resource_len(), 0);
assert_eq!(state.workspace_observe_tasks.reserved_pending_len(), 0);
let batches = seal_successful_test_update(&mut state).into_parts();
assert!(batches.workspace_observe_tasks.is_empty());
assert!(state.take_import_error().is_none());
}
#[test]
fn workspace_observe_task_cancellation_removes_uncommitted_start() {
let identity = PluginIdentity::try_new("runtime").expect("identity");
let host = crate::plugin::PluginHostContext::for_test(
identity.as_str(),
PluginCapabilities {
workspace_observe: vec![WorkspacePathGrant::new("docs")],
..PluginCapabilities::default()
},
);
let mut handles = crate::plugin::PluginHandleStore::new(identity.clone());
let view = crate::ecs::components::buffer::ViewEntity(test_entity(39));
let _view_handle = handles.issue_view(view).expect("view handle");
let _buffer_handle = handles
.issue_buffer(
crate::ecs::components::buffer::BufferEntity(test_entity(390)),
Some(TextRevision::from(0)),
Some(view),
)
.expect("buffer handle");
let mut state = store_state(PluginRuntimeLimits::default());
let _resources = begin_test_update(&mut state, &host, &handles);
let task = Host::workspace_observe_start(&mut state, String::from("docs/input.txt"))
.expect("workspace observe start should return a task resource");
assert_eq!(state.workspace_observe_tasks.live_resource_len(), 1);
assert_eq!(state.workspace_observe_tasks.reserved_pending_len(), 1);
let report = state.cancel_workspace_observe_tasks(&identity);
assert_eq!(report.canceled_pending(), 0);
assert_eq!(report.discarded_retained(), 0);
assert_eq!(state.workspace_observe_tasks.live_resource_len(), 0);
assert_eq!(state.workspace_observe_tasks.reserved_pending_len(), 0);
HostWorkspaceObserveTask::drop(&mut state, task)
.expect("dropping a canceled uncommitted task resource should clean up");
assert_eq!(state.workspace_observe_tasks.live_resource_len(), 0);
assert_eq!(state.workspace_observe_tasks.reserved_pending_len(), 0);
assert!(state.take_import_error().is_none());
let batches = seal_successful_test_update(&mut state).into_parts();
assert!(batches.workspace_observe_tasks.is_empty());
}
#[test]
fn workspace_observe_task_shutdown_cleanup_removes_uncommitted_start() {
let identity = PluginIdentity::try_new("runtime").expect("identity");
let host = crate::plugin::PluginHostContext::for_test(
identity.as_str(),
PluginCapabilities {
workspace_observe: vec![WorkspacePathGrant::new("docs")],
..PluginCapabilities::default()
},
);
let mut handles = crate::plugin::PluginHandleStore::new(identity);
let view = crate::ecs::components::buffer::ViewEntity(test_entity(40));
let _view_handle = handles.issue_view(view).expect("view handle");
let _buffer_handle = handles
.issue_buffer(
crate::ecs::components::buffer::BufferEntity(test_entity(400)),
Some(TextRevision::from(0)),
Some(view),
)
.expect("buffer handle");
let mut state = store_state(PluginRuntimeLimits::default());
let _resources = begin_test_update(&mut state, &host, &handles);
let task = Host::workspace_observe_start(&mut state, String::from("docs/input.txt"))
.expect("workspace observe start should return a task resource");
assert_eq!(state.workspace_observe_tasks.live_resource_len(), 1);
assert_eq!(state.workspace_observe_tasks.reserved_pending_len(), 1);
let reports = state.cancel_all_workspace_observe_tasks();
assert!(reports.is_empty());
assert_eq!(state.workspace_observe_tasks.live_resource_len(), 0);
assert_eq!(state.workspace_observe_tasks.reserved_pending_len(), 0);
HostWorkspaceObserveTask::drop(&mut state, task)
.expect("dropping a shutdown-canceled task resource should clean up");
assert!(state.take_import_error().is_none());
let batches = seal_successful_test_update(&mut state).into_parts();
assert!(batches.workspace_observe_tasks.is_empty());
}
#[test]
fn workspace_observe_task_drop_wrong_resource_records_redacted_rejection() {
let host = crate::plugin::PluginHostContext::for_test(
"runtime",
PluginCapabilities {
workspace_observe: vec![WorkspacePathGrant::new("docs")],
..PluginCapabilities::default()
},
);
let mut handles = crate::plugin::PluginHandleStore::new(
PluginIdentity::try_new("runtime").expect("identity"),
);
let view = crate::ecs::components::buffer::ViewEntity(test_entity(38));
let _view_handle = handles.issue_view(view).expect("view handle");
let _buffer_handle = handles
.issue_buffer(
crate::ecs::components::buffer::BufferEntity(test_entity(380)),
Some(TextRevision::from(0)),
Some(view),
)
.expect("buffer handle");
let mut state = store_state(PluginRuntimeLimits::default());
let resources = begin_test_update(&mut state, &host, &handles);
let wrong_task_resource = wasmtime::component::Resource::<
crate::plugin::PluginWorkspaceObserveTaskHandle,
>::new_own(resources.view_borrow().rep());
let error = HostWorkspaceObserveTask::drop(&mut state, wrong_task_resource)
.expect_err("task drop should reject a view resource rep");
let import_error = state.take_import_error().expect("typed import error");
assert!(error.to_string().contains("plugin host import rejected"));
assert!(matches!(
import_error,
crate::plugin::PluginHostImportError::ResourceHandle {
kind: crate::plugin::PluginHostResourceKind::WorkspaceObserveTask,
reason: crate::plugin::PluginResourceHandleRejectionReason::WrongType,
}
));
let runtime_error = WasmtimePluginRuntimeError::HostImport {
identity: PluginIdentity::try_new("runtime").expect("identity"),
export: PluginOperationalExport::Update,
import: PluginOperationalImport::WorkspaceObserveTaskDrop,
source: import_error,
};
let event = runtime_error.operational_event().expect("event");
assert_eq!(
event.to_string(),
"plugin \"runtime\" rejected import workspace-observe-task.drop: resource-handle"
);
let event_debug = format!("{event:?}");
assert!(!event_debug.contains("ViewEntity"));
assert!(!event_debug.contains("WrongType"));
assert!(!event.to_string().contains("wrong-type"));
let _discard = state.end_update().expect("session").discard();
}
#[test]
fn workspace_observe_task_revocation_cleanup_cancels_queue_and_resources() {
let root = temp_dir("workspace-observe-task-revoke");
fs::create_dir(root.join("docs")).expect("docs directory");
fs::write(root.join("docs/first.txt"), b"first bytes").expect("first fixture");
fs::write(root.join("docs/second.txt"), b"second bytes").expect("second fixture");
let filesystem =
crate::fs_utils::FilesystemConfig::from_workspace_root(&root).expect("filesystem");
let identity = PluginIdentity::try_new("runtime").expect("identity");
let host = crate::plugin::PluginHostContext::for_test(
identity.as_str(),
PluginCapabilities {
workspace_observe: vec![WorkspacePathGrant::new("docs")],
..PluginCapabilities::default()
},
);
let mut handles = crate::plugin::PluginHandleStore::new(identity.clone());
let view = crate::ecs::components::buffer::ViewEntity(test_entity(33));
let _view_handle = handles.issue_view(view).expect("view handle");
let _buffer_handle = handles
.issue_buffer(
crate::ecs::components::buffer::BufferEntity(test_entity(330)),
Some(TextRevision::from(0)),
Some(view),
)
.expect("buffer handle");
let mut state = store_state(PluginRuntimeLimits::default());
let _resources = begin_test_update(&mut state, &host, &handles);
let first = Host::workspace_observe_start(&mut state, String::from("docs/first.txt"))
.expect("first task resource");
let first_handle = state
.resources
.get(&first)
.expect("first task handle")
.clone();
let second = Host::workspace_observe_start(&mut state, String::from("docs/second.txt"))
.expect("second task resource");
let second_handle = state
.resources
.get(&second)
.expect("second task handle")
.clone();
let batches = seal_successful_test_update(&mut state).into_parts();
let enqueue = state
.workspace_observe_tasks
.enqueue(batches.workspace_observe_tasks)
.expect("task batch should enqueue");
assert_eq!(enqueue.task_handles().len(), 2);
let _completed = state
.workspace_observe_tasks
.execute_next(&filesystem)
.expect("one task should execute");
assert_eq!(state.workspace_observe_tasks.pending_len(), 1);
assert_eq!(state.workspace_observe_tasks.retained_len(), 1);
assert_eq!(state.workspace_observe_tasks.live_resource_len(), 2);
let report = state.cancel_workspace_observe_tasks(&identity);
assert_eq!(report.identity(), "runtime");
assert_eq!(report.canceled_pending(), 1);
assert_eq!(report.discarded_retained(), 1);
assert_eq!(state.workspace_observe_tasks.pending_len(), 0);
assert_eq!(state.workspace_observe_tasks.retained_len(), 0);
assert_eq!(state.workspace_observe_tasks.live_resource_len(), 0);
assert!(matches!(
state.workspace_observe_tasks.poll(&first_handle),
crate::plugin::PluginWorkspaceObserveTaskPoll::Unknown
));
assert!(matches!(
state.workspace_observe_tasks.poll(&second_handle),
crate::plugin::PluginWorkspaceObserveTaskPoll::Unknown
));
let second_report = state.cancel_workspace_observe_tasks(&identity);
assert_eq!(second_report.canceled_pending(), 0);
assert_eq!(second_report.discarded_retained(), 0);
let _cleanup = fs::remove_dir_all(root);
}
#[test]
fn denied_workspace_observe_task_start_records_typed_rejection() {
let host = crate::plugin::PluginHostContext::for_test("runtime", PluginCapabilities::default());
let mut handles = crate::plugin::PluginHandleStore::new(
PluginIdentity::try_new("runtime").expect("identity"),
);
let view = crate::ecs::components::buffer::ViewEntity(test_entity(32));
let _view_handle = handles.issue_view(view).expect("view handle");
let _buffer_handle = handles
.issue_buffer(
crate::ecs::components::buffer::BufferEntity(test_entity(320)),
Some(TextRevision::from(0)),
Some(view),
)
.expect("buffer handle");
let mut state = store_state(PluginRuntimeLimits::default());
let _resources = begin_test_update(&mut state, &host, &handles);
let error = Host::workspace_observe_start(&mut state, String::from("docs/input.txt"))
.expect_err("denied workspace observe should trap");
let import_error = state.take_import_error().expect("typed import error");
let discard = state.end_update().expect("session").discard();
assert!(error.to_string().contains("plugin host import rejected"));
assert!(matches!(
import_error,
crate::plugin::PluginHostImportError::Authorization(_)
));
assert_eq!(discard.workspace_observe_tasks().discarded_tasks(), 0);
assert_eq!(state.workspace_observe_tasks.reserved_pending_len(), 0);
assert!(!format!("{error:?}").contains("docs/input.txt"));
}
#[test]
fn denied_workspace_observe_task_poll_invalidates_resource_before_queue_access() {
let host = crate::plugin::PluginHostContext::for_test(
"runtime",
PluginCapabilities {
workspace_observe: vec![WorkspacePathGrant::new("docs/private")],
..PluginCapabilities::default()
},
);
let denied_host = crate::plugin::PluginHostContext::for_test(
"runtime",
PluginCapabilities {
workspace_observe: vec![WorkspacePathGrant::new("docs/public")],
..PluginCapabilities::default()
},
);
let mut handles = crate::plugin::PluginHandleStore::new(
PluginIdentity::try_new("runtime").expect("identity"),
);
let view = crate::ecs::components::buffer::ViewEntity(test_entity(34));
let _view_handle = handles.issue_view(view).expect("view handle");
let _buffer_handle = handles
.issue_buffer(
crate::ecs::components::buffer::BufferEntity(test_entity(340)),
Some(TextRevision::from(0)),
Some(view),
)
.expect("buffer handle");
let mut state = store_state(PluginRuntimeLimits::default());
let _resources = begin_test_update(&mut state, &host, &handles);
let task = Host::workspace_observe_start(&mut state, String::from("docs/private/input.txt"))
.expect("workspace observe start should return task resource");
let batches = seal_successful_test_update(&mut state).into_parts();
let _discard = batches.workspace_observe_tasks.discard();
let _resources = begin_test_update(&mut state, &denied_host, &handles);
let task_borrow = wasmtime::component::Resource::<
crate::plugin::PluginWorkspaceObserveTaskHandle,
>::new_borrow(task.rep());
let error = Host::workspace_observe_poll(&mut state, task_borrow)
.expect_err("poll should require current observe authority");
let import_error = state.take_import_error().expect("typed import error");
let _discard = state.end_update().expect("session").discard();
assert!(error.to_string().contains("plugin host import rejected"));
assert!(matches!(
import_error,
crate::plugin::PluginHostImportError::Authorization(
crate::plugin::PluginAuthorizationError::Denied {
capability: crate::plugin::PluginCapabilityShape::WorkspaceObserve,
..
}
)
));
assert_eq!(state.workspace_observe_tasks.live_resource_len(), 0);
}
#[test]
fn invalidated_workspace_observe_task_drop_is_idempotent() {
let host = crate::plugin::PluginHostContext::for_test(
"runtime",
PluginCapabilities {
workspace_observe: vec![WorkspacePathGrant::new("docs/private")],
..PluginCapabilities::default()
},
);
let denied_host = crate::plugin::PluginHostContext::for_test(
"runtime",
PluginCapabilities {
workspace_observe: vec![WorkspacePathGrant::new("docs/public")],
..PluginCapabilities::default()
},
);
let mut handles = crate::plugin::PluginHandleStore::new(
PluginIdentity::try_new("runtime").expect("identity"),
);
let view = crate::ecs::components::buffer::ViewEntity(test_entity(36));
let _view_handle = handles.issue_view(view).expect("view handle");
let _buffer_handle = handles
.issue_buffer(
crate::ecs::components::buffer::BufferEntity(test_entity(360)),
Some(TextRevision::from(0)),
Some(view),
)
.expect("buffer handle");
let mut state = store_state(PluginRuntimeLimits::default());
let _resources = begin_test_update(&mut state, &host, &handles);
let task = Host::workspace_observe_start(&mut state, String::from("docs/private/input.txt"))
.expect("workspace observe start should return task resource");
let batches = seal_successful_test_update(&mut state).into_parts();
let _discard = batches.workspace_observe_tasks.discard();
let _resources = begin_test_update(&mut state, &denied_host, &handles);
let task_borrow = wasmtime::component::Resource::<
crate::plugin::PluginWorkspaceObserveTaskHandle,
>::new_borrow(task.rep());
let error = Host::workspace_observe_poll(&mut state, task_borrow)
.expect_err("poll should require current observe authority");
let import_error = state.take_import_error().expect("typed import error");
assert!(error.to_string().contains("plugin host import rejected"));
assert!(matches!(
import_error,
crate::plugin::PluginHostImportError::Authorization(_)
));
assert_eq!(state.workspace_observe_tasks.live_resource_len(), 0);
HostWorkspaceObserveTask::drop(&mut state, task)
.expect("drop of a host-invalidated task resource should be a no-op");
assert!(state.take_import_error().is_none());
assert_eq!(state.workspace_observe_tasks.live_resource_len(), 0);
let _discard = state.end_update().expect("session").discard();
}
#[test]
fn invalidated_workspace_observe_task_resource_cannot_alias_new_task() {
let host = crate::plugin::PluginHostContext::for_test(
"runtime",
PluginCapabilities {
workspace_observe: vec![WorkspacePathGrant::new("docs/private")],
..PluginCapabilities::default()
},
);
let denied_host = crate::plugin::PluginHostContext::for_test(
"runtime",
PluginCapabilities {
workspace_observe: vec![WorkspacePathGrant::new("docs/public")],
..PluginCapabilities::default()
},
);
let mut handles = crate::plugin::PluginHandleStore::new(
PluginIdentity::try_new("runtime").expect("identity"),
);
let view = crate::ecs::components::buffer::ViewEntity(test_entity(37));
let _view_handle = handles.issue_view(view).expect("view handle");
let _buffer_handle = handles
.issue_buffer(
crate::ecs::components::buffer::BufferEntity(test_entity(370)),
Some(TextRevision::from(0)),
Some(view),
)
.expect("buffer handle");
let mut state = store_state(PluginRuntimeLimits::default());
let _resources = begin_test_update(&mut state, &host, &handles);
let old_task_resource =
Host::workspace_observe_start(&mut state, String::from("docs/private/input.txt"))
.expect("workspace observe start should return task resource");
let batches = seal_successful_test_update(&mut state).into_parts();
let _discard = batches.workspace_observe_tasks.discard();
let _resources = begin_test_update(&mut state, &denied_host, &handles);
let stale_borrow = wasmtime::component::Resource::<
crate::plugin::PluginWorkspaceObserveTaskHandle,
>::new_borrow(old_task_resource.rep());
let _error = Host::workspace_observe_poll(&mut state, stale_borrow)
.expect_err("poll should require current observe authority");
let import_error = state.take_import_error().expect("typed import error");
assert!(matches!(
import_error,
crate::plugin::PluginHostImportError::Authorization(_)
));
let _discard = state.end_update().expect("session").discard();
assert_eq!(state.workspace_observe_tasks.live_resource_len(), 0);
let _resources = begin_test_update(&mut state, &host, &handles);
let next = Host::workspace_observe_start(&mut state, String::from("docs/private/next.txt"))
.expect("new task should not reuse the invalidated rep");
assert_ne!(next.rep(), old_task_resource.rep());
let stale_borrow = wasmtime::component::Resource::<
crate::plugin::PluginWorkspaceObserveTaskHandle,
>::new_borrow(old_task_resource.rep());
let _error = Host::workspace_observe_poll(&mut state, stale_borrow)
.expect_err("invalidated resource should stay stale");
let import_error = state.take_import_error().expect("typed import error");
assert!(matches!(
import_error,
crate::plugin::PluginHostImportError::ResourceHandle {
kind: crate::plugin::PluginHostResourceKind::WorkspaceObserveTask,
reason: crate::plugin::PluginResourceHandleRejectionReason::Missing,
}
));
HostWorkspaceObserveTask::drop(&mut state, old_task_resource)
.expect("dropping invalidated stale resource should not affect next task");
assert_eq!(state.workspace_observe_tasks.live_resource_len(), 1);
let next_borrow = wasmtime::component::Resource::<
crate::plugin::PluginWorkspaceObserveTaskHandle,
>::new_borrow(next.rep());
assert!(matches!(
Host::workspace_observe_poll(&mut state, next_borrow)
.expect("new task should still be usable"),
WitWorkspaceObserveTaskPoll::Pending
));
HostWorkspaceObserveTask::drop(&mut state, next).expect("new task resource should drop");
assert_eq!(state.workspace_observe_tasks.live_resource_len(), 0);
let _discard = state.end_update().expect("session").discard();
}
#[test]
fn denied_workspace_observe_task_take_invalidates_resource_before_queue_access() {
let host = crate::plugin::PluginHostContext::for_test(
"runtime",
PluginCapabilities {
workspace_observe: vec![WorkspacePathGrant::new("docs/private")],
..PluginCapabilities::default()
},
);
let denied_host = crate::plugin::PluginHostContext::for_test(
"runtime",
PluginCapabilities {
workspace_observe: vec![WorkspacePathGrant::new("docs/public")],
..PluginCapabilities::default()
},
);
let mut handles = crate::plugin::PluginHandleStore::new(
PluginIdentity::try_new("runtime").expect("identity"),
);
let view = crate::ecs::components::buffer::ViewEntity(test_entity(35));
let _view_handle = handles.issue_view(view).expect("view handle");
let _buffer_handle = handles
.issue_buffer(
crate::ecs::components::buffer::BufferEntity(test_entity(350)),
Some(TextRevision::from(0)),
Some(view),
)
.expect("buffer handle");
let mut state = store_state(PluginRuntimeLimits::default());
let _resources = begin_test_update(&mut state, &host, &handles);
let task = Host::workspace_observe_start(&mut state, String::from("docs/private/input.txt"))
.expect("workspace observe start should return task resource");
let batches = seal_successful_test_update(&mut state).into_parts();
let _discard = batches.workspace_observe_tasks.discard();
let _resources = begin_test_update(&mut state, &denied_host, &handles);
let task_borrow = wasmtime::component::Resource::<
crate::plugin::PluginWorkspaceObserveTaskHandle,
>::new_borrow(task.rep());
let error = Host::workspace_observe_take(&mut state, task_borrow)
.expect_err("take should require current observe authority");
let import_error = state.take_import_error().expect("typed import error");
let _discard = state.end_update().expect("session").discard();
assert!(error.to_string().contains("plugin host import rejected"));
assert!(matches!(
import_error,
crate::plugin::PluginHostImportError::Authorization(
crate::plugin::PluginAuthorizationError::Denied {
capability: crate::plugin::PluginCapabilityShape::WorkspaceObserve,
..
}
)
));
assert_eq!(state.workspace_observe_tasks.live_resource_len(), 0);
}
#[test]
fn component_status_publish_queues_status_effect_through_runtime_update() {
let runtime = WasmtimePluginRuntime::new().expect("runtime");
let view = crate::ecs::components::buffer::ViewEntity(test_entity(10));
let spec = PluginRuntimeSpec::new(
AuthorizedPluginComponent::for_test(
PluginIdentity::try_new("component-status").expect("identity"),
EscapedDisplayText::from_display_text("plugins/component-status.wasm"),
status_publish_component_fixture("component ok"),
),
PluginWitWorld::current(),
PluginCapabilities {
status_publish: true,
..PluginCapabilities::default()
},
PluginRuntimeLimits::default()
.validate()
.expect("limits should validate"),
);
let mut component = runtime.instantiate(&spec).expect("component should load");
let mut state = PluginInstanceState::new(spec);
issue_update_handles(
&mut state,
view,
crate::ecs::components::buffer::BufferEntity(test_entity(100)),
TextRevision::from(0),
);
let report = component
.update(&state)
.expect("component update should return")
.into_parts()
.effects
.drain();
assert!(report.buffer_edits().is_empty());
assert_eq!(
report.status_messages(),
&[crate::ecs::events::status::StatusMessageRequested {
target: view,
message: crate::ecs::events::status::StatusMessage::Info(
crate::StatusInfoText::escaped_display("component ok")
),
}]
);
}
#[test]
fn denied_component_status_publish_discards_runtime_update() {
let runtime = WasmtimePluginRuntime::new().expect("runtime");
let view = crate::ecs::components::buffer::ViewEntity(test_entity(11));
let spec = PluginRuntimeSpec::new(
AuthorizedPluginComponent::for_test(
PluginIdentity::try_new("component-denied").expect("identity"),
EscapedDisplayText::from_display_text("plugins/component-denied.wasm"),
status_publish_component_fixture("secret component status"),
),
PluginWitWorld::current(),
PluginCapabilities::default(),
PluginRuntimeLimits::default()
.validate()
.expect("limits should validate"),
);
let mut component = runtime.instantiate(&spec).expect("component should load");
let mut state = PluginInstanceState::new(spec);
issue_update_handles(
&mut state,
view,
crate::ecs::components::buffer::BufferEntity(test_entity(101)),
TextRevision::from(0),
);
let error = component
.update(&state)
.expect_err("denied status import should fail update");
assert!(matches!(
&error,
WasmtimePluginRuntimeError::UpdateFailed { source, discarded }
if matches!(
source.as_ref(),
WasmtimePluginRuntimeError::HostImport {
source: crate::plugin::PluginHostImportError::Authorization(_),
..
}
) && discarded.effects().discarded_effects() == 0
));
assert_eq!(
error
.operational_event()
.expect("denial should emit event")
.to_string(),
"plugin \"component-denied\" denied import status.publish"
);
assert!(!format!("{error:?}").contains("secret"));
assert!(!error.to_string().contains("secret"));
}
#[test]
fn component_update_requires_active_update_context_before_guest_call() {
let runtime = WasmtimePluginRuntime::new().expect("runtime");
let spec = PluginRuntimeSpec::new(
AuthorizedPluginComponent::for_test(
PluginIdentity::try_new("component-missing-update").expect("identity"),
EscapedDisplayText::from_display_text("plugins/component-missing-update.wasm"),
status_publish_component_fixture("secret component status"),
),
PluginWitWorld::current(),
PluginCapabilities {
status_publish: true,
..PluginCapabilities::default()
},
PluginRuntimeLimits::default()
.validate()
.expect("limits should validate"),
);
let mut component = runtime.instantiate(&spec).expect("component should load");
let state = PluginInstanceState::new(spec);
let error = component
.update(&state)
.expect_err("missing update handles should fail before guest execution");
assert!(matches!(
&error,
&WasmtimePluginRuntimeError::SchedulingDenied(
crate::plugin::PluginInstanceAccessError::MissingUpdateView
)
));
assert!(!format!("{error:?}").contains("secret"));
assert!(!error.to_string().contains("secret"));
}
#[test]
fn component_update_rejects_state_from_another_plugin_before_guest_call() {
let runtime = WasmtimePluginRuntime::new().expect("runtime");
let runtime_spec = PluginRuntimeSpec::new(
AuthorizedPluginComponent::for_test(
PluginIdentity::try_new("component-runtime").expect("identity"),
EscapedDisplayText::from_display_text("plugins/component-runtime.wasm"),
status_publish_component_fixture("secret runtime status"),
),
PluginWitWorld::current(),
PluginCapabilities {
status_publish: true,
..PluginCapabilities::default()
},
PluginRuntimeLimits::default()
.validate()
.expect("limits should validate"),
);
let state_spec = PluginRuntimeSpec::new(
AuthorizedPluginComponent::for_test(
PluginIdentity::try_new("component-state").expect("identity"),
EscapedDisplayText::from_display_text("plugins/component-state.wasm"),
status_publish_component_fixture("secret state status"),
),
PluginWitWorld::current(),
PluginCapabilities {
status_publish: true,
..PluginCapabilities::default()
},
PluginRuntimeLimits::default()
.validate()
.expect("limits should validate"),
);
let mut component = runtime
.instantiate(&runtime_spec)
.expect("component should load");
let state = PluginInstanceState::new(state_spec);
let error = component
.update(&state)
.expect_err("mismatched state should fail before guest execution");
assert!(matches!(
&error,
WasmtimePluginRuntimeError::InstanceStateMismatch {
runtime_identity,
state_identity,
} if runtime_identity.as_str() == "component-runtime"
&& state_identity.as_str() == "component-state"
));
assert_eq!(
error.phase(),
WasmtimePluginRuntimePhase::InstanceStateMismatch
);
assert_eq!(
error.operational_event().expect("event").kind(),
crate::plugin::PluginOperationalEventKind::RuntimeFailure {
failure: crate::plugin::PluginOperationalRuntimeFailure::InstanceStateMismatch,
}
);
assert!(!format!("{error:?}").contains("secret"));
assert!(!error.to_string().contains("secret"));
}
enum ExpectedRuntimeFailure {
GuestTrap,
Timeout {
timeout_ms: u64,
event: &'static str,
},
HostDecode,
}
impl ExpectedRuntimeFailure {
fn assert_matches(&self, error: &WasmtimePluginRuntimeError) {
let WasmtimePluginRuntimeError::UpdateFailed { source, discarded } = error else {
panic!("expected failed update with discard report, got {error:?}");
};
assert_eq!(discarded.effects().discarded_effects(), 1);
assert_eq!(discarded.workspace_io().discarded_requests(), 0);
match self {
Self::GuestTrap => assert!(matches!(
source.as_ref(),
WasmtimePluginRuntimeError::GuestTrap {
export: PluginOperationalExport::Update,
..
}
)),
Self::Timeout { timeout_ms, event } => {
assert!(matches!(
source.as_ref(),
WasmtimePluginRuntimeError::Timeout {
export: PluginOperationalExport::Update,
timeout_ms: actual,
..
} if actual == timeout_ms
));
assert_eq!(
error
.operational_event()
.expect("timeout should emit event")
.to_string(),
*event
);
}
Self::HostDecode => assert!(matches!(
source.as_ref(),
WasmtimePluginRuntimeError::HostImport {
source: crate::plugin::PluginHostImportError::Decode(_),
..
}
)),
}
}
}
fn assert_abusive_component_discards_queued_effect(
identity: &str,
component_bytes: Vec<u8>,
capabilities: PluginCapabilities,
limits: crate::plugin::ValidatedPluginRuntimeLimits,
issue_handles: impl FnOnce(&mut PluginInstanceState),
expected: &ExpectedRuntimeFailure,
redacted_needles: &[&str],
) {
let runtime = WasmtimePluginRuntime::new().expect("runtime");
let identity = PluginIdentity::try_new(identity).expect("identity");
let spec = PluginRuntimeSpec::new(
AuthorizedPluginComponent::for_test(
identity.clone(),
EscapedDisplayText::from_display_text(format!("plugins/{}.wasm", identity.as_str())),
component_bytes,
),
PluginWitWorld::current(),
capabilities,
limits,
);
let mut component = runtime.instantiate(&spec).expect("component should load");
let mut state = PluginInstanceState::new(spec);
issue_handles(&mut state);
let error = component
.update(&state)
.expect_err("abusive component update should fail");
expected.assert_matches(&error);
let debug = format!("{error:?}");
let display = error.to_string();
for needle in redacted_needles {
assert!(!debug.contains(needle));
assert!(!display.contains(needle));
}
}
fn issue_view_handle(state: &mut PluginInstanceState, entity_index: u32, raw: u64) {
let view = crate::ecs::components::buffer::ViewEntity(test_entity(entity_index));
let handle = state
.handles_mut()
.expect("state should be active")
.issue_view(view)
.expect("view handle");
let shape = handle.shape();
assert_eq!(shape.raw().get(), raw);
assert_eq!(shape.generation().as_u64(), 0);
}
fn issue_update_handles(
state: &mut PluginInstanceState,
view: crate::ecs::components::buffer::ViewEntity,
buffer: crate::ecs::components::buffer::BufferEntity,
revision: TextRevision,
) {
let handles = state.handles_mut().expect("state should be active");
let _view_handle = handles.issue_view(view).expect("view handle");
let _buffer_handle = handles
.issue_buffer(buffer, Some(revision), Some(view))
.expect("buffer handle");
}
fn issue_observed_update_handles(
state: &mut PluginInstanceState,
view: crate::ecs::components::buffer::ViewEntity,
buffer: crate::ecs::components::buffer::BufferEntity,
snapshot: PluginBufferSnapshot,
) {
let handles = state.handles_mut().expect("state should be active");
let _view_handle = handles.issue_view(view).expect("view handle");
let _buffer_handle = handles
.issue_observed_buffer(buffer, snapshot, Some(view))
.expect("buffer handle");
}
fn issue_view_and_buffer_handles(
state: &mut PluginInstanceState,
view_entity_index: u32,
buffer_entity_index: u32,
) {
issue_view_handle(state, view_entity_index, 1);
let buffer = crate::ecs::components::buffer::BufferEntity(test_entity(buffer_entity_index));
let handle = state
.handles_mut()
.expect("state should be active")
.issue_buffer(buffer, Some(TextRevision::from(1)), None)
.expect("buffer handle");
assert_eq!(handle.shape().raw().get(), 2);
assert_eq!(handle.shape().generation().as_u64(), 0);
}
#[test]
fn component_fuel_exhaustion_discards_queued_effects() {
assert_abusive_component_discards_queued_effect(
"component-fuel",
status_publish_then_loop_component_fixture(b"queued before fuel"),
PluginCapabilities {
status_publish: true,
..PluginCapabilities::default()
},
PluginRuntimeLimits {
fuel_per_update: 1_000,
timeout_ms: 1_000,
..PluginRuntimeLimits::default()
}
.validate()
.expect("limits should validate"),
|state| issue_view_and_buffer_handles(state, 19, 190),
&ExpectedRuntimeFailure::GuestTrap,
&["queued before fuel"],
);
}
#[test]
fn component_wall_clock_timeout_discards_queued_effects() {
assert_abusive_component_discards_queued_effect(
"component-timeout",
status_publish_then_loop_component_fixture(b"queued before timeout"),
PluginCapabilities {
status_publish: true,
..PluginCapabilities::default()
},
PluginRuntimeLimits {
fuel_per_update: crate::plugin::MAX_PLUGIN_FUEL_PER_UPDATE,
timeout_ms: 50,
..PluginRuntimeLimits::default()
}
.validate()
.expect("limits should validate"),
|state| issue_view_and_buffer_handles(state, 20, 200),
&ExpectedRuntimeFailure::Timeout {
timeout_ms: 50,
event: "plugin \"component-timeout\" timed out after 50 ms",
},
&["queued before timeout"],
);
}
#[test]
fn component_invalid_guest_string_discards_queued_effects() {
assert_abusive_component_discards_queued_effect(
"component-invalid-string",
status_publish_twice_component_fixture(
StatusCallFixture {
handle_rep: UPDATE_VIEW_REP,
text: b"queued before invalid string",
},
StatusCallFixture {
handle_rep: UPDATE_VIEW_REP,
text: b"secret invalid \xff",
},
),
PluginCapabilities {
status_publish: true,
..PluginCapabilities::default()
},
PluginRuntimeLimits::default()
.validate()
.expect("limits should validate"),
|state| issue_view_and_buffer_handles(state, 21, 210),
&ExpectedRuntimeFailure::GuestTrap,
&["secret"],
);
}
#[test]
fn component_oversized_host_payload_discards_queued_effects() {
assert_abusive_component_discards_queued_effect(
"component-oversized",
status_publish_twice_component_fixture(
StatusCallFixture {
handle_rep: UPDATE_VIEW_REP,
text: b"ok",
},
StatusCallFixture {
handle_rep: UPDATE_VIEW_REP,
text: b"secret oversized payload",
},
),
PluginCapabilities {
status_publish: true,
..PluginCapabilities::default()
},
PluginRuntimeLimits {
max_message_bytes: 4,
..PluginRuntimeLimits::default()
}
.validate()
.expect("limits should validate"),
|state| issue_view_and_buffer_handles(state, 22, 220),
&ExpectedRuntimeFailure::HostDecode,
&["secret"],
);
}
#[test]
fn component_invalid_resource_handle_discards_queued_effects() {
assert_abusive_component_discards_queued_effect(
"component-invalid-resource",
status_publish_twice_component_fixture(
StatusCallFixture {
handle_rep: UPDATE_VIEW_REP,
text: b"queued before invalid resource",
},
StatusCallFixture {
handle_rep: 999,
text: b"secret invalid resource",
},
),
PluginCapabilities {
status_publish: true,
..PluginCapabilities::default()
},
PluginRuntimeLimits::default()
.validate()
.expect("limits should validate"),
|state| issue_view_and_buffer_handles(state, 23, 230),
&ExpectedRuntimeFailure::GuestTrap,
&["secret"],
);
}
#[test]
fn component_wrong_resource_handle_discards_queued_effects() {
assert_abusive_component_discards_queued_effect(
"component-wrong-resource",
status_publish_then_buffer_insert_component_fixture(
StatusCallFixture {
handle_rep: UPDATE_VIEW_REP,
text: b"queued before wrong resource",
},
BufferInsertFixture {
handle_rep: UPDATE_VIEW_REP,
byte_index: 0,
text: b"secret wrong-resource edit",
},
),
PluginCapabilities {
status_publish: true,
buffer_propose_edit: true,
..PluginCapabilities::default()
},
PluginRuntimeLimits::default()
.validate()
.expect("limits should validate"),
|state| issue_view_and_buffer_handles(state, 24, 25),
&ExpectedRuntimeFailure::GuestTrap,
&["secret"],
);
}
#[test]
fn component_buffer_propose_edit_queues_revision_guarded_effect_through_runtime_update() {
let runtime = WasmtimePluginRuntime::new().expect("runtime");
let buffer = crate::ecs::components::buffer::BufferEntity(test_entity(15));
let spec = PluginRuntimeSpec::new(
AuthorizedPluginComponent::for_test(
PluginIdentity::try_new("component-buffer").expect("identity"),
EscapedDisplayText::from_display_text("plugins/component-buffer.wasm"),
buffer_propose_edit_component_fixture(3, "component edit"),
),
PluginWitWorld::current(),
PluginCapabilities {
buffer_propose_edit: true,
..PluginCapabilities::default()
},
PluginRuntimeLimits::default()
.validate()
.expect("limits should validate"),
);
let mut component = runtime.instantiate(&spec).expect("component should load");
let mut state = PluginInstanceState::new(spec);
issue_update_handles(
&mut state,
crate::ecs::components::buffer::ViewEntity(test_entity(150)),
buffer,
TextRevision::from(12),
);
let report = component
.update(&state)
.expect("component update should return")
.into_parts()
.effects
.drain();
assert!(report.status_messages().is_empty());
assert_eq!(report.buffer_edits().len(), 1);
assert_eq!(report.buffer_edits()[0].target, buffer);
assert_eq!(
report.buffer_edits()[0].provenance.base_revision(),
crate::text_stream::TextRevision::from(12)
);
assert_eq!(
report.buffer_edits()[0].provenance.source_identity(),
"component-buffer"
);
assert_eq!(
crate::buffer::BufferEditShape::from_edit(&report.buffer_edits()[0].edit)
.replacement_byte_len(),
Some(14)
);
}
#[test]
fn denied_component_buffer_propose_edit_discards_runtime_update() {
let runtime = WasmtimePluginRuntime::new().expect("runtime");
let buffer = crate::ecs::components::buffer::BufferEntity(test_entity(16));
let spec = PluginRuntimeSpec::new(
AuthorizedPluginComponent::for_test(
PluginIdentity::try_new("component-buffer-denied").expect("identity"),
EscapedDisplayText::from_display_text("plugins/component-buffer-denied.wasm"),
buffer_propose_edit_component_fixture(0, "secret component edit"),
),
PluginWitWorld::current(),
PluginCapabilities::default(),
PluginRuntimeLimits::default()
.validate()
.expect("limits should validate"),
);
let mut component = runtime.instantiate(&spec).expect("component should load");
let mut state = PluginInstanceState::new(spec);
issue_update_handles(
&mut state,
crate::ecs::components::buffer::ViewEntity(test_entity(160)),
buffer,
TextRevision::from(1),
);
let error = component
.update(&state)
.expect_err("denied buffer edit import should fail update");
assert!(matches!(
&error,
WasmtimePluginRuntimeError::UpdateFailed { source, discarded }
if matches!(
source.as_ref(),
WasmtimePluginRuntimeError::HostImport {
source: crate::plugin::PluginHostImportError::Authorization(_),
..
}
) && discarded.effects().discarded_effects() == 0
));
assert!(!format!("{error:?}").contains("secret"));
assert!(!error.to_string().contains("secret"));
}
#[test]
fn component_buffer_observe_returns_snapshot_through_runtime_update() {
let runtime = WasmtimePluginRuntime::new().expect("runtime");
let buffer = crate::ecs::components::buffer::BufferEntity(test_entity(17));
let spec = PluginRuntimeSpec::new(
AuthorizedPluginComponent::for_test(
PluginIdentity::try_new("component-observe").expect("identity"),
EscapedDisplayText::from_display_text("plugins/component-observe.wasm"),
buffer_observe_component_fixture(),
),
PluginWitWorld::current(),
PluginCapabilities {
buffer_observe: true,
..PluginCapabilities::default()
},
PluginRuntimeLimits::default()
.validate()
.expect("limits should validate"),
);
let mut component = runtime.instantiate(&spec).expect("component should load");
let mut state = PluginInstanceState::new(spec);
issue_observed_update_handles(
&mut state,
crate::ecs::components::buffer::ViewEntity(test_entity(170)),
buffer,
PluginBufferSnapshot::new(TextRevision::from(4), "secret observed text"),
);
let report = component
.update(&state)
.expect("component update should return")
.into_parts()
.effects
.drain();
assert!(report.buffer_edits().is_empty());
assert!(report.status_messages().is_empty());
}
#[test]
fn component_buffer_observe_then_propose_edit_queues_revision_guarded_effect() {
let runtime = WasmtimePluginRuntime::new().expect("runtime");
let buffer = crate::ecs::components::buffer::BufferEntity(test_entity(26));
let spec = PluginRuntimeSpec::new(
AuthorizedPluginComponent::for_test(
PluginIdentity::try_new("component-observe-propose").expect("identity"),
EscapedDisplayText::from_display_text("plugins/component-observe-propose.wasm"),
buffer_observe_then_propose_edit_component_fixture(0, "observed "),
),
PluginWitWorld::current(),
PluginCapabilities {
buffer_observe: true,
buffer_propose_edit: true,
..PluginCapabilities::default()
},
PluginRuntimeLimits::default()
.validate()
.expect("limits should validate"),
);
let mut component = runtime.instantiate(&spec).expect("component should load");
let mut state = PluginInstanceState::new(spec);
issue_observed_update_handles(
&mut state,
crate::ecs::components::buffer::ViewEntity(test_entity(260)),
buffer,
PluginBufferSnapshot::new(TextRevision::from(8), "secret observed text"),
);
let report = component
.update(&state)
.expect("component update should return")
.into_parts()
.effects
.drain();
assert!(report.status_messages().is_empty());
assert_eq!(report.buffer_edits().len(), 1);
assert_eq!(report.buffer_edits()[0].target, buffer);
assert_eq!(
report.buffer_edits()[0].provenance.base_revision(),
TextRevision::from(8)
);
assert_eq!(
report.buffer_edits()[0].provenance.source_identity(),
"component-observe-propose"
);
assert_eq!(
crate::buffer::BufferEditShape::from_edit(&report.buffer_edits()[0].edit)
.replacement_byte_len(),
Some(9)
);
}
#[test]
fn denied_component_buffer_observe_discards_runtime_update() {
let runtime = WasmtimePluginRuntime::new().expect("runtime");
let buffer = crate::ecs::components::buffer::BufferEntity(test_entity(18));
let spec = PluginRuntimeSpec::new(
AuthorizedPluginComponent::for_test(
PluginIdentity::try_new("component-observe-denied").expect("identity"),
EscapedDisplayText::from_display_text("plugins/component-observe-denied.wasm"),
buffer_observe_component_fixture(),
),
PluginWitWorld::current(),
PluginCapabilities::default(),
PluginRuntimeLimits::default()
.validate()
.expect("limits should validate"),
);
let mut component = runtime.instantiate(&spec).expect("component should load");
let mut state = PluginInstanceState::new(spec);
issue_observed_update_handles(
&mut state,
crate::ecs::components::buffer::ViewEntity(test_entity(180)),
buffer,
PluginBufferSnapshot::new(TextRevision::from(4), "secret observed text"),
);
let error = component
.update(&state)
.expect_err("denied observe import should fail update");
assert!(matches!(
&error,
WasmtimePluginRuntimeError::UpdateFailed { source, discarded }
if matches!(
source.as_ref(),
WasmtimePluginRuntimeError::HostImport {
source: crate::plugin::PluginHostImportError::Authorization(_),
..
}
) && discarded.effects().discarded_effects() == 0
));
assert!(!format!("{error:?}").contains("secret"));
assert!(!error.to_string().contains("secret"));
}
#[test]
fn component_workspace_artifact_write_queues_deferred_io() {
let root = temp_dir("workspace-artifact-component");
fs::create_dir(root.join("docs")).expect("docs directory");
let filesystem =
crate::fs_utils::FilesystemConfig::from_workspace_root(&root).expect("filesystem");
let runtime = WasmtimePluginRuntime::new().expect("runtime");
let spec = PluginRuntimeSpec::new(
AuthorizedPluginComponent::for_test(
PluginIdentity::try_new("component-workspace").expect("identity"),
EscapedDisplayText::from_display_text("plugins/component-workspace.wasm"),
workspace_artifact_write_component_fixture(&[WorkspaceWriteFixture {
path: b"docs/component.txt",
bytes: b"component artifact",
}]),
),
PluginWitWorld::current(),
PluginCapabilities {
workspace_artifact_write: vec![WorkspacePathGrant::new("docs")],
..PluginCapabilities::default()
},
PluginRuntimeLimits::default()
.validate()
.expect("limits should validate"),
);
let mut component = runtime.instantiate(&spec).expect("component should load");
let mut state = PluginInstanceState::new(spec);
issue_update_handles(
&mut state,
crate::ecs::components::buffer::ViewEntity(test_entity(190)),
crate::ecs::components::buffer::BufferEntity(test_entity(19)),
TextRevision::from(0),
);
let batches = component
.update(&state)
.expect("component update should return")
.into_parts();
let effect_report = batches.effects.drain();
let workspace_report = batches.workspace_io.execute_synchronous(&filesystem);
assert!(effect_report.status_messages().is_empty());
assert!(effect_report.buffer_edits().is_empty());
assert_eq!(workspace_report.identity(), "component-workspace");
assert_eq!(workspace_report.completions().len(), 1);
assert!(
workspace_report.completions()[0]
.as_write()
.expect("completion should be a write")
.outcome()
.is_ok()
);
assert_eq!(
fs::read(root.join("docs/component.txt")).expect("component artifact"),
b"component artifact"
);
let _cleanup = fs::remove_dir_all(root);
}
#[test]
fn component_workspace_observe_task_delivers_bytes_after_scheduling() {
let root = temp_dir("workspace-observe-task-component");
fs::create_dir(root.join("docs")).expect("docs directory");
fs::write(root.join("docs/input.txt"), b"component observe").expect("fixture");
let filesystem =
crate::fs_utils::FilesystemConfig::from_workspace_root(&root).expect("filesystem");
let runtime = WasmtimePluginRuntime::new().expect("runtime");
let spec = PluginRuntimeSpec::new(
AuthorizedPluginComponent::for_test(
PluginIdentity::try_new("component-workspace-observe").expect("identity"),
EscapedDisplayText::from_display_text("plugins/component-workspace-observe.wasm"),
workspace_observe_task_component_fixture(b"docs/input.txt"),
),
PluginWitWorld::current(),
PluginCapabilities {
workspace_observe: vec![WorkspacePathGrant::new("docs")],
..PluginCapabilities::default()
},
PluginRuntimeLimits::default()
.validate()
.expect("limits should validate"),
);
let mut component = runtime.instantiate(&spec).expect("component should load");
let mut state = PluginInstanceState::new(spec);
issue_update_handles(
&mut state,
crate::ecs::components::buffer::ViewEntity(test_entity(192)),
crate::ecs::components::buffer::BufferEntity(test_entity(21)),
TextRevision::from(0),
);
let scheduled = component
.update_and_enqueue_workspace_observe_tasks(&state)
.expect("first update should start and schedule observe task");
assert_eq!(scheduled.identity(), "component-workspace-observe");
assert_eq!(
scheduled.scheduled_workspace_observe_tasks().identity(),
"component-workspace-observe"
);
assert_eq!(
scheduled.owner_batches().identity(),
"component-workspace-observe"
);
let scheduled = scheduled.into_parts();
let owner_work = scheduled.owner_batches.into_parts();
assert!(owner_work.effects.is_empty());
assert!(owner_work.workspace_io.is_empty());
assert_eq!(scheduled.scheduled_workspace_observe_tasks.task_count(), 1);
let completions = component
.execute_workspace_observe_tasks_up_to(
&state,
&filesystem,
NonZeroUsize::new(1).expect("task budget should be non-zero"),
)
.expect("active component should execute one observe task");
assert_eq!(completions.len(), 1);
assert_eq!(
completions[0].outcome().bytes(),
Some(b"component observe".as_slice())
);
assert!(
component
.execute_next_workspace_observe_task(&state, &filesystem)
.expect("active component should accept an empty task queue")
.is_none()
);
let batches = component
.update(&state)
.expect("second update should poll and take observe task")
.into_parts();
assert!(batches.effects.is_empty());
assert!(batches.workspace_io.is_empty());
assert!(batches.workspace_observe_tasks.is_empty());
assert_eq!(
component
.store
.data()
.workspace_observe_tasks
.retained_len(),
0
);
let _cleanup = fs::remove_dir_all(root);
}
#[test]
fn component_workspace_observe_task_drop_before_return_discards_request() {
let runtime = WasmtimePluginRuntime::new().expect("runtime");
let spec = PluginRuntimeSpec::new(
AuthorizedPluginComponent::for_test(
PluginIdentity::try_new("component-workspace-drop-started").expect("identity"),
EscapedDisplayText::from_display_text("plugins/component-workspace-drop-started.wasm"),
workspace_observe_start_drop_component_fixture(b"docs/input.txt"),
),
PluginWitWorld::current(),
PluginCapabilities {
workspace_observe: vec![WorkspacePathGrant::new("docs")],
..PluginCapabilities::default()
},
PluginRuntimeLimits::default()
.validate()
.expect("limits should validate"),
);
let mut component = runtime.instantiate(&spec).expect("component should load");
let mut state = PluginInstanceState::new(spec);
issue_update_handles(
&mut state,
crate::ecs::components::buffer::ViewEntity(test_entity(191)),
crate::ecs::components::buffer::BufferEntity(test_entity(211)),
TextRevision::from(0),
);
let batches = component
.update(&state)
.expect("component update should return")
.into_parts();
assert!(batches.effects.is_empty());
assert!(batches.workspace_io.is_empty());
assert!(batches.workspace_observe_tasks.is_empty());
assert_eq!(
component
.store
.data()
.workspace_observe_tasks
.live_resource_len(),
0
);
assert_eq!(
component
.store
.data()
.workspace_observe_tasks
.reserved_pending_len(),
0
);
assert_eq!(
component.store.data().workspace_observe_tasks.pending_len(),
0
);
}
#[test]
fn component_update_and_schedule_helper_preserves_owner_queue_work() {
let root = temp_dir("workspace-observe-task-owner-queues");
fs::create_dir(root.join("docs")).expect("docs directory");
fs::write(root.join("docs/input.txt"), b"scheduled observe").expect("fixture");
let filesystem =
crate::fs_utils::FilesystemConfig::from_workspace_root(&root).expect("filesystem");
let mut app = runtime_ecs_test_app();
let buffer = BufferEntity(editor_buffer_entity(&mut app));
let view = ViewEntity(editor_view_entity(&mut app));
let runtime = WasmtimePluginRuntime::new().expect("runtime");
let spec = PluginRuntimeSpec::new(
AuthorizedPluginComponent::for_test(
PluginIdentity::try_new("component-workspace-owner-queues").expect("identity"),
EscapedDisplayText::from_display_text("plugins/component-workspace-owner-queues.wasm"),
workspace_observe_start_status_and_write_component_fixture(
b"docs/input.txt",
b"scheduled status",
WorkspaceWriteFixture {
path: b"docs/output.txt",
bytes: b"scheduled artifact",
},
),
),
PluginWitWorld::current(),
PluginCapabilities {
status_publish: true,
workspace_observe: vec![WorkspacePathGrant::new("docs")],
workspace_artifact_write: vec![WorkspacePathGrant::new("docs")],
..PluginCapabilities::default()
},
PluginRuntimeLimits::default()
.validate()
.expect("limits should validate"),
);
let mut component = runtime.instantiate(&spec).expect("component should load");
let mut state = PluginInstanceState::new(spec);
issue_update_handles(&mut state, view, buffer, TextRevision::from(0));
let scheduled = component
.update_and_enqueue_workspace_observe_tasks(&state)
.expect("update should seal owners and schedule observe tasks");
assert_scheduled_update_debug_redacts(
&scheduled,
"component-workspace-owner-queues",
1,
&[
"docs/input.txt",
"scheduled observe",
"scheduled status",
"docs/output.txt",
"scheduled artifact",
],
);
let mut workspace_queue = PluginWorkspaceIoWorkerQueue::default();
let publication = {
let mut intent_queue = app.world_mut().resource_mut::<PluginIntentQueue>();
PluginOwnerQueues::new(&mut intent_queue, &mut workspace_queue)
.push_scheduled_update(scheduled)
.expect("owner queues should accept paired scheduled work")
};
assert_scheduled_owner_publication_success(
&publication,
"component-workspace-owner-queues",
1,
1,
1,
);
let publication = publication.into_parts();
assert_eq!(
publication.scheduled_workspace_observe_tasks.task_count(),
1
);
app.update();
app.update();
let modal_state = app
.world()
.get::<VimModalState>(view.get())
.expect("editor view should have modal state");
assert_eq!(
modal_state.editor.status.message(),
Some(&crate::vim::VimStatusMessage::Info(
crate::StatusInfoText::escaped_display("scheduled status")
))
);
assert!(app.world().resource::<PluginIntentQueue>().is_empty());
let reports = workspace_queue.execute_all(&filesystem);
assert_eq!(reports.len(), 1);
assert_eq!(
fs::read(root.join("docs/output.txt")).expect("artifact"),
b"scheduled artifact"
);
let completions = component
.execute_all_workspace_observe_tasks(&state, &filesystem)
.expect("scheduled observe tasks should execute");
assert_eq!(completions.len(), 1);
assert_eq!(
completions[0].outcome().bytes(),
Some(b"scheduled observe".as_slice())
);
let _cleanup = fs::remove_dir_all(root);
}
#[test]
fn scheduled_update_owner_queue_error_keeps_task_report_and_owner_batches() {
let root = temp_dir("workspace-observe-task-owner-queue-error");
fs::create_dir(root.join("docs")).expect("docs directory");
fs::write(root.join("docs/input.txt"), b"queued observe").expect("fixture");
let filesystem =
crate::fs_utils::FilesystemConfig::from_workspace_root(&root).expect("filesystem");
let runtime = WasmtimePluginRuntime::new().expect("runtime");
let identity = "component-workspace-owner-queue-error";
let spec = PluginRuntimeSpec::new(
AuthorizedPluginComponent::for_test(
PluginIdentity::try_new(identity).expect("identity"),
EscapedDisplayText::from_display_text("plugins/component-owner-queue-error.wasm"),
workspace_observe_start_status_and_write_component_fixture(
b"docs/input.txt",
b"secret owner status",
WorkspaceWriteFixture {
path: b"docs/secret-output.txt",
bytes: b"secret artifact",
},
),
),
PluginWitWorld::current(),
PluginCapabilities {
status_publish: true,
workspace_observe: vec![WorkspacePathGrant::new("docs")],
workspace_artifact_write: vec![WorkspacePathGrant::new("docs")],
..PluginCapabilities::default()
},
PluginRuntimeLimits::default()
.validate()
.expect("limits should validate"),
);
let mut component = runtime.instantiate(&spec).expect("component should load");
let mut state = PluginInstanceState::new(spec);
issue_update_handles(
&mut state,
crate::ecs::components::buffer::ViewEntity(test_entity(212)),
crate::ecs::components::buffer::BufferEntity(test_entity(213)),
TextRevision::from(0),
);
let scheduled = component
.update_and_enqueue_workspace_observe_tasks(&state)
.expect("update should schedule observe tasks before owner publication");
let mut intent_queue = PluginIntentQueue::default();
let mut workspace_queue = PluginWorkspaceIoWorkerQueue::with_limit(
PluginWorkspaceIoWorkerQueueLimit::try_new(1)
.expect("workspace queue limit should validate"),
);
workspace_queue
.push(workspace_write_batch(
"queued-workspace",
"docs/queued.txt",
b"queued",
))
.expect("prefill should saturate workspace owner queue");
let error = PluginOwnerQueues::new(&mut intent_queue, &mut workspace_queue)
.push_scheduled_update(scheduled)
.expect_err("workspace owner queue should reject scheduled owner work");
assert_eq!(error.queue_error().kind().as_str(), "workspace-io");
assert_eq!(error.owner_batches().identity(), identity);
assert_eq!(
error.scheduled_workspace_observe_tasks().identity(),
identity
);
assert_eq!(error.scheduled_workspace_observe_tasks().task_count(), 1);
assert_eq!(
component.store.data().workspace_observe_tasks.pending_len(),
1
);
let event = error.operational_event().expect("workspace queue event");
assert_eq!(event.identity(), Some(identity));
let debug = format!("{error:?}");
let display = error.to_string();
assert_scheduled_owner_publication_debug_redacts(&debug, &display);
let completion = component
.execute_next_workspace_observe_task(&state, &filesystem)
.expect("active component should execute the scheduled task")
.expect("scheduled task should remain queued after owner rejection");
assert_eq!(
completion.outcome().bytes(),
Some(b"queued observe".as_slice())
);
let discard = error.discard_owner_batches();
assert_eq!(discard.identity(), identity);
assert_eq!(discard.owner_work().effects().discarded_effects(), 1);
assert_eq!(discard.owner_work().workspace_io().discarded_requests(), 1);
assert_eq!(discard.scheduled_workspace_observe_tasks().task_count(), 1);
let discard_debug = format!("{discard:?}");
assert_scheduled_owner_publication_debug_shape(&discard_debug);
assert_eq!(intent_queue.len(), 0);
assert_eq!(workspace_queue.len(), 1);
let _cleanup = fs::remove_dir_all(root);
}
#[test]
fn scheduled_update_owner_publication_retry_preserves_task_report() {
let root = temp_dir("workspace-observe-task-owner-queue-retry");
fs::create_dir(root.join("docs")).expect("docs directory");
fs::write(root.join("docs/input.txt"), b"retry observe").expect("fixture");
let filesystem =
crate::fs_utils::FilesystemConfig::from_workspace_root(&root).expect("filesystem");
let runtime = WasmtimePluginRuntime::new().expect("runtime");
let identity = "component-workspace-owner-retry";
let spec = runtime_spec_with_capabilities(
identity,
workspace_observe_start_status_and_write_component_fixture(
b"docs/input.txt",
b"retry owner status",
WorkspaceWriteFixture {
path: b"docs/retry-output.txt",
bytes: b"retry artifact",
},
),
PluginCapabilities {
status_publish: true,
workspace_observe: vec![WorkspacePathGrant::new("docs")],
workspace_artifact_write: vec![WorkspacePathGrant::new("docs")],
..PluginCapabilities::default()
},
);
let mut component = runtime.instantiate(&spec).expect("component should load");
let mut state = PluginInstanceState::new(spec);
issue_update_handles(
&mut state,
crate::ecs::components::buffer::ViewEntity(test_entity(214)),
crate::ecs::components::buffer::BufferEntity(test_entity(215)),
TextRevision::from(0),
);
let scheduled = component
.update_and_enqueue_workspace_observe_tasks(&state)
.expect("update should schedule observe tasks before owner publication");
let mut intent_queue = PluginIntentQueue::default();
let mut workspace_queue = PluginWorkspaceIoWorkerQueue::with_limit(
PluginWorkspaceIoWorkerQueueLimit::try_new(1)
.expect("workspace queue limit should validate"),
);
workspace_queue
.push(workspace_write_batch(
"queued-workspace",
"docs/queued.txt",
b"queued",
))
.expect("prefill should saturate workspace owner queue");
let error = PluginOwnerQueues::new(&mut intent_queue, &mut workspace_queue)
.push_scheduled_update(scheduled)
.expect_err("workspace owner queue should reject scheduled owner work");
assert_eq!(error.scheduled_workspace_observe_tasks().task_count(), 1);
assert_eq!(
component.store.data().workspace_observe_tasks.pending_len(),
1
);
let error = PluginOwnerQueues::new(&mut intent_queue, &mut workspace_queue)
.retry_scheduled_update_owner_publication(error)
.expect_err("retry should keep the paired error while capacity is exhausted");
assert_eq!(error.scheduled_workspace_observe_tasks().task_count(), 1);
assert_eq!(intent_queue.len(), 0);
assert_eq!(workspace_queue.len(), 1);
assert_eq!(
component.store.data().workspace_observe_tasks.pending_len(),
1
);
let prefilled = workspace_queue.execute_all(&filesystem);
assert_eq!(prefilled.len(), 1);
let publication = PluginOwnerQueues::new(&mut intent_queue, &mut workspace_queue)
.retry_scheduled_update_owner_publication(error)
.expect("retry should publish owner work after capacity frees");
assert_scheduled_owner_publication_success(&publication, identity, 1, 1, 1);
assert_eq!(intent_queue.len(), 1);
assert_eq!(workspace_queue.len(), 1);
assert_eq!(
component.store.data().workspace_observe_tasks.pending_len(),
1
);
let completion = component
.execute_next_workspace_observe_task(&state, &filesystem)
.expect("active component should execute the scheduled task")
.expect("scheduled task should remain queued after owner retry");
assert_eq!(
completion.outcome().bytes(),
Some(b"retry observe".as_slice())
);
let reports = workspace_queue.execute_all(&filesystem);
assert_eq!(reports.len(), 1);
assert_eq!(
fs::read(root.join("docs/retry-output.txt")).expect("artifact"),
b"retry artifact"
);
let _cleanup = fs::remove_dir_all(root);
}
#[test]
fn component_workspace_observe_task_failed_update_tombstones_started_resource() {
let (root, mut component, state, identity) = failed_workspace_observe_start_component_fixture();
let error = component
.update(&state)
.expect_err("status denial should fail after task start");
let discard = error
.discarded_update()
.expect("failed update should discard pending work");
assert_eq!(discard.workspace_observe_tasks().discarded_tasks(), 1);
assert_eq!(
component
.store
.data()
.workspace_observe_tasks
.live_resource_len(),
0
);
let tracked_reps = component
.store
.data()
.workspace_observe_tasks
.tracked_resource_reps();
assert_eq!(tracked_reps.len(), 1);
let stale_rep = tracked_reps[0];
assert_failed_update_tombstone_does_not_alias_next_task(
&mut component,
&state,
&identity,
stale_rep,
);
let _cleanup = fs::remove_dir_all(root);
}
#[test]
fn begin_update_tombstones_stale_uncommitted_workspace_observe_resources() {
let identity = PluginIdentity::try_new("runtime").expect("identity");
let host = crate::plugin::PluginHostContext::for_test(
identity.as_str(),
PluginCapabilities {
workspace_observe: vec![WorkspacePathGrant::new("docs")],
..PluginCapabilities::default()
},
);
let mut handles = crate::plugin::PluginHandleStore::new(identity);
let view = crate::ecs::components::buffer::ViewEntity(test_entity(331));
let _view_handle = handles.issue_view(view).expect("view handle");
let _buffer_handle = handles
.issue_buffer(
crate::ecs::components::buffer::BufferEntity(test_entity(332)),
Some(TextRevision::from(0)),
Some(view),
)
.expect("buffer handle");
let mut state = store_state(PluginRuntimeLimits::default());
let _resources = begin_test_update(&mut state, &host, &handles);
let stale_task = Host::workspace_observe_start(&mut state, String::from("docs/stale.txt"))
.expect("task resource");
assert_eq!(state.workspace_observe_tasks.live_resource_len(), 1);
let abandoned_session = state.end_update().expect("session");
drop(abandoned_session);
let _next_resources = begin_test_update(&mut state, &host, &handles);
assert_eq!(state.workspace_observe_tasks.live_resource_len(), 0);
let stale_borrow =
wasmtime::component::Resource::<PluginWorkspaceObserveTaskHandle>::new_borrow(
stale_task.rep(),
);
let error = Host::workspace_observe_poll(&mut state, stale_borrow)
.expect_err("stale uncommitted task resource should be tombstoned");
let import_error = state.take_import_error().expect("typed import error");
assert!(error.to_string().contains("plugin host import rejected"));
assert!(matches!(
import_error,
crate::plugin::PluginHostImportError::ResourceHandle {
kind: crate::plugin::PluginHostResourceKind::WorkspaceObserveTask,
reason: PluginResourceHandleRejectionReason::Missing,
}
));
let _discard = state.end_update().expect("session").discard();
}
fn failed_workspace_observe_start_component_fixture() -> (
std::path::PathBuf,
WasmtimePluginInstance,
PluginInstanceState,
PluginIdentity,
) {
let root = temp_dir("workspace-observe-task-component-failed-start");
fs::create_dir(root.join("docs")).expect("docs directory");
let runtime = WasmtimePluginRuntime::new().expect("runtime");
let identity = PluginIdentity::try_new("component-workspace-failed-start").expect("identity");
let spec = PluginRuntimeSpec::new(
AuthorizedPluginComponent::for_test(
identity.clone(),
EscapedDisplayText::from_display_text("plugins/component-workspace-failed-start.wasm"),
workspace_observe_start_then_status_component_fixture(
b"docs/input.txt",
b"status after task start",
),
),
PluginWitWorld::current(),
PluginCapabilities {
workspace_observe: vec![WorkspacePathGrant::new("docs")],
..PluginCapabilities::default()
},
PluginRuntimeLimits::default()
.validate()
.expect("limits should validate"),
);
let component = runtime.instantiate(&spec).expect("component should load");
let mut state = PluginInstanceState::new(spec);
issue_update_handles(
&mut state,
crate::ecs::components::buffer::ViewEntity(test_entity(196)),
crate::ecs::components::buffer::BufferEntity(test_entity(25)),
TextRevision::from(0),
);
(root, component, state, identity)
}
fn assert_failed_update_tombstone_does_not_alias_next_task(
component: &mut WasmtimePluginInstance,
state: &PluginInstanceState,
identity: &PluginIdentity,
stale_rep: WasmtimeWorkspaceObserveTaskResourceRep,
) {
let host = crate::plugin::PluginHostContext::for_test(
identity.as_str(),
PluginCapabilities {
workspace_observe: vec![WorkspacePathGrant::new("docs")],
..PluginCapabilities::default()
},
);
let _resources = begin_test_update(
component.store.data_mut(),
&host,
state.handles().expect("state should still be active"),
);
let next =
Host::workspace_observe_start(component.store.data_mut(), String::from("docs/next.txt"))
.expect("new task should not reuse failed-update resource rep");
assert_ne!(next.rep(), stale_rep.raw());
let stale_borrow = stale_rep.borrowed_task();
let _error = Host::workspace_observe_take(component.store.data_mut(), stale_borrow)
.expect_err("failed-update resource should be tombstoned");
let import_error = component
.store
.data_mut()
.take_import_error()
.expect("typed import error");
assert!(matches!(
import_error,
crate::plugin::PluginHostImportError::ResourceHandle {
kind: crate::plugin::PluginHostResourceKind::WorkspaceObserveTask,
reason: crate::plugin::PluginResourceHandleRejectionReason::Missing,
}
));
HostWorkspaceObserveTask::drop(component.store.data_mut(), stale_rep.owned_task())
.expect("stale resource drop should not affect next task");
assert_eq!(
component
.store
.data()
.workspace_observe_tasks
.live_resource_len(),
1
);
HostWorkspaceObserveTask::drop(component.store.data_mut(), next).expect("new task should drop");
let _discard = component
.store
.data_mut()
.end_update()
.expect("session")
.discard();
}
#[test]
fn component_revoke_state_cleans_workspace_observe_tasks() {
let root = temp_dir("workspace-observe-task-component-revoke");
fs::create_dir(root.join("docs")).expect("docs directory");
let filesystem =
crate::fs_utils::FilesystemConfig::from_workspace_root(&root).expect("filesystem");
let runtime = WasmtimePluginRuntime::new().expect("runtime");
let (mut component, mut state, workspace_observe_tasks) = workspace_observe_task_batch_fixture(
&runtime,
"component-workspace-revoke",
b"docs/input.txt",
);
let enqueue = component
.enqueue_workspace_observe_tasks(&state, workspace_observe_tasks)
.expect("task batch should enqueue");
assert_eq!(enqueue.task_handles().len(), 1);
assert_eq!(
component.store.data().workspace_observe_tasks.pending_len(),
1
);
assert_eq!(
component
.store
.data()
.workspace_observe_tasks
.live_resource_len(),
1
);
let cleanup = component
.revoke_state(&mut state, PluginRevocationReason::GrantsChanged)
.expect("revocation cleanup should match component identity");
assert_eq!(cleanup.identity(), "component-workspace-revoke");
assert_eq!(
cleanup.identity_proof().as_str(),
"component-workspace-revoke"
);
assert_eq!(
cleanup.revocation().reason(),
PluginRevocationReason::GrantsChanged
);
assert_eq!(
cleanup.revocation().unload_policy(),
PluginUnloadPolicy::Unload
);
assert_eq!(
cleanup.workspace_observe_tasks().identity(),
"component-workspace-revoke"
);
assert_eq!(cleanup.workspace_observe_tasks().canceled_pending(), 1);
assert_eq!(cleanup.workspace_observe_tasks().discarded_retained(), 0);
assert_eq!(
component
.store
.data()
.workspace_observe_tasks
.live_resource_len(),
0
);
let cleanup = cleanup.into_parts();
assert_eq!(cleanup.revocation.identity(), "component-workspace-revoke");
assert_eq!(cleanup.workspace_observe_tasks.canceled_pending(), 1);
assert_grants_changed_scheduling_denied(
&component
.execute_all_workspace_observe_tasks(&state, &filesystem)
.expect_err("revoked component should not execute observe tasks"),
);
assert_grants_changed_scheduling_denied(
&component
.update(&state)
.expect_err("revoked component should not be scheduled"),
);
assert_grants_changed_scheduling_denied(
&component
.execute_next_workspace_observe_task(&state, &filesystem)
.expect_err("revoked component should not execute one observe task"),
);
assert_grants_changed_scheduling_denied(
&component
.execute_workspace_observe_tasks_up_to(
&state,
&filesystem,
NonZeroUsize::new(1).expect("task budget should be non-zero"),
)
.expect_err("revoked component should not execute bounded observe tasks"),
);
let _cleanup = fs::remove_dir_all(root);
}
#[test]
fn component_shutdown_cleanup_cancels_workspace_observe_tasks() {
let root = temp_dir("workspace-observe-task-component-shutdown");
fs::create_dir(root.join("docs")).expect("docs directory");
let filesystem =
crate::fs_utils::FilesystemConfig::from_workspace_root(&root).expect("filesystem");
let runtime = WasmtimePluginRuntime::new().expect("runtime");
let (mut component, state, workspace_observe_tasks) = workspace_observe_task_batch_fixture(
&runtime,
"component-workspace-shutdown",
b"docs/input.txt",
);
let enqueue = component
.enqueue_workspace_observe_tasks(&state, workspace_observe_tasks)
.expect("task batch should enqueue");
assert_eq!(enqueue.task_handles().len(), 1);
assert_eq!(
component.store.data().workspace_observe_tasks.pending_len(),
1
);
assert_eq!(
component
.store
.data()
.workspace_observe_tasks
.live_resource_len(),
1
);
let reports = component.cancel_workspace_observe_tasks_for_shutdown();
assert_eq!(reports.len(), 1);
assert_eq!(reports[0].identity(), "component-workspace-shutdown");
assert_eq!(reports[0].canceled_pending(), 1);
assert_eq!(reports[0].discarded_retained(), 0);
assert_eq!(
component.store.data().workspace_observe_tasks.pending_len(),
0
);
assert_eq!(
component
.store
.data()
.workspace_observe_tasks
.live_resource_len(),
0
);
assert!(
component
.execute_all_workspace_observe_tasks(&state, &filesystem)
.expect("active component should execute an empty queue")
.is_empty()
);
assert!(
component
.cancel_workspace_observe_tasks_for_shutdown()
.is_empty()
);
let _cleanup = fs::remove_dir_all(root);
}
#[test]
fn component_workspace_observe_task_revocation_blocks_late_enqueue_and_execute() {
let root = temp_dir("workspace-observe-task-component-revoke-sealed");
fs::create_dir(root.join("docs")).expect("docs directory");
fs::write(root.join("docs/input.txt"), b"component observe").expect("fixture");
let filesystem =
crate::fs_utils::FilesystemConfig::from_workspace_root(&root).expect("filesystem");
let runtime = WasmtimePluginRuntime::new().expect("runtime");
let spec = PluginRuntimeSpec::new(
AuthorizedPluginComponent::for_test(
PluginIdentity::try_new("component-workspace-revoke-sealed").expect("identity"),
EscapedDisplayText::from_display_text("plugins/component-workspace-revoke-sealed.wasm"),
workspace_observe_task_component_fixture(b"docs/input.txt"),
),
PluginWitWorld::current(),
PluginCapabilities {
workspace_observe: vec![WorkspacePathGrant::new("docs")],
..PluginCapabilities::default()
},
PluginRuntimeLimits::default()
.validate()
.expect("limits should validate"),
);
let mut component = runtime.instantiate(&spec).expect("component should load");
let mut state = PluginInstanceState::new(spec);
issue_update_handles(
&mut state,
crate::ecs::components::buffer::ViewEntity(test_entity(195)),
crate::ecs::components::buffer::BufferEntity(test_entity(24)),
TextRevision::from(0),
);
let batches = component
.update(&state)
.expect("first update should start observe task")
.into_parts();
assert_eq!(batches.workspace_observe_tasks.task_count(), 1);
let _revocation = state.revoke(PluginRevocationReason::GrantsChanged);
let enqueue_error = component
.enqueue_workspace_observe_tasks(&state, batches.workspace_observe_tasks)
.expect_err("revoked component should not enqueue sealed observe tasks");
assert!(matches!(
enqueue_error.scheduling_error(),
Some(crate::plugin::PluginInstanceAccessError::Revoked {
reason: PluginRevocationReason::GrantsChanged,
..
})
));
assert_eq!(
enqueue_error.kind(),
WasmtimeWorkspaceObserveTaskEnqueueErrorKind::SchedulingDenied
);
assert_eq!(enqueue_error.batch().task_count(), 1);
let discard = enqueue_error.discard_batch();
assert_eq!(discard.identity(), "component-workspace-revoke-sealed");
assert_eq!(discard.discarded_tasks(), 1);
assert_eq!(
component.store.data().workspace_observe_tasks.pending_len(),
0
);
assert!(matches!(
component
.execute_all_workspace_observe_tasks(&state, &filesystem)
.expect_err("revoked component should not execute observe tasks"),
WasmtimePluginRuntimeError::SchedulingDenied(
crate::plugin::PluginInstanceAccessError::Revoked {
reason: PluginRevocationReason::GrantsChanged,
..
}
)
));
let _cleanup = fs::remove_dir_all(root);
}
#[test]
fn wasmtime_observe_task_enqueue_revocation_error_is_redacted_and_retry_safe() {
let runtime = WasmtimePluginRuntime::new().expect("runtime");
let (mut component, mut state, batch) = workspace_observe_task_batch_fixture(
&runtime,
"component-workspace-revoke-redacted",
b"docs/secret-revoked-task.txt",
);
let _revocation = state.revoke(PluginRevocationReason::GrantsChanged);
let error = component
.enqueue_workspace_observe_tasks(&state, batch)
.expect_err("revoked component should keep the sealed task batch");
assert!(matches!(
error.scheduling_error(),
Some(crate::plugin::PluginInstanceAccessError::Revoked {
reason: PluginRevocationReason::GrantsChanged,
..
})
));
assert_eq!(
error.kind(),
WasmtimeWorkspaceObserveTaskEnqueueErrorKind::SchedulingDenied
);
assert_eq!(error.batch().task_count(), 1);
assert!(error.queue_error().is_none());
assert!(error.operational_event().is_none());
assert_eq!(
component
.store
.data()
.workspace_observe_tasks
.reserved_pending_len(),
1
);
assert_wasmtime_task_enqueue_error_redacts(&error, &["secret-revoked-task"]);
let discard = error.discard_batch();
assert_eq!(discard.identity(), "component-workspace-revoke-redacted");
assert_eq!(discard.discarded_tasks(), 1);
assert_eq!(
component
.store
.data()
.workspace_observe_tasks
.reserved_pending_len(),
0
);
}
#[test]
fn wasmtime_observe_task_enqueue_state_mismatch_keeps_batch_redacted() {
let runtime = WasmtimePluginRuntime::new().expect("runtime");
let (mut component, _state, batch) = workspace_observe_task_batch_fixture(
&runtime,
"component-workspace-state-owned",
b"docs/secret-state-mismatch-task.txt",
);
let other_state = PluginInstanceState::new(runtime_spec_with_capabilities(
"component-workspace-state-other",
status_publish_component_fixture("unused"),
PluginCapabilities {
status_publish: true,
..PluginCapabilities::default()
},
));
let error = component
.enqueue_workspace_observe_tasks(&other_state, batch)
.expect_err("mismatched state should keep the sealed task batch");
assert_eq!(
error.kind(),
WasmtimeWorkspaceObserveTaskEnqueueErrorKind::InstanceStateMismatch
);
let identities = error
.instance_state_mismatch_identities()
.expect("mismatch identities should be available");
assert_eq!(
identities.runtime_identity.as_str(),
"component-workspace-state-owned"
);
assert_eq!(
identities.state_identity.as_str(),
"component-workspace-state-other"
);
assert_eq!(error.batch().identity(), "component-workspace-state-owned");
assert_eq!(error.batch().task_count(), 1);
assert!(error.scheduling_error().is_none());
assert!(error.queue_error().is_none());
let event = error.operational_event().expect("mismatch event");
assert_eq!(event.identity(), Some("component-workspace-state-owned"));
assert_eq!(
event.kind(),
crate::plugin::PluginOperationalEventKind::RuntimeFailure {
failure: crate::plugin::PluginOperationalRuntimeFailure::InstanceStateMismatch,
}
);
assert_wasmtime_task_enqueue_error_redacts(&error, &["secret-state-mismatch-task"]);
let discard = error.discard_batch();
assert_eq!(discard.identity(), "component-workspace-state-owned");
assert_eq!(discard.discarded_tasks(), 1);
assert_eq!(
component
.store
.data()
.workspace_observe_tasks
.reserved_pending_len(),
0
);
}
#[test]
fn wasmtime_observe_task_enqueue_batch_identity_mismatch_keeps_batch_retryable() {
let runtime = WasmtimePluginRuntime::new().expect("runtime");
let (mut source_component, source_state, batch) = workspace_observe_task_batch_fixture(
&runtime,
"component-workspace-source",
b"docs/secret-cross-runtime-task.txt",
);
let (mut other_component, other_state, other_batch) = workspace_observe_task_batch_fixture(
&runtime,
"component-workspace-other",
b"docs/other-task.txt",
);
let _other_discard = other_batch.discard();
let error = other_component
.enqueue_workspace_observe_tasks(&other_state, batch)
.expect_err("batch from another plugin should reject before queue admission");
assert_eq!(
error.kind(),
WasmtimeWorkspaceObserveTaskEnqueueErrorKind::BatchIdentityMismatch
);
let identities = error
.batch_identity_mismatch_identities()
.expect("batch mismatch identities should be available");
assert_eq!(
identities.runtime_identity.as_str(),
"component-workspace-other"
);
assert_eq!(
identities.batch_identity.as_str(),
"component-workspace-source"
);
assert!(error.scheduling_error().is_none());
assert!(error.instance_state_mismatch_identities().is_none());
assert!(error.queue_error().is_none());
let event = error.operational_event().expect("mismatch event");
assert_eq!(event.identity(), Some("component-workspace-other"));
assert_eq!(
event.kind(),
crate::plugin::PluginOperationalEventKind::RuntimeFailure {
failure: crate::plugin::PluginOperationalRuntimeFailure::BatchIdentityMismatch,
}
);
assert_eq!(error.batch().identity(), "component-workspace-source");
assert_eq!(error.batch().task_count(), 1);
assert_wasmtime_task_enqueue_error_redacts(&error, &["secret-cross-runtime-task"]);
assert_eq!(
source_component
.store
.data()
.workspace_observe_tasks
.reserved_pending_len(),
1
);
assert_eq!(
other_component
.store
.data()
.workspace_observe_tasks
.reserved_pending_len(),
0
);
assert_eq!(
other_component
.store
.data()
.workspace_observe_tasks
.pending_len(),
0
);
let retry = source_component
.enqueue_workspace_observe_tasks(&source_state, error.into_batch())
.expect("returned batch should retry on its owning runtime");
assert_eq!(retry.identity(), "component-workspace-source");
assert_eq!(retry.task_handles().len(), 1);
assert_eq!(
source_component
.store
.data()
.workspace_observe_tasks
.reserved_pending_len(),
0
);
assert_eq!(
source_component
.store
.data()
.workspace_observe_tasks
.pending_len(),
1
);
}
#[test]
fn wasmtime_observe_task_enqueue_wrong_runtime_same_identity_keeps_batch_retryable() {
let runtime = WasmtimePluginRuntime::new().expect("runtime");
let (mut source_component, source_state, batch) = workspace_observe_task_batch_fixture(
&runtime,
"component-workspace-shared",
b"docs/secret-cross-queue-task.txt",
);
let (mut other_component, other_state, other_batch) = workspace_observe_task_batch_fixture(
&runtime,
"component-workspace-shared",
b"docs/other-task.txt",
);
let _other_discard = other_batch.discard();
let error = other_component
.enqueue_workspace_observe_tasks(&other_state, batch)
.expect_err("batch reserved from another runtime queue should reject");
let queue_error = error.queue_error().expect("queue rejection");
assert_eq!(
error.kind(),
WasmtimeWorkspaceObserveTaskEnqueueErrorKind::Queue
);
assert_eq!(
queue_error.kind(),
PluginWorkspaceObserveTaskQueueErrorKind::ReservationMismatch
);
assert!(matches!(
queue_error,
PluginWorkspaceObserveTaskQueueError::ReservationMismatch {
reason: PluginWorkspaceObserveTaskReservationMismatch::QueueMismatch,
..
}
));
assert!(error.batch_identity_mismatch_identities().is_none());
assert_eq!(error.batch().identity(), "component-workspace-shared");
assert_eq!(error.batch().task_count(), 1);
assert_wasmtime_task_enqueue_error_redacts(&error, &["secret-cross-queue-task"]);
assert_eq!(
source_component
.store
.data()
.workspace_observe_tasks
.reserved_pending_len(),
1
);
assert_eq!(
other_component
.store
.data()
.workspace_observe_tasks
.reserved_pending_len(),
0
);
assert_eq!(
other_component
.store
.data()
.workspace_observe_tasks
.pending_len(),
0
);
let retry = source_component
.enqueue_workspace_observe_tasks(&source_state, error.into_batch())
.expect("returned batch should retry on its owning runtime");
assert_eq!(retry.identity(), "component-workspace-shared");
assert_eq!(retry.task_handles().len(), 1);
assert_eq!(
source_component
.store
.data()
.workspace_observe_tasks
.reserved_pending_len(),
0
);
assert_eq!(
source_component
.store
.data()
.workspace_observe_tasks
.pending_len(),
1
);
}
#[test]
fn wasmtime_update_schedule_error_recovers_owner_batches_and_retryable_task_batch() {
let runtime = WasmtimePluginRuntime::new().expect("runtime");
let identity = "component-workspace-schedule";
let secret_path = "docs/secret-scheduled-task.txt";
let (mut component, state, batch) =
workspace_observe_task_batch_fixture(&runtime, identity, secret_path.as_bytes());
let other_state = PluginInstanceState::new(runtime_spec_with_capabilities(
"component-workspace-schedule-other",
status_publish_component_fixture("unused"),
PluginCapabilities {
status_publish: true,
..PluginCapabilities::default()
},
));
let enqueue_error = component
.enqueue_workspace_observe_tasks(&other_state, batch)
.expect_err("mismatched state should keep the task batch retryable");
let error = super::WasmtimePluginUpdateScheduleError::from(
WasmtimeWorkspaceObserveTaskScheduleError::new(
PluginHostOwnerBatches::for_test(
status_effect_batch(identity, "secret scheduled owner status"),
workspace_write_batch(
identity,
"docs/secret-scheduled-owner-write.txt",
b"secret scheduled owner bytes",
),
),
enqueue_error,
),
);
assert!(error.update_error().is_none());
let schedule_error = error
.workspace_observe_task_schedule_error()
.expect("schedule error should expose post-update payload");
assert_eq!(schedule_error.identity(), identity);
assert!(!schedule_error.effects().is_empty());
assert!(!schedule_error.workspace_io().is_empty());
assert_eq!(schedule_error.enqueue_error().batch().task_count(), 1);
assert_eq!(
error
.workspace_observe_task_enqueue_error()
.expect("enqueue error")
.batch()
.identity(),
identity
);
let event = error.operational_event().expect("mismatch event");
assert_eq!(event.identity(), Some(identity));
assert_eq!(
event.kind(),
crate::plugin::PluginOperationalEventKind::RuntimeFailure {
failure: crate::plugin::PluginOperationalRuntimeFailure::InstanceStateMismatch,
}
);
let debug = format!("{error:?}");
let display = error.to_string();
assert_wasmtime_schedule_error_debug_redacts(
&debug,
&display,
WASMTIME_SCHEDULE_ERROR_REDACTED_NEEDLES,
);
let schedule_error = error
.into_workspace_observe_task_schedule_error()
.expect("post-update schedule error should be recoverable");
let scheduled = component
.retry_workspace_observe_task_schedule(&state, schedule_error)
.expect("retrying with the owning state should schedule task work");
assert_eq!(scheduled.identity(), identity);
assert_eq!(
scheduled.scheduled_workspace_observe_tasks().task_count(),
1
);
assert_eq!(
component
.store
.data()
.workspace_observe_tasks
.reserved_pending_len(),
0
);
assert_eq!(
component.store.data().workspace_observe_tasks.pending_len(),
1
);
let mut intent_queue = PluginIntentQueue::default();
let mut workspace_io_queue = PluginWorkspaceIoWorkerQueue::default();
let publication = PluginOwnerQueues::new(&mut intent_queue, &mut workspace_io_queue)
.push_scheduled_update(scheduled)
.expect("recovered owner work should enqueue atomically");
assert_eq!(publication.identity(), identity);
assert_eq!(intent_queue.len(), 1);
assert_eq!(workspace_io_queue.len(), 1);
let cleanup = component.cancel_workspace_observe_tasks_for_shutdown();
assert_eq!(cleanup.len(), 1);
assert_eq!(cleanup[0].identity(), identity);
assert_eq!(cleanup[0].canceled_pending(), 1);
}
#[test]
fn wasmtime_scheduled_update_proofs_enforce_one_identity() {
assert_panics_with(
|| {
let _scheduled = WasmtimePluginScheduledUpdate::new(
owner_batches_for_identity("left"),
empty_task_enqueue_report("right"),
);
},
"scheduled Wasmtime update must share one plugin identity",
);
assert_panics_with(
|| {
let _error = WasmtimeWorkspaceObserveTaskScheduleError::new(
owner_batches_for_identity("left"),
super::WasmtimeWorkspaceObserveTaskEnqueueError::batch_identity_mismatch(
test_identity("left"),
test_identity("right"),
empty_workspace_observe_task_batch("right"),
),
);
},
"Wasmtime observe-task schedule error must share one plugin identity",
);
assert_panics_with(
|| {
let _report = super::WasmtimeWorkspaceObserveTaskScheduleDiscardReport::new(
owner_batches_for_identity("left").discard(),
empty_workspace_observe_task_batch("right").discard(),
);
},
"Wasmtime observe-task schedule discard report must share one plugin identity",
);
}
#[test]
fn wasmtime_update_schedule_error_discards_paired_unpublished_work() {
let runtime = WasmtimePluginRuntime::new().expect("runtime");
let identity = "component-workspace-schedule-discard";
let secret_path = "docs/secret-discarded-scheduled-task.txt";
let (mut component, _state, batch) =
workspace_observe_task_batch_fixture(&runtime, identity, secret_path.as_bytes());
let other_state = PluginInstanceState::new(runtime_spec_with_capabilities(
"component-workspace-schedule-discard-other",
status_publish_component_fixture("unused"),
PluginCapabilities {
status_publish: true,
..PluginCapabilities::default()
},
));
let enqueue_error = component
.enqueue_workspace_observe_tasks(&other_state, batch)
.expect_err("mismatched state should keep the task batch retryable");
let schedule_error = WasmtimeWorkspaceObserveTaskScheduleError::new(
PluginHostOwnerBatches::for_test(
status_effect_batch(identity, "secret discarded scheduled status"),
workspace_write_batch(
identity,
"docs/secret-discarded-scheduled-owner-write.txt",
b"secret discarded scheduled bytes",
),
),
enqueue_error,
);
assert_eq!(
component
.store
.data()
.workspace_observe_tasks
.reserved_pending_len(),
1
);
let report = schedule_error.discard();
assert_eq!(report.identity(), identity);
assert_eq!(report.owner_work().effects().discarded_effects(), 1);
assert_eq!(report.owner_work().workspace_io().discarded_requests(), 1);
assert_eq!(report.workspace_observe_tasks().discarded_tasks(), 1);
let trace = PluginRuntimeTraceEvent::from(&report);
assert_eq!(
trace.to_string(),
"plugin \"component-workspace-schedule-discard\" trace update-discarded effects=1 workspace_io=1 workspace_observe_tasks=1"
);
assert_eq!(
component
.store
.data()
.workspace_observe_tasks
.reserved_pending_len(),
0
);
assert_eq!(
component.store.data().workspace_observe_tasks.pending_len(),
0
);
let debug = format!("{report:?}");
let trace = trace.to_string();
assert!(debug.contains("WasmtimeWorkspaceObserveTaskScheduleDiscardReport"));
assert!(debug.contains("discarded_effect_count: 1"));
assert!(debug.contains("discarded_workspace_io_request_count: 1"));
assert!(debug.contains("discarded_workspace_observe_task_count: 1"));
assert!(!debug.contains("PluginHostOwnerDiscardReport"));
assert!(!debug.contains("PluginWorkspaceObserveTaskDiscardReport"));
for secret in [
secret_path,
"secret-discarded-scheduled-task",
"secret discarded scheduled status",
"secret-discarded-scheduled-owner-write",
"secret discarded scheduled bytes",
] {
assert!(!debug.contains(secret));
assert!(!trace.contains(secret));
}
}
#[test]
fn component_revoke_state_rejects_wrong_identity_without_mutating_state() {
let runtime = WasmtimePluginRuntime::new().expect("runtime");
let spec = PluginRuntimeSpec::new(
AuthorizedPluginComponent::for_test(
PluginIdentity::try_new("component-workspace-owned").expect("identity"),
EscapedDisplayText::from_display_text("plugins/component-workspace-owned.wasm"),
status_publish_component_fixture("unused"),
),
PluginWitWorld::current(),
PluginCapabilities {
status_publish: true,
..PluginCapabilities::default()
},
PluginRuntimeLimits::default()
.validate()
.expect("limits should validate"),
);
let mut component = runtime.instantiate(&spec).expect("component should load");
let other_spec = PluginRuntimeSpec::new(
AuthorizedPluginComponent::for_test(
PluginIdentity::try_new("component-workspace-other").expect("identity"),
EscapedDisplayText::from_display_text("plugins/component-workspace-other.wasm"),
status_publish_component_fixture("unused"),
),
PluginWitWorld::current(),
PluginCapabilities {
status_publish: true,
..PluginCapabilities::default()
},
PluginRuntimeLimits::default()
.validate()
.expect("limits should validate"),
);
let mut other_state = PluginInstanceState::new(other_spec);
let generation = other_state
.handles()
.expect("state should remain active")
.generation();
let error = component
.revoke_state(&mut other_state, PluginRevocationReason::DisabledByConfig)
.expect_err("wrong state identity should reject before revocation");
assert!(matches!(
error,
WasmtimePluginRuntimeError::InstanceStateMismatch {
runtime_identity,
state_identity,
} if runtime_identity.as_str() == "component-workspace-owned"
&& state_identity.as_str() == "component-workspace-other"
));
assert!(other_state.may_schedule());
assert_eq!(
other_state
.handles()
.expect("state should not be revoked")
.generation(),
generation
);
}
#[test]
fn component_workspace_observe_task_revocation_cleanup_rejects_wrong_identity() {
let runtime = WasmtimePluginRuntime::new().expect("runtime");
let spec = PluginRuntimeSpec::new(
AuthorizedPluginComponent::for_test(
PluginIdentity::try_new("component-workspace-owned").expect("identity"),
EscapedDisplayText::from_display_text("plugins/component-workspace-owned.wasm"),
status_publish_component_fixture("unused"),
),
PluginWitWorld::current(),
PluginCapabilities {
status_publish: true,
..PluginCapabilities::default()
},
PluginRuntimeLimits::default()
.validate()
.expect("limits should validate"),
);
let mut component = runtime.instantiate(&spec).expect("component should load");
let other_spec = PluginRuntimeSpec::new(
AuthorizedPluginComponent::for_test(
PluginIdentity::try_new("component-workspace-other").expect("identity"),
EscapedDisplayText::from_display_text("plugins/component-workspace-other.wasm"),
status_publish_component_fixture("unused"),
),
PluginWitWorld::current(),
PluginCapabilities {
status_publish: true,
..PluginCapabilities::default()
},
PluginRuntimeLimits::default()
.validate()
.expect("limits should validate"),
);
let mut other_state = PluginInstanceState::new(other_spec);
let revocation = other_state.revoke(PluginRevocationReason::DisabledByConfig);
let error = component
.cancel_workspace_observe_tasks_for_revocation(&revocation)
.expect_err("wrong revocation identity should fail closed");
assert!(matches!(
error,
WasmtimePluginRuntimeError::InstanceStateMismatch {
runtime_identity,
state_identity,
} if runtime_identity.as_str() == "component-workspace-owned"
&& state_identity.as_str() == "component-workspace-other"
));
}
#[test]
fn component_invalid_workspace_artifact_path_discards_queued_workspace_io() {
let root = temp_dir("workspace-artifact-invalid");
fs::create_dir(root.join("docs")).expect("docs directory");
let runtime = WasmtimePluginRuntime::new().expect("runtime");
let spec = PluginRuntimeSpec::new(
AuthorizedPluginComponent::for_test(
PluginIdentity::try_new("component-workspace-invalid").expect("identity"),
EscapedDisplayText::from_display_text("plugins/component-workspace-invalid.wasm"),
workspace_artifact_write_component_fixture(&[
WorkspaceWriteFixture {
path: b"docs/queued.txt",
bytes: b"queued before invalid path",
},
WorkspaceWriteFixture {
path: b"docs/../secret.txt",
bytes: b"secret artifact",
},
]),
),
PluginWitWorld::current(),
PluginCapabilities {
workspace_artifact_write: vec![WorkspacePathGrant::new("docs")],
..PluginCapabilities::default()
},
PluginRuntimeLimits::default()
.validate()
.expect("limits should validate"),
);
let mut component = runtime.instantiate(&spec).expect("component should load");
let mut state = PluginInstanceState::new(spec);
issue_update_handles(
&mut state,
crate::ecs::components::buffer::ViewEntity(test_entity(191)),
crate::ecs::components::buffer::BufferEntity(test_entity(20)),
TextRevision::from(0),
);
let error = component
.update(&state)
.expect_err("invalid workspace path should fail update");
assert!(matches!(
&error,
WasmtimePluginRuntimeError::UpdateFailed { source, discarded }
if matches!(
source.as_ref(),
WasmtimePluginRuntimeError::HostImport {
source: crate::plugin::PluginHostImportError::Decode(_),
..
}
) && discarded.effects().discarded_effects() == 0
&& discarded.workspace_io().discarded_requests() == 1
));
assert!(!root.join("docs/queued.txt").exists());
assert!(!format!("{error:?}").contains("secret"));
assert!(!error.to_string().contains("secret"));
let _cleanup = fs::remove_dir_all(root);
}
#[test]
fn component_buffer_propose_edit_reaches_plugin_intent_queue_and_owner() {
let mut app = runtime_ecs_test_app();
let buffer = BufferEntity(editor_buffer_entity(&mut app));
let runtime = WasmtimePluginRuntime::new().expect("runtime");
let spec = PluginRuntimeSpec::new(
AuthorizedPluginComponent::for_test(
PluginIdentity::try_new("component-queue").expect("identity"),
EscapedDisplayText::from_display_text("plugins/component-queue.wasm"),
buffer_propose_edit_component_fixture(0, "component "),
),
PluginWitWorld::current(),
PluginCapabilities {
buffer_propose_edit: true,
..PluginCapabilities::default()
},
PluginRuntimeLimits::default()
.validate()
.expect("limits should validate"),
);
let mut component = runtime.instantiate(&spec).expect("component should load");
let mut state = PluginInstanceState::new(spec);
issue_update_handles(
&mut state,
crate::ecs::components::buffer::ViewEntity(test_entity(181)),
buffer,
TextRevision::from(0),
);
let batches = component
.update(&state)
.expect("component update should return")
.into_parts();
app.world_mut()
.resource_mut::<PluginIntentQueue>()
.push_sealed(batches.effects)
.expect("intent queue should accept component batch");
app.update();
app.update();
let buffer_text = app
.world()
.get::<BufferText>(buffer.get())
.expect("editor buffer should exist");
assert_eq!(buffer_text.stream.as_str(), "component alma");
assert!(app.world().resource::<PluginIntentQueue>().is_empty());
}
#[test]
fn component_buffer_observe_then_propose_edit_reaches_intent_queue_and_owner() {
let mut app = runtime_ecs_test_app();
let buffer = BufferEntity(editor_buffer_entity(&mut app));
let runtime = WasmtimePluginRuntime::new().expect("runtime");
let spec = PluginRuntimeSpec::new(
AuthorizedPluginComponent::for_test(
PluginIdentity::try_new("component-observe-queue").expect("identity"),
EscapedDisplayText::from_display_text("plugins/component-observe-queue.wasm"),
buffer_observe_then_propose_edit_component_fixture(0, "observed "),
),
PluginWitWorld::current(),
PluginCapabilities {
buffer_observe: true,
buffer_propose_edit: true,
..PluginCapabilities::default()
},
PluginRuntimeLimits::default()
.validate()
.expect("limits should validate"),
);
let mut component = runtime.instantiate(&spec).expect("component should load");
let mut state = PluginInstanceState::new(spec);
issue_observed_update_handles(
&mut state,
crate::ecs::components::buffer::ViewEntity(test_entity(182)),
buffer,
PluginBufferSnapshot::new(TextRevision::from(0), "alma"),
);
let batches = component
.update(&state)
.expect("component update should return")
.into_parts();
app.world_mut()
.resource_mut::<PluginIntentQueue>()
.push_sealed(batches.effects)
.expect("intent queue should accept component batch");
app.update();
app.update();
let buffer_text = app
.world()
.get::<BufferText>(buffer.get())
.expect("editor buffer should exist");
assert_eq!(buffer_text.stream.as_str(), "observed alma");
assert!(app.world().resource::<PluginIntentQueue>().is_empty());
let observed = app.world().resource::<RuntimeOwnerResults>();
assert_eq!(observed.edited.len(), 1);
assert!(observed.edit_rejected.is_empty());
}
#[test]
fn runtime_produced_batches_from_multiple_plugins_drain_fifo_through_owner() {
let mut app = runtime_ecs_test_app();
let buffer = BufferEntity(editor_buffer_entity(&mut app));
let runtime = WasmtimePluginRuntime::new().expect("runtime");
let first = runtime_buffer_insert_effects(
&runtime,
"component-order-a",
buffer,
TextRevision::from(0),
0,
"a",
);
let second = runtime_buffer_insert_effects(
&runtime,
"component-order-b",
buffer,
TextRevision::from(1),
1,
"b",
);
{
let mut queue = app.world_mut().resource_mut::<PluginIntentQueue>();
queue
.push_sealed(first)
.expect("intent queue should accept first component batch");
queue
.push_sealed(second)
.expect("intent queue should accept second component batch");
}
app.update();
app.update();
let buffer_text = app
.world()
.get::<BufferText>(buffer.get())
.expect("editor buffer should exist");
assert_eq!(buffer_text.stream.as_str(), "abalma");
assert!(app.world().resource::<PluginIntentQueue>().is_empty());
let observed = app.world().resource::<RuntimeOwnerResults>();
assert_eq!(observed.edited.len(), 2);
assert!(observed.edit_rejected.is_empty());
}
fn temp_dir(name: &str) -> std::path::PathBuf {
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("clock")
.as_nanos();
let root = std::env::temp_dir().join(format!("alma-plugin-{name}-{nanos}"));
fs::create_dir(&root).expect("temp dir");
root
}
fn assert_grants_changed_scheduling_denied(error: &WasmtimePluginRuntimeError) {
assert!(matches!(
error,
WasmtimePluginRuntimeError::SchedulingDenied(
crate::plugin::PluginInstanceAccessError::Revoked {
reason: PluginRevocationReason::GrantsChanged,
..
}
)
));
}
fn store_state(limits: PluginRuntimeLimits) -> WasmtimeStoreState {
WasmtimeStoreState::new(
StoreLimitsBuilder::new().build(),
limits.validate().expect("limits should validate"),
)
}
fn import_session(
host: &crate::plugin::PluginHostContext,
handles: &crate::plugin::PluginHandleStore,
) -> crate::plugin::PluginHostImportSession {
crate::plugin::PluginHostImportSession::from_snapshot(
PluginHostImportSnapshot::new(host, handles).expect("host state should match"),
crate::plugin::PluginEffectBatchLimit::default(),
crate::plugin::PluginWorkspaceIoBudget::try_new(4, 64, 64).expect("budget"),
)
}
fn begin_test_update(
state: &mut WasmtimeStoreState,
host: &crate::plugin::PluginHostContext,
handles: &crate::plugin::PluginHandleStore,
) -> WasmtimeUpdateResources {
let view = handles.first_view_handle().expect("view handle");
let buffer = handles.first_buffer_handle().expect("buffer handle");
state
.begin_update(import_session(host, handles), view, buffer)
.expect("update resources")
}
fn seal_successful_test_update(
state: &mut WasmtimeStoreState,
) -> crate::plugin::PluginHostImportBatches {
let session = state.end_update().expect("session");
state.commit_workspace_observe_tasks_started_in_update();
session.seal()
}
fn test_entity(index: u32) -> Entity {
Entity::from_raw_u32(index).expect("test entity index should be valid")
}
fn runtime_ecs_test_app() -> App {
let mut app = App::new();
let _app = app
.insert_resource(InitialEditorBuffer {
stream: TextByteStream::new("alma"),
file: crate::buffer::BufferFile::scratch(0),
})
.init_resource::<RuntimeOwnerResults>()
.add_plugins(EditorCorePlugin)
.add_plugins(BufferPlugin)
.add_systems(Update, capture_runtime_owner_results);
app.update();
app
}
fn editor_buffer_entity(app: &mut App) -> Entity {
let mut query = app
.world_mut()
.query_filtered::<Entity, With<EditorBuffer>>();
query
.iter(app.world())
.next()
.expect("editor buffer should exist")
}
fn editor_view_entity(app: &mut App) -> Entity {
let mut query = app.world_mut().query_filtered::<Entity, With<EditorView>>();
query
.iter(app.world())
.next()
.expect("editor view should exist")
}
fn runtime_buffer_insert_effects(
runtime: &WasmtimePluginRuntime,
identity: &str,
buffer: BufferEntity,
base_revision: TextRevision,
byte_index: u64,
text: &str,
) -> SealedPluginEffectBatch {
let identity = PluginIdentity::try_new(identity).expect("identity");
let spec = PluginRuntimeSpec::new(
AuthorizedPluginComponent::for_test(
identity.clone(),
EscapedDisplayText::from_display_text(format!("plugins/{}.wasm", identity.as_str())),
buffer_propose_edit_component_fixture(byte_index, text),
),
PluginWitWorld::current(),
PluginCapabilities {
buffer_propose_edit: true,
..PluginCapabilities::default()
},
PluginRuntimeLimits::default()
.validate()
.expect("limits should validate"),
);
let mut component = runtime.instantiate(&spec).expect("component should load");
let mut state = PluginInstanceState::new(spec);
issue_update_handles(
&mut state,
crate::ecs::components::buffer::ViewEntity(test_entity(183)),
buffer,
base_revision,
);
component
.update(&state)
.expect("component update should return")
.into_parts()
.effects
}
fn workspace_observe_task_batch_fixture(
runtime: &WasmtimePluginRuntime,
identity: &str,
path: &[u8],
) -> (
WasmtimePluginInstance,
PluginInstanceState,
SealedPluginWorkspaceObserveTaskBatch,
) {
let mut component = runtime
.instantiate(&runtime_spec_with_capabilities(
identity,
workspace_observe_task_component_fixture(path),
PluginCapabilities {
workspace_observe: vec![WorkspacePathGrant::new("docs")],
..PluginCapabilities::default()
},
))
.expect("component should load");
let mut state = PluginInstanceState::new(runtime_spec_with_capabilities(
identity,
workspace_observe_task_component_fixture(path),
PluginCapabilities {
workspace_observe: vec![WorkspacePathGrant::new("docs")],
..PluginCapabilities::default()
},
));
issue_update_handles(
&mut state,
crate::ecs::components::buffer::ViewEntity(test_entity(194)),
crate::ecs::components::buffer::BufferEntity(test_entity(23)),
TextRevision::from(0),
);
let batches = component
.update(&state)
.expect("component update should start observe task")
.into_parts();
assert!(batches.effects.is_empty());
assert!(batches.workspace_io.is_empty());
assert_eq!(batches.workspace_observe_tasks.task_count(), 1);
assert_eq!(
component
.store
.data()
.workspace_observe_tasks
.reserved_pending_len(),
1
);
(component, state, batches.workspace_observe_tasks)
}
fn owner_batches_for_identity(identity: &str) -> PluginHostOwnerBatches {
PluginHostOwnerBatches::for_test(
status_effect_batch(identity, "status"),
workspace_write_batch(identity, "docs/output.txt", b"bytes"),
)
}
fn empty_task_enqueue_report(
identity: &str,
) -> crate::plugin::PluginWorkspaceObserveTaskEnqueueReport {
let mut queue = PluginWorkspaceObserveTaskQueue::default();
empty_workspace_observe_task_batch(identity)
.enqueue(&mut queue)
.expect("empty observe-task batch should enqueue")
}
fn empty_workspace_observe_task_batch(identity: &str) -> SealedPluginWorkspaceObserveTaskBatch {
PendingPluginWorkspaceObserveTaskBatch::new(test_identity(identity)).seal()
}
fn status_effect_batch(identity: &str, status: &str) -> SealedPluginEffectBatch {
let mut batch =
PendingPluginUpdateBatch::new(test_identity(identity), PluginEffectBatchLimit::default());
batch
.push_status_text(
crate::ecs::components::buffer::ViewEntity(test_entity(211)),
status,
)
.expect("status effect should fit");
batch.seal()
}
fn workspace_write_batch(identity: &str, path: &str, bytes: &[u8]) -> SealedPluginWorkspaceIoBatch {
let host = PluginHostContext::for_test(
identity,
PluginCapabilities {
workspace_artifact_write: vec![WorkspacePathGrant::new("docs")],
..PluginCapabilities::default()
},
);
let mut batch = PendingPluginWorkspaceIoBatch::new(
test_identity(identity),
PluginWorkspaceIoBudget::try_new(1, 64, 64).expect("budget should validate"),
);
let access = host
.authorize_workspace_artifact_write(WorkspacePathRef::try_from(path).expect("path"))
.expect("workspace write should be authorized");
batch
.push_write(access, bytes.to_vec())
.expect("workspace write should fit");
batch.seal()
}
fn test_identity(identity: &str) -> PluginIdentity {
PluginIdentity::try_new(identity).expect("identity")
}
fn runtime_spec_with_capabilities(
identity: &str,
component_bytes: Vec<u8>,
capabilities: PluginCapabilities,
) -> PluginRuntimeSpec {
let identity = PluginIdentity::try_new(identity).expect("identity");
PluginRuntimeSpec::new(
AuthorizedPluginComponent::for_test(
identity.clone(),
EscapedDisplayText::from_display_text(format!("plugins/{}.wasm", identity.as_str())),
component_bytes,
),
PluginWitWorld::current(),
capabilities,
PluginRuntimeLimits::default()
.validate()
.expect("limits should validate"),
)
}
fn assert_wasmtime_task_enqueue_error_redacts(
error: &super::WasmtimeWorkspaceObserveTaskEnqueueError,
redacted_needles: &[&str],
) {
let debug = format!("{error:?}");
let display = error.to_string();
assert!(!debug.is_empty());
assert!(!display.is_empty());
assert!(debug.contains("kind"));
assert!(debug.contains("batch_identity"));
assert!(debug.contains("task_count"));
assert!(!debug.contains("SealedPluginWorkspaceObserveTaskBatch"));
assert!(!debug.contains("task_handles"));
for needle in redacted_needles {
assert!(!debug.contains(needle));
assert!(!display.contains(needle));
}
}
fn assert_wasmtime_schedule_error_debug_redacts(
debug: &str,
display: &str,
redacted_needles: &[&str],
) {
assert!(debug.contains("effect_count"));
assert!(debug.contains("workspace_io_request_count"));
assert!(debug.contains("enqueue_error_kind"));
assert!(debug.contains("workspace_observe_task_count"));
assert!(!debug.contains("PluginHostOwnerBatches"));
assert!(!debug.contains("SealedPluginWorkspaceObserveTaskBatch"));
assert!(!debug.contains("task_handles"));
for needle in redacted_needles {
assert!(!debug.contains(needle));
assert!(!display.contains(needle));
}
}
fn assert_scheduled_update_debug_redacts(
scheduled: &WasmtimePluginScheduledUpdate,
identity: &str,
scheduled_workspace_observe_task_count: usize,
redacted_needles: &[&str],
) {
assert_eq!(scheduled.identity(), identity);
assert_eq!(
scheduled.scheduled_workspace_observe_tasks().identity(),
identity
);
assert_eq!(
scheduled.scheduled_workspace_observe_tasks().task_count(),
scheduled_workspace_observe_task_count
);
let debug = format!("{scheduled:?}");
assert!(debug.contains("effect_count"));
assert!(debug.contains("workspace_io_request_count"));
assert!(debug.contains("scheduled_workspace_observe_task_count"));
assert!(!debug.contains("PluginHostOwnerBatches"));
assert!(!debug.contains("task_handles"));
for needle in redacted_needles {
assert!(!debug.contains(needle));
}
}
fn assert_scheduled_owner_publication_success(
publication: &crate::ecs::PluginScheduledUpdateOwnerPublicationReport,
identity: &str,
effect_count: usize,
workspace_io_request_count: usize,
scheduled_workspace_observe_task_count: usize,
) {
assert_eq!(publication.identity(), identity);
assert_eq!(publication.owner_publication().identity(), identity);
assert_eq!(publication.effect_count(), effect_count);
assert_eq!(
publication.workspace_io_request_count(),
workspace_io_request_count
);
assert_eq!(
publication.scheduled_workspace_observe_tasks().task_count(),
scheduled_workspace_observe_task_count
);
let debug = format!("{publication:?}");
assert!(debug.contains(&format!("effect_count: {effect_count}")));
assert!(debug.contains(&format!(
"workspace_io_request_count: {workspace_io_request_count}"
)));
assert!(debug.contains(&format!(
"scheduled_workspace_observe_task_count: {scheduled_workspace_observe_task_count}"
)));
assert!(!debug.contains("PluginOwnerPublicationReport"));
assert!(!debug.contains("PluginWorkspaceObserveTaskEnqueueReport"));
}
fn assert_scheduled_owner_publication_debug_redacts(debug: &str, display: &str) {
assert_scheduled_owner_publication_debug_shape(debug);
assert!(debug.contains("effect_count"));
assert!(debug.contains("workspace_io_request_count"));
assert!(!debug.contains("PluginHostOwnerBatches"));
assert!(!debug.contains("PluginOwnerQueueEnqueueError"));
for needle in SCHEDULED_OWNER_PUBLICATION_REDACTED_NEEDLES {
assert!(!debug.contains(needle));
assert!(!display.contains(needle));
}
}
fn assert_scheduled_owner_publication_debug_shape(debug: &str) {
assert!(debug.contains("scheduled_workspace_observe_task_count"));
assert!(!debug.contains("task_handles"));
assert!(!debug.contains("PluginHostOwnerDiscardReport"));
assert!(!debug.contains("PluginWorkspaceObserveTaskEnqueueReport"));
}
fn assert_panics_with(action: impl FnOnce(), expected: &str) {
let panic = panic::catch_unwind(AssertUnwindSafe(action))
.expect_err("expected invariant assertion to panic");
let message = panic_message(panic.as_ref());
assert!(
message.contains(expected),
"panic message {message:?} did not contain {expected:?}",
);
}
fn panic_message(payload: &(dyn Any + Send)) -> &str {
payload
.downcast_ref::<&str>()
.copied()
.or_else(|| payload.downcast_ref::<String>().map(String::as_str))
.unwrap_or("<non-string panic>")
}
const SCHEDULED_OWNER_PUBLICATION_REDACTED_NEEDLES: &[&str] = &[
"docs/input.txt",
"queued observe",
"secret owner status",
"docs/secret-output.txt",
"secret artifact",
];
const WASMTIME_SCHEDULE_ERROR_REDACTED_NEEDLES: &[&str] = &[
"secret-scheduled-task",
"secret scheduled owner status",
"secret-scheduled-owner-write",
"secret scheduled owner bytes",
];
fn capture_runtime_owner_results(
mut edited: MessageReader<BufferEdited>,
mut edit_rejected: MessageReader<BufferEditRejected>,
mut observed: bevy::prelude::ResMut<RuntimeOwnerResults>,
) {
observed.edited.extend(edited.read().copied());
observed.edit_rejected.extend(edit_rejected.read().cloned());
}
fn status_publish_component_fixture(text: &str) -> Vec<u8> {
let mut resolve = Resolve::new();
let package = resolve
.push_str(
"alma-editor-plugin.wit",
include_str!("../../../../wit/alma-editor-plugin.wit"),
)
.expect("fixture WIT should parse");
let world = resolve
.select_world(&[package], Some("plugin"))
.expect("fixture WIT should contain plugin world");
let (status_module, status_field, _) =
host_import(&resolve, world, WitHostImport::StatusPublish.wit_name());
let drop_imports = resource_drop_imports(&resolve, world);
let load_export = lifecycle_export_name(&resolve, world, "load");
let update_export = lifecycle_export_name(&resolve, world, "update");
let mut module = fixture_status_logic_module(
text.as_bytes(),
FixtureHostImport::new(&status_module, &status_field),
&drop_imports,
FixtureLifecycleExports::new(&load_export, &update_export),
)
.finish();
embed_component_metadata(&mut module, &resolve, world, StringEncoding::UTF8)
.expect("fixture metadata should embed");
ComponentEncoder::default()
.module(&module)
.expect("fixture module should decode")
.validate(true)
.encode()
.expect("fixture component should encode")
}
fn status_publish_twice_component_fixture(
first: StatusCallFixture<'_>,
second: StatusCallFixture<'_>,
) -> Vec<u8> {
let mut resolve = Resolve::new();
let package = resolve
.push_str(
"alma-editor-plugin.wit",
include_str!("../../../../wit/alma-editor-plugin.wit"),
)
.expect("fixture WIT should parse");
let world = resolve
.select_world(&[package], Some("plugin"))
.expect("fixture WIT should contain plugin world");
let (status_module, status_field, _) =
host_import(&resolve, world, WitHostImport::StatusPublish.wit_name());
let drop_imports = resource_drop_imports(&resolve, world);
let load_export = lifecycle_export_name(&resolve, world, "load");
let update_export = lifecycle_export_name(&resolve, world, "update");
let mut module = fixture_status_twice_logic_module(
first,
second,
false,
FixtureHostImport::new(&status_module, &status_field),
&drop_imports,
FixtureLifecycleExports::new(&load_export, &update_export),
)
.finish();
embed_component_metadata(&mut module, &resolve, world, StringEncoding::UTF8)
.expect("fixture metadata should embed");
ComponentEncoder::default()
.module(&module)
.expect("fixture module should decode")
.validate(true)
.encode()
.expect("fixture component should encode")
}
fn status_publish_then_loop_component_fixture(text: &[u8]) -> Vec<u8> {
status_publish_twice_or_loop_component_fixture(
StatusCallFixture {
handle_rep: UPDATE_VIEW_REP,
text,
},
None,
true,
)
}
fn status_publish_twice_or_loop_component_fixture(
first: StatusCallFixture<'_>,
second: Option<StatusCallFixture<'_>>,
loops: bool,
) -> Vec<u8> {
let mut resolve = Resolve::new();
let package = resolve
.push_str(
"alma-editor-plugin.wit",
include_str!("../../../../wit/alma-editor-plugin.wit"),
)
.expect("fixture WIT should parse");
let world = resolve
.select_world(&[package], Some("plugin"))
.expect("fixture WIT should contain plugin world");
let (status_module, status_field, _) =
host_import(&resolve, world, WitHostImport::StatusPublish.wit_name());
let drop_imports = resource_drop_imports(&resolve, world);
let load_export = lifecycle_export_name(&resolve, world, "load");
let update_export = lifecycle_export_name(&resolve, world, "update");
let second = second.unwrap_or(StatusCallFixture {
handle_rep: first.handle_rep,
text: b"",
});
let mut module = fixture_status_twice_logic_module(
first,
second,
loops,
FixtureHostImport::new(&status_module, &status_field),
&drop_imports,
FixtureLifecycleExports::new(&load_export, &update_export),
)
.finish();
embed_component_metadata(&mut module, &resolve, world, StringEncoding::UTF8)
.expect("fixture metadata should embed");
ComponentEncoder::default()
.module(&module)
.expect("fixture module should decode")
.validate(true)
.encode()
.expect("fixture component should encode")
}
fn status_publish_then_buffer_insert_component_fixture(
status: StatusCallFixture<'_>,
edit: BufferInsertFixture<'_>,
) -> Vec<u8> {
let mut resolve = Resolve::new();
let package = resolve
.push_str(
"alma-editor-plugin.wit",
include_str!("../../../../wit/alma-editor-plugin.wit"),
)
.expect("fixture WIT should parse");
let world = resolve
.select_world(&[package], Some("plugin"))
.expect("fixture WIT should contain plugin world");
let (status_module, status_field, _) =
host_import(&resolve, world, WitHostImport::StatusPublish.wit_name());
let (buffer_module, buffer_field, buffer_func) =
host_import(&resolve, world, WitHostImport::BufferProposeEdit.wit_name());
let drop_imports = resource_drop_imports(&resolve, world);
let load_export = lifecycle_export_name(&resolve, world, "load");
let update_export = lifecycle_export_name(&resolve, world, "update");
let mut module = fixture_status_then_buffer_edit_logic_module(
&resolve,
&buffer_func,
status,
edit,
&status_module,
&status_field,
&buffer_module,
&buffer_field,
&drop_imports,
&load_export,
&update_export,
)
.finish();
embed_component_metadata(&mut module, &resolve, world, StringEncoding::UTF8)
.expect("fixture metadata should embed");
ComponentEncoder::default()
.module(&module)
.expect("fixture module should decode")
.validate(true)
.encode()
.expect("fixture component should encode")
}
fn buffer_propose_edit_component_fixture(byte_index: u64, text: &str) -> Vec<u8> {
let mut resolve = Resolve::new();
let package = resolve
.push_str(
"alma-editor-plugin.wit",
include_str!("../../../../wit/alma-editor-plugin.wit"),
)
.expect("fixture WIT should parse");
let world = resolve
.select_world(&[package], Some("plugin"))
.expect("fixture WIT should contain plugin world");
let (buffer_module, buffer_field, buffer_func) =
host_import(&resolve, world, WitHostImport::BufferProposeEdit.wit_name());
let drop_imports = resource_drop_imports(&resolve, world);
let load_export = lifecycle_export_name(&resolve, world, "load");
let update_export = lifecycle_export_name(&resolve, world, "update");
let mut module = fixture_buffer_edit_logic_module(
&resolve,
&buffer_func,
BufferInsertFixture {
handle_rep: UPDATE_BUFFER_REP,
byte_index,
text: text.as_bytes(),
},
FixtureHostImport::new(&buffer_module, &buffer_field),
&drop_imports,
FixtureLifecycleExports::new(&load_export, &update_export),
)
.finish();
embed_component_metadata(&mut module, &resolve, world, StringEncoding::UTF8)
.expect("fixture metadata should embed");
ComponentEncoder::default()
.module(&module)
.expect("fixture module should decode")
.validate(true)
.encode()
.expect("fixture component should encode")
}
fn buffer_observe_component_fixture() -> Vec<u8> {
let mut resolve = Resolve::new();
let package = resolve
.push_str(
"alma-editor-plugin.wit",
include_str!("../../../../wit/alma-editor-plugin.wit"),
)
.expect("fixture WIT should parse");
let world = resolve
.select_world(&[package], Some("plugin"))
.expect("fixture WIT should contain plugin world");
let (buffer_module, buffer_field, buffer_func) =
host_import(&resolve, world, WitHostImport::BufferObserve.wit_name());
let drop_imports = resource_drop_imports(&resolve, world);
let load_export = lifecycle_export_name(&resolve, world, "load");
let update_export = lifecycle_export_name(&resolve, world, "update");
let mut module = fixture_buffer_observe_logic_module(
&resolve,
&buffer_func,
&buffer_module,
&buffer_field,
&drop_imports,
&load_export,
&update_export,
)
.finish();
embed_component_metadata(&mut module, &resolve, world, StringEncoding::UTF8)
.expect("fixture metadata should embed");
ComponentEncoder::default()
.module(&module)
.expect("fixture module should decode")
.validate(true)
.encode()
.expect("fixture component should encode")
}
fn buffer_observe_then_propose_edit_component_fixture(byte_index: u64, text: &str) -> Vec<u8> {
let mut resolve = Resolve::new();
let package = resolve
.push_str(
"alma-editor-plugin.wit",
include_str!("../../../../wit/alma-editor-plugin.wit"),
)
.expect("fixture WIT should parse");
let world = resolve
.select_world(&[package], Some("plugin"))
.expect("fixture WIT should contain plugin world");
let (observe_module, observe_field, observe_func) =
host_import(&resolve, world, WitHostImport::BufferObserve.wit_name());
let (propose_module, propose_field, propose_func) =
host_import(&resolve, world, WitHostImport::BufferProposeEdit.wit_name());
let drop_imports = resource_drop_imports(&resolve, world);
let load_export = lifecycle_export_name(&resolve, world, "load");
let update_export = lifecycle_export_name(&resolve, world, "update");
let mut module = fixture_buffer_observe_then_edit_logic_module(
&resolve,
&observe_func,
&propose_func,
BufferInsertFixture {
handle_rep: UPDATE_BUFFER_REP,
byte_index,
text: text.as_bytes(),
},
&observe_module,
&observe_field,
&propose_module,
&propose_field,
&drop_imports,
&load_export,
&update_export,
)
.finish();
embed_component_metadata(&mut module, &resolve, world, StringEncoding::UTF8)
.expect("fixture metadata should embed");
ComponentEncoder::default()
.module(&module)
.expect("fixture module should decode")
.validate(true)
.encode()
.expect("fixture component should encode")
}
fn workspace_artifact_write_component_fixture(calls: &[WorkspaceWriteFixture<'_>]) -> Vec<u8> {
assert!(!calls.is_empty());
let mut resolve = Resolve::new();
let package = resolve
.push_str(
"alma-editor-plugin.wit",
include_str!("../../../../wit/alma-editor-plugin.wit"),
)
.expect("fixture WIT should parse");
let world = resolve
.select_world(&[package], Some("plugin"))
.expect("fixture WIT should contain plugin world");
let (workspace_module, workspace_field, workspace_func) = host_import(
&resolve,
world,
WitHostImport::WorkspaceArtifactWrite.wit_name(),
);
let drop_imports = resource_drop_imports(&resolve, world);
let load_export = lifecycle_export_name(&resolve, world, "load");
let update_export = lifecycle_export_name(&resolve, world, "update");
let mut module = fixture_workspace_artifact_write_logic_module(
&resolve,
&workspace_func,
calls,
&workspace_module,
&workspace_field,
&drop_imports,
&load_export,
&update_export,
)
.finish();
embed_component_metadata(&mut module, &resolve, world, StringEncoding::UTF8)
.expect("fixture metadata should embed");
ComponentEncoder::default()
.module(&module)
.expect("fixture module should decode")
.validate(true)
.encode()
.expect("fixture component should encode")
}
fn workspace_observe_task_component_fixture(path: &[u8]) -> Vec<u8> {
let mut resolve = Resolve::new();
let package = resolve
.push_str(
"alma-editor-plugin.wit",
include_str!("../../../../wit/alma-editor-plugin.wit"),
)
.expect("fixture WIT should parse");
let world = resolve
.select_world(&[package], Some("plugin"))
.expect("fixture WIT should contain plugin world");
let (start_module, start_field, start_func) = host_import(
&resolve,
world,
WitHostImport::WorkspaceObserveStart.wit_name(),
);
let (poll_module, poll_field, poll_func) = host_import(
&resolve,
world,
WitHostImport::WorkspaceObservePoll.wit_name(),
);
let (take_module, take_field, take_func) = host_import(
&resolve,
world,
WitHostImport::WorkspaceObserveTake.wit_name(),
);
let (task_drop_module, task_drop_field) =
resource_drop_import(&resolve, world, "workspace-observe-task");
let drop_imports = resource_drop_imports(&resolve, world);
let load_export = lifecycle_export_name(&resolve, world, "load");
let update_export = lifecycle_export_name(&resolve, world, "update");
let mut module = fixture_workspace_observe_task_logic_module(
&resolve,
WorkspaceObserveTaskFixtureImports {
start: FixtureHostImport::new(&start_module, &start_field),
poll: FixtureHostImport::new(&poll_module, &poll_field),
take: FixtureHostImport::new(&take_module, &take_field),
task_drop: FixtureHostImport::new(&task_drop_module, &task_drop_field),
},
WorkspaceObserveTaskFixtureFunctions {
start: &start_func,
poll: &poll_func,
take: &take_func,
},
path,
&drop_imports,
FixtureLifecycleExports::new(&load_export, &update_export),
)
.finish();
embed_component_metadata(&mut module, &resolve, world, StringEncoding::UTF8)
.expect("fixture metadata should embed");
ComponentEncoder::default()
.module(&module)
.expect("fixture module should decode")
.validate(true)
.encode()
.expect("fixture component should encode")
}
fn workspace_observe_start_drop_component_fixture(path: &[u8]) -> Vec<u8> {
let mut resolve = Resolve::new();
let package = resolve
.push_str(
"alma-editor-plugin.wit",
include_str!("../../../../wit/alma-editor-plugin.wit"),
)
.expect("fixture WIT should parse");
let world = resolve
.select_world(&[package], Some("plugin"))
.expect("fixture WIT should contain plugin world");
let (start_module, start_field, start_func) = host_import(
&resolve,
world,
WitHostImport::WorkspaceObserveStart.wit_name(),
);
let (task_drop_module, task_drop_field) =
resource_drop_import(&resolve, world, "workspace-observe-task");
let drop_imports = resource_drop_imports(&resolve, world);
let load_export = lifecycle_export_name(&resolve, world, "load");
let update_export = lifecycle_export_name(&resolve, world, "update");
let mut module = fixture_workspace_observe_start_drop_logic_module(
&resolve,
&start_func,
path,
FixtureHostImport::new(&start_module, &start_field),
FixtureHostImport::new(&task_drop_module, &task_drop_field),
&drop_imports,
FixtureLifecycleExports::new(&load_export, &update_export),
)
.finish();
embed_component_metadata(&mut module, &resolve, world, StringEncoding::UTF8)
.expect("fixture metadata should embed");
ComponentEncoder::default()
.module(&module)
.expect("fixture module should decode")
.validate(true)
.encode()
.expect("fixture component should encode")
}
fn workspace_observe_start_then_status_component_fixture(path: &[u8], status: &[u8]) -> Vec<u8> {
let mut resolve = Resolve::new();
let package = resolve
.push_str(
"alma-editor-plugin.wit",
include_str!("../../../../wit/alma-editor-plugin.wit"),
)
.expect("fixture WIT should parse");
let world = resolve
.select_world(&[package], Some("plugin"))
.expect("fixture WIT should contain plugin world");
let (start_module, start_field, start_func) = host_import(
&resolve,
world,
WitHostImport::WorkspaceObserveStart.wit_name(),
);
let (status_module, status_field, _status_func) =
host_import(&resolve, world, WitHostImport::StatusPublish.wit_name());
let drop_imports = resource_drop_imports(&resolve, world);
let load_export = lifecycle_export_name(&resolve, world, "load");
let update_export = lifecycle_export_name(&resolve, world, "update");
let mut module = fixture_workspace_observe_start_then_status_logic_module(
&resolve,
&start_func,
path,
status,
WorkspaceObserveStartThenStatusFixtureImports {
start: FixtureHostImport::new(&start_module, &start_field),
status: FixtureHostImport::new(&status_module, &status_field),
},
&drop_imports,
FixtureLifecycleExports::new(&load_export, &update_export),
)
.finish();
embed_component_metadata(&mut module, &resolve, world, StringEncoding::UTF8)
.expect("fixture metadata should embed");
ComponentEncoder::default()
.module(&module)
.expect("fixture module should decode")
.validate(true)
.encode()
.expect("fixture component should encode")
}
fn workspace_observe_start_status_and_write_component_fixture(
observe_path: &[u8],
status: &[u8],
write: WorkspaceWriteFixture<'_>,
) -> Vec<u8> {
let mut resolve = Resolve::new();
let package = resolve
.push_str(
"alma-editor-plugin.wit",
include_str!("../../../../wit/alma-editor-plugin.wit"),
)
.expect("fixture WIT should parse");
let world = resolve
.select_world(&[package], Some("plugin"))
.expect("fixture WIT should contain plugin world");
let (start_module, start_field, start_func) = host_import(
&resolve,
world,
WitHostImport::WorkspaceObserveStart.wit_name(),
);
let (status_module, status_field, _status_func) =
host_import(&resolve, world, WitHostImport::StatusPublish.wit_name());
let (write_module, write_field, write_func) = host_import(
&resolve,
world,
WitHostImport::WorkspaceArtifactWrite.wit_name(),
);
let drop_imports = resource_drop_imports(&resolve, world);
let load_export = lifecycle_export_name(&resolve, world, "load");
let update_export = lifecycle_export_name(&resolve, world, "update");
let mut module = fixture_workspace_observe_start_status_and_write_logic_module(
&resolve,
&start_func,
&write_func,
observe_path,
status,
write,
WorkspaceObserveStartStatusWriteFixtureImports {
start: FixtureHostImport::new(&start_module, &start_field),
status: FixtureHostImport::new(&status_module, &status_field),
write: FixtureHostImport::new(&write_module, &write_field),
},
&drop_imports,
FixtureLifecycleExports::new(&load_export, &update_export),
)
.finish();
embed_component_metadata(&mut module, &resolve, world, StringEncoding::UTF8)
.expect("fixture metadata should embed");
ComponentEncoder::default()
.module(&module)
.expect("fixture module should decode")
.validate(true)
.encode()
.expect("fixture component should encode")
}
#[derive(Clone, Copy)]
struct WorkspaceWriteFixture<'a> {
path: &'a [u8],
bytes: &'a [u8],
}
#[derive(Clone, Copy)]
struct FixtureHostImport<'a> {
module: &'a str,
field: &'a str,
}
impl<'a> FixtureHostImport<'a> {
fn new(module: &'a str, field: &'a str) -> Self {
Self { module, field }
}
}
#[derive(Clone, Copy)]
struct FixtureLifecycleExports<'a> {
load: &'a str,
update: &'a str,
}
impl<'a> FixtureLifecycleExports<'a> {
fn new(load: &'a str, update: &'a str) -> Self {
Self { load, update }
}
}
#[derive(Clone, Copy)]
struct WorkspaceObserveTaskFixtureImports<'a> {
start: FixtureHostImport<'a>,
poll: FixtureHostImport<'a>,
take: FixtureHostImport<'a>,
task_drop: FixtureHostImport<'a>,
}
#[derive(Clone, Copy)]
struct WorkspaceObserveTaskFixtureFunctions<'a> {
start: &'a WitFunction,
poll: &'a WitFunction,
take: &'a WitFunction,
}
#[derive(Clone, Copy)]
struct WorkspaceObserveStartThenStatusFixtureImports<'a> {
start: FixtureHostImport<'a>,
status: FixtureHostImport<'a>,
}
#[derive(Clone, Copy)]
struct WorkspaceObserveStartStatusWriteFixtureImports<'a> {
start: FixtureHostImport<'a>,
status: FixtureHostImport<'a>,
write: FixtureHostImport<'a>,
}
struct WorkspaceObserveTaskFixtureSignatures {
start: WasmSignature,
poll: WasmSignature,
take: WasmSignature,
}
fn fixture_status_logic_module(
text: &[u8],
status_import: FixtureHostImport<'_>,
drop_imports: &ResourceDropImports,
lifecycle_exports: FixtureLifecycleExports<'_>,
) -> Module {
let mut types = TypeSection::new();
types
.ty()
.function([ValType::I32, ValType::I32, ValType::I32], []);
types.ty().function([], []);
types.ty().function([ValType::I32, ValType::I32], []);
types.ty().function([ValType::I32], []);
let mut imports = ImportSection::new();
let _ = imports.import(
status_import.module,
status_import.field,
EntityType::Function(0),
);
let _ = imports.import(
&drop_imports.view_module,
&drop_imports.view_field,
EntityType::Function(3),
);
let _ = imports.import(
&drop_imports.buffer_module,
&drop_imports.buffer_field,
EntityType::Function(3),
);
let mut functions = FunctionSection::new();
let _ = functions.function(1);
let _ = functions.function(2);
let mut memories = MemorySection::new();
let _ = memories.memory(fixture_memory_type());
let mut exports = ExportSection::new();
let _ = exports.export("memory", ExportKind::Memory, 0);
let _ = exports.export(lifecycle_exports.load, ExportKind::Func, 3);
let _ = exports.export(lifecycle_exports.update, ExportKind::Func, 4);
let mut load = Function::new([]);
let _ = load.instruction(&Instruction::End);
let mut update = Function::new([]);
let _ = update.instruction(&Instruction::LocalGet(0));
let _ = update.instruction(&Instruction::I32Const(0));
let _ = update.instruction(&Instruction::I32Const(
i32::try_from(text.len()).expect("fixture text should fit i32"),
));
let _ = update.instruction(&Instruction::Call(0));
push_update_resource_drops(&mut update, 1, 2);
let _ = update.instruction(&Instruction::End);
let mut code = CodeSection::new();
let _ = code.function(&load);
let _ = code.function(&update);
let mut data = wasm_encoder::DataSection::new();
let _ = data.active(0, &ConstExpr::i32_const(0), text.iter().copied());
let mut module = Module::new();
let _ = module.section(&types);
let _ = module.section(&imports);
let _ = module.section(&functions);
let _ = module.section(&memories);
let _ = module.section(&exports);
let _ = module.section(&code);
let _ = module.section(&data);
module
}
#[derive(Clone, Copy)]
struct StatusCallFixture<'a> {
handle_rep: u32,
text: &'a [u8],
}
fn fixture_status_twice_logic_module(
first: StatusCallFixture<'_>,
second: StatusCallFixture<'_>,
loops: bool,
status_import: FixtureHostImport<'_>,
drop_imports: &ResourceDropImports,
lifecycle_exports: FixtureLifecycleExports<'_>,
) -> Module {
let mut types = TypeSection::new();
types
.ty()
.function([ValType::I32, ValType::I32, ValType::I32], []);
types.ty().function([], []);
types.ty().function([ValType::I32, ValType::I32], []);
types.ty().function([ValType::I32], []);
let mut imports = ImportSection::new();
let _ = imports.import(
status_import.module,
status_import.field,
EntityType::Function(0),
);
let _ = imports.import(
&drop_imports.view_module,
&drop_imports.view_field,
EntityType::Function(3),
);
let _ = imports.import(
&drop_imports.buffer_module,
&drop_imports.buffer_field,
EntityType::Function(3),
);
let mut functions = FunctionSection::new();
let _ = functions.function(1);
let _ = functions.function(2);
let mut memories = MemorySection::new();
let _ = memories.memory(fixture_memory_type());
let mut exports = ExportSection::new();
let _ = exports.export("memory", ExportKind::Memory, 0);
let _ = exports.export(lifecycle_exports.load, ExportKind::Func, 3);
let _ = exports.export(lifecycle_exports.update, ExportKind::Func, 4);
let mut load = Function::new([]);
let _ = load.instruction(&Instruction::End);
let mut update = Function::new([]);
push_status_call(&mut update, first, 0);
let second_offset = i32::try_from(first.text.len()).expect("fixture text should fit i32");
if loops {
let _ = update.instruction(&Instruction::Loop(BlockType::Empty));
let _ = update.instruction(&Instruction::Br(0));
let _ = update.instruction(&Instruction::End);
} else {
push_status_call(&mut update, second, second_offset);
}
push_update_resource_drops(&mut update, 1, 2);
let _ = update.instruction(&Instruction::End);
let mut code = CodeSection::new();
let _ = code.function(&load);
let _ = code.function(&update);
let mut data = wasm_encoder::DataSection::new();
let bytes = [first.text, second.text].concat();
let _ = data.active(0, &ConstExpr::i32_const(0), bytes);
let mut module = Module::new();
let _ = module.section(&types);
let _ = module.section(&imports);
let _ = module.section(&functions);
let _ = module.section(&memories);
let _ = module.section(&exports);
let _ = module.section(&code);
let _ = module.section(&data);
module
}
#[allow(clippy::too_many_arguments)]
fn fixture_status_then_buffer_edit_logic_module(
resolve: &Resolve,
buffer_func: &WitFunction,
status: StatusCallFixture<'_>,
edit: BufferInsertFixture<'_>,
status_module: &str,
status_field: &str,
buffer_module: &str,
buffer_field: &str,
drop_imports: &ResourceDropImports,
load_export: &str,
update_export: &str,
) -> Module {
let signature = resolve.wasm_signature(AbiVariant::GuestImport, buffer_func);
assert_eq!(signature.params.as_slice(), buffer_insert_signature());
assert!(signature.results.is_empty());
let mut types = TypeSection::new();
types
.ty()
.function([ValType::I32, ValType::I32, ValType::I32], []);
types.ty().function(
signature.params.iter().copied().map(val_type),
signature.results.iter().copied().map(val_type),
);
types.ty().function([], []);
types.ty().function([ValType::I32, ValType::I32], []);
types.ty().function([ValType::I32], []);
let mut imports = ImportSection::new();
let _ = imports.import(status_module, status_field, EntityType::Function(0));
let _ = imports.import(buffer_module, buffer_field, EntityType::Function(1));
let _ = imports.import(
&drop_imports.view_module,
&drop_imports.view_field,
EntityType::Function(4),
);
let _ = imports.import(
&drop_imports.buffer_module,
&drop_imports.buffer_field,
EntityType::Function(4),
);
let mut functions = FunctionSection::new();
let _ = functions.function(2);
let _ = functions.function(3);
let mut memories = MemorySection::new();
let _ = memories.memory(fixture_memory_type());
let mut exports = ExportSection::new();
let _ = exports.export("memory", ExportKind::Memory, 0);
let _ = exports.export(load_export, ExportKind::Func, 4);
let _ = exports.export(update_export, ExportKind::Func, 5);
let mut load = Function::new([]);
let _ = load.instruction(&Instruction::End);
let mut update = Function::new([]);
push_status_call(&mut update, status, 0);
let edit_text_offset = i32::try_from(status.text.len()).expect("fixture text should fit i32");
push_buffer_insert_call(&mut update, edit, edit_text_offset, 1);
push_update_resource_drops(&mut update, 2, 3);
let _ = update.instruction(&Instruction::End);
let mut code = CodeSection::new();
let _ = code.function(&load);
let _ = code.function(&update);
let mut data = wasm_encoder::DataSection::new();
let bytes = [status.text, edit.text].concat();
let _ = data.active(0, &ConstExpr::i32_const(0), bytes);
let mut module = Module::new();
let _ = module.section(&types);
let _ = module.section(&imports);
let _ = module.section(&functions);
let _ = module.section(&memories);
let _ = module.section(&exports);
let _ = module.section(&code);
let _ = module.section(&data);
module
}
fn push_status_call(function: &mut Function, status: StatusCallFixture<'_>, ptr: i32) {
if status.handle_rep == UPDATE_VIEW_REP {
let _ = function.instruction(&Instruction::LocalGet(0));
} else {
let _ = function.instruction(&Instruction::I32Const(
i32::try_from(status.handle_rep).expect("fixture resource rep should fit i32"),
));
}
let _ = function.instruction(&Instruction::I32Const(ptr));
let _ = function.instruction(&Instruction::I32Const(
i32::try_from(status.text.len()).expect("fixture text should fit i32"),
));
let _ = function.instruction(&Instruction::Call(0));
}
fn push_update_resource_drops(
function: &mut Function,
view_drop_index: u32,
buffer_drop_index: u32,
) {
let _ = function.instruction(&Instruction::LocalGet(0));
let _ = function.instruction(&Instruction::Call(view_drop_index));
let _ = function.instruction(&Instruction::LocalGet(1));
let _ = function.instruction(&Instruction::Call(buffer_drop_index));
}
#[derive(Clone, Copy)]
struct BufferInsertFixture<'a> {
handle_rep: u32,
byte_index: u64,
text: &'a [u8],
}
fn fixture_buffer_edit_logic_module(
resolve: &Resolve,
buffer_func: &WitFunction,
edit: BufferInsertFixture<'_>,
buffer_import: FixtureHostImport<'_>,
drop_imports: &ResourceDropImports,
lifecycle_exports: FixtureLifecycleExports<'_>,
) -> Module {
let signature = resolve.wasm_signature(AbiVariant::GuestImport, buffer_func);
assert_eq!(signature.params.as_slice(), buffer_insert_signature());
assert!(signature.results.is_empty());
let mut types = TypeSection::new();
types.ty().function(
signature.params.iter().copied().map(val_type),
signature.results.iter().copied().map(val_type),
);
types.ty().function([], []);
types.ty().function([ValType::I32, ValType::I32], []);
types.ty().function([ValType::I32], []);
let mut imports = ImportSection::new();
let _ = imports.import(
buffer_import.module,
buffer_import.field,
EntityType::Function(0),
);
let _ = imports.import(
&drop_imports.view_module,
&drop_imports.view_field,
EntityType::Function(3),
);
let _ = imports.import(
&drop_imports.buffer_module,
&drop_imports.buffer_field,
EntityType::Function(3),
);
let mut functions = FunctionSection::new();
let _ = functions.function(1);
let _ = functions.function(2);
let mut memories = MemorySection::new();
let _ = memories.memory(fixture_memory_type());
let mut exports = ExportSection::new();
let _ = exports.export("memory", ExportKind::Memory, 0);
let _ = exports.export(lifecycle_exports.load, ExportKind::Func, 3);
let _ = exports.export(lifecycle_exports.update, ExportKind::Func, 4);
let mut load = Function::new([]);
let _ = load.instruction(&Instruction::End);
let mut update = Function::new([]);
push_buffer_insert_call(&mut update, edit, 0, 0);
push_update_resource_drops(&mut update, 1, 2);
let _ = update.instruction(&Instruction::End);
let mut code = CodeSection::new();
let _ = code.function(&load);
let _ = code.function(&update);
let mut data = wasm_encoder::DataSection::new();
let _ = data.active(0, &ConstExpr::i32_const(0), edit.text.iter().copied());
let mut module = Module::new();
let _ = module.section(&types);
let _ = module.section(&imports);
let _ = module.section(&functions);
let _ = module.section(&memories);
let _ = module.section(&exports);
let _ = module.section(&code);
let _ = module.section(&data);
module
}
fn fixture_buffer_observe_logic_module(
resolve: &Resolve,
buffer_func: &WitFunction,
buffer_module: &str,
buffer_field: &str,
drop_imports: &ResourceDropImports,
load_export: &str,
update_export: &str,
) -> Module {
let signature = resolve.wasm_signature(AbiVariant::GuestImport, buffer_func);
assert_eq!(signature.params.first(), Some(&WasmType::I32));
let mut types = TypeSection::new();
types.ty().function(
signature.params.iter().copied().map(val_type),
signature.results.iter().copied().map(val_type),
);
types.ty().function([], []);
types.ty().function([ValType::I32, ValType::I32], []);
types.ty().function(
[ValType::I32, ValType::I32, ValType::I32, ValType::I32],
[ValType::I32],
);
types.ty().function([ValType::I32], []);
let mut imports = ImportSection::new();
let _ = imports.import(buffer_module, buffer_field, EntityType::Function(0));
let _ = imports.import(
&drop_imports.view_module,
&drop_imports.view_field,
EntityType::Function(4),
);
let _ = imports.import(
&drop_imports.buffer_module,
&drop_imports.buffer_field,
EntityType::Function(4),
);
let mut functions = FunctionSection::new();
let _ = functions.function(1);
let _ = functions.function(2);
let _ = functions.function(3);
let mut memories = MemorySection::new();
let _ = memories.memory(fixture_memory_type());
let mut exports = ExportSection::new();
let _ = exports.export("memory", ExportKind::Memory, 0);
let _ = exports.export("cabi_realloc", ExportKind::Func, 5);
let _ = exports.export(load_export, ExportKind::Func, 3);
let _ = exports.export(update_export, ExportKind::Func, 4);
let mut load = Function::new([]);
let _ = load.instruction(&Instruction::End);
let mut update = Function::new([]);
let _ = update.instruction(&Instruction::LocalGet(1));
for extra in signature.params.iter().skip(1) {
push_zero(&mut update, *extra);
}
let _ = update.instruction(&Instruction::Call(0));
for _result in &signature.results {
let _ = update.instruction(&Instruction::Drop);
}
push_update_resource_drops(&mut update, 1, 2);
let _ = update.instruction(&Instruction::End);
let mut realloc = Function::new([]);
let _ = realloc.instruction(&Instruction::I32Const(1024));
let _ = realloc.instruction(&Instruction::End);
let mut code = CodeSection::new();
let _ = code.function(&load);
let _ = code.function(&update);
let _ = code.function(&realloc);
let mut module = Module::new();
let _ = module.section(&types);
let _ = module.section(&imports);
let _ = module.section(&functions);
let _ = module.section(&memories);
let _ = module.section(&exports);
let _ = module.section(&code);
module
}
#[allow(clippy::too_many_arguments)]
fn fixture_buffer_observe_then_edit_logic_module(
resolve: &Resolve,
observe_func: &WitFunction,
propose_func: &WitFunction,
edit: BufferInsertFixture<'_>,
observe_module: &str,
observe_field: &str,
propose_module: &str,
propose_field: &str,
drop_imports: &ResourceDropImports,
load_export: &str,
update_export: &str,
) -> Module {
let observe_signature = resolve.wasm_signature(AbiVariant::GuestImport, observe_func);
assert_eq!(observe_signature.params.first(), Some(&WasmType::I32));
let propose_signature = resolve.wasm_signature(AbiVariant::GuestImport, propose_func);
assert_eq!(
propose_signature.params.as_slice(),
buffer_insert_signature()
);
assert!(propose_signature.results.is_empty());
let mut types = TypeSection::new();
types.ty().function(
observe_signature.params.iter().copied().map(val_type),
observe_signature.results.iter().copied().map(val_type),
);
types.ty().function(
propose_signature.params.iter().copied().map(val_type),
propose_signature.results.iter().copied().map(val_type),
);
types.ty().function([], []);
types.ty().function([ValType::I32, ValType::I32], []);
types.ty().function(
[ValType::I32, ValType::I32, ValType::I32, ValType::I32],
[ValType::I32],
);
types.ty().function([ValType::I32], []);
let mut imports = ImportSection::new();
let _ = imports.import(observe_module, observe_field, EntityType::Function(0));
let _ = imports.import(propose_module, propose_field, EntityType::Function(1));
let _ = imports.import(
&drop_imports.view_module,
&drop_imports.view_field,
EntityType::Function(5),
);
let _ = imports.import(
&drop_imports.buffer_module,
&drop_imports.buffer_field,
EntityType::Function(5),
);
let mut functions = FunctionSection::new();
let _ = functions.function(2);
let _ = functions.function(3);
let _ = functions.function(4);
let mut memories = MemorySection::new();
let _ = memories.memory(fixture_memory_type());
let mut exports = ExportSection::new();
let _ = exports.export("memory", ExportKind::Memory, 0);
let _ = exports.export("cabi_realloc", ExportKind::Func, 6);
let _ = exports.export(load_export, ExportKind::Func, 4);
let _ = exports.export(update_export, ExportKind::Func, 5);
let mut load = Function::new([]);
let _ = load.instruction(&Instruction::End);
let mut update = Function::new([]);
let _ = update.instruction(&Instruction::LocalGet(1));
for (index, extra) in observe_signature.params.iter().skip(1).enumerate() {
if index == 0 && val_type(*extra) == ValType::I32 {
let _ = update.instruction(&Instruction::I32Const(128));
} else {
push_zero(&mut update, *extra);
}
}
let _ = update.instruction(&Instruction::Call(0));
for _result in &observe_signature.results {
let _ = update.instruction(&Instruction::Drop);
}
push_buffer_insert_call(&mut update, edit, 0, 1);
push_update_resource_drops(&mut update, 2, 3);
let _ = update.instruction(&Instruction::End);
let mut realloc = Function::new([]);
let _ = realloc.instruction(&Instruction::I32Const(1024));
let _ = realloc.instruction(&Instruction::End);
let mut code = CodeSection::new();
let _ = code.function(&load);
let _ = code.function(&update);
let _ = code.function(&realloc);
let mut data = wasm_encoder::DataSection::new();
let _ = data.active(0, &ConstExpr::i32_const(0), edit.text.iter().copied());
let mut module = Module::new();
let _ = module.section(&types);
let _ = module.section(&imports);
let _ = module.section(&functions);
let _ = module.section(&memories);
let _ = module.section(&exports);
let _ = module.section(&code);
let _ = module.section(&data);
module
}
#[allow(clippy::too_many_arguments)]
fn fixture_workspace_artifact_write_logic_module(
resolve: &Resolve,
workspace_func: &WitFunction,
calls: &[WorkspaceWriteFixture<'_>],
workspace_module: &str,
workspace_field: &str,
drop_imports: &ResourceDropImports,
load_export: &str,
update_export: &str,
) -> Module {
let signature = resolve.wasm_signature(AbiVariant::GuestImport, workspace_func);
assert_eq!(
signature.params.as_slice(),
workspace_artifact_write_signature()
);
assert!(signature.results.is_empty());
let mut types = TypeSection::new();
types.ty().function(
signature.params.iter().copied().map(val_type),
signature.results.iter().copied().map(val_type),
);
types.ty().function([], []);
types.ty().function([ValType::I32, ValType::I32], []);
types.ty().function([ValType::I32], []);
let mut imports = ImportSection::new();
let _ = imports.import(workspace_module, workspace_field, EntityType::Function(0));
let _ = imports.import(
&drop_imports.view_module,
&drop_imports.view_field,
EntityType::Function(3),
);
let _ = imports.import(
&drop_imports.buffer_module,
&drop_imports.buffer_field,
EntityType::Function(3),
);
let mut functions = FunctionSection::new();
let _ = functions.function(1);
let _ = functions.function(2);
let mut memories = MemorySection::new();
let _ = memories.memory(fixture_memory_type());
let mut exports = ExportSection::new();
let _ = exports.export("memory", ExportKind::Memory, 0);
let _ = exports.export(load_export, ExportKind::Func, 3);
let _ = exports.export(update_export, ExportKind::Func, 4);
let mut load = Function::new([]);
let _ = load.instruction(&Instruction::End);
let mut update = Function::new([]);
let mut offset = 0i32;
for call in calls {
let path_pointer = offset;
let path_len = i32::try_from(call.path.len()).expect("fixture path should fit i32");
let bytes_pointer = path_pointer
.checked_add(path_len)
.expect("fixture data offset should fit i32");
let bytes_len = i32::try_from(call.bytes.len()).expect("fixture bytes should fit i32");
push_workspace_artifact_write_call(&mut update, *call, path_pointer, bytes_pointer, 0);
offset = bytes_pointer
.checked_add(bytes_len)
.expect("fixture data offset should fit i32");
}
push_update_resource_drops(&mut update, 1, 2);
let _ = update.instruction(&Instruction::End);
let mut code = CodeSection::new();
let _ = code.function(&load);
let _ = code.function(&update);
let mut data = wasm_encoder::DataSection::new();
let _ = data.active(
0,
&ConstExpr::i32_const(0),
workspace_write_fixture_bytes(calls),
);
let mut module = Module::new();
let _ = module.section(&types);
let _ = module.section(&imports);
let _ = module.section(&functions);
let _ = module.section(&memories);
let _ = module.section(&exports);
let _ = module.section(&code);
let _ = module.section(&data);
module
}
fn fixture_workspace_observe_task_logic_module(
resolve: &Resolve,
imports: WorkspaceObserveTaskFixtureImports<'_>,
functions: WorkspaceObserveTaskFixtureFunctions<'_>,
path: &[u8],
drop_imports: &ResourceDropImports,
lifecycle_exports: FixtureLifecycleExports<'_>,
) -> Module {
let signatures = workspace_observe_task_fixture_signatures(resolve, functions);
let types = workspace_observe_task_fixture_types(&signatures);
let imports_section = workspace_observe_task_fixture_imports(imports, drop_imports);
let functions_section = workspace_observe_task_fixture_function_section();
let mut memories = MemorySection::new();
let _ = memories.memory(fixture_memory_type());
let globals = workspace_observe_task_fixture_globals();
let exports = workspace_observe_task_fixture_exports(lifecycle_exports);
let mut load = Function::new([]);
let _ = load.instruction(&Instruction::End);
let mut update = Function::new([]);
let _ = update.instruction(&Instruction::GlobalGet(1));
let _ = update.instruction(&Instruction::I32Eqz);
let _ = update.instruction(&Instruction::If(BlockType::Empty));
push_workspace_observe_start_call(&mut update, path, 0);
let _ = update.instruction(&Instruction::GlobalSet(0));
let _ = update.instruction(&Instruction::I32Const(1));
let _ = update.instruction(&Instruction::GlobalSet(1));
let _ = update.instruction(&Instruction::Else);
push_workspace_observe_poll_call(&mut update, &signatures.poll, 1);
push_workspace_observe_take_call(&mut update, &signatures.take, 2);
let _ = update.instruction(&Instruction::GlobalGet(0));
let _ = update.instruction(&Instruction::Call(3));
let _ = update.instruction(&Instruction::I32Const(2));
let _ = update.instruction(&Instruction::GlobalSet(1));
let _ = update.instruction(&Instruction::End);
push_update_resource_drops(&mut update, 4, 5);
let _ = update.instruction(&Instruction::End);
let mut realloc = Function::new([]);
let _ = realloc.instruction(&Instruction::I32Const(1024));
let _ = realloc.instruction(&Instruction::End);
let mut code = CodeSection::new();
let _ = code.function(&load);
let _ = code.function(&update);
let _ = code.function(&realloc);
let mut data = wasm_encoder::DataSection::new();
let _ = data.active(0, &ConstExpr::i32_const(0), path.iter().copied());
let mut module = Module::new();
let _ = module.section(&types);
let _ = module.section(&imports_section);
let _ = module.section(&functions_section);
let _ = module.section(&memories);
let _ = module.section(&globals);
let _ = module.section(&exports);
let _ = module.section(&code);
let _ = module.section(&data);
module
}
fn fixture_workspace_observe_start_drop_logic_module(
resolve: &Resolve,
start_func: &WitFunction,
path: &[u8],
start_import: FixtureHostImport<'_>,
task_drop_import: FixtureHostImport<'_>,
drop_imports: &ResourceDropImports,
lifecycle_exports: FixtureLifecycleExports<'_>,
) -> Module {
let start = resolve.wasm_signature(AbiVariant::GuestImport, start_func);
assert_eq!(start.params.as_slice(), workspace_observe_start_signature());
assert_eq!(start.results.as_slice(), &[WasmType::I32]);
let mut types = TypeSection::new();
types.ty().function(
start.params.iter().copied().map(val_type),
start.results.iter().copied().map(val_type),
);
types.ty().function([ValType::I32], []);
types.ty().function([], []);
types.ty().function([ValType::I32, ValType::I32], []);
let mut imports_section = ImportSection::new();
for (import, type_index) in [
(start_import, 0),
(task_drop_import, 1),
(
FixtureHostImport::new(&drop_imports.view_module, &drop_imports.view_field),
1,
),
(
FixtureHostImport::new(&drop_imports.buffer_module, &drop_imports.buffer_field),
1,
),
] {
let _ = imports_section.import(
import.module,
import.field,
EntityType::Function(type_index),
);
}
let mut functions = FunctionSection::new();
let _ = functions.function(2);
let _ = functions.function(3);
let mut memories = MemorySection::new();
let _ = memories.memory(fixture_memory_type());
let mut exports = ExportSection::new();
let _ = exports.export("memory", ExportKind::Memory, 0);
let _ = exports.export(lifecycle_exports.load, ExportKind::Func, 4);
let _ = exports.export(lifecycle_exports.update, ExportKind::Func, 5);
let mut load = Function::new([]);
let _ = load.instruction(&Instruction::End);
let mut update = Function::new([]);
push_workspace_observe_start_call(&mut update, path, 0);
let _ = update.instruction(&Instruction::Call(1));
push_update_resource_drops(&mut update, 2, 3);
let _ = update.instruction(&Instruction::End);
let mut code = CodeSection::new();
let _ = code.function(&load);
let _ = code.function(&update);
let mut data = wasm_encoder::DataSection::new();
let _ = data.active(0, &ConstExpr::i32_const(0), path.iter().copied());
let mut module = Module::new();
let _ = module.section(&types);
let _ = module.section(&imports_section);
let _ = module.section(&functions);
let _ = module.section(&memories);
let _ = module.section(&exports);
let _ = module.section(&code);
let _ = module.section(&data);
module
}
fn fixture_workspace_observe_start_then_status_logic_module(
resolve: &Resolve,
start_func: &WitFunction,
path: &[u8],
status: &[u8],
imports: WorkspaceObserveStartThenStatusFixtureImports<'_>,
drop_imports: &ResourceDropImports,
lifecycle_exports: FixtureLifecycleExports<'_>,
) -> Module {
const STATUS_OFFSET: i32 = 512;
let start = resolve.wasm_signature(AbiVariant::GuestImport, start_func);
assert_eq!(start.params.as_slice(), workspace_observe_start_signature());
assert_eq!(start.results.as_slice(), &[WasmType::I32]);
let mut types = TypeSection::new();
types.ty().function(
start.params.iter().copied().map(val_type),
start.results.iter().copied().map(val_type),
);
types
.ty()
.function([ValType::I32, ValType::I32, ValType::I32], []);
types.ty().function([], []);
types.ty().function([ValType::I32, ValType::I32], []);
types.ty().function([ValType::I32], []);
let mut imports_section = ImportSection::new();
for (import, type_index) in [
(imports.start, 0),
(imports.status, 1),
(
FixtureHostImport::new(&drop_imports.view_module, &drop_imports.view_field),
4,
),
(
FixtureHostImport::new(&drop_imports.buffer_module, &drop_imports.buffer_field),
4,
),
] {
let _ = imports_section.import(
import.module,
import.field,
EntityType::Function(type_index),
);
}
let mut functions = FunctionSection::new();
let _ = functions.function(2);
let _ = functions.function(3);
let mut memories = MemorySection::new();
let _ = memories.memory(fixture_memory_type());
let mut globals = GlobalSection::new();
let _ = globals.global(
GlobalType {
val_type: ValType::I32,
mutable: true,
shared: false,
},
&ConstExpr::i32_const(0),
);
let mut exports = ExportSection::new();
let _ = exports.export("memory", ExportKind::Memory, 0);
let _ = exports.export(lifecycle_exports.load, ExportKind::Func, 4);
let _ = exports.export(lifecycle_exports.update, ExportKind::Func, 5);
let mut load = Function::new([]);
let _ = load.instruction(&Instruction::End);
let mut update = Function::new([]);
push_workspace_observe_start_call(&mut update, path, 0);
let _ = update.instruction(&Instruction::GlobalSet(0));
let _ = update.instruction(&Instruction::LocalGet(0));
let _ = update.instruction(&Instruction::I32Const(STATUS_OFFSET));
let _ = update.instruction(&Instruction::I32Const(
i32::try_from(status.len()).expect("fixture status should fit i32"),
));
let _ = update.instruction(&Instruction::Call(1));
push_update_resource_drops(&mut update, 2, 3);
let _ = update.instruction(&Instruction::End);
let mut code = CodeSection::new();
let _ = code.function(&load);
let _ = code.function(&update);
let mut data = wasm_encoder::DataSection::new();
let _ = data.active(0, &ConstExpr::i32_const(0), path.iter().copied());
let _ = data.active(
0,
&ConstExpr::i32_const(STATUS_OFFSET),
status.iter().copied(),
);
let mut module = Module::new();
let _ = module.section(&types);
let _ = module.section(&imports_section);
let _ = module.section(&functions);
let _ = module.section(&memories);
let _ = module.section(&globals);
let _ = module.section(&exports);
let _ = module.section(&code);
let _ = module.section(&data);
module
}
#[allow(clippy::too_many_arguments, clippy::too_many_lines)]
fn fixture_workspace_observe_start_status_and_write_logic_module(
resolve: &Resolve,
start_func: &WitFunction,
write_func: &WitFunction,
observe_path: &[u8],
status: &[u8],
write: WorkspaceWriteFixture<'_>,
imports: WorkspaceObserveStartStatusWriteFixtureImports<'_>,
drop_imports: &ResourceDropImports,
lifecycle_exports: FixtureLifecycleExports<'_>,
) -> Module {
const STATUS_OFFSET: i32 = 512;
const WRITE_OFFSET: i32 = 1024;
let start = resolve.wasm_signature(AbiVariant::GuestImport, start_func);
assert_eq!(start.params.as_slice(), workspace_observe_start_signature());
assert_eq!(start.results.as_slice(), &[WasmType::I32]);
let write_signature = resolve.wasm_signature(AbiVariant::GuestImport, write_func);
assert_eq!(
write_signature.params.as_slice(),
workspace_artifact_write_signature()
);
assert!(write_signature.results.is_empty());
let mut types = TypeSection::new();
types.ty().function(
start.params.iter().copied().map(val_type),
start.results.iter().copied().map(val_type),
);
types
.ty()
.function([ValType::I32, ValType::I32, ValType::I32], []);
types.ty().function(
write_signature.params.iter().copied().map(val_type),
write_signature.results.iter().copied().map(val_type),
);
types.ty().function([], []);
types.ty().function([ValType::I32, ValType::I32], []);
types.ty().function([ValType::I32], []);
let mut imports_section = ImportSection::new();
for (import, type_index) in [
(imports.start, 0),
(imports.status, 1),
(imports.write, 2),
(
FixtureHostImport::new(&drop_imports.view_module, &drop_imports.view_field),
5,
),
(
FixtureHostImport::new(&drop_imports.buffer_module, &drop_imports.buffer_field),
5,
),
] {
let _ = imports_section.import(
import.module,
import.field,
EntityType::Function(type_index),
);
}
let mut functions = FunctionSection::new();
let _ = functions.function(3);
let _ = functions.function(4);
let mut memories = MemorySection::new();
let _ = memories.memory(fixture_memory_type());
let mut globals = GlobalSection::new();
let _ = globals.global(
GlobalType {
val_type: ValType::I32,
mutable: true,
shared: false,
},
&ConstExpr::i32_const(0),
);
let mut exports = ExportSection::new();
let _ = exports.export("memory", ExportKind::Memory, 0);
let _ = exports.export(lifecycle_exports.load, ExportKind::Func, 5);
let _ = exports.export(lifecycle_exports.update, ExportKind::Func, 6);
let mut load = Function::new([]);
let _ = load.instruction(&Instruction::End);
let write_path_pointer = WRITE_OFFSET;
let write_bytes_pointer = write_path_pointer
.checked_add(i32::try_from(write.path.len()).expect("fixture path should fit i32"))
.expect("fixture data offset should fit i32");
let mut update = Function::new([]);
push_workspace_observe_start_call(&mut update, observe_path, 0);
let _ = update.instruction(&Instruction::GlobalSet(0));
let _ = update.instruction(&Instruction::LocalGet(0));
let _ = update.instruction(&Instruction::I32Const(STATUS_OFFSET));
let _ = update.instruction(&Instruction::I32Const(
i32::try_from(status.len()).expect("fixture status should fit i32"),
));
let _ = update.instruction(&Instruction::Call(1));
push_workspace_artifact_write_call(
&mut update,
write,
write_path_pointer,
write_bytes_pointer,
2,
);
push_update_resource_drops(&mut update, 3, 4);
let _ = update.instruction(&Instruction::End);
let mut code = CodeSection::new();
let _ = code.function(&load);
let _ = code.function(&update);
let mut data = wasm_encoder::DataSection::new();
let _ = data.active(0, &ConstExpr::i32_const(0), observe_path.iter().copied());
let _ = data.active(
0,
&ConstExpr::i32_const(STATUS_OFFSET),
status.iter().copied(),
);
let _ = data.active(0, &ConstExpr::i32_const(WRITE_OFFSET), {
let mut bytes = Vec::new();
bytes.extend_from_slice(write.path);
bytes.extend_from_slice(write.bytes);
bytes
});
let mut module = Module::new();
let _ = module.section(&types);
let _ = module.section(&imports_section);
let _ = module.section(&functions);
let _ = module.section(&memories);
let _ = module.section(&globals);
let _ = module.section(&exports);
let _ = module.section(&code);
let _ = module.section(&data);
module
}
fn workspace_observe_task_fixture_signatures(
resolve: &Resolve,
functions: WorkspaceObserveTaskFixtureFunctions<'_>,
) -> WorkspaceObserveTaskFixtureSignatures {
let start = resolve.wasm_signature(AbiVariant::GuestImport, functions.start);
assert_eq!(start.params.as_slice(), workspace_observe_start_signature());
assert_eq!(start.results.as_slice(), &[WasmType::I32]);
let poll = resolve.wasm_signature(AbiVariant::GuestImport, functions.poll);
assert_eq!(poll.params.first(), Some(&WasmType::I32));
let take = resolve.wasm_signature(AbiVariant::GuestImport, functions.take);
assert_eq!(take.params.first(), Some(&WasmType::I32));
WorkspaceObserveTaskFixtureSignatures { start, poll, take }
}
fn workspace_observe_task_fixture_types(
signatures: &WorkspaceObserveTaskFixtureSignatures,
) -> TypeSection {
let mut types = TypeSection::new();
for signature in [&signatures.start, &signatures.poll, &signatures.take] {
types.ty().function(
signature.params.iter().copied().map(val_type),
signature.results.iter().copied().map(val_type),
);
}
types.ty().function([ValType::I32], []);
types.ty().function([], []);
types.ty().function([ValType::I32, ValType::I32], []);
types.ty().function(
[ValType::I32, ValType::I32, ValType::I32, ValType::I32],
[ValType::I32],
);
types
}
fn workspace_observe_task_fixture_imports(
imports: WorkspaceObserveTaskFixtureImports<'_>,
drop_imports: &ResourceDropImports,
) -> ImportSection {
let mut section = ImportSection::new();
for (import, type_index) in [
(imports.start, 0),
(imports.poll, 1),
(imports.take, 2),
(imports.task_drop, 3),
] {
let _ = section.import(
import.module,
import.field,
EntityType::Function(type_index),
);
}
let _ = section.import(
&drop_imports.view_module,
&drop_imports.view_field,
EntityType::Function(3),
);
let _ = section.import(
&drop_imports.buffer_module,
&drop_imports.buffer_field,
EntityType::Function(3),
);
section
}
fn workspace_observe_task_fixture_function_section() -> FunctionSection {
let mut functions = FunctionSection::new();
let _ = functions.function(4);
let _ = functions.function(5);
let _ = functions.function(6);
functions
}
fn workspace_observe_task_fixture_globals() -> GlobalSection {
let mut globals = GlobalSection::new();
let mutable_i32 = GlobalType {
val_type: ValType::I32,
mutable: true,
shared: false,
};
let _ = globals.global(mutable_i32, &ConstExpr::i32_const(0));
let _ = globals.global(mutable_i32, &ConstExpr::i32_const(0));
globals
}
fn workspace_observe_task_fixture_exports(
lifecycle_exports: FixtureLifecycleExports<'_>,
) -> ExportSection {
let mut exports = ExportSection::new();
let _ = exports.export("memory", ExportKind::Memory, 0);
let _ = exports.export("cabi_realloc", ExportKind::Func, 8);
let _ = exports.export(lifecycle_exports.load, ExportKind::Func, 6);
let _ = exports.export(lifecycle_exports.update, ExportKind::Func, 7);
exports
}
struct ResourceDropImports {
view_module: String,
view_field: String,
buffer_module: String,
buffer_field: String,
}
fn resource_drop_imports(resolve: &Resolve, world: wit_parser::WorldId) -> ResourceDropImports {
let (view_module, view_field) = resource_drop_import(resolve, world, "view-handle");
let (buffer_module, buffer_field) = resource_drop_import(resolve, world, "buffer-handle");
ResourceDropImports {
view_module,
view_field,
buffer_module,
buffer_field,
}
}
fn resource_drop_import(
resolve: &Resolve,
world: wit_parser::WorldId,
resource_name: &str,
) -> (String, String) {
let world = &resolve.worlds[world];
let (key, interface) = world
.imports
.iter()
.find_map(|(key, item)| match item {
WorldItem::Interface { id, .. } => Some((key, &resolve.interfaces[*id])),
WorldItem::Function(_) | WorldItem::Type(_) => None,
})
.expect("fixture world should import host interface");
let resource = interface
.types
.get(resource_name)
.copied()
.unwrap_or_else(|| panic!("fixture host interface should define {resource_name}"));
resolve.wasm_import_name(
ManglingAndAbi::Standard32,
WasmImport::ResourceIntrinsic {
interface: Some(key),
resource,
intrinsic: ResourceIntrinsic::ImportedDrop,
},
)
}
fn host_import(
resolve: &Resolve,
world: wit_parser::WorldId,
name: &str,
) -> (String, String, WitFunction) {
let world = &resolve.worlds[world];
let (key, interface) = world
.imports
.iter()
.find_map(|(key, item)| match item {
WorldItem::Interface { id, .. } => Some((key, &resolve.interfaces[*id])),
WorldItem::Function(_) | WorldItem::Type(_) => None,
})
.expect("fixture world should import host interface");
let function = interface
.functions
.get(name)
.unwrap_or_else(|| panic!("fixture host interface should import {name}"));
let (module, field) = resolve.wasm_import_name(
ManglingAndAbi::Standard32,
WasmImport::Func {
interface: Some(key),
func: function,
},
);
(module, field, function.clone())
}
fn lifecycle_export_name(resolve: &Resolve, world: wit_parser::WorldId, name: &str) -> String {
let function = match resolve
.worlds
.get(world)
.expect("fixture world should exist")
.exports
.get(&WorldKey::Name(name.to_owned()))
.expect("fixture lifecycle export should exist")
{
WorldItem::Function(function) => function,
WorldItem::Interface { .. } | WorldItem::Type(_) => {
panic!("fixture lifecycle export should be a function")
}
};
resolve.wasm_export_name(
ManglingAndAbi::Standard32,
WasmExport::Func {
interface: None,
func: function,
kind: WasmExportKind::Normal,
},
)
}
fn push_buffer_insert_call(
function: &mut Function,
edit: BufferInsertFixture<'_>,
text_pointer: i32,
import_index: u32,
) {
if edit.handle_rep == UPDATE_BUFFER_REP {
let _ = function.instruction(&Instruction::LocalGet(1));
} else {
let _ = function.instruction(&Instruction::I32Const(
i32::try_from(edit.handle_rep).expect("fixture resource rep should fit i32"),
));
}
for argument in buffer_insert_arguments_with_text_pointer(edit, text_pointer) {
push_const(function, argument);
}
let _ = function.instruction(&Instruction::Call(import_index));
}
fn push_workspace_artifact_write_call(
function: &mut Function,
write: WorkspaceWriteFixture<'_>,
path_pointer: i32,
bytes_pointer: i32,
import_index: u32,
) {
for argument in [
ConstArgument::I32(path_pointer),
ConstArgument::I32(i32::try_from(write.path.len()).expect("fixture path should fit")),
ConstArgument::I32(bytes_pointer),
ConstArgument::I32(i32::try_from(write.bytes.len()).expect("fixture bytes should fit")),
] {
push_const(function, argument);
}
let _ = function.instruction(&Instruction::Call(import_index));
}
fn push_workspace_observe_start_call(function: &mut Function, path: &[u8], import_index: u32) {
for argument in [
ConstArgument::I32(0),
ConstArgument::I32(i32::try_from(path.len()).expect("fixture path should fit")),
] {
push_const(function, argument);
}
let _ = function.instruction(&Instruction::Call(import_index));
}
fn push_workspace_observe_poll_call(
function: &mut Function,
signature: &wit_parser::abi::WasmSignature,
import_index: u32,
) {
let _ = function.instruction(&Instruction::GlobalGet(0));
push_guest_import_extra_params(function, signature.params.iter().skip(1), 512);
let _ = function.instruction(&Instruction::Call(import_index));
drop_guest_import_results(function, &signature.results);
}
fn push_workspace_observe_take_call(
function: &mut Function,
signature: &wit_parser::abi::WasmSignature,
import_index: u32,
) {
let _ = function.instruction(&Instruction::GlobalGet(0));
push_guest_import_extra_params(function, signature.params.iter().skip(1), 768);
let _ = function.instruction(&Instruction::Call(import_index));
drop_guest_import_results(function, &signature.results);
}
fn push_guest_import_extra_params<'a>(
function: &mut Function,
params: impl Iterator<Item = &'a WasmType>,
result_area_pointer: i32,
) {
for (index, param) in params.enumerate() {
if index == 0 && val_type(*param) == ValType::I32 {
let _ = function.instruction(&Instruction::I32Const(result_area_pointer));
} else {
push_zero(function, *param);
}
}
}
fn drop_guest_import_results(function: &mut Function, results: &[WasmType]) {
for _result in results {
let _ = function.instruction(&Instruction::Drop);
}
}
fn workspace_write_fixture_bytes(calls: &[WorkspaceWriteFixture<'_>]) -> Vec<u8> {
let mut bytes = Vec::new();
for call in calls {
bytes.extend_from_slice(call.path);
bytes.extend_from_slice(call.bytes);
}
bytes
}
fn buffer_insert_arguments_with_text_pointer(
edit: BufferInsertFixture<'_>,
text_pointer: i32,
) -> [ConstArgument; 5] {
[
ConstArgument::I32(1),
ConstArgument::I64(edit.byte_index),
ConstArgument::I64(0),
ConstArgument::I32(i32::try_from(edit.text.len()).expect("fixture text should fit i32")),
ConstArgument::I32(text_pointer),
]
}
const fn buffer_insert_signature() -> &'static [WasmType; 6] {
&[
WasmType::I32,
WasmType::I32,
WasmType::PointerOrI64,
WasmType::PointerOrI64,
WasmType::Pointer,
WasmType::Length,
]
}
const fn workspace_artifact_write_signature() -> &'static [WasmType; 4] {
&[
WasmType::Pointer,
WasmType::Length,
WasmType::Pointer,
WasmType::Length,
]
}
const fn workspace_observe_start_signature() -> &'static [WasmType; 2] {
&[WasmType::Pointer, WasmType::Length]
}
#[derive(Clone, Copy)]
enum ConstArgument {
I32(i32),
I64(u64),
}
fn push_const(function: &mut Function, argument: ConstArgument) {
match argument {
ConstArgument::I32(value) => {
let _ = function.instruction(&Instruction::I32Const(value));
}
ConstArgument::I64(value) => {
let _ = function.instruction(&Instruction::I64Const(
i64::try_from(value).expect("fixture argument should fit i64"),
));
}
}
}
fn push_zero(function: &mut Function, value: WasmType) {
match val_type(value) {
ValType::I32 => {
let _ = function.instruction(&Instruction::I32Const(0));
}
ValType::I64 => {
let _ = function.instruction(&Instruction::I64Const(0));
}
ValType::F32 => {
let _ = function.instruction(&Instruction::F32Const(0.0.into()));
}
ValType::F64 => {
let _ = function.instruction(&Instruction::F64Const(0.0.into()));
}
ValType::V128 | ValType::Ref(_) => panic!("fixture does not support this ABI type"),
}
}
const fn val_type(value: WasmType) -> ValType {
match value {
WasmType::I32 | WasmType::Pointer | WasmType::Length => ValType::I32,
WasmType::I64 | WasmType::PointerOrI64 => ValType::I64,
WasmType::F32 => ValType::F32,
WasmType::F64 => ValType::F64,
}
}
const fn fixture_memory_type() -> MemoryType {
MemoryType {
minimum: 1,
maximum: Some(1),
memory64: false,
shared: false,
page_size_log2: None,
}
}