blob: 41cc9aed23e6df9fb4ff9d52686780213c315c5a [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::*;
use maka_agent_facts::event::{Fact, InvocationOutcome, ToolOutcome};
async fn fixture(base: &str) -> Fixture {
Fixture::configured(base, None, None, None, |settings| {
settings.goals = true;
settings.tool_discovery = true;
})
.await
}
async fn current(f: &Fixture, session: &str) -> Value {
f.query("goal", json!({"sessionId":session})).await.unwrap()["current"].clone()
}
async fn wait_status(f: &Fixture, session: &str, status: &str) -> Value {
loop {
let value = current(f, session).await;
if value["status"] == status && value["pending"] == false {
return value;
}
tokio::time::sleep(Duration::from_millis(10)).await;
}
}
async fn approval(f: &Fixture, session: &str) -> Value {
let config = f.owner.session(session).await.unwrap().configuration;
json!({"target":{"sessionId":session,"workspace":config.workspace,"permissions":config.permissions,"boundaryRevision":config.boundary_revision},"model":f.selection().await,"thinkingLevel":null})
}
async fn create(
f: &Fixture,
session: &str,
start: bool,
iterations: u32,
budget: Option<u64>,
) -> Value {
f.mutation(2, "changeGoal", json!({"kind":"create","sessionId":session,"objective":"Verify the original native Goal 中😀","start":start,"maxIterations":iterations,"tokenBudget":budget,"approval":approval(f,session).await}), Uuid::new_v4()).await.unwrap()
}
async fn control(f: &Fixture, session: &str, action: &str) -> Value {
let current = current(f, session).await;
f.mutation(2, "changeGoal", json!({"kind":"control","sessionId":session,"goalId":current["id"],"revision":current["revision"],"action":action}), Uuid::new_v4()).await.unwrap()
}
async fn report(socket: &mut TcpStream, code: bool, status: &str) {
let input = json!({"status":status,"note":format!("Verified checkpoint {status} 中😀")});
let (name, input) = if code {
(
"exec",
json!({"code":format!("text(await tools.GoalStatus({input}));")}),
)
} else {
("GoalStatus", input)
};
service_tools::named_tool_response(socket, name, input).await;
}
#[tokio::test]
async fn goal_real_model_direct_and_code_mode_continue_only_after_settlement_and_keep_restart_receipts()
{
tokio::time::timeout(Duration::from_secs(90), async {
for code in [false, true] {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let mut f = fixture(&format!("http://{}/v1", listener.local_addr().unwrap())).await;
selection::edit(&f, |changes| {
changes.model_overrides = Patch::Set(BTreeMap::from([("fixture-model".into(), ModelOverride { code_mode: Some(code), ..Default::default() })]));
}).await;
let session = f.create("Goal execution").await;
let other = f.create("Goal isolation").await;
assert_eq!(create(&f, &session, true, 3, None).await["kind"], "committed");
let (mut first, _) = listener.accept().await.unwrap();
let sent = request(&mut first).await;
let names: Vec<_> = sent["tools"].as_array().unwrap().iter().filter_map(|tool| tool["function"]["name"].as_str()).collect();
assert!(names.contains(&if code { "exec" } else { "GoalStatus" }), "{names:?}");
assert!(!f.app.shutdown_if_idle().await.unwrap(), "an active Goal keeps the original Host alive");
report(&mut first, code, "progress").await;
let (mut finish, _) = listener.accept().await.unwrap();
let result = request(&mut finish).await;
assert!(result["messages"].to_string().contains("recorded"), "{result}");
let pending = current(&f, &session).await;
assert_eq!(pending["status"], "active");
assert_eq!(pending["report"]["status"], "progress");
assert_eq!(pending["iterations"], 1);
response(&mut finish).await;
let (mut next, _) = listener.accept().await.unwrap();
let second = request(&mut next).await;
assert!(second["messages"].to_string().contains("Verified checkpoint progress"));
assert_eq!(current(&f, &session).await["iterations"], 2);
report(&mut next, code, "achieved").await;
let (mut finish, _) = listener.accept().await.unwrap();
request(&mut finish).await;
assert_eq!(current(&f, &session).await["status"], "active", "model claims do not settle an unfinished iteration");
response(&mut finish).await;
let achieved = wait_status(&f, &session, "achieved").await;
assert_eq!(achieved["iterations"], 2);
assert!(achieved["consumed"]["known"].as_u64().unwrap() >= 6);
assert!(achieved["consumed"]["missing"].as_u64().unwrap() > 0);
assert!(current(&f, &other).await.is_null());
assert!(f.query("goal", json!({"sessionId":"foreign-session"})).await.is_err());
let facts = service_tools::facts(&f, &session).await;
assert_eq!(facts.iter().filter(|fact| matches!(fact, Fact::InvocationOpened { .. })).count(), 2);
assert_eq!(facts.iter().filter(|fact| matches!(fact, Fact::ToolDispatched { name, .. } if name == "GoalStatus")).count(), 2);
assert!(facts.iter().any(|fact| matches!(fact, Fact::ToolSettled { outcome: ToolOutcome::Succeeded { .. }, .. })));
todo_tests::reopen(&mut f).await;
assert_eq!(current(&f, &session).await, achieved);
assert!(tokio::time::timeout(Duration::from_millis(100), listener.accept()).await.is_err());
f.app.shutdown().await.unwrap();
f.config.shutdown().await.unwrap();
}
}).await.unwrap();
}
#[tokio::test]
async fn goal_pause_stops_later_iterations_and_cancel_targets_only_the_original_goal_run() {
tokio::time::timeout(Duration::from_secs(60), async {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let f = fixture(&format!("http://{}/v1", listener.local_addr().unwrap())).await;
let session = f.create("Pause and cancel Goal").await;
let sibling = f.create("Independent user request").await;
assert_eq!(
create(&f, &session, true, 5, None).await["kind"],
"committed"
);
let (mut first, _) = listener.accept().await.unwrap();
request(&mut first).await;
assert_eq!(control(&f, &session, "pause").await["kind"], "committed");
response(&mut first).await;
wait_status(&f, &session, "paused").await;
assert!(
tokio::time::timeout(Duration::from_millis(100), listener.accept())
.await
.is_err()
);
assert_eq!(control(&f, &session, "resume").await["kind"], "committed");
let (mut goal_run, _) = listener.accept().await.unwrap();
request(&mut goal_run).await;
assert_eq!(f.send(&sibling).await.unwrap()["kind"], "started");
assert!(f.owner.runs.lock().await.controls.contains_key(&sibling));
assert_eq!(control(&f, &session, "cancel").await["kind"], "committed");
let mut byte = [0];
assert_eq!(
goal_run.read(&mut byte).await.unwrap(),
0,
"the original model connection is physically cancelled"
);
let cancelled = wait_status(&f, &session, "cancelled").await;
assert_eq!(cancelled["iterations"], 2);
assert!(f.owner.runs.lock().await.controls.contains_key(&sibling));
let (mut sibling_run, _) = listener.accept().await.unwrap();
request(&mut sibling_run).await;
response(&mut sibling_run).await;
f.drained().await;
assert!(
service_tools::facts(&f, &session)
.await
.iter()
.any(|fact| matches!(
fact,
Fact::InvocationEnded {
outcome: InvocationOutcome::Cancelled { .. }
}
))
);
assert!(
service_tools::facts(&f, &sibling)
.await
.iter()
.any(|fact| matches!(
fact,
Fact::InvocationEnded {
outcome: InvocationOutcome::Completed
}
))
);
f.app.shutdown().await.unwrap();
})
.await
.unwrap();
}
#[tokio::test]
async fn goal_usage_threshold_missing_counters_and_iteration_limit_stop_real_continuations() {
tokio::time::timeout(Duration::from_secs(60), async {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let f = fixture(&format!("http://{}/v1", listener.local_addr().unwrap())).await;
for (name, budget, report_missing, expected) in [
("Known budget", Some(3), false, "budget_limited"),
("Unknown budget", Some(100_000), true, "budget_unknown"),
("Bounded iterations", None, false, "max_iterations"),
] {
let session = f.create(name).await;
assert_eq!(
create(&f, &session, true, 1, budget).await["kind"],
"committed"
);
let (mut socket, _) = listener.accept().await.unwrap();
request(&mut socket).await;
if report_missing {
report(&mut socket, false, "progress").await;
let (mut next, _) = listener.accept().await.unwrap();
request(&mut next).await;
response(&mut next).await;
} else {
response(&mut socket).await;
}
let stopped = wait_status(&f, &session, expected).await;
assert_eq!(stopped["iterations"], 1);
assert_eq!(stopped["consumed"]["known"], 3);
assert!(
tokio::time::timeout(Duration::from_millis(100), listener.accept())
.await
.is_err()
);
}
f.app.shutdown().await.unwrap();
})
.await
.unwrap();
}
#[tokio::test]
async fn goal_original_business_receipt_and_reserved_iteration_survive_restart_without_replay() {
use maka_capabilities::storage::Namespace;
use maka_goal::goal::{Control, Repository};
tokio::time::timeout(Duration::from_secs(60), async {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let mut f = fixture(&format!("http://{}/v1", listener.local_addr().unwrap())).await;
let session = f.create("Recover Goal").await;
let input = json!({"kind":"create","sessionId":session,"objective":"Preserve this original approval 中😀","start":false,"maxIterations":1,"tokenBudget":3,"approval":approval(&f,&session).await});
let client = f.app.client();
let prepared = client.prepare_mutation(f.binding(2).await, "changeGoal".into(), input.clone(), scope(&f.issuer,true), Uuid::new_v4(), Attachments::new().into()).await.unwrap();
let reference = prepared.reference().clone();
f.store.record_application_intent(reference.source.clone(), prepared.consumer().instance().clone(), reference.clone(), Value::Null).await.unwrap();
// Commit the business transaction without delivering its reply to Host.
let committed = f.owner.goal_change(serde_json::from_value(input).unwrap(), reference.clone(), &scope(&f.issuer,true)).await.unwrap();
let committed = serde_json::to_value(committed).unwrap();
assert_eq!(committed["kind"], "committed");
let armed = current(&f,&session).await;
todo_tests::reopen(&mut f).await;
assert_eq!(current(&f,&session).await,armed);
assert_eq!(f.query("goalReceipt",json!({"reference":reference})).await.unwrap(),committed);
f.app.shutdown().await.unwrap();
let storage = Arc::new(maka_host_runtime::storage::Storage::new(f.store.clone(), f.owner.prefix.clone(), Namespace::new("maka.agent.goal",maka_plugin_core::scope::Scope::Profile).unwrap()));
let repo: Repository<MessageBasis> = Repository::new(storage.clone());
let mut saved = repo.read(&session).await.unwrap().unwrap();
saved.goal.control(Control::Resume).unwrap();
saved.goal.reserve().unwrap();
let original = saved.goal.clone();
repo.save(saved).await.unwrap();
todo_tests::reopen(&mut f).await;
let (mut socket,_) = listener.accept().await.unwrap();
assert!(request(&mut socket).await.to_string().contains("Preserve this original approval"));
response(&mut socket).await;
assert_eq!(wait_status(&f,&session,"budget_limited").await["iterations"],1);
f.app.shutdown().await.unwrap();
// A lost Goal settlement retains its outbox, but the original turn already
// completed. Even revocation must not rewrite or redispatch that result.
let mut saved = repo.read(&session).await.unwrap().unwrap();
saved.goal = original;
repo.save(saved).await.unwrap();
f.store.set_session_archived::<SessionConfiguration>(&session,true,now()).await.unwrap();
todo_tests::reopen(&mut f).await;
assert_eq!(wait_status(&f,&session,"budget_limited").await["iterations"],1);
assert_eq!(service_tools::facts(&f,&session).await.iter().filter(|fact| matches!(fact,Fact::InvocationOpened { .. })).count(),1);
assert!(tokio::time::timeout(Duration::from_millis(100),listener.accept()).await.is_err());
assert_eq!(f.query("goalReceipt",json!({"reference":reference})).await.unwrap(),committed);
f.app.shutdown().await.unwrap();
storage.finish().await.unwrap();
}).await.unwrap();
}
#[tokio::test]
async fn goal_rechecks_privacy_and_revoked_session_before_original_outbox_admission() {
use maka_capabilities::storage::Namespace;
use maka_goal::goal::{Control, Repository};
tokio::time::timeout(Duration::from_secs(45), async {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let mut f = fixture(&format!("http://{}/v1",listener.local_addr().unwrap())).await;
let session = f.create("Revocable Goal").await;
let mut stale = approval(&f,&session).await;
stale["target"]["boundaryRevision"] = json!(99);
let rejected = f.mutation(2,"changeGoal",json!({"kind":"create","sessionId":session,"objective":"Stale boundary","start":true,"maxIterations":2,"tokenBudget":null,"approval":stale}),Uuid::new_v4()).await.unwrap();
assert_eq!(rejected["kind"],"rejected");
assert!(current(&f,&session).await.is_null());
let policy = f.config.runtime_policy().await.unwrap();
f.config.set_privacy(policy.revision,maka_agent_facts::configuration::policy::PrivacyPolicy{incognito_active:true}).await.unwrap();
assert_eq!(create(&f,&session,true,2,None).await["reason"],"goal_incognito");
assert!(current(&f,&session).await.is_null());
let policy = f.config.runtime_policy().await.unwrap();
f.config.set_privacy(policy.revision,maka_agent_facts::configuration::policy::PrivacyPolicy{incognito_active:false}).await.unwrap();
assert_eq!(create(&f,&session,false,2,None).await["kind"],"committed");
f.app.shutdown().await.unwrap();
let storage = Arc::new(maka_host_runtime::storage::Storage::new(f.store.clone(),f.owner.prefix.clone(),Namespace::new("maka.agent.goal",maka_plugin_core::scope::Scope::Profile).unwrap()));
let repo: Repository<MessageBasis> = Repository::new(storage.clone());
let mut saved = repo.read(&session).await.unwrap().unwrap();
saved.goal.control(Control::Resume).unwrap();
saved.goal.reserve().unwrap();
repo.save(saved).await.unwrap();
f.store.set_session_archived::<SessionConfiguration>(&session,true,now()).await.unwrap();
todo_tests::reopen(&mut f).await;
let blocked = wait_status(&f,&session,"blocked").await;
assert_eq!(blocked["iterations"],1);
assert!(service_tools::facts(&f,&session).await.is_empty());
assert!(tokio::time::timeout(Duration::from_millis(100),listener.accept()).await.is_err());
f.app.shutdown().await.unwrap();
storage.finish().await.unwrap();
}).await.unwrap();
}
#[tokio::test]
async fn goal_prepared_report_cannot_override_a_newer_user_pause() {
use maka_agent_facts::{
tool_call::ToolCallIdentity,
tools::{ToolCallContext, ToolJournal, ToolPreparer},
};
tokio::time::timeout(Duration::from_secs(45), async {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let f = fixture(&format!("http://{}/v1",listener.local_addr().unwrap())).await;
let session = f.create("User controls Goal").await;
assert_eq!(create(&f,&session,true,2,None).await["kind"],"committed");
let (mut socket,_) = listener.accept().await.unwrap();
request(&mut socket).await;
let invocation = f.owner.runs.lock().await.controls.get(&session).unwrap().invocation.clone();
let input = json!({"status":"achieved","note":"A prepared report cannot overrule later user intent"});
let pending = f.owner.goals.prepare("GoalStatus".into(),input.clone(),ToolCallContext{invocation:invocation.clone(),operation_id:"late-report".into()},CancellationToken::new()).await.unwrap();
assert_eq!(control(&f,&session,"pause").await["kind"],"committed");
assert!(ToolJournal::new(f.store.clone(),invocation).invoke_prepared_call("late-report".into(),ToolCallIdentity::standalone("late-report".into()),"GoalStatus".into(),input,CancellationToken::new(),pending).await.is_err());
response(&mut socket).await;
let paused = wait_status(&f,&session,"paused").await;
assert!(paused["report"].is_null());
assert_eq!(paused["iterations"],1);
assert!(tokio::time::timeout(Duration::from_millis(100),listener.accept()).await.is_err());
f.app.shutdown().await.unwrap();
}).await.unwrap();
}
#[tokio::test]
async fn goal_cancelled_before_dispatch_never_calls_the_model() {
tokio::time::timeout(Duration::from_secs(30), async {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let f = fixture(&format!("http://{}/v1", listener.local_addr().unwrap())).await;
let session = f.create("Cancel accepted Goal queue").await;
let producer = f.owner.producer_gate.lock().await;
assert_eq!(
create(&f, &session, true, 2, None).await["kind"],
"committed"
);
assert_eq!(control(&f, &session, "cancel").await["kind"], "committed");
drop(producer);
wait_status(&f, &session, "cancelled").await;
assert!(
f.store
.message_queue(&session)
.await
.unwrap()
.entries
.is_empty()
);
assert!(
tokio::time::timeout(Duration::from_millis(100), listener.accept())
.await
.is_err()
);
let facts = service_tools::facts(&f, &session).await;
assert_eq!(
facts
.iter()
.filter(|fact| matches!(fact, Fact::InvocationOpened { .. }))
.count(),
0
);
assert!(!facts.iter().any(|fact| matches!(
fact,
Fact::InvocationEnded {
outcome: InvocationOutcome::Completed
}
)));
f.app.shutdown().await.unwrap();
})
.await
.unwrap();
}
#[tokio::test]
async fn goal_language_stays_approved_until_an_idle_resume_renews_both_captures() {
fn system(request: &Value) -> String {
request["messages"]
.as_array()
.unwrap()
.iter()
.filter(|message| message["role"] == "system")
.map(|message| message["content"].as_str().unwrap())
.collect::<Vec<_>>()
.join("\n")
}
fn user(request: &Value) -> &str {
request["messages"]
.as_array()
.unwrap()
.iter()
.rev()
.find(|message| message["role"] == "user")
.unwrap()["content"]
.as_str()
.unwrap()
}
tokio::time::timeout(Duration::from_secs(60), async {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let home = tempfile::tempdir().unwrap();
let f = Fixture::configured(&format!("http://{}/v1", listener.local_addr().unwrap()), None, None, None, |settings| {
settings.goals = true;
settings.tool_discovery = true;
settings.assistant = Some(contract::AssistantConfiguration { home: Some(home.path().to_str().unwrap().into()) });
}).await;
let session = f.create("Goal language authority").await;
let english = prompt_language::select_language(&f, "en").await;
let objective = "GOAL_ORIGINAL_🦀 原样数据 \\\"quoted\\\" must survive every approval";
let input = json!({"kind":"create","sessionId":session,"objective":objective,"start":true,"maxIterations":8,"tokenBudget":null,"approval":approval(&f,&session).await});
let operation = Uuid::new_v4();
let created = f.mutation(2, "changeGoal", input.clone(), operation).await.unwrap();
let mut watch = f.app.client().subscribe(f.binding(2).await, "watchGoal".into(), json!({"sessionId":session}),
scope(&f.issuer, true), Attachments::new().into()).await.unwrap();
let (mut socket, _) = listener.accept().await.unwrap();
let first = request(&mut socket).await;
let first_invocation = f.store.latest_turn_boundary(&session).await.unwrap().unwrap().invocation;
let first_configuration = f.store.invocation_configuration(&first_invocation).await.unwrap().unwrap();
let accepted = session_context::composition(&f, &session).await;
let chinese = prompt_language::select_language(&f, "zh-CN").await;
let pending = current(&f, &session).await;
let retried = f.mutation(2, "changeGoal", input, operation).await.unwrap();
let after_retry = current(&f, &session).await;
report(&mut socket, false, "progress").await;
let (mut socket, _) = listener.accept().await.unwrap();
let followup = request(&mut socket).await;
response(&mut socket).await;
let (mut socket, _) = listener.accept().await.unwrap();
let original_chain = request(&mut socket).await;
let chain_invocation = f.store.latest_turn_boundary(&session).await.unwrap().unwrap().invocation;
let chain_configuration = f.store.invocation_configuration(&chain_invocation).await.unwrap().unwrap();
let paused = control(&f, &session, "pause").await;
response(&mut socket).await;
let idle_english = loop {
let observed = watch.recv().await.unwrap();
if observed["current"]["status"] == "paused" && observed["current"]["pending"] == false {
break observed["current"].clone();
}
};
let renewed_cn = f.mutation(2, "changeGoal", json!({"kind":"control","sessionId":session,"goalId":idle_english["id"],"revision":idle_english["revision"],"action":"resume","approval":approval(&f,&session).await}), Uuid::new_v4()).await.unwrap();
let (mut socket, _) = listener.accept().await.unwrap();
let cn_request = request(&mut socket).await;
let cn_invocation = f.store.latest_turn_boundary(&session).await.unwrap().unwrap().invocation;
let cn_configuration = f.store.invocation_configuration(&cn_invocation).await.unwrap().unwrap();
let paused_cn = control(&f, &session, "pause").await;
response(&mut socket).await;
let idle_cn = loop {
let observed = watch.recv().await.unwrap();
if observed["current"]["status"] == "paused" && observed["current"]["pending"] == false {
break observed["current"].clone();
}
};
let traditional = prompt_language::select_language(&f, "zh-TW").await;
let renewed_tw = f.mutation(2, "changeGoal", json!({"kind":"control","sessionId":session,"goalId":idle_cn["id"],"revision":idle_cn["revision"],"action":"resume","approval":approval(&f,&session).await}), Uuid::new_v4()).await.unwrap();
let (mut socket, _) = listener.accept().await.unwrap();
let tw_request = request(&mut socket).await;
let tw_invocation = f.store.latest_turn_boundary(&session).await.unwrap().unwrap().invocation;
let tw_configuration = f.store.invocation_configuration(&tw_invocation).await.unwrap().unwrap();
let paused_tw = control(&f, &session, "pause").await;
response(&mut socket).await;
let idle_tw = loop {
let observed = watch.recv().await.unwrap();
if observed["current"]["status"] == "paused" && observed["current"]["pending"] == false {
break observed["current"].clone();
}
};
let completed = control(&f, &session, "complete").await;
drop(watch);
f.app.shutdown().await.unwrap();
f.config.shutdown().await.unwrap();
for receipt in [english, chinese, traditional, created.clone(), paused, renewed_cn, paused_cn, renewed_tw, paused_tw, completed] {
assert_eq!(receipt["kind"], "committed", "{receipt}");
}
assert_eq!(retried, created, "retrying accepted Goal approval returns its exact original receipt");
assert_eq!(pending["pending"], true);
assert_eq!(after_retry, pending, "pending operation retry must not replace the approved Goal");
assert_eq!(user(&followup), user(&first), "the accepted iteration retains exact input after the setting changes");
assert!(system(&followup).contains(accepted.system_prompt.as_deref().unwrap()));
for request in [&first, &followup, &original_chain] {
assert!(system(request).contains(accepted.system_prompt.as_deref().unwrap()));
assert!(user(request).contains(objective));
assert!(user(request).contains("iteration"), "the approved English Arm keeps rendering English iterations: {request}");
}
assert_eq!(serde_json::to_value(first_configuration.language).unwrap(), "en");
assert_eq!(serde_json::to_value(chain_configuration.language).unwrap(), "en");
for (request, configuration, tag) in [
(&cn_request, &cn_configuration, "zh-CN"),
(&tw_request, &tw_configuration, "zh-TW"),
] {
assert!(system(request).contains(&configuration.system_prompt.as_ref().unwrap().text), "renewed captured guidance reaches the actual request: {request}");
assert!(user(request).contains("迭代"), "renewed Arm language reaches actual iteration input: {request}");
assert!(user(request).contains(objective));
assert_eq!(serde_json::to_value(configuration.language).unwrap(), tag);
}
for idle in [idle_english, idle_cn, idle_tw] {
assert_eq!(idle["status"], "paused");
assert_eq!(idle["pending"], false);
assert_eq!(idle["objective"], objective);
}
}).await.unwrap();
}