use super::*;
#[test]
fn durable_terminal_receipt_uses_the_admitted_tenant() {
let request_id = "durable-tenant-receipt";
let (kernel, request, store, invocations) = durable_admission_fixture(request_id);
let session_id = kernel
.open_session(
request.capability.subject.to_hex(),
vec![request.capability.clone()],
)
.expect("tenant session");
kernel
.set_session_auth_context(
&session_id,
oauth_auth_with_enterprise_tenant("tenant-durable"),
)
.expect("tenant authentication");
kernel
.activate_session(&session_id)
.expect("active tenant session");
let response = kernel
.evaluate_tool_call_sync_with_session_context(&request, None, None, Some(&session_id))
.expect("tenant-scoped durable dispatch");
assert_eq!(
response.receipt.tenant_id.as_deref(),
Some("tenant-durable")
);
assert!(response.receipt.verify_signature().expect("signed receipt"));
assert_eq!(
store
.operation()
.binding()
.to_persisted()
.authenticated_tenant_id
.as_str(),
"tenant-durable"
);
let replay = kernel
.evaluate_tool_call_sync_with_session_context(&request, None, None, Some(&session_id))
.expect("tenant-scoped durable replay");
assert_eq!(replay.receipt.id, response.receipt.id);
assert_eq!(replay.receipt.tenant_id.as_deref(), Some("tenant-durable"));
assert_eq!(invocations.load(Ordering::SeqCst), 1);
}
#[test]
fn startup_recovery_rejects_a_changed_post_return_plan() {
let (mut kernel, request, store, invocations) =
durable_admission_fixture("durable-startup-post-hook-change");
kernel.add_post_invocation_hook(Box::new(StableRedactingPostInvocationHook {
replacement: "first",
}));
store.fail_next_evaluation_begin();
kernel
.evaluate_tool_call_blocking(&request)
.expect_err("injected finalization crash");
assert_eq!(
store.operation().state(),
AdmissionOperationState::Finalizing
);
let rotated_fence = StoreMutationFence {
store_uuid: admission_test_fence().store_uuid,
lease_id: "test-admission-lease-2".to_owned(),
owner_epoch: 2,
};
store.rotate_fence(rotated_fence.clone());
let mut recovered_config = make_config();
recovered_config.keypair = kernel.config.keypair.clone();
recovered_config.policy_hash = sha256_hex(b"durable-admission-test-policy");
let mut recovered_kernel = make_kernel(recovered_config);
recovered_kernel
.set_durable_admission_store(store.clone(), store.clone(), rotated_fence)
.expect("qualified admission store");
recovered_kernel.add_post_invocation_hook(Box::new(StableRedactingPostInvocationHook {
replacement: "second",
}));
let error = recovered_kernel
.reconcile_recoverable_admissions()
.expect_err("changed recovery plan must fail closed");
assert!(error
.to_string()
.contains("recovered post-return plan does not match durable admission"));
assert_eq!(
store.operation().state(),
AdmissionOperationState::Finalizing
);
assert_eq!(invocations.load(Ordering::SeqCst), 1);
}
#[test]
fn an_unrelated_cumulative_grant_does_not_withdraw_an_exempt_admission() {
struct MixedEffectServer;
#[async_trait::async_trait]
impl ToolServerConnection for MixedEffectServer {
fn server_id(&self) -> &str {
"mixed-effect-server"
}
fn tool_names(&self) -> Vec<String> {
vec![
"lookup".to_owned(),
"write".to_owned(),
"escrow".to_owned(),
"audit".to_owned(),
]
}
fn tool_is_read_only(&self, tool_name: &str) -> bool {
tool_name == "lookup" || tool_name == "audit"
}
async fn invoke(
&self,
_tool_name: &str,
_arguments: serde_json::Value,
_nested_flow_bridge: Option<&mut dyn NestedFlowBridge>,
) -> Result<serde_json::Value, KernelError> {
Ok(serde_json::json!({"ok": true}))
}
}
fn cumulative_grant(tool: &str, budget_id: &str) -> ToolGrant {
let mut grant = make_grant("mixed-effect-server", tool);
grant
.constraints
.push(Constraint::RequireCumulativeApprovalAbove {
threshold: MonetaryAmount {
units: 100,
currency: "USD".to_owned(),
},
approval_budget_id: budget_id.to_owned(),
approval_budget_epoch: 1,
cumulative_approval_root_binding: None,
});
grant
}
let mut kernel = make_kernel(make_config());
kernel.register_tool_server(Box::new(MixedEffectServer));
let agent = make_keypair();
let capability = make_capability(
&kernel,
&agent,
make_scope(vec![
make_grant("mixed-effect-server", "lookup"),
make_grant("mixed-effect-server", "write"),
cumulative_grant("escrow", "budget-escrow"),
cumulative_grant("audit", "budget-audit"),
]),
300,
);
let admission_is_exempt = |tool: &str| -> Result<bool, KernelError> {
let request = make_request(
&format!("durable-unrelated-cumulative-{tool}"),
&capability,
tool,
"mixed-effect-server",
);
let matching = resolve_required_matching_grants(
&capability,
&request.tool_name,
&request.server_id,
&request.arguments,
request.model_metadata.as_ref(),
)
.expect("matching grant");
kernel
.begin_durable_tool_admission(&request, &matching, current_unix_timestamp_ms())
.map(|admission| admission.is_none())
};
assert!(admission_is_exempt("lookup").expect("read-only admission"));
assert!(admission_is_exempt("write").expect("side-effecting admission"));
let uncovered = admission_is_exempt("audit")
.expect_err("cumulative read-only tool must not stay exempt");
assert!(
uncovered
.to_string()
.contains("requires durable admission coverage"),
"unexpected coverage denial: {uncovered}"
);
let unstored =
admission_is_exempt("escrow").expect_err("cumulative tool must require a durable store");
assert!(
unstored
.to_string()
.contains("no qualified admission operation store is configured"),
"unexpected store denial: {unstored}"
);
}
#[test]
fn a_missing_nested_session_root_compensates_the_durable_admission() {
let (kernel, request, store, invocations) =
durable_admission_fixture("durable-nested-missing-session-root");
let parent_context = make_operation_context(
&SessionId::new("sess-durable-nested-missing-root"),
"req-parent-durable-missing-root",
&request.agent_id,
);
let mut client = NoopNestedFlowClient;
let response = kernel
.evaluate_tool_call_with_nested_flow_client(&parent_context, &request, &mut client, None)
.expect("missing session roots must fail closed as a deny, not propagate");
assert_eq!(response.verdict, Verdict::Deny);
assert!(
response
.reason
.as_deref()
.is_some_and(|reason| reason.contains("unknown session")),
"deny must come from the session roots lookup, got {:?}",
response.reason
);
assert_eq!(
store.operation().state(),
AdmissionOperationState::CompensatedBeforeDispatch
);
assert_eq!(invocations.load(Ordering::SeqCst), 0);
}