fix: normalize each Gemini response call before Tool dispatch
This commit is contained in:
@@ -553,6 +553,620 @@ pub(super) mod plugin_runtime_tests {
|
||||
f.shutdown().await;
|
||||
}
|
||||
|
||||
// Same-name calls in one Gemini response must remain distinct through
|
||||
// adapter normalization, committed Host history and real Component dispatch.
|
||||
#[derive(Clone)]
|
||||
struct GeminiTwoCallModel {
|
||||
tool_name: String,
|
||||
https: bool,
|
||||
separate_frames: bool,
|
||||
requests: Arc<Mutex<Vec<ModelRequest>>>,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl LlmClient for GeminiTwoCallModel {
|
||||
fn clone_boxed(&self) -> Box<dyn LlmClient> {
|
||||
Box::new(self.clone())
|
||||
}
|
||||
|
||||
fn tool_call_completion_support(&self) -> agen::llm_client::ToolCallCompletionSupport {
|
||||
use agen::llm_client::scheme::{Scheme, gemini::GeminiScheme};
|
||||
// Match HttpTransport: Gemini does not have authoritative per-call
|
||||
// done events. Do not silently test the Engine's PerBlock path.
|
||||
let support = GeminiScheme::new().tool_call_completion_support();
|
||||
assert_eq!(
|
||||
support,
|
||||
agen::llm_client::ToolCallCompletionSupport::ResponseComplete
|
||||
);
|
||||
support
|
||||
}
|
||||
|
||||
async fn stream(
|
||||
&self,
|
||||
request: ModelRequest,
|
||||
) -> std::result::Result<agen::llm_client::client::ResponseStream, ClientError> {
|
||||
use agen::llm_client::scheme::{Scheme, gemini::GeminiScheme};
|
||||
let turn = {
|
||||
let mut requests = self.requests.lock().unwrap();
|
||||
let turn = requests.len();
|
||||
requests.push(request);
|
||||
turn
|
||||
};
|
||||
let scheme = GeminiScheme::new();
|
||||
let mut state = <GeminiScheme as Scheme>::State::default();
|
||||
let frames = match turn {
|
||||
// Exercise both two parts in one frame and part-index restart
|
||||
// across frames. Neither path fabricates Engine call IDs.
|
||||
0 => {
|
||||
let parts = ["1", "2"]
|
||||
.into_iter()
|
||||
.map(|text| {
|
||||
json!({"functionCall":{"name":self.tool_name,"args":
|
||||
if self.https { json!({}) } else { json!({"text":text}) }}})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let chunks = if self.separate_frames {
|
||||
parts.into_iter().map(|part| vec![part]).collect::<Vec<_>>()
|
||||
} else {
|
||||
vec![parts]
|
||||
};
|
||||
chunks.into_iter().map(|parts| {
|
||||
json!({"candidates":[{"index":0,"content":{"role":"model","parts":parts}}]})
|
||||
}).chain([json!({"candidates":[{"index":0,"finishReason":"STOP"}]})]).collect::<Vec<_>>()
|
||||
}
|
||||
1 => vec![json!({"candidates":[{"index":0,"content":{"role":"model",
|
||||
"parts":[{"text":"Observed both tool results."}]},"finishReason":"STOP"}]})],
|
||||
_ => panic!("restore must not start an extra model turn"),
|
||||
};
|
||||
let mut events = Vec::new();
|
||||
for frame in frames {
|
||||
events.extend(scheme.parse_sse("", &frame.to_string(), &mut state)?);
|
||||
}
|
||||
// Transport owns this terminal event; the scheme parses wire frames.
|
||||
events.push(ModelEvent::Status(StatusEvent {
|
||||
status: ResponseStatus::Completed,
|
||||
}));
|
||||
Ok(Box::pin(futures::stream::iter(events.into_iter().map(Ok))))
|
||||
}
|
||||
}
|
||||
|
||||
fn assert_gemini_two_call_history(items: &[agen::Item], public_name: &str, https: bool) {
|
||||
let calls = items
|
||||
.iter()
|
||||
.filter_map(|item| match item {
|
||||
agen::Item::ToolCall {
|
||||
call_id,
|
||||
name,
|
||||
arguments,
|
||||
..
|
||||
} => Some((call_id, name, arguments)),
|
||||
_ => None,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(calls.len(), 2, "same-name calls must not alias");
|
||||
assert_ne!(
|
||||
calls[0].0, calls[1].0,
|
||||
"provider correlation IDs must be distinct"
|
||||
);
|
||||
let results = items
|
||||
.iter()
|
||||
.filter_map(|item| match item {
|
||||
agen::Item::ToolResult {
|
||||
call_id,
|
||||
summary,
|
||||
content,
|
||||
disposition,
|
||||
is_error,
|
||||
..
|
||||
} => Some((call_id, summary, content, disposition, is_error)),
|
||||
_ => None,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(results.len(), 2);
|
||||
for (index, (id, name, arguments)) in calls.iter().enumerate() {
|
||||
assert_eq!(name.as_str(), public_name);
|
||||
let text = (index + 1).to_string();
|
||||
assert_eq!(
|
||||
serde_json::from_str::<Value>(arguments).unwrap(),
|
||||
if https {
|
||||
json!({})
|
||||
} else {
|
||||
json!({"text":text})
|
||||
}
|
||||
);
|
||||
let matching = results
|
||||
.iter()
|
||||
.filter(|result| result.0 == *id)
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(
|
||||
matching.len(),
|
||||
1,
|
||||
"each call has exactly one correlated result"
|
||||
);
|
||||
let (_, summary, content, disposition, is_error) = *matching[0];
|
||||
assert_eq!(
|
||||
*disposition,
|
||||
agen::ToolResultDisposition::Success,
|
||||
"{summary}: {content:?}"
|
||||
);
|
||||
assert!(!*is_error);
|
||||
let output: Value = serde_json::from_str(content.as_deref().unwrap()).unwrap();
|
||||
if https {
|
||||
assert_eq!(summary, "HTTP response observed");
|
||||
assert_eq!(output["structured"], json!({"status":503,"body_bytes":9}));
|
||||
} else {
|
||||
assert_eq!(
|
||||
output["structured"],
|
||||
json!({"tool":"example_echo","text":text})
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn gemini_committed_calls(
|
||||
saved: &session_store::RestoredState,
|
||||
public_name: &str,
|
||||
) -> Vec<session_store::LoggedHistoryEntry> {
|
||||
let calls = saved.annotated_history.iter().filter(|entry| matches!(
|
||||
&entry.item, session_store::LoggedItem::ToolCall { name, .. } if name == public_name
|
||||
)).cloned().collect::<Vec<_>>();
|
||||
assert_eq!(
|
||||
calls.len(),
|
||||
2,
|
||||
"both calls must have committed Host entries"
|
||||
);
|
||||
assert_ne!(calls[0].metadata.entry_id, calls[1].metadata.entry_id);
|
||||
for call in &calls {
|
||||
assert!(matches!(
|
||||
&call.metadata.origin,
|
||||
session_store::LoggedSessionHistoryOrigin::ModelOutput { .. }
|
||||
));
|
||||
}
|
||||
calls
|
||||
}
|
||||
|
||||
fn gemini_invocation_rows(f: &Fixture) -> Vec<(String, String)> {
|
||||
f.api
|
||||
.config_store
|
||||
.with_conn(|conn| {
|
||||
let mut statement = conn.prepare(
|
||||
"SELECT invocation_id,consumer_json FROM plugin_invocations \
|
||||
WHERE workspace_id=?1 AND plan_id=?2 AND worker_id=?3 \
|
||||
AND installation_id=?4 ORDER BY invocation_id",
|
||||
)?;
|
||||
let rows = statement.query_map(
|
||||
rusqlite::params![
|
||||
TEST_WORKSPACE_ID,
|
||||
f.plan.plan_id,
|
||||
f.source.worker_id,
|
||||
f.installed.installation.installation_id,
|
||||
],
|
||||
|row| Ok((row.get(0)?, row.get(1)?)),
|
||||
)?;
|
||||
Ok(rows.collect::<rusqlite::Result<Vec<_>>>()?)
|
||||
})
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn gemini_receipt_rows(f: &Fixture) -> Vec<(String, String, String, i64, Vec<u8>)> {
|
||||
f.api
|
||||
.config_store
|
||||
.with_conn(|conn| {
|
||||
let mut statement = conn.prepare(
|
||||
"SELECT r.invocation_id,r.effect_key,r.state,r.response_status,r.response_body \
|
||||
FROM plugin_invocations i JOIN plugin_effect_receipts r \
|
||||
USING(workspace_id,invocation_id) WHERE i.workspace_id=?1 \
|
||||
AND i.plan_id=?2 AND i.worker_id=?3 AND i.installation_id=?4 \
|
||||
ORDER BY r.invocation_id,r.effect_key",
|
||||
)?;
|
||||
let rows = statement.query_map(
|
||||
rusqlite::params![
|
||||
TEST_WORKSPACE_ID,
|
||||
f.plan.plan_id,
|
||||
f.source.worker_id,
|
||||
f.installed.installation.installation_id,
|
||||
],
|
||||
|row| {
|
||||
Ok((
|
||||
row.get(0)?,
|
||||
row.get(1)?,
|
||||
row.get(2)?,
|
||||
row.get(3)?,
|
||||
row.get(4)?,
|
||||
))
|
||||
},
|
||||
)?;
|
||||
Ok(rows.collect::<rusqlite::Result<Vec<_>>>()?)
|
||||
})
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
async fn gemini_two_calls_through_runtime_and_restore(https: bool, restore_pending: bool) {
|
||||
let mut peer = if https {
|
||||
Some(
|
||||
Peer::new(
|
||||
HOST,
|
||||
vec![
|
||||
response(503, &[], b"peer data"),
|
||||
response(503, &[], b"peer data"),
|
||||
],
|
||||
)
|
||||
.await,
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
// Pure Echo accepts distinct text inputs; HTTPSComponent only accepts
|
||||
// {}, but must still perform TWO independent effects/receipts.
|
||||
let mut f = Fixture::new(peer.as_ref().map(peer_binding)).await;
|
||||
f.api.plugin_https_transport = peer
|
||||
.as_ref()
|
||||
.map(|peer| Arc::new(peer.transport_for_requests(2)));
|
||||
allow_artifact_read(&f);
|
||||
let (artifacts, delivery_server) = artifact_source(&f, false).await;
|
||||
assert!(delivery_server.is_none());
|
||||
let worker_ref = WorkerRef::try_from(&f.source).unwrap();
|
||||
let providers = Arc::new(RuntimeProviders {
|
||||
worker: worker_ref.clone(),
|
||||
artifacts,
|
||||
owner: f
|
||||
.owner(peer.as_ref().map(|peer| peer.transport_for_requests(2)))
|
||||
.await,
|
||||
});
|
||||
let bridge = PinnedPluginToolRuntime::new(
|
||||
providers,
|
||||
&f._directory.path().join("gemini-runtime-cache"),
|
||||
)
|
||||
.unwrap();
|
||||
let public_name = format!("{}__{}", f.installed.installation.installation_id, f.tool);
|
||||
let model = GeminiTwoCallModel {
|
||||
tool_name: public_name.clone(),
|
||||
https,
|
||||
separate_frames: restore_pending,
|
||||
requests: Arc::default(),
|
||||
};
|
||||
let mut manifest = worker::WorkerManifest::from_toml(&format!(
|
||||
"[worker]\nname=\"{}\"\n[model]\nscheme=\"gemini\"\nmodel_id=\"scripted\"\n[engine]\n[scope]\nallow=[]\n",
|
||||
f.source.worker_id,
|
||||
)).unwrap();
|
||||
// Needed by the real metadata restore path, not a new Profile grant.
|
||||
manifest
|
||||
.plugins
|
||||
.selections
|
||||
.push(manifest::PluginProfileSelection {
|
||||
installation_id: f.installed.installation.installation_id.clone(),
|
||||
tools: vec![f.tool.into()],
|
||||
logging: false,
|
||||
https_slots: if https { vec!["api".into()] } else { vec![] },
|
||||
config: Default::default(),
|
||||
});
|
||||
let store_root = f._directory.path().join("gemini-tool-history");
|
||||
let metadata_root = f._directory.path().join("gemini-worker-metadata");
|
||||
let mut host = worker::Worker::new(
|
||||
manifest.clone(),
|
||||
agen::Engine::new_annotated(Box::new(model.clone()) as Box<dyn LlmClient>)
|
||||
.system_prompt("Use the selected tool twice."),
|
||||
session_store::CombinedStore::new(
|
||||
session_store::FsStore::new(&store_root).unwrap(),
|
||||
session_store::FsWorkerStore::new(&metadata_root).unwrap(),
|
||||
),
|
||||
worker::WorkerWorkspaceContext::unavailable(
|
||||
Some(worker::WorkspaceId::new(TEST_WORKSPACE_ID).unwrap()),
|
||||
"unrelated Workspace operations unavailable",
|
||||
),
|
||||
worker::WorkerFilesystemAuthority::None,
|
||||
manifest::Scope::empty(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let session_id = host.session_id();
|
||||
let snapshot = runtime_api::PluginExecutionSnapshot {
|
||||
plan_id: f.plan.plan_id.clone(),
|
||||
fingerprint: f.plan.fingerprint.clone(),
|
||||
plan: f.plan.plan.clone(),
|
||||
};
|
||||
let feature = bridge
|
||||
.prepare(&worker_ref, &snapshot, &session_id.to_string())
|
||||
.await
|
||||
.unwrap();
|
||||
let report = host.install_features(FeatureRegistryBuilder::new().with_module(feature));
|
||||
assert!(!report.has_errors(), "{}", report.error_message());
|
||||
host.enable_worker_metadata_write_through().unwrap();
|
||||
if restore_pending {
|
||||
host.add_pre_tool_call_hook(PauseBeforeFirstCall);
|
||||
}
|
||||
host.wire_history_persistence();
|
||||
let result = tokio::time::timeout(
|
||||
std::time::Duration::from_secs(30),
|
||||
host.run(vec![protocol::Segment::Text {
|
||||
content: "Call the selected tool twice in one response.".into(),
|
||||
}]),
|
||||
)
|
||||
.await
|
||||
.expect("two-call runtime deadline")
|
||||
.unwrap();
|
||||
let mut pending_calls = None;
|
||||
if restore_pending {
|
||||
assert!(matches!(result, worker::WorkerRunResult::Paused));
|
||||
assert_eq!(model.requests.lock().unwrap().len(), 1);
|
||||
assert_eq!(f.count("plugin_invocations"), 0);
|
||||
assert_eq!(f.count("plugin_effect_receipts"), 0);
|
||||
if let Some(peer) = &peer {
|
||||
assert_eq!(peer.connection_count(), 0);
|
||||
}
|
||||
let reopened = session_store::FsStore::new(&store_root).unwrap();
|
||||
let segments = reopened.list_segments(session_id).unwrap();
|
||||
assert_eq!(segments.len(), 1);
|
||||
let pending = session_store::restore(&reopened, session_id, segments[0]).unwrap();
|
||||
let calls = gemini_committed_calls(&pending, &public_name);
|
||||
let consumers = persisted_call_consumers(session_id, &pending, &public_name);
|
||||
assert_eq!(consumers.len(), 2);
|
||||
assert_ne!(consumers[0], consumers[1]);
|
||||
assert!(
|
||||
!pending
|
||||
.history
|
||||
.iter()
|
||||
.any(|item| matches!(item, agen::Item::ToolResult { .. }))
|
||||
);
|
||||
// Inspect live Host annotations as well as the reopened Store.
|
||||
let live_ids = host
|
||||
.session_history()
|
||||
.iter()
|
||||
.filter_map(|entry| match &entry.item {
|
||||
agen::Item::ToolCall { .. } => Some(entry.annotation.entry_id.clone()),
|
||||
_ => None,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(
|
||||
live_ids,
|
||||
calls
|
||||
.iter()
|
||||
.map(|entry| entry.metadata.entry_id.clone())
|
||||
.collect::<Vec<_>>()
|
||||
);
|
||||
drop(host);
|
||||
let mut restored = worker::Worker::prepare_restored_from_worker_metadata_with_context_and_model_client(
|
||||
&manifest.worker.name, manifest.clone(), session_store::CombinedStore::new(
|
||||
reopened, session_store::FsWorkerStore::new(&metadata_root).unwrap(),
|
||||
), worker::PromptCatalogSource::builtins_only(),
|
||||
worker::WorkerWorkspaceContext::unavailable(Some(worker::WorkspaceId::new(TEST_WORKSPACE_ID).unwrap()), "unrelated Workspace operations unavailable"),
|
||||
worker::WorkerFilesystemAuthority::None, Some(Box::new(model.clone())),
|
||||
).await.unwrap().activate().await.unwrap();
|
||||
assert_eq!(restored.session_id(), session_id);
|
||||
let reloaded = session_store::restore(
|
||||
&session_store::FsStore::new(&store_root).unwrap(),
|
||||
session_id,
|
||||
segments[0],
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(gemini_committed_calls(&reloaded, &public_name), calls);
|
||||
assert_eq!(
|
||||
persisted_call_consumers(session_id, &reloaded, &public_name),
|
||||
consumers
|
||||
);
|
||||
let restored_ids = restored
|
||||
.session_history()
|
||||
.iter()
|
||||
.filter_map(|entry| match &entry.item {
|
||||
agen::Item::ToolCall { .. } => Some(entry.annotation.entry_id.clone()),
|
||||
_ => None,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(
|
||||
restored_ids, live_ids,
|
||||
"metadata reopen retains both live Host entries"
|
||||
);
|
||||
let feature = bridge
|
||||
.prepare(&worker_ref, &snapshot, &session_id.to_string())
|
||||
.await
|
||||
.unwrap();
|
||||
let report =
|
||||
restored.install_features(FeatureRegistryBuilder::new().with_module(feature));
|
||||
assert!(!report.has_errors(), "{}", report.error_message());
|
||||
restored.wire_history_persistence();
|
||||
let result =
|
||||
tokio::time::timeout(std::time::Duration::from_secs(30), restored.resume())
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert!(matches!(result, worker::WorkerRunResult::Finished));
|
||||
pending_calls = Some((calls, consumers));
|
||||
host = restored;
|
||||
} else {
|
||||
assert!(matches!(result, worker::WorkerRunResult::Finished));
|
||||
}
|
||||
assert_gemini_two_call_history(&host.history(), &public_name, https);
|
||||
{
|
||||
use agen::llm_client::scheme::{Scheme, gemini::GeminiScheme};
|
||||
let requests = model.requests.lock().unwrap();
|
||||
assert_eq!(requests.len(), 2);
|
||||
assert_eq!(
|
||||
requests[0]
|
||||
.tools
|
||||
.iter()
|
||||
.map(|tool| tool.name.as_str())
|
||||
.collect::<Vec<_>>(),
|
||||
[public_name.as_str()]
|
||||
);
|
||||
assert_gemini_two_call_history(&requests[1].items, &public_name, https);
|
||||
let scheme = GeminiScheme::new();
|
||||
let body =
|
||||
scheme.build_request_body("scripted", &requests[1], &scheme.default_capability());
|
||||
let responses = body["contents"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.flat_map(|content| content["parts"].as_array().unwrap())
|
||||
.filter_map(|part| part.get("functionResponse"))
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(responses.len(), 2);
|
||||
for response in responses {
|
||||
assert_eq!(
|
||||
response["name"], public_name,
|
||||
"functionResponse uses the real Tool name, not a generated call ID"
|
||||
);
|
||||
assert_eq!(response["response"]["name"], public_name);
|
||||
}
|
||||
}
|
||||
let reopened = session_store::FsStore::new(&store_root).unwrap();
|
||||
let segments = reopened.list_segments(session_id).unwrap();
|
||||
assert_eq!(segments.len(), 1);
|
||||
let saved = session_store::restore(&reopened, session_id, segments[0]).unwrap();
|
||||
assert_gemini_two_call_history(&saved.history, &public_name, https);
|
||||
let calls = gemini_committed_calls(&saved, &public_name);
|
||||
let mut consumers = persisted_call_consumers(session_id, &saved, &public_name);
|
||||
assert_eq!(consumers.len(), 2);
|
||||
assert_ne!(consumers[0], consumers[1]);
|
||||
if let Some((pending, expected_consumers)) = pending_calls {
|
||||
assert_eq!(
|
||||
calls, pending,
|
||||
"resume preserves both committed Host entries"
|
||||
);
|
||||
assert_eq!(consumers, expected_consumers);
|
||||
}
|
||||
let invocations = gemini_invocation_rows(&f);
|
||||
assert_eq!(invocations.len(), 2);
|
||||
assert_ne!(invocations[0].0, invocations[1].0);
|
||||
let mut actual_consumers = invocations
|
||||
.iter()
|
||||
.map(
|
||||
|(_, consumer)| match serde_json::from_str::<PluginConsumer>(consumer).unwrap() {
|
||||
PluginConsumer::ToolCall {
|
||||
tool_call_id,
|
||||
tool_name,
|
||||
} => {
|
||||
assert_eq!(tool_name, f.tool);
|
||||
tool_call_id
|
||||
}
|
||||
other => panic!("unexpected consumer: {other:?}"),
|
||||
},
|
||||
)
|
||||
.collect::<Vec<_>>();
|
||||
consumers.sort();
|
||||
actual_consumers.sort();
|
||||
assert_eq!(
|
||||
actual_consumers, consumers,
|
||||
"SQLite admission uses both durable Host consumers"
|
||||
);
|
||||
let receipts = gemini_receipt_rows(&f);
|
||||
assert_eq!(f.count("plugin_invocations"), 2);
|
||||
assert_eq!(receipts.len(), if https { 2 } else { 0 });
|
||||
assert_eq!(f.count("plugin_effect_receipts"), if https { 2 } else { 0 });
|
||||
if let Some(peer) = &mut peer {
|
||||
assert_eq!(
|
||||
peer.connection_count(),
|
||||
2,
|
||||
"identical HTTPS inputs are separate effects"
|
||||
);
|
||||
for _ in 0..2 {
|
||||
let observed =
|
||||
tokio::time::timeout(std::time::Duration::from_secs(5), peer.observed())
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(
|
||||
String::from_utf8(observed)
|
||||
.unwrap()
|
||||
.starts_with("GET /status HTTP/1.1\r\n")
|
||||
);
|
||||
}
|
||||
for (index, (invocation, key, state, status, body)) in receipts.iter().enumerate() {
|
||||
assert_eq!(invocation, &invocations[index].0);
|
||||
assert_eq!(key, "status-v1");
|
||||
assert_eq!(state, "completed");
|
||||
assert_eq!(*status, 503);
|
||||
assert_eq!(body, b"peer data");
|
||||
}
|
||||
}
|
||||
// A completed metadata reopen/activation is not a new run. Do not call
|
||||
// resume() (which requires a paused logical run), or fabricate history.
|
||||
drop(host);
|
||||
let mut completed =
|
||||
worker::Worker::prepare_restored_from_worker_metadata_with_context_and_model_client(
|
||||
&manifest.worker.name,
|
||||
manifest.clone(),
|
||||
session_store::CombinedStore::new(
|
||||
reopened,
|
||||
session_store::FsWorkerStore::new(&metadata_root).unwrap(),
|
||||
),
|
||||
worker::PromptCatalogSource::builtins_only(),
|
||||
worker::WorkerWorkspaceContext::unavailable(
|
||||
Some(worker::WorkspaceId::new(TEST_WORKSPACE_ID).unwrap()),
|
||||
"unrelated Workspace operations unavailable",
|
||||
),
|
||||
worker::WorkerFilesystemAuthority::None,
|
||||
Some(Box::new(model.clone())),
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.activate()
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(completed.session_id(), session_id);
|
||||
let feature = bridge
|
||||
.prepare(&worker_ref, &snapshot, &session_id.to_string())
|
||||
.await
|
||||
.unwrap();
|
||||
let report = completed.install_features(FeatureRegistryBuilder::new().with_module(feature));
|
||||
assert!(!report.has_errors(), "{}", report.error_message());
|
||||
completed.wire_history_persistence();
|
||||
assert!(completed.engine().active_run_turn_count().is_none());
|
||||
assert_gemini_two_call_history(&completed.history(), &public_name, https);
|
||||
let completed_ids = completed
|
||||
.session_history()
|
||||
.iter()
|
||||
.filter_map(|entry| match &entry.item {
|
||||
agen::Item::ToolCall { .. } => Some(entry.annotation.entry_id.clone()),
|
||||
_ => None,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(
|
||||
completed_ids,
|
||||
calls
|
||||
.iter()
|
||||
.map(|entry| entry.metadata.entry_id.clone())
|
||||
.collect::<Vec<_>>(),
|
||||
"completed Worker activation retains both committed Host identities"
|
||||
);
|
||||
let after = session_store::restore(
|
||||
&session_store::FsStore::new(&store_root).unwrap(),
|
||||
session_id,
|
||||
segments[0],
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(after.annotated_history, saved.annotated_history);
|
||||
assert_eq!(gemini_committed_calls(&after, &public_name), calls);
|
||||
assert_eq!(gemini_invocation_rows(&f), invocations);
|
||||
assert_eq!(gemini_receipt_rows(&f), receipts);
|
||||
assert_eq!(model.requests.lock().unwrap().len(), 2);
|
||||
assert_eq!(f.count("plugin_invocations"), 2);
|
||||
assert_eq!(f.count("plugin_effect_receipts"), if https { 2 } else { 0 });
|
||||
if let Some(peer) = &peer {
|
||||
assert_eq!(peer.connection_count(), 2);
|
||||
}
|
||||
drop(completed);
|
||||
f.shutdown().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn gemini_same_response_pure_calls_keep_distinct_inputs_and_completed_restore_is_inert() {
|
||||
gemini_two_calls_through_runtime_and_restore(false, false).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn gemini_same_response_https_calls_commit_two_effects_and_completed_restore_is_inert() {
|
||||
gemini_two_calls_through_runtime_and_restore(true, false).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn gemini_same_response_pending_pure_calls_restore_both_host_consumers() {
|
||||
gemini_two_calls_through_runtime_and_restore(false, true).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn gemini_same_response_pending_https_calls_restore_both_host_consumers_and_effects() {
|
||||
gemini_two_calls_through_runtime_and_restore(true, true).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn remote_pinned_https_tool_commits_normal_history_and_receipt_then_fences_live_denial() {
|
||||
https_through_runtime_and_history(true, false).await;
|
||||
|
||||
Reference in New Issue
Block a user