use super::*;
#[test]
fn inferred_operating_style_does_not_narrow_accepted_intake() {
for style in [OperatingMode::Plan, OperatingMode::Diagnose] {
let mut recorded = RecordedDispositions::new();
let lexicon = newt_core::agentic::DispositionLexicon::default();
let (intake, selected) = comprehend_accepted_prompt(
&ModelInputOrigin::Operator,
"Implement the parser change.",
None,
None,
&mut recorded,
&lexicon,
|_| (style, OperatingMode::Auto),
);
assert_eq!(selected, style);
assert_eq!(
intake.disposition(),
newt_core::agentic::PromptDisposition::Act,
"an inferred {style:?} style must not narrow accepted task authority"
);
}
}
fn ctx() -> newt_core::TurnPromptContext {
newt_core::TurnPromptContext::ephemeral_operator(
"conv",
b"extract the module and open a PR".to_vec(),
b"extract the module and open a PR".to_vec(),
)
}
#[test]
fn bare_continue_after_round_cap_links_to_the_interrupted_objective() {
let parent = ctx();
let got = upgrade_origin_for_interrupted_objective(
ModelInputOrigin::Operator,
"continue",
Some(&parent),
None,
);
match got {
ModelInputOrigin::OperatorContinuation { parent: linked } => assert_eq!(
linked.submitted_prompt().id(),
parent.submitted_prompt().id(),
"the nudge must re-enter the interrupted objective's lineage"
),
other => panic!("bare continue must link, got {other:?}"),
}
}
#[test]
fn substantive_input_stays_fresh_even_with_an_interrupted_objective() {
let parent = ctx();
let got = upgrade_origin_for_interrupted_objective(
ModelInputOrigin::Operator,
"now refactor newt-tui/src/lib.rs instead and open a PR",
Some(&parent),
None,
);
assert!(
matches!(got, ModelInputOrigin::Operator),
"a new ask must never be silently chained to a stale objective"
);
}
#[test]
fn no_interrupted_objective_means_no_upgrade() {
let got = upgrade_origin_for_interrupted_objective(
ModelInputOrigin::Operator,
"continue",
None,
None,
);
assert!(matches!(got, ModelInputOrigin::Operator));
}
#[test]
fn pending_clarification_continuations_are_left_untouched() {
let parent = ctx();
let pending = ModelInputOrigin::OperatorContinuation {
parent: Box::new(ctx()),
};
let before_id = match &pending {
ModelInputOrigin::OperatorContinuation { parent } => parent.submitted_prompt().id(),
_ => unreachable!(),
};
let got = upgrade_origin_for_interrupted_objective(pending, "continue", Some(&parent), None);
match got {
ModelInputOrigin::OperatorContinuation { parent: kept } => assert_eq!(
kept.submitted_prompt().id(),
before_id,
"a pending-clarification link outranks the round-cap link"
),
other => panic!("existing continuation must be preserved, got {other:?}"),
}
}
#[test]
fn durable_substantive_operator_prompt_consumes_the_round_cap_link() {
let mut interrupted = Some(ctx());
consume_interrupted_objective_for_accepted_prompt(
&mut interrupted,
&ModelInputOrigin::Operator,
);
assert!(
interrupted.is_none(),
"a fresh accepted objective must not leave the old cap link armed"
);
}
#[test]
fn accepted_continuations_and_harness_retries_keep_the_round_cap_link() {
for origin in [
ModelInputOrigin::OperatorContinuation {
parent: Box::new(ctx()),
},
ModelInputOrigin::HarnessRetry {
parent: Box::new(ctx()),
},
] {
let mut interrupted = Some(ctx());
consume_interrupted_objective_for_accepted_prompt(&mut interrupted, &origin);
assert!(
interrupted.is_some(),
"continuations and derived input must preserve the objective link"
);
}
}
#[test]
fn accepted_web_objective_consumes_the_old_round_cap_link() {
let mut interrupted = Some(ctx());
consume_interrupted_objective_for_accepted_prompt(
&mut interrupted,
&ModelInputOrigin::WebInjected {
inbox_id: "inbox".to_string(),
},
);
assert!(interrupted.is_none());
}
#[test]
fn a_repair_exhausted_at_the_round_cap_keeps_the_pause_affordance() {
use newt_core::TurnEndReason::RepairExhausted;
let reply = "The check still fails.";
assert!(paused_at_round_cap(Some(RepairExhausted), true));
assert!(decorate_round_cap_reply(reply, Some(RepairExhausted), true).contains("`continue`"));
assert!(!paused_at_round_cap(Some(RepairExhausted), false));
assert_eq!(
decorate_round_cap_reply(reply, Some(RepairExhausted), false),
reply
);
assert!(!paused_at_round_cap(
Some(newt_core::TurnEndReason::Completed),
true
));
}
#[test]
fn round_cap_footer_is_deterministic_and_only_decorates_capped_replies() {
let reply = "Completed the parser; tests remain.";
let capped = decorate_round_cap_reply(reply, Some(newt_core::TurnEndReason::RoundCap), false);
assert!(capped.starts_with(reply), "{capped}");
assert!(capped.contains("If work remains"), "{capped}");
assert!(capped.contains("`continue`"), "{capped}");
assert!(capped.contains("`/rounds <n>`"), "{capped}");
assert_eq!(
decorate_round_cap_reply(reply, None, false),
reply,
"ordinary replies must remain byte-for-byte unchanged"
);
}
#[test]
fn terminal_notices_preserve_questions_and_incomplete_observations() {
let question = "Which project should I update?";
let awaiting = decorate_round_cap_reply(
question,
Some(newt_core::TurnEndReason::AwaitingOperator),
false,
);
assert_eq!(
awaiting, question,
"host notices must not become model material"
);
let mut metrics = newt_core::TurnMetrics {
end_reason: Some(newt_core::TurnEndReason::AwaitingOperator),
..Default::default()
};
assert!(metrics.display_line().contains("awaiting operator"));
for reason in [
newt_core::TurnEndReason::NarrationCapExhausted,
newt_core::TurnEndReason::NarrationFinalRound,
] {
let reply = "I will inspect the test failure.";
let incomplete = decorate_round_cap_reply(reply, Some(reason), false);
assert_eq!(incomplete, reply);
metrics.end_reason = Some(reason);
assert!(metrics.display_line().contains("incomplete"));
}
assert_eq!(
decorate_round_cap_reply(
"The answer is 3.",
Some(newt_core::TurnEndReason::Completed),
false
),
"The answer is 3."
);
}
#[test]
fn capped_progress_is_persistable_without_duplicate_notices_and_resumes_its_objective() {
let parent = ctx();
let core_handoff = "Progress captured.\n\nPaused at the tool-round limit (40 rounds).";
let persisted = decorate_round_cap_reply(
core_handoff,
Some(newt_core::TurnEndReason::RoundCap),
false,
);
assert_eq!(
persisted.matches("tool-round limit").count(),
1,
"the TUI adds only the interactive affordance: {persisted}"
);
assert_eq!(persisted.matches('⏸').count(), 1, "{persisted}");
assert!(persisted.contains("`continue`"), "{persisted}");
let resumed = upgrade_origin_for_interrupted_objective(
ModelInputOrigin::Operator,
"continue",
Some(&parent),
None,
);
match resumed {
ModelInputOrigin::OperatorContinuation { parent: linked } => assert_eq!(
linked.submitted_prompt().id(),
parent.submitted_prompt().id(),
"the persisted capped turn must resume the interrupted objective"
),
other => panic!("capped progress must resume as a continuation, got {other:?}"),
}
}
#[test]
fn the_failed_turn_footer_phrase_resumes_the_failed_objective() {
let failed = ctx();
assert!(failed_turn_footer().contains("`continue`"));
assert!(newt_core::classifiers::is_bare_continuation("continue"));
match upgrade_origin_for_interrupted_objective(
ModelInputOrigin::Operator,
"continue",
Some(&failed),
None,
) {
ModelInputOrigin::OperatorContinuation { parent } => {
assert_eq!(
parent.submitted_prompt().id(),
failed.submitted_prompt().id()
);
}
other => panic!("the footer phrase must resume the failed objective, got {other:?}"),
}
}
fn objective(text: &str) -> newt_core::TurnPromptContext {
newt_core::TurnPromptContext::ephemeral_operator(
"conv",
text.as_bytes().to_vec(),
text.as_bytes().to_vec(),
)
}
fn accept(recorded: &mut RecordedDispositions, text: &str) -> newt_core::TurnPromptContext {
let context = objective(text);
let intake = intake_for_accepted_prompt(
&ModelInputOrigin::Operator,
text,
None,
recorded,
&newt_core::agentic::DispositionLexicon::default(),
);
record_turn_disposition(recorded, &context, &intake);
context
}
fn resume(
recorded: &mut RecordedDispositions,
parent: &newt_core::TurnPromptContext,
nudge: &str,
pending: Option<&PendingClarification>,
) -> (
newt_core::TurnPromptContext,
newt_core::agentic::PromptIntake,
) {
let origin = match pending {
Some(pending) => ModelInputOrigin::OperatorContinuation {
parent: pending.parent.clone(),
},
None => upgrade_origin_for_interrupted_objective(
ModelInputOrigin::Operator,
nudge,
Some(parent),
None,
),
};
assert!(
matches!(origin, ModelInputOrigin::OperatorContinuation { .. }),
"{nudge:?} must link to the pending objective"
);
let context =
newt_core::TurnPromptContext::ephemeral_operator_continuation("conv", nudge, nudge, parent)
.expect("same-conversation continuation");
let intake = intake_for_accepted_prompt(
&origin,
nudge,
pending,
recorded,
&newt_core::agentic::DispositionLexicon::default(),
);
record_turn_disposition(recorded, &context, &intake);
(context, intake)
}
#[test]
fn a_question_shaped_retry_resumes_only_a_pending_objective() {
let parent = ctx();
for nudge in ["try now?", "try again?", "retry"] {
match upgrade_origin_for_interrupted_objective(
ModelInputOrigin::Operator,
nudge,
Some(&parent),
None,
) {
ModelInputOrigin::OperatorContinuation { parent: linked } => assert_eq!(
linked.submitted_prompt().id(),
parent.submitted_prompt().id()
),
other => panic!("{nudge:?} must resume the pending objective, got {other:?}"),
}
assert!(matches!(
upgrade_origin_for_interrupted_objective(ModelInputOrigin::Operator, nudge, None, None),
ModelInputOrigin::Operator
));
}
}
#[test]
fn a_bare_continuation_takes_the_objectives_disposition_not_its_own() {
use newt_core::agentic::{PromptDisposition, PromptIntake};
let text = "Can you install skills for that herdr tool?";
assert_eq!(
PromptIntake::analyze(text).disposition(),
PromptDisposition::Act
);
assert_eq!(
PromptIntake::analyze("continue?").disposition(),
PromptDisposition::Explain,
"precondition: the nudge alone would narrow"
);
for nudge in ["retry", "continue?"] {
let mut recorded = RecordedDispositions::new();
let parent = accept(&mut recorded, text);
let (_, intake) = resume(&mut recorded, &parent, nudge, None);
assert_eq!(intake.disposition(), PromptDisposition::Act, "{nudge:?}");
}
}
#[test]
fn an_explain_objective_resumed_with_continue_stays_explain() {
assert_eq!(
newt_core::agentic::PromptIntake::analyze("continue").disposition(),
newt_core::agentic::PromptDisposition::Act,
"precondition: the nudge alone would widen"
);
let mut recorded = RecordedDispositions::new();
let parent = accept(&mut recorded, "How does prompt intake work?");
let (_, intake) = resume(&mut recorded, &parent, "continue", None);
assert_eq!(
intake.disposition(),
newt_core::agentic::PromptDisposition::Explain
);
}
#[test]
fn a_chained_continue_keeps_the_objectives_disposition() {
let mut recorded = RecordedDispositions::new();
let objective = accept(&mut recorded, "How does prompt intake work?");
let (first, _) = resume(&mut recorded, &objective, "continue", None);
let (_, second) = resume(&mut recorded, &first, "continue", None);
assert_eq!(
second.disposition(),
newt_core::agentic::PromptDisposition::Explain
);
}
#[test]
fn a_clarified_objective_resumed_after_a_cap_does_not_ask_again() {
let mut recorded = RecordedDispositions::new();
let question = "Should we use SQLite or Postgres for the cache?";
let objective = accept(&mut recorded, question);
let asked = newt_core::agentic::PromptIntake::analyze(question);
assert_eq!(
asked.disposition(),
newt_core::agentic::PromptDisposition::Ask,
"fixture needs a pending decision"
);
let pending = PendingClarification {
parent: Box::new(objective.clone()),
intake: asked,
};
let (answered, resolved) = resume(&mut recorded, &objective, "1: sqlite", Some(&pending));
assert_ne!(
resolved.disposition(),
newt_core::agentic::PromptDisposition::Ask
);
let (_, resumed) = resume(&mut recorded, &answered, "continue", None);
assert_eq!(resumed.disposition(), resolved.disposition());
}
#[test]
fn a_resumed_objective_uses_its_recorded_disposition_not_a_rereading() {
let mut recorded = RecordedDispositions::new();
let parent = objective("How does prompt intake work?");
recorded.insert(
parent.submitted_prompt().id(),
newt_core::agentic::PromptDisposition::Research,
);
let (_, intake) = resume(&mut recorded, &parent, "continue", None);
assert_eq!(
intake.disposition(),
newt_core::agentic::PromptDisposition::Research
);
}
#[test]
fn a_pending_clarification_still_resolves_with_the_operators_answer() {
let question = "Should we use SQLite or Postgres for the cache?";
let mut recorded = RecordedDispositions::new();
let parent = objective(question);
let pending = PendingClarification {
parent: Box::new(parent.clone()),
intake: newt_core::agentic::PromptIntake::analyze(question),
};
let (_, resolved) = resume(&mut recorded, &parent, "1: sqlite", Some(&pending));
assert_eq!(resolved.manifest().pending_decision_count(), 0);
}
#[tokio::test]
async fn a_resumed_task_keeps_its_record_but_current_narrowing_wins() {
use newt_core::agentic::{PromptDisposition, PromptIntake};
let lexicon = newt_core::agentic::DispositionLexicon::default();
let mut recorded = RecordedDispositions::new();
let text = "Implement the parser change.";
let objective = accept(&mut recorded, text);
assert_eq!(
recorded.get(&objective.submitted_prompt().id()),
Some(&PromptDisposition::Act),
"fixture needs an Act objective"
);
let origin = upgrade_origin_for_interrupted_objective(
ModelInputOrigin::Operator,
"continue",
Some(&objective),
None,
);
let turn = newt_core::TurnPromptContext::ephemeral_operator_continuation(
"conv", "continue", "continue", &objective,
)
.expect("same-conversation continuation");
let (intake, mode) = comprehend_accepted_prompt(
&origin,
"continue",
None,
Some(&turn),
&mut recorded,
&lexicon,
|intake: &PromptIntake| {
(
effective_operating_mode(OperatingMode::Plan, intake, false, None),
OperatingMode::Plan,
)
},
);
assert_eq!(mode, OperatingMode::Plan);
assert_eq!(intake.disposition(), PromptDisposition::Plan);
assert_eq!(
recorded.get(&turn.submitted_prompt().id()),
Some(&PromptDisposition::Act),
"the record keeps what the task asked for, not the mode's narrowing"
);
let refused = newt_core::agentic::execute_tool_with_offload_and_prompt_and_artifacts(
"write_file",
&serde_json::json!({ "path": "must-not-write.txt", "content": "no" }),
"/nonexistent-workspace",
false,
20,
&operating_mode_caveats(mode, false, newt_core::Caveats::top()),
&mut newt_core::agentic::NoMcp,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
false,
None,
None,
intake.disposition(),
)
.await;
assert!(
refused.contains("Tool `write_file` is not available for this request"),
"{refused}"
);
}
#[test]
fn observed_report_continue_after_normal_finish_keeps_objective() {
let parent = ctx();
let got = upgrade_origin_for_interrupted_objective(
ModelInputOrigin::Operator,
"continue",
None,
Some(&parent),
);
let ModelInputOrigin::OperatorContinuation { parent: linked } = got else {
panic!("continue lost the completed turn's objective");
};
let next = newt_core::TurnPromptContext::ephemeral_operator_continuation(
"conv", "continue", "continue", &linked,
)
.unwrap();
assert_eq!(
next.active().root_prompt_id(),
parent.active().root_prompt_id()
);
assert!(matches!(
upgrade_origin_for_interrupted_objective(
ModelInputOrigin::Operator,
"implement a different feature",
None,
Some(&parent),
),
ModelInputOrigin::Operator
));
}
#[test]
fn observed_report_restored_plan_has_harness_voice() {
let original = vec![
newt_core::MemMessage::system("system policy"),
newt_core::MemMessage::user("continue"),
];
assert_eq!(messages_with_plan(original.clone(), None), original);
let plan = "<plan>\n✓ 1. Extract helper\n→ 2. Verify\n</plan>";
let messages = messages_with_plan(original, Some(plan));
assert_eq!(messages[0].content, "system policy");
let note = messages
.iter()
.find(|m| m.content.contains("<plan>"))
.unwrap();
assert_eq!(note.role, newt_core::Role::User);
assert!(note.content.contains("Harness-restored plan"));
assert!(note.content.contains("agent-maintained, advisory"));
assert_eq!(messages.last().unwrap().content, "continue");
}