use serde_json::{Value, json};
use super::anthropic::AnthropicAdapter;
use super::mock_http::{MockProvider, Reply};
use crate::models::ModelError;
use crate::models::config::{ModelConfig, NativeCompaction};
use crate::models::stream::StreamEvent;
use crate::models::traits::Model;
use crate::models::types::{ChatMessage, ModelResponse, ProviderContinuation};
const SSE: &str = "text/event-stream";
fn fixture(name: &str) -> String {
let path = format!(
"{}/tests/fixtures/streams/anthropic/{name}",
env!("CARGO_MANIFEST_DIR")
);
std::fs::read_to_string(path).expect("stream fixture under tests/fixtures/streams/anthropic")
}
fn adapter(provider: &MockProvider) -> AnthropicAdapter {
AnthropicAdapter::new(
"key".into(),
"claude-nova-7".into(),
format!("{}/v1", provider.url),
)
.expect("adapter")
}
fn compacting() -> ModelConfig {
ModelConfig {
native_compaction: Some(NativeCompaction {
trigger_tokens: 170_000,
}),
..ModelConfig::default()
}
}
async fn chat(
model: &dyn Model,
history: &[ChatMessage],
config: &ModelConfig,
) -> (Result<ModelResponse, ModelError>, Vec<StreamEvent>) {
let (tx, mut rx) = tokio::sync::mpsc::channel(256);
let drain = tokio::spawn(async move {
let mut events = Vec::new();
while let Some(event) = rx.recv().await {
events.push(event);
}
events
});
let result = model.chat(history, config, Some(tx)).await;
(result, drain.await.expect("drain"))
}
#[tokio::test]
async fn a_turn_that_asks_sends_the_edit_and_its_beta() {
let provider = MockProvider::start(|_| Reply::stream(SSE, &fixture("text.sse"))).await;
let adapter = adapter(&provider);
let (result, _) = chat(&adapter, &[ChatMessage::user("hi")], &compacting()).await;
result.expect("chat");
let sent = provider.received();
assert_eq!(sent.len(), 1);
assert_eq!(sent[0].header("anthropic-beta"), Some("compact-2026-01-12"));
assert_eq!(
sent[0].body["context_management"],
json!({"edits": [{
"type": "compact_20260112",
"trigger": {"type": "input_tokens", "value": 170_000},
}]})
);
}
#[tokio::test]
async fn a_turn_that_does_not_ask_sends_neither() {
let provider = MockProvider::start(|_| Reply::stream(SSE, &fixture("text.sse"))).await;
let adapter = adapter(&provider);
let (result, _) = chat(
&adapter,
&[ChatMessage::user("hi")],
&ModelConfig::default(),
)
.await;
result.expect("chat");
let sent = provider.received();
assert_eq!(sent[0].header("anthropic-beta"), None);
assert!(sent[0].body.get("context_management").is_none());
}
#[tokio::test]
async fn the_compaction_block_is_kept_and_replayed_verbatim() {
let provider = MockProvider::start(|_| Reply::stream(SSE, &fixture("compaction.sse"))).await;
let adapter = adapter(&provider);
let (result, events) = chat(&adapter, &[ChatMessage::user("hi")], &compacting()).await;
let response = result.expect("chat");
assert_eq!(response.content, "Continuing.");
let block =
json!({"type": "compaction", "content": "Summary: the user is rewriting the parser."});
let continuation = response
.provider_continuation
.clone()
.expect("continuation");
assert_eq!(continuation.anthropic_compaction(), Some(&block));
assert!(
events
.iter()
.any(|e| matches!(e, StreamEvent::Status(s) if s.contains("compacted"))),
"the user hears that the provider compacted"
);
let history = vec![
ChatMessage::user("hi"),
ChatMessage::assistant(&response.content).with_provider_continuation(continuation),
ChatMessage::user("go on"),
];
let (result, _) = chat(&adapter, &history, &compacting()).await;
result.expect("second chat");
let sent = provider.bodies_to("/messages");
let assistant = &sent[1]["messages"][1];
assert_eq!(assistant["role"], "assistant");
assert_eq!(assistant["content"][0], block);
assert_eq!(assistant["content"][1]["text"], "Continuing.");
}
#[tokio::test]
async fn a_model_that_refuses_it_is_retried_without_it_and_remembered() {
let provider = MockProvider::start(|r| {
if r.body.get("context_management").is_some() {
return Reply::error(
400,
&json!({"type": "error", "error": {
"type": "invalid_request_error",
"message": "context_management: compaction is not supported for this model.",
}}),
);
}
Reply::stream(SSE, &fixture("text.sse"))
})
.await;
let adapter = adapter(&provider);
assert!(adapter.compacts_natively(), "optimistic until refused");
let (result, _) = chat(&adapter, &[ChatMessage::user("hi")], &compacting()).await;
assert_eq!(result.expect("retry succeeds").content, "Hello, world");
let sent = provider.received();
assert_eq!(sent.len(), 2);
assert!(sent[1].body.get("context_management").is_none());
assert_eq!(sent[1].header("anthropic-beta"), None);
assert!(
!adapter.compacts_natively(),
"the harness compacts for this model from now on"
);
let (result, _) = chat(&adapter, &[ChatMessage::user("hi")], &compacting()).await;
result.expect("third call");
assert_eq!(provider.received().len(), 3);
}
#[test]
fn only_what_follows_the_last_compaction_counts_as_context() {
let compacted = |text: &str| {
ChatMessage::assistant(text).with_provider_continuation(ProviderContinuation::Anthropic {
signature: String::new(),
compaction: Some(json!({"type": "compaction", "content": text})),
native_tool_calls: Vec::new(),
})
};
let history = vec![
ChatMessage::user("one"),
compacted("first"),
ChatMessage::user("two"),
compacted("second"),
ChatMessage::user("three"),
];
let live = ChatMessage::since_provider_compaction(&history);
let contents: Vec<&str> = live.iter().map(|m| m.content.as_str()).collect();
assert_eq!(contents, ["second", "three"]);
let untouched = vec![ChatMessage::user("a"), ChatMessage::assistant("b")];
assert_eq!(ChatMessage::since_provider_compaction(&untouched).len(), 2);
}
#[test]
fn a_continuation_without_compaction_serializes_as_before() {
let old: ProviderContinuation =
serde_json::from_value(json!({"provider": "anthropic", "signature": "sig"}))
.expect("old shape loads");
assert_eq!(old, ProviderContinuation::anthropic("sig".to_string()));
let written: Value = serde_json::to_value(&old).expect("serialize");
assert_eq!(
written,
json!({"provider": "anthropic", "signature": "sig"})
);
}