use super::*;
#[test]
fn ephemeral_session_saves_nothing() {
let (_state, _workspace, store, _persona_store) = resume_fixture();
// The ephemeral arm of the save seam: no store handle → no row, no
// turn, no error — asserted against a real store on the same root.
save_turn_if_persistent(
None,
&newt_core::new_conversation_id(),
None,
"ephemeral task",
"ephemeral reply",
&[],
&[],
None,
None,
&std::collections::BTreeMap::new(),
&newt_core::PlanSnapshot::default(),
)
.unwrap();
assert!(
store.list().unwrap().is_empty(),
"--ephemeral must leave zero conversation rows"
);
// The persistent arm still writes (the seam routes, never drops).
let id = newt_core::new_conversation_id();
save_turn_if_persistent(
Some(&store),
&id,
None,
"kept task",
"kept reply",
&[],
&[],
None,
None,
&std::collections::BTreeMap::new(),
&newt_core::PlanSnapshot::default(),
)
.unwrap();
assert_eq!(store.list().unwrap().len(), 1);
}
#[test]
fn ephemeral_notice_names_both_halves() {
// The notice doubles as the /conversation + /recall answer in an
// ephemeral session: it must say nothing is saved AND nothing resumed.
assert!(EPHEMERAL_SESSION_NOTICE.contains("nothing saved"));
assert!(EPHEMERAL_SESSION_NOTICE.contains("nothing resumed"));
}
/// Step 18.5 (#247) compressed-session round-trip: a session that
/// compressed mid-flight persists the compaction record through the save
/// path; a fresh session restoring it gets the summary message back in
/// the working set (recognizable by the pipeline's marker) instead of
/// the raw pre-compression history — the memory.rs:919-class bug.
#[serial_test::serial(real_fs)]
#[tokio::test]
async fn compressed_session_round_trips_summary_through_save_and_restore() {
// rebuild_system_prompt below republishes process-global runtime
// settings as a side effect; hold the guard for the whole test.
let _guard = newt_core::test_guard::GlobalSettingsGuard::acquire();
let tmp = tempfile::TempDir::new().unwrap();
let workspace = tmp.path().join("workspace");
fs::create_dir_all(&workspace).unwrap();
let store =
newt_core::ConversationStore::new(tmp.path().join("state"), &workspace, 100).unwrap();
let id = newt_core::new_conversation_id();
let metrics = |input_tokens: u32| newt_core::TurnMetrics {
usage: Some(newt_core::TokenUsage {
input_tokens,
output_tokens: 9,
}),
..Default::default()
};
// Live session: Summarizing provider with a stub summarizer.
let mut memory = newt_core::MemoryManager::new();
// Leave enough room for the irreducible active-prompt metadata + exact
// user pair. A smaller authoritative budget must refuse compression
// rather than summarize either half of that pair.
memory.add_provider(newt_core::Summarizing::new(512).with_summarizer(
|_req: String| -> newt_core::SummarizeFuture {
Box::pin(async { Ok(("FACTS FROM THE COMPRESSED MIDDLE".to_string(), None)) })
},
));
let big = "x".repeat(200);
for i in 0..5u32 {
let task = format!("early task {i}");
memory
.sync_all_with_active_task(&task, &big, &metrics(10 + i), &task)
.await;
save_successful_conversation_turn(
&store,
&id,
None,
&task,
&big,
&[],
&[],
Some(newt_core::TokenUsage {
input_tokens: 10 + i,
output_tokens: 9,
}),
memory.take_compaction_record(),
&std::collections::BTreeMap::new(),
&newt_core::PlanSnapshot::default(),
)
.unwrap();
}
// The over-budget turn mints the compaction record during sync.
memory
.sync_all_with_active_task("final task", &big, &metrics(600), "final task")
.await;
let record = memory.take_compaction_record();
assert!(record.is_some(), "compression must mint a record");
save_successful_conversation_turn(
&store,
&id,
None,
"final task",
&big,
&[],
&[],
Some(newt_core::TokenUsage {
input_tokens: 600,
output_tokens: 9,
}),
record,
&std::collections::BTreeMap::new(),
&newt_core::PlanSnapshot::default(),
)
.unwrap();
// Fresh session restores through the command path (no summarizer —
// restore must never need one).
let persona_store = PersonaStore::new(tmp.path().join("personas"));
let mut memory2 = newt_core::MemoryManager::new();
memory2.add_provider(newt_core::Summarizing::new(512));
let workspace_str = workspace.to_str().unwrap();
let mut system = rebuild_system_prompt(workspace_str, &memory2, None, "test-session");
let mut active_persona = None;
let mut active_conversation_id = newt_core::new_conversation_id();
let mut compress_state = newt_core::CompressState::new();
let scratchpad_store = newt_core::SessionScratchpadStore::default();
let step_ledger = newt_core::SessionStepLedger::default();
let mut active_prompt_context = None;
let mut ctx = ConversationCommandContext {
store: &store,
persona_store: &persona_store,
workspace: workspace_str,
memory: &mut memory2,
system: &mut system,
active_persona: &mut active_persona,
active_conversation_id: &mut active_conversation_id,
compress_state: &mut compress_state,
scratchpad: &scratchpad_store,
step_ledger: &step_ledger,
active_prompt_context: &mut active_prompt_context,
mode_states: &ConversationModeStates::default(),
};
handle_conversation_command(&format!("/conversation restore {id}"), &mut ctx).unwrap();
let messages = memory2.build_messages(&system, "next task");
let summary = messages
.iter()
.find(|m| m.content.starts_with(newt_core::agentic::SUMMARY_PREFIX))
.expect("the compaction summary must survive restore");
assert!(summary.content.contains("FACTS FROM THE COMPRESSED MIDDLE"));
assert!(summary
.content
.contains(newt_core::agentic::SUMMARY_END_MARKER));
// The triggering turn survives alongside the summary; the summarized
// early history is not duplicated next to its own summary.
assert!(messages.iter().any(|m| m.content == "final task"));
assert!(!messages.iter().any(|m| m.content == "early task 0"));
// The lone-sided summary record never dispatches an empty message.
assert!(!messages.iter().any(|m| m.content.is_empty()));
}
/// Operator-only reports must not become assistant-authored SQLite history.
#[test]
fn observed_report_persistence_stores_model_explanation_only() {
let (_state, _workspace, store, _persona_store) = resume_fixture();
let id = newt_core::new_conversation_id();
let reply = "## Observed\n\nReport `sample` — root `project`.\n\nFiles since objective/adoption snapshot (LF counts; absence is 0):\n- No content changes observed.\n\nLast observed checks per exact command/cwd (historical observations, not current-tree certification):\n- No check observation available.\n\nGoverned publication observations (not a live remote-state check):\n- No governed receipt available.\n\nLast turn harness notices (historical, not assistant-authored):\n- `The tool loop reached the tool-round limit (75 rounds).`\n- `Captured working state: Plan: finish verification`\n\n## Model explanation\n\nI extracted the helper.";
save_turn_if_persistent(
Some(&store),
&id,
None,
"refactor",
reply,
&[],
&[],
None,
None,
&std::collections::BTreeMap::new(),
&newt_core::PlanSnapshot::default(),
)
.unwrap();
let saved = store.load(&id).unwrap();
assert_eq!(saved.turns[0].assistant, "I extracted the helper.");
assert!(
reply.starts_with("## Observed"),
"operator output stays complete"
);
}