fix: preserve invocation recovery across rewind and revalidate replay authority

This commit is contained in:
2026-10-05 20:14:19 +09:00
parent 31f7847d1f
commit 1a45542842
8 changed files with 409 additions and 21 deletions
+33 -1
View File
@@ -26,6 +26,7 @@ struct Trace {
calls: Arc<Mutex<Vec<String>>>,
requests: Arc<Mutex<Vec<Request>>>,
available: Arc<AtomicBool>,
argument_allowed: Arc<AtomicBool>,
}
impl Default for Trace {
@@ -34,6 +35,7 @@ impl Default for Trace {
calls: Arc::default(),
requests: Arc::default(),
available: Arc::new(AtomicBool::new(true)),
argument_allowed: Arc::new(AtomicBool::new(true)),
}
}
}
@@ -104,7 +106,10 @@ impl FeatureInvocationHandler for FakeFeature {
&self,
invocation: &FeatureInvocation,
) -> Result<(), FeatureInvocationHandlerError> {
if invocation.arguments[0].value == InvocationValue::String("denied".into()) {
if invocation.arguments[0].value == InvocationValue::String("denied".into())
|| (invocation.arguments[0].value == InvocationValue::String("first".into())
&& !self.0.argument_allowed.load(Ordering::SeqCst))
{
return Err(FeatureInvocationHandlerError::failed(
"host permission denied",
));
@@ -349,6 +354,33 @@ async fn feature_replay_reuses_result_and_rejects_id_payload_changes() {
);
}
#[tokio::test]
async fn feature_argument_permission_revocation_fences_success_replay_and_resume() {
let trace = Trace::default();
let (_dir, mut worker) = fixture(trace.clone()).await;
worker.run(vec![invoke("same", "first")]).await.unwrap();
trace.argument_allowed.store(false, Ordering::SeqCst);
// The feature remains available: only this argument's authority was revoked.
assert_eq!(worker.feature_invocations().descriptors().len(), 1);
assert!(worker.resume().await.is_err());
assert!(
worker
.run_for_notification(protocol::InvokeKind::Notify)
.await
.is_err()
);
assert!(worker.run(vec![invoke("same", "first")]).await.is_err());
let replay = results(&worker).pop().unwrap();
assert_eq!(replay.status, FeatureInvocationStatus::Failed);
assert!(replay.context.is_none());
assert!(replay.message.contains("host permission denied"));
assert_eq!(*trace.calls.lock().unwrap(), ["first", "llm"]);
// Reauthorization allows context reuse, never another business execution.
trace.argument_allowed.store(true, Ordering::SeqCst);
worker.run(vec![invoke("same", "first")]).await.unwrap();
assert_eq!(*trace.calls.lock().unwrap(), ["first", "llm", "llm"]);
}
#[tokio::test]
async fn feature_success_replay_does_not_resurrect_disabled_installed_capability() {
let trace = Trace::default();