chore: import upstream snapshot with attribution
FreeBSD Smoke / FreeBSD Smoke (x86_64) (push) Has been cancelled
CI / Quality Guardrails (push) Has been cancelled
CI / Build & Test (macos-latest) (push) Has been cancelled
CI / Build & Test (ubuntu-latest) (push) Has been cancelled
CI / Build & Test (windows-latest) (push) Has been cancelled
CI / Format (push) Has been cancelled
CI / PowerShell Syntax (push) Has been cancelled
CI / Windows Cross-Target Check (Linux) (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:10:34 +08:00
commit a789495a98
1551 changed files with 718128 additions and 0 deletions
@@ -0,0 +1,724 @@
use super::*;
use jcode_base::message::{ContentBlock, Message, Role};
struct EnvVarGuard {
key: &'static str,
previous: Option<std::ffi::OsString>,
}
impl EnvVarGuard {
fn set_path(key: &'static str, value: &std::path::Path) -> Self {
let previous = std::env::var_os(key);
jcode_base::env::set_var(key, value);
Self { key, previous }
}
fn set_value(key: &'static str, value: &str) -> Self {
let previous = std::env::var_os(key);
jcode_base::env::set_var(key, value);
Self { key, previous }
}
fn unset(key: &'static str) -> Self {
let previous = std::env::var_os(key);
jcode_base::env::remove_var(key);
Self { key, previous }
}
}
impl Drop for EnvVarGuard {
fn drop(&mut self) {
if let Some(previous) = &self.previous {
jcode_base::env::set_var(self.key, previous);
} else {
jcode_base::env::remove_var(self.key);
}
}
}
#[test]
fn available_models_include_gemini_defaults() {
let provider = GeminiProvider::new();
let models = provider.available_models();
assert!(models.contains(&"gemini-3-pro-preview"));
assert!(models.contains(&"gemini-3.1-pro-preview"));
assert!(models.contains(&"gemini-2.5-pro"));
assert!(models.contains(&"gemini-2.5-flash"));
}
#[test]
fn set_model_accepts_gemini_models() {
let provider = GeminiProvider::new();
provider.set_model("gemini-2.5-flash").unwrap();
assert_eq!(provider.model(), "gemini-2.5-flash");
}
#[test]
fn detects_model_not_found_errors() {
let err = anyhow::anyhow!(
"Gemini request generateContent failed (HTTP 404 Not Found): {{\"error\":{{\"status\":\"NOT_FOUND\",\"message\":\"Requested entity was not found.\"}}}}"
);
assert!(is_gemini_model_not_found_error(&err));
}
#[test]
fn fallback_models_skip_current_model() {
assert_eq!(
gemini_fallback_models("gemini-2.5-flash"),
vec![
"gemini-3.1-pro-preview",
"gemini-3-pro-preview",
"gemini-2.5-pro",
"gemini-3-flash-preview",
"gemini-2.0-flash",
]
);
}
#[test]
fn extract_gemini_model_ids_discovers_nested_models() {
let response = json!({
"routing": {
"manual": {
"models": [
{"id": "gemini-3-pro-preview"},
{"name": "gemini-3.1-pro-preview"}
]
},
"auto": ["gemini-3-flash-preview", "not-a-model"]
}
});
assert_eq!(
extract_gemini_model_ids(&response),
vec![
"gemini-3.1-pro-preview".to_string(),
"gemini-3-pro-preview".to_string(),
"gemini-3-flash-preview".to_string(),
]
);
}
#[test]
fn available_models_display_prefers_discovered_models_and_current_model() {
let provider = GeminiProvider::new();
provider.set_model("gemini-4-pro-preview").unwrap();
*provider.fetched_models.write().unwrap() = vec![
"gemini-3-flash-preview".to_string(),
"gemini-3-pro-preview".to_string(),
];
assert_eq!(
provider.available_models_display(),
vec![
"gemini-3-pro-preview".to_string(),
"gemini-3-flash-preview".to_string(),
"gemini-4-pro-preview".to_string(),
]
);
}
#[test]
fn available_models_display_without_discovery_uses_current_model_only() {
let _guard = jcode_base::storage::lock_test_env();
let temp = tempfile::TempDir::new().expect("tempdir");
let _home = EnvVarGuard::set_path("JCODE_HOME", temp.path());
let provider = GeminiProvider::new();
provider.set_model("gemini-4-pro-preview").unwrap();
assert_eq!(
provider.available_models_display(),
vec!["gemini-4-pro-preview".to_string()]
);
}
#[test]
fn available_models_display_seeds_from_persisted_catalog() {
let _guard = jcode_base::storage::lock_test_env();
let temp = tempfile::TempDir::new().expect("tempdir");
let _home = EnvVarGuard::set_path("JCODE_HOME", temp.path());
let path = GeminiProvider::persisted_catalog_path().expect("catalog path");
jcode_base::storage::write_json(
&path,
&PersistedCatalog {
models: vec!["gemini-3-pro-preview".to_string()],
fetched_at_rfc3339: chrono::Utc::now().to_rfc3339(),
},
)
.expect("write persisted catalog");
let provider = GeminiProvider::new();
assert!(
provider
.available_models_display()
.contains(&"gemini-3-pro-preview".to_string())
);
}
#[test]
fn build_contents_replays_thought_signature_on_function_call() {
// Gemini 3 (Antigravity Cloud Code backend) rejects function calls that
// omit the original thoughtSignature on later turns. Verify the signature
// captured on the ToolUse block is replayed verbatim on the functionCall
// part. A later unsigned call inherits the most recent real signature so the
// backend (which 400s a fully-unsigned turn) accepts it (issue #339).
let messages = vec![
Message {
role: Role::Assistant,
content: vec![ContentBlock::ToolUse {
id: "call_sig".to_string(),
name: "read".to_string(),
input: json!({"path":"README.md"}),
thought_signature: Some("SIGNATURE_ABC".to_string()),
}],
timestamp: None,
tool_duration_ms: None,
},
Message {
role: Role::Assistant,
content: vec![ContentBlock::ToolUse {
id: "call_nosig".to_string(),
name: "bash".to_string(),
input: json!({"command":"ls"}),
thought_signature: None,
}],
timestamp: None,
tool_duration_ms: None,
},
];
let contents = build_contents(&messages);
assert_eq!(
contents[0].parts[0].thought_signature.as_deref(),
Some("SIGNATURE_ABC"),
"signature must be replayed on the matching function call part"
);
assert_eq!(
contents[1].parts[0].thought_signature.as_deref(),
Some("SIGNATURE_ABC"),
"an unsigned later call must inherit the most recent real signature so \
the backend does not reject a fully-unsigned turn"
);
}
#[test]
fn build_contents_replays_every_signature_across_multi_tool_history() {
// Regression guard for the Antigravity/Cloud Code 400
// ("Function call is missing a thought_signature ... position 5"): the
// backend validates *every* functionCall in the replayed history, not just
// the latest one. A multi-turn transcript where an earlier tool_use drops
// its signature is exactly what triggers the field failure, so assert that
// each captured signature survives serialization onto its matching part.
let signatures = ["SIG_A", "SIG_B", "SIG_C"];
let mut messages = Vec::new();
for (idx, sig) in signatures.iter().enumerate() {
messages.push(Message {
role: Role::Assistant,
content: vec![ContentBlock::ToolUse {
id: format!("call_{idx}"),
name: "bash".to_string(),
input: json!({ "command": format!("echo {idx}") }),
thought_signature: Some(sig.to_string()),
}],
timestamp: None,
tool_duration_ms: None,
});
messages.push(Message {
role: Role::User,
content: vec![ContentBlock::ToolResult {
tool_use_id: format!("call_{idx}"),
content: format!("out {idx}"),
is_error: Some(false),
}],
timestamp: None,
tool_duration_ms: None,
});
}
let contents = build_contents(&messages);
let replayed: Vec<Option<&str>> = contents
.iter()
.flat_map(|content| content.parts.iter())
.filter(|part| part.function_call.is_some())
.map(|part| part.thought_signature.as_deref())
.collect();
assert_eq!(
replayed,
vec![Some("SIG_A"), Some("SIG_B"), Some("SIG_C")],
"every functionCall in the history must carry its captured thought_signature, \
not just the most recent one"
);
}
#[test]
fn build_contents_carries_first_signature_onto_unsigned_same_turn_siblings() {
// Issue #339: when Gemini-3 emits MULTIPLE function calls in ONE turn it
// signs only the first; the siblings persist without a signature. The
// Antigravity/Cloud Code backend then rejects the unsigned siblings with
// "Function call is missing a thought_signature ... position N". Verify the
// first call's signature is carried forward onto same-turn siblings that
// lack one (the backend accepts a replayed signature on sibling calls).
let messages = vec![Message {
role: Role::Assistant,
content: vec![
ContentBlock::ToolUse {
id: "call_todo".to_string(),
name: "todo".to_string(),
input: json!({ "items": ["a", "b"] }),
thought_signature: Some("SIG_TURN_1".to_string()),
},
ContentBlock::ToolUse {
id: "call_bash".to_string(),
name: "bash".to_string(),
input: json!({ "command": "ls" }),
thought_signature: None,
},
ContentBlock::ToolUse {
id: "call_write".to_string(),
name: "write".to_string(),
input: json!({ "path": "a.txt", "content": "hi" }),
thought_signature: None,
},
],
timestamp: None,
tool_duration_ms: None,
}];
let contents = build_contents(&messages);
let replayed: Vec<Option<&str>> = contents
.iter()
.flat_map(|content| content.parts.iter())
.filter(|part| part.function_call.is_some())
.map(|part| part.thought_signature.as_deref())
.collect();
assert_eq!(
replayed,
vec![Some("SIG_TURN_1"), Some("SIG_TURN_1"), Some("SIG_TURN_1")],
"every functionCall in a multi-call turn must carry a signature so the \
backend does not reject unsigned siblings"
);
}
#[test]
fn build_contents_carries_signature_forward_across_turns_for_unsigned_calls() {
// Issue #339: the Antigravity/Cloud Code backend 400s an assistant turn
// whose function calls are ALL unsigned. A later turn made entirely of
// locally synthesized / unsigned tool calls (auto-poke continuation, batch,
// manual tool use, or an imported pre-signature session) must inherit the
// most recent real signature from earlier in the conversation so at least
// one call carries a signature and the backend accepts the turn.
let messages = vec![
Message {
role: Role::Assistant,
content: vec![ContentBlock::ToolUse {
id: "turn1".to_string(),
name: "read".to_string(),
input: json!({ "path": "README.md" }),
thought_signature: Some("SIG_TURN_1".to_string()),
}],
timestamp: None,
tool_duration_ms: None,
},
Message {
role: Role::User,
content: vec![ContentBlock::ToolResult {
tool_use_id: "turn1".to_string(),
content: "ok".to_string(),
is_error: Some(false),
}],
timestamp: None,
tool_duration_ms: None,
},
Message {
role: Role::Assistant,
content: vec![ContentBlock::ToolUse {
id: "turn2".to_string(),
name: "bash".to_string(),
input: json!({ "command": "ls" }),
thought_signature: None,
}],
timestamp: None,
tool_duration_ms: None,
},
];
let contents = build_contents(&messages);
let last_turn_sig = contents
.last()
.and_then(|content| content.parts.first())
.and_then(|part| part.thought_signature.as_deref());
assert_eq!(
last_turn_sig,
Some("SIG_TURN_1"),
"a fully-unsigned later turn must inherit the most recent real signature \
so the backend does not reject it"
);
}
#[test]
fn build_contents_leaves_unsigned_calls_unsigned_when_no_prior_signature_exists() {
// If the conversation has never produced a real signature there is nothing
// to carry; we must not fabricate one out of thin air.
let messages = vec![Message {
role: Role::Assistant,
content: vec![ContentBlock::ToolUse {
id: "call".to_string(),
name: "bash".to_string(),
input: json!({ "command": "ls" }),
thought_signature: None,
}],
timestamp: None,
tool_duration_ms: None,
}];
let contents = build_contents(&messages);
assert_eq!(
contents[0].parts[0].thought_signature, None,
"with no prior signature in the conversation, an unsigned call stays unsigned"
);
}
#[test]
fn build_contents_preserves_tool_calls_and_results() {
let messages = vec![
Message {
role: Role::Assistant,
content: vec![ContentBlock::ToolUse {
id: "call_1".to_string(),
name: "read".to_string(),
input: json!({"path":"README.md"}),
thought_signature: None,
}],
timestamp: None,
tool_duration_ms: None,
},
Message {
role: Role::User,
content: vec![ContentBlock::ToolResult {
tool_use_id: "call_1".to_string(),
content: "ok".to_string(),
is_error: None,
}],
timestamp: None,
tool_duration_ms: None,
},
];
let contents = build_contents(&messages);
assert_eq!(contents.len(), 2);
assert_eq!(contents[0].role, "model");
assert_eq!(contents[1].role, "user");
assert_eq!(
contents[0].parts[0].function_call.as_ref().unwrap().name,
"read"
);
assert_eq!(
contents[1].parts[0]
.function_response
.as_ref()
.unwrap()
.name,
"read"
);
}
#[test]
fn build_contents_normalizes_non_object_tool_call_args_for_gemini_struct() {
let messages = vec![Message {
role: Role::Assistant,
content: vec![ContentBlock::ToolUse {
id: "call_primitive".to_string(),
name: "read".to_string(),
input: json!(20),
thought_signature: None,
}],
timestamp: None,
tool_duration_ms: None,
}];
let contents = build_contents(&messages);
assert_eq!(
contents[0].parts[0].function_call.as_ref().unwrap().args,
json!({})
);
}
#[test]
fn build_tools_uses_function_declarations() {
let defs = vec![ToolDefinition {
name: "read".to_string(),
description: "Read a file".to_string(),
input_schema: json!({"type":"object","properties":{"path":{"type":"string"}}}),
}];
let built = build_tools(&defs).unwrap();
assert_eq!(built.len(), 1);
assert_eq!(built[0].function_declarations[0].name, "read");
}
fn schema_contains_key(schema: &Value, key: &str) -> bool {
match schema {
Value::Object(map) => {
map.contains_key(key) || map.values().any(|value| schema_contains_key(value, key))
}
Value::Array(items) => items.iter().any(|value| schema_contains_key(value, key)),
_ => false,
}
}
#[test]
fn build_tools_rewrites_const_for_gemini_schema_compatibility() {
let defs = vec![ToolDefinition {
name: "batch".to_string(),
description: "Batch tools".to_string(),
input_schema: json!({
"type": "object",
"properties": {
"tool_calls": {
"type": "array",
"items": {
"oneOf": [
{
"type": "object",
"properties": {
"tool": { "type": "string", "const": "read" },
"file_path": { "type": "string" }
},
"required": ["tool", "file_path"]
}
]
}
}
}
}),
}];
let built = build_tools(&defs).expect("gemini tools");
let parameters = &built[0].function_declarations[0].parameters;
assert!(!schema_contains_key(parameters, "const"));
assert_eq!(
parameters["properties"]["tool_calls"]["items"]["oneOf"][0]["properties"]["tool"]["enum"],
json!(["read"])
);
}
#[test]
fn build_tools_strips_additional_properties_for_gemini_schema_compatibility() {
// The Gemini Code Assist generateContent endpoint rejects `additionalProperties`
// (and other draft-JSON-Schema keywords) with HTTP 400, so build_tools must
// strip them recursively while preserving the rest of the schema.
let defs = vec![ToolDefinition {
name: "read".to_string(),
description: "Reads a file".to_string(),
input_schema: json!({
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"file_path": { "type": "string" },
"opts": {
"type": "object",
"properties": { "limit": { "type": "integer" } },
"additionalProperties": false
}
},
"required": ["file_path"],
"additionalProperties": false
}),
}];
let built = build_tools(&defs).expect("gemini tools");
let parameters = &built[0].function_declarations[0].parameters;
assert!(!schema_contains_key(parameters, "additionalProperties"));
assert!(!schema_contains_key(parameters, "$schema"));
// Real schema content is preserved.
assert_eq!(
parameters["properties"]["file_path"]["type"],
json!("string")
);
assert_eq!(
parameters["properties"]["opts"]["properties"]["limit"]["type"],
json!("integer")
);
assert_eq!(parameters["required"], json!(["file_path"]));
}
#[test]
fn parses_prompt_feedback_block_reason() {
let response: VertexGenerateContentResponse = serde_json::from_value(json!({
"promptFeedback": {
"blockReason": "PROHIBITED_CONTENT",
"blockReasonMessage": "Prompt violated policy"
}
}))
.expect("parse prompt feedback");
let feedback = response.prompt_feedback.expect("missing prompt feedback");
assert_eq!(feedback.block_reason.as_deref(), Some("PROHIBITED_CONTENT"));
assert_eq!(
feedback.block_reason_message.as_deref(),
Some("Prompt violated policy")
);
}
#[test]
fn parses_candidate_finish_message() {
let response: VertexGenerateContentResponse = serde_json::from_value(json!({
"candidates": [
{
"finishReason": "SAFETY",
"finishMessage": "Response blocked by safety filters"
}
]
}))
.expect("parse candidate");
let candidate = response
.candidates
.expect("missing candidates")
.into_iter()
.next()
.expect("missing first candidate");
assert_eq!(candidate.finish_reason.as_deref(), Some("SAFETY"));
assert_eq!(
candidate.finish_message.as_deref(),
Some("Response blocked by safety filters")
);
}
#[test]
fn auth_mode_prefers_api_key_when_present() {
let _guard = jcode_base::storage::lock_test_env();
let temp = tempfile::TempDir::new().expect("tempdir");
let _home = EnvVarGuard::set_path("JCODE_HOME", temp.path());
let _google = EnvVarGuard::unset("GOOGLE_API_KEY");
let _force = EnvVarGuard::unset("JCODE_GEMINI_FORCE_OAUTH");
let _key = EnvVarGuard::set_value("GEMINI_API_KEY", "test-developer-key");
match GeminiProvider::auth_mode() {
GeminiAuthMode::ApiKey(key) => assert_eq!(key, "test-developer-key"),
GeminiAuthMode::Oauth => panic!("expected API-key auth mode when GEMINI_API_KEY is set"),
}
}
#[test]
fn auth_mode_force_oauth_overrides_api_key() {
let _guard = jcode_base::storage::lock_test_env();
let temp = tempfile::TempDir::new().expect("tempdir");
let _home = EnvVarGuard::set_path("JCODE_HOME", temp.path());
let _google = EnvVarGuard::unset("GOOGLE_API_KEY");
let _key = EnvVarGuard::set_value("GEMINI_API_KEY", "test-developer-key");
let _force = EnvVarGuard::set_value("JCODE_GEMINI_FORCE_OAUTH", "1");
assert!(matches!(GeminiProvider::auth_mode(), GeminiAuthMode::Oauth));
}
#[test]
fn auth_mode_defaults_to_oauth_without_api_key() {
let _guard = jcode_base::storage::lock_test_env();
let temp = tempfile::TempDir::new().expect("tempdir");
let _home = EnvVarGuard::set_path("JCODE_HOME", temp.path());
let _key = EnvVarGuard::unset("GEMINI_API_KEY");
let _google = EnvVarGuard::unset("GOOGLE_API_KEY");
let _force = EnvVarGuard::unset("JCODE_GEMINI_FORCE_OAUTH");
assert!(matches!(GeminiProvider::auth_mode(), GeminiAuthMode::Oauth));
}
#[test]
fn developer_api_base_url_defaults_to_generativelanguage() {
let _guard = jcode_base::storage::lock_test_env();
let _endpoint = EnvVarGuard::unset("GEMINI_API_ENDPOINT");
let _version = EnvVarGuard::unset("GEMINI_API_VERSION");
assert_eq!(
GeminiProvider::developer_api_base_url(),
"https://generativelanguage.googleapis.com/v1beta"
);
}
#[test]
fn developer_api_base_url_honors_env_overrides() {
let _guard = jcode_base::storage::lock_test_env();
let _endpoint = EnvVarGuard::set_value("GEMINI_API_ENDPOINT", "https://example.test/");
let _version = EnvVarGuard::set_value("GEMINI_API_VERSION", "/v9/");
assert_eq!(
GeminiProvider::developer_api_base_url(),
"https://example.test/v9"
);
}
#[test]
fn developer_api_response_parses_without_code_assist_envelope() {
// The Developer API returns the bare generateContent body; ensure it maps
// onto the same response type the Code Assist envelope yields.
let response: VertexGenerateContentResponse = serde_json::from_value(json!({
"candidates": [
{
"content": {
"role": "model",
"parts": [{ "text": "hello from developer api" }]
},
"finishReason": "STOP"
}
],
"usageMetadata": {
"promptTokenCount": 3,
"candidatesTokenCount": 5
}
}))
.expect("parse developer api response");
let candidate = response
.candidates
.expect("missing candidates")
.into_iter()
.next()
.expect("missing first candidate");
assert_eq!(candidate.finish_reason.as_deref(), Some("STOP"));
let text = candidate
.content
.expect("missing content")
.parts
.into_iter()
.next()
.and_then(|part| part.text)
.expect("missing text");
assert_eq!(text, "hello from developer api");
}
#[test]
fn system_instruction_tool_guard_only_applies_with_tools() {
// Without tools, the system instruction is passed through unchanged.
let plain = super::build_system_instruction_with_tool_guard("You are helpful.", false)
.expect("system instruction present");
let plain_text = plain.parts[0].text.clone().unwrap();
assert_eq!(plain_text, "You are helpful.");
assert!(!plain_text.contains("Function calling"));
// With tools, the MALFORMED_FUNCTION_CALL prevention guidance is appended.
let guarded = super::build_system_instruction_with_tool_guard("You are helpful.", true)
.expect("system instruction present");
let guarded_text = guarded.parts[0].text.clone().unwrap();
assert!(guarded_text.starts_with("You are helpful."));
assert!(guarded_text.contains("Function calling"));
assert!(guarded_text.contains("native function call, not code"));
assert!(guarded_text.contains("default_api."));
}
#[test]
fn system_instruction_tool_guard_with_empty_system_still_emits_guidance() {
// An empty base system prompt plus tools must still carry the guard so the
// model is steered away from pseudo-code tool calls.
let guarded = super::build_system_instruction_with_tool_guard("", true)
.expect("guard-only instruction present");
let text = guarded.parts[0].text.clone().unwrap();
assert!(text.contains("Function calling"));
// Empty system and no tools yields no instruction at all.
assert!(super::build_system_instruction_with_tool_guard("", false).is_none());
}
File diff suppressed because it is too large Load Diff