blob: a14ccc532eceb2e9e5019c1dccec397d362e5b57 [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
use super::Application;
use super::{support, timer};
use maka_capabilities::call::ReadResource;
use maka_plugin_core::{
composition::{Input, PresentationTarget},
contract::ResourceType,
runtime::Mount,
};
use maka_ui_runtime::actions::*;
use maka_ui_runtime::{
ComponentCall, ComponentOutput, Frame, NativeCapability, NativeComponent, NativeExport,
NativeRegistry, NativeView,
};
use serde_json::{Value, json};
use std::{cell::Cell, collections::BTreeMap, rc::Rc, sync::Arc};
struct Leaf {
captures: Rc<Cell<usize>>,
large: bool,
}
impl NativeComponent<()> for Leaf {
fn call(
&self,
call: ComponentCall<'_>,
_values: &maka_ui_runtime::abi::ValueBindings,
) -> maka_ui_runtime::Result<ComponentOutput<NativeView<()>>> {
let revision = match call {
ComponentCall::Mount { .. } => 0,
ComponentCall::Update { state, .. } | ComponentCall::Event { state, .. } => {
state["revision"].as_u64().unwrap() + 1
}
};
Ok(Frame {revision,state:json!({"revision":revision,"blob":if self.large {"x".repeat(1_048_430)} else {String::new()}}),surface:None,view:NativeView::new(Rc::new(()),BTreeMap::new(), vec![])? }.output(vec![]))
}
fn capture(&self, _: CaptureCall<'_>) -> maka_ui_runtime::Result<CapturePlan> {
self.captures.set(self.captures.get() + 1);
Ok(CapturePlan::Native)
}
}
struct Text(String);
impl ReadResource for Text {
fn resource_type(&self) -> ResourceType {
maka_ui_runtime::text::text_resource_type()
}
fn bytes(&self) -> usize {
self.0.len()
}
fn metadata(&self) -> Value {
json!({"revision":3,"bytes":self.0.len()})
}
fn read(&self, r: std::ops::Range<usize>) -> Result<Vec<u8>, String> {
self.0
.get(r)
.map(|s| s.as_bytes().to_vec())
.ok_or("range".into())
}
}
fn fixture(
large: bool,
) -> (
Input,
Vec<maka_packages::Package>,
NativeRegistry<()>,
Rc<Cell<usize>>,
) {
fixture_with_continuation(large, false)
}
fn fixture_with_continuation(
large: bool,
guarded: bool,
) -> (
Input,
Vec<maka_packages::Package>,
NativeRegistry<()>,
Rc<Cell<usize>>,
) {
let (input, packages, contract) = if guarded {
support::continuation_fixture()
} else {
let (input, packages) = support::resource_fixture();
(
input,
packages,
maka_ui_runtime::textarea::textarea_contract(),
)
};
let calls = Rc::new(Cell::new(0));
let observed = calls.clone();
let mut native = crate::support::scene::registry(PresentationTarget::Tui);
native
.register("maka.ui.textarea", move |_, _values| {
Ok(BTreeMap::from([(
"Textarea".into(),
NativeExport {
contract: contract.clone(),
value: NativeCapability::Component(Rc::new(Leaf {
captures: observed.clone(),
large,
})),
},
)]))
})
.unwrap();
(input, packages, native, calls)
}
fn application(
large: bool,
timer: &tokio::runtime::Runtime,
) -> (Application, Mount, Rc<Cell<usize>>) {
let (input, packages, native, calls) = fixture(large);
let mut app = Application::new(
&input,
packages,
timer.handle().clone(),
maka_ui_js::execution(Default::default()),
native,
Default::default(),
)
.unwrap();
let root = app.mount_launch(json!({})).unwrap();
(app, root, calls)
}
fn capture(app: &mut Application, root: &Mount, text: &str) -> Arc<dyn ReadResource> {
let snapshot: Arc<dyn ReadResource> = Arc::new(Text(text.into()));
let result = snapshot.clone();
app.capture_actions(root, |target| {
assert_eq!(target.method(), "snapshot");
assert_eq!(
target.output(),
&maka_plugin_core::contract::CaptureOutput::Resource {
resource: maka_ui_runtime::text::text_resource_type()
}
);
Ok(NativeCapture::Resource(snapshot.clone()))
})
.unwrap();
result
}
fn close_all(app: &mut Application, roots: &[Mount]) {
for r in roots {
app.retire(r).unwrap();
}
for m in app.retiring_mounts() {
app.close(&m, Ok(())).unwrap();
}
app.shutdown().unwrap();
}
#[test]
fn mounted_wrapper_capture_and_typed_resource_action_keep_exact_root_and_internal_result_authority()
{
let timer = timer();
let (mut app, a, calls) = application(false, &timer);
let b = app.mount_launch(json!({})).unwrap();
let inner = app
.child_mount(app.child_mount(&a, "editor").unwrap(), "draft")
.unwrap()
.clone();
app.dispatch(&a, 0, "read", json!({})).unwrap();
assert_eq!(calls.get(), 1);
app.capture_actions(&a, |target| {
assert_eq!(target.mount(), &inner);
Ok(NativeCapture::Resource(Arc::new(Text("Start".into()))))
})
.unwrap();
assert!(app.start_task(&b).unwrap().is_none());
let accepted = app.start_task(&a).unwrap().unwrap();
let done = timer.block_on(accepted.future);
let maka_ui_runtime::actions::TaskCompletion::Query(ref completion) = done else {
panic!("expected query completion")
};
assert_eq!(completion.result.as_ref().unwrap()["text"], "Start");
app.finish_task(accepted.id, done).unwrap();
assert_eq!(
app.deliver_action_results(&b, |_, _, _| Ok(false)).unwrap(),
0
);
assert_eq!(app.frame(&b).unwrap().revision, 0);
assert_eq!(
app.deliver_action_results(&a, |_, _, _| Ok(false)).unwrap(),
1
);
assert_eq!(
app.frame(&a).unwrap().state["outcome"]["value"]["text"],
"Start"
);
assert!(
app.dispatch(
&a,
2,
"read_done",
json!({"status":"succeeded","value":{"text":"forged"}})
)
.is_err()
);
close_all(&mut app, &[a, b]);
}
#[test]
fn untrusted_metadata_or_native_plan_cannot_manufacture_resource_authority() {
let timer = timer();
let (mut app, root, calls) = application(false, &timer);
assert!(
app.dispatch(
&root,
0,
"read",
json!({"capture":{"kind":"current","owner":"forged-id"}})
)
.is_err()
);
assert_eq!(calls.get(), 0);
assert_eq!(app.pending_action_count(), 0);
assert_eq!(app.frame(&root).unwrap().revision, 0);
close_all(&mut app, &[root]);
let (input, mut packages, native, _) = fixture(false);
let i = packages
.iter()
.position(|p| p.manifest().id == "dynamic.composer")
.unwrap();
let old = packages.remove(i);
let source = std::str::from_utf8(old.file("index.mjs").unwrap())
.unwrap()
.replace(
"return {kind:'forward',source:{child:'draft',method,input}};",
"return {kind:'native'};",
);
let composer = support::package(
"dynamic.composer",
old.manifest()
.application
.as_ref()
.unwrap()
.declaration()
.clone(),
Some(source),
);
let mut input = input;
input.instances.get_mut("composer").unwrap().package = composer.application_ref().into();
packages.push(composer);
let mut app = Application::new(
&input,
packages,
timer.handle().clone(),
maka_ui_js::execution(Default::default()),
native,
Default::default(),
)
.unwrap();
let root = app.mount_launch(json!({})).unwrap();
assert!(app.dispatch(&root, 0, "read", Value::Null).is_err());
assert_eq!(app.pending_action_count(), 0);
close_all(&mut app, &[root]);
}
#[test]
fn capture_failure_is_an_owned_not_accepted_outcome_after_scene_commit() {
let timer = timer();
let (mut app, root, _) = application(false, &timer);
app.dispatch(&root, 0, "read", json!({})).unwrap();
assert_eq!(app.frame(&root).unwrap().revision, 1);
app.capture_actions(&root, |_| {
Err(maka_ui_runtime::Error::Invalid("revision conflict".into()))
})
.unwrap();
assert!(app.start_task(&root).unwrap().is_none());
app.deliver_action_results(&root, |_, _, _| Ok(false))
.unwrap();
assert_eq!(
app.frame(&root).unwrap().state["outcome"]["status"],
"not_accepted"
);
assert!(!app.is_retiring(&root));
close_all(&mut app, &[root]);
}
#[test]
fn source_removal_before_admission_cancels_but_accepted_snapshot_outlives_removed_subtree() {
let timer = timer();
for accepted_first in [false, true] {
let (mut app, root, _) = application(false, &timer);
app.dispatch(&root, 0, "read", Value::Null).unwrap();
let snapshot = capture(&mut app, &root, "Start");
let weak = Arc::downgrade(&snapshot);
drop(snapshot);
let accepted = if accepted_first {
app.start_task(&root).unwrap()
} else {
None
};
app.dispatch(&root, 1, "remove", Value::Null).unwrap();
for m in app.retiring_mounts() {
app.close(&m, Ok(())).unwrap();
}
if let Some(work) = accepted {
let done = timer.block_on(work.future);
let maka_ui_runtime::actions::TaskCompletion::Query(ref completion) = done else {
panic!("expected query completion")
};
assert_eq!(completion.result.as_ref().unwrap()["text"], "Start");
app.finish_task(work.id, done).unwrap();
} else {
assert!(app.start_task(&root).unwrap().is_none());
}
app.deliver_action_results(&root, |_, _, _| Ok(false))
.unwrap();
assert_eq!(
app.frame(&root).unwrap().state["outcome"]["status"],
if accepted_first {
"succeeded"
} else {
"not_accepted"
}
);
assert!(weak.upgrade().is_none());
close_all(&mut app, &[root]);
}
}
#[test]
fn specific_consumer_mount_is_checked_even_when_same_provider_has_another_live_root() {
let timer = timer();
let (mut app, a, _) = application(false, &timer);
let b = app.mount_launch(json!({})).unwrap();
app.dispatch(&a, 0, "read", Value::Null).unwrap();
capture(&mut app, &a, "Start");
app.retire(&a).unwrap();
assert!(app.start_task(&a).unwrap().is_none());
assert_eq!(app.accepted_action_count(), 0);
app.dispatch(&b, 0, "read", Value::Null).unwrap();
capture(&mut app, &b, "Start");
let work = app.start_task(&b).unwrap().unwrap();
let done = timer.block_on(work.future);
app.finish_task(work.id, done).unwrap();
app.deliver_action_results(&b, |_, _, _| Ok(false)).unwrap();
close_all(&mut app, &[a, b]);
}
#[test]
fn failed_or_lost_result_handlers_do_not_hold_accepted_work_or_healthy_pending_mounts() {
let timer = timer();
for feedback in [
"if(event.kind==='action')throw new Error('result handler failed');",
"if(event.kind==='action'){const out=frame(next);out.decision={preventDefault:true};return out;}",
] {
let (input, mut packages, native, _) = fixture(false);
let i = packages
.iter()
.position(|p| p.manifest().id == "dynamic.app")
.unwrap();
let old = packages.remove(i);
let source = std::str::from_utf8(old.file("index.mjs").unwrap())
.unwrap()
.replace(
"if(event.kind==='action')next.outcome=event.event;",
feedback,
);
let root_pkg = support::package(
"dynamic.app",
old.manifest()
.application
.as_ref()
.unwrap()
.declaration()
.clone(),
Some(source),
);
let mut input = input;
input.instances.get_mut("app").unwrap().package = root_pkg.application_ref().into();
packages.push(root_pkg);
let mut app = Application::new(
&input,
packages,
timer.handle().clone(),
maka_ui_js::execution(Default::default()),
native,
Default::default(),
)
.unwrap();
let root = app.mount_launch(json!({})).unwrap();
app.dispatch(&root, 0, "read", Value::Null).unwrap();
capture(&mut app, &root, "Start");
let work = app.start_task(&root).unwrap().unwrap();
let done = timer.block_on(work.future);
app.finish_task(work.id, done).unwrap();
assert_eq!(app.accepted_action_count(), 0);
assert!(
app.deliver_action_results(&root, |_, _, _| Ok(false))
.is_err()
);
assert!(app.is_retiring(&root));
close_all(&mut app, &[root]);
}
}
struct CompletionService {
value: Value,
cleanup_error: bool,
}
impl NativeService for CompletionService {
fn call(&self, _: ServiceRequest) -> ServiceFuture {
let value = self.value.clone();
let failed = self.cleanup_error;
Box::pin(async move {
ServiceCompletion {
result: Ok(value),
cleanup: if failed {
Err("cleanup unknown".into())
} else {
Ok(())
},
}
})
}
}
fn service_fixture(
timer: &tokio::runtime::Runtime,
value: Value,
cleanup_error: bool,
padding: usize,
) -> (Application, Mount) {
let (mut input, mut packages, mut native, _) = fixture(false);
let mut contract = maka_ui_runtime::text::reader_contract();
let maka_plugin_core::contract::Interface::Service { methods } = &mut contract.interface else {
unreachable!()
};
methods.get_mut("read").unwrap().output = json!({"type":"string"}).into();
for name in ["dynamic.app", "native.reader"] {
let i = packages
.iter()
.position(|p| p.manifest().id == name)
.unwrap();
let old = packages.remove(i);
let mut declaration = old
.manifest()
.application
.as_ref()
.unwrap()
.declaration()
.clone();
let entry = declaration
.entries
.get_mut(&maka_plugin_core::package::EntryTarget::Tui)
.unwrap();
let source = if name == "dynamic.app" {
entry.imports.get_mut("reader").unwrap().contract = contract.clone();
Some(
std::str::from_utf8(old.file("index.mjs").unwrap())
.unwrap()
.replace(
"revision:0,show:true",
&format!("revision:0,show:true,padding:'x'.repeat({padding})"),
),
)
} else {
entry.exports.get_mut("Reader").unwrap().contract = contract.clone();
entry.implementation = maka_plugin_core::package::Implementation::Native {
implementation: "test.service".into(),
};
None
};
let package = support::package(name, declaration, source);
input
.instances
.get_mut(if name == "dynamic.app" {
"app"
} else {
"reader"
})
.unwrap()
.package = package.application_ref().into();
packages.push(package);
}
native
.register("test.service", move |_, _values| {
Ok(BTreeMap::from([(
"Reader".into(),
NativeExport {
contract: contract.clone(),
value: NativeCapability::Service(Arc::new(CompletionService {
value: value.clone(),
cleanup_error,
})),
},
)]))
})
.unwrap();
let mut app = Application::new(
&input,
packages,
timer.handle().clone(),
maka_ui_js::execution(Default::default()),
native,
Default::default(),
)
.unwrap();
let root = app.mount_launch(json!({})).unwrap();
(app, root)
}
#[test]
fn cleanup_failure_preserves_work_and_input_until_explicit_recovery_without_replaying_outcome() {
let timer = timer();
let (mut app, root) = service_fixture(&timer, json!("ok"), true, 0);
app.dispatch(&root, 0, "read", Value::Null).unwrap();
let snapshot = capture(&mut app, &root, "Start");
let weak = Arc::downgrade(&snapshot);
drop(snapshot);
let work = app.start_task(&root).unwrap().unwrap();
let done = timer.block_on(work.future);
assert!(app.finish_task(work.id, done).is_err());
assert_eq!(app.accepted_action_count(), 1);
assert!(weak.upgrade().is_some());
assert_eq!(
app.deliver_action_results(&root, |_, _, _| Ok(false))
.unwrap(),
0
);
assert!(
app.is_retiring(&root),
"cleanup failure retires service consumers"
);
assert!(
app.complete_action_cleanup(work.id, Err("still unknown".into()))
.is_err()
);
assert!(weak.upgrade().is_some());
app.retire(&root).unwrap();
for m in app.retiring_mounts() {
app.close(&m, Ok(())).unwrap();
}
assert!(!app.can_shutdown());
assert!(app.shutdown().is_err());
app.complete_action_cleanup(work.id, Ok(())).unwrap();
assert!(weak.upgrade().is_none());
assert!(app.can_shutdown());
assert_eq!(app.pending_action_count(), 0);
app.shutdown().unwrap();
}
struct ExactFeedback;
impl NativeComponent<()> for ExactFeedback {
fn call(
&self,
call: ComponentCall<'_>,
_values: &maka_ui_runtime::abi::ValueBindings,
) -> maka_ui_runtime::Result<ComponentOutput<NativeView<()>>> {
let (state, initial) = match call {
ComponentCall::Mount { props } => {
(json!({"revision":0,"marker":props["marker"]}), true)
}
ComponentCall::Event {
state,
input,
origin,
..
} => {
assert!(matches!(origin, maka_ui_runtime::EventOrigin::Action));
let mut next = state.clone();
next["revision"] = json!(state["revision"].as_u64().unwrap() + 1);
next["outcome"] = input.clone();
(next, false)
}
ComponentCall::Update { state, .. } => {
let mut next = state.clone();
next["revision"] = json!(state["revision"].as_u64().unwrap() + 1);
(next, false)
}
};
// Native callbacks can use the portable tree; presentation shape does
// not determine the actual consumer codec.
let mut output = Frame {
revision: state["revision"].as_u64().unwrap(),
state,
surface: Some(maka_ui_runtime::Surface::Terminal {
binding: "surface".into(),
}),
view: crate::support::scene::view(crate::support::scene::Node::Text {
key: "result".into(),
text: "Result".into(),
color: 0,
})?,
}
.output(vec![]);
if initial {
output.actions.push(Action {
recovery: Value::Null,
continuations: Vec::new(),
key: "read".into(),
binding: "reader".into(),
member: 0,
method: "read".into(),
input: Value::Null,
resources: BTreeMap::new(),
});
}
Ok(output)
}
}
#[test]
fn actual_native_query_preserves_exact_integer_while_javascript_fails_without_retiring() {
use maka_plugin_core::{
composition::Target,
contract::{
Availability, Cardinality, Contract, Export, Import, Interface, Method, MethodMode,
Transport,
},
package::{EntryTarget, Implementation},
};
const WIDE: u64 = 9_007_199_254_740_993;
let timer = timer();
for is_native in [true, false] {
let (mut input, mut packages) = support::fixture("");
let old = packages.pop().unwrap();
let mut declaration = old
.manifest()
.application
.as_ref()
.unwrap()
.declaration()
.clone();
let entry = declaration.entries.get_mut(&EntryTarget::Tui).unwrap();
let component = entry.exports["App"].contract.clone();
let contract = Contract {
name: "test.ExactQuery".into(),
version: 1,
transport: Transport::Local,
interface: Interface::Service {
methods: BTreeMap::from([(
"read".into(),
Method {
progress: None,
mode: MethodMode::Query,
input: json!({"type":"null"}).into(),
output: json!({"type":"integer"}).into(),
resources: BTreeMap::new(),
},
)]),
},
};
entry.imports.insert(
"reader".into(),
Import {
contract: contract.clone(),
cardinality: Cardinality::One,
},
);
if is_native {
entry.scene = None;
entry.implementation = Implementation::Native {
implementation: "test.exact-feedback".into(),
};
}
let source = format!(
r#"
const contract = {};
const frame = state => ({{frame:{{revision:state.revision,state,surface:{{kind:'terminal',binding:'surface'}},view:{{kind:'text',key:'result',text:'Result',color:0}}}},events:[],actions:[]}});
export function activate() {{ return {{App:{{contract,kind:'component',component:['app']}}}}; }}
export const app = {{
mount() {{ const out=frame({{revision:0}});out.actions=[{{key:'read',binding:'reader',member:0,method:'read',input:null,resources:{{}}}}];return out; }},
update(state) {{return frame({{...state,revision:state.revision+1}});}},
event(state,input) {{return frame({{...state,revision:state.revision+1,outcome:input.event}});}}
}};
"#,
serde_json::to_string(&component).unwrap()
);
let root = support::package(
"dynamic.app",
declaration.clone(),
(!is_native).then_some(source),
);
let node = input.instances.get_mut("app").unwrap();
node.package = root.application_ref().into();
node.bindings.insert(
"reader".into(),
vec![Target::Instance {
path: vec!["reader".into()],
export: "Reader".into(),
}],
);
let entry = declaration.entries.get_mut(&EntryTarget::Tui).unwrap();
entry.scene = None;
entry.implementation = Implementation::Native {
implementation: "test.exact-query".into(),
};
entry.imports.clear();
entry.exports = BTreeMap::from([(
"Reader".into(),
Export {
contract: contract.clone(),
availability: Availability::Required,
},
)]);
let reader = support::package("native.reader", declaration, None);
input
.instances
.insert("reader".into(), support::node(&reader, 3));
packages.extend([root, reader]);
let mut native = crate::support::scene::registry(PresentationTarget::Tui);
native
.register("test.exact-feedback", move |_, _values| {
Ok(BTreeMap::from([(
"App".into(),
NativeExport {
contract: component.clone(),
value: NativeCapability::Component(Rc::new(ExactFeedback)),
},
)]))
})
.unwrap();
native
.register("test.exact-query", move |_, _values| {
Ok(BTreeMap::from([(
"Reader".into(),
NativeExport {
contract: contract.clone(),
value: NativeCapability::Service(Arc::new(CompletionService {
value: json!(WIDE),
cleanup_error: false,
})),
},
)]))
})
.unwrap();
let mut app = Application::new(
&input,
packages,
timer.handle().clone(),
maka_ui_js::execution(Default::default()),
native,
Default::default(),
)
.unwrap();
let mount = app
.mount_launch(if is_native {
json!({"marker":WIDE})
} else {
json!({})
})
.unwrap();
app.capture_actions(&mount, |_| unreachable!("query has no resources"))
.unwrap();
let task = app.start_task(&mount).unwrap().unwrap();
let completion = timer.block_on(task.future);
assert!(app.finish_task(task.id, completion).unwrap().is_none());
assert_eq!(
app.deliver_action_results(&mount, |_, _, _| Ok(false))
.unwrap(),
1
);
assert!(!app.is_retiring(&mount));
assert_eq!(app.accepted_action_count(), 0);
let state = &app.frame(&mount).unwrap().state;
if is_native {
assert_eq!(state["outcome"]["status"], "succeeded");
assert_eq!(state["outcome"]["value"].as_u64(), Some(WIDE));
assert_eq!(state["marker"].as_u64(), Some(WIDE));
} else {
assert_eq!(state["outcome"]["status"], "failed");
assert!(
state["outcome"]["reason"]
.as_str()
.unwrap()
.contains("delivery failed")
);
}
close_all(&mut app, &[mount]);
}
}
type RemoteFixture = (
Input,
Vec<maka_packages::Package>,
NativeRegistry<()>,
RemoteServices,
maka_plugin_core::plan::RemoteCapability,
);
fn remote_fixture(
mode: maka_plugin_core::contract::MethodMode,
cardinality: maka_plugin_core::contract::Cardinality,
actual: bool,
) -> RemoteFixture {
use maka_plugin_core::{
contract::{Availability, Export, Interface, Transport},
package::EntryTarget,
plan::RemoteCapability,
};
let (mut input, mut packages, native, _) = fixture(false);
input.instances.remove("reader");
packages.retain(|p| p.manifest().id != "native.reader");
let old = packages.remove(
packages
.iter()
.position(|p| p.manifest().id == "dynamic.app")
.unwrap(),
);
let mut contract = maka_ui_runtime::text::reader_contract();
contract.transport = Transport::Remote;
let Interface::Service { methods } = &mut contract.interface else {
unreachable!()
};
methods.get_mut("read").unwrap().mode = mode;
let mut declaration = old
.manifest()
.application
.as_ref()
.unwrap()
.declaration()
.clone();
let import = declaration
.entries
.get_mut(&EntryTarget::Tui)
.unwrap()
.imports
.get_mut("reader")
.unwrap();
import.contract = contract.clone();
import.cardinality = cardinality;
let package = support::package(
"dynamic.app",
declaration,
Some(
std::str::from_utf8(old.file("index.mjs").unwrap())
.unwrap()
.to_owned(),
),
);
let target = RemoteCapability {
deployment: "host-a".into(),
instance: maka_plugin_core::composition::InstanceId(vec![uuid::Uuid::from_u128(11)]),
export: "Reader".into(),
};
let app = input.instances.get_mut("app").unwrap();
app.package = package.application_ref().into();
app.bindings.insert(
"reader".into(),
vec![maka_plugin_core::composition::Target::Remote {
deployment: target.deployment.clone(),
instance: target.instance.clone(),
export: target.export.clone(),
}],
);
packages.push(package);
let remotes = RemoteServices {
declarations: BTreeMap::from([(
target.clone(),
Export {
contract: contract.clone(),
availability: Availability::Required,
},
)]),
registrations: if actual {
vec![RemoteService {
target: target.clone(),
registration_epoch: uuid::Uuid::new_v4(),
contract,
implementation: Arc::new(CompletionService {
value: json!({"text":"Start","metadata":{}}),
cleanup_error: false,
}),
}]
} else {
vec![]
},
};
(input, packages, native, remotes, target)
}
#[test]
fn remote_metadata_proves_compatibility_but_only_actual_endpoint_proves_readiness() {
use maka_plugin_core::contract::{Cardinality, MethodMode};
let timer = timer();
let (input, packages, native, remotes, _) =
remote_fixture(MethodMode::Query, Cardinality::One, false);
assert!(matches!(
Application::new(
&input,
packages,
timer.handle().clone(),
maka_ui_js::execution(Default::default()),
native,
remotes
),
Err(maka_ui_runtime::Error::Core(
maka_plugin_core::Error::NotReady(_)
))
));
let (input, packages, native, remotes, _) =
remote_fixture(MethodMode::Query, Cardinality::Optional, false);
let mut app = Application::new(
&input,
packages,
timer.handle().clone(),
maka_ui_js::execution(Default::default()),
native,
remotes,
)
.unwrap();
let root = app.mount_launch(json!({})).unwrap();
close_all(&mut app, &[root]);
}
#[test]
fn remote_disconnect_retires_consumer_but_preserves_accepted_query_and_snapshot() {
use maka_plugin_core::contract::{Cardinality, MethodMode};
let timer = timer();
let (input, packages, native, remotes, target) =
remote_fixture(MethodMode::Query, Cardinality::One, true);
let mut app = Application::new(
&input,
packages,
timer.handle().clone(),
maka_ui_js::execution(Default::default()),
native,
remotes,
)
.unwrap();
let root = app.mount_launch(json!({})).unwrap();
app.dispatch(&root, 0, "read", Value::Null).unwrap();
let snapshot = capture(&mut app, &root, "Start");
let weak = Arc::downgrade(&snapshot);
drop(snapshot);
let work = app.start_task(&root).unwrap().unwrap();
let attachment = app.remote_activation(&target).unwrap();
app.retire_remote(&attachment).unwrap();
assert!(app.is_retiring(&root));
assert!(app.dispatch(&root, 1, "read", Value::Null).is_err());
for mount in app.retiring_mounts() {
app.close(&mount, Ok(())).unwrap();
}
assert!(app.shutdown().is_err());
assert!(weak.upgrade().is_some());
let done = timer.block_on(work.future);
let maka_ui_runtime::actions::TaskCompletion::Query(ref completion) = done else {
panic!("expected query completion")
};
assert_eq!(completion.result.as_ref().unwrap()["text"], "Start");
app.finish_task(work.id, done).unwrap();
assert!(weak.upgrade().is_none());
app.shutdown().unwrap();
}
#[test]
fn ui_admits_resource_mutations_and_enforces_persisted_callers_and_subscription_demand() {
use maka_plugin_core::contract::{Cardinality, MethodMode};
let timer = timer();
for mode in [MethodMode::Mutation, MethodMode::Subscription] {
let (input, packages, native, remotes, _) = remote_fixture(mode, Cardinality::One, true);
let result = Application::new(
&input,
packages,
timer.handle().clone(),
maka_ui_js::execution(Default::default()),
native,
remotes,
);
let mut app = result.unwrap();
let root = app.mount_launch(json!({})).unwrap();
// A resource-capable registration still requires a persisted caller
// for mutations and a subscription declaration for stream demand.
let reason = app
.dispatch(&root, 0, "read", Value::Null)
.unwrap_err()
.to_string();
let expected = if mode == MethodMode::Mutation {
"mutation requires a persisted UI source"
} else {
"method mode mismatch"
};
assert!(reason.contains(expected), "{reason}");
assert_eq!(app.frame(&root).unwrap().revision, 0);
assert!(!app.is_retiring(&root));
assert_eq!(app.pending_action_count(), 0);
close_all(&mut app, &[root]);
}
}
#[test]
fn delayed_disconnect_token_cannot_retire_same_target_in_a_replacement_ui_kernel() {
use maka_plugin_core::contract::{Cardinality, MethodMode};
let timer = timer();
let (input, packages, native, remotes, target) =
remote_fixture(MethodMode::Query, Cardinality::One, true);
let wire_epoch = remotes.registrations[0].registration_epoch;
let mut old = Application::new(
&input,
packages,
timer.handle().clone(),
maka_ui_js::execution(Default::default()),
native,
remotes,
)
.unwrap();
let old_root = old.mount_launch(json!({})).unwrap();
let old_attachment = old.remote_activation(&target).unwrap();
let (input, packages, native, mut remotes, _) =
remote_fixture(MethodMode::Query, Cardinality::One, true);
remotes.registrations[0].registration_epoch = wire_epoch;
let mut replacement = Application::new(
&input,
packages,
timer.handle().clone(),
maka_ui_js::execution(Default::default()),
native,
remotes,
)
.unwrap();
let root = replacement.mount_launch(json!({})).unwrap();
assert!(replacement.retire_remote(&old_attachment).is_err());
assert!(!replacement.is_retiring(&root));
let actual_attachment = replacement.remote_activation(&target).unwrap();
replacement.retire_remote(&actual_attachment).unwrap();
assert!(replacement.is_retiring(&root));
close_all(&mut replacement, &[root]);
close_all(&mut old, &[old_root]);
}
fn guarded_application(timer: &tokio::runtime::Runtime) -> (Application, Mount) {
guarded_application_with(timer, |source| source)
}
fn guarded_application_with(
timer: &tokio::runtime::Runtime,
change: impl FnOnce(String) -> String,
) -> (Application, Mount) {
let (mut input, mut packages, native, _) = fixture_with_continuation(false, true);
let index = packages
.iter()
.position(|p| p.manifest().id == "dynamic.app")
.unwrap();
let old = packages.remove(index);
let source = change(
std::str::from_utf8(old.file("index.mjs").unwrap())
.unwrap()
.to_owned(),
);
let package = support::package(
"dynamic.app",
old.manifest()
.application
.as_ref()
.unwrap()
.declaration()
.clone(),
Some(source),
);
input.instances.get_mut("app").unwrap().package = package.application_ref().into();
packages.push(package);
let mut app = Application::new(
&input,
packages,
timer.handle().clone(),
maka_ui_js::execution(Default::default()),
native,
Default::default(),
)
.unwrap();
let root = app.mount_launch(json!({})).unwrap();
(app, root)
}
fn capture_guarded(app: &mut Application, root: &Mount, bytes: usize) {
app.capture_actions(root, |target| match target.method() {
"snapshot" => Ok(NativeCapture::Resource(Arc::new(Text("Start".into())))),
"reading" => Ok(NativeCapture::Continuation(ContinuationWitness::new(
7u64, bytes,
))),
_ => unreachable!(),
})
.unwrap();
}
fn guarded_request(app: &mut Application, root: &Mount, key: &str, bytes: usize) -> ActionTask {
let revision = app.frame(root).unwrap().revision;
app.dispatch(root, revision, "read", json!({"key":key}))
.unwrap();
capture_guarded(app, root, bytes);
app.start_task(root).unwrap().unwrap()
}
#[test]
fn continuation_checks_precede_publication_and_preserve_business_completion() {
let timer = timer();
let (mut app, root) = guarded_application(&timer);
let original = app.child_mount(&root, "editor").unwrap().clone();
let task = guarded_request(&mut app, &root, "read_done", 8);
let done = timer.block_on(task.future);
let TaskCompletion::Query(ref completion) = done else {
panic!("query")
};
assert_eq!(completion.result.as_ref().unwrap()["text"], "Start");
app.finish_task(task.id, done).unwrap();
assert_eq!(app.accepted_action_count(), 0);
assert!(app.frame(&root).unwrap().state.get("outcome").is_none());
let checked = Cell::new(0);
assert_eq!(
app.deliver_action_results(&root, |target, _, witness| {
assert_eq!(target.method(), "reading");
assert_eq!(witness.downcast_ref::<u64>(), Some(&7));
checked.set(checked.get() + 1);
Ok(false)
})
.unwrap(),
1
);
assert_eq!(checked.get(), 1);
assert_eq!(
app.frame(&root).unwrap().state["outcome"]["status"],
"stale"
);
assert_eq!(app.child_mount(&root, "editor").unwrap(), &original);
assert!(!app.is_retiring(&root));
close_all(&mut app, &[root]);
}
#[test]
fn action_keys_correlate_out_of_order_stale_feedback_and_remain_reserved_until_delivery() {
let timer = timer();
let (mut app, root) = guarded_application(&timer);
let a = guarded_request(&mut app, &root, "A", 8);
for _ in 0..2 {
let revision = app.frame(&root).unwrap().revision;
assert!(
app.dispatch(&root, revision, "read", json!({"key":"A"}))
.is_err()
);
assert_eq!(app.frame(&root).unwrap().revision, revision);
assert!(!app.is_retiring(&root));
}
let b = guarded_request(&mut app, &root, "B", 8);
app.finish_task(b.id, timer.block_on(b.future)).unwrap();
let revision = app.frame(&root).unwrap().revision;
assert!(
app.dispatch(&root, revision, "read", json!({"key":"B"}))
.is_err()
);
app.deliver_action_results(&root, |_, _, _| Ok(true))
.unwrap();
app.finish_task(a.id, timer.block_on(a.future)).unwrap();
app.deliver_action_results(&root, |_, _, _| Ok(false))
.unwrap();
let results = &app.frame(&root).unwrap().state["results"];
assert_eq!(results[0]["key"], "B");
assert_eq!(results[0]["outcome"]["status"], "succeeded");
assert_eq!(results[1]["key"], "A");
assert_eq!(results[1]["outcome"]["status"], "stale");
let next = guarded_request(&mut app, &root, "A", 8);
app.finish_task(next.id, timer.block_on(next.future))
.unwrap();
app.deliver_action_results(&root, |_, _, _| Ok(true))
.unwrap();
close_all(&mut app, &[root]);
}
#[test]
fn original_continuation_edges_cannot_revive_after_source_replacement() {
let timer = timer();
for completed_first in [false, true] {
let (mut app, root) = guarded_application(&timer);
let original = app.child_mount(&root, "editor").unwrap().clone();
let task = guarded_request(&mut app, &root, "read_done", 8);
let mut done = Some(timer.block_on(task.future));
if completed_first {
app.finish_task(task.id, done.take().unwrap()).unwrap();
}
let revision = app.frame(&root).unwrap().revision;
app.dispatch(&root, revision, "remove", Value::Null)
.unwrap();
app.dispatch(&root, revision + 1, "restore", Value::Null)
.unwrap();
assert_ne!(app.child_mount(&root, "editor").unwrap(), &original);
if !completed_first {
app.finish_task(task.id, done.take().unwrap()).unwrap();
}
app.deliver_action_results(&root, |_, _, _| {
panic!("broken original edge cannot reach native validation")
})
.unwrap();
assert_eq!(
app.frame(&root).unwrap().state["outcome"]["status"],
"stale"
);
assert!(!app.is_retiring(&root));
close_all(&mut app, &[root]);
}
}
#[test]
fn stale_continuation_does_not_confirm_failed_executor_cleanup_or_revive_consumer() {
let timer = timer();
let (mut app, root) = guarded_application(&timer);
let task = guarded_request(&mut app, &root, "read_done", 8);
let TaskCompletion::Query(mut done) = timer.block_on(task.future) else {
panic!("query")
};
assert_eq!(done.result.as_ref().unwrap()["text"], "Start");
done.cleanup = Err("actual executor close failed".into());
assert!(
app.finish_task(task.id, TaskCompletion::Query(done))
.is_err()
);
assert_eq!(app.failed_action_ids(), vec![task.id]);
assert!(app.is_retiring(&root));
assert_eq!(
app.deliver_action_results(&root, |_, _, _| panic!("retired consumer is not admitted"))
.unwrap(),
0
);
assert_eq!(app.failed_action_ids(), vec![task.id]);
assert_eq!(app.accepted_action_count(), 1);
app.complete_action_cleanup(task.id, Ok(())).unwrap();
assert_eq!(app.accepted_action_count(), 0);
close_all(&mut app, &[root]);
}
#[test]
fn completed_undelivered_witnesses_count_toward_the_same_capture_budget() {
let timer = timer();
let (mut app, root) = guarded_application(&timer);
let task = guarded_request(&mut app, &root, "A", 32 * 1024 * 1024);
app.finish_task(task.id, timer.block_on(task.future))
.unwrap();
let revision = app.frame(&root).unwrap().revision;
app.dispatch(&root, revision, "read", json!({"key":"B"}))
.unwrap();
app.capture_actions(&root, |target| match target.method() {
"snapshot" => Ok(NativeCapture::Resource(Arc::new(Text("Start".into())))),
"reading" => Ok(NativeCapture::Continuation(ContinuationWitness::new(
7u64,
32 * 1024 * 1024,
))),
_ => unreachable!(),
})
.unwrap();
assert!(app.start_task(&root).unwrap().is_none());
app.deliver_action_results(&root, |_, _, _| Ok(true))
.unwrap();
let results = &app.frame(&root).unwrap().state["results"];
assert_eq!(results[0]["outcome"]["status"], "succeeded");
assert_eq!(results[1]["outcome"]["status"], "not_accepted");
assert!(
results[1]["outcome"]["reason"]
.as_str()
.unwrap()
.contains("resource window is busy")
);
let next = guarded_request(&mut app, &root, "B", 32 * 1024 * 1024);
app.finish_task(next.id, timer.block_on(next.future))
.unwrap();
app.deliver_action_results(&root, |_, _, _| Ok(true))
.unwrap();
close_all(&mut app, &[root]);
}
#[test]
fn query_recovery_metadata_is_rejected_before_publication_or_capture() {
let timer = timer();
let (mut app, root) = guarded_application_with(&timer, |source| {
source.replace(
"return out;",
"out.actions[0].recovery={format:'draft@1'};return out;",
)
});
let before = app.frame(&root).unwrap().revision;
assert!(
app.dispatch(&root, before, "read", Value::Null)
.unwrap_err()
.to_string()
.contains("recovery metadata requires a mutation")
);
assert_eq!(app.frame(&root).unwrap().revision, before);
assert_eq!(app.pending_action_count(), 0);
app.capture_actions(&root, |_| {
panic!("rejected metadata cannot start a capture")
})
.unwrap();
assert!(!app.is_retiring(&root));
close_all(&mut app, &[root]);
}
#[test]
fn duplicate_action_keys_in_one_callback_do_not_publish_or_start_capture() {
let timer = timer();
let (mut app, root) = guarded_application_with(&timer, |source| {
source.replace(
"return out;",
"out.actions.push({...out.actions[0]});return out;",
)
});
let before = app.frame(&root).unwrap().revision;
assert!(
app.dispatch(&root, before, "read", Value::Null)
.unwrap_err()
.to_string()
.contains("feedback key already in use")
);
assert_eq!(app.frame(&root).unwrap().revision, before);
assert_eq!(app.pending_action_count(), 0);
app.capture_actions(&root, |_| panic!("no capture admitted"))
.unwrap();
assert!(!app.is_retiring(&root));
close_all(&mut app, &[root]);
}
#[test]
fn feedback_can_atomically_reissue_its_original_key() {
let timer = timer();
let (mut app, root) = guarded_application_with(&timer, |source| {
source.replace("if(event.kind==='action'){", "if(event.kind==='action'&&!state.reissued){next.reissued=true;const out=frame(next);out.actions.push({key:event.key,binding:'reader',member:0,method:'read',input:{start:0,end:5},resources:{text:{child:'editor',method:'snapshot',input:{kind:'current'}}},continuations:[{child:'editor',method:'reading',input:null}]});return out;}if(event.kind==='action'){")
});
let task = guarded_request(&mut app, &root, "same_key", 8);
app.finish_task(task.id, timer.block_on(task.future))
.unwrap();
assert_eq!(
app.deliver_action_results(&root, |_, _, _| Ok(true))
.unwrap(),
1
);
assert_eq!(app.pending_action_count(), 1);
assert_eq!(app.frame(&root).unwrap().state["reissued"], true);
capture_guarded(&mut app, &root, 8);
let next = app.start_task(&root).unwrap().unwrap();
assert_ne!(task.id, next.id);
app.finish_task(next.id, timer.block_on(next.future))
.unwrap();
assert_eq!(
app.deliver_action_results(&root, |_, _, _| Ok(true))
.unwrap(),
1
);
assert_eq!(
app.frame(&root).unwrap().state["results"][0]["key"],
"same_key"
);
assert_eq!(app.pending_action_count(), 0);
close_all(&mut app, &[root]);
}
struct RecordingSource {
sources: Arc<std::sync::Mutex<Vec<maka_plugin_core::deployment::DeploymentRef>>>,
}
impl NativeService for RecordingSource {
fn call(&self, request: ServiceRequest) -> ServiceFuture {
self.sources.lock().unwrap().push(
request
.source
.deployment()
.expect("actual owned UI source")
.clone(),
);
Box::pin(async {
ServiceCompletion {
result: Ok(json!({"text":"Start","metadata":{}})),
cleanup: Ok(()),
}
})
}
}
#[test]
fn live_remote_epoch_update_preserves_original_query_source_and_waits_for_accepted_work() {
use maka_event_log::{
EventLog,
root::{RootNamespaces, RootOwner},
};
use maka_plugin_core::{
contract::{Cardinality, MethodMode},
deployment::RemoteObservation,
};
let timer = timer();
let dir = tempfile::tempdir().unwrap();
let ns = RootNamespaces {
ownership: dir.path().join("owners"),
control: dir.path().join("control"),
};
let store = Arc::new(
timer
.block_on(EventLog::for_root(Arc::new(
RootOwner::create(&dir.path().join("root"), &ns).unwrap(),
)))
.unwrap(),
);
let (input, packages, native, mut remotes, target) =
remote_fixture(MethodMode::Query, Cardinality::One, true);
let seen = Arc::new(std::sync::Mutex::new(Vec::new()));
let service: Arc<dyn NativeService> = Arc::new(RecordingSource {
sources: seen.clone(),
});
let wire = remotes.registrations[0].registration_epoch;
let contract = remotes.registrations[0].contract.clone();
remotes.registrations[0].implementation = service.clone();
let observations = remotes
.declarations
.into_iter()
.map(|(capability, declaration)| RemoteObservation {
capability,
declaration,
})
.collect::<Vec<_>>();
let deployment = uuid::Uuid::new_v4();
let original = timer
.block_on(store.commit_application(
deployment,
0,
input.clone(),
observations.clone(),
packages.clone(),
))
.unwrap();
let mut app = timer
.block_on(Application::start_from_store(
store.clone(),
original.clone(),
PresentationTarget::Tui,
timer.handle().clone(),
maka_ui_js::execution(Default::default()),
native,
remotes.registrations,
))
.unwrap();
let root = app.mount_launch(json!({})).unwrap();
let registration = |epoch| RemoteService {
target: target.clone(),
registration_epoch: epoch,
contract: contract.clone(),
implementation: service.clone(),
};
app.dispatch(&root, 0, "read", Value::Null).unwrap();
let snapshot = capture(&mut app, &root, "Start");
let mut desired = input.clone();
let mut extra = desired.instances["surface"].clone();
extra.key = uuid::Uuid::from_u128(19);
desired.instances.insert("extra".into(), extra);
let next = timer
.block_on(store.commit_application(
deployment,
1,
desired.clone(),
observations.clone(),
packages.clone(),
))
.unwrap();
timer
.block_on(app.adopt_committed(next.clone(), vec![registration(wire)]))
.unwrap();
app.advance_deployment().unwrap();
assert!(!app.is_retiring(&root));
let task = app.start_task(&root).unwrap().unwrap();
let done = timer.block_on(task.future);
app.finish_task(task.id, done).unwrap();
app.deliver_action_results(&root, |_, _, _| Ok(false))
.unwrap();
assert_eq!(*seen.lock().unwrap(), vec![original.clone()]);
let rev = app.frame(&root).unwrap().revision;
app.dispatch(&root, rev, "read", Value::Null).unwrap();
let held = capture(&mut app, &root, "Start");
let old_task = app.start_task(&root).unwrap().unwrap();
let attachment = app.remote_activation(&target).unwrap();
let latest = timer
.block_on(store.commit_application(deployment, 2, desired, observations, packages))
.unwrap();
let new_wire = uuid::Uuid::new_v4();
timer
.block_on(app.adopt_committed(latest.clone(), vec![registration(new_wire)]))
.unwrap();
app.advance_deployment().unwrap();
assert!(app.is_retiring(&root));
assert_eq!(app.remote_activation(&target).unwrap(), attachment);
for mount in app.retiring_mounts() {
app.close(&mount, Ok(())).unwrap();
}
app.advance_deployment().unwrap();
assert_eq!(app.remote_activation(&target).unwrap(), attachment);
assert!(app.deployment_status().converging);
let done = timer.block_on(old_task.future);
app.finish_task(old_task.id, done).unwrap();
assert_eq!(*seen.lock().unwrap(), vec![original.clone(), original]);
app.advance_deployment().unwrap();
assert_eq!(app.deployment_status().effective, Some(latest.clone()));
assert_ne!(app.remote_activation(&target).unwrap(), attachment);
let fresh = app.mount_launch(json!({})).unwrap();
app.dispatch(&fresh, 0, "read", Value::Null).unwrap();
let new_snapshot = capture(&mut app, &fresh, "Start");
let task = app.start_task(&fresh).unwrap().unwrap();
let done = timer.block_on(task.future);
app.finish_task(task.id, done).unwrap();
assert_eq!(seen.lock().unwrap().last(), Some(&latest));
close_all(&mut app, &[fresh]);
drop((app, snapshot, held, new_snapshot, service));
timer
.block_on(Arc::try_unwrap(store).ok().unwrap().close())
.unwrap();
}
#[test]
fn complete_oversized_action_holds_resource_window_until_original_settlement() {
let timer = timer();
let (mut app, first, _) = application(false, &timer);
let competing = app.mount_launch(json!({})).unwrap();
let after_release = app.mount_launch(json!({})).unwrap();
app.dispatch(&first, 0, "read", Value::Null).unwrap();
let snapshot: Arc<dyn ReadResource> =
Arc::new(Text(format!("Start{}", "x".repeat(64 * 1024 * 1024))));
let weak = Arc::downgrade(&snapshot);
let captured = app
.capture_actions(&first, |_| Ok(NativeCapture::Resource(snapshot.clone())))
.unwrap();
drop(snapshot);
let work = app.start_task(&first).unwrap();
app.dispatch(&competing, 0, "read", Value::Null).unwrap();
let competing_captured = app
.capture_actions(&competing, |_| {
Ok(NativeCapture::Resource(Arc::new(Text("Start".into()))))
})
.unwrap();
let competing_rejected = app.start_task(&competing).unwrap().is_none();
app.deliver_action_results(&competing, |_, _, _| Ok(false))
.unwrap();
let competing_status = app.frame(&competing).unwrap().state["outcome"]["status"].clone();
let retained_before_settlement = weak.upgrade().is_some();
let completed = if let Some(work) = work {
let done = timer.block_on(work.future);
let succeeded = matches!(&done, TaskCompletion::Query(completion) if completion.result.as_ref().is_ok_and(|value| value["text"] == "Start"));
app.finish_task(work.id, done).unwrap();
succeeded
} else {
false
};
app.deliver_action_results(&first, |_, _, _| Ok(false))
.unwrap();
let released = weak.upgrade().is_none();
app.dispatch(&after_release, 0, "read", Value::Null)
.unwrap();
let recaptured = app
.capture_actions(&after_release, |_| {
Ok(NativeCapture::Resource(Arc::new(Text("Start".into()))))
})
.unwrap();
let work = app.start_task(&after_release).unwrap();
let read_after_release = if let Some(work) = work {
let done = timer.block_on(work.future);
let succeeded = matches!(&done, TaskCompletion::Query(completion) if completion.result.as_ref().is_ok_and(|value| value["text"] == "Start"));
app.finish_task(work.id, done).unwrap();
succeeded
} else {
false
};
app.deliver_action_results(&after_release, |_, _, _| Ok(false))
.unwrap();
close_all(&mut app, &[first, competing, after_release]);
assert_eq!(
captured, 1,
"one complete oversized native resource must be admitted"
);
assert!(
retained_before_settlement && completed,
"the original query must retain and read its admitted resource"
);
assert_eq!(competing_captured, 0);
assert!(competing_rejected);
assert_eq!(competing_status, "not_accepted");
assert!(
released,
"original resource must release after acknowledged settlement"
);
assert_eq!(recaptured, 1);
assert!(read_after_release);
}
#[test]
fn one_complete_component_frame_keeps_seventeen_original_action_keys_and_sources() {
use std::panic::{AssertUnwindSafe, catch_unwind};
let timer = timer();
let (mut input, mut packages, native, _) = fixture(false);
let index = packages
.iter()
.position(|package| package.manifest().id == "dynamic.app")
.unwrap();
let original = packages.remove(index);
let old = "out.actions.push({key:'read_done',binding:'reader',member:0,method:'read',input:{start:event.event?.start??0,end:event.event?.end??5},resources:{text:{child:'editor',method:'snapshot',input:event.event?.capture??{kind:'current'}}}});return out;";
let source = std::str::from_utf8(original.file("index.mjs").unwrap()).unwrap();
assert!(source.contains(old));
let source = source.replace(old, "for(let index=0;index<17;index++)out.actions.push({key:'read_done_'+index,binding:'reader',member:0,method:'read',input:{start:index,end:index+1},resources:{text:{child:'editor',method:'snapshot',input:{kind:'current'}}}});return out;")
.replace("if(event.kind==='action')next.outcome=event.event;", "if(event.kind==='action')next.results=[...(state.results??[]),{key:event.key,outcome:event.event}];");
let package = support::package(
"dynamic.app",
original
.manifest()
.application
.as_ref()
.unwrap()
.declaration()
.clone(),
Some(source),
);
input.instances.get_mut("app").unwrap().package = package.application_ref().into();
packages.push(package);
let mut app = Application::new(
&input,
packages,
timer.handle().clone(),
maka_ui_js::execution(Default::default()),
native,
Default::default(),
)
.unwrap();
let root = app.mount_launch(json!({})).unwrap();
let completed = catch_unwind(AssertUnwindSafe(|| -> Result<(), String> {
app.dispatch(&root, 0, "read", Value::Null)
.map_err(|error| error.to_string())?;
let text = "ABCDEFGHIJKLMNOPQ";
let snapshot: Arc<dyn ReadResource> = Arc::new(Text(text.into()));
let weak = Arc::downgrade(&snapshot);
let captured = app
.capture_actions(&root, |target| {
assert_eq!(target.method(), "snapshot");
Ok(NativeCapture::Resource(snapshot.clone()))
})
.map_err(|error| error.to_string())?;
drop(snapshot);
assert_eq!(captured, 17);
let mut completed = 0;
while let Some(task) = app.start_task(&root).map_err(|error| error.to_string())? {
let completion = timer.block_on(task.future);
app.finish_task(task.id, completion)
.map_err(|error| error.to_string())?;
completed += 1;
}
assert_eq!(completed, 17);
app.deliver_action_results(&root, |_, _, _| Ok(false))
.map_err(|error| error.to_string())?;
let expected: Vec<_> = text.chars().enumerate().map(|(index, character)| json!({"key":format!("read_done_{index}"),"outcome":{"status":"succeeded","value":{"text":character.to_string(),"metadata":{"revision":3,"bytes":17}}}})).collect();
assert_eq!(app.frame(&root).unwrap().state["results"], json!(expected));
assert!(
weak.upgrade().is_none(),
"all original resource custody must release after settlement"
);
assert_eq!(app.accepted_action_count(), 0);
Ok(())
}));
close_all(&mut app, &[root]);
let result = completed.unwrap_or_else(|panic| std::panic::resume_unwind(panic));
assert!(
result.is_ok(),
"complete original component action frame failed: {}",
result.err().unwrap_or_default()
);
}