use super::utils::{open_readonly_sqlite_opt, sqlite_for_each_row_on};
use super::{normalize_workspace_key, workspace_label_from_key, UnifiedMessage};
use crate::{pricing, provider_identity, TokenBreakdown};
use rusqlite::Connection;
use std::collections::{HashMap, HashSet};
use std::ops::RangeInclusive;
use std::path::Path;
pub fn parse_antigravity_cli_file(path: &Path) -> Vec<UnifiedMessage> {
let Some(conn) = open_readonly_sqlite_opt(path) else {
return Vec::new();
};
let session_id = path
.file_stem()
.and_then(|stem| stem.to_str())
.unwrap_or("unknown")
.to_string();
let meta = read_trajectory_meta(&conn, path);
let mut blobs: Vec<Vec<u8>> = Vec::new();
sqlite_for_each_row_on(
&conn,
path,
"SELECT data FROM gen_metadata ORDER BY idx",
None,
&mut |row| {
blobs.push(row.get::<_, Vec<u8>>(0)?);
Ok(())
},
);
let session_models = SessionModels::from_blobs(&blobs);
let mut messages = Vec::new();
let mut seen_response_ids: HashSet<String> = HashSet::new();
for blob in &blobs {
if let Some(mut message) = parse_gen_metadata(
blob,
&session_id,
meta.fallback_ms,
meta.created_ms,
&session_models,
&mut seen_response_ids,
) {
if meta.workspace_key.is_some() {
message.set_workspace(meta.workspace_key.clone(), meta.workspace_label.clone());
}
messages.push(message);
}
}
messages
}
#[derive(Default)]
struct SessionModels {
by_display: HashMap<String, String>,
sole_model: Option<String>,
}
impl SessionModels {
fn from_blobs(blobs: &[Vec<u8>]) -> Self {
let mut by_display: HashMap<&str, Option<&str>> = HashMap::new();
let mut distinct: HashSet<&str> = HashSet::new();
let mut unresolved_labels: Vec<&str> = Vec::new();
for blob in blobs {
let Some(chat_model) = message_field(blob, 1) else {
continue;
};
let label = non_empty_string_field(chat_model, 21);
let Some(model) = non_empty_string_field(chat_model, 19) else {
unresolved_labels.extend(label);
continue;
};
if is_antigravity_routing_label(model) {
unresolved_labels.extend(label);
continue;
}
distinct.insert(model);
if let Some(label) = label {
by_display
.entry(label)
.and_modify(|resolved| {
if let Some(existing) = *resolved {
if existing != model {
let existing_canon =
pricing::aliases::resolve_alias(existing).unwrap_or(existing);
let new_canon =
pricing::aliases::resolve_alias(model).unwrap_or(model);
if existing_canon != new_canon {
*resolved = None;
}
}
}
})
.or_insert(Some(model));
}
}
let by_display: HashMap<String, String> = by_display
.into_iter()
.filter_map(|(label, model)| Some((label.to_string(), model?.to_string())))
.collect();
let every_label_identified = unresolved_labels
.iter()
.all(|label| by_display.contains_key(*label));
let sole_model = match (distinct.len(), every_label_identified) {
(1, true) => distinct.iter().next().map(|model| (*model).to_string()),
_ => None,
};
Self {
by_display,
sole_model,
}
}
fn recover(&self, chat_model: &[u8]) -> Option<&str> {
match non_empty_string_field(chat_model, 21) {
Some(label) => self.by_display.get(label).map(String::as_str),
None => self.sole_model.as_deref(),
}
}
}
fn is_antigravity_routing_label(model: &str) -> bool {
model.trim().eq_ignore_ascii_case("gemini-default")
}
fn display_label_to_model_id(label: &str) -> Option<&'static str> {
match label.trim() {
"Gemini 3.5 Flash (Low)" => Some("gemini-3.5-flash-extra-low"),
"Gemini 3.5 Flash (Medium)" => Some("gemini-3.5-flash-medium"),
"Gemini 3.5 Flash (High)" => Some("gemini-3.5-flash-high"),
_ => None,
}
}
fn parse_gen_metadata(
blob: &[u8],
session_id: &str,
session_timestamp: i64,
session_anchor: Option<i64>,
session_models: &SessionModels,
seen_response_ids: &mut HashSet<String>,
) -> Option<UnifiedMessage> {
let chat_model = message_field(blob, 1)?;
let usage = message_field(chat_model, 4)?;
let timestamp = message_field(chat_model, 9)
.and_then(|gen| generation_timestamp_ms(gen, session_anchor))
.unwrap_or(session_timestamp);
let to_i64 = |v: u64| i64::try_from(v).unwrap_or(i64::MAX);
let input = to_i64(varint_field(usage, 1).unwrap_or(0))
.saturating_add(to_i64(varint_field(usage, 2).unwrap_or(0)));
let cache_read = to_i64(varint_field(usage, 5).unwrap_or(0));
let output = to_i64(varint_field(usage, 9).unwrap_or(0));
let reasoning = to_i64(varint_field(usage, 10).unwrap_or(0));
if input == 0 && output == 0 && cache_read == 0 && reasoning == 0 {
return None;
}
let dedup_key = string_field(usage, 11)
.filter(|text| !text.trim().is_empty())
.map(|text| text.to_string());
if let Some(key) = &dedup_key {
if !seen_response_ids.insert(key.clone()) {
return None;
}
}
let response_model = non_empty_string_field(chat_model, 19);
let model_raw = response_model
.filter(|m| !is_antigravity_routing_label(m))
.or_else(|| session_models.recover(chat_model))
.or_else(|| {
non_empty_string_field(chat_model, 21).and_then(display_label_to_model_id)
})
.or(response_model)
.unwrap_or("unknown");
let model_id = pricing::aliases::resolve_alias(model_raw)
.unwrap_or(model_raw)
.to_string();
let provider_id = provider_identity::inferred_provider_from_model(&model_id)
.unwrap_or("antigravity")
.to_string();
Some(UnifiedMessage::new_with_dedup(
"antigravity-cli",
model_id,
provider_id,
session_id,
timestamp,
TokenBreakdown {
input,
output,
cache_read,
cache_write: 0,
reasoning,
},
0.0,
dedup_key,
))
}
struct TrajectoryMeta {
created_ms: Option<i64>,
fallback_ms: i64,
workspace_key: Option<String>,
workspace_label: Option<String>,
}
fn read_trajectory_meta(conn: &Connection, path: &Path) -> TrajectoryMeta {
let blob: Option<Vec<u8>> = conn
.query_row(
"SELECT data FROM trajectory_metadata_blob LIMIT 1",
[],
|row| row.get::<_, Vec<u8>>(0),
)
.ok();
let mut created_ms = None;
let mut workspace_key = None;
let mut workspace_label = None;
if let Some(blob) = &blob {
created_ms = session_created_ms(blob).filter(|&ms| ms > 0);
if let Some(uri) = message_field(blob, 1).and_then(|folder| string_field(folder, 1)) {
if let Some(path_str) = file_uri_to_path(uri) {
workspace_key = normalize_workspace_key(&path_str);
workspace_label = workspace_key.as_deref().and_then(workspace_label_from_key);
}
}
}
TrajectoryMeta {
created_ms,
fallback_ms: created_ms.unwrap_or_else(|| file_modified_ms(path)),
workspace_key,
workspace_label,
}
}
fn session_created_ms(blob: &[u8]) -> Option<i64> {
proto_timestamp_ms(message_field(blob, 2)?)
}
fn generation_timestamp_ms(gen: &[u8], session_anchor: Option<i64>) -> Option<i64> {
if let Some(ms) = message_field(gen, 4)
.and_then(proto_timestamp_ms)
.filter(|&ms| ms > 0)
{
return Some(ms);
}
message_field(gen, 10).and_then(|payload| inferred_epoch_ms(payload, session_anchor))
}
fn inferred_epoch_ms(payload: &[u8], session_anchor: Option<i64>) -> Option<i64> {
inferred_epoch_ms_at(
payload,
session_anchor,
chrono::Utc::now().timestamp_millis(),
)
}
fn inferred_epoch_ms_at(payload: &[u8], session_anchor: Option<i64>, now_ms: i64) -> Option<i64> {
let window = session_window_ms(session_anchor?, now_ms)?;
let accepted = |ms: i64| window.contains(&ms);
if let Some(ms) =
proto_timestamp_ms(payload).filter(|&ms| plausible_epoch_ms(ms) && accepted(ms))
{
return Some(ms);
}
if let Some(ms) = varint_field(payload, 1)
.and_then(epoch_scalar_to_ms)
.filter(|&ms| accepted(ms))
{
return Some(ms);
}
if let Some(ms) = fixed64_field(payload, 1)
.and_then(epoch_scalar_to_ms)
.filter(|&ms| accepted(ms))
{
return Some(ms);
}
let raw: [u8; 8] = payload.try_into().ok()?;
let le = epoch_scalar_with_unit(u64::from_le_bytes(raw));
let be = epoch_scalar_with_unit(u64::from_be_bytes(raw));
let settled = match (le, be) {
(Some((_, le_ms)), Some((_, be_ms))) if le_ms == be_ms => Some(le_ms),
(Some((le_unit, le_ms)), Some((be_unit, _))) if be_unit.is_finer_than(le_unit) => {
Some(le_ms)
}
(Some(_), Some(_)) => None,
(Some((_, le_ms)), None) => Some(le_ms),
(None, Some((_, be_ms))) => Some(be_ms),
(None, None) => None,
};
settled.filter(|&ms| accepted(ms))
}
const UNSET_TIME_SENTINEL: u64 = u64::MAX;
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
enum EpochUnit {
Seconds,
Millis,
Micros,
Nanos,
}
impl EpochUnit {
fn is_finer_than(self, other: Self) -> bool {
self > other
}
}
fn epoch_scalar_with_unit(value: u64) -> Option<(EpochUnit, i64)> {
if value == UNSET_TIME_SENTINEL {
return None;
}
let value = i64::try_from(value).ok()?;
[
(EpochUnit::Seconds, value.checked_mul(1_000)),
(EpochUnit::Millis, Some(value)),
(EpochUnit::Micros, Some(value / 1_000)),
(EpochUnit::Nanos, Some(value / 1_000_000)),
]
.into_iter()
.filter_map(|(unit, ms)| ms.map(|ms| (unit, ms)))
.find(|&(_, ms)| plausible_epoch_ms(ms))
}
fn epoch_scalar_to_ms(value: u64) -> Option<i64> {
epoch_scalar_with_unit(value).map(|(_, ms)| ms)
}
fn plausible_epoch_ms(ms: i64) -> bool {
const MIN_MS: i64 = 1_577_836_800_000;
const MAX_MS: i64 = 4_102_444_800_000;
(MIN_MS..=MAX_MS).contains(&ms)
}
const SESSION_START_TOLERANCE_MS: i64 = 60 * 60 * 1_000;
const FUTURE_TOLERANCE_MS: i64 = 60 * 60 * 1_000;
fn session_window_ms(session_created: i64, now_ms: i64) -> Option<RangeInclusive<i64>> {
if session_created <= 0 {
return None;
}
let earliest = session_created.saturating_sub(SESSION_START_TOLERANCE_MS);
let latest = now_ms.saturating_add(FUTURE_TOLERANCE_MS);
Some(earliest..=latest)
}
fn proto_timestamp_ms(ts: &[u8]) -> Option<i64> {
let seconds = varint_field(ts, 1)? as i64;
let nanos = i64::try_from(varint_field(ts, 2).unwrap_or(0)).ok()?;
if !(0..=999_999_999).contains(&nanos) {
return None;
}
seconds.checked_mul(1000)?.checked_add(nanos / 1_000_000)
}
fn file_modified_ms(path: &Path) -> i64 {
std::fs::metadata(path)
.and_then(|metadata| metadata.modified())
.ok()
.map(|time| chrono::DateTime::<chrono::Utc>::from(time).timestamp_millis())
.unwrap_or(0)
}
fn file_uri_to_path(uri: &str) -> Option<String> {
let decoded = percent_decode(uri.strip_prefix("file://")?);
let bytes = decoded.as_bytes();
let path = if bytes.first() == Some(&b'/') {
if bytes.len() >= 3 && bytes[2] == b':' {
decoded[1..].to_string()
} else {
decoded
}
} else {
format!("//{decoded}")
};
Some(path)
}
fn percent_decode(input: &str) -> String {
let bytes = input.as_bytes();
let mut out = Vec::with_capacity(bytes.len());
let mut i = 0;
while i < bytes.len() {
if bytes[i] == b'%' && i + 2 < bytes.len() {
if let (Some(hi), Some(lo)) = (hex_value(bytes[i + 1]), hex_value(bytes[i + 2])) {
out.push((hi << 4) | lo);
i += 3;
continue;
}
}
out.push(bytes[i]);
i += 1;
}
String::from_utf8_lossy(&out).into_owned()
}
fn hex_value(byte: u8) -> Option<u8> {
match byte {
b'0'..=b'9' => Some(byte - b'0'),
b'a'..=b'f' => Some(byte - b'a' + 10),
b'A'..=b'F' => Some(byte - b'A' + 10),
_ => None,
}
}
enum Wire<'a> {
Varint(u64),
Len(&'a [u8]),
Fixed64(u64),
Fixed32,
}
struct ProtoReader<'a> {
buf: &'a [u8],
pos: usize,
}
impl<'a> ProtoReader<'a> {
fn new(buf: &'a [u8]) -> Self {
Self { buf, pos: 0 }
}
fn read_varint(&mut self) -> Option<u64> {
let mut result: u64 = 0;
let mut shift = 0u32;
loop {
let byte = *self.buf.get(self.pos)?;
self.pos += 1;
result |= u64::from(byte & 0x7f) << shift;
if byte & 0x80 == 0 {
return Some(result);
}
shift += 7;
if shift >= 64 {
return None;
}
}
}
fn next_field(&mut self) -> Option<(u64, Wire<'a>)> {
if self.pos >= self.buf.len() {
return None;
}
let tag = self.read_varint()?;
let field = tag >> 3;
let wire = match tag & 0x7 {
0 => Wire::Varint(self.read_varint()?),
1 => {
let end = self.pos.checked_add(8).filter(|&p| p <= self.buf.len())?;
let bytes: [u8; 8] = self.buf[self.pos..end].try_into().ok()?;
self.pos = end;
Wire::Fixed64(u64::from_le_bytes(bytes))
}
2 => {
let len = self.read_varint()? as usize;
let end = self.pos.checked_add(len).filter(|&p| p <= self.buf.len())?;
let bytes = &self.buf[self.pos..end];
self.pos = end;
Wire::Len(bytes)
}
5 => {
self.pos = self.pos.checked_add(4).filter(|&p| p <= self.buf.len())?;
Wire::Fixed32
}
_ => return None,
};
Some((field, wire))
}
}
fn message_field(buf: &[u8], field: u64) -> Option<&[u8]> {
let mut reader = ProtoReader::new(buf);
while let Some((found, wire)) = reader.next_field() {
if found == field {
if let Wire::Len(bytes) = wire {
return Some(bytes);
}
}
}
None
}
fn varint_field(buf: &[u8], field: u64) -> Option<u64> {
let mut reader = ProtoReader::new(buf);
while let Some((found, wire)) = reader.next_field() {
if found == field {
if let Wire::Varint(value) = wire {
return Some(value);
}
}
}
None
}
fn fixed64_field(buf: &[u8], field: u64) -> Option<u64> {
let mut reader = ProtoReader::new(buf);
while let Some((found, wire)) = reader.next_field() {
if found == field {
if let Wire::Fixed64(value) = wire {
return Some(value);
}
}
}
None
}
fn string_field(buf: &[u8], field: u64) -> Option<&str> {
message_field(buf, field).and_then(|bytes| std::str::from_utf8(bytes).ok())
}
fn non_empty_string_field(buf: &[u8], field: u64) -> Option<&str> {
string_field(buf, field).filter(|text| !text.trim().is_empty())
}
#[cfg(test)]
mod tests {
use super::*;
use rusqlite::{params, Connection};
fn enc_varint(field: u64, value: u64) -> Vec<u8> {
let mut out = encode_varint(field << 3);
out.extend(encode_varint(value));
out
}
fn enc_len(field: u64, payload: &[u8]) -> Vec<u8> {
let mut out = encode_varint((field << 3) | 2);
out.extend(encode_varint(payload.len() as u64));
out.extend_from_slice(payload);
out
}
fn encode_varint(mut value: u64) -> Vec<u8> {
let mut out = Vec::new();
loop {
let mut byte = (value & 0x7f) as u8;
value >>= 7;
if value != 0 {
byte |= 0x80;
}
out.push(byte);
if value == 0 {
break;
}
}
out
}
fn enc_fixed64(field: u64, value: u64) -> Vec<u8> {
let mut out = encode_varint((field << 3) | 1);
out.extend_from_slice(&value.to_le_bytes());
out
}
fn recent_epoch_seconds() -> i64 {
chrono::Utc::now().timestamp() - 3_600
}
fn build_row_with_gen9(gen9: &[u8], response_id: &str) -> Vec<u8> {
let mut usage = Vec::new();
usage.extend(enc_varint(2, 500)); usage.extend(enc_varint(9, 300)); usage.extend(enc_len(11, response_id.as_bytes()));
let mut chat_model = Vec::new();
chat_model.extend(enc_len(4, &usage));
chat_model.extend(enc_len(9, gen9));
chat_model.extend(enc_len(19, b"gemini-3-flash-a"));
enc_len(1, &chat_model)
}
fn gen9_timestamp(gen9: &[u8], session_fallback: i64) -> i64 {
gen9_timestamp_anchored(gen9, session_fallback, Some(session_fallback))
}
fn gen9_timestamp_anchored(gen9: &[u8], session_fallback: i64, anchor: Option<i64>) -> i64 {
let mut seen = HashSet::new();
parse_gen_metadata(
&build_row_with_gen9(gen9, "resp"),
"s",
session_fallback,
anchor,
&SessionModels::default(),
&mut seen,
)
.expect("row parses")
.timestamp
}
fn parse_isolated_row(
blob: &[u8],
session_id: &str,
session_timestamp: i64,
seen_response_ids: &mut HashSet<String>,
) -> Option<UnifiedMessage> {
parse_gen_metadata(
blob,
session_id,
session_timestamp,
Some(session_timestamp),
&SessionModels::default(),
seen_response_ids,
)
}
fn build_gen_metadata() -> Vec<u8> {
build_gen_metadata_with_model("gemini-3-flash-a")
}
fn build_gen_metadata_with_model(model: &str) -> Vec<u8> {
build_row(Some(model), None, "resp-1")
}
fn build_row(model: Option<&str>, display: Option<&str>, response_id: &str) -> Vec<u8> {
let mut usage = Vec::new();
usage.extend(enc_varint(1, 1132)); usage.extend(enc_varint(2, 500)); usage.extend(enc_varint(5, 16000)); usage.extend(enc_varint(9, 300)); usage.extend(enc_varint(10, 40)); usage.extend(enc_len(11, response_id.as_bytes()));
let mut chat_model = Vec::new();
chat_model.extend(enc_len(4, &usage));
if let Some(model) = model {
chat_model.extend(enc_len(19, model.as_bytes()));
}
if let Some(display) = display {
chat_model.extend(enc_len(21, display.as_bytes()));
}
enc_len(1, &chat_model)
}
fn write_conversation(path: &Path, blobs: &[Vec<u8>]) {
let conn = Connection::open(path).unwrap();
conn.execute_batch("CREATE TABLE gen_metadata (idx integer, data blob, size integer);")
.unwrap();
for (idx, blob) in blobs.iter().enumerate() {
conn.execute(
"INSERT INTO gen_metadata (idx, data, size) VALUES (?1, ?2, 0)",
params![idx as i64, blob],
)
.unwrap();
}
}
fn build_trajectory_meta() -> Vec<u8> {
let workspace = enc_len(1, b"file:///C:/Users/Frank/obsidian-vault");
let created = {
let mut created = Vec::new();
created.extend(enc_varint(1, 1_781_502_653)); created.extend(enc_varint(2, 0)); created
};
let mut blob = Vec::new();
blob.extend(enc_len(1, &workspace));
blob.extend(enc_len(2, &created));
blob
}
#[test]
fn overlarge_varint_token_counts_are_clamped_not_wrapped() {
let mut usage = Vec::new();
usage.extend(enc_varint(1, u64::MAX)); usage.extend(enc_varint(2, 10)); usage.extend(enc_varint(9, u64::MAX)); usage.extend(enc_len(11, b"resp-overflow"));
let mut chat_model = Vec::new();
chat_model.extend(enc_len(4, &usage));
chat_model.extend(enc_len(19, b"gemini-3-flash-a"));
let blob = enc_len(1, &chat_model);
let mut seen = HashSet::new();
let msg = parse_isolated_row(&blob, "s", 1_000, &mut seen).expect("parses");
assert_eq!(msg.tokens.output, i64::MAX);
assert_eq!(msg.tokens.input, i64::MAX); assert!(msg.tokens.input >= 0 && msg.tokens.output >= 0);
}
#[test]
fn parses_tokens_model_and_workspace_from_db() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("session-test.db");
{
let conn = Connection::open(&path).unwrap();
conn.execute_batch(
"CREATE TABLE gen_metadata (idx integer, data blob, size integer);
CREATE TABLE trajectory_metadata_blob (id text, data blob);",
)
.unwrap();
conn.execute(
"INSERT INTO gen_metadata (idx, data, size) VALUES (0, ?1, 0)",
params![build_gen_metadata()],
)
.unwrap();
conn.execute(
"INSERT INTO trajectory_metadata_blob (id, data) VALUES ('main', ?1)",
params![build_trajectory_meta()],
)
.unwrap();
}
let messages = parse_antigravity_cli_file(&path);
assert_eq!(messages.len(), 1);
let message = &messages[0];
assert_eq!(message.client, "antigravity-cli");
assert_eq!(message.model_id, "gemini-3.5-flash-high");
assert_eq!(message.provider_id, "google");
assert_eq!(message.session_id, "session-test");
assert_eq!(message.tokens.input, 1632); assert_eq!(message.tokens.cache_read, 16000);
assert_eq!(message.tokens.output, 300);
assert_eq!(message.tokens.reasoning, 40);
assert_eq!(message.dedup_key.as_deref(), Some("resp-1"));
assert_eq!(message.timestamp, 1_781_502_653_000);
assert_eq!(
message.workspace_key.as_deref(),
Some("C:/Users/Frank/obsidian-vault")
);
assert_eq!(message.workspace_label.as_deref(), Some("obsidian-vault"));
}
#[test]
fn resolves_current_antigravity_cli_response_model() {
let blob = build_gen_metadata_with_model("gemini-3-flash-agent");
let mut seen = HashSet::new();
let message = parse_isolated_row(&blob, "session", 1_000, &mut seen).unwrap();
assert_eq!(message.model_id, "gemini-3.5-flash-high");
assert_eq!(message.provider_id, "google");
}
#[test]
fn gemini_default_response_model_is_preserved() {
let blob = build_gen_metadata_with_model("gemini-default");
let mut seen = HashSet::new();
let message = parse_isolated_row(&blob, "session", 1_000, &mut seen).unwrap();
assert_eq!(message.model_id, "gemini-default");
assert_eq!(message.provider_id, "google");
}
#[test]
fn missing_response_model_is_recovered_from_the_display_label() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("continuation.db");
write_conversation(
&path,
&[
build_row(
Some("gemini-3.6-flash"),
Some("Gemini 3.6 Flash (High)"),
"resp-0",
),
build_row(None, Some("Gemini 3.6 Flash (High)"), "resp-1"),
],
);
let messages = parse_antigravity_cli_file(&path);
assert_eq!(messages.len(), 2);
for message in &messages {
assert_eq!(message.model_id, "gemini-3.6-flash");
assert_eq!(message.provider_id, "google");
}
}
#[test]
fn recovery_reads_the_whole_conversation_not_just_earlier_rows() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("gap-first.db");
write_conversation(
&path,
&[
build_row(None, Some("Gemini 3.6 Flash (High)"), "resp-0"),
build_row(
Some("gemini-3.6-flash"),
Some("Gemini 3.6 Flash (High)"),
"resp-1",
),
],
);
let messages = parse_antigravity_cli_file(&path);
assert_eq!(messages.len(), 2);
assert_eq!(messages[0].model_id, "gemini-3.6-flash");
}
#[test]
fn recovered_model_still_resolves_through_the_alias_table() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("aliased.db");
write_conversation(
&path,
&[
build_row(
Some("gemini-3-flash-a"),
Some("Gemini 3.5 Flash (High)"),
"resp-0",
),
build_row(None, Some("Gemini 3.5 Flash (High)"), "resp-1"),
],
);
let messages = parse_antigravity_cli_file(&path);
assert_eq!(messages.len(), 2);
assert_eq!(messages[1].model_id, "gemini-3.5-flash-high");
assert_eq!(messages[1].provider_id, "google");
}
#[test]
fn a_display_label_no_row_identified_is_not_guessed_at() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("switched.db");
write_conversation(
&path,
&[
build_row(
Some("gemini-3.6-flash"),
Some("Gemini 3.6 Flash (High)"),
"resp-0",
),
build_row(None, Some("Gemini 3.1 Pro"), "resp-1"),
],
);
let messages = parse_antigravity_cli_file(&path);
assert_eq!(messages.len(), 2);
assert_eq!(messages[0].model_id, "gemini-3.6-flash");
assert_eq!(messages[1].model_id, "unknown");
assert_eq!(messages[1].provider_id, "antigravity");
}
#[test]
fn a_display_label_used_by_two_models_is_not_used_for_recovery() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("ambiguous.db");
write_conversation(
&path,
&[
build_row(Some("gemini-3-flash-a"), Some("Gemini Flash"), "resp-0"),
build_row(Some("gemini-3.6-flash"), Some("Gemini Flash"), "resp-1"),
build_row(None, Some("Gemini Flash"), "resp-2"),
],
);
let messages = parse_antigravity_cli_file(&path);
assert_eq!(messages.len(), 3);
assert_eq!(messages[2].model_id, "unknown");
}
#[test]
fn two_spellings_of_one_priced_model_are_not_an_ambiguous_label() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("aliased.db");
write_conversation(
&path,
&[
build_row(
Some("gemini-pro-default"),
Some("Gemini 3.1 Pro (High)"),
"resp-0",
),
build_row(
Some("gemini-pro-agent"),
Some("Gemini 3.1 Pro (High)"),
"resp-1",
),
build_row(None, Some("Gemini 3.1 Pro (High)"), "resp-2"),
],
);
let messages = parse_antigravity_cli_file(&path);
assert_eq!(messages.len(), 3);
assert_eq!(messages[2].model_id, "gemini-3.1-pro");
assert_ne!(messages[2].model_id, "unknown");
}
#[test]
fn a_row_with_no_model_fields_falls_back_to_a_single_model_conversation() {
let dir = tempfile::tempdir().unwrap();
let single = dir.path().join("single-model.db");
write_conversation(
&single,
&[
build_row(
Some("gemini-3.6-flash"),
Some("Gemini 3.6 Flash (High)"),
"resp-0",
),
build_row(None, None, "resp-1"),
],
);
let messages = parse_antigravity_cli_file(&single);
assert_eq!(messages.len(), 2);
assert_eq!(messages[1].model_id, "gemini-3.6-flash");
let mixed = dir.path().join("mixed-models.db");
write_conversation(
&mixed,
&[
build_row(Some("gemini-3.6-flash"), None, "resp-0"),
build_row(Some("gemini-3.1-pro"), None, "resp-1"),
build_row(None, None, "resp-2"),
],
);
let messages = parse_antigravity_cli_file(&mixed);
assert_eq!(messages.len(), 3);
assert_eq!(messages[2].model_id, "unknown");
}
#[test]
fn a_label_no_row_identified_withholds_the_sole_model_fallback() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("unnamed-second-model.db");
write_conversation(
&path,
&[
build_row(
Some("gemini-3.6-flash"),
Some("Gemini 3.6 Flash (High)"),
"resp-0",
),
build_row(None, Some("Gemini 3.1 Pro"), "resp-1"),
build_row(None, None, "resp-2"),
],
);
let messages = parse_antigravity_cli_file(&path);
assert_eq!(messages.len(), 3);
assert_eq!(messages[0].model_id, "gemini-3.6-flash");
assert_eq!(messages[1].model_id, "unknown");
assert_eq!(messages[2].model_id, "unknown");
}
#[test]
fn per_generation_timestamp_overrides_session_fallback() {
let session_fallback = 111_000_i64;
let mut usage = Vec::new();
usage.extend(enc_varint(2, 500)); usage.extend(enc_varint(9, 300)); usage.extend(enc_len(11, b"with-time"));
let mut gen_time = Vec::new();
gen_time.extend(enc_varint(1, 1_781_000_000)); gen_time.extend(enc_varint(2, 250_000_000)); let gen9 = enc_len(4, &gen_time);
let mut chat_model = Vec::new();
chat_model.extend(enc_len(4, &usage));
chat_model.extend(enc_len(9, &gen9));
chat_model.extend(enc_len(19, b"gemini-3-flash-a"));
let blob = enc_len(1, &chat_model);
let mut seen = HashSet::new();
let message = parse_isolated_row(&blob, "s", session_fallback, &mut seen).unwrap();
assert_eq!(
message.timestamp,
1_781_000_000 * 1000 + 250,
"per-generation #9.#4 timestamp must override the session fallback"
);
let mut seen2 = HashSet::new();
let fallback_msg =
parse_isolated_row(&build_gen_metadata(), "s", session_fallback, &mut seen2).unwrap();
assert_eq!(
fallback_msg.timestamp, session_fallback,
"a row without #9.#4 must use the session-created fallback"
);
}
#[test]
fn agy_1_1_18_gen9_field_10_dates_the_turn() {
let seconds = 1_781_502_653_i64; let expected_ms = seconds * 1_000;
let session_fallback = expected_ms - 60_000;
let sentinel = enc_varint(2, u64::MAX);
let mut nested_ts = Vec::new();
nested_ts.extend(enc_varint(1, seconds as u64));
nested_ts.extend(enc_varint(2, 250_000_000)); let mut gen9 = sentinel.clone();
gen9.extend(enc_len(10, &nested_ts));
assert_eq!(
gen9_timestamp(&gen9, session_fallback),
expected_ms + 250,
"a nested Timestamp in #9.#10 must date the turn"
);
for (unit, scalar) in [
("seconds", seconds as u64),
("millis", (seconds * 1_000) as u64),
("micros", (seconds * 1_000_000) as u64),
("nanos", (seconds * 1_000_000_000) as u64),
] {
let mut gen9 = sentinel.clone();
gen9.extend(enc_len(10, &enc_varint(1, scalar)));
assert_eq!(
gen9_timestamp(&gen9, session_fallback),
expected_ms,
"nested varint {unit} must date the turn"
);
let mut gen9 = sentinel.clone();
gen9.extend(enc_len(10, &enc_fixed64(1, scalar)));
assert_eq!(
gen9_timestamp(&gen9, session_fallback),
expected_ms,
"nested fixed64 {unit} must date the turn"
);
assert_eq!(
epoch_scalar_with_unit(scalar.swap_bytes()),
None,
"the {unit} fixture's mirror reading must not be a time, or this \
case exercises the ambiguity rule instead of the {unit} decode"
);
for (order, raw) in [
("little-endian", scalar.to_le_bytes()),
("big-endian", scalar.to_be_bytes()),
] {
let mut gen9 = sentinel.clone();
gen9.extend(enc_len(10, &raw));
assert_eq!(
gen9_timestamp(&gen9, session_fallback),
expected_ms,
"raw {order} {unit} must date the turn"
);
}
}
}
#[test]
fn ambiguous_competing_endianness_payload_falls_back_to_session_timestamp() {
let intended_ms = 1_788_132_511_000_i64;
let raw_be = (intended_ms as u64).to_be_bytes();
let wide_session_fallback = 1_781_502_653_000_i64;
let sentinel = enc_varint(2, u64::MAX);
let mut gen9 = sentinel.clone();
gen9.extend(enc_len(10, &raw_be));
assert_eq!(
gen9_timestamp(&gen9, wide_session_fallback),
wide_session_fallback,
"competing in-window endianness readings must fall back to session timestamp"
);
let intended_ms_2 = 1_788_274_719_000_i64;
let raw_be_2 = (intended_ms_2 as u64).to_be_bytes();
let mut gen9_2 = sentinel;
gen9_2.extend(enc_len(10, &raw_be_2));
assert_eq!(
gen9_timestamp(&gen9_2, wide_session_fallback),
wide_session_fallback,
"competing in-window endianness readings must fall back to session timestamp"
);
}
#[test]
fn competing_endianness_verdict_does_not_depend_on_the_scan_clock() {
const HOUR_MS: i64 = 60 * 60 * 1_000;
const DAY_MS: i64 = 24 * HOUR_MS;
let raw = 1_788_274_719_000_u64.to_be_bytes();
assert_eq!(raw, [0x00, 0x00, 0x01, 0xa0, 0x5d, 0x7a, 0xb9, 0x18]);
let anchor = 1_781_502_653_000_i64;
let le = epoch_scalar_with_unit(u64::from_le_bytes(raw));
let be = epoch_scalar_with_unit(u64::from_be_bytes(raw));
assert_eq!(le, Some((EpochUnit::Nanos, 1_781_589_670_136)));
assert_eq!(be, Some((EpochUnit::Millis, 1_788_274_719_000)));
let (le_ms, be_ms) = (le.unwrap().1, be.unwrap().1);
let before_mirror = le_ms + DAY_MS;
assert!(
be_ms > before_mirror + FUTURE_TOLERANCE_MS,
"the BE reading must still be in the future for the first clock"
);
let after_mirror = be_ms + DAY_MS;
let verdicts = [before_mirror, after_mirror]
.map(|now_ms| inferred_epoch_ms_at(&raw, Some(anchor), now_ms));
assert_eq!(
verdicts[0], verdicts[1],
"the same bytes must date the turn the same way on both scan days"
);
assert_eq!(
verdicts[1], None,
"two plausible readings that disagree are ambiguous whether or not the window has admitted both yet"
);
}
struct EndiannessCase {
raw: [u8; 8],
anchor: i64,
turn_ms: i64,
le: Option<(EpochUnit, i64)>,
be: Option<(EpochUnit, i64)>,
verdict: Option<i64>,
}
fn assert_one_verdict_at_every_clock(cases: &[EndiannessCase]) {
const DAY_MS: i64 = 24 * 60 * 60 * 1_000;
const YEAR_MS: i64 = 365 * DAY_MS;
for case in cases {
assert_eq!(
epoch_scalar_with_unit(u64::from_le_bytes(case.raw)),
case.le,
"LE reading of {:02x?}",
case.raw
);
assert_eq!(
epoch_scalar_with_unit(u64::from_be_bytes(case.raw)),
case.be,
"BE reading of {:02x?}",
case.raw
);
for elapsed in [0, DAY_MS, 30 * DAY_MS, YEAR_MS, 5 * YEAR_MS, 25 * YEAR_MS] {
assert_eq!(
inferred_epoch_ms_at(&case.raw, Some(case.anchor), case.turn_ms + elapsed),
case.verdict,
"raw {:02x?} scanned {elapsed} ms after the turn",
case.raw
);
}
}
}
#[test]
fn a_mirror_beyond_the_old_horizon_cannot_enter_the_contest_late() {
assert_one_verdict_at_every_clock(&[
EndiannessCase {
raw: [0x00, 0x00, 0x01, 0xa0, 0x78, 0xf4, 0x03, 0x1b],
anchor: 1_788_734_000_000,
turn_ms: 1_788_735_652_635,
le: Some((EpochUnit::Nanos, 1_946_668_262_871)),
be: Some((EpochUnit::Millis, 1_788_735_652_635)),
verdict: None,
},
EndiannessCase {
raw: [0x00, 0x00, 0x01, 0xa0, 0x78, 0xf4, 0x01, 0x1b],
anchor: 1_788_734_000_000,
turn_ms: 1_788_735_652_123,
le: Some((EpochUnit::Nanos, 1_946_105_312_918)),
be: Some((EpochUnit::Millis, 1_788_735_652_123)),
verdict: None,
},
]);
}
#[test]
fn a_finer_unit_mirror_never_unseats_the_little_endian_reading() {
assert_one_verdict_at_every_clock(&[
EndiannessCase {
raw: 1_781_395_221_u64.to_le_bytes(),
anchor: 1_781_395_221_000 - 60_000,
turn_ms: 1_781_395_221_000,
le: Some((EpochUnit::Seconds, 1_781_395_221_000)),
be: Some((EpochUnit::Nanos, 1_580_531_927_520)),
verdict: Some(1_781_395_221_000),
},
EndiannessCase {
raw: 1_781_395_222_u64.to_le_bytes(),
anchor: 1_781_395_222_000 - 60_000,
turn_ms: 1_781_395_222_000,
le: Some((EpochUnit::Seconds, 1_781_395_222_000)),
be: Some((EpochUnit::Nanos, 1_652_589_521_558)),
verdict: Some(1_781_395_222_000),
},
EndiannessCase {
raw: 1_781_395_200_000_022_u64.to_le_bytes(),
anchor: 1_781_395_200_000 - 60_000,
turn_ms: 1_781_395_200_000,
le: Some((EpochUnit::Micros, 1_781_395_200_000)),
be: Some((EpochUnit::Nanos, 1_639_338_182_377)),
verdict: Some(1_781_395_200_000),
},
EndiannessCase {
raw: 1_781_502_653_000_u64.to_le_bytes(),
anchor: 1_781_502_653_000 - 60_000,
turn_ms: 1_781_502_653_000,
le: Some((EpochUnit::Millis, 1_781_502_653_000)),
be: None,
verdict: Some(1_781_502_653_000),
},
]);
}
#[test]
fn genuine_le_millis_payload_dates_the_turn_with_competing_reversed_nanos() {
let le_millis = (1_788_132_511_000_i64 & !0xff) | 0x18; let raw_le = (le_millis as u64).to_le_bytes();
let wide_session_fallback = 1_740_000_000_000_i64;
let sentinel = enc_varint(2, u64::MAX);
let mut gen9 = sentinel;
gen9.extend(enc_len(10, &raw_le));
assert_eq!(
gen9_timestamp(&gen9, wide_session_fallback),
le_millis,
"genuine LE milliseconds reading must date the turn over the reversed BE nanoseconds ghost"
);
}
#[test]
fn unrecognised_gen9_payloads_fall_back_to_the_session_timestamp() {
let session_fallback = 1_781_502_653_000_i64;
assert_eq!(
gen9_timestamp(&enc_varint(2, u64::MAX), session_fallback),
session_fallback,
"the #9.#2 unset sentinel must never be read as a time"
);
let mut sentinel_payload = enc_varint(2, u64::MAX);
sentinel_payload.extend(enc_len(10, &u64::MAX.to_le_bytes()));
assert_eq!(
gen9_timestamp(&sentinel_payload, session_fallback),
session_fallback,
"u64::MAX in the #10 payload must never be read as a time"
);
assert_eq!(epoch_scalar_to_ms(u64::MAX), None);
let opaque = [0x9a, 0x3f, 0x00, 0x11, 0xc4, 0x7e, 0x5d, 0x02];
assert_eq!(
gen9_timestamp(&enc_len(10, &opaque), session_fallback),
session_fallback,
"an opaque 8-byte #10 must fall back rather than produce a date"
);
let stale = 631_152_000_u64; let far_future = 7_258_118_400_u64; for bogus in [stale, far_future] {
assert_eq!(
gen9_timestamp(&enc_len(10, &bogus.to_le_bytes()), session_fallback),
session_fallback,
"raw out-of-range {bogus} must fall back to the session stamp"
);
assert_eq!(
gen9_timestamp(&enc_len(10, &enc_varint(1, bogus)), session_fallback),
session_fallback,
"nested out-of-range {bogus} must fall back to the session stamp"
);
}
}
#[test]
fn explicit_field_4_timestamp_outranks_the_inferred_field_10_reading() {
let session_fallback = 1_781_502_653_000_i64;
let seconds = recent_epoch_seconds();
let mut explicit = Vec::new();
explicit.extend(enc_varint(1, seconds as u64));
explicit.extend(enc_varint(2, 500_000_000)); let mut gen9 = enc_len(4, &explicit);
let other = ((seconds - 7_200) * 1_000_000) as u64;
gen9.extend(enc_len(10, &other.to_le_bytes()));
assert_eq!(
gen9_timestamp(&gen9, session_fallback),
seconds * 1_000 + 500,
"the explicit #9.#4 Timestamp must keep priority over #9.#10"
);
}
#[test]
fn explicit_field_4_timestamp_is_not_bounded_by_the_session_window() {
let session_start = chrono::Utc::now().timestamp_millis() - 3 * 24 * 60 * 60 * 1_000;
let long_before_session = 1_609_459_200_i64;
let mut explicit = Vec::new();
explicit.extend(enc_varint(1, long_before_session as u64));
explicit.extend(enc_varint(2, 0));
let gen9 = enc_len(4, &explicit);
assert_eq!(
gen9_timestamp(&gen9, session_start),
long_before_session * 1_000,
"the explicit #9.#4 Timestamp must stay unbounded by the session window"
);
}
#[test]
fn inferred_gen9_timestamps_must_land_inside_the_session_window() {
const HOUR_MS: i64 = 60 * 60 * 1_000;
let now_ms = chrono::Utc::now().timestamp_millis();
let session_start = now_ms - 3 * 24 * 60 * 60 * 1_000;
let payload = |ms: i64| enc_len(10, &(ms as u64).to_le_bytes());
let genuine = session_start + HOUR_MS;
assert_eq!(
gen9_timestamp(&payload(genuine), session_start),
genuine,
"a turn inside its own session must still date the row"
);
let latest = now_ms - HOUR_MS;
assert_eq!(
gen9_timestamp(&payload(latest), session_start),
latest,
"a turn from minutes ago must still date the row"
);
let slightly_early = session_start - HOUR_MS / 2;
assert_eq!(
gen9_timestamp(&payload(slightly_early), session_start),
slightly_early,
"the one-hour skew allowance below the session stamp must be honoured"
);
let before_tolerance = session_start - 2 * HOUR_MS;
assert!(
plausible_epoch_ms(before_tolerance),
"the rejection must come from the session window, not the absolute one"
);
assert_eq!(
gen9_timestamp(&payload(before_tolerance), session_start),
session_start,
"a stamp beyond the skew allowance below the session start must fall back"
);
let before_session = session_start - 24 * HOUR_MS;
assert!(plausible_epoch_ms(before_session));
assert_eq!(
gen9_timestamp(&payload(before_session), session_start),
session_start,
"a stamp predating the session must fall back to the session stamp"
);
let far_future = now_ms + 2 * 365 * 24 * HOUR_MS;
assert!(
plausible_epoch_ms(far_future),
"the absolute window still accepts two years out, so only the \
session window can reject this"
);
assert_eq!(
gen9_timestamp(&payload(far_future), session_start),
session_start,
"a stamp in the future must fall back to the session stamp"
);
}
#[test]
fn an_opaque_payload_that_reads_as_a_plausible_date_is_still_rejected() {
let session_start = chrono::Utc::now().timestamp_millis() - 3 * 24 * 60 * 60 * 1_000;
let opaque = [0x37, 0xa9, 0x5c, 0xd3, 0x1e, 0x4b, 0x62, 0x17];
let absolute = epoch_scalar_to_ms(u64::from_le_bytes(opaque))
.expect("these bytes do decode under the absolute plausibility window");
assert_eq!(
absolute, 1_684_991_806_357,
"2023-05-25, read as nanoseconds"
);
assert!(plausible_epoch_ms(absolute));
assert_eq!(
gen9_timestamp(&enc_len(10, &opaque), session_start),
session_start,
"a payload that only looks like a date must not re-date the turn"
);
}
#[test]
fn a_missing_session_anchor_declines_every_inferred_reading() {
let seconds = recent_epoch_seconds();
let sentinel = enc_varint(2, u64::MAX);
let fallback = 1_781_502_653_000_i64;
for anchor in [None, Some(0_i64), Some(-1)] {
let mut nested_ts = Vec::new();
nested_ts.extend(enc_varint(1, seconds as u64));
nested_ts.extend(enc_varint(2, 0));
let mut gen9 = sentinel.clone();
gen9.extend(enc_len(10, &nested_ts));
assert_eq!(
gen9_timestamp_anchored(&gen9, fallback, anchor),
fallback,
"a nested Timestamp must be declined without a session anchor"
);
let mut gen9 = sentinel.clone();
gen9.extend(enc_len(10, &enc_varint(1, seconds as u64)));
assert_eq!(
gen9_timestamp_anchored(&gen9, fallback, anchor),
fallback,
"a nested scalar must be declined without a session anchor"
);
let mut gen9 = sentinel.clone();
gen9.extend(enc_len(10, &(seconds as u64).to_le_bytes()));
assert_eq!(
gen9_timestamp_anchored(&gen9, fallback, anchor),
fallback,
"a raw payload must be declined without a session anchor"
);
}
let now_ms = chrono::Utc::now().timestamp_millis();
assert!(session_window_ms(0, now_ms).is_none());
assert!(session_window_ms(-1, now_ms).is_none());
assert!(session_window_ms(1, now_ms).is_some());
}
#[test]
fn an_mtime_only_session_anchors_no_inference() {
const HOUR_MS: i64 = 60 * 60 * 1_000;
let dir = tempfile::tempdir().unwrap();
let would_pass_under_mtime = chrono::Utc::now().timestamp_millis() - HOUR_MS / 2;
let mut gen9 = enc_varint(2, u64::MAX); gen9.extend(enc_len(10, &(would_pass_under_mtime as u64).to_le_bytes()));
let row = build_row_with_gen9(&gen9, "turn-1");
let no_table = dir.path().join("no-meta-table.db");
write_conversation(&no_table, std::slice::from_ref(&row));
let no_created = dir.path().join("no-created-at.db");
write_conversation(&no_created, std::slice::from_ref(&row));
{
let conn = Connection::open(&no_created).unwrap();
conn.execute_batch("CREATE TABLE trajectory_metadata_blob (id text, data blob);")
.unwrap();
let workspace = enc_len(1, b"file:///home/frank/vault");
conn.execute(
"INSERT INTO trajectory_metadata_blob (id, data) VALUES ('main', ?1)",
params![enc_len(1, &workspace)],
)
.unwrap();
}
for path in [&no_table, &no_created] {
let messages = parse_antigravity_cli_file(path);
assert_eq!(messages.len(), 1, "{}", path.display());
let mtime = file_modified_ms(path);
assert!(mtime > 0, "the fixture must have a positive mtime");
assert!(
session_window_ms(mtime, chrono::Utc::now().timestamp_millis())
.expect("a positive mtime does build a window")
.contains(&would_pass_under_mtime),
"the payload has to be one an mtime-anchored window would have \
accepted, or this test proves nothing"
);
assert_ne!(
messages[0].timestamp, would_pass_under_mtime,
"an mtime is not a session start and must not anchor inference"
);
assert_eq!(
messages[0].timestamp, mtime,
"with no decoded created-at the row keeps its fallback dating"
);
}
}
#[test]
fn epoch_scalar_unit_detection_is_unambiguous() {
let seconds = recent_epoch_seconds();
let expected = seconds * 1_000;
assert_eq!(epoch_scalar_to_ms(seconds as u64), Some(expected));
assert_eq!(epoch_scalar_to_ms((seconds * 1_000) as u64), Some(expected));
assert_eq!(
epoch_scalar_to_ms((seconds * 1_000_000) as u64),
Some(expected)
);
assert_eq!(
epoch_scalar_to_ms((seconds * 1_000_000_000) as u64),
Some(expected)
);
assert_eq!(epoch_scalar_to_ms(0), None);
assert_eq!(epoch_scalar_to_ms(u64::MAX), None);
}
#[test]
fn agy_1_1_18_rows_are_dated_per_turn_not_at_session_start() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("session-1118.db");
let session_created_ms = 1_781_502_653_000_i64; let two_days_ago = recent_epoch_seconds() - 2 * 24 * 60 * 60;
let now_ish = recent_epoch_seconds();
let row = |seconds: i64, id: &str| {
let mut gen9 = enc_varint(2, u64::MAX); gen9.extend(enc_len(10, &((seconds * 1_000_000) as u64).to_le_bytes()));
build_row_with_gen9(&gen9, id)
};
{
let conn = Connection::open(&path).unwrap();
conn.execute_batch(
"CREATE TABLE gen_metadata (idx integer, data blob, size integer);
CREATE TABLE trajectory_metadata_blob (id text, data blob);",
)
.unwrap();
for (idx, blob) in [row(two_days_ago, "turn-1"), row(now_ish, "turn-2")]
.iter()
.enumerate()
{
conn.execute(
"INSERT INTO gen_metadata (idx, data, size) VALUES (?1, ?2, 0)",
params![idx as i64, blob],
)
.unwrap();
}
conn.execute(
"INSERT INTO trajectory_metadata_blob (id, data) VALUES ('main', ?1)",
params![build_trajectory_meta()],
)
.unwrap();
}
let messages = parse_antigravity_cli_file(&path);
assert_eq!(messages.len(), 2);
assert_eq!(messages[0].timestamp, two_days_ago * 1_000);
assert_eq!(messages[1].timestamp, now_ish * 1_000);
assert!(
messages.iter().all(|m| m.timestamp != session_created_ms),
"no row may keep the session-created stamp once #9.#10 decodes"
);
}
#[test]
fn dedupes_repeated_response_ids_and_skips_zero_usage() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("dupes.db");
let mut zero_usage = Vec::new();
zero_usage.extend(enc_len(11, b"zero"));
let mut zero_chat = Vec::new();
zero_chat.extend(enc_len(4, &zero_usage));
zero_chat.extend(enc_len(19, b"gemini-3-flash-a"));
let zero_blob = enc_len(1, &zero_chat);
{
let conn = Connection::open(&path).unwrap();
conn.execute_batch("CREATE TABLE gen_metadata (idx integer, data blob, size integer);")
.unwrap();
for (idx, blob) in [
(0, build_gen_metadata()),
(1, build_gen_metadata()),
(2, zero_blob),
] {
conn.execute(
"INSERT INTO gen_metadata (idx, data, size) VALUES (?1, ?2, 0)",
params![idx, blob],
)
.unwrap();
}
}
let messages = parse_antigravity_cli_file(&path);
assert_eq!(messages.len(), 1);
assert!(messages[0].timestamp > 0);
}
#[test]
fn emitted_model_string_resolves_to_priced_alias() {
assert_eq!(
pricing::aliases::resolve_alias("gemini-3-flash-a"),
Some("gemini-3.5-flash-high")
);
}
#[test]
fn output_and_thinking_map_to_fields_9_and_10() {
let output = 300u64;
let thinking = 40u64;
let total_output = output + thinking;
let mut usage = Vec::new();
usage.extend(enc_varint(1, 1132)); usage.extend(enc_varint(2, 500)); usage.extend(enc_varint(3, total_output)); usage.extend(enc_varint(9, output)); usage.extend(enc_varint(10, thinking)); usage.extend(enc_len(11, b"invariant-1"));
let mut chat_model = Vec::new();
chat_model.extend(enc_len(4, &usage));
chat_model.extend(enc_len(19, b"gemini-3-flash-a"));
let blob = enc_len(1, &chat_model);
let mut seen = HashSet::new();
let message = parse_isolated_row(&blob, "session", 0, &mut seen).unwrap();
assert_eq!(message.tokens.output, output as i64);
assert_eq!(message.tokens.reasoning, thinking as i64);
assert_eq!(
(message.tokens.output + message.tokens.reasoning) as u64,
total_output
);
}
#[test]
fn malformed_blob_returns_none_without_panic() {
let mut seen = HashSet::new();
assert!(parse_isolated_row(&[], "s", 0, &mut seen).is_none());
assert!(parse_isolated_row(&[0xff, 0xff, 0xff, 0xff], "s", 0, &mut seen).is_none());
let truncated = [(1u8 << 3) | 2, 0x7f, 0x01, 0x02];
assert!(parse_isolated_row(&truncated, "s", 0, &mut seen).is_none());
let inner = [(4u8 << 3) | 2, 0x40, 0x00];
let mut outer = vec![(1u8 << 3) | 2, inner.len() as u8];
outer.extend_from_slice(&inner);
assert!(parse_isolated_row(&outer, "s", 0, &mut seen).is_none());
}
#[test]
fn proto_timestamp_ms_overflow_returns_none_without_panic() {
let mut overflow = Vec::new();
overflow.extend(enc_varint(1, i64::MAX as u64)); overflow.extend(enc_varint(2, 0)); assert_eq!(proto_timestamp_ms(&overflow), None);
let ok_seconds = i64::MAX / 1000;
let mut ok = Vec::new();
ok.extend(enc_varint(1, ok_seconds as u64));
ok.extend(enc_varint(2, 0));
assert_eq!(proto_timestamp_ms(&ok), Some(ok_seconds * 1000));
let mut normal = Vec::new();
normal.extend(enc_varint(1, 1_781_000_000));
normal.extend(enc_varint(2, 250_000_000)); assert_eq!(
proto_timestamp_ms(&normal),
Some(1_781_000_000 * 1000 + 250)
);
}
#[test]
fn proto_timestamp_ms_rejects_out_of_range_nanos() {
let mut bad_nanos = Vec::new();
bad_nanos.extend(enc_varint(1, 1_781_000_000)); bad_nanos.extend(enc_varint(2, 1_000_000_000)); assert_eq!(proto_timestamp_ms(&bad_nanos), None);
let mut huge_nanos = Vec::new();
huge_nanos.extend(enc_varint(1, 1_781_000_000));
huge_nanos.extend(enc_varint(2, u64::MAX));
assert_eq!(proto_timestamp_ms(&huge_nanos), None);
let mut max_nanos = Vec::new();
max_nanos.extend(enc_varint(1, 1_781_000_000));
max_nanos.extend(enc_varint(2, 999_999_999));
assert_eq!(
proto_timestamp_ms(&max_nanos),
Some(1_781_000_000 * 1000 + 999)
);
let session_fallback = 222_000_i64;
let mut usage = Vec::new();
usage.extend(enc_varint(2, 500)); usage.extend(enc_varint(9, 300)); usage.extend(enc_len(11, b"bad-nanos"));
let mut gen_time = Vec::new();
gen_time.extend(enc_varint(1, 1_781_000_000)); gen_time.extend(enc_varint(2, 1_000_000_000)); let gen9 = enc_len(4, &gen_time);
let mut chat_model = Vec::new();
chat_model.extend(enc_len(4, &usage));
chat_model.extend(enc_len(9, &gen9));
chat_model.extend(enc_len(19, b"gemini-3-flash-a"));
let blob = enc_len(1, &chat_model);
let mut seen = HashSet::new();
let message = parse_isolated_row(&blob, "s", session_fallback, &mut seen).unwrap();
assert_eq!(
message.timestamp, session_fallback,
"out-of-range per-generation nanos must fall back to the session timestamp"
);
}
#[test]
fn file_uri_to_path_handles_windows_posix_and_unc() {
assert_eq!(
file_uri_to_path("file:///C:/Users/Frank/obsidian-vault").as_deref(),
Some("C:/Users/Frank/obsidian-vault")
);
assert_eq!(
file_uri_to_path("file:///home/frank/project").as_deref(),
Some("/home/frank/project")
);
assert_eq!(
file_uri_to_path("file://server/share/code").as_deref(),
Some("//server/share/code")
);
assert_eq!(
file_uri_to_path("file:///D:/%E6%88%91%E7%9A%84").as_deref(),
Some("D:/我的")
);
assert_eq!(file_uri_to_path("not-a-file-uri"), None);
}
#[test]
fn routing_label_row_is_resolved_from_sibling_display_label() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("routing-sibling.db");
write_conversation(
&path,
&[
build_row(
Some("gemini-3.5-flash-high"),
Some("Gemini 3.5 Flash (High)"),
"resp-0",
),
build_row(
Some("gemini-default"),
Some("Gemini 3.5 Flash (High)"),
"resp-1",
),
],
);
let messages = parse_antigravity_cli_file(&path);
assert_eq!(messages.len(), 2);
assert_eq!(messages[1].model_id, "gemini-3.5-flash-high");
assert_eq!(messages[1].provider_id, "google");
}
#[test]
fn routing_label_does_not_poison_by_display_ambiguity() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("routing-no-poison.db");
write_conversation(
&path,
&[
build_row(
Some("gemini-3.6-flash"),
Some("Gemini 3.6 Flash (High)"),
"resp-0",
),
build_row(
Some("gemini-default"),
Some("Gemini 3.6 Flash (High)"),
"resp-1",
),
build_row(None, Some("Gemini 3.6 Flash (High)"), "resp-2"),
],
);
let messages = parse_antigravity_cli_file(&path);
assert_eq!(messages.len(), 3);
assert_eq!(messages[1].model_id, "gemini-3.6-flash");
assert_eq!(messages[2].model_id, "gemini-3.6-flash");
}
#[test]
fn routing_label_resolved_from_display_label_parsing() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("routing-display.db");
write_conversation(
&path,
&[
build_row(
Some("gemini-default"),
Some("Gemini 3.5 Flash (Low)"),
"resp-0",
),
build_row(
Some("gemini-default"),
Some("Gemini 3.5 Flash (Medium)"),
"resp-1",
),
],
);
let messages = parse_antigravity_cli_file(&path);
assert_eq!(messages.len(), 2);
assert_eq!(messages[0].model_id, "gemini-3.5-flash-extra-low");
assert_eq!(messages[0].provider_id, "google");
assert_eq!(messages[1].model_id, "gemini-3.5-flash-medium");
assert_eq!(messages[1].provider_id, "google");
}
#[test]
fn routing_label_without_any_evidence_is_kept() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("routing-unresolved.db");
write_conversation(
&path,
&[build_row(
Some("gemini-default"),
Some("Gemini 9.9 Flash (Mystery)"),
"resp-0",
)],
);
let messages = parse_antigravity_cli_file(&path);
assert_eq!(messages.len(), 1);
assert_eq!(messages[0].model_id, "gemini-default");
assert_eq!(messages[0].provider_id, "google");
}
#[test]
fn routing_label_recover_still_resolves_through_the_alias_table() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("routing-alias.db");
write_conversation(
&path,
&[build_row(
Some("gemini-default"),
Some("Gemini 3.5 Flash (High)"),
"resp-0",
)],
);
let messages = parse_antigravity_cli_file(&path);
assert_eq!(messages.len(), 1);
assert_eq!(messages[0].model_id, "gemini-3.5-flash-high");
assert_eq!(messages[0].provider_id, "google");
}
}