blob: 9cc5e197d000694179b18c95598b1b2feedbf39e [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 crate::support::invocation;
mod recovery;
mod websocket;
use std::collections::BTreeMap;
use std::sync::Arc;
use std::time::Duration;
use maka_agent_engine::{Engine, RunError, RunInput};
use maka_agent_facts::event::Fact;
use maka_agent_facts::event::Invocation;
use maka_agent_facts::event::InvocationOutcome;
use maka_agent_facts::event::ModelInterruption;
use maka_agent_facts::event::TerminalStatus;
use maka_agent_facts::model::ModelEvent;
use maka_agent_facts::model::error::ModelFailureCause;
use maka_code_runtime::CellLimits;
use maka_event_log::EventLog;
use maka_model::{ModelError, ModelExecutor, ProviderConfig, ProviderKind};
use maka_tools::CodeExecutor;
use maka_tools::ToolMode;
use serde_json::json;
use sha2::{Digest, Sha256};
use tokio::io::AsyncWriteExt;
use tokio::net::{TcpListener, TcpStream};
use tokio::sync::Notify;
use tokio_util::sync::CancellationToken;
fn input(base: &str, suffix: &str) -> RunInput {
RunInput {
model_source: None,
model_revision: None,
provider_id: "fixture".into(),
main_output_limit: None,
context: None,
invocation: Invocation {
session_id: "session".into(),
turn_id: format!("turn-{suffix}"),
run_id: format!("run-{suffix}"),
invocation_id: format!("invocation-{suffix}"),
},
request_fingerprint: None,
provider: ProviderConfig {
adapter: None,
capabilities: Default::default(),
kind: ProviderKind::OpenaiChatCompletions,
model: "test".into(),
base_url: base.into(),
auth: maka_model::ProviderAuth::ApiKey("test-key".into()),
headers: BTreeMap::new(),
network: Default::default(),
body_overlay: None,
},
provider_options: json!({}),
supports_vision: false,
configuration: invocation::configuration(ToolMode::Direct),
work: maka_agent_engine::RunWork::Message {
allow_prior_unknown: false,
source_messages: Vec::new(),
message: format!("question {suffix}").into(),
tools: Default::default(),
},
}
}
use crate::support::http;
use http::read_request;
async fn respond(socket: &mut TcpStream, partial: bool) {
let text = if partial {
"unfinished-secret-fragment"
} else {
"done"
};
let chunk = json!({"id":"reply","object":"chat.completion.chunk","created":1,"model":"test","choices":[{"index":0,"delta":{"content":text},"finish_reason":null}]});
socket
.write_all(
b"HTTP/1.1 200 OK\r\nContent-Type: text/event-stream\r\nConnection: close\r\n\r\n",
)
.await
.unwrap();
socket
.write_all(format!("data: {chunk}\n\n").as_bytes())
.await
.unwrap();
if !partial {
let end = json!({"id":"reply","object":"chat.completion.chunk","created":1,"model":"test","choices":[{"index":0,"delta":{},"finish_reason":"stop"}]});
socket
.write_all(format!("data: {end}\n\ndata: [DONE]\n\n").as_bytes())
.await
.unwrap();
}
}
fn engine(log: Arc<EventLog>) -> Engine {
Engine::new(
log,
ModelExecutor::new(Duration::from_secs(20)).unwrap(),
CodeExecutor::new(CellLimits::default()).unwrap(),
)
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn safe_retry_preserves_failed_evidence_and_cancellation_stops_backoff() {
tokio::time::timeout(Duration::from_secs(15), async {
for (cancel, reset) in [(false, false), (true, false), (false, true)] {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let base = format!("http://{}/v1", listener.local_addr().unwrap());
let observed = Arc::new(Notify::new());
let reset_ready = observed.clone();
let server = tokio::spawn(async move {
let (mut socket, _) = listener.accept().await.unwrap();
let mut requests = vec![read_request(&mut socket).await];
respond(&mut socket, true).await;
let intent = json!({"id":"reply","object":"chat.completion.chunk","created":1,"model":"test",
"choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"id":"unexecuted","type":"function",
"function":{"name":"Read","arguments":"{\"path\":\"never-read\"}"}}]},"finish_reason":null}]});
socket.write_all(format!("data: {intent}\n\n").as_bytes()).await.unwrap();
if reset {
reset_ready.notified().await;
socket.set_zero_linger().unwrap();
}
drop(socket); // Real EOF or RST, never a fabricated model error.
if !cancel {
let (mut socket, _) = listener.accept().await.unwrap();
requests.push(read_request(&mut socket).await);
respond(&mut socket, false).await;
}
(requests, listener)
});
let directory = tempfile::tempdir().unwrap();
let log = Arc::new(EventLog::open(&directory.path().join("events.sqlite")).await.unwrap());
let worker = engine(log.clone());
let cancellation = CancellationToken::new();
let mut commits = log.subscribe_commits();
let stop = async {
if reset {
loop {
let prefix = log.prefix(100, 128 * 1024).await.unwrap();
if prefix.events.iter().any(|event| matches!(&event.event.fact,
Fact::ModelObserved { event: ModelEvent::PartDelta { text, .. }, .. }
if text == "unfinished-secret-fragment")) { break; }
commits.changed().await.unwrap();
}
observed.notify_one();
}
if cancel {
loop {
let prefix = log.prefix(100, 128 * 1024).await.unwrap();
if prefix.events.iter().any(|event| matches!(event.event.fact,
Fact::ModelInterrupted { status: ModelInterruption::RetryableFailure, .. })) { break; }
commits.changed().await.unwrap();
}
cancellation.cancel();
}
};
let (result, ()) = tokio::join!(worker.run(input(&base, "retry"), cancellation.clone()), stop);
if cancel { assert!(matches!(result, Err(RunError::Cancelled))); }
else { result.unwrap(); }
worker.drain().await;
let (requests, listener) = server.await.unwrap();
assert_eq!(requests.len(), if cancel { 1 } else { 2 });
if !cancel { assert_eq!(requests[0], requests[1], "retry must use the identical frozen prompt, not failed text or tool intent"); }
assert!(tokio::time::timeout(Duration::from_millis(20), listener.accept()).await.is_err());
let prefix = log.prefix(100, 128 * 1024).await.unwrap();
let requested: Vec<_> = prefix.events.iter().filter_map(|event| match &event.event.fact {
Fact::ModelRequested { step_id, input_digest, .. } => Some((step_id, input_digest)), _ => None,
}).collect();
assert_eq!(requested.len(), requests.len());
if !cancel { assert_ne!(requested[0].0, requested[1].0); assert_eq!(requested[0].1, requested[1].1); }
assert_eq!(prefix.events.iter().filter(|event| matches!(event.event.fact,
Fact::ModelInterrupted { status: ModelInterruption::RetryableFailure, .. })).count(), 1);
assert!(prefix.events.iter().any(|event| matches!(&event.event.fact,
Fact::ModelInterrupted { diagnostic: Some(message), .. } if !message.is_empty())),
"a recovered request must retain its original failure cause");
assert!(prefix.events.iter().any(|event| matches!(&event.event.fact,
Fact::ModelObserved { event: ModelEvent::PartDelta { text, .. }, .. } if text == "unfinished-secret-fragment")));
assert!(!prefix.events.iter().any(|event| matches!(event.event.fact,
Fact::ToolDispatched { .. } | Fact::ToolRejected { .. })));
}
}).await.expect("retry and cancellation must settle within their bounds");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn continuation_claim_replays_only_its_lineage_and_retries_with_frozen_input() {
use maka_agent_facts::event::EventWrite;
use maka_agent_facts::event::LogScope;
use maka_agent_facts::event::RuntimeEvent;
use maka_agent_facts::input::InvocationInput;
tokio::time::timeout(Duration::from_secs(20), async {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let base = format!("http://{}/v1", listener.local_addr().unwrap());
let directory = tempfile::tempdir().unwrap();
let log = Arc::new(EventLog::open(&directory.path().join("events.sqlite")).await.unwrap());
let mut source = input(&base, "source");
source.configuration.workspace_identity = Some(
maka_agent_facts::execution::WorkspaceIdentity::from_marker_id("c193cd58-f929-4ba2-bfb5-6887beaf8132").unwrap());
let opening = |text: &str| Fact::InvocationOpened {
configuration: Some(Box::new(source.configuration.clone())),
input: InvocationInput::Message { content: text.into(), request_fingerprint: None,
source_messages: Vec::new(), },
};
let route = format!("sha256:{}", hex::encode(Sha256::digest(serde_json::to_vec(&source.provider).unwrap())));
for fact in [
opening("question source"),
Fact::ModelRequested { item_acceptance: false, step_id: "old".into(), model_id: "test".into(), purpose: maka_agent_facts::context::ModelPurpose::Main, context: None, context_breakdown: None,
source_scope: LogScope::Session { id: "session".into() }, source_high_water: 1,
source_digest: "fixture".into(), input_digest: "fixture".into(), route_identity: route,
checkpoint_event_id: None, effective_source_digest: None },
Fact::ModelCompleted { step_id: "old".into(), output: serde_json::from_value(json!({
"parts":[{"kind":"text","text_kind":"text","text":"trimmed old tail","provider_options":null}],
"finish_reason":"stop","usage":{}
})).unwrap() },
Fact::InvocationEnded { outcome: InvocationOutcome::Cancelled { source: "user".into() } },
] {
log.append(&EventWrite::plain(RuntimeEvent::new(source.invocation.clone(), fact)).unwrap()).await.unwrap();
}
let prefix = log.run_source("session", &source.invocation.run_id, None).await.unwrap().unwrap();
let boundary = prefix.boundary;
for fact in [opening("unrelated later branch"), Fact::InvocationEnded { outcome: InvocationOutcome::Completed }] {
log.append(&EventWrite::plain(RuntimeEvent::new(input(&base, "branch").invocation, fact)).unwrap()).await.unwrap();
}
let continuation = |id: &str| {
let mut child = input(&base, id);
child.configuration = source.configuration.clone();
child.request_fingerprint = Some(maka_agent_facts::artifact::content_digest(id.as_bytes()));
child.work = maka_agent_engine::RunWork::Continuation { source: boundary.clone(),tools: Default::default() };
child
};
let server = tokio::spawn(async move {
let mut requests = Vec::new();
for partial in [true, false] {
let (mut socket, _) = listener.accept().await.unwrap();
requests.push(read_request(&mut socket).await);
respond(&mut socket, partial).await;
}
(requests, listener)
});
let worker = engine(log.clone());
let before = log.prefix(100, 128 * 1024).await.unwrap();
worker.check_continuation(&continuation("preview"), &CancellationToken::new()).await.unwrap();
assert_eq!(log.prefix(100, 128 * 1024).await.unwrap().digest, before.digest, "query cannot claim or mutate the source");
worker.run(continuation("child"), CancellationToken::new()).await.unwrap();
let (requests, listener) = server.await.unwrap();
assert_eq!(requests[0], requests[1]);
let prompt = serde_json::to_string(&requests[0]["messages"]).unwrap();
assert!(prompt.contains("question source"));
for excluded in ["trimmed old tail", "unrelated later branch", "question child", "unfinished-secret-fragment"] {
assert!(!prompt.contains(excluded), "{excluded}: {prompt}");
}
let mut moved = continuation("moved");
moved.configuration.workspace_identity = None;
assert!(worker.run(moved, CancellationToken::new()).await.is_err());
assert!(worker.run(continuation("duplicate"), CancellationToken::new()).await.is_err());
worker.drain().await;
assert!(tokio::time::timeout(Duration::from_millis(20), listener.accept()).await.is_err());
let facts = log.prefix(100, 128 * 1024).await.unwrap();
let claims: Vec<_> = facts.events.iter().filter_map(|s| match &s.event.fact {
Fact::InvocationOpened { input: InvocationInput::Continuation { claim, .. }, .. } => Some(claim),
_ => None,
}).collect();
assert_eq!(claims.len(), 1, "only one canonical claim may acquire the sealed source");
let attempts: Vec<_> = facts.events.iter().filter_map(|s| match &s.event.fact {
Fact::ModelRequested { step_id, source_high_water, source_digest, input_digest, .. } if s.event.invocation.run_id == "run-child" =>
Some(((*source_high_water, source_digest), input_digest, step_id)),
_ => None,
}).collect();
assert_eq!(attempts.len(), 2);
assert_eq!(attempts[0].0, attempts[1].0, "retry preserves the original canonical cut");
assert_eq!(attempts[0].1, &claims[0].replay.digest);
assert_eq!(attempts[0].1, attempts[1].1);
assert_ne!(attempts[0].2, attempts[1].2, "each physical attempt has its own identity");
assert_eq!(facts.events.iter().filter(|s| matches!(s.event.fact,
Fact::ModelInterrupted { status: ModelInterruption::RetryableFailure, .. })).count(), 1);
for fact in [
opening("later unknown effect"),
Fact::ToolDispatched { presentation: None,
title: None, operation_id: "unknown".into(),
call: maka_agent_facts::tool_call::ToolCallIdentity::standalone("unknown-call".into()),
name: "write".into(), input: json!({}) },
Fact::InvocationEnded { outcome: InvocationOutcome::Failed { class: "outcome_unknown".into(), message: None, model_failure: None } },
] {
log.append(&EventWrite::plain(RuntimeEvent::new(input(&base, "unknown").invocation, fact)).unwrap()).await.unwrap();
}
let unsafe_preview = worker.check_continuation(&continuation("unsafe-preview"), &CancellationToken::new()).await;
assert!(matches!(&unsafe_preview,
Err(RunError::Store(maka_event_log::StoreError::HistoryUnsettled))),
"a safe historical cut cannot hide a later unresolved Session effect: {unsafe_preview:?}");
}).await.expect("continuation must settle");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn model_timeout_cause_survives_reopening_the_log() {
tokio::time::timeout(Duration::from_secs(10), async {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let base = format!("http://{}/v1", listener.local_addr().unwrap());
let server = tokio::spawn(async move {
let (mut socket, _) = listener.accept().await.unwrap();
read_request(&mut socket).await;
std::future::pending::<()>().await;
});
let directory = tempfile::tempdir().unwrap();
let path = directory.path().join("events.sqlite");
{
let log = Arc::new(EventLog::open(&path).await.unwrap());
let engine = Engine::new(
log.clone(),
ModelExecutor::new(Duration::from_millis(250)).unwrap(),
CodeExecutor::new(CellLimits::default()).unwrap(),
);
assert!(matches!(
engine
.run(input(&base, "timeout"), CancellationToken::new())
.await,
Err(RunError::Model(ModelError::TimedOut))
));
drop(engine);
Arc::try_unwrap(log).ok().unwrap().close().await.unwrap();
}
server.abort();
let reopened = EventLog::open(&path).await.unwrap();
let prefix = reopened.prefix(100, 128 * 1024).await.unwrap();
assert!(matches!(
&prefix.events.last().unwrap().event.fact,
Fact::InvocationEnded {
outcome: InvocationOutcome::Failed { class, message: Some(message), model_failure: Some(ModelFailureCause::TimedOut) }
} if class == "model_timeout" && message == &ModelError::TimedOut.to_string()
));
assert!(prefix.events.iter().any(|event| matches!(&event.event.fact,
Fact::ModelInterrupted { model_failure: Some(ModelFailureCause::TimedOut), .. })));
assert_eq!(
prefix.project_invocation("invocation-timeout").terminal,
Some(TerminalStatus::Failed)
);
})
.await
.expect("model timeout must durably settle");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn partial_text_survives_cancellation_without_becoming_replayed_model_history() {
tokio::time::timeout(Duration::from_secs(30), async {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let base = format!("http://{}/v1", listener.local_addr().unwrap());
let release = Arc::new(Notify::new());
let server_release = release.clone();
let server = tokio::spawn(async move {
let mut requests = Vec::new();
for index in 0..3 {
let (mut socket, _) = listener.accept().await.unwrap();
requests.push(read_request(&mut socket).await);
respond(&mut socket, index == 0).await;
if index == 0 {
// Keep the first response incomplete until cancellation has
// settled; observing bytes sent is not a durability barrier.
server_release.notified().await;
}
}
requests
});
let directory = tempfile::tempdir().unwrap();
let path = directory.path().join("events.sqlite");
{
let log = Arc::new(EventLog::open(&path).await.unwrap());
let engine = engine(log.clone());
let cancellation = CancellationToken::new();
let running_engine = engine.clone();
let running_token = cancellation.clone();
let first = input(&base, "cancelled");
let running = tokio::spawn(async move {
running_engine.run(first, running_token).await
});
let observed = loop {
let prefix = log.prefix(100, 128 * 1024).await.unwrap();
if prefix.events.iter().any(|stored| matches!(
&stored.event.fact,
Fact::ModelObserved { event: ModelEvent::PartDelta { text, .. }, .. }
if text == "unfinished-secret-fragment"
)) {
break prefix;
}
assert!(!running.is_finished(), "model ended before durable partial text");
tokio::task::yield_now().await;
};
let active = observed.project_invocation("invocation-cancelled");
assert_eq!(active.terminal, None);
assert_eq!(active.unfinished_model_steps.len(), 1);
cancellation.cancel();
assert!(matches!(running.await.unwrap(), Err(RunError::Model(ModelError::Cancelled))));
let settled = log.prefix(100, 128 * 1024).await.unwrap();
let tail = &settled.events[observed.events.len()..];
let interrupted = tail.iter().position(|stored| matches!(
&stored.event.fact,
Fact::ModelInterrupted { step_id, status: ModelInterruption::Cancelled, .. }
if step_id == &active.unfinished_model_steps[0]
)).unwrap();
assert!(matches!(&tail.last().unwrap().event.fact,
Fact::InvocationEnded { outcome: InvocationOutcome::Cancelled { source } }
if source == "runtime_cancellation"));
assert!(interrupted < tail.len() - 1);
assert!(!settled.events.iter().any(|stored| matches!(
stored.event.fact, Fact::ModelCompleted { .. }
)));
let state = settled.project_invocation("invocation-cancelled");
assert_eq!(state.terminal, Some(TerminalStatus::Cancelled));
assert!(state.unfinished_model_steps.is_empty());
release.notify_one();
engine.run(input(&base, "next"), CancellationToken::new()).await.unwrap();
drop(engine);
Arc::try_unwrap(log).ok().unwrap().close().await.unwrap();
}
let log = Arc::new(EventLog::open(&path).await.unwrap());
engine(log.clone()).run(input(&base, "reopened"), CancellationToken::new()).await.unwrap();
let prefix = log.prefix(100, 128 * 1024).await.unwrap();
assert!(prefix.events.iter().any(|stored| matches!(
&stored.event.fact,
Fact::InvocationEnded { outcome: InvocationOutcome::Cancelled { source } }
if stored.event.invocation.invocation_id == "invocation-cancelled"
&& source == "runtime_cancellation"
)));
assert!(prefix.events.iter().any(|stored| matches!(
&stored.event.fact,
Fact::ModelObserved { event: ModelEvent::PartDelta { text, .. }, .. }
if text == "unfinished-secret-fragment"
)));
for suffix in ["cancelled", "next", "reopened"] {
let state = prefix.project_invocation(&format!("invocation-{suffix}"));
assert!(state.unfinished_model_steps.is_empty());
assert_eq!(state.terminal, Some(if suffix == "cancelled" {
TerminalStatus::Cancelled
} else {
TerminalStatus::Completed
}));
}
let requests = server.await.unwrap();
// This test owns conversation replay; system instructions have their own contract.
let conversation = |request: &serde_json::Value| {
request["messages"].as_array().unwrap().iter()
.filter(|message| message["role"] != "system")
.cloned().collect::<Vec<_>>()
};
assert_eq!(conversation(&requests[0]), vec![json!({"role":"user","content":"question cancelled"})]);
// Couple the digest to the system bytes actually sent, without fixing their wording.
let mut expected_prompt: Vec<_> = requests[0]["messages"].as_array().unwrap().iter()
.filter(|message| message["role"] == "system").cloned().collect();
expected_prompt.push(json!({"role":"user","content":[{"type":"text","text":"question cancelled"}]}));
// Rebuild each exact pre-request prefix in an independent log. Its
// digest includes original event identities and sequence, not history.
let replica = EventLog::open(&directory.path().join("replica.sqlite")).await.unwrap();
for stored in &prefix.events {
if let Fact::ModelRequested { source_scope, source_high_water, source_digest, input_digest, .. } = &stored.event.fact {
let source = replica.scoped_prefix(source_scope.clone(), 100, 128 * 1024).await.unwrap();
assert_eq!(*source_scope, maka_agent_facts::event::LogScope::Session {
id: stored.event.invocation.session_id.clone(),
});
assert_eq!(*source_high_water, source.high_water);
assert_eq!(*source_digest, source.digest);
if stored.event.invocation.invocation_id == "invocation-cancelled" {
let expected = serde_json::to_vec(&json!({
"projection":"maka.model-history.v1",
"prompt": expected_prompt,
"tools":[], "providerOptions":{}, "maxOutputTokens":requests[0]["max_tokens"]
})).unwrap();
assert_eq!(*input_digest, format!("sha256:{}", hex::encode(Sha256::digest(expected))));
}
}
assert_eq!(replica.append(&maka_agent_facts::event::EventWrite::plain(stored.event.clone()).unwrap()).await.unwrap(), stored.sequence);
}
assert_eq!(conversation(&requests[1]), vec![
json!({"role":"user","content":"question cancelled"}),
json!({"role":"user","content":"question next"})
]);
assert_eq!(conversation(&requests[2]), vec![
json!({"role":"user","content":"question cancelled"}),
json!({"role":"user","content":"question next"}),
json!({"role":"assistant","content":"done"}),
json!({"role":"user","content":"question reopened"})
]);
}).await.expect("partial model cancellation and recovery must make bounded progress");
}
#[tokio::test]
async fn captured_language_is_evidenced_without_becoming_a_provider_parameter() {
use maka_agent_facts::language::Language;
// Independent wire-evidence types preserve canonical field order at every level.
#[derive(serde::Serialize)]
struct Text<'a> {
#[serde(rename = "type")]
kind: &'static str,
text: &'a str,
}
#[derive(serde::Serialize)]
#[serde(tag = "role", rename_all = "lowercase")]
enum Prompt<'a> {
System { content: &'a str },
User { content: [Text<'a>; 1] },
}
#[derive(serde::Serialize)]
#[serde(rename_all = "camelCase")]
struct Evidence<'a> {
projection: &'static str,
#[serde(skip_serializing_if = "Option::is_none")]
language: Option<&'static str>,
prompt: [Prompt<'a>; 2],
tools: [(); 0],
provider_options: serde_json::Value,
max_output_tokens: u64,
}
for (language, tag, context_label) in [
(Language::En, None, "Session context"),
(Language::ZhCn, Some("zh-CN"), "会话上下文"),
(Language::ZhTw, Some("zh-TW"), "工作階段上下文"),
] {
let directory = tempfile::tempdir().unwrap();
let log = Arc::new(
EventLog::open(&directory.path().join("events.sqlite"))
.await
.unwrap(),
);
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let base = format!("http://{}/v1", listener.local_addr().unwrap());
let server = tokio::spawn(async move {
let (mut socket, _) = listener.accept().await.unwrap();
let request = read_request(&mut socket).await;
respond(&mut socket, false).await;
request
});
let mut run = input(&base, "language");
run.configuration.language = language;
let maka_agent_engine::RunWork::Message { message, .. } = &mut run.work else {
unreachable!()
};
*message = "Keep 原文 /tmp/exact.rs unchanged".into();
let worker = engine(log.clone());
worker.run(run, CancellationToken::new()).await.unwrap();
worker.drain().await;
let request = server.await.unwrap();
assert!(request.get("language").is_none());
let system = request["messages"][0].clone();
assert_eq!(system["role"], "system");
assert!(system["content"].as_str().unwrap().contains(context_label));
assert_eq!(
request["messages"][1],
json!({"role":"user","content":"Keep 原文 /tmp/exact.rs unchanged"})
);
let evidence = Evidence {
projection: "maka.model-history.v1",
language: tag,
prompt: [
Prompt::System {
content: system["content"].as_str().unwrap(),
},
Prompt::User {
content: [Text {
kind: "text",
text: "Keep 原文 /tmp/exact.rs unchanged",
}],
},
],
tools: [],
provider_options: json!({}),
max_output_tokens: request["max_tokens"].as_u64().unwrap(),
};
let expected_digest = format!(
"sha256:{}",
hex::encode(Sha256::digest(serde_json::to_vec(&evidence).unwrap()))
);
let prefix = log.prefix(100, 128 * 1024).await.unwrap();
let opening = prefix
.events
.iter()
.find_map(|event| match &event.event.fact {
Fact::InvocationOpened {
configuration: Some(configuration),
..
} => Some(configuration),
_ => None,
})
.unwrap();
assert_eq!(opening.language, language);
let recorded = serde_json::to_value(opening).unwrap();
assert_eq!(
recorded.get("language").and_then(serde_json::Value::as_str),
tag
);
let digest = prefix
.events
.iter()
.find_map(|event| match &event.event.fact {
Fact::ModelRequested { input_digest, .. } => Some(input_digest),
_ => None,
})
.unwrap();
assert_eq!(digest, &expected_digest);
log.shutdown().await.unwrap();
}
}