blob: b7ea0e927b5bb5d14625b7d551869fb87b8a5507 [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
use super::*;
#[tokio::test]
async fn observe_exposes_last_known_context_only_for_an_active_matching_backend() {
tokio::time::timeout(Duration::from_secs(30), async {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let fixture = Fixture::new(
&format!("http://{}/v1", listener.local_addr().unwrap()),
None,
)
.await;
let session = fixture.create("Context metrics").await;
fixture.send(&session).await.unwrap();
let (mut first, _) = listener.accept().await.unwrap();
request(&mut first).await;
response(&mut first).await;
fixture.drained().await;
let uploaded = attachments::upload(
&fixture,
&session,
"context.txt",
b"new context whose token count is unknown",
Uuid::new_v4(),
)
.await
.unwrap();
let sent = fixture
.mutation(
2,
"send",
json!({"sessionId":session,"message":"Read this","attachments":[uploaded["attachment"]["id"]],"model":fixture.selection().await}),
Uuid::new_v4(),
)
.await
.unwrap();
let (mut second, _) = listener.accept().await.unwrap();
request(&mut second).await;
let active = fixture.query("observe", json!({"sessionId":session})).await.unwrap();
assert_eq!(active["run"]["invocationId"], sent["invocationId"]);
assert_eq!(active["metrics"]["contextTokens"], 3);
assert_eq!(active["metrics"]["contextLastKnown"], true);
let record = fixture.store.get_session::<SessionConfiguration>(&session).await.unwrap().unwrap();
let original_backend = record.configuration.backend.clone();
fixture.store.update_session_metadata::<SessionConfiguration, _>(&session, record.revision, |configuration| {
configuration.backend = Some(contract::BackendPreference::Model {
connection_id: "different-connection".into(),
model_id: "different-model".into(),
thinking_level: None,
});
Ok(())
}).await.unwrap();
let mismatch = fixture.query("observe", json!({"sessionId":session})).await.unwrap();
assert!(mismatch["metrics"]["contextTokens"].is_null());
assert_eq!(mismatch["metrics"]["contextLastKnown"], false);
let record = fixture.store.get_session::<SessionConfiguration>(&session).await.unwrap().unwrap();
fixture.store.update_session_metadata::<SessionConfiguration, _>(&session, record.revision, move |configuration| {
configuration.backend = original_backend;
Ok(())
}).await.unwrap();
fixture.mutation(2, "stop", json!({"sessionId":session,"invocationId":sent["invocationId"]}), Uuid::new_v4()).await.unwrap();
fixture.drained().await;
assert!(fixture.store.context_usage(&session).await.unwrap().1.unwrap().last_known);
let idle = fixture.query("observe", json!({"sessionId":session})).await.unwrap();
assert!(idle["run"].is_null());
assert!(idle["metrics"]["contextTokens"].is_null());
assert_eq!(idle["metrics"]["contextLastKnown"], false);
fixture.app.shutdown().await.unwrap();
fixture.config.shutdown().await.unwrap();
}).await.unwrap();
}
#[tokio::test]
async fn context_reports_original_prepared_request_across_completion_failure_fence_and_restart() {
use maka_agent_facts::event::Fact;
tokio::time::timeout(Duration::from_secs(45), async {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let mut f = Fixture::new(&format!("http://{}/v1", listener.local_addr().unwrap()), None).await;
selection::edit(&f, |changes| {
changes.model_overrides = Patch::Set(BTreeMap::from([("fixture-model".into(), ModelOverride {
context_window: Some(1_000_000), input_limit: Some(900_000), ..Default::default()
})]));
}).await;
let session = f.create("Context report").await;
let empty = f.query("context", json!({"sessionId":session,"through":null})).await.unwrap();
assert_eq!(empty["request"], Value::Null);
let started = f.send(&session).await.unwrap();
let (mut socket, _) = listener.accept().await.unwrap();
let sent = request(&mut socket).await;
let pending = f.query("context", json!({"sessionId":session,"through":null})).await.unwrap();
let original = &pending["request"];
assert_eq!(original["invocationId"], started["invocationId"]);
assert_eq!(original["state"], "requested");
assert_eq!(original["modelId"], "fixture-model");
assert_eq!(original["contextWindow"], 1_000_000);
assert_eq!(original["inputLimit"], 900_000);
assert_eq!(original["reportedInputTokens"], Value::Null);
let facts = service_tools::facts(&f, &session).await;
let measured = facts.iter().find_map(|fact| match fact {
Fact::ModelRequested { step_id, context_breakdown: Some(measured), .. }
if original["stepId"].as_str() == Some(step_id.as_str()) => Some(measured),
_ => None,
}).expect("the actual HTTP request has a canonical prepared-input measurement");
assert_eq!(original["breakdown"], serde_json::to_value(measured).unwrap());
assert_eq!(original["breakdown"]["counts"]["tools"], sent["tools"].as_array().map_or(0, Vec::len));
assert_eq!(original["breakdown"]["counts"]["messages"], sent["messages"].as_array().unwrap().len());
assert!(!pending.to_string().contains("private-fixture-key"));
response(&mut socket).await;
f.drained().await;
let completed = f.query("context", json!({"sessionId":session,"through":null})).await.unwrap();
assert_eq!(completed["request"]["state"], "completed");
assert_eq!(completed["request"]["reportedInputTokens"], 1);
assert_eq!(completed["request"]["stepId"], original["stepId"]);
assert_eq!(completed["request"]["breakdown"], original["breakdown"]);
assert_eq!(f.query("context", json!({"sessionId":session,"through":pending["through"]})).await.unwrap(), pending);
let record = f.store.get_session::<SessionConfiguration>(&session).await.unwrap().unwrap();
f.store.update_session_metadata::<SessionConfiguration, _>(&session, record.revision, |configuration| {
configuration.backend = Some(contract::BackendPreference::Model {
connection_id: "different-connection".into(), model_id: "different-model".into(), thinking_level: None,
});
Ok(())
}).await.unwrap();
let changed = f.query("context", json!({"sessionId":session,"through":null})).await.unwrap();
assert_eq!(changed["request"], completed["request"], "current backend metadata cannot rewrite the original request");
let second = f.send(&session).await.unwrap();
let (mut socket, _) = listener.accept().await.unwrap();
request(&mut socket).await;
socket.write_all(b"HTTP/1.1 400 Bad Request\r\nContent-Type: application/json\r\nContent-Length: 2\r\nConnection: close\r\n\r\n{}").await.unwrap();
f.drained().await;
let failed = f.query("context", json!({"sessionId":session,"through":null})).await.unwrap();
assert_eq!(failed["request"]["invocationId"], second["invocationId"]);
assert_eq!(failed["request"]["state"], "interrupted");
assert_eq!(failed["request"]["reportedInputTokens"], Value::Null, "failed latest request must not borrow the previous successful usage");
assert_ne!(failed["request"]["stepId"], original["stepId"]);
assert_eq!(f.query("context", json!({"sessionId":session,"through":completed["through"]})).await.unwrap(), completed);
todo_tests::reopen(&mut f).await;
assert_eq!(f.query("context", json!({"sessionId":session,"through":failed["through"]})).await.unwrap(), failed);
assert_eq!(f.query("context", json!({"sessionId":"wh_not-owned","through":null})).await.unwrap_err(), "agent_session_unavailable");
f.app.shutdown().await.unwrap();
f.config.shutdown().await.unwrap();
}).await.unwrap();
}