69fc3d8ca0
将 durable 取消、失败收口、审批决议和外部结果对账迁入 RuntimeService 把 Host 工具、MCP、Skill 与外部 backend 桥接拆为私有模块并保持公开 API 补充 Runtime 控制测试、依赖反向门禁和架构验收文档
155 lines
5.2 KiB
Rust
155 lines
5.2 KiB
Rust
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
|
|
|
|
use agent_runtime_core::{ApprovalDecision, Message, RunStatus};
|
|
use agent_runtime_sqlite::{RuntimeService, RuntimeServiceError, WorkerLease};
|
|
|
|
fn user(text: &str) -> Message {
|
|
Message::user(text).expect("valid user message")
|
|
}
|
|
|
|
fn wait_until_epoch_ms(target: i64) {
|
|
let deadline = Instant::now() + Duration::from_secs(2);
|
|
loop {
|
|
let now = SystemTime::now()
|
|
.duration_since(UNIX_EPOCH)
|
|
.expect("system clock")
|
|
.as_millis() as i64;
|
|
if now >= target {
|
|
return;
|
|
}
|
|
assert!(Instant::now() < deadline, "lease did not expire");
|
|
std::thread::sleep(Duration::from_millis(1));
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn queued_cancel_is_atomic_and_runtime_visible() {
|
|
let service = RuntimeService::in_memory().expect("runtime");
|
|
let handle = service.prepare_run("queued cancel").expect("prepare");
|
|
|
|
let cancelled = service.cancel_run(&handle.run_id).expect("cancel");
|
|
assert_eq!(cancelled.status, "cancelled");
|
|
assert_eq!(
|
|
service
|
|
.get_run(&handle.run_id)
|
|
.expect("run")
|
|
.unwrap()
|
|
.status,
|
|
"cancelled"
|
|
);
|
|
let snapshot = service
|
|
.load_runtime_snapshot(&handle.runtime_id)
|
|
.expect("snapshot")
|
|
.expect("runtime exists");
|
|
assert_eq!(
|
|
snapshot.run(&handle.run_id).expect("run snapshot").status(),
|
|
RunStatus::Cancelled
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn active_and_expired_cancel_stay_on_the_reconciliation_gate() {
|
|
let service = RuntimeService::in_memory().expect("runtime");
|
|
let active = service.prepare_run("active cancel").expect("prepare");
|
|
let active_lease = WorkerLease::new(&active.run_id);
|
|
service
|
|
.claim_run_with_lease(&active.run_id, &active_lease, Duration::from_secs(30))
|
|
.expect("claim active");
|
|
let requested = service.cancel_run(&active.run_id).expect("request cancel");
|
|
assert_eq!(requested.status, "cancel_requested");
|
|
assert_eq!(
|
|
service
|
|
.get_run(&active.run_id)
|
|
.expect("run")
|
|
.unwrap()
|
|
.status,
|
|
"cancel_requested"
|
|
);
|
|
|
|
// A short lease represents a worker that disappeared before its first
|
|
// heartbeat; cancellation must reconcile it instead of guessing safe.
|
|
let stale = service.prepare_run("stale cancel").expect("prepare");
|
|
let stale_lease = WorkerLease::new(&stale.run_id);
|
|
let (_, stale_record) = service
|
|
.claim_run_with_lease(&stale.run_id, &stale_lease, Duration::from_millis(1))
|
|
.expect("claim stale");
|
|
wait_until_epoch_ms(stale_record.lease_expires_at);
|
|
let reconciled = service.cancel_run(&stale.run_id).expect("reconcile cancel");
|
|
assert_eq!(reconciled.status, "reconciling");
|
|
let snapshot = service
|
|
.load_runtime_snapshot(&stale.runtime_id)
|
|
.expect("snapshot")
|
|
.expect("runtime exists");
|
|
assert_eq!(
|
|
snapshot.run(&stale.run_id).expect("run snapshot").status(),
|
|
RunStatus::Reconciling
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn fail_unclaimed_run_closes_run_runtime_and_session() {
|
|
let service = RuntimeService::in_memory().expect("runtime");
|
|
let handle = service.prepare_run("setup failure").expect("prepare");
|
|
|
|
let failed = service
|
|
.fail_unclaimed_run(&handle.run_id, "provider 配置失败")
|
|
.expect("fail run");
|
|
assert_eq!(failed.status, "failed");
|
|
assert_eq!(
|
|
service
|
|
.get_session(&handle.session_id)
|
|
.expect("session")
|
|
.unwrap()
|
|
.status,
|
|
"failed"
|
|
);
|
|
let snapshot = service
|
|
.load_runtime_snapshot(&handle.runtime_id)
|
|
.expect("snapshot")
|
|
.expect("runtime exists");
|
|
assert_eq!(
|
|
snapshot.run(&handle.run_id).expect("run snapshot").status(),
|
|
RunStatus::Failed
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn invalid_approval_and_reconciliation_leave_runtime_unchanged() {
|
|
let service = RuntimeService::in_memory().expect("runtime");
|
|
let handle = service
|
|
.prepare_run_with_messages("invalid control", vec![user("invalid control")])
|
|
.expect("prepare");
|
|
let before = service
|
|
.load_runtime_snapshot(&handle.runtime_id)
|
|
.expect("snapshot")
|
|
.expect("runtime exists");
|
|
|
|
let approval_error = service
|
|
.resolve_approval_decision("missing-approval", ApprovalDecision::Allow)
|
|
.expect_err("missing approval must fail");
|
|
assert!(!matches!(approval_error, RuntimeServiceError::Core(_)));
|
|
let reconcile_error = service
|
|
.reconcile_provider_result(&handle.run_id, "missing-provider-request", vec![user("x")])
|
|
.expect_err("missing checkpoint must fail");
|
|
assert!(matches!(
|
|
reconcile_error,
|
|
RuntimeServiceError::InvalidInput(_)
|
|
));
|
|
|
|
let after = service
|
|
.load_runtime_snapshot(&handle.runtime_id)
|
|
.expect("snapshot")
|
|
.expect("runtime exists");
|
|
assert_eq!(after.revision(), before.revision());
|
|
assert_eq!(
|
|
after.run(&handle.run_id).expect("run snapshot").status(),
|
|
before.run(&handle.run_id).expect("run snapshot").status()
|
|
);
|
|
assert!(
|
|
service
|
|
.get_approval("missing-approval")
|
|
.expect("approval lookup")
|
|
.is_none()
|
|
);
|
|
}
|