use wasmtime::component::{Component, HasSelf, Linker};
use wasmtime::{Config, Engine, Store};
use super::{Stub, support};
use harmos_guest::Message;
use harmos_guest::operations::{WireAnswer, WireCommand};
use harmos_operations::lifecycle::Initialize;
wasmtime::component::bindgen!({
path: "wit",
world: "plugin",
exports: { default: async },
with: {
"harmos:guest/operation-context": crate::harmos::guest::operation_context,
"harmos:guest/capabilities": crate::harmos::guest::capabilities,
},
});
#[harmos_guest::message(harmos_path = ::harmos_guest)]
#[derive(Clone, Debug, PartialEq)]
struct Point {
x: i32,
y: i32,
}
#[harmos_guest::message(harmos_path = ::harmos_guest)]
#[derive(Clone, Debug, PartialEq)]
struct Request {
from: Point,
to: Point,
}
#[harmos_guest::message(harmos_path = ::harmos_guest)]
#[derive(Clone, Debug, PartialEq)]
struct Response {
points: Vec<Point>,
}
struct Session {
store: Store<Stub>,
bindings: Plugin,
}
impl Session {
async fn phase(&mut self, command: WireCommand) -> WireAnswer {
let payload = command.encode().unwrap();
let bytes = self
.store
.run_concurrent(async |accessor| self.bindings.call_operate(accessor, "sandbox_guest".to_owned(), payload).await)
.await
.unwrap()
.unwrap()
.unwrap();
WireAnswer::decode(&bytes).unwrap()
}
async fn call<D: harmos_domain::operations::OperationDeclaration>(&mut self, invocation: u64, payload: Vec<u8>) -> Vec<u8> {
assert!(matches!(
self.phase(WireCommand::Prepare {
invocation,
operation: harmos_domain::QualifiedId::qualify("sandbox_guest", D::ID).unwrap().as_str().to_owned(),
fingerprint: (harmos_domain::operations::Entry::of::<D>().fingerprint)().unwrap().0,
generation: 1,
payload
})
.await,
WireAnswer::Ready { .. }
));
for _ in 0..2 {
assert!(matches!(
self.phase(WireCommand::Initialize { invocation, context: [1; 32] }).await,
WireAnswer::Ready { payload } if payload.is_empty()
));
}
let answer = self
.phase(WireCommand::Attempt {
invocation,
context: [0; 32],
values: Vec::new(),
})
.await;
let WireAnswer::Ready { payload } = answer else { panic!("{answer:?}") };
assert!(matches!(self.phase(WireCommand::Finalize { invocation }).await, WireAnswer::Ready { .. }));
payload
}
}
#[tokio::test]
async fn sdk_lifecycle_retained() {
let _report = crate::report::system();
let bytes = support::component();
let mut config = Config::new();
config.wasm_component_model(true);
config.wasm_component_model_async(true);
let engine = Engine::new(&config).unwrap();
let component = Component::new(&engine, bytes).unwrap();
let mut linker = Linker::new(&engine);
Plugin::add_to_linker::<_, HasSelf<_>>(&mut linker, |stub| stub).unwrap();
let exports = component.component_type();
for name in ["initialize", "shutdown"] {
assert!(exports.get_export(&engine, name).is_none(), "the unified component exports no independent {name} function");
}
let mut store = Store::new(&engine, Stub::default());
let bindings = Plugin::instantiate_async(&mut store, &component, &linker).await.unwrap();
let mut session = Session { store, bindings };
let route = || {
Request {
from: Point { x: 0, y: 0 },
to: Point { x: 3, y: 4 },
}
.encode()
};
assert!(
matches!(
session
.phase(WireCommand::Prepare {
invocation: 1,
operation: "sandbox_guest.schematic.route".to_owned(),
fingerprint: (harmos_domain::operations::Entry::of::<schematic_operations::schematic::ProposeRoute>().fingerprint)()
.unwrap()
.0,
generation: 1,
payload: route()
})
.await,
WireAnswer::Failed { .. }
),
"ordinary handlers are absent before SDK initialization"
);
assert!(
matches!(
session
.phase(WireCommand::Prepare {
invocation: 7,
operation: "sandbox_guest.harmos.initialize".to_owned(),
fingerprint: (harmos_domain::operations::Entry::of::<harmos_operations::lifecycle::Initialize>().fingerprint)()
.unwrap()
.0,
generation: 1,
payload: Initialize {
config_json: br#""unexpected""#.to_vec(),
generation_context: Vec::new(),
assignments: Vec::new()
}
.encode(),
})
.await,
WireAnswer::Ready { .. }
),
"inert preparation only retains the original request"
);
assert!(matches!(session.phase(WireCommand::Initialize { invocation: 7, context: [0; 32] }).await, WireAnswer::Ready { .. }));
assert!(
matches!(
session
.phase(WireCommand::Attempt {
invocation: 7,
context: [0; 32],
values: Vec::new()
})
.await,
WireAnswer::Failed { .. }
),
"the first run validates configuration before plugin initialization"
);
assert!(session.store.data().calls.is_empty(), "a refused initialization asked the host for nothing");
assert!(
matches!(session.phase(WireCommand::Finalize { invocation: 7 }).await, WireAnswer::Ready { .. }),
"original finalization releases the failed invocation's capacity"
);
let registrations = session.bindings.call_register(&mut session.store).await.unwrap().unwrap();
assert!(registrations.is_empty(), "the schematic guest registers no catalog");
assert!(
matches!(
session
.phase(WireCommand::Prepare {
invocation: 8,
operation: "sandbox_guest.schematic.route".to_owned(),
fingerprint: (harmos_domain::operations::Entry::of::<schematic_operations::schematic::ProposeRoute>().fingerprint)()
.unwrap()
.0,
generation: 1,
payload: route()
})
.await,
WireAnswer::Failed { .. }
),
"metadata collection does not initialize resources or ordinary handlers"
);
let request = Initialize {
config_json: b"null".to_vec(),
generation_context: Vec::new(),
assignments: Vec::new(),
};
assert!(session.call::<harmos_operations::lifecycle::Initialize>(2, request.encode()).await.is_empty());
let routed = session.call::<schematic_operations::schematic::ProposeRoute>(3, route()).await;
assert_eq!(Response::decode(&routed).unwrap().points, [Point { x: 0, y: 0 }, Point { x: 3, y: 0 }, Point { x: 3, y: 4 },]);
assert!(session.call::<harmos_operations::lifecycle::Shutdown>(4, vec![]).await.is_empty());
assert!(
matches!(
session
.phase(WireCommand::Prepare {
invocation: 5,
operation: "sandbox_guest.schematic.route".to_owned(),
fingerprint: (harmos_domain::operations::Entry::of::<schematic_operations::schematic::ProposeRoute>().fingerprint)()
.unwrap()
.0,
generation: 1,
payload: route()
})
.await,
WireAnswer::Failed { .. }
),
"shutdown revokes ordinary admission"
);
assert!(
session.call::<harmos_operations::lifecycle::Shutdown>(6, vec![]).await.is_empty(),
"SDK shutdown is idempotent on the same closed owner"
);
}