blob: d53a60e69e6804f83f23793550ae5cc70ab5946a [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 maka_agent_facts::tools::ToolExecutor;
use maka_agent_facts::tools::ToolFuture;
use maka_code_runtime::{CellAbort, CellContext, CellLimits, CellOutput, CellResult, CellStore};
use maka_js_runtime::repl::Repl;
use serde_json::{Value, json};
use std::{
sync::{
Arc,
atomic::{AtomicUsize, Ordering},
},
time::Duration,
};
use tokio_util::sync::CancellationToken;
const BOOTSTRAP: &str = r#"
Object.defineProperty(globalThis, 'invoke', { value: async () => {
const result = await call('fixture', {});
if (!result.ok) throw Error(result.error.message);
return result.value;
}});
Object.defineProperty(globalThis, 'write', { value: value => emit({kind:'text', text:JSON.stringify(value)}) });
"#;
struct Fixture(usize);
impl ToolExecutor for Fixture {
fn names(&self) -> Vec<String> {
vec!["fixture".into()]
}
fn invoke(&self, _: String, _: Value, _: CancellationToken) -> ToolFuture {
let value = self.0;
Box::pin(async move { Ok(json!(value)) })
}
}
fn context() -> CellContext {
CellContext::new(CellStore::default(), vec![])
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn repl_keeps_bindings_but_uses_current_call_and_preserves_output_on_exception() {
let repl = Repl::new(BOOTSTRAP, CellLimits::default());
for (source, authority, expected, success) in [
(
"const target = {value: await invoke()}; write(target.value)",
1,
"1",
true,
),
(
"write([target.value, await invoke()]); throw Error('expected')",
2,
"[1,2]",
false,
),
(
"const target = {value: 3}; write([target.value, typeof Deno, typeof tools, typeof fetch])",
4,
"[3,\"undefined\",\"undefined\",\"undefined\"]",
true,
),
] {
let output = context();
let result = repl
.evaluate(
source.into(),
Arc::new(Fixture(authority)),
CancellationToken::new(),
output.clone(),
Duration::from_secs(5),
)
.await
.unwrap();
assert_eq!(
matches!(result, CellResult::Success { .. }),
success,
"{result:?}"
);
let output: [CellOutput; 1] = output.take_output().try_into().unwrap();
let [CellOutput::Text { text }] = output else {
panic!("expected text")
};
assert_eq!(text, expected);
}
repl.close().await.unwrap();
}
struct Held {
started: CancellationToken,
released: CancellationToken,
settled: Arc<AtomicUsize>,
}
impl ToolExecutor for Held {
fn names(&self) -> Vec<String> {
vec!["fixture".into()]
}
fn invoke(&self, _: String, _: Value, _: CancellationToken) -> ToolFuture {
self.started.cancel();
let released = self.released.clone();
let settled = self.settled.clone();
Box::pin(async move {
released.cancelled().await;
settled.fetch_add(1, Ordering::SeqCst);
Ok(Value::Null)
})
}
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn cancellation_waits_for_native_settlement_and_requires_reset() {
let repl = Arc::new(Repl::new(BOOTSTRAP, CellLimits::default()));
let stop = CancellationToken::new();
let started = CancellationToken::new();
let released = CancellationToken::new();
let settled = Arc::new(AtomicUsize::new(0));
let task = {
let repl = repl.clone();
let stop = stop.clone();
let held = Held {
started: started.clone(),
released: released.clone(),
settled: settled.clone(),
};
tokio::spawn(async move {
repl.evaluate(
"await invoke()".into(),
Arc::new(held),
stop,
context(),
Duration::from_secs(5),
)
.await
})
};
started.cancelled().await;
stop.cancel();
assert!(!task.is_finished());
released.cancel();
assert!(matches!(task.await.unwrap(), Err(CellAbort::Cancelled)));
assert!(repl.is_closed());
assert_eq!(settled.load(Ordering::SeqCst), 1);
assert!(
repl.evaluate(
"write('late')".into(),
Arc::new(Fixture(2)),
CancellationToken::new(),
context(),
Duration::from_secs(5)
)
.await
.is_err()
);
Arc::try_unwrap(repl).ok().unwrap().close().await.unwrap();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn loops_and_unresolved_await_are_bounded_and_do_not_reuse_the_heap() {
for (source, heap) in [
("while (true) {}", false),
("await new Promise(() => {})", false),
(
"const a = []; while (true) a.push(new Array(10000).fill('heap'))",
true,
),
] {
let limits = CellLimits {
heap_bytes: 8 * 1024 * 1024,
..CellLimits::default()
};
let repl = Repl::new(BOOTSTRAP, limits);
let result = repl
.evaluate(
source.into(),
Arc::new(Fixture(0)),
CancellationToken::new(),
context(),
if heap {
Duration::from_secs(5)
} else {
Duration::from_millis(100)
},
)
.await
.unwrap();
assert!(matches!(result, CellResult::Failure { .. }), "{result:?}");
if heap {
assert!(
matches!(&result,CellResult::Failure {error,..} if error.kind==maka_code_runtime::CellDiagnosticKind::LimitExceeded && error.message.contains("heap")),
"heap guard must fire before the wall timeout: {result:?}"
);
}
let next = repl
.evaluate(
"write(1)".into(),
Arc::new(Fixture(0)),
CancellationToken::new(),
context(),
Duration::from_secs(1),
)
.await;
assert!(
next.is_err(),
"source={source:?}; first={result:?}; next={next:?}"
);
repl.close().await.unwrap();
}
}