use super::*;
use super::permission_prompt_tests::{publish_low_danger, scripted_gate, store_and_conv};
use newt_core::interaction_gate::{authorize_action, mint_offer, now_tick, permission_registry};
use newt_core::{AnswerOutcome, Caveats, DenialKind, PermissionGate, PermissionRequest};
use std::cell::Cell;
use std::rc::Rc;
use std::sync::{Arc, Barrier};
fn request() -> PermissionRequest {
PermissionRequest {
tool: "run_command".into(),
kind: DenialKind::Exec,
target: "bash".into(),
reason: String::new(),
harness_bound: false,
}
}
fn offer(
audience: Audience,
) -> (
InteractionDefinition,
newt_interaction::InteractionInstance,
newt_interaction::Lifecycle,
) {
let definition = permission_definition(
&request(),
&danger::DangerTable::builtin(),
audience.clone(),
);
let (instance, lifecycle) =
mint_offer(&definition, "ws", "conv-1", &[audience], now_tick()).expect("mints");
(definition, instance, lifecycle)
}
#[test]
fn the_gate_timeout_is_shorter_than_the_store_ttl() {
let gate_nanos = i64::try_from(WEB_DECISION_TIMEOUT.as_nanos()).expect("fits");
let store_ttl = newt_core::ConversationStore::PERMISSION_REQUEST_TTL_NANOS;
assert!(
gate_nanos < store_ttl,
"the gate must give up while the offer is still answerable: \
gate {gate_nanos}ns vs store TTL {store_ttl}ns"
);
let (_d, instance, _l) = offer(Audience::Terminal);
assert_eq!(instance.ttl_ticks, store_ttl);
}
#[test]
fn an_expired_permission_denies_by_default() {
let (definition, instance, lifecycle) = offer(Audience::Terminal);
assert!(!newt_interaction::Lifecycle::has_elapsed(
&instance,
instance.provenance.minted_tick
));
assert!(newt_interaction::Lifecycle::has_elapsed(
&instance,
instance.provenance.minted_tick + instance.ttl_ticks
));
let expired = lifecycle.expire().expect("expires");
let registry = permission_registry(Audience::Terminal);
for action in [PromptChoice::AllowOnce, PromptChoice::AllowSession] {
assert!(
authorize_action(
&definition,
&instance,
&expired,
"ws",
®istry,
action,
Audience::Terminal
)
.is_err(),
"an expired offer authorized {action:?}"
);
}
}
#[test]
fn the_registry_is_independent_of_the_form() {
let (definition, instance, lifecycle) = offer(Audience::Terminal);
assert!(authorize_action(
&definition,
&instance,
&lifecycle,
"ws",
&[],
PromptChoice::AllowOnce,
Audience::Terminal
)
.is_err());
let low = PermissionRequest {
tool: "http".into(),
kind: DenialKind::Net,
target: "https://example.com/api".into(),
reason: String::new(),
harness_bound: false,
};
let terminal_form =
permission_definition(&low, &danger::DangerTable::builtin(), Audience::Terminal);
let (inst, life) =
mint_offer(&terminal_form, "ws", "conv-1", &[Audience::Web], now_tick()).expect("mints");
assert!(
authorize_action(
&terminal_form,
&inst,
&life,
"ws",
&permission_registry(Audience::Web),
PromptChoice::AllowPermanent,
Audience::Web
)
.is_err(),
"the web authorized a durable grant"
);
}
#[test]
fn a_foreign_workspace_cannot_authorize() {
let (definition, instance, lifecycle) = offer(Audience::Terminal);
let registry = permission_registry(Audience::Terminal);
assert!(authorize_action(
&definition,
&instance,
&lifecycle,
"ws",
®istry,
PromptChoice::AllowOnce,
Audience::Terminal
)
.is_ok());
assert!(
authorize_action(
&definition,
&instance,
&lifecycle,
"ws-elsewhere",
®istry,
PromptChoice::AllowOnce,
Audience::Terminal
)
.is_err(),
"a foreign workspace key authorized a decision"
);
}
#[test]
fn the_default_terminal_path_performs_no_store_write() {
let (_r, _w, store, conv) = store_and_conv();
let prompts = Rc::new(Cell::new(0));
let mut state = PermissionPromptState::default();
{
let mut gate = scripted_gate(
&mut state,
Caveats::default(),
None,
None,
vec![PromptChoice::AllowOnce],
Rc::clone(&prompts),
);
gate.conversation_id = conv.clone();
let _ = gate.ask(&[request()]);
}
assert_eq!(prompts.get(), 1, "the terminal prompt did not run");
assert!(
store.pending_interaction_offer(&conv).unwrap().is_none(),
"the DEFAULT terminal path published to the store"
);
publish_low_danger(&store, &conv);
assert!(
store.pending_interaction_offer(&conv).unwrap().is_some(),
"the pending read cannot see a published offer, so the check above is vacuous"
);
}
#[test]
fn a_permission_resolves_exactly_once_through_the_controller() {
let (_r, _w, store, conv) = store_and_conv();
let request_id = publish_low_danger(&store, &conv);
assert_eq!(
store
.answer_interaction_offer(&conv, &request_id, PromptChoice::AllowOnce, Audience::Web)
.unwrap(),
AnswerOutcome::Answered
);
assert_eq!(
store
.answer_interaction_offer(&conv, &request_id, PromptChoice::Deny, Audience::Web)
.unwrap(),
AnswerOutcome::AlreadyResolved
);
for _ in 0..3 {
assert_eq!(
store.take_interaction_decision(&conv, &request_id).unwrap(),
Some(PromptChoice::AllowOnce)
);
}
assert!(store.pending_interaction_offer(&conv).unwrap().is_none());
}
#[test]
fn an_undisplayed_action_is_refused_by_the_controller() {
let (_r, _w, store, conv) = store_and_conv();
let request_id = publish_low_danger(&store, &conv);
assert_eq!(
store
.answer_interaction_offer(
&conv,
&request_id,
PromptChoice::AllowSession,
Audience::Web
)
.unwrap(),
AnswerOutcome::InvalidAction
);
assert_eq!(
store
.answer_interaction_offer(&conv, &request_id, PromptChoice::AllowOnce, Audience::Web)
.unwrap(),
AnswerOutcome::Answered
);
}
#[test]
fn separate_connections_racing_one_permission_resolve_once() {
let root = tempfile::tempdir().unwrap();
let ws = tempfile::tempdir().unwrap();
let seed = newt_core::ConversationStore::new(root.path(), ws.path(), 100).unwrap();
let conv = seed.create("s", None).unwrap();
let request_id = publish_low_danger(&seed, &conv);
drop(seed);
let barrier = Arc::new(Barrier::new(3));
let mut workers = Vec::new();
for action in [PromptChoice::AllowOnce, PromptChoice::Deny] {
let root_path = root.path().to_path_buf();
let ws_path = ws.path().to_path_buf();
let conv = conv.clone();
let request_id = request_id.clone();
let barrier = Arc::clone(&barrier);
workers.push(std::thread::spawn(move || {
let store = newt_core::ConversationStore::new(root_path, ws_path, 100).unwrap();
barrier.wait();
(
action,
store.answer_interaction_offer(&conv, &request_id, action, Audience::Web),
)
}));
}
barrier.wait();
let outcomes: Vec<_> = workers.into_iter().map(|w| w.join().unwrap()).collect();
let winners: Vec<PromptChoice> = outcomes
.iter()
.filter(|(_, r)| matches!(r, Ok(AnswerOutcome::Answered)))
.map(|(a, _)| *a)
.collect();
assert_eq!(
winners.len(),
1,
"exactly one connection must win; got {outcomes:#?}"
);
assert!(
outcomes
.iter()
.any(|(_, r)| matches!(r, Ok(AnswerOutcome::AlreadyResolved))),
"the loser must be told it lost, not silently succeed: {outcomes:#?}"
);
let reopened = newt_core::ConversationStore::new(root.path(), ws.path(), 100).unwrap();
let observed: PromptChoice = reopened
.take_interaction_decision(&conv, &request_id)
.unwrap()
.expect("a verdict was recorded");
assert_eq!(
observed, winners[0],
"the recorded verdict is not the winner's"
);
}
#[test]
fn the_loser_observes_the_winners_verdict() {
let (_r, _w, store, conv) = store_and_conv();
let request_id = publish_low_danger(&store, &conv);
assert_eq!(
store
.answer_interaction_offer(&conv, &request_id, PromptChoice::AllowOnce, Audience::Web)
.unwrap(),
AnswerOutcome::Answered
);
assert_eq!(
store
.answer_interaction_offer(&conv, &request_id, PromptChoice::Deny, Audience::Web)
.unwrap(),
AnswerOutcome::AlreadyResolved
);
let observed: PromptChoice = store
.take_interaction_decision(&conv, &request_id)
.unwrap()
.expect("a verdict");
assert_eq!(observed, PromptChoice::AllowOnce);
}