use super::*;
use newt_core::agentic::{PlanEntry, PromptDisposition};
#[test]
fn act_disposition_is_always_model_during_act() {
for mode in [
OperatingMode::Chat,
OperatingMode::Dev,
OperatingMode::Admin,
OperatingMode::Plan,
OperatingMode::Diagnose,
OperatingMode::Auto,
OperatingMode::FullAuto,
] {
assert_eq!(
plan_entry_for_turn(PromptDisposition::Act, mode),
PlanEntry::ModelDuringAct,
"an Act-disposition turn is always ModelDuringAct, regardless of {mode:?}"
);
}
}
#[test]
fn plan_disposition_under_mode_plan_is_operator_selected() {
assert_eq!(
plan_entry_for_turn(PromptDisposition::Plan, OperatingMode::Plan),
PlanEntry::OperatorSelected
);
}
#[test]
fn plan_disposition_under_any_other_mode_is_intake_inferred() {
for mode in [
OperatingMode::Chat,
OperatingMode::Dev,
OperatingMode::Admin,
OperatingMode::Diagnose,
OperatingMode::Auto,
OperatingMode::FullAuto,
] {
assert_eq!(
plan_entry_for_turn(PromptDisposition::Plan, mode),
PlanEntry::IntakeInferred,
"Plan disposition under {mode:?} (not /mode plan) is intake-inferred"
);
}
}
#[test]
fn unexpected_dispositions_fail_safe_to_intake_inferred() {
for disposition in [
PromptDisposition::Ask,
PromptDisposition::Explain,
PromptDisposition::Research,
] {
assert_eq!(
plan_entry_for_turn(disposition, OperatingMode::Chat),
PlanEntry::IntakeInferred,
"{disposition:?} must fail safe, never ModelDuringAct"
);
}
}
#[test]
fn a_plan_turn_that_presented_a_draft_is_asked_even_without_the_model_flag() {
assert!(plan_approval_due(
PromptDisposition::Plan,
false,
false,
true,
false
));
}
#[test]
fn a_plan_turn_with_no_draft_is_not_asked() {
assert!(!plan_approval_due(
PromptDisposition::Plan,
false,
false,
false,
false
));
}
#[test]
fn model_entered_plan_with_a_draft_is_asked_and_a_plain_act_turn_is_not() {
assert!(plan_approval_due(
PromptDisposition::Act,
true,
false,
true,
false
));
assert!(!plan_approval_due(
PromptDisposition::Act,
false,
false,
false,
false
));
}
#[test]
fn an_exit_request_is_always_asked() {
assert!(plan_approval_due(
PromptDisposition::Act,
false,
true,
false,
false
));
assert!(plan_approval_due(
PromptDisposition::Plan,
false,
true,
false,
false
));
}
#[test]
fn a_non_plan_disposition_is_not_asked_without_an_exit_request() {
for disposition in [
PromptDisposition::Ask,
PromptDisposition::Explain,
PromptDisposition::Research,
] {
assert!(
!plan_approval_due(disposition, false, false, true, false),
"{disposition:?} is not a plan turn"
);
}
}
#[test]
fn a_plan_turn_that_set_a_ledger_plan_is_asked_without_a_draft() {
assert!(plan_approval_due(
PromptDisposition::Plan,
false,
false,
false,
true,
));
assert!(!plan_approval_due(
PromptDisposition::Act,
false,
false,
false,
true,
));
}
#[test]
fn selected_target_approval_seeds_the_ledger_plan_when_no_draft_was_presented() {
let states = ConversationModeStates::default();
let a = objective("refactor the parser");
states.plan.set_plan_mode(true).unwrap();
let mut gate = ScriptedGate::new([answer("y")]);
let block = "<plan>\nSelected source (agent data): \"packages/parser/nested/root.py\"\n✓ 1. inspect packages/parser/nested/root.py\n→ 2. repair packages/parser/nested/root.py\n</plan>";
let effects = run_plan_approval(
Some(&mut gate),
PlanEntry::ModelDuringAct,
&states.plan,
&states.plan_draft,
Some(&a),
PlanApprovalSeed {
exit_guidance: "GUIDANCE",
ledger_plan: Some(block),
},
false,
);
assert!(!states.plan.is_plan_mode(), "clamp lifted");
let (input, _) = effects.queued.expect("one continuation").into_input();
let ReadOutcome::Line(text) = input else {
panic!("a line of input")
};
assert!(text.contains("approved the plan below"), "{text}");
assert!(text.contains(block), "{text}");
}
#[test]
fn full_auto_approves_without_asking() {
let states = ConversationModeStates::default();
let a = objective("A");
presented_plan(&states, &a, "# plan A");
let mut gate = ScriptedGate::new([]);
let effects = run_plan_approval(
Some(&mut gate),
PlanEntry::ModelDuringAct,
&states.plan,
&states.plan_draft,
Some(&a),
PlanApprovalSeed {
exit_guidance: "GUIDANCE",
ledger_plan: None,
},
true,
);
assert!(gate.asked.is_empty(), "no question under full-auto");
assert!(!states.plan.is_plan_mode(), "clamp lifted");
assert!(effects.queued.is_some());
assert!(
effects.notice.contains("auto-approved"),
"{}",
effects.notice
);
}
#[test]
fn approval_seed_text_carries_the_plan_when_present_and_the_guidance_always() {
let with_plan = plan_approval_seed_text(Some("# Refactor\n1. step"), "GUIDANCE");
assert!(
with_plan.starts_with(newt_core::agentic::PLAN_APPROVAL_PREFIX),
"{with_plan}"
);
assert!(with_plan.contains("Implement it now"), "{with_plan}");
assert!(with_plan.contains("# Refactor\n1. step"), "{with_plan}");
assert!(with_plan.contains("GUIDANCE"), "{with_plan}");
let without = plan_approval_seed_text(None, "GUIDANCE");
assert!(without.contains("Continue the task"), "{without}");
assert!(!without.contains("Implement it now"), "{without}");
assert!(without.contains("GUIDANCE"), "{without}");
}
#[test]
fn approval_intake_resumes_as_act_even_when_the_text_quotes_a_plan() {
let lexicon = newt_core::agentic::DispositionLexicon::default();
let text = plan_approval_seed_text(
Some("# Plan\n1. Plan the refactor of the parser\n2. Outline the steps"),
"guidance",
);
assert_eq!(
plan_approval_intake(&text, &lexicon).disposition(),
PromptDisposition::Act
);
}
#[test]
fn approval_intake_resumes_as_act_even_when_the_text_opens_collaboratively() {
let lexicon = newt_core::agentic::DispositionLexicon::default();
let text = "let's refactor the parser";
assert_eq!(
newt_core::agentic::PromptIntake::analyze_with(text, &lexicon).disposition(),
PromptDisposition::Plan,
"control: on its own this wording reads as Plan"
);
assert_eq!(
plan_approval_intake(text, &lexicon).disposition(),
PromptDisposition::Act
);
}
use newt_core::agentic::{
HumanQuestionOutcome, PermissionDecision, PermissionGate, PermissionRequest,
PlanDraftSink as _, PlanModeControl as _,
};
use newt_core::Caveats;
struct ScriptedGate {
outcomes: Vec<HumanQuestionOutcome>,
asked: Vec<String>,
}
impl ScriptedGate {
fn new(outcomes: impl IntoIterator<Item = HumanQuestionOutcome>) -> Self {
let mut outcomes: Vec<_> = outcomes.into_iter().collect();
outcomes.reverse();
Self {
outcomes,
asked: Vec::new(),
}
}
}
impl PermissionGate for ScriptedGate {
fn ask(&mut self, _: &[PermissionRequest]) -> PermissionDecision {
panic!("plan approval must never request a permission grant");
}
fn refresh_caveats(&mut self, _: &Caveats) -> PermissionDecision {
panic!("plan approval must never re-mint caveats");
}
fn ask_question(&mut self, question: &str) -> HumanQuestionOutcome {
self.asked.push(question.to_string());
self.outcomes
.pop()
.expect("a scripted outcome per question")
}
fn ask_choice(
&mut self,
question: &str,
options: &[newt_core::agentic::ChoiceSpec],
) -> HumanQuestionOutcome {
assert_eq!(
options.iter().map(|o| o.id).collect::<Vec<_>>(),
vec!["yes", "no", "discuss"],
"the plan question offers exactly its three answers, yes first"
);
self.ask_question(question)
}
}
#[test]
fn discuss_opens_one_follow_up_and_feeds_its_text_back() {
let states = ConversationModeStates::default();
let a = objective("A");
presented_plan(&states, &a, "# plan A");
let mut gate = ScriptedGate::new([answer("discuss"), answer("split transport first")]);
let effects = approve(&mut gate, &states, PlanEntry::ModelDuringAct, Some(&a));
assert_eq!(gate.asked.len(), 2, "{:?}", gate.asked);
assert!(
gate.asked[1].starts_with("What should change"),
"{:?}",
gate.asked
);
assert!(states.plan.is_plan_mode(), "the clamp stays");
let (input, origin) = effects.queued.expect("the feedback runs next").into_input();
let ReadOutcome::Line(text) = input else {
panic!("a line of input")
};
assert_eq!(text, "split transport first");
assert!(matches!(
origin,
ModelInputOrigin::OperatorContinuation { .. }
));
}
fn answer(text: &str) -> HumanQuestionOutcome {
HumanQuestionOutcome::Answer(text.to_string())
}
fn objective(text: &str) -> newt_core::TurnPromptContext {
newt_core::TurnPromptContext::ephemeral_operator("conv-1", text, text)
}
fn presented_plan(
states: &ConversationModeStates,
objective: &newt_core::TurnPromptContext,
markdown: &str,
) -> newt_core::agentic::PresentedPlan {
states.plan.set_plan_mode(true).unwrap();
let noop = |_: &std::path::Path, _: &str| Ok(());
states
.plan_draft
.bind("conv-1", plan_objective(Some(objective)), &noop)
.save_draft(markdown.to_string())
.unwrap();
states
.plan_draft
.take_for_presentation(plan_objective(Some(objective)).unwrap())
.expect("presented")
}
fn approve(
gate: &mut ScriptedGate,
states: &ConversationModeStates,
entry: PlanEntry,
parent: Option<&newt_core::TurnPromptContext>,
) -> PlanApprovalEffects {
run_plan_approval(
Some(gate),
entry,
&states.plan,
&states.plan_draft,
parent,
PlanApprovalSeed {
exit_guidance: "GUIDANCE",
ledger_plan: None,
},
false,
)
}
#[test]
fn approval_queues_exactly_one_harness_turn_with_the_presented_plan() {
let states = ConversationModeStates::default();
let a = objective("refactor the parser");
let shown = presented_plan(&states, &a, "# plan A");
let mut gate = ScriptedGate::new([answer("y")]);
let effects = approve(&mut gate, &states, PlanEntry::ModelDuringAct, Some(&a));
assert_eq!(gate.asked, vec!["Approve this plan? [Y/n/discuss] "]);
assert!(!states.plan.is_plan_mode(), "clamp lifted");
assert!(!effects.switch_to_dev);
let (input, origin) = effects.queued.expect("one continuation").into_input();
let ReadOutcome::Line(text) = input else {
panic!("a line of input")
};
assert!(text.starts_with(newt_core::agentic::PLAN_APPROVAL_PREFIX));
assert!(text.contains(&shown.draft.markdown), "{text}");
assert!(text.contains("GUIDANCE"));
match origin {
ModelInputOrigin::HarnessPlanApproval { parent } => {
assert_eq!(
parent.active().root_prompt_id(),
a.active().root_prompt_id()
);
}
other => panic!("harness origin expected, got {other:?}"),
}
assert!(
states
.plan_draft
.take_approved(a.active().root_prompt_id())
.is_none(),
"the snapshot is consumed by the one approval"
);
}
#[test]
fn every_non_approval_outcome_queues_nothing_and_keeps_the_clamp() {
let outcomes = [
answer("n"),
HumanQuestionOutcome::InputFailed,
HumanQuestionOutcome::Cancelled,
HumanQuestionOutcome::ExitRequested,
HumanQuestionOutcome::InputClosed,
HumanQuestionOutcome::Unavailable,
];
for outcome in outcomes {
let states = ConversationModeStates::default();
let a = objective("A");
presented_plan(&states, &a, "# plan A");
let mut gate = ScriptedGate::new([outcome.clone()]);
let effects = approve(&mut gate, &states, PlanEntry::ModelDuringAct, Some(&a));
assert!(effects.queued.is_none(), "{outcome:?} queued a turn");
assert!(states.plan.is_plan_mode(), "{outcome:?} lifted the clamp");
assert!(!effects.switch_to_dev);
assert!(
states
.plan_draft
.take_approved(a.active().root_prompt_id())
.is_some(),
"{outcome:?} must leave the snapshot for a later approval"
);
}
let states = ConversationModeStates::default();
let a = objective("A");
presented_plan(&states, &a, "# plan A");
let effects = run_plan_approval(
None,
PlanEntry::ModelDuringAct,
&states.plan,
&states.plan_draft,
Some(&a),
PlanApprovalSeed {
exit_guidance: "GUIDANCE",
ledger_plan: None,
},
false,
);
assert!(effects.queued.is_none());
assert!(states.plan.is_plan_mode());
}
#[test]
fn discussion_preserves_lineage_clamp_and_snapshot_then_approval_seeds_it() {
let states = ConversationModeStates::default();
let a = objective("A");
let shown = presented_plan(&states, &a, "# plan A");
let mut gate = ScriptedGate::new([answer("why step 2?"), answer("yes")]);
let effects = approve(&mut gate, &states, PlanEntry::ModelDuringAct, Some(&a));
assert!(states.plan.is_plan_mode(), "discussion keeps the clamp");
let (input, origin) = effects.queued.expect("the discussion turn").into_input();
assert!(matches!(input, ReadOutcome::Line(ref t) if t == "why step 2?"));
let ModelInputOrigin::OperatorContinuation { parent } = origin else {
panic!("operator-authored text keeps an operator origin")
};
assert_eq!(
parent.active().root_prompt_id(),
a.active().root_prompt_id()
);
let discuss =
newt_core::TurnPromptContext::ephemeral_operator_continuation("conv-1", "d", "d", &a)
.unwrap();
assert!(states
.plan_draft
.take_for_presentation(discuss.active().root_prompt_id())
.is_none());
let effects = approve(
&mut gate,
&states,
PlanEntry::ModelDuringAct,
Some(&discuss),
);
let (input, _) = effects
.queued
.expect("the implementation turn")
.into_input();
let ReadOutcome::Line(text) = input else {
panic!()
};
assert!(text.contains(&shown.draft.markdown), "{text}");
assert!(!states.plan.is_plan_mode());
}
#[test]
fn approval_under_objective_b_never_seeds_objective_a_plan() {
let states = ConversationModeStates::default();
let a = objective("A");
presented_plan(&states, &a, "# plan A");
let b = objective("B");
states.plan.set_plan_mode(true).unwrap();
states.plan.request_exit().unwrap();
assert!(states
.plan_draft
.take_for_presentation(b.active().root_prompt_id())
.is_none());
let mut gate = ScriptedGate::new([answer("y")]);
let effects = approve(&mut gate, &states, PlanEntry::ModelDuringAct, Some(&b));
let (input, origin) = effects.queued.expect("B's continuation").into_input();
let ReadOutcome::Line(text) = input else {
panic!()
};
assert!(!text.contains("# plan A"), "A leaked into B: {text}");
assert!(text.contains("Continue the task"), "{text}");
let ModelInputOrigin::HarnessPlanApproval { parent } = origin else {
panic!()
};
assert_eq!(
parent.active().root_prompt_id(),
b.active().root_prompt_id()
);
assert!(
states
.plan_draft
.take_approved(a.active().root_prompt_id())
.is_some(),
"A's snapshot is untouched by B's approval"
);
}
#[test]
fn a_repeated_exit_cannot_consume_the_approval_twice() {
let states = ConversationModeStates::default();
let a = objective("A");
presented_plan(&states, &a, "# plan A");
let mut gate = ScriptedGate::new([answer("y"), answer("y")]);
let first = approve(&mut gate, &states, PlanEntry::ModelDuringAct, Some(&a));
let (ReadOutcome::Line(first_text), _) = first.queued.unwrap().into_input() else {
panic!()
};
assert!(first_text.contains("# plan A"));
states.plan.set_plan_mode(true).unwrap();
states.plan.request_exit().unwrap();
let second = approve(&mut gate, &states, PlanEntry::ModelDuringAct, Some(&a));
let (ReadOutcome::Line(second_text), _) = second.queued.unwrap().into_input() else {
panic!()
};
assert!(
!second_text.contains("# plan A"),
"the approved snapshot must not be replayed: {second_text}"
);
}
#[test]
fn operator_selected_approval_asks_about_the_mode_switch_and_requests_it() {
let states = ConversationModeStates::default();
let a = objective("A");
presented_plan(&states, &a, "# plan A");
let mut gate = ScriptedGate::new([answer("y")]);
let effects = approve(&mut gate, &states, PlanEntry::OperatorSelected, Some(&a));
assert_eq!(
gate.asked,
vec!["Approve this plan and switch to /mode dev? [Y/n/discuss] "]
);
assert!(effects.switch_to_dev);
assert!(effects.queued.is_some());
}
#[test]
fn approval_without_a_receipt_queues_nothing() {
let states = ConversationModeStates::default();
states.plan.set_plan_mode(true).unwrap();
let mut gate = ScriptedGate::new([answer("y")]);
let effects = approve(&mut gate, &states, PlanEntry::ModelDuringAct, None);
assert!(effects.queued.is_none());
assert!(!states.plan.is_plan_mode());
assert!(effects.notice.contains("Send a message"));
}
#[test]
fn exit_plan_mode_only_requests_and_the_request_is_read_once() {
let states = ConversationModeStates::default();
assert!(!states.plan.exit_requested());
states.plan.set_plan_mode(true).unwrap();
states.plan.request_exit().unwrap();
assert!(
states.plan.is_plan_mode(),
"request does not lift the clamp"
);
assert!(states.plan.exit_requested());
assert!(
states.plan.exit_requested(),
"the loop only observes the request"
);
assert!(states.plan.take_exit_requested());
assert!(!states.plan.exit_requested());
assert!(!states.plan.take_exit_requested(), "consumed once");
assert!(states.plan.is_plan_mode());
}
#[test]
fn boundary_clear_leaves_no_snapshot_for_a_later_approval() {
let states = ConversationModeStates::default();
let a = objective("A");
presented_plan(&states, &a, "# plan A");
states.clear();
assert!(!states.plan.is_plan_mode());
assert!(states
.plan_draft
.take_approved(a.active().root_prompt_id())
.is_none());
let noop = |_: &std::path::Path, _: &str| Ok(());
assert!(states
.plan_draft
.bind("conv-1", plan_objective(Some(&a)), &noop)
.latest_draft()
.is_none());
}
#[test]
fn the_presented_snapshot_is_the_saved_markdown_byte_for_byte() {
let states = ConversationModeStates::default();
let a = objective("A");
let shown = presented_plan(&states, &a, "# Title\n\n1. step one\n");
assert_eq!(shown.draft.markdown, "# Title\n\n1. step one\n");
assert_eq!(shown.draft.revision, 1);
}
#[test]
fn discussion_2799_cannot_be_reinterpreted_as_approval_or_rejection() {
for entry in [
PlanEntry::ModelDuringAct,
PlanEntry::OperatorSelected,
PlanEntry::IntakeInferred,
] {
for feedback in ["yes", "continue", "go", "no", " yes\n"] {
let states = ConversationModeStates::default();
let parent = objective("A");
presented_plan(&states, &parent, "# plan A");
let mut gate = ScriptedGate::new([answer("discuss"), answer(feedback)]);
let effects = approve(&mut gate, &states, entry, Some(&parent));
assert!(
states.plan.is_plan_mode(),
"{entry:?}/{feedback:?}: clamp lifted"
);
assert!(!effects.switch_to_dev, "discussion cannot switch mode");
let (input, origin) = effects.queued.expect("verbatim feedback turn").into_input();
let ReadOutcome::Line(text) = input else {
panic!("feedback line")
};
assert_eq!(text, feedback);
assert!(matches!(
origin,
ModelInputOrigin::OperatorContinuation { .. }
));
assert!(
states
.plan_draft
.take_approved(parent.active().root_prompt_id())
.is_some(),
"discussion consumed approval snapshot"
);
assert_eq!(gate.asked.len(), 2);
}
}
}
#[test]
fn discussion_2799_unanswered_follow_up_retains_clamp_and_snapshot() {
for outcome in [
HumanQuestionOutcome::Cancelled,
HumanQuestionOutcome::Unavailable,
HumanQuestionOutcome::InputClosed,
HumanQuestionOutcome::ExitRequested,
answer(""),
] {
let states = ConversationModeStates::default();
let parent = objective("A");
presented_plan(&states, &parent, "# plan A");
let mut gate = ScriptedGate::new([answer("discuss"), outcome.clone()]);
let effects = approve(
&mut gate,
&states,
PlanEntry::OperatorSelected,
Some(&parent),
);
assert!(states.plan.is_plan_mode(), "{outcome:?}");
assert!(!effects.switch_to_dev);
assert!(effects.queued.is_none());
assert!(states
.plan_draft
.take_approved(parent.active().root_prompt_id())
.is_some());
assert_eq!(gate.asked.len(), 2);
}
}
#[test]
fn plan_default_yes_blank_approval_seeds_implementation() {
for entry in [
PlanEntry::IntakeInferred,
PlanEntry::ModelDuringAct,
PlanEntry::OperatorSelected,
] {
for text in ["", " "] {
let states = ConversationModeStates::default();
let parent = objective("A");
let shown = presented_plan(&states, &parent, "# approved default");
let mut gate = ScriptedGate::new([answer(text)]);
let effects = approve(&mut gate, &states, entry, Some(&parent));
assert!(!states.plan.is_plan_mode());
assert_eq!(effects.switch_to_dev, entry == PlanEntry::OperatorSelected);
assert!(gate.asked[0].contains("[Y/n/discuss]"));
let (input, origin) = effects.queued.expect("one implementing turn").into_input();
let ReadOutcome::Line(line) = input else {
panic!("implementation line")
};
assert!(line.contains(&shown.draft.markdown));
assert!(matches!(
origin,
ModelInputOrigin::HarnessPlanApproval { .. }
));
assert!(states
.plan_draft
.take_approved(parent.active().root_prompt_id())
.is_none());
}
}
}