mod common;
use std::time::Duration;
use common::mini_test_helpers::{parity_session, mock_llm, trace_with_run_dir};
use common::observability_parity::{
assert_audit_contains, assert_stdout_lacks_substring, assert_stdout_tool_vocab,
trace_contains_substring,
};
use malvin::agent_backend::mini::{
record_http_exchange, run_inner_loop, LoopDriverConfig, LoopDriverRun, MiniHttpExchangeRecord, MiniTerminalReason, MiniTraceSink, MockStep,
};
use malvin::observability::audit_only_session_update_fields;
use malvin::malvin_mini::CompletionResponse;
#[test]
fn contract_plain_lines_bash_fence_audit_only_on_stdout() {
let tmp = tempfile::tempdir().expect("tempdir");
let log_path = tmp.path().join("stdout.log");
malvin::output::set_stdout_log_path(Some(log_path.clone()));
let mut sink = trace_with_run_dir(&tmp, false);
sink.plain_lines = true;
sink.record_assistant_audit("```bash\ncat plan_dco.md\n```");
assert_stdout_lacks_substring(&log_path, "plan_dco.md");
assert_audit_contains(&tmp.path().join("trace.jsonl"), "agent_message_chunk");
assert_audit_contains(&tmp.path().join("trace.jsonl"), "plan_dco.md");
malvin::output::set_stdout_log_path(None);
}
#[test]
fn contract_audit_only_keys_never_in_stdout_log() {
let tmp = tempfile::tempdir().expect("tempdir");
let log_path = tmp.path().join("stdout.log");
malvin::output::set_stdout_log_path(Some(log_path.clone()));
let sink = trace_with_run_dir(&tmp, true);
record_http_exchange(
&sink,
MiniHttpExchangeRecord {
attempt: 1,
status: Some(502),
body: Some("upstream"),
error: Some("bad gateway".into()),
},
);
let trace_path = tmp.path().join("trace.jsonl");
assert_audit_contains(&trace_path, "miniHttpExchange");
for field in audit_only_session_update_fields() {
assert_stdout_lacks_substring(&log_path, field);
}
malvin::output::set_stdout_log_path(None);
}
#[tokio::test]
async fn contract_mini_terminal_in_trace_not_stdout() {
let tmp = tempfile::tempdir().expect("tempdir");
let log_path = tmp.path().join("stdout.log");
malvin::output::set_stdout_log_path(Some(log_path.clone()));
let trace = trace_with_run_dir(&tmp, false);
let mut session = parity_session(tmp.path());
let out = run_inner_loop(LoopDriverRun {
llm: &mock_llm(vec![MockStep::Ok(CompletionResponse {
content: malvin::malvin_mini::format_wire_turn("- progress", "done without fence"),
usage: None,
reasoning: None,
})]),
session: &mut session,
user_prompt: "go",
config: &LoopDriverConfig {
max_http_turns: 4,
max_bash_execs: 128,
max_http_retries: 1,
max_transport_retries: 3,
max_shrink_passes: 0,
mini_constraints: "c",
expects_investigation: false,
},
trace: &trace,
timing: None,
llm_phase: None,
single_attempt: true,
gate_attempt: 1,
retry_strategy: malvin::agent_backend::mini::MiniRetryStrategy::CumulativeTranscript,
})
.await
.expect("loop");
assert_eq!(out.terminal.reason, MiniTerminalReason::FencelessComplete);
let trace_path = tmp.path().join("trace.jsonl");
trace_contains_substring(&trace_path, "miniTerminal");
assert_stdout_lacks_substring(&log_path, "miniTerminal");
assert_stdout_lacks_substring(&log_path, "fenceless_complete");
malvin::output::set_stdout_log_path(None);
}
#[test]
fn contract_tagged_run_dual_emits_bash_to_both_channels() {
let tmp = tempfile::tempdir().expect("tempdir");
let log_path = tmp.path().join("stdout.log");
malvin::output::set_stdout_log_path(Some(log_path.clone()));
let sink = MiniTraceSink::new(
Some(tmp.path().to_path_buf()),
malvin::acp::AgentIoOptions {
force: false,
no_tee: false,
raw_output: false,
show_thoughts_on_stdout: false,
emit_stdout_markdown: false,
log_full_outgoing_prompts: false,
},
);
sink.mini_bash_exec("echo hi", 0, Duration::from_millis(2), None);
assert_stdout_tool_vocab(&log_path, &["Run"]);
assert_audit_contains(&tmp.path().join("trace.jsonl"), "tool_call");
malvin::output::set_stdout_log_path(None);
}
#[test]
fn contract_record_assistant_audit_leaves_stdout_empty() {
let tmp = tempfile::tempdir().expect("tempdir");
let log_path = tmp.path().join("stdout.log");
malvin::output::set_stdout_log_path(Some(log_path.clone()));
let sink = trace_with_run_dir(&tmp, false);
sink.record_assistant_audit("secret audit-only body");
assert_stdout_lacks_substring(&log_path, "secret audit-only body");
assert_audit_contains(&tmp.path().join("trace.jsonl"), "secret audit-only body");
malvin::output::set_stdout_log_path(None);
}
#[tokio::test]
async fn contract_acp_tool_tee_narrative_vocab_audit_json_stays_off_stdout() {
let tmp = tempfile::tempdir().expect("tempdir");
let log_path = tmp.path().join("stdout.log");
let trace_path = tmp.path().join("trace.jsonl");
let (trace, _stdout) =
malvin::acp::contract_acp_tee_tool_fixture(&trace_path, &log_path).await;
assert_stdout_tool_vocab(&log_path, &["Run"]);
assert_audit_contains(&trace_path, "echo contract");
assert_stdout_lacks_substring(&log_path, "toolCallId");
assert_stdout_lacks_substring(&log_path, "sessionUpdate");
for field in audit_only_session_update_fields() {
assert_stdout_lacks_substring(&log_path, field);
}
assert!(
trace.contains("echo contract"),
"trace must record tool audit content; got:\n{trace}"
);
}
#[test]
fn kiss_cov_acp_contract_fixture_symbols() {
let _ = malvin::acp::contract_acp_tee_tool_fixture;
}