Files
wehub-resource-sync f99010fae1
Desktop Artifacts / Desktop Build (Linux) (push) Waiting to run
Desktop Artifacts / Desktop Build (Windows) (push) Waiting to run
Desktop Artifacts / Desktop Build (Linux (arm64)) (push) Waiting to run
Desktop Artifacts (macOS) / Desktop Build (macOS (aarch64)) (push) Waiting to run
Desktop Artifacts (macOS) / Desktop Build (macOS (x86_64)) (push) Waiting to run
CI / lint (push) Failing after 1s
CI / frontend (push) Failing after 1s
CI / scripts (push) Failing after 1s
CI / Go Test (ubuntu-latest) (push) Failing after 0s
CI / frontend-node-25 (push) Failing after 1s
CI / docs (push) Failing after 0s
CI / coverage (push) Failing after 0s
CI / e2e (push) Failing after 0s
Docker / build-and-push (push) Failing after 1s
CI / integration (push) Failing after 4m43s
CI / Go Test (windows-latest) (push) Has been cancelled
CI / Desktop Unit Tests (Windows) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:30:36 +08:00

3990 lines
145 KiB
Go

package parser
import (
"bytes"
"crypto/aes"
"crypto/cipher"
"database/sql"
"encoding/binary"
"encoding/json"
"os"
"path/filepath"
"strings"
"testing"
"time"
_ "github.com/mattn/go-sqlite3"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// newAntigravityTestProvider builds a concrete antigravityProvider for the given
// roots so package tests can exercise the folded discovery, source-lookup, and
// parse behavior directly through provider methods.
func newAntigravityTestProvider(t *testing.T, roots ...string) *antigravityProvider {
t.Helper()
provider, ok := NewProvider(AgentAntigravity, ProviderConfig{Roots: roots})
require.True(t, ok)
ap, ok := provider.(*antigravityProvider)
require.True(t, ok)
return ap
}
// newAntigravityCLITestProvider builds a concrete antigravityCLIProvider for the
// given roots.
func newAntigravityCLITestProvider(t *testing.T, roots ...string) *antigravityCLIProvider {
t.Helper()
provider, ok := NewProvider(AgentAntigravityCLI, ProviderConfig{Roots: roots})
require.True(t, ok)
cp, ok := provider.(*antigravityCLIProvider)
require.True(t, ok)
return cp
}
// discoverAntigravityTestSessions discovers IDE sessions under root through the
// provider, returning the legacy DiscoveredFile shape the tests assert against.
// It replaces the removed package-level DiscoverAntigravitySessions entrypoint.
func discoverAntigravityTestSessions(t *testing.T, root string) []DiscoveredFile {
t.Helper()
paths := newAntigravityTestProvider(t, root).sources.discoverSessionPaths(root)
files := make([]DiscoveredFile, 0, len(paths))
for _, path := range paths {
files = append(files, DiscoveredFile{Path: path, Agent: AgentAntigravity})
}
return files
}
// findAntigravityTestSourceFile resolves an IDE session id to a DB path through
// the provider, replacing the removed FindAntigravitySourceFile.
func findAntigravityTestSourceFile(t *testing.T, root, id string) string {
t.Helper()
return newAntigravityTestProvider(t, root).sources.findSourceFile(root, id)
}
// parseAntigravityTestSession parses an IDE session DB through the provider-owned
// parse method, replacing the removed package-level ParseAntigravitySession.
func parseAntigravityTestSession(
t *testing.T, path, project, machine string,
) (*ParsedSession, []ParsedMessage, []ParsedUsageEvent, error) {
t.Helper()
return newAntigravityTestProvider(t).parseSession(path, project, machine)
}
// discoverAntigravityCLITestSessions discovers CLI sessions under root through
// the provider, replacing the removed DiscoverAntigravityCLISessions.
func discoverAntigravityCLITestSessions(t *testing.T, root string) []DiscoveredFile {
t.Helper()
return newAntigravityCLITestProvider(t, root).sources.discoverSessions(root)
}
// findAntigravityCLITestSourceFile resolves a CLI session id to a source path
// through the provider, replacing the removed FindAntigravityCLISourceFile.
func findAntigravityCLITestSourceFile(t *testing.T, root, id string) string {
t.Helper()
return newAntigravityCLITestProvider(t, root).sources.findSourceFile(root, id)
}
// parseAntigravityCLITestSessionWithStatus parses a CLI session through the
// provider-owned parse method, replacing the removed package-level
// ParseAntigravityCLISessionWithStatus.
func parseAntigravityCLITestSessionWithStatus(
t *testing.T, path, project, machine string,
) (*ParsedSession, []ParsedMessage, []ParsedUsageEvent, AntigravityCLIParseStatus, error) {
t.Helper()
return newAntigravityCLITestProvider(t).parseSessionWithStatus(path, project, machine)
}
// parseAntigravityCLITestSession is the no-status convenience wrapper the tests
// use, replacing the removed package-level ParseAntigravityCLISession.
func parseAntigravityCLITestSession(
t *testing.T, path, project, machine string,
) (*ParsedSession, []ParsedMessage, error) {
t.Helper()
sess, msgs, _, _, err := parseAntigravityCLITestSessionWithStatus(t, path, project, machine)
return sess, msgs, err
}
// ---- protobuf wire walker -------------------------------------
// agProtoEncode is a tiny test-only encoder used to hand-craft
// payloads for the wire walker. It supports varint, length-
// delimited bytes, and nested messages (re-encoded recursively).
type pbField struct {
num int
wire int
varint uint64
bytes []byte
}
func encodeVarint(v uint64) []byte {
buf := make([]byte, binary.MaxVarintLen64)
n := binary.PutUvarint(buf, v)
return buf[:n]
}
func encodePB(fields []pbField) []byte {
var out []byte
for _, f := range fields {
tag := uint64(f.num<<3) | uint64(f.wire)
out = append(out, encodeVarint(tag)...)
switch f.wire {
case pbWireVarint:
out = append(out, encodeVarint(f.varint)...)
case pbWireBytes:
out = append(out, encodeVarint(uint64(len(f.bytes)))...)
out = append(out, f.bytes...)
}
}
return out
}
func TestAgProtoParseAndExtract(t *testing.T) {
inner := encodePB([]pbField{
{num: 1, wire: pbWireVarint, varint: 1779326586},
{num: 2, wire: pbWireVarint, varint: 12345},
})
payload := encodePB([]pbField{
{num: 1, wire: pbWireVarint, varint: 7},
{
num: 17,
wire: pbWireBytes,
bytes: []byte("Hi, what's up next?"),
},
{num: 5, wire: pbWireBytes, bytes: inner},
})
fields, err := agProtoParse(payload)
require.NoError(t, err, "parse")
require.Len(t, fields, 3)
// Field 17 should be a UTF-8 string with no nested decoding.
got, _ := agProtoFind(fields, 17)
s, ok := agProtoString(got)
require.True(t, ok, "field 17 string ok")
assert.Equal(t, "Hi, what's up next?", s, "field 17")
// Field 5 should have nested fields parsed as a Timestamp.
tsf, _ := agProtoFind(fields, 5)
require.NotNil(t, tsf.Nested, "field 5 not parsed as nested")
sec, nanos, ok := agProtoTimestamp(tsf.Nested)
require.True(t, ok, "timestamp ok")
assert.Equal(t, int64(1779326586), sec, "timestamp sec")
assert.Equal(t, int32(12345), nanos, "timestamp nanos")
strs := agProtoCollectStrings(fields, 5)
require.Len(t, strs, 1)
assert.Equal(t, "Hi, what's up next?", strs[0])
}
// TestAgProtoTimestampNanosRange pins the nanos guard: the protobuf
// Timestamp spec bounds nanos to [0, 1e9), and an out-of-range varint
// cast to int32 could go negative and shift time.Unix results outside
// the plausibility window callers check on seconds alone.
func TestAgProtoTimestampNanosRange(t *testing.T) {
tests := []struct {
name string
nanos uint64
wantOK bool
}{
{"max valid nanos", 999_999_999, true},
{"one past the cap", 1_000_000_000, false},
{"int32-overflowing nanos", 4_000_000_000, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
payload := encodePB([]pbField{
{num: 1, wire: pbWireVarint, varint: 1_779_326_586},
{num: 2, wire: pbWireVarint, varint: tt.nanos},
})
fields, err := agProtoParse(payload)
require.NoError(t, err, "parse")
sec, nanos, ok := agProtoTimestamp(fields)
assert.Equal(t, tt.wantOK, ok, "timestamp ok")
if tt.wantOK {
assert.Equal(t, int64(1_779_326_586), sec, "timestamp sec")
assert.Equal(t, int32(tt.nanos), nanos, "timestamp nanos")
}
})
}
}
// TestAgProtoLengthOverflow feeds a length-delimited field whose
// declared length is near uint64-max. The pre-fix code computed
// pos+ln in uint64 and wrapped, then sliced with int(ln) which
// panicked. The fix compares ln against (len(data)-pos) without
// addition.
func TestAgProtoLengthOverflow(t *testing.T) {
// Tag for field 1, wire 2 (length-delimited).
tag := []byte{0x0A}
// Encode the largest uvarint (10 bytes, value 2^64-1).
huge := make([]byte, 10)
for i := range 9 {
huge[i] = 0xFF
}
huge[9] = 0x01
payload := append(append([]byte{}, tag...), huge...)
payload = append(payload, []byte("only-a-few-bytes")...)
// Must return an error rather than panicking or returning a
// bogus slice.
defer func() {
if r := recover(); r != nil {
t.Fatalf("agProtoParse panicked: %v", r)
}
}()
_, err := agProtoParse(payload)
require.Error(t, err, "expected error for oversized length")
}
// TestAgProtoFieldBudget verifies the total-fields cap: a flat run
// of minimal two-byte varint fields amplifies into ~100-byte field
// structs, so without the budget an unbounded blob could allocate
// two orders of magnitude more memory than its size. Exhaustion
// truncates instead of failing, so a payload past the budget keeps
// its decoded prefix rather than losing all content.
func TestAgProtoFieldBudget(t *testing.T) {
// One field per two bytes: tag 0x08 (field 1, varint), value 0.
dense := func(fields int) []byte {
return bytes.Repeat([]byte{0x08, 0x00}, fields)
}
within, err := agProtoParse(dense(1000))
require.NoError(t, err)
assert.Len(t, within, 1000)
truncated, err := agProtoParse(dense(agProtoMaxFields + 10))
require.NoError(t, err)
assert.Len(t, truncated, agProtoMaxFields,
"expected truncation at the field budget")
// Speculative nested re-parses consume the shared budget too: a
// small envelope around a dense payload must not bypass it. The
// exhausted child stays opaque (no Nested) and the field after
// it is truncated away, but the parse itself still succeeds.
fields, err := agProtoParse(encodePB([]pbField{
{num: 1, wire: pbWireBytes, bytes: dense(agProtoMaxFields)},
{num: 2, wire: pbWireVarint, varint: 1},
}))
require.NoError(t, err)
require.Len(t, fields, 1,
"expected the post-exhaustion field to be truncated")
assert.Nil(t, fields[0].Nested,
"expected the budget-exhausted child to stay opaque")
}
// TestAgProtoLooksLikePrefix exercises the prefix-tolerant
// validator used by the decryption retry loop. It must accept a
// well-formed prefix followed by a truncated final field, but
// reject random bytes.
func TestAgProtoLooksLikePrefix(t *testing.T) {
complete := encodePB([]pbField{
{num: 1, wire: pbWireVarint, varint: 42},
{num: 2, wire: pbWireBytes, bytes: []byte("hello there")},
})
require.True(t, agProtoLooksLikePrefix(complete), "complete message rejected")
// Append a length-delimited field whose declared length runs
// past the end of the buffer - agProtoParse rejects this, but
// the prefix-tolerant check should accept since at least one
// full field decoded cleanly first.
truncated := append(append([]byte{}, complete...),
// tag for field 3, wire 2; length 100; only 3 actual bytes
0x1A, 0x64, 0x41, 0x42, 0x43,
)
assert.True(t, agProtoLooksLikePrefix(truncated), "truncated tail rejected")
_, err := agProtoParse(truncated)
require.Error(t, err, "agProtoParse should still reject truncated tail")
// Pure garbage with zero clean fields → reject.
assert.False(t, agProtoLooksLikePrefix([]byte{0x00, 0x00, 0x00}), "zero-field-number garbage accepted")
assert.False(t, agProtoLooksLikePrefix(nil), "empty input accepted")
}
func TestEarliestAntigravityTimestamp(t *testing.T) {
older := encodePB([]pbField{
{num: 1, wire: pbWireVarint, varint: 1700000000},
{num: 2, wire: pbWireVarint, varint: 0},
})
newer := encodePB([]pbField{
{num: 1, wire: pbWireVarint, varint: 1779326586},
})
payload := encodePB([]pbField{
{num: 3, wire: pbWireBytes, bytes: newer},
{num: 4, wire: pbWireBytes, bytes: older},
})
fields, err := agProtoParse(payload)
require.NoError(t, err, "parse")
got := earliestAntigravityTimestamp(fields)
assert.Equal(t, int64(1700000000), got.Unix())
}
// ---- CLI parser -----------------------------------------------
func TestAntigravityCLIDiscoverAndParse(t *testing.T) {
root := t.TempDir()
id := "11111111-2222-3333-4444-555555555555"
mustMkdir(t, filepath.Join(root, "conversations"))
mustMkdir(t, filepath.Join(root, "implicit"))
mustMkdir(t, filepath.Join(root, "brain", id))
// Encrypted .pb stub (content does not matter without a key)
mustWrite(t, filepath.Join(root, "conversations", id+".pb"),
[]byte("encrypted-placeholder"))
// brain artifact + metadata
mustWrite(t, filepath.Join(root, "brain", id, "task.md"),
[]byte("# Task\n\n- step one"))
mustWrite(t,
filepath.Join(root, "brain", id, "task.md.metadata.json"),
[]byte(`{
"artifactType": "ARTIFACT_TYPE_TASK",
"summary": "Top task summary",
"updatedAt": "2026-05-20T22:47:27.078Z"
}`))
// history.jsonl: one row for our session, one for another
mustWrite(t, filepath.Join(root, "history.jsonl"),
[]byte(`{"display":"hello world","timestamp":1779000000000,`+
`"workspace":"/tmp/proj","conversationId":"`+id+`"}
{"display":"other","timestamp":1779000001000,"workspace":"/tmp/x","conversationId":"other-id"}`))
// Discovery should return the .pb with the right project.
files := discoverAntigravityCLITestSessions(t, root)
require.Len(t, files, 1, "discover")
assert.Equal(t, "/tmp/proj", files[0].Project, "project")
// Find by id should locate the same .pb.
assert.Equal(t, files[0].Path, findAntigravityCLITestSourceFile(t, root, id), "find")
sess, msgs, err := parseAntigravityCLITestSession(t,
files[0].Path, files[0].Project, "test-machine",
)
require.NoError(t, err, "parse")
assert.Equal(t, "antigravity-cli:"+id, sess.ID)
// One user message from history + one assistant from brain.
require.Len(t, msgs, 2)
assert.Equal(t, RoleUser, msgs[0].Role)
assert.Contains(t, msgs[0].Content, "hello world")
assert.Equal(t, RoleAssistant, msgs[1].Role)
assert.Contains(t, msgs[1].Content, "step one")
assert.Contains(t, msgs[1].Content, "Top task summary")
assert.Equal(t, 2, sess.MessageCount)
assert.Equal(t, 1, sess.UserMessageCount)
assert.Equal(t, "hello world", sess.FirstMessage)
// StartedAt is the user message timestamp (epoch ms).
assert.Equal(t, int64(1779000000000), sess.StartedAt.UnixMilli())
}
func TestAntigravityCLIDiscoverAndParseDB(t *testing.T) {
root := t.TempDir()
id := "33333333-4444-5555-6666-777777777777"
mustMkdir(t, filepath.Join(root, "conversations"))
mustMkdir(t, filepath.Join(root, "brain", id))
dbPath := filepath.Join(root, "conversations", id+".db")
createAntigravityTestDB(t, dbPath)
mustWrite(t, filepath.Join(root, "conversations", id+".pb"),
[]byte("old-encrypted-placeholder"))
mustWrite(t, filepath.Join(root, "history.jsonl"),
[]byte(`{"display":"db prompt fallback","timestamp":1779000000000,`+
`"workspace":"/tmp/db-proj","conversationId":"`+id+`"}`))
files := discoverAntigravityCLITestSessions(t, root)
require.Len(t, files, 1, "discover")
assert.Equal(t, dbPath, files[0].Path, "prefer db over pb")
assert.Equal(t, "/tmp/db-proj", files[0].Project, "project")
assert.Equal(t, dbPath, findAntigravityCLITestSourceFile(t, root, id), "find")
sess, msgs, err := parseAntigravityCLITestSession(t,
files[0].Path, files[0].Project, "test-machine",
)
require.NoError(t, err, "parse")
assert.Equal(t, "antigravity-cli:"+id, sess.ID)
assert.Equal(t, AgentAntigravityCLI, sess.Agent)
assert.Equal(t, dbPath, sess.File.Path)
assert.Equal(t, "/tmp/db-proj", sess.Project)
require.Len(t, msgs, 2)
assert.Equal(t, RoleUser, msgs[0].Role)
assert.Equal(t, "db prompt fallback", msgs[0].Content)
assert.Equal(t, RoleAssistant, msgs[1].Role)
assert.Contains(t, msgs[1].Content, "assistant reply content body")
assert.Equal(t, 2, sess.MessageCount)
assert.Equal(t, 1, sess.UserMessageCount)
assert.Equal(t, "db prompt fallback", sess.FirstMessage)
}
func TestAntigravityCLIProjectFallbackPromptAndProximity(t *testing.T) {
root := t.TempDir()
id := "f0f0f0f0-f1f1-f2f2-f3f3-f4f4f4f4f4f4"
mustMkdir(t, filepath.Join(root, "conversations"))
mustMkdir(t, filepath.Join(root, "brain", id))
dbPath := filepath.Join(root, "conversations", id+".db")
createAntigravityTestDB(t, dbPath) // user prompt: "user prompt text goes here", ts: 1779000000
// Create history.jsonl with a row omitting conversationId, matching text, and close timestamp (1779000000000 ms)
mustWrite(t, filepath.Join(root, "history.jsonl"),
[]byte(`{"display":" user prompt text goes here ","timestamp":1779000010000,"workspace":"/tmp/fallback-proj"}`))
sess, msgs, err := parseAntigravityCLITestSession(t, dbPath, "", "m")
require.NoError(t, err)
require.Len(t, msgs, 2)
assert.Equal(t, "/tmp/fallback-proj", sess.Project, "should successfully fallback infer project")
}
func TestAntigravityCLIProjectFallbackStrictWindow(t *testing.T) {
root := t.TempDir()
id := "e0e0e0e0-e1e1-e2e2-e3e3-e4e4e4e4e4e4"
mustMkdir(t, filepath.Join(root, "conversations"))
mustMkdir(t, filepath.Join(root, "brain", id))
dbPath := filepath.Join(root, "conversations", id+".db")
createAntigravityTestDB(t, dbPath) // user prompt: "user prompt text goes here", ts: 1779000000
// Create history.jsonl with timestamp outside the 1-minute window (e.g., 65 seconds later)
mustWrite(t, filepath.Join(root, "history.jsonl"),
[]byte(`{"display":"user prompt text goes here","timestamp":1779000065000,"workspace":"/tmp/too-late-proj"}`))
sess, _, err := parseAntigravityCLITestSession(t, dbPath, "", "m")
require.NoError(t, err)
assert.Empty(t, sess.Project, "should reject match outside 1-minute window")
}
func TestAntigravityCLIProjectFallbackAmbiguous(t *testing.T) {
root := t.TempDir()
id := "d0d0d0d0-d1d1-d2d2-d3d3-d4d4d4d4d4d4"
mustMkdir(t, filepath.Join(root, "conversations"))
mustMkdir(t, filepath.Join(root, "brain", id))
dbPath := filepath.Join(root, "conversations", id+".db")
createAntigravityTestDB(t, dbPath) // user prompt: "user prompt text goes here", ts: 1779000000
// Create history.jsonl with two rows having matching prompts, same timestamp difference, but different workspaces
mustWrite(t, filepath.Join(root, "history.jsonl"),
[]byte(`{"display":"user prompt text goes here","timestamp":1779000005000,"workspace":"/tmp/proj-a"}
{"display":"user prompt text goes here","timestamp":1779000005000,"workspace":"/tmp/proj-b"}`))
sess, _, err := parseAntigravityCLITestSession(t, dbPath, "", "m")
require.NoError(t, err)
assert.Empty(t, sess.Project, "should reject ambiguous match with different workspaces at same time closeness")
}
func TestAntigravityCLIProjectFallbackShortPrompt(t *testing.T) {
root := t.TempDir()
id := "c0c0c0c0-c1c1-c2c2-c3c3-c4c4c4c4c4c4"
mustMkdir(t, filepath.Join(root, "conversations"))
mustMkdir(t, filepath.Join(root, "brain", id))
dbPath := filepath.Join(root, "conversations", id+".db")
createAntigravityOvershortPromptDB(t, dbPath) // user prompt: "hi", ts: 1779000000
// Create history.jsonl with matching short prompt "hi"
mustWrite(t, filepath.Join(root, "history.jsonl"),
[]byte(`{"display":"hi","timestamp":1779000005000,"workspace":"/tmp/short-proj"}`))
sess, _, err := parseAntigravityCLITestSession(t, dbPath, "", "m")
require.NoError(t, err)
assert.Empty(t, sess.Project, "should reject matching short prompts")
}
func createAntigravityOvershortPromptDB(t *testing.T, path string) {
t.Helper()
db, err := sql.Open("sqlite3", path)
require.NoError(t, err, "open")
defer db.Close()
createAntigravityStepTables(t, db)
tsEarly := encodePB([]pbField{
{num: 1, wire: pbWireVarint, varint: 1779000000},
})
userPayload := encodePB([]pbField{
{num: 5, wire: pbWireBytes, bytes: tsEarly},
{
num: 17,
wire: pbWireBytes,
bytes: []byte("hi"),
},
})
mustExec(t, db,
`INSERT INTO steps (idx, step_type, step_payload) `+
`VALUES (?, ?, ?)`,
0, 14, userPayload)
}
func TestAntigravityCLIDBFileInfoIncludesSQLiteSidecars(t *testing.T) {
root := t.TempDir()
id := "44444444-5555-6666-7777-888888888888"
mustMkdir(t, filepath.Join(root, "conversations"))
dbPath := filepath.Join(root, "conversations", id+".db")
mustWrite(t, dbPath, []byte("db"))
mustWrite(t, dbPath+"-wal", []byte("wal"))
mustWrite(t, dbPath+"-shm", []byte("shm"))
early := time.Unix(1779000000, 0)
late := time.Unix(1779000300, 0)
require.NoError(t, os.Chtimes(dbPath, early, early))
require.NoError(t, os.Chtimes(dbPath+"-wal", late, late))
require.NoError(t, os.Chtimes(dbPath+"-shm", early, early))
info, err := AntigravityCLIFileInfo(dbPath)
require.NoError(t, err)
assert.Equal(t, int64(len("dbwalshm")), info.Size())
assert.Equal(t, late.UnixNano(), info.ModTime().UnixNano())
}
func TestAntigravityCLIFileInfoIncludesHistoryForLegacySync(t *testing.T) {
early := time.Unix(1779000000, 0)
late := time.Unix(1779000300, 0)
history := []byte(`{"display":"history prompt","timestamp":1779000000000,` +
`"workspace":"/tmp/proj","conversationId":"id"}` + "\n")
t.Run("db session", func(t *testing.T) {
root := t.TempDir()
id := "14141414-2525-3636-4747-585858585858"
mustMkdir(t, filepath.Join(root, "conversations"))
dbPath := filepath.Join(root, "conversations", id+".db")
historyPath := filepath.Join(root, "history.jsonl")
mustWrite(t, dbPath, []byte("db"))
mustWrite(t, historyPath, history)
require.NoError(t, os.Chtimes(dbPath, early, early))
require.NoError(t, os.Chtimes(historyPath, late, late))
info, err := AntigravityCLIFileInfo(dbPath)
require.NoError(t, err)
assert.Equal(t, int64(len("db")+len(history)), info.Size())
assert.Equal(t, late.UnixNano(), info.ModTime().UnixNano())
})
t.Run("pb session", func(t *testing.T) {
root := t.TempDir()
id := "15151515-2626-3737-4848-595959595959"
mustMkdir(t, filepath.Join(root, "implicit"))
pbPath := filepath.Join(root, "implicit", id+".pb")
historyPath := filepath.Join(root, "history.jsonl")
mustWrite(t, pbPath, []byte("pb"))
mustWrite(t, historyPath, history)
require.NoError(t, os.Chtimes(pbPath, early, early))
require.NoError(t, os.Chtimes(historyPath, late, late))
info, err := AntigravityCLIFileInfo(pbPath)
require.NoError(t, err)
assert.Equal(t, int64(len("pb")+len(history)), info.Size())
assert.Equal(t, late.UnixNano(), info.ModTime().UnixNano())
})
}
// TestAntigravityCLIFileInfoIncludesBrainArtifacts pins brain
// artifacts into the CLI composite fingerprint: the parser renders
// brain/<id>/*.md (+ .metadata.json) as messages, so a brain-only
// add/edit/delete must change the effective file info or skip checks
// keep stale brain messages.
func TestAntigravityCLIFileInfoIncludesBrainArtifacts(t *testing.T) {
early := time.Unix(1779000000, 0)
late := time.Unix(1779000300, 0)
t.Run("db session", func(t *testing.T) {
root := t.TempDir()
id := "14141414-2525-3636-4747-585858585858"
mustMkdir(t, filepath.Join(root, "conversations"))
dbPath := filepath.Join(root, "conversations", id+".db")
mustWrite(t, dbPath, []byte("db"))
brainDir := filepath.Join(root, "brain", id)
mustMkdir(t, brainDir)
mdPath := filepath.Join(brainDir, "task.md")
mustWrite(t, mdPath, []byte("brain doc"))
require.NoError(t, os.Chtimes(dbPath, early, early))
require.NoError(t, os.Chtimes(mdPath, late, late))
info, err := AntigravityCLIFileInfo(dbPath)
require.NoError(t, err)
assert.Equal(t, int64(len("db")+len("brain doc")), info.Size())
assert.Equal(t, late.UnixNano(), info.ModTime().UnixNano())
})
t.Run("pb session", func(t *testing.T) {
root := t.TempDir()
id := "15151515-2626-3737-4848-595959595959"
mustMkdir(t, filepath.Join(root, "implicit"))
pbPath := filepath.Join(root, "implicit", id+".pb")
mustWrite(t, pbPath, []byte("pb"))
brainDir := filepath.Join(root, "brain", id)
mustMkdir(t, brainDir)
mdPath := filepath.Join(brainDir, "task.md")
mustWrite(t, mdPath, []byte("brain doc"))
require.NoError(t, os.Chtimes(pbPath, early, early))
require.NoError(t, os.Chtimes(mdPath, late, late))
info, err := AntigravityCLIFileInfo(pbPath)
require.NoError(t, err)
assert.Equal(t, int64(len("pb")+len("brain doc")), info.Size())
assert.Equal(t, late.UnixNano(), info.ModTime().UnixNano())
})
}
func TestAntigravityCLIDBInsertsShortHistoryPrompt(t *testing.T) {
root := t.TempDir()
id := "55555555-6666-7777-8888-999999999999"
mustMkdir(t, filepath.Join(root, "conversations"))
dbPath := filepath.Join(root, "conversations", id+".db")
createAntigravityShortPromptDB(t, dbPath)
mustWrite(t, filepath.Join(root, "history.jsonl"),
[]byte(`{"display":"fix lint","timestamp":1779000000000,`+
`"workspace":"/tmp/db-proj","conversationId":"`+id+`"}`))
sess, msgs, err := parseAntigravityCLITestSession(t,
dbPath, "", "test-machine",
)
require.NoError(t, err)
require.Len(t, msgs, 2)
assert.Equal(t, RoleUser, msgs[0].Role)
assert.Equal(t, "fix lint", msgs[0].Content)
assert.Equal(t, RoleAssistant, msgs[1].Role)
assert.Contains(t, msgs[1].Content, "assistant reply content body")
assert.Equal(t, 1, sess.UserMessageCount)
assert.Equal(t, "fix lint", sess.FirstMessage)
}
func TestAntigravityCLIDiscoverIgnoresJunk(t *testing.T) {
root := t.TempDir()
mustMkdir(t, filepath.Join(root, "conversations"))
// Non-.pb files in the conversations dir are ignored.
mustWrite(t,
filepath.Join(root, "conversations", "README.txt"),
[]byte("x"))
// .pb files whose stem isn't a valid session id (contains
// characters outside [A-Za-z0-9_-]) are skipped.
mustWrite(t,
filepath.Join(root, "conversations", "bad.name.pb"),
[]byte("x"))
assert.Empty(t, discoverAntigravityCLITestSessions(t, root))
}
// ---- IDE parser -----------------------------------------------
func TestAntigravityIDEDiscoverAndParse(t *testing.T) {
root := t.TempDir()
id := "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"
mustMkdir(t, filepath.Join(root, "conversations"))
mustMkdir(t, filepath.Join(root, "annotations"))
mustMkdir(t, filepath.Join(root, "brain", id))
dbPath := filepath.Join(root, "conversations", id+".db")
createAntigravityTestDB(t, dbPath)
mustWrite(t,
filepath.Join(root, "annotations", id+".pbtxt"),
[]byte("last_user_view_time:{seconds:1779326586 nanos:0}\n"))
mustWrite(t,
filepath.Join(root, "brain", id, "plan.md"),
[]byte("# Plan"))
mustWrite(t,
filepath.Join(root, "brain", id, "plan.md.metadata.json"),
[]byte(`{"summary":"Plan summary","updatedAt":"2026-05-20T22:47:27Z"}`))
files := discoverAntigravityTestSessions(t, root)
require.Len(t, files, 1)
assert.Equal(t, dbPath, files[0].Path)
assert.Equal(t, dbPath, findAntigravityTestSourceFile(t, root, id))
sess, msgs, _, err := parseAntigravityTestSession(t,
dbPath, "", "test-machine",
)
require.NoError(t, err, "parse")
assert.Equal(t, "antigravity:"+id, sess.ID)
// 2 step rows + 1 brain artifact = 3 messages
require.Len(t, msgs, 3)
// step_type=14 should be flagged as user
var sawUser, sawAssistant bool
for _, m := range msgs {
if m.Role == RoleUser {
sawUser = true
assert.Contains(t, m.Content, "user prompt text")
}
if m.Role == RoleAssistant &&
strings.Contains(m.Content, "Plan summary") {
sawAssistant = true
}
}
assert.True(t, sawUser, "missing user role")
assert.True(t, sawAssistant, "missing assistant role")
// Annotation overrides endedAt to 2026-05-20T... =
// 1779326586
assert.Equal(t, int64(1779326586), sess.EndedAt.Unix())
}
func TestDecodeAntigravityStepFiltersInternalStrings(t *testing.T) {
ts := encodePB([]pbField{
{num: 1, wire: pbWireVarint, varint: 1779000000},
})
payload := encodePB([]pbField{
{num: 5, wire: pbWireBytes, bytes: ts},
{
num: 17, wire: pbWireBytes,
bytes: []byte("67fdbde7-4a15-4599-a206-5ed536cf1fc4"),
},
{
num: 18, wire: pbWireBytes,
bytes: []byte("can you review the proposed issues before filing?"),
},
{
num: 19, wire: pbWireBytes,
bytes: []byte("can you review the proposed issues before filing?"),
},
{
num: 20, wire: pbWireBytes,
bytes: []byte("/home/mj/.gemini/antigravity-cli/skills"),
},
})
msg, ok := decodeAntigravityStep(0, 14, payload)
require.True(t, ok)
assert.Equal(t, RoleUser, msg.Role)
assert.Equal(t, "can you review the proposed issues before filing?", msg.Content)
assert.NotContains(t, msg.Content, "[step")
assert.NotContains(t, msg.Content, "67fdbde7")
assert.NotContains(t, msg.Content, ".gemini")
}
func TestDecodeAntigravityStepKeepsBareURLUserPrompt(t *testing.T) {
// Captured from an Antigravity CLI SQLite conversation generated by:
// agy --prompt "https://example.com/agentsview-bare-url-verification-2-20260615"
// The user prompt lives in the observed step_payload model:
// top-level field 1 = CortexStepType USER_INPUT (14), with field 19
// carrying a prompt submessage whose field 2 is the clean URL text.
const prompt = "https://example.com/agentsview-bare-url-verification-2-20260615"
promptPayload := encodePB([]pbField{
{
num: 2,
wire: pbWireBytes,
bytes: []byte(prompt),
},
{
num: 3,
wire: pbWireBytes,
bytes: append([]byte{'\n', byte(len(prompt))}, []byte(prompt)...),
},
})
payload := encodePB([]pbField{
{
num: 1,
wire: pbWireVarint,
varint: 14,
},
{
num: 19,
wire: pbWireBytes,
bytes: promptPayload,
},
})
msg, ok := decodeAntigravityStep(0, 0, payload)
require.True(t, ok)
assert.Equal(t, RoleUser, msg.Role)
assert.Equal(t, prompt, msg.Content)
}
func TestDecodeAntigravityStepKeepsCleanAssistantText(t *testing.T) {
payload := encodePB([]pbField{
{
num: 17, wire: pbWireBytes,
bytes: []byte("06b66779-eebe-4869-ba1b-ccf3d42be70b"),
},
{
num: 18, wire: pbWireBytes,
bytes: []byte("-3750763034362895579"),
},
{
num: 19, wire: pbWireBytes,
bytes: []byte("mYseaoyPDcS6qtsP7c6Z6QE"),
},
{
num: 20, wire: pbWireBytes,
bytes: []byte("I will start by listing the directory structure."),
},
{
num: 21, wire: pbWireBytes,
bytes: []byte(`{"DirectoryPath":"/tmp/project","toolAction":"Listing workspace files","toolSummary":"List directory contents"}`),
},
{
num: 22, wire: pbWireBytes,
bytes: []byte("MODEL_PLACEHOLDER_M20"),
},
{
num: 23, wire: pbWireBytes,
bytes: []byte("I will start by listing the directory structure."),
},
})
msg, ok := decodeAntigravityStep(1, 15, payload)
require.True(t, ok)
assert.Equal(t, RoleAssistant, msg.Role)
assert.Equal(t, "I will start by listing the directory structure.", msg.Content)
assert.NotContains(t, msg.Content, "[step")
assert.NotContains(t, msg.Content, "06b66779")
assert.NotContains(t, msg.Content, "-3750763034362895579")
assert.NotContains(t, msg.Content, "mYseaoyPDcS6qtsP7c6Z6QE")
assert.NotContains(t, msg.Content, "toolAction")
assert.NotContains(t, msg.Content, "MODEL_PLACEHOLDER")
}
// TestDecodeAntigravityStepKeepsBareURLAssistantContent verifies that an
// assistant step whose only displayable content is a bare URL is not
// dropped: the URL noise filter suppresses such links only when other
// content (prose or a tool call) carries the step.
func TestDecodeAntigravityStepKeepsBareURLAssistantContent(t *testing.T) {
const url = "https://example.com/release-notes"
payload := encodePB([]pbField{
{num: 17, wire: pbWireBytes, bytes: []byte(url)},
})
msg, ok := decodeAntigravityStep(0, 15, payload)
require.True(t, ok, "URL-only assistant step must survive")
assert.Equal(t, RoleAssistant, msg.Role)
assert.Equal(t, url, msg.Content)
}
// TestDecodeAntigravityStepKeepsShortBareURLAssistantContent verifies
// that a URL-only assistant message shorter than the 20-rune prose
// collection threshold still survives via the dedicated bare-URL pass.
func TestDecodeAntigravityStepKeepsShortBareURLAssistantContent(t *testing.T) {
const url = "https://go.dev"
require.Less(t, len(url), 20, "test must exercise the sub-threshold path")
payload := encodePB([]pbField{
{num: 17, wire: pbWireBytes, bytes: []byte(url)},
})
msg, ok := decodeAntigravityStep(0, 15, payload)
require.True(t, ok, "short URL-only assistant step must survive")
assert.Equal(t, RoleAssistant, msg.Role)
assert.Equal(t, url, msg.Content)
}
// TestDecodeAntigravityStepKeepsShortBareURLUserPrompt verifies that a
// URL-only user prompt shorter than the 20-rune prose collection
// threshold survives via the dedicated bare-URL pass.
func TestDecodeAntigravityStepKeepsShortBareURLUserPrompt(t *testing.T) {
const url = "https://go.dev"
require.Less(t, len(url), 20, "test must exercise the sub-threshold path")
payload := encodePB([]pbField{
{num: 19, wire: pbWireBytes, bytes: []byte(url)},
})
msg, ok := decodeAntigravityStep(0, 14, payload)
require.True(t, ok, "short URL-only user prompt must survive")
assert.Equal(t, RoleUser, msg.Role)
assert.Equal(t, url, msg.Content)
}
// TestDecodeAntigravityStepKeepsProseStartingWithURL verifies that
// assistant prose that merely begins with a link is preserved; only a
// bare URL token is treated as metadata noise.
func TestDecodeAntigravityStepKeepsProseStartingWithURL(t *testing.T) {
const prose = "https://example.com is the canonical docs site."
payload := encodePB([]pbField{
{num: 17, wire: pbWireBytes, bytes: []byte(prose)},
})
msg, ok := decodeAntigravityStep(0, 15, payload)
require.True(t, ok)
assert.Equal(t, RoleAssistant, msg.Role)
assert.Equal(t, prose, msg.Content)
}
// TestDecodeAntigravityStepDropsBareURLAlongsideProse verifies that the
// URL noise filter still suppresses a bare-URL echo when the step has
// other displayable prose to carry it.
func TestDecodeAntigravityStepDropsBareURLAlongsideProse(t *testing.T) {
payload := encodePB([]pbField{
{num: 17, wire: pbWireBytes, bytes: []byte("https://example.com/fetched")},
{num: 18, wire: pbWireBytes, bytes: []byte("Fetched the release notes.")},
})
msg, ok := decodeAntigravityStep(0, 15, payload)
require.True(t, ok)
assert.Equal(t, RoleAssistant, msg.Role)
assert.Equal(t, "Fetched the release notes.", msg.Content)
assert.NotContains(t, msg.Content, "https://example.com/fetched")
}
// TestDecodeAntigravityStepSanitizesNUL verifies that NUL bytes in
// otherwise-valid content are replaced rather than the string (or
// the whole message) being dropped: NUL-delimited tool output such
// as `git ls-files -z` is realistic transcript content, while a NUL
// that leaks into persisted text breaks `pg push` (SQLSTATE 22021).
func TestDecodeAntigravityStepSanitizesNUL(t *testing.T) {
payload := encodePB([]pbField{
{
num: 17, wire: pbWireBytes,
bytes: []byte("file_a.go\x00file_b.go\x00file_c.go"),
},
})
msg, ok := decodeAntigravityStep(0, 2, payload)
require.True(t, ok, "NUL-bearing content must survive, not drop")
assert.NotContains(t, msg.Content, "\x00")
assert.Equal(t,
"file_a.gofile_b.gofile_c.go", msg.Content)
}
func TestMergeAntigravityDBHistoryMessagesAppendsMissingPrompts(t *testing.T) {
msgs := []ParsedMessage{
{Role: RoleUser, Content: "first decoded prompt"},
{Role: RoleAssistant, Content: "assistant"},
{Role: RoleUser, Content: "second decoded prompt"},
}
history := []ParsedMessage{
{Role: RoleUser, Content: "only tagged history prompt"},
}
got := mergeAntigravityDBHistoryMessages(msgs, history)
require.Len(t, got, 4)
assert.Equal(t, "first decoded prompt", got[0].Content)
assert.Equal(t, "second decoded prompt", got[2].Content)
assert.Equal(t, "only tagged history prompt", got[3].Content)
}
func TestMergeAntigravityDBHistoryMessagesIgnoresBlankHistoryRows(t *testing.T) {
ts := time.Unix(1779000000, 0)
msgs := []ParsedMessage{
{Role: RoleUser, Content: "decoded prompt"},
{Role: RoleAssistant, Content: "assistant"},
}
history := []ParsedMessage{
{Role: RoleUser, Content: ""},
{Role: RoleUser, Content: "history prompt", Timestamp: ts},
{Role: RoleUser, Content: " "},
}
got := mergeAntigravityDBHistoryMessages(msgs, history)
assert.Equal(t, "history prompt", got[0].Content)
assert.Equal(t, len("history prompt"), got[0].ContentLength)
assert.Equal(t, ts, got[0].Timestamp)
}
// ---- crypto: key loading --------------------------------------
func TestAntigravityKeyMissing(t *testing.T) {
// loadAntigravityKey memoizes via sync.Once, so we test the
// observable behavior via hasAntigravityKey on a process
// without the env var. Set+unset to be explicit.
t.Setenv("ANTIGRAVITY_KEY", "")
// Cannot reset sync.Once without restructuring the source.
// At minimum verify hasAntigravityKey doesn't panic.
_ = hasAntigravityKey()
}
// ---- crypto: cipher round-trips -------------------------------
// TestDecryptAesGCMRoundTrip encrypts a payload with stdlib AES-GCM
// in the same layout decryptAesGCM expects (12-byte nonce prefix +
// ciphertext-with-tag) and confirms recovery. GCM is Antigravity's
// primary cipher per the handoff.
func TestDecryptAesGCMRoundTrip(t *testing.T) {
key := bytes.Repeat([]byte{0x42}, 32)
plaintext := []byte("hello antigravity gcm world")
block, err := aes.NewCipher(key)
require.NoError(t, err, "new cipher")
gcm, err := cipher.NewGCM(block)
require.NoError(t, err, "new gcm")
nonce := bytes.Repeat([]byte{0x01}, 12)
ct := gcm.Seal(nil, nonce, plaintext, nil)
data := append(append([]byte{}, nonce...), ct...)
got := decryptAesGCM(data, key, 0)
assert.True(t, bytes.Equal(got, plaintext), "decrypt: got %q want %q", got, plaintext)
// Wrong key → nil (auth tag fails).
bad := bytes.Repeat([]byte{0x43}, 32)
assert.Nil(t, decryptAesGCM(data, bad, 0), "wrong key should fail")
// Too-short input → nil, not panic.
assert.Nil(t, decryptAesGCM([]byte{0x00}, key, 0), "short input should return nil")
}
// TestDecryptAesGCMSkip confirms the leading-bytes skip works as
// documented (the brute-forcer tries 0/1/2/4/8 byte prefixes).
func TestDecryptAesGCMSkip(t *testing.T) {
key := bytes.Repeat([]byte{0x42}, 32)
plaintext := []byte("with leading junk bytes")
block, _ := aes.NewCipher(key)
gcm, _ := cipher.NewGCM(block)
nonce := bytes.Repeat([]byte{0x02}, 12)
ct := gcm.Seal(nil, nonce, plaintext, nil)
prefix := []byte{0xDE, 0xAD, 0xBE, 0xEF}
data := append(append([]byte{}, prefix...), nonce...)
data = append(data, ct...)
got := decryptAesGCM(data, key, len(prefix))
assert.True(t, bytes.Equal(got, plaintext), "decrypt with skip: got %q want %q", got, plaintext)
}
func TestStripPKCS7(t *testing.T) {
cases := []struct {
name string
in []byte
want []byte
}{
{
name: "valid one-byte pad",
in: []byte{0x41, 0x42, 0x43, 0x01},
want: []byte{0x41, 0x42, 0x43},
},
{
name: "valid four-byte pad",
in: []byte{
0x41, 0x42, 0x43, 0x44,
0x04, 0x04, 0x04, 0x04,
},
want: []byte{0x41, 0x42, 0x43, 0x44},
},
{
name: "empty input passes through",
in: []byte{},
want: []byte{},
},
{
name: "pad byte zero is invalid → unchanged",
in: []byte{0x41, 0x00},
want: []byte{0x41, 0x00},
},
{
name: "pad larger than block size → unchanged",
in: []byte{0x41, 0x42, 0xFF},
want: []byte{0x41, 0x42, 0xFF},
},
{
name: "inconsistent pad bytes → unchanged",
in: []byte{0x41, 0x02, 0x03},
want: []byte{0x41, 0x02, 0x03},
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
assert.Equal(t, tc.want, stripPKCS7(tc.in))
})
}
}
// ---- CLI parser: discovery edges ------------------------------
// TestAntigravityCLIDiscoverImplicit confirms .pb files under
// implicit/ are discovered alongside conversations/.
func TestAntigravityCLIDiscoverImplicit(t *testing.T) {
root := t.TempDir()
convID := "aaaaaaaa-1111-2222-3333-444444444444"
implID := "bbbbbbbb-5555-6666-7777-888888888888"
mustMkdir(t, filepath.Join(root, "conversations"))
mustMkdir(t, filepath.Join(root, "implicit"))
mustWrite(t,
filepath.Join(root, "conversations", convID+".pb"),
[]byte("x"))
mustWrite(t,
filepath.Join(root, "implicit", implID+".pb"),
[]byte("x"))
files := discoverAntigravityCLITestSessions(t, root)
require.Len(t, files, 2, "got files, want 2 (one per subdir)")
var sawConv, sawImpl bool
for _, f := range files {
switch filepath.Base(filepath.Dir(f.Path)) {
case "conversations":
sawConv = true
case "implicit":
sawImpl = true
}
}
assert.True(t, sawConv, "missing conv subdir")
assert.True(t, sawImpl, "missing impl subdir")
// The provider source lookup routes implicit-tagged ids to the
// implicit/ subdir; bare ids resolve under conversations/.
wantImpl := filepath.Join("implicit", implID+".pb")
gotImpl := findAntigravityCLITestSourceFile(t, root, "implicit-"+implID)
require.NotEmpty(t, gotImpl)
assert.True(t, strings.HasSuffix(gotImpl, wantImpl), "find implicit: %q", gotImpl)
wantConv := filepath.Join("conversations", convID+".pb")
gotConv := findAntigravityCLITestSourceFile(t, root, convID)
require.NotEmpty(t, gotConv)
assert.True(t, strings.HasSuffix(gotConv, wantConv), "find conv: %q", gotConv)
// A bare implicit-only UUID must NOT resolve under conversations/.
assert.Empty(t, findAntigravityCLITestSourceFile(t, root, implID),
"bare implicit id should not resolve")
}
// TestAntigravityCLIImplicitSessionIDDistinct ensures a UUID that
// appears under both conversations/ and implicit/ produces two
// distinct storage IDs, so one record doesn't overwrite the other.
func TestAntigravityCLIImplicitSessionIDDistinct(t *testing.T) {
root := t.TempDir()
id := "cccccccc-9999-aaaa-bbbb-dddddddddddd"
mustMkdir(t, filepath.Join(root, "conversations"))
mustMkdir(t, filepath.Join(root, "implicit"))
convPath := filepath.Join(root, "conversations", id+".pb")
implPath := filepath.Join(root, "implicit", id+".pb")
mustWrite(t, convPath, []byte("x"))
mustWrite(t, implPath, []byte("x"))
convSess, _, err := parseAntigravityCLITestSession(t, convPath, "", "m")
require.NoError(t, err, "parse conv")
implSess, _, err := parseAntigravityCLITestSession(t, implPath, "", "m")
require.NoError(t, err, "parse impl")
assert.NotEqual(t, implSess.ID, convSess.ID, "session ids collide")
assert.Equal(t, "antigravity-cli:"+id, convSess.ID, "conv id")
assert.Equal(t, "antigravity-cli:implicit-"+id, implSess.ID, "impl id")
// Round-trip: each storage id resolves back to its own file.
assert.Equal(t, convPath, findAntigravityCLITestSourceFile(t, root, id), "round-trip conv")
assert.Equal(t, implPath, findAntigravityCLITestSourceFile(t, root, "implicit-"+id), "round-trip impl")
}
func TestBuildAntigravityProjectMapRobust(t *testing.T) {
root := t.TempDir()
path := filepath.Join(root, "history.jsonl")
// Missing file → empty map, no error.
assert.Empty(t, buildAntigravityProjectMap(path), "missing file")
// Mix of valid rows, blank lines, garbage, and rows missing
// one of the two required fields. Only the valid rows survive.
mustWrite(t, path, []byte(
`{"conversationId":"id-1","workspace":"/tmp/a"}`+"\n"+
""+"\n"+
`not json at all`+"\n"+
`{"conversationId":"id-2"}`+"\n"+
`{"workspace":"/tmp/orphan"}`+"\n"+
`{"conversationId":"id-3","workspace":"/tmp/c"}`+"\n",
))
m := buildAntigravityProjectMap(path)
require.Len(t, m, 2, "map entries")
assert.Equal(t, "/tmp/a", m["id-1"])
assert.Equal(t, "/tmp/c", m["id-3"])
_, ok := m["id-2"]
assert.False(t, ok, "id-2 had no workspace, should be absent")
}
// ---- helpers --------------------------------------------------
func mustMkdir(t *testing.T, p string) {
t.Helper()
require.NoError(t, os.MkdirAll(p, 0o755), "mkdir %s", p)
}
func mustWrite(t *testing.T, p string, b []byte) {
t.Helper()
require.NoError(t, os.WriteFile(p, b, 0o644), "write %s", p)
}
// createAntigravityTestDB writes a minimal antigravity IDE
// SQLite database with two synthetic steps: a user prompt
// (step_type=14) and an assistant step (step_type=17).
func createAntigravityTestDB(t *testing.T, path string) {
t.Helper()
db, err := sql.Open("sqlite3", path)
require.NoError(t, err, "open")
defer db.Close()
createAntigravityStepTables(t, db)
tsEarly := encodePB([]pbField{
{num: 1, wire: pbWireVarint, varint: 1779000000},
})
userPayload := encodePB([]pbField{
{num: 5, wire: pbWireBytes, bytes: tsEarly},
{
num: 17,
wire: pbWireBytes,
bytes: []byte("user prompt text goes here"),
},
})
tsLate := encodePB([]pbField{
{num: 1, wire: pbWireVarint, varint: 1779000100},
})
asstPayload := encodePB([]pbField{
{num: 5, wire: pbWireBytes, bytes: tsLate},
{
num: 17,
wire: pbWireBytes,
bytes: []byte("assistant reply content body"),
},
})
mustExec(t, db,
`INSERT INTO steps (idx, step_type, step_payload) `+
`VALUES (?, ?, ?)`,
0, 14, userPayload)
mustExec(t, db,
`INSERT INTO steps (idx, step_type, step_payload) `+
`VALUES (?, ?, ?)`,
1, 17, asstPayload)
}
func createAntigravityShortPromptDB(t *testing.T, path string) {
t.Helper()
db, err := sql.Open("sqlite3", path)
require.NoError(t, err, "open")
defer db.Close()
createAntigravityStepTables(t, db)
tsEarly := encodePB([]pbField{
{num: 1, wire: pbWireVarint, varint: 1779000000},
})
userPayload := encodePB([]pbField{
{num: 5, wire: pbWireBytes, bytes: tsEarly},
{
num: 17,
wire: pbWireBytes,
bytes: []byte("fix lint"),
},
})
tsLate := encodePB([]pbField{
{num: 1, wire: pbWireVarint, varint: 1779000100},
})
asstPayload := encodePB([]pbField{
{num: 5, wire: pbWireBytes, bytes: tsLate},
{
num: 17,
wire: pbWireBytes,
bytes: []byte("assistant reply content body"),
},
})
mustExec(t, db,
`INSERT INTO steps (idx, step_type, step_payload) `+
`VALUES (?, ?, ?)`,
0, 14, userPayload)
mustExec(t, db,
`INSERT INTO steps (idx, step_type, step_payload) `+
`VALUES (?, ?, ?)`,
1, 17, asstPayload)
}
func createAntigravityStepTables(t *testing.T, db *sql.DB) {
t.Helper()
mustExec(t, db, `CREATE TABLE trajectory_meta (
trajectory_id text, cascade_id text,
trajectory_type integer, source integer,
PRIMARY KEY (trajectory_id))`)
mustExec(t, db, `CREATE TABLE steps (
idx integer, step_type integer NOT NULL DEFAULT 0,
status integer NOT NULL DEFAULT 0,
has_subtrajectory numeric NOT NULL DEFAULT false,
metadata blob, error_details blob,
permissions blob, task_details blob,
render_info blob, step_payload blob,
step_format integer NOT NULL DEFAULT 0,
PRIMARY KEY (idx))`)
}
func mustExec(
t *testing.T, db *sql.DB, q string, args ...any,
) {
t.Helper()
_, err := db.Exec(q, args...)
require.NoError(t, err, "exec %q", q)
}
// createAntigravityToolCallDB creates a .db with two steps:
// - step_type=14 (user): plain text prompt
// - step_type=17 (assistant): payload containing the string "view_file"
// embedded as a protobuf string field, simulating a planner response
// that invoked the view_file tool.
func createAntigravityToolCallDB(t *testing.T, path string) {
t.Helper()
db, err := sql.Open("sqlite3", path)
require.NoError(t, err, "open")
defer db.Close()
createAntigravityStepTables(t, db)
tsEarly := encodePB([]pbField{
{num: 1, wire: pbWireVarint, varint: 1779000000},
})
userPayload := encodePB([]pbField{
{num: 5, wire: pbWireBytes, bytes: tsEarly},
{num: 17, wire: pbWireBytes, bytes: []byte("please check the file")},
})
tsLate := encodePB([]pbField{
{num: 1, wire: pbWireVarint, varint: 1779000100},
})
// Embed "view_file" as a string field inside a nested message
// that also contains a UUID-like tool-use ID. This mirrors the
// protobuf shape observed in real Antigravity .db step payloads.
toolCallNested := encodePB([]pbField{
{num: 3, wire: pbWireBytes, bytes: []byte("view_file")},
{num: 4, wire: pbWireBytes, bytes: []byte("aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee")},
})
asstPayload := encodePB([]pbField{
{num: 5, wire: pbWireBytes, bytes: tsLate},
{num: 17, wire: pbWireBytes, bytes: []byte("I will read the file for you")},
{num: 8, wire: pbWireBytes, bytes: toolCallNested},
})
mustExec(t, db,
`INSERT INTO steps (idx, step_type, step_payload) VALUES (?, ?, ?)`,
0, 14, userPayload)
mustExec(t, db,
`INSERT INTO steps (idx, step_type, step_payload) VALUES (?, ?, ?)`,
1, 17, asstPayload)
}
// TestDecodeAntigravityStepToolCall is the tracer bullet: parsing a .db
// session whose assistant step contains a known tool name ("view_file")
// produces a message with HasToolUse=true and one ToolCall entry.
func TestDecodeAntigravityStepToolCall(t *testing.T) {
root := t.TempDir()
id := "aaaa1111-bbbb-cccc-dddd-eeeeeeeeeeee"
mustMkdir(t, filepath.Join(root, "conversations"))
dbPath := filepath.Join(root, "conversations", id+".db")
createAntigravityToolCallDB(t, dbPath)
sess, msgs, _, err := parseAntigravityTestSession(t, dbPath, "/tmp/proj", "m")
require.NoError(t, err)
require.NotNil(t, sess)
// Find the assistant message.
var asstMsg *ParsedMessage
for i := range msgs {
if msgs[i].Role == RoleAssistant {
asstMsg = &msgs[i]
break
}
}
require.NotNil(t, asstMsg, "expected an assistant message")
assert.True(t, asstMsg.HasToolUse, "assistant message should have HasToolUse=true")
require.Len(t, asstMsg.ToolCalls, 1, "expected one tool call")
assert.Equal(t, "view_file", asstMsg.ToolCalls[0].ToolName)
assert.Equal(t, "Read", asstMsg.ToolCalls[0].Category)
}
// silence unused warning on time import in case the file is
// trimmed in the future.
var _ = time.Time{}
// TestDecodeAntigravityStepUserNoToolCalls asserts that a user step
// (step_type=14) whose payload happens to contain a known tool name string
// does NOT produce any ToolCall entries - tool calls only belong on assistant
// messages.
func TestDecodeAntigravityStepUserNoToolCalls(t *testing.T) {
root := t.TempDir()
id := "bbbb2222-cccc-dddd-eeee-ffffffffffff"
mustMkdir(t, filepath.Join(root, "conversations"))
dbPath := filepath.Join(root, "conversations", id+".db")
// Build a .db where the USER step payload contains "view_file".
db, err := sql.Open("sqlite3", dbPath)
require.NoError(t, err)
defer db.Close()
createAntigravityStepTables(t, db)
ts := encodePB([]pbField{{num: 1, wire: pbWireVarint, varint: 1779000000}})
// User step (step_type=14) whose payload includes a tool name string.
userPayload := encodePB([]pbField{
{num: 5, wire: pbWireBytes, bytes: ts},
{num: 17, wire: pbWireBytes, bytes: []byte("please use view_file on that path")},
})
mustExec(t, db, `INSERT INTO steps (idx, step_type, step_payload) VALUES (?, ?, ?)`,
0, 14, userPayload)
sess, msgs, _, err := parseAntigravityTestSession(t, dbPath, "/tmp/proj", "m")
require.NoError(t, err)
require.NotNil(t, sess)
require.Len(t, msgs, 1)
assert.Equal(t, RoleUser, msgs[0].Role)
assert.False(t, msgs[0].HasToolUse, "user step should not have HasToolUse=true")
assert.Empty(t, msgs[0].ToolCalls, "user step should have no ToolCalls")
}
// TestDecodeAntigravityStepNoFalsePositives asserts that an assistant step
// whose payload contains only plain prose (no known tool name strings)
// produces no ToolCall entries and HasToolUse=false.
func TestDecodeAntigravityStepNoFalsePositives(t *testing.T) {
root := t.TempDir()
id := "cccc3333-dddd-eeee-ffff-000000000000"
mustMkdir(t, filepath.Join(root, "conversations"))
dbPath := filepath.Join(root, "conversations", id+".db")
db, err := sql.Open("sqlite3", dbPath)
require.NoError(t, err)
defer db.Close()
createAntigravityStepTables(t, db)
ts := encodePB([]pbField{{num: 1, wire: pbWireVarint, varint: 1779000000}})
// Assistant step with no known tool names - just prose.
asstPayload := encodePB([]pbField{
{num: 5, wire: pbWireBytes, bytes: ts},
{num: 17, wire: pbWireBytes, bytes: []byte("Here is my analysis of the codebase and recommendations for improvement.")},
})
mustExec(t, db, `INSERT INTO steps (idx, step_type, step_payload) VALUES (?, ?, ?)`,
0, 17, asstPayload)
sess, msgs, _, err := parseAntigravityTestSession(t, dbPath, "/tmp/proj", "m")
require.NoError(t, err)
require.NotNil(t, sess)
require.Len(t, msgs, 1)
assert.Equal(t, RoleAssistant, msgs[0].Role)
assert.False(t, msgs[0].HasToolUse, "plain-prose step should not have HasToolUse=true")
assert.Empty(t, msgs[0].ToolCalls, "plain-prose step should have no ToolCalls")
}
// TestDecodeAntigravityStepMultipleToolCalls asserts that an assistant step
// whose payload contains two distinct known tool names produces two ToolCall
// entries with the correct names and categories.
func TestDecodeAntigravityStepMultipleToolCalls(t *testing.T) {
root := t.TempDir()
id := "dddd4444-eeee-ffff-0000-111111111111"
mustMkdir(t, filepath.Join(root, "conversations"))
dbPath := filepath.Join(root, "conversations", id+".db")
db, err := sql.Open("sqlite3", dbPath)
require.NoError(t, err)
defer db.Close()
createAntigravityStepTables(t, db)
ts := encodePB([]pbField{{num: 1, wire: pbWireVarint, varint: 1779000000}})
// Two tool call nested messages in the same assistant step.
tc1 := encodePB([]pbField{
{num: 3, wire: pbWireBytes, bytes: []byte("view_file")},
{num: 4, wire: pbWireBytes, bytes: []byte("11111111-2222-3333-4444-555555555555")},
})
tc2 := encodePB([]pbField{
{num: 3, wire: pbWireBytes, bytes: []byte("run_command")},
{num: 4, wire: pbWireBytes, bytes: []byte("66666666-7777-8888-9999-aaaaaaaaaaaa")},
})
asstPayload := encodePB([]pbField{
{num: 5, wire: pbWireBytes, bytes: ts},
{num: 17, wire: pbWireBytes, bytes: []byte("reading file and running command")},
{num: 8, wire: pbWireBytes, bytes: tc1},
{num: 9, wire: pbWireBytes, bytes: tc2},
})
mustExec(t, db, `INSERT INTO steps (idx, step_type, step_payload) VALUES (?, ?, ?)`,
0, 17, asstPayload)
sess, msgs, _, err := parseAntigravityTestSession(t, dbPath, "/tmp/proj", "m")
require.NoError(t, err)
require.NotNil(t, sess)
require.Len(t, msgs, 1)
assert.True(t, msgs[0].HasToolUse)
require.Len(t, msgs[0].ToolCalls, 2, "expected two tool calls")
toolNames := []string{msgs[0].ToolCalls[0].ToolName, msgs[0].ToolCalls[1].ToolName}
assert.ElementsMatch(t, []string{"view_file", "run_command"}, toolNames)
for _, tc := range msgs[0].ToolCalls {
switch tc.ToolName {
case "view_file":
assert.Equal(t, "Read", tc.Category)
case "run_command":
assert.Equal(t, "Bash", tc.Category)
}
}
}
func TestAntigravityCLITrajectoryParse(t *testing.T) {
root := t.TempDir()
id := "22222222-3333-4444-5555-666666666666"
mustMkdir(t, filepath.Join(root, "conversations"))
mustMkdir(t, filepath.Join(root, "implicit"))
// Create stub .pb file
pbPath := filepath.Join(root, "conversations", id+".pb")
mustWrite(t, pbPath, []byte("pb-stub"))
// Create trajectory JSON sidecar
trajectoryJSON := `{
"trajectoryId": "traj-id",
"cascadeId": "` + id + `",
"steps": [
{
"type": "CORTEX_STEP_TYPE_USER_INPUT",
"status": "STATUS_COMPLETED",
"metadata": {
"createdAt": "2026-05-20T22:40:00Z"
},
"userInput": {
"userResponse": "check files please"
}
},
{
"type": "CORTEX_STEP_TYPE_PLANNER_RESPONSE",
"status": "STATUS_COMPLETED",
"metadata": {
"createdAt": "2026-05-20T22:41:00Z"
},
"plannerResponse": {
"thinking": "I should run a command",
"response": "running command now",
"toolCalls": [
{
"name": "run_command",
"argumentsJson": "{\"command\":\"ls -la\"}",
"id": "tc-1"
}
]
}
},
{
"type": "CORTEX_STEP_TYPE_RUN_COMMAND",
"status": "STATUS_COMPLETED",
"metadata": {
"createdAt": "2026-05-20T22:42:00Z",
"executionId": "tc-1"
},
"runCommand": {
"commandLine": "ls -la",
"cwd": "/tmp",
"combinedOutput": "\"file1.txt\nfile2.txt\""
}
},
{
"type": "CORTEX_STEP_TYPE_SYSTEM_MESSAGE",
"status": "STATUS_COMPLETED",
"metadata": {
"createdAt": "2026-05-20T22:43:00Z"
},
"systemMessage": {
"message": "system warning: low memory"
}
},
{
"type": "CORTEX_STEP_TYPE_CHECKPOINT",
"status": "STATUS_COMPLETED",
"metadata": {
"createdAt": "2026-05-20T22:44:00Z"
},
"checkpoint": {
"userRequests": ["request1"],
"sessionSummary": "everything is fine"
}
}
]
}`
sidecarPath := filepath.Join(root, "conversations", id+".trajectory.json")
mustWrite(t, sidecarPath, []byte(trajectoryJSON))
sess, msgs, err := parseAntigravityCLITestSession(t, pbPath, "", "test-machine")
require.NoError(t, err)
assert.Equal(t, "antigravity-cli:"+id, sess.ID)
assert.Equal(t, "check files please", sess.FirstMessage)
// Expected messages:
// 1. User: check files please
// 2. Assistant: running command now (with tool call)
// 3. User: synthetic message with tool results
// 4. User (IsSystem): Low memory warning
// 5. User (IsSystem): Checkpoint info
require.Len(t, msgs, 5)
assert.Equal(t, RoleUser, msgs[0].Role)
assert.Equal(t, "check files please", msgs[0].Content)
assert.Equal(t, RoleAssistant, msgs[1].Role)
assert.Equal(t, "running command now", msgs[1].Content)
assert.True(t, msgs[1].HasThinking)
assert.Equal(t, "I should run a command", msgs[1].ThinkingText)
require.Len(t, msgs[1].ToolCalls, 1)
assert.Equal(t, "tc-1", msgs[1].ToolCalls[0].ToolUseID)
assert.Equal(t, "run_command", msgs[1].ToolCalls[0].ToolName)
assert.Equal(t, "Bash", msgs[1].ToolCalls[0].Category)
assert.Equal(t, RoleUser, msgs[2].Role)
assert.Equal(t, "", msgs[2].Content)
require.Len(t, msgs[2].ToolResults, 1)
assert.Equal(t, "tc-1", msgs[2].ToolResults[0].ToolUseID)
assert.Contains(t, msgs[2].ToolResults[0].ContentRaw, "file1.txt")
assert.Equal(t, RoleUser, msgs[3].Role)
assert.True(t, msgs[3].IsSystem)
assert.Equal(t, "system warning: low memory", msgs[3].Content)
assert.Equal(t, RoleUser, msgs[4].Role)
assert.True(t, msgs[4].IsSystem)
assert.Contains(t, msgs[4].Content, "everything is fine")
// Verify FileInfo size and mtime are effective (sum of sizes, max of mtimes)
pbStat, _ := os.Stat(pbPath)
sidecarStat, _ := os.Stat(sidecarPath)
expectedSize := pbStat.Size() + sidecarStat.Size()
assert.Equal(t, expectedSize, sess.File.Size)
}
func TestAntigravityCLITrajectoryWithoutSupportedMessagesFallsBack(t *testing.T) {
tcs := []struct {
name string
sidecar string
}{
{
name: "empty object",
sidecar: `{}`,
},
{
name: "unknown step only",
sidecar: `{
"steps": [
{
"type": "CORTEX_STEP_TYPE_FUTURE_ONLY",
"metadata": {
"createdAt": "2026-05-20T22:40:00Z"
},
"futurePayload": {
"text": "not supported yet"
}
}
]
}`,
},
{
name: "tool result only",
sidecar: `{
"steps": [
{
"type": "CORTEX_STEP_TYPE_RUN_COMMAND",
"metadata": {
"createdAt": "2026-05-20T22:40:00Z",
"executionId": "tc-1"
},
"runCommand": {
"commandLine": "ls",
"combinedOutput": "\"file1.txt\""
}
}
]
}`,
},
}
for _, tc := range tcs {
t.Run(tc.name, func(t *testing.T) {
root := t.TempDir()
id := "33333333-4444-5555-6666-777777777777"
mustMkdir(t, filepath.Join(root, "conversations"))
pbPath := filepath.Join(root, "conversations", id+".pb")
mustWrite(t, pbPath, []byte("pb-stub"))
mustWrite(t, filepath.Join(root, "conversations", id+".trajectory.json"), []byte(tc.sidecar))
mustWrite(t, filepath.Join(root, "history.jsonl"),
[]byte(`{"display":"history fallback","timestamp":1779000000000,`+
`"workspace":"/tmp/proj","conversationId":"`+id+`"}`))
sess, msgs, err := parseAntigravityCLITestSession(t, pbPath, "", "test-machine")
require.NoError(t, err)
require.Len(t, msgs, 1)
assert.Equal(t, RoleUser, msgs[0].Role)
assert.Equal(t, "history fallback", msgs[0].Content)
assert.Equal(t, 1, sess.MessageCount)
assert.Equal(t, "history fallback", sess.FirstMessage)
})
}
}
// writeAntigravityTestSidecar writes a trajectory sidecar with the first
// numSteps of a fixed user-input / planner-response / run-command sequence.
func writeAntigravityTestSidecar(
t *testing.T, root, id string, numSteps int,
) string {
t.Helper()
return writeAntigravityTestSidecarWithGenMetadata(
t, root, id, numSteps, "",
)
}
// writeAntigravityTestSidecarWithGenMetadata writes the same fixed
// step sequence as writeAntigravityTestSidecar plus an optional
// generatorMetadata JSON array (steps: 0=USER_INPUT,
// 1=PLANNER_RESPONSE, 2=RUN_COMMAND; realistic generations use
// stepIndices [1,2]).
func writeAntigravityTestSidecarWithGenMetadata(
t *testing.T, root, id string, numSteps int, genMetadataJSON string,
) string {
t.Helper()
allSteps := []string{
`{
"type": "CORTEX_STEP_TYPE_USER_INPUT",
"status": "STATUS_COMPLETED",
"metadata": {"createdAt": "2026-06-10T20:40:00Z"},
"userInput": {"userResponse": "sidecar prompt"}
}`,
`{
"type": "CORTEX_STEP_TYPE_PLANNER_RESPONSE",
"status": "STATUS_COMPLETED",
"metadata": {"createdAt": "2026-06-10T20:41:00Z"},
"plannerResponse": {
"thinking": "sidecar thinking",
"response": "sidecar assistant reply",
"toolCalls": [{
"name": "run_command",
"argumentsJson": "{\"command\":\"ls\"}",
"id": "tc-1"
}]
}
}`,
`{
"type": "CORTEX_STEP_TYPE_RUN_COMMAND",
"status": "STATUS_COMPLETED",
"metadata": {"createdAt": "2026-06-10T20:42:00Z", "executionId": "tc-1"},
"runCommand": {
"commandLine": "ls",
"cwd": "/tmp",
"combinedOutput": "\"out.txt\""
}
}`,
}
require.LessOrEqual(t, numSteps, len(allSteps))
body := `{"trajectoryId":"traj","cascadeId":"` + id + `","steps":[` +
strings.Join(allSteps[:numSteps], ",") + `]`
if genMetadataJSON != "" {
body += `,"generatorMetadata":` + genMetadataJSON
}
body += `}`
p := filepath.Join(root, "conversations", id+".trajectory.json")
mustWrite(t, p, []byte(body))
return p
}
func TestAntigravityCLIDBPrefersSidecarWithEqualCoverage(t *testing.T) {
root := t.TempDir()
id := "66666666-7777-8888-9999-aaaaaaaaaaaa"
mustMkdir(t, filepath.Join(root, "conversations"))
dbPath := filepath.Join(root, "conversations", id+".db")
createAntigravityTestDB(t, dbPath) // 2 raw steps
writeAntigravityTestSidecar(t, root, id, 2)
mustWrite(t, filepath.Join(root, "history.jsonl"),
[]byte(`{"display":"history prompt","timestamp":1779000000000,`+
`"workspace":"/tmp/db-proj","conversationId":"`+id+`"}`))
sess, msgs, _, status, err := parseAntigravityCLITestSessionWithStatus(t,
dbPath, "", "test-machine",
)
require.NoError(t, err)
assert.False(t, status.NeedsRetry)
// Sidecar messages win: structured tool call, thinking, and the
// sidecar's own user prompt (no history.jsonl merge).
require.Len(t, msgs, 2)
assert.Equal(t, RoleUser, msgs[0].Role)
assert.Equal(t, "sidecar prompt", msgs[0].Content)
assert.Equal(t, RoleAssistant, msgs[1].Role)
assert.Equal(t, "sidecar assistant reply", msgs[1].Content)
assert.True(t, msgs[1].HasThinking)
require.Len(t, msgs[1].ToolCalls, 1)
assert.Equal(t, "tc-1", msgs[1].ToolCalls[0].ToolUseID)
assert.Equal(t, "sidecar prompt", sess.FirstMessage)
}
func TestAntigravityCLIDBKeepsDBDecodeWhenSidecarLags(t *testing.T) {
root := t.TempDir()
id := "77777777-8888-9999-aaaa-bbbbbbbbbbbb"
mustMkdir(t, filepath.Join(root, "conversations"))
dbPath := filepath.Join(root, "conversations", id+".db")
createAntigravityTestDB(t, dbPath) // 2 raw steps
// Sidecar covers only 1 of 2 steps -- a live session agy-reader has
// not caught up with yet. The fuller DB decode must win.
writeAntigravityTestSidecar(t, root, id, 1)
mustWrite(t, filepath.Join(root, "history.jsonl"),
[]byte(`{"display":"history prompt","timestamp":1779000000000,`+
`"workspace":"/tmp/db-proj","conversationId":"`+id+`"}`))
_, msgs, _, status, err := parseAntigravityCLITestSessionWithStatus(t,
dbPath, "", "test-machine",
)
require.NoError(t, err)
assert.False(t, status.NeedsRetry)
require.Len(t, msgs, 2)
assert.Equal(t, "history prompt", msgs[0].Content, "history merge applies")
assert.Contains(t, msgs[1].Content, "assistant reply content body")
}
func TestAntigravityCLIDBSidecarUsedWhenDBDecodeEmpty(t *testing.T) {
root := t.TempDir()
id := "88888888-9999-aaaa-bbbb-cccccccccccc"
mustMkdir(t, filepath.Join(root, "conversations"))
dbPath := filepath.Join(root, "conversations", id+".db")
db, err := sql.Open("sqlite3", dbPath)
require.NoError(t, err)
createAntigravityStepTables(t, db)
require.NoError(t, db.Close())
writeAntigravityTestSidecar(t, root, id, 2)
_, msgs, _, status, err := parseAntigravityCLITestSessionWithStatus(t,
dbPath, "", "test-machine",
)
require.NoError(t, err)
assert.False(t, status.NeedsRetry,
"sidecar provided full-resolution data; no retry needed")
require.Len(t, msgs, 2)
assert.Equal(t, "sidecar prompt", msgs[0].Content)
require.Len(t, msgs[1].ToolCalls, 1)
}
func TestAntigravityCLIDBFileInfoIncludesTrajectorySidecar(t *testing.T) {
root := t.TempDir()
id := "99999999-aaaa-bbbb-cccc-dddddddddddd"
mustMkdir(t, filepath.Join(root, "conversations"))
dbPath := filepath.Join(root, "conversations", id+".db")
mustWrite(t, dbPath, []byte("db"))
sidecarPath := filepath.Join(
root, "conversations", id+".trajectory.json",
)
mustWrite(t, sidecarPath, []byte("sidecar"))
early := time.Unix(1779000000, 0)
late := time.Unix(1779000300, 0)
require.NoError(t, os.Chtimes(dbPath, early, early))
require.NoError(t, os.Chtimes(sidecarPath, late, late))
info, err := AntigravityCLIFileInfo(dbPath)
require.NoError(t, err)
assert.Equal(t, int64(len("db")+len("sidecar")), info.Size())
assert.Equal(t, late.UnixNano(), info.ModTime().UnixNano(),
"agy-reader sidecar update must change the .db fingerprint")
}
func TestAntigravityCLIPBUsesSidecarDespiteOlderMtime(t *testing.T) {
root := t.TempDir()
id := "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"
mustMkdir(t, filepath.Join(root, "conversations"))
pbPath := filepath.Join(root, "conversations", id+".pb")
mustWrite(t, pbPath, []byte("pb-stub"))
sidecarPath := writeAntigravityTestSidecar(t, root, id, 2)
// Sidecar predates the .pb -- e.g. the encrypted file was touched
// after the final agy-reader sync. The old mtime gate rejected the
// sidecar here and fell back to low-fidelity history rows; the
// sidecar must win regardless because .pb has no richer decode.
early := time.Unix(1779000000, 0)
late := time.Unix(1779000300, 0)
require.NoError(t, os.Chtimes(sidecarPath, early, early))
require.NoError(t, os.Chtimes(pbPath, late, late))
mustWrite(t, filepath.Join(root, "history.jsonl"),
[]byte(`{"display":"history prompt","timestamp":1779000000000,`+
`"workspace":"/tmp/pb-proj","conversationId":"`+id+`"}`))
sess, msgs, err := parseAntigravityCLITestSession(t, pbPath, "", "test-machine")
require.NoError(t, err)
require.Len(t, msgs, 2)
assert.Equal(t, "sidecar prompt", msgs[0].Content)
assert.Equal(t, RoleAssistant, msgs[1].Role)
require.Len(t, msgs[1].ToolCalls, 1)
assert.Equal(t, "sidecar prompt", sess.FirstMessage)
}
// createAntigravityUndecodableDB writes a .db whose steps rows carry
// payloads the heuristic decoder cannot turn into displayable messages.
func createAntigravityUndecodableDB(t *testing.T, path string, rows int) {
t.Helper()
db, err := sql.Open("sqlite3", path)
require.NoError(t, err)
defer db.Close()
createAntigravityStepTables(t, db)
for i := range rows {
mustExec(t, db,
`INSERT INTO steps (idx, step_type, step_payload) `+
`VALUES (?, ?, ?)`,
i, 99, []byte{0xff, 0xff, 0xff})
}
}
func TestAntigravityCLIDBPartialSidecarNotPersistedAsCurrent(t *testing.T) {
root := t.TempDir()
id := "bbbbbbbb-cccc-dddd-eeee-ffffffffffff"
mustMkdir(t, filepath.Join(root, "conversations"))
dbPath := filepath.Join(root, "conversations", id+".db")
createAntigravityUndecodableDB(t, dbPath, 3)
// Sidecar lags the DB (2 of 3 steps): best available transcript, but
// the row must stay retryable rather than persist as current.
writeAntigravityTestSidecar(t, root, id, 2)
_, msgs, _, status, err := parseAntigravityCLITestSessionWithStatus(t,
dbPath, "", "test-machine",
)
require.NoError(t, err)
assert.True(t, status.NeedsRetry,
"partial sidecar with undecodable DB rows must leave the row stale")
require.Len(t, msgs, 2)
assert.Equal(t, "sidecar prompt", msgs[0].Content)
}
func TestAntigravityCLIDBCoveringSidecarRescuesUndecodableRows(t *testing.T) {
root := t.TempDir()
id := "cccccccc-dddd-eeee-ffff-000000000000"
mustMkdir(t, filepath.Join(root, "conversations"))
dbPath := filepath.Join(root, "conversations", id+".db")
createAntigravityUndecodableDB(t, dbPath, 3)
writeAntigravityTestSidecar(t, root, id, 3)
_, msgs, _, status, err := parseAntigravityCLITestSessionWithStatus(t,
dbPath, "", "test-machine",
)
require.NoError(t, err)
assert.False(t, status.NeedsRetry,
"covering sidecar is full-resolution data; no retry needed")
require.Len(t, msgs, 3)
require.Len(t, msgs[1].ToolCalls, 1)
}
func TestAntigravityCLISidecarWinsKeepsTokenUsage(t *testing.T) {
root := t.TempDir()
id := "dddddddd-eeee-ffff-0000-111111111111"
mustMkdir(t, filepath.Join(root, "conversations"))
dbPath := filepath.Join(root, "conversations", id+".db")
db, err := sql.Open("sqlite3", dbPath)
require.NoError(t, err)
createAntigravityStepTables(t, db)
mustExec(t, db, `CREATE TABLE gen_metadata (idx integer, data blob, size integer, PRIMARY KEY (idx))`)
tsEarly := encodePB([]pbField{{num: 1, wire: pbWireVarint, varint: 1779000000}})
userPayload := encodePB([]pbField{
{num: 5, wire: pbWireBytes, bytes: tsEarly},
{num: 17, wire: pbWireBytes, bytes: []byte("user question text goes here and is long")},
})
tsLate := encodePB([]pbField{{num: 1, wire: pbWireVarint, varint: 1779000100}})
asstPayload := encodePB([]pbField{
{num: 5, wire: pbWireBytes, bytes: tsLate},
{num: 17, wire: pbWireBytes, bytes: []byte("assistant response body goes here and is long")},
})
mustExec(t, db, `INSERT INTO steps (idx, step_type, step_payload) VALUES (0, 14, ?)`, userPayload)
mustExec(t, db, `INSERT INTO steps (idx, step_type, step_payload) VALUES (1, 17, ?)`, asstPayload)
genData := createAntigravityMockGenMetadata(t, 2400, 180, 0, "Test Gemini 3.5")
mustExec(t, db, `INSERT INTO gen_metadata (idx, data, size) VALUES (1, ?, ?)`, genData, len(genData))
require.NoError(t, db.Close())
// Sidecar covers both raw steps, so its transcript wins over the
// DB decode. This sidecar has no generatorMetadata, so the selected
// messages carry no token fields and usage flows only from the DB
// gen_metadata events.
writeAntigravityTestSidecar(t, root, id, 2)
sess, msgs, usageEvents, status, err := parseAntigravityCLITestSessionWithStatus(t,
dbPath, "", "test-machine",
)
require.NoError(t, err)
assert.False(t, status.NeedsRetry)
require.NotEmpty(t, msgs)
assert.Equal(t, "sidecar prompt", msgs[0].Content, "sidecar transcript should win")
require.Len(t, usageEvents, 1)
assert.Equal(t, 2400, usageEvents[0].InputTokens)
assert.Equal(t, 180, usageEvents[0].OutputTokens)
// Session totals must come from gen_metadata usage even though
// the sidecar transcript has no per-message token metadata.
assert.Equal(t, 180, sess.TotalOutputTokens)
assert.Equal(t, 2400, sess.PeakContextTokens)
assert.True(t, sess.HasTotalOutputTokens)
assert.True(t, sess.HasPeakContextTokens)
}
func TestAntigravityCLISidecarRescueKeepsGenMetadataUsage(t *testing.T) {
root := t.TempDir()
id := "eeeeeeee-ffff-0000-1111-222222222222"
mustMkdir(t, filepath.Join(root, "conversations"))
dbPath := filepath.Join(root, "conversations", id+".db")
db, err := sql.Open("sqlite3", dbPath)
require.NoError(t, err)
createAntigravityStepTables(t, db)
mustExec(t, db, `CREATE TABLE gen_metadata (idx integer, data blob, size integer, PRIMARY KEY (idx))`)
// Two raw steps the heuristic cannot decode.
for i := range 2 {
mustExec(t, db,
`INSERT INTO steps (idx, step_type, step_payload) VALUES (?, ?, ?)`,
i, 99, []byte{0xff, 0xff, 0xff})
}
genData := createAntigravityMockGenMetadata(t, 2400, 180, 0, "Test Gemini 3.5")
mustExec(t, db, `INSERT INTO gen_metadata (idx, data, size) VALUES (1, ?, ?)`, genData, len(genData))
require.NoError(t, db.Close())
// Covering sidecar rescues the undecodable rows.
writeAntigravityTestSidecar(t, root, id, 2)
sess, msgs, usageEvents, status, err := parseAntigravityCLITestSessionWithStatus(t,
dbPath, "", "test-machine",
)
require.NoError(t, err)
assert.False(t, status.NeedsRetry, "covering sidecar is full-resolution data")
require.NotEmpty(t, msgs)
assert.Equal(t, "sidecar prompt", msgs[0].Content, "sidecar transcript should win")
// gen_metadata usage must survive even though no DB step decoded.
require.Len(t, usageEvents, 1)
assert.Equal(t, "Test Gemini 3.5", usageEvents[0].Model)
assert.Equal(t, 2400, usageEvents[0].InputTokens)
assert.Equal(t, 180, usageEvents[0].OutputTokens)
assert.Equal(t, 0, usageEvents[0].ReasoningTokens, "reasoning not available in gen_metadata")
assert.Equal(t, 180, sess.TotalOutputTokens)
assert.Equal(t, 2400, sess.PeakContextTokens)
}
func TestAntigravityCLIPBSidecarEmitsUsageEvents(t *testing.T) {
root := t.TempDir()
id := "ffffffff-0000-1111-2222-333333333333"
mustMkdir(t, filepath.Join(root, "conversations"))
pbPath := filepath.Join(root, "conversations", id+".pb")
mustWrite(t, pbPath, []byte("pb-junk-bytes"))
// Gen A claims the planner step (1) and its tool step (2). Gen B
// only claims a non-planner step. Gen C is the failed/retried
// usage:{} case and must be skipped.
genJSON := `[
{
"stepIndices": [1, 2],
"chatModel": {
"model": "MODEL_PLACEHOLDER_M20",
"chatStartMetadata": {"createdAt": "2026-06-10T20:40:30Z"},
"usage": {
"inputTokens": "19733",
"outputTokens": "253",
"thinkingOutputTokens": "208"
}
}
},
{
"stepIndices": [2],
"chatModel": {
"model": "MODEL_PLACEHOLDER_M20",
"usage": {
"inputTokens": "1820",
"outputTokens": "97",
"cacheReadTokens": "18661"
}
}
},
{
"stepIndices": [2],
"chatModel": {
"model": "MODEL_PLACEHOLDER_M20",
"usage": {}
}
}
]`
writeAntigravityTestSidecarWithGenMetadata(t, root, id, 3, genJSON)
sess, msgs, usageEvents, status, err := parseAntigravityCLITestSessionWithStatus(t,
pbPath, "", "test-machine",
)
require.NoError(t, err)
assert.False(t, status.NeedsRetry)
require.Len(t, usageEvents, 2, "usage:{} generation must be skipped")
evA := usageEvents[0]
assert.Equal(t, "sidecar", evA.Source)
assert.Equal(t, "MODEL_PLACEHOLDER_M20", evA.Model,
"placeholder model names are kept verbatim")
assert.Equal(t, 19733, evA.InputTokens)
assert.Equal(t, 253, evA.OutputTokens,
"outputTokens already includes thinking; no re-fold")
assert.Equal(t, 208, evA.ReasoningTokens)
assert.Equal(t, 0, evA.CacheReadInputTokens)
assert.Equal(t, "2026-06-10T20:40:30Z", evA.OccurredAt,
"chatStartMetadata.createdAt formatted RFC3339Nano")
assert.Empty(t, evA.DedupKey)
assert.Nil(t, evA.MessageOrdinal)
assert.Equal(t, sess.ID, evA.SessionID)
evB := usageEvents[1]
assert.Equal(t, "sidecar", evB.Source)
assert.Equal(t, 1820, evB.InputTokens)
assert.Equal(t, 97, evB.OutputTokens)
assert.Equal(t, 0, evB.ReasoningTokens)
assert.Equal(t, 18661, evB.CacheReadInputTokens)
assert.Empty(t, evB.OccurredAt,
"no chatStartMetadata and no mapped planner step")
assert.Equal(t, sess.ID, evB.SessionID)
// Gen A attributes its tokens to the planner message.
require.Len(t, msgs, 3)
planner := msgs[1]
assert.Equal(t, RoleAssistant, planner.Role)
assert.Equal(t, "MODEL_PLACEHOLDER_M20", planner.Model)
assert.Equal(t, 19733, planner.ContextTokens,
"context = input + cacheRead of the first attributing gen")
assert.Equal(t, 253, planner.OutputTokens)
assert.True(t, planner.HasContextTokens)
assert.True(t, planner.HasOutputTokens)
assert.Empty(t, planner.TokenUsage,
"raw token_usage must stay empty or analytics double-count")
// Session totals come from the events: output sums, peak context
// is max(input + cacheRead).
assert.Equal(t, 253+97, sess.TotalOutputTokens)
assert.Equal(t, 1820+18661, sess.PeakContextTokens)
assert.True(t, sess.HasTotalOutputTokens)
assert.True(t, sess.HasPeakContextTokens)
}
func TestAntigravityCLIDBGenMetadataWinsOverSidecarUsage(t *testing.T) {
root := t.TempDir()
id := "abababab-cdcd-efef-0101-232323232323"
mustMkdir(t, filepath.Join(root, "conversations"))
dbPath := filepath.Join(root, "conversations", id+".db")
db, err := sql.Open("sqlite3", dbPath)
require.NoError(t, err)
createAntigravityStepTables(t, db)
mustExec(t, db, `CREATE TABLE gen_metadata (idx integer, data blob, size integer, PRIMARY KEY (idx))`)
tsEarly := encodePB([]pbField{{num: 1, wire: pbWireVarint, varint: 1779000000}})
userPayload := encodePB([]pbField{
{num: 5, wire: pbWireBytes, bytes: tsEarly},
{num: 17, wire: pbWireBytes, bytes: []byte("user question text goes here and is long")},
})
tsLate := encodePB([]pbField{{num: 1, wire: pbWireVarint, varint: 1779000100}})
asstPayload := encodePB([]pbField{
{num: 5, wire: pbWireBytes, bytes: tsLate},
{num: 17, wire: pbWireBytes, bytes: []byte("assistant response body goes here and is long")},
})
mustExec(t, db, `INSERT INTO steps (idx, step_type, step_payload) VALUES (0, 14, ?)`, userPayload)
mustExec(t, db, `INSERT INTO steps (idx, step_type, step_payload) VALUES (1, 17, ?)`, asstPayload)
genData := createAntigravityMockGenMetadata(t, 2400, 180, 0, "Test Gemini 3.5")
mustExec(t, db, `INSERT INTO gen_metadata (idx, data, size) VALUES (1, ?, ?)`, genData, len(genData))
require.NoError(t, db.Close())
// Covering sidecar whose generatorMetadata carries DIFFERENT
// numbers than the DB gen_metadata: the DB events must win so the
// generation is not double counted, while the sidecar transcript
// still carries per-message attribution from the sidecar numbers.
genJSON := `[{
"stepIndices": [1],
"chatModel": {
"model": "MODEL_PLACEHOLDER_M132",
"usage": {
"inputTokens": "5000",
"outputTokens": "111",
"thinkingOutputTokens": "60"
}
}
}]`
writeAntigravityTestSidecarWithGenMetadata(t, root, id, 2, genJSON)
sess, msgs, usageEvents, status, err := parseAntigravityCLITestSessionWithStatus(t,
dbPath, "", "test-machine",
)
require.NoError(t, err)
assert.False(t, status.NeedsRetry)
require.NotEmpty(t, msgs)
assert.Equal(t, "sidecar prompt", msgs[0].Content, "sidecar transcript should win")
// Exactly one event: gen_metadata wins, sidecar events fill gaps only.
require.Len(t, usageEvents, 1)
assert.Equal(t, "generation", usageEvents[0].Source)
assert.Equal(t, 2400, usageEvents[0].InputTokens)
assert.Equal(t, 180, usageEvents[0].OutputTokens)
// Session totals follow the winning gen_metadata events.
assert.Equal(t, 180, sess.TotalOutputTokens)
assert.Equal(t, 2400, sess.PeakContextTokens)
// Per-message attribution comes from the sidecar generatorMetadata.
require.Len(t, msgs, 2)
planner := msgs[1]
assert.Equal(t, RoleAssistant, planner.Role)
assert.Equal(t, "MODEL_PLACEHOLDER_M132", planner.Model)
assert.Equal(t, 5000, planner.ContextTokens)
assert.Equal(t, 111, planner.OutputTokens)
assert.True(t, planner.HasContextTokens)
assert.True(t, planner.HasOutputTokens)
}
func TestAntigravityCLIDBWithoutGenMetadataGetsSidecarUsage(t *testing.T) {
root := t.TempDir()
id := "acacacac-bdbd-cece-dfdf-454545454545"
mustMkdir(t, filepath.Join(root, "conversations"))
dbPath := filepath.Join(root, "conversations", id+".db")
createAntigravityTestDB(t, dbPath) // 2 raw steps, no gen_metadata table
genJSON := `[{
"stepIndices": [1],
"chatModel": {
"model": "MODEL_PLACEHOLDER_M20",
"usage": {
"inputTokens": "1500",
"outputTokens": "77",
"thinkingOutputTokens": "20",
"cacheReadTokens": "300"
}
}
}]`
writeAntigravityTestSidecarWithGenMetadata(t, root, id, 2, genJSON)
sess, msgs, usageEvents, status, err := parseAntigravityCLITestSessionWithStatus(t,
dbPath, "", "test-machine",
)
require.NoError(t, err)
assert.False(t, status.NeedsRetry)
require.NotEmpty(t, msgs)
assert.Equal(t, "sidecar prompt", msgs[0].Content)
// No gen_metadata table: sidecar generatorMetadata fills the gap.
require.Len(t, usageEvents, 1)
assert.Equal(t, "sidecar", usageEvents[0].Source)
assert.Equal(t, "MODEL_PLACEHOLDER_M20", usageEvents[0].Model)
assert.Equal(t, 1500, usageEvents[0].InputTokens)
assert.Equal(t, 77, usageEvents[0].OutputTokens)
assert.Equal(t, 20, usageEvents[0].ReasoningTokens)
assert.Equal(t, 300, usageEvents[0].CacheReadInputTokens)
assert.Equal(t, sess.ID, usageEvents[0].SessionID)
assert.Equal(t, 77, sess.TotalOutputTokens)
assert.Equal(t, 1800, sess.PeakContextTokens)
assert.True(t, sess.HasTotalOutputTokens)
assert.True(t, sess.HasPeakContextTokens)
}
func TestAntigravityCLINonCoveringSidecarUsageRejected(t *testing.T) {
root := t.TempDir()
id := "acacacac-bdbd-cece-dfdf-565656565656"
mustMkdir(t, filepath.Join(root, "conversations"))
dbPath := filepath.Join(root, "conversations", id+".db")
createAntigravityTestDB(t, dbPath) // 2 raw steps, no gen_metadata table
// Sidecar lags the DB: 1 step vs 2 raw rows. Its generatorMetadata
// would decode to a usage event, but a lagging sidecar has only seen
// part of the session, so persisting it would underreport totals on
// a row that looks current.
genJSON := `[{
"stepIndices": [0],
"chatModel": {
"model": "MODEL_PLACEHOLDER_M20",
"usage": {
"inputTokens": "1500",
"outputTokens": "77"
}
}
}]`
writeAntigravityTestSidecarWithGenMetadata(t, root, id, 1, genJSON)
sess, msgs, usageEvents, status, err := parseAntigravityCLITestSessionWithStatus(t,
dbPath, "", "test-machine",
)
require.NoError(t, err)
assert.False(t, status.NeedsRetry)
require.NotEmpty(t, msgs)
assert.Equal(t, "user prompt text goes here", msgs[0].Content,
"non-covering sidecar must lose the transcript to the DB decode")
assert.Empty(t, usageEvents,
"non-covering sidecar usage must be rejected like its transcript")
assert.False(t, sess.HasTotalOutputTokens)
assert.False(t, sess.HasPeakContextTokens)
}
func TestAgyTokenCountUnmarshal(t *testing.T) {
tcs := []struct {
name string
json string
want int
}{
{"quoted number", `"21186"`, 21186},
{"bare number", `21186`, 21186},
{"empty string", `""`, 0},
{"null", `null`, 0},
{"garbage string", `"abc"`, 0},
{"object garbage", `{"bogus":true}`, 0},
{"negative quoted", `"-5"`, 0},
{"negative bare", `-5`, 0},
}
for _, tc := range tcs {
t.Run(tc.name, func(t *testing.T) {
var c agyTokenCount
require.NoError(t, json.Unmarshal([]byte(tc.json), &c),
"garbage must decode to 0, never error")
assert.Equal(t, tc.want, int(c))
})
}
t.Run("surrounding object survives garbage", func(t *testing.T) {
var u agyChatModelUsage
require.NoError(t, json.Unmarshal([]byte(
`{"inputTokens":{"bogus":true},"outputTokens":"5"}`,
), &u))
assert.Equal(t, 0, int(u.InputTokens))
assert.Equal(t, 5, int(u.OutputTokens))
})
}
func TestAntigravityCLISidecarUsageStepIndexEdgeCases(t *testing.T) {
const usageJSON = `"usage":{"inputTokens":"100","outputTokens":"10"}`
tcs := []struct {
name string
gens string
wantEvents int
wantAttrib bool
wantContext int
wantOutput int
}{
{
name: "non-planner step indices emit event without attribution",
gens: `[{"stepIndices":[2],"chatModel":{` +
`"model":"MODEL_PLACEHOLDER_M20",` + usageJSON + `}}]`,
wantEvents: 1,
},
{
name: "empty step indices",
gens: `[{"stepIndices":[],"chatModel":{` +
`"model":"MODEL_PLACEHOLDER_M20",` + usageJSON + `}}]`,
wantEvents: 1,
},
{
name: "out of range step index does not panic",
gens: `[{"stepIndices":[99],"chatModel":{` +
`"model":"MODEL_PLACEHOLDER_M20",` + usageJSON + `}}]`,
wantEvents: 1,
},
{
name: "two gens claiming one planner: first attributes",
gens: `[{"stepIndices":[1],"chatModel":{` +
`"model":"MODEL_PLACEHOLDER_M20",` + usageJSON + `}},` +
`{"stepIndices":[1],"chatModel":{` +
`"model":"MODEL_PLACEHOLDER_M132",` +
`"usage":{"inputTokens":"999","outputTokens":"99"}}}]`,
wantEvents: 2,
wantAttrib: true,
wantContext: 100,
wantOutput: 10,
},
{
name: "missing chatModel skipped",
gens: `[{"stepIndices":[1]}]`,
wantEvents: 0,
},
}
for _, tc := range tcs {
t.Run(tc.name, func(t *testing.T) {
root := t.TempDir()
id := "edededed-0101-2323-4545-676767676767"
mustMkdir(t, filepath.Join(root, "conversations"))
pbPath := filepath.Join(root, "conversations", id+".pb")
mustWrite(t, pbPath, []byte("pb-junk-bytes"))
writeAntigravityTestSidecarWithGenMetadata(
t, root, id, 3, tc.gens,
)
_, msgs, usageEvents, _, err := parseAntigravityCLITestSessionWithStatus(t,
pbPath, "", "test-machine",
)
require.NoError(t, err)
assert.Len(t, usageEvents, tc.wantEvents)
var planner *ParsedMessage
for i := range msgs {
if msgs[i].Role == RoleAssistant {
planner = &msgs[i]
break
}
}
require.NotNil(t, planner)
if tc.wantAttrib {
assert.True(t, planner.HasContextTokens)
assert.True(t, planner.HasOutputTokens)
assert.Equal(t, tc.wantContext, planner.ContextTokens)
assert.Equal(t, tc.wantOutput, planner.OutputTokens)
assert.Equal(t, "MODEL_PLACEHOLDER_M20", planner.Model,
"second gen claiming the planner stays event-only")
} else {
assert.False(t, planner.HasContextTokens)
assert.False(t, planner.HasOutputTokens)
assert.Zero(t, planner.ContextTokens)
assert.Zero(t, planner.OutputTokens)
}
})
}
}
// TestAntigravitySessionFileMetadataIncludesWAL verifies the persisted
// file fingerprint covers WAL sidecars: WAL-only commits do not touch
// the main .db, so size/mtime must combine the sidecar set or the sync
// skip check will never reparse a live session.
func TestAntigravitySessionFileMetadataIncludesWAL(t *testing.T) {
root := t.TempDir()
id := "12121212-3434-5656-7878-909090909090"
mustMkdir(t, filepath.Join(root, "conversations"))
dbPath := filepath.Join(root, "conversations", id+".db")
createAntigravityTestDB(t, dbPath)
mainInfo, err := os.Stat(dbPath)
require.NoError(t, err)
walPath := dbPath + "-wal"
mustWrite(t, walPath, []byte("wal bytes"))
walTime := mainInfo.ModTime().Add(5 * time.Second)
require.NoError(t, os.Chtimes(walPath, walTime, walTime))
sess, _, _, err := parseAntigravityTestSession(t, dbPath, "p", "m")
require.NoError(t, err)
// The parse's own read-only open can create or touch -shm/-wal
// siblings, so the expected composite comes from post-parse disk
// state - the same state the next sync's skip check will stat.
var wantSize int64
for _, p := range []string{dbPath, walPath, dbPath + "-shm"} {
fi, statErr := os.Stat(p)
if statErr != nil {
continue
}
wantSize += fi.Size()
}
require.Greater(t, wantSize, mainInfo.Size(),
"setup: WAL sidecar must contribute to the composite")
assert.Equal(t, wantSize, sess.File.Size,
"file size must include WAL/SHM sidecars")
assert.Equal(t, walTime.UnixNano(), sess.File.Mtime,
"file mtime must reflect the newest sidecar")
}
// TestAntigravityFileInfoIncludesBrainArtifacts pins brain artifacts
// into the IDE composite fingerprint: the provider parse renders
// brain/<id>/*.md (+ .metadata.json) as messages, so a brain-only
// add/edit/delete must change the effective file info or skip checks
// keep stale brain messages.
func TestAntigravityFileInfoIncludesBrainArtifacts(t *testing.T) {
root := t.TempDir()
id := "13131313-2424-3535-4646-575757575757"
mustMkdir(t, filepath.Join(root, "conversations"))
dbPath := filepath.Join(root, "conversations", id+".db")
mustWrite(t, dbPath, []byte("db"))
brainDir := filepath.Join(root, "brain", id)
mustMkdir(t, brainDir)
mdPath := filepath.Join(brainDir, "task.md")
metaPath := filepath.Join(brainDir, "task.md.metadata.json")
strayPath := filepath.Join(brainDir, "scratch.txt")
mustWrite(t, mdPath, []byte("plan body"))
mustWrite(t, metaPath, []byte(`{"summary":"s"}`))
// Files the parser never reads must not affect the fingerprint.
mustWrite(t, strayPath, []byte("xxxxxxxx"))
early := time.Unix(1779000000, 0)
late := time.Unix(1779000300, 0)
require.NoError(t, os.Chtimes(dbPath, early, early))
require.NoError(t, os.Chtimes(mdPath, early, early))
require.NoError(t, os.Chtimes(metaPath, late, late))
require.NoError(t, os.Chtimes(strayPath, early, early))
info, err := AntigravityFileInfo(dbPath)
require.NoError(t, err)
wantSize := int64(
len("db") + len("plan body") + len(`{"summary":"s"}`),
)
assert.Equal(t, wantSize, info.Size(),
"brain artifacts must contribute to the composite size")
assert.Equal(t, late.UnixNano(), info.ModTime().UnixNano(),
"newest brain file must drive the composite mtime")
}
func TestAntigravityTokenUsage(t *testing.T) {
root := t.TempDir()
id := "55555555-6666-7777-8888-999999999999"
mustMkdir(t, filepath.Join(root, "conversations"))
dbPath := filepath.Join(root, "conversations", id+".db")
db, err := sql.Open("sqlite3", dbPath)
require.NoError(t, err)
defer db.Close()
createAntigravityStepTables(t, db)
// Create gen_metadata table
mustExec(t, db, `CREATE TABLE gen_metadata (idx integer, data blob, size integer, PRIMARY KEY (idx))`)
// Create user prompt step (idx=0) and assistant reply step (idx=1)
tsEarly := encodePB([]pbField{{num: 1, wire: pbWireVarint, varint: 1779000000}})
userPayload := encodePB([]pbField{
{num: 5, wire: pbWireBytes, bytes: tsEarly},
{num: 17, wire: pbWireBytes, bytes: []byte("user question text goes here and is long")},
})
tsLate := encodePB([]pbField{{num: 1, wire: pbWireVarint, varint: 1779000100}})
asstPayload := encodePB([]pbField{
{num: 5, wire: pbWireBytes, bytes: tsLate},
{num: 17, wire: pbWireBytes, bytes: []byte("assistant response body goes here and is long")},
})
mustExec(t, db, `INSERT INTO steps (idx, step_type, step_payload) VALUES (0, 14, ?)`, userPayload)
mustExec(t, db, `INSERT INTO steps (idx, step_type, step_payload) VALUES (1, 17, ?)`, asstPayload)
// Create gen_metadata entry for the assistant step (idx=1)
genData := createAntigravityMockGenMetadata(t, 2400, 180, 0, "Test Gemini 3.5")
mustExec(t, db, `INSERT INTO gen_metadata (idx, data, size) VALUES (1, ?, ?)`, genData, len(genData))
sess, msgs, usageEvents, err := parseAntigravityTestSession(t, dbPath, "test-project", "test-machine")
require.NoError(t, err)
// 1. Verify model and message token counts
require.Len(t, msgs, 2)
assert.Equal(t, RoleUser, msgs[0].Role)
assert.Equal(t, RoleAssistant, msgs[1].Role)
assert.Equal(t, "Test Gemini 3.5", msgs[1].Model)
assert.Equal(t, 2400, msgs[1].ContextTokens)
assert.Equal(t, 180, msgs[1].OutputTokens)
assert.True(t, msgs[1].HasContextTokens)
assert.True(t, msgs[1].HasOutputTokens)
// 2. Verify usage events
require.Len(t, usageEvents, 1)
assert.Equal(t, "generation", usageEvents[0].Source)
assert.Equal(t, "Test Gemini 3.5", usageEvents[0].Model)
assert.Equal(t, 2400, usageEvents[0].InputTokens)
assert.Equal(t, 180, usageEvents[0].OutputTokens)
assert.Equal(t, 0, usageEvents[0].ReasoningTokens, "reasoning not available in gen_metadata")
// 3. Verify session rollup
assert.Equal(t, 180, sess.TotalOutputTokens)
assert.Equal(t, 2400, sess.PeakContextTokens)
assert.True(t, sess.HasTotalOutputTokens)
assert.True(t, sess.HasPeakContextTokens)
}
// TestAntigravityTokenUsageCachedTokens verifies the gen_metadata path
// when cache-read tokens are present. ContextTokens is the full context
// window (uncached input + cache-read), InputTokens carries just the
// uncached portion, and CacheReadInputTokens carries the cache-read
// count. HasContextTokens must be true when context > 0.
func TestAntigravityTokenUsageCachedTokens(t *testing.T) {
root := t.TempDir()
id := "55555555-6666-7777-8888-aaaaaaaaaaaa"
mustMkdir(t, filepath.Join(root, "conversations"))
dbPath := filepath.Join(root, "conversations", id+".db")
db, err := sql.Open("sqlite3", dbPath)
require.NoError(t, err)
defer db.Close()
createAntigravityStepTables(t, db)
mustExec(t, db, `CREATE TABLE gen_metadata (idx integer, data blob, size integer, PRIMARY KEY (idx))`)
tsEarly := encodePB([]pbField{{num: 1, wire: pbWireVarint, varint: 1779000000}})
userPayload := encodePB([]pbField{
{num: 5, wire: pbWireBytes, bytes: tsEarly},
{num: 17, wire: pbWireBytes, bytes: []byte("user question text goes here and is long")},
})
tsLate := encodePB([]pbField{{num: 1, wire: pbWireVarint, varint: 1779000100}})
asstPayload := encodePB([]pbField{
{num: 5, wire: pbWireBytes, bytes: tsLate},
{num: 17, wire: pbWireBytes, bytes: []byte("assistant response body goes here and is long")},
})
mustExec(t, db, `INSERT INTO steps (idx, step_type, step_payload) VALUES (0, 14, ?)`, userPayload)
mustExec(t, db, `INSERT INTO steps (idx, step_type, step_payload) VALUES (1, 17, ?)`, asstPayload)
// uncachedInput=2200, totalOutput=180, cacheRead=200
genData := createAntigravityMockGenMetadata(t, 2200, 180, 200, "Test Gemini 3.5")
mustExec(t, db, `INSERT INTO gen_metadata (idx, data, size) VALUES (1, ?, ?)`, genData, len(genData))
sess, msgs, usageEvents, err := parseAntigravityTestSession(t, dbPath, "test-project", "test-machine")
require.NoError(t, err)
// Message token counts: context = uncached + cache-read.
require.Len(t, msgs, 2)
assert.Equal(t, RoleAssistant, msgs[1].Role)
assert.Equal(t, 2400, msgs[1].ContextTokens, "ContextTokens = uncached + cache-read = 2200 + 200")
assert.Equal(t, 180, msgs[1].OutputTokens)
assert.True(t, msgs[1].HasContextTokens, "HasContextTokens true when context > 0")
assert.True(t, msgs[1].HasOutputTokens)
// Usage event: InputTokens is the uncached portion, CacheReadInputTokens the rest.
require.Len(t, usageEvents, 1)
assert.Equal(t, "generation", usageEvents[0].Source)
assert.Equal(t, 2200, usageEvents[0].InputTokens, "InputTokens = uncached input")
assert.Equal(t, 200, usageEvents[0].CacheReadInputTokens)
assert.Equal(t, 180, usageEvents[0].OutputTokens)
assert.Equal(t, 0, usageEvents[0].ReasoningTokens, "reasoning not available in gen_metadata")
// Session rollup: PeakContextTokens is the peak context (uncached + cache-read).
assert.Equal(t, 180, sess.TotalOutputTokens)
assert.Equal(t, 2400, sess.PeakContextTokens)
assert.True(t, sess.HasTotalOutputTokens)
assert.True(t, sess.HasPeakContextTokens)
}
// TestAntigravityTokenUsageCachedTokensAllInputs verifies the edge case
// where input == cached (so uncached = 0). HasContextTokens must still be
// true because cached > 0, and InputTokens (uncached) should be 0.
func TestAntigravityTokenUsageCachedTokensAllInputs(t *testing.T) {
root := t.TempDir()
id := "55555555-6666-7777-8888-bbbbbbbbbbbb"
mustMkdir(t, filepath.Join(root, "conversations"))
dbPath := filepath.Join(root, "conversations", id+".db")
db, err := sql.Open("sqlite3", dbPath)
require.NoError(t, err)
defer db.Close()
createAntigravityStepTables(t, db)
mustExec(t, db, `CREATE TABLE gen_metadata (idx integer, data blob, size integer, PRIMARY KEY (idx))`)
tsEarly := encodePB([]pbField{{num: 1, wire: pbWireVarint, varint: 1779000000}})
userPayload := encodePB([]pbField{
{num: 5, wire: pbWireBytes, bytes: tsEarly},
{num: 17, wire: pbWireBytes, bytes: []byte("user question text goes here and is long")},
})
tsLate := encodePB([]pbField{{num: 1, wire: pbWireVarint, varint: 1779000100}})
asstPayload := encodePB([]pbField{
{num: 5, wire: pbWireBytes, bytes: tsLate},
{num: 17, wire: pbWireBytes, bytes: []byte("assistant response body goes here and is long")},
})
mustExec(t, db, `INSERT INTO steps (idx, step_type, step_payload) VALUES (0, 14, ?)`, userPayload)
mustExec(t, db, `INSERT INTO steps (idx, step_type, step_payload) VALUES (1, 17, ?)`, asstPayload)
// uncachedInput=50, totalOutput=100, cacheRead=200
// Nearly all input tokens served from cache (50 uncached out of 250 total context).
genData := createAntigravityMockGenMetadata(t, 50, 100, 200, "Test Gemini 3.5")
mustExec(t, db, `INSERT INTO gen_metadata (idx, data, size) VALUES (1, ?, ?)`, genData, len(genData))
sess, msgs, usageEvents, err := parseAntigravityTestSession(t, dbPath, "test-project", "test-machine")
require.NoError(t, err)
require.Len(t, msgs, 2)
assert.Equal(t, RoleAssistant, msgs[1].Role)
assert.Equal(t, 250, msgs[1].ContextTokens, "ContextTokens = uncached + cache-read = 50 + 200")
assert.Equal(t, 100, msgs[1].OutputTokens)
assert.True(t, msgs[1].HasContextTokens, "HasContextTokens true when context > 0")
assert.True(t, msgs[1].HasOutputTokens)
require.Len(t, usageEvents, 1)
assert.Equal(t, 50, usageEvents[0].InputTokens, "InputTokens = uncached input = 50")
assert.Equal(t, 200, usageEvents[0].CacheReadInputTokens)
assert.Equal(t, 100, usageEvents[0].OutputTokens)
assert.Equal(t, 0, usageEvents[0].ReasoningTokens)
assert.Equal(t, 100, sess.TotalOutputTokens)
assert.Equal(t, 250, sess.PeakContextTokens)
assert.True(t, sess.HasTotalOutputTokens)
assert.True(t, sess.HasPeakContextTokens)
}
// TestAntigravityTokenUsageMixedDecode covers a session where one
// gen_metadata row belongs to a decoded step and another to a step the
// heuristic cannot render. Session totals must include both, not just
// the tokens reachable through decoded messages.
func TestAntigravityTokenUsageMixedDecode(t *testing.T) {
root := t.TempDir()
id := "55555555-6666-7777-8888-999999999997"
mustMkdir(t, filepath.Join(root, "conversations"))
dbPath := filepath.Join(root, "conversations", id+".db")
db, err := sql.Open("sqlite3", dbPath)
require.NoError(t, err)
defer db.Close()
createAntigravityStepTables(t, db)
mustExec(t, db, `CREATE TABLE gen_metadata (idx integer, data blob, size integer, PRIMARY KEY (idx))`)
tsEarly := encodePB([]pbField{{num: 1, wire: pbWireVarint, varint: 1779000000}})
userPayload := encodePB([]pbField{
{num: 5, wire: pbWireBytes, bytes: tsEarly},
{num: 17, wire: pbWireBytes, bytes: []byte("user question text goes here and is long")},
})
tsLate := encodePB([]pbField{{num: 1, wire: pbWireVarint, varint: 1779000100}})
asstPayload := encodePB([]pbField{
{num: 5, wire: pbWireBytes, bytes: tsLate},
{num: 17, wire: pbWireBytes, bytes: []byte("assistant response body goes here and is long")},
})
mustExec(t, db, `INSERT INTO steps (idx, step_type, step_payload) VALUES (0, 14, ?)`, userPayload)
mustExec(t, db, `INSERT INTO steps (idx, step_type, step_payload) VALUES (1, 17, ?)`, asstPayload)
// Undecodable step with its own gen_metadata usage.
mustExec(t, db, `INSERT INTO steps (idx, step_type, step_payload) VALUES (2, 99, ?)`, []byte{0xff, 0xff, 0xff})
genDecoded := createAntigravityMockGenMetadata(t, 2400, 180, 0, "Test Gemini 3.5")
mustExec(t, db, `INSERT INTO gen_metadata (idx, data, size) VALUES (1, ?, ?)`, genDecoded, len(genDecoded))
genUndecoded := createAntigravityMockGenMetadata(t, 3000, 220, 0, "Test Gemini 3.5")
mustExec(t, db, `INSERT INTO gen_metadata (idx, data, size) VALUES (2, ?, ?)`, genUndecoded, len(genUndecoded))
sess, msgs, usageEvents, err := parseAntigravityTestSession(t, dbPath, "test-project", "test-machine")
require.NoError(t, err)
require.Len(t, msgs, 2, "undecodable step contributes no message")
require.Len(t, usageEvents, 2, "both gen rows emit usage events")
assert.Equal(t, 400, sess.TotalOutputTokens, "totals must include undecoded gen usage")
assert.Equal(t, 3000, sess.PeakContextTokens, "peak must include undecoded gen usage")
assert.True(t, sess.HasTotalOutputTokens)
assert.True(t, sess.HasPeakContextTokens)
}
// TestAntigravityCLITokenUsageMixedDecode is the CLI-path variant: the
// DB decode wins (no sidecar), one gen row maps to a decoded step and
// one to an undecodable step.
func TestAntigravityCLITokenUsageMixedDecode(t *testing.T) {
root := t.TempDir()
id := "ffffffff-0000-1111-2222-333333333333"
mustMkdir(t, filepath.Join(root, "conversations"))
dbPath := filepath.Join(root, "conversations", id+".db")
db, err := sql.Open("sqlite3", dbPath)
require.NoError(t, err)
createAntigravityStepTables(t, db)
mustExec(t, db, `CREATE TABLE gen_metadata (idx integer, data blob, size integer, PRIMARY KEY (idx))`)
tsLate := encodePB([]pbField{{num: 1, wire: pbWireVarint, varint: 1779000100}})
asstPayload := encodePB([]pbField{
{num: 5, wire: pbWireBytes, bytes: tsLate},
{num: 17, wire: pbWireBytes, bytes: []byte("assistant response body goes here and is long")},
})
mustExec(t, db, `INSERT INTO steps (idx, step_type, step_payload) VALUES (0, 17, ?)`, asstPayload)
mustExec(t, db, `INSERT INTO steps (idx, step_type, step_payload) VALUES (1, 99, ?)`, []byte{0xff, 0xff, 0xff})
genDecoded := createAntigravityMockGenMetadata(t, 2400, 180, 0, "Test Gemini 3.5")
mustExec(t, db, `INSERT INTO gen_metadata (idx, data, size) VALUES (0, ?, ?)`, genDecoded, len(genDecoded))
genUndecoded := createAntigravityMockGenMetadata(t, 3000, 220, 0, "Test Gemini 3.5")
mustExec(t, db, `INSERT INTO gen_metadata (idx, data, size) VALUES (1, ?, ?)`, genUndecoded, len(genUndecoded))
require.NoError(t, db.Close())
sess, msgs, usageEvents, _, err := parseAntigravityCLITestSessionWithStatus(t,
dbPath, "", "test-machine",
)
require.NoError(t, err)
require.NotEmpty(t, msgs)
require.Len(t, usageEvents, 2, "both gen rows emit usage events")
assert.Equal(t, 400, sess.TotalOutputTokens, "totals must include undecoded gen usage")
assert.Equal(t, 3000, sess.PeakContextTokens, "peak must include undecoded gen usage")
}
// TestAntigravityZeroMessageKeepsUsageEvents covers a DB whose only
// step is undecodable but carries gen_metadata usage: the parse yields
// no messages, yet the usage events (and the totals derived from
// them) must still be returned so daily usage analytics see them.
func TestAntigravityZeroMessageKeepsUsageEvents(t *testing.T) {
root := t.TempDir()
id := "55555555-6666-7777-8888-999999999996"
mustMkdir(t, filepath.Join(root, "conversations"))
dbPath := filepath.Join(root, "conversations", id+".db")
db, err := sql.Open("sqlite3", dbPath)
require.NoError(t, err)
defer db.Close()
createAntigravityStepTables(t, db)
mustExec(t, db, `CREATE TABLE gen_metadata (idx integer, data blob, size integer, PRIMARY KEY (idx))`)
mustExec(t, db, `INSERT INTO steps (idx, step_type, step_payload) VALUES (0, 99, ?)`, []byte{0xff, 0xff, 0xff})
genData := createAntigravityMockGenMetadata(t, 2400, 180, 0, "Test Gemini 3.5")
mustExec(t, db, `INSERT INTO gen_metadata (idx, data, size) VALUES (0, ?, ?)`, genData, len(genData))
sess, msgs, usageEvents, err := parseAntigravityTestSession(t, dbPath, "test-project", "test-machine")
require.NoError(t, err)
assert.Empty(t, msgs)
require.Len(t, usageEvents, 1, "usage events must survive zero-message parses")
assert.Equal(t, "Test Gemini 3.5", usageEvents[0].Model)
assert.Equal(t, 180, sess.TotalOutputTokens)
assert.Equal(t, 2400, sess.PeakContextTokens)
}
// TestAntigravityCLIZeroMessageKeepsUsageEvents is the CLI variant: an
// undecodable .db with gen_metadata, no sidecar, and no history still
// returns its usage events alongside the retry-flagged session.
func TestAntigravityCLIZeroMessageKeepsUsageEvents(t *testing.T) {
root := t.TempDir()
id := "00000000-1111-2222-3333-444444444444"
mustMkdir(t, filepath.Join(root, "conversations"))
dbPath := filepath.Join(root, "conversations", id+".db")
db, err := sql.Open("sqlite3", dbPath)
require.NoError(t, err)
createAntigravityStepTables(t, db)
mustExec(t, db, `CREATE TABLE gen_metadata (idx integer, data blob, size integer, PRIMARY KEY (idx))`)
mustExec(t, db, `INSERT INTO steps (idx, step_type, step_payload) VALUES (0, 99, ?)`, []byte{0xff, 0xff, 0xff})
genData := createAntigravityMockGenMetadata(t, 3000, 220, 0, "Test Gemini 3.5")
mustExec(t, db, `INSERT INTO gen_metadata (idx, data, size) VALUES (0, ?, ?)`, genData, len(genData))
require.NoError(t, db.Close())
sess, msgs, usageEvents, status, err := parseAntigravityCLITestSessionWithStatus(t,
dbPath, "", "test-machine",
)
require.NoError(t, err)
assert.True(t, status.NeedsRetry, "undecodable rows stay retryable")
assert.Empty(t, msgs)
require.Len(t, usageEvents, 1, "usage events must survive zero-message parses")
assert.Equal(t, 220, usageEvents[0].OutputTokens)
assert.Equal(t, 220, sess.TotalOutputTokens)
assert.Equal(t, 3000, sess.PeakContextTokens)
}
// undecodableAntigravityGenMetadata builds a gen_metadata blob whose token
// block is rejected by the heuristic (field 2 exceeds the plausibility cap),
// so it decodes into no usage event even though the row is present.
func undecodableAntigravityGenMetadata() []byte {
return encodePB([]pbField{
{num: 1, wire: pbWireVarint, varint: 1371},
{num: 2, wire: pbWireVarint, varint: 679261000}, // implausible input
{num: 3, wire: pbWireVarint, varint: 100},
})
}
// TestAntigravityGenMetadataWithoutUsageFlag exercises the IDE parser's
// GenMetadataWithoutUsage flag: it is set only when the steps DB carried
// gen_metadata rows that decoded into zero usage events. An absent or empty
// gen_metadata table, or rows that decode into usage, leave it false.
func TestAntigravityGenMetadataWithoutUsageFlag(t *testing.T) {
decodableStep := func(t *testing.T, db *sql.DB) {
t.Helper()
ts := encodePB([]pbField{{num: 1, wire: pbWireVarint, varint: 1779000100}})
asstPayload := encodePB([]pbField{
{num: 5, wire: pbWireBytes, bytes: ts},
{num: 17, wire: pbWireBytes, bytes: []byte("assistant response body goes here and is long")},
})
mustExec(t, db,
`INSERT INTO steps (idx, step_type, step_payload) VALUES (0, 17, ?)`,
asstPayload)
}
tests := []struct {
name string
setupDB func(t *testing.T, db *sql.DB)
wantFlag bool
}{
{
name: "no gen_metadata table",
setupDB: func(t *testing.T, db *sql.DB) {
createAntigravityStepTables(t, db)
decodableStep(t, db)
},
wantFlag: false,
},
{
name: "empty gen_metadata table",
setupDB: func(t *testing.T, db *sql.DB) {
createAntigravityStepTables(t, db)
mustExec(t, db, `CREATE TABLE gen_metadata (idx integer, data blob, size integer, PRIMARY KEY (idx))`)
decodableStep(t, db)
},
wantFlag: false,
},
{
name: "gen_metadata rows decode to usage",
setupDB: func(t *testing.T, db *sql.DB) {
createAntigravityStepTables(t, db)
mustExec(t, db, `CREATE TABLE gen_metadata (idx integer, data blob, size integer, PRIMARY KEY (idx))`)
decodableStep(t, db)
genData := createAntigravityMockGenMetadata(t, 2400, 180, 0, "Test Gemini 3.5")
mustExec(t, db, `INSERT INTO gen_metadata (idx, data, size) VALUES (0, ?, ?)`, genData, len(genData))
},
wantFlag: false,
},
{
name: "gen_metadata rows all undecodable",
setupDB: func(t *testing.T, db *sql.DB) {
createAntigravityStepTables(t, db)
mustExec(t, db, `CREATE TABLE gen_metadata (idx integer, data blob, size integer, PRIMARY KEY (idx))`)
decodableStep(t, db)
blob := undecodableAntigravityGenMetadata()
mustExec(t, db, `INSERT INTO gen_metadata (idx, data, size) VALUES (0, ?, ?)`, blob, len(blob))
},
wantFlag: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
root := t.TempDir()
id := "55555555-6666-7777-8888-abababababab"
mustMkdir(t, filepath.Join(root, "conversations"))
dbPath := filepath.Join(root, "conversations", id+".db")
db, err := sql.Open("sqlite3", dbPath)
require.NoError(t, err)
tt.setupDB(t, db)
require.NoError(t, db.Close())
sess, _, usageEvents, err := parseAntigravityTestSession(t,
dbPath, "test-project", "test-machine")
require.NoError(t, err)
assert.Equal(t, tt.wantFlag, sess.GenMetadataWithoutUsage)
if tt.wantFlag {
assert.Empty(t, usageEvents,
"flag implies zero decoded usage events")
}
})
}
}
// TestAntigravityCLIGenMetadataWithoutUsageFlag covers the CLI parser's
// critical timing: the flag is computed from the FINAL usageEvents, after the
// trajectory-sidecar gap-fill. A session whose gen_metadata failed to decode
// but whose sidecar supplied usage is NOT flagged; a session with neither
// source of usage is.
func TestAntigravityCLIGenMetadataWithoutUsageFlag(t *testing.T) {
tests := []struct {
name string
setup func(t *testing.T, root, id, dbPath string)
wantFlag bool
wantUsed bool // usage events expected on the parse result
}{
{
name: "undecodable gen_metadata rescued by sidecar usage",
setup: func(t *testing.T, root, id, dbPath string) {
db, err := sql.Open("sqlite3", dbPath)
require.NoError(t, err)
createAntigravityStepTables(t, db)
mustExec(t, db, `CREATE TABLE gen_metadata (idx integer, data blob, size integer, PRIMARY KEY (idx))`)
for i := range 2 {
mustExec(t, db,
`INSERT INTO steps (idx, step_type, step_payload) VALUES (?, ?, ?)`,
i, 99, []byte{0xff, 0xff, 0xff})
}
blob := undecodableAntigravityGenMetadata()
mustExec(t, db, `INSERT INTO gen_metadata (idx, data, size) VALUES (1, ?, ?)`, blob, len(blob))
require.NoError(t, db.Close())
// Covering sidecar carries generatorMetadata usage that fills
// the gap left by the undecodable gen_metadata.
genJSON := `[{
"stepIndices": [1],
"chatModel": {
"model": "MODEL_PLACEHOLDER_M20",
"usage": {"inputTokens": "1500", "outputTokens": "77"}
}
}]`
writeAntigravityTestSidecarWithGenMetadata(t, root, id, 2, genJSON)
},
wantFlag: false,
wantUsed: true,
},
{
name: "undecodable gen_metadata and no sidecar usage",
setup: func(t *testing.T, root, id, dbPath string) {
db, err := sql.Open("sqlite3", dbPath)
require.NoError(t, err)
createAntigravityStepTables(t, db)
mustExec(t, db, `CREATE TABLE gen_metadata (idx integer, data blob, size integer, PRIMARY KEY (idx))`)
mustExec(t, db,
`INSERT INTO steps (idx, step_type, step_payload) VALUES (0, 99, ?)`,
[]byte{0xff, 0xff, 0xff})
blob := undecodableAntigravityGenMetadata()
mustExec(t, db, `INSERT INTO gen_metadata (idx, data, size) VALUES (0, ?, ?)`, blob, len(blob))
require.NoError(t, db.Close())
// No sidecar written: nothing fills the usage gap.
},
wantFlag: true,
wantUsed: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
root := t.TempDir()
id := "00000000-1111-2222-3333-cdcdcdcdcdcd"
mustMkdir(t, filepath.Join(root, "conversations"))
dbPath := filepath.Join(root, "conversations", id+".db")
tt.setup(t, root, id, dbPath)
sess, _, usageEvents, _, err := parseAntigravityCLITestSessionWithStatus(t,
dbPath, "", "test-machine")
require.NoError(t, err)
assert.Equal(t, tt.wantFlag, sess.GenMetadataWithoutUsage)
if tt.wantUsed {
assert.NotEmpty(t, usageEvents,
"sidecar gap-fill should supply usage events")
} else {
assert.Empty(t, usageEvents,
"flag implies zero decoded usage events")
}
})
}
}
func TestAntigravityTokenUsageDynamicField(t *testing.T) {
root := t.TempDir()
id := "55555555-6666-7777-8888-999999999998"
mustMkdir(t, filepath.Join(root, "conversations"))
dbPath := filepath.Join(root, "conversations", id+".db")
db, err := sql.Open("sqlite3", dbPath)
require.NoError(t, err)
defer db.Close()
createAntigravityStepTables(t, db)
// Create gen_metadata table
mustExec(t, db, `CREATE TABLE gen_metadata (idx integer, data blob, size integer, PRIMARY KEY (idx))`)
// Create user prompt step (idx=0) and assistant reply step (idx=1)
tsEarly := encodePB([]pbField{{num: 1, wire: pbWireVarint, varint: 1779000000}})
userPayload := encodePB([]pbField{
{num: 5, wire: pbWireBytes, bytes: tsEarly},
{num: 17, wire: pbWireBytes, bytes: []byte("user question text goes here and is long")},
})
tsLate := encodePB([]pbField{{num: 1, wire: pbWireVarint, varint: 1779000100}})
asstPayload := encodePB([]pbField{
{num: 5, wire: pbWireBytes, bytes: tsLate},
{num: 17, wire: pbWireBytes, bytes: []byte("assistant response body goes here")},
})
mustExec(t, db, `INSERT INTO steps (idx, step_type, step_payload) VALUES (0, 14, ?)`, userPayload)
mustExec(t, db, `INSERT INTO steps (idx, step_type, step_payload) VALUES (1, 17, ?)`, asstPayload)
// Create gen_metadata entry with a dynamic model field number: 1187 (for MODEL_PLACEHOLDER_M187)
genData := createAntigravityMockGenMetadataWithField(t, 1187, 5000, 400, 0, "Test Gemini 3.5 Flash")
mustExec(t, db, `INSERT INTO gen_metadata (idx, data, size) VALUES (1, ?, ?)`, genData, len(genData))
sess, msgs, usageEvents, err := parseAntigravityTestSession(t, dbPath, "test-project", "test-machine")
require.NoError(t, err)
require.Len(t, msgs, 2)
assert.Equal(t, "Test Gemini 3.5 Flash", msgs[1].Model)
assert.Equal(t, 5000, msgs[1].ContextTokens)
assert.Equal(t, 400, msgs[1].OutputTokens)
require.Len(t, usageEvents, 1)
assert.Equal(t, "Test Gemini 3.5 Flash", usageEvents[0].Model)
assert.Equal(t, 5000, usageEvents[0].InputTokens)
assert.Equal(t, 400, usageEvents[0].OutputTokens)
assert.Equal(t, 0, usageEvents[0].ReasoningTokens, "reasoning not available in gen_metadata")
assert.Equal(t, 400, sess.TotalOutputTokens)
assert.Equal(t, 5000, sess.PeakContextTokens)
}
func createAntigravityMockGenMetadata(t *testing.T, uncachedInput, totalOutput, cacheRead int, model string) []byte {
return createAntigravityMockGenMetadataWithField(t, 1020, uncachedInput, totalOutput, cacheRead, model)
}
func createAntigravityMockGenMetadataWithField(t *testing.T, fieldNum int, uncachedInput, totalOutput, cacheRead int, model string) []byte {
// Build token usage inner block with remapped field semantics
// cross-validated against sidecar ground truth:
// f2 = uncached input (inputTokens)
// f3 = total output including thinking (outputTokens)
// f4 = absent (never carries semantics)
// f5 = cache-read (cacheReadTokens, present when > 0)
usageInner := encodePB([]pbField{
{num: 1, wire: pbWireVarint, varint: uint64(fieldNum)},
{num: 2, wire: pbWireVarint, varint: uint64(uncachedInput)},
{num: 3, wire: pbWireVarint, varint: uint64(totalOutput)},
})
if cacheRead > 0 {
usageInner = encodePB([]pbField{
{num: 1, wire: pbWireVarint, varint: uint64(fieldNum)},
{num: 2, wire: pbWireVarint, varint: uint64(uncachedInput)},
{num: 3, wire: pbWireVarint, varint: uint64(totalOutput)},
{num: 5, wire: pbWireVarint, varint: uint64(cacheRead)},
})
}
// Build Field 2 (Nested message) of Field 17
f17Inner := encodePB([]pbField{
{num: 2, wire: pbWireBytes, bytes: usageInner},
})
// Build top-level fields: Field 17 (Nested bytes), Field 21 (String bytes)
topFields := []pbField{
{num: 17, wire: pbWireBytes, bytes: f17Inner},
}
if model != "" {
topFields = append(topFields, pbField{num: 21, wire: pbWireBytes, bytes: []byte(model)})
}
return encodePB(topFields)
}
// TestExtractTokenUsageFalsePositiveGuards verifies that decoy blocks
// satisfying the field1 model-kind range are rejected when the expected
// token fields are missing or implausible, so the walk continues to the
// real token block instead of stopping early with junk values.
func TestExtractTokenUsageFalsePositiveGuards(t *testing.T) {
realToken := encodePB([]pbField{
{num: 1, wire: pbWireVarint, varint: 1187},
{num: 2, wire: pbWireVarint, varint: 5529}, // uncached input
{num: 3, wire: pbWireVarint, varint: 72}, // total output
{num: 5, wire: pbWireVarint, varint: 38885}, // cache-read
})
tests := []struct {
name string
decoy []pbField
}{
{
name: "decoy without output field",
decoy: []pbField{
{num: 1, wire: pbWireVarint, varint: 1371},
{num: 2, wire: pbWireVarint, varint: 1234},
},
},
{
name: "decoy with implausible input",
decoy: []pbField{
{num: 1, wire: pbWireVarint, varint: 1371},
{num: 2, wire: pbWireVarint, varint: 679261000},
{num: 3, wire: pbWireVarint, varint: 100},
},
},
{
name: "decoy with implausible output",
decoy: []pbField{
{num: 1, wire: pbWireVarint, varint: 1371},
{num: 2, wire: pbWireVarint, varint: 1234},
{num: 3, wire: pbWireVarint, varint: 679261000},
},
},
{
name: "decoy with wrong-typed output",
decoy: []pbField{
{num: 1, wire: pbWireVarint, varint: 1371},
{num: 2, wire: pbWireVarint, varint: 1234},
{num: 3, wire: pbWireBytes, bytes: []byte("not a varint")},
},
},
{
name: "decoy with implausible cache-read",
decoy: []pbField{
{num: 1, wire: pbWireVarint, varint: 1371},
{num: 2, wire: pbWireVarint, varint: 1234},
{num: 3, wire: pbWireVarint, varint: 100},
{num: 5, wire: pbWireVarint, varint: 679261000},
},
},
{
// Input and output are each within the cap, but their
// combined footprint exceeds the plausibility threshold,
// rejecting blocks that individually pass per-field checks.
name: "decoy with input+output above cap",
decoy: []pbField{
{num: 1, wire: pbWireVarint, varint: 1371},
{num: 2, wire: pbWireVarint, varint: 1_999_999},
{num: 3, wire: pbWireVarint, varint: 1_999_999},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Decoy first in DFS order so acceptance would
// shadow the real block.
data := encodePB([]pbField{
{num: 1, wire: pbWireBytes, bytes: encodePB(tt.decoy)},
{num: 2, wire: pbWireBytes, bytes: realToken},
})
block, ok := extractTokenUsage(data)
require.True(t, ok, "real token block should be found")
assert.Equal(t, 5529, block.UncachedInput, "uncached input tokens")
assert.Equal(t, 72, block.TotalOutput, "total output tokens")
assert.Equal(t, 38885, block.CacheRead, "cache-read tokens")
})
}
}
// TestExtractModelNameRejectsNonPrintable verifies that field 21/19
// payloads that are valid UTF-8 but not human-readable text (nested
// protobuf messages whose low bytes pass utf8.Valid) are rejected
// instead of being persisted as the model name. The fragment bytes are
// the exact gen_metadata field-21 payload observed in production
// sessions, which leaked into messages.model with an embedded NUL byte
// and broke `pg push` (PG rejects NUL with SQLSTATE 22021).
func TestExtractModelNameRejectsNonPrintable(t *testing.T) {
// hex 080020022A0201024001: a nested message (field 1 varint,
// field 4 varint, field 5 bytes, field 8 varint), all bytes
// < 0x80 so utf8.Valid accepts it.
protoFragment := []byte{
0x08, 0x00, 0x20, 0x02, 0x2A, 0x02,
0x01, 0x02, 0x40, 0x01,
}
tests := []struct {
name string
data []byte
want string
}{
{
name: "real protobuf fragment in field 21",
data: encodePB([]pbField{
{num: 21, wire: pbWireBytes, bytes: protoFragment},
}),
want: "",
},
{
name: "fragment in field 21 falls back to field 19",
data: encodePB([]pbField{
{num: 21, wire: pbWireBytes, bytes: protoFragment},
{num: 19, wire: pbWireBytes, bytes: []byte("gemini-3-pro")},
}),
want: "gemini-3-pro",
},
{
name: "fragment at top level falls through to nested model",
data: encodePB([]pbField{
{num: 21, wire: pbWireBytes, bytes: protoFragment},
{num: 2, wire: pbWireBytes, bytes: encodePB([]pbField{
{num: 21, wire: pbWireBytes, bytes: []byte("gemini-3-flash")},
})},
}),
want: "gemini-3-flash",
},
{
name: "digits-only value is rejected",
data: encodePB([]pbField{
{num: 21, wire: pbWireBytes, bytes: []byte("12345678")},
}),
want: "",
},
{
name: "plain model name still accepted",
data: encodePB([]pbField{
{num: 21, wire: pbWireBytes, bytes: []byte("Test Gemini 3.5")},
}),
want: "Test Gemini 3.5",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.want, extractModelName(tt.data))
})
}
}
// TestExtractTokenUsageNoCacheReadField verifies a real token block is
// accepted when field 5 (cache-read) is absent: proto3 omits zero-valued
// fields, and a fresh session with no cache hits legitimately has
// f5=0, which is omitted from the wire.
func TestExtractTokenUsageNoCacheReadField(t *testing.T) {
block := encodePB([]pbField{
{num: 1, wire: pbWireVarint, varint: 1187},
{num: 2, wire: pbWireVarint, varint: 5529}, // uncached input
{num: 3, wire: pbWireVarint, varint: 72}, // total output
})
data := encodePB([]pbField{
{num: 1, wire: pbWireBytes, bytes: block},
})
result, ok := extractTokenUsage(data)
require.True(t, ok, "token block without cache-read should be accepted")
assert.Equal(t, 5529, result.UncachedInput, "uncached input tokens")
assert.Equal(t, 72, result.TotalOutput, "total output tokens")
assert.Equal(t, 0, result.CacheRead, "cache-read defaults to 0 when absent")
}
// TestTokenBlockFromIgnoresField4 verifies that a protobuf block
// where field 4 is present (always 0/absent in real blocks) is still
// accepted as long as all required fields are valid. Field 4 carries
// no semantics and is tolerated but ignored.
func TestTokenBlockFromIgnoresField4(t *testing.T) {
block := encodePB([]pbField{
{num: 1, wire: pbWireVarint, varint: 1371},
{num: 2, wire: pbWireVarint, varint: 500},
{num: 3, wire: pbWireVarint, varint: 100},
{num: 4, wire: pbWireVarint, varint: 0}, // f4 is tolerated
})
data := encodePB([]pbField{
{num: 1, wire: pbWireBytes, bytes: block},
})
result, ok := extractTokenUsage(data)
assert.True(t, ok, "block with f4=0 should be accepted")
assert.Equal(t, 500, result.UncachedInput)
assert.Equal(t, 100, result.TotalOutput)
assert.Equal(t, 0, result.CacheRead)
}
// TestExtractTokenUsageLargeValueFalsePositive is a regression test for the
// case where a nested message has field1 ∈ [1000, 5000) but field2 holds an
// implausibly large value (e.g. a nanosecond-latency counter). The real token
// block should be returned instead of the decoy.
func TestExtractTokenUsageLargeValueFalsePositive(t *testing.T) {
// Decoy block: field1=1371 (in range), field2=679261000 (implausibly large)
decoy := encodePB([]pbField{
{num: 1, wire: pbWireVarint, varint: 1371},
{num: 2, wire: pbWireVarint, varint: 679261000},
})
// A wrapper that embeds the decoy nested under field 8, matching the real
// session layout where the false-positive lives at [1].[9].[8].
innerWrapper := encodePB([]pbField{
{num: 8, wire: pbWireBytes, bytes: decoy},
})
outerWrapper := encodePB([]pbField{
{num: 9, wire: pbWireBytes, bytes: innerWrapper},
})
// Real token block with remapped field semantics:
// f2=5529 (uncached input), f3=72 (total output), f5=38885 (cache-read).
realToken := encodePB([]pbField{
{num: 1, wire: pbWireVarint, varint: 1187},
{num: 2, wire: pbWireVarint, varint: 5529},
{num: 3, wire: pbWireVarint, varint: 72},
{num: 5, wire: pbWireVarint, varint: 38885},
})
realWrapper := encodePB([]pbField{
{num: 4, wire: pbWireBytes, bytes: realToken},
})
// Top-level: field1 contains the real token block, field1 also has
// the decoy-containing wrapper. The decoy appears first in DFS order
// to validate that it is correctly skipped.
data := encodePB([]pbField{
{num: 1, wire: pbWireBytes, bytes: outerWrapper}, // decoy path first
{num: 2, wire: pbWireBytes, bytes: realWrapper}, // real token block second
})
block, ok := extractTokenUsage(data)
require.True(t, ok, "extractTokenUsage should find a token block")
assert.Equal(t, 5529, block.UncachedInput, "uncached input tokens")
assert.Equal(t, 72, block.TotalOutput, "total output tokens")
assert.Equal(t, 38885, block.CacheRead, "cache-read tokens")
}
func TestAntigravityAdjacentToolCalls(t *testing.T) {
// A flat sequence of strings simulating two consecutive tool calls, e.g. run_command.
// The new logic should prefer the following UUID and not deduplicate distinct calls.
fields := []agProtoField{
{Number: 1, Wire: pbWireBytes, Bytes: []byte("run_command")},
{Number: 1, Wire: pbWireBytes, Bytes: []byte("11111111-1111-1111-1111-111111111111")},
{Number: 1, Wire: pbWireBytes, Bytes: []byte(`{"command": "ls"}`)},
{Number: 1, Wire: pbWireBytes, Bytes: []byte("run_command")},
{Number: 1, Wire: pbWireBytes, Bytes: []byte("22222222-2222-2222-2222-222222222222")},
{Number: 1, Wire: pbWireBytes, Bytes: []byte(`{"command": "pwd"}`)},
}
calls := extractAntigravityToolCalls(0, fields)
require.Len(t, calls, 2)
assert.Equal(t, "run_command", calls[0].ToolName)
assert.Equal(t, "Bash", calls[0].Category)
assert.Equal(t, "11111111-1111-1111-1111-111111111111", calls[0].ToolUseID)
assert.Equal(t, `{"command": "ls"}`, calls[0].InputJSON)
assert.Equal(t, "run_command", calls[1].ToolName)
assert.Equal(t, "Bash", calls[1].Category)
assert.Equal(t, "22222222-2222-2222-2222-222222222222", calls[1].ToolUseID)
assert.Equal(t, `{"command": "pwd"}`, calls[1].InputJSON)
}
func TestAntigravityAdjacentToolCallsNoUUIDs(t *testing.T) {
// Two identical tool names with no adjacent UUIDs or JSON.
// Both must be emitted as separate calls with synthetic IDs.
fields := []agProtoField{
{Number: 1, Wire: pbWireBytes, Bytes: []byte("run_command")},
{Number: 1, Wire: pbWireBytes, Bytes: []byte("run_command")},
}
calls := extractAntigravityToolCalls(0, fields)
require.Len(t, calls, 2)
assert.Equal(t, "run_command", calls[0].ToolName)
assert.Equal(t, "run_command", calls[1].ToolName)
// Synthetic IDs should differ because they encode the string index.
assert.NotEqual(t, calls[0].ToolUseID, calls[1].ToolUseID)
assert.Contains(t, calls[0].ToolUseID, "ag-step-")
assert.Contains(t, calls[1].ToolUseID, "ag-step-")
}
func TestAntigravityToolCallUUIDBackwardFallback(t *testing.T) {
// UUID appears before the tool name (e.g. tool at end of string list).
// The backward scan offsets (-1, -2) should still pick it up.
fields := []agProtoField{
{Number: 1, Wire: pbWireBytes, Bytes: []byte("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa")},
{Number: 1, Wire: pbWireBytes, Bytes: []byte("view_file")},
}
calls := extractAntigravityToolCalls(0, fields)
require.Len(t, calls, 1)
assert.Equal(t, "view_file", calls[0].ToolName)
assert.Equal(t, "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", calls[0].ToolUseID)
}
// TestAntigravityToolCallsRejectsGenericStrings verifies that standalone
// generic strings like "read", "write", "message", "process" that happen
// to match the global taxonomy but are NOT known Antigravity tool names
// are not fabricated into tool calls without corroborating evidence.
func TestAntigravityToolCallsRejectsGenericStrings(t *testing.T) {
t.Parallel()
tests := []struct {
name string
fields []agProtoField
wantLen int
}{
{
name: "standalone read with no evidence",
fields: []agProtoField{
{Number: 1, Wire: pbWireBytes, Bytes: []byte("read")},
},
wantLen: 0,
},
{
name: "standalone write with no evidence",
fields: []agProtoField{
{Number: 1, Wire: pbWireBytes, Bytes: []byte("write")},
},
wantLen: 0,
},
{
name: "standalone message with no evidence",
fields: []agProtoField{
{Number: 1, Wire: pbWireBytes, Bytes: []byte("message")},
},
wantLen: 0,
},
{
name: "standalone process with no evidence",
fields: []agProtoField{
{Number: 1, Wire: pbWireBytes, Bytes: []byte("process")},
},
wantLen: 0,
},
{
name: "string containing subagent is not a tool name",
fields: []agProtoField{
{Number: 1, Wire: pbWireBytes, Bytes: []byte("some_subagent_thing")},
},
wantLen: 0,
},
{
name: "read with adjacent UUID is rejected",
fields: []agProtoField{
{Number: 1, Wire: pbWireBytes, Bytes: []byte("read")},
{Number: 2, Wire: pbWireBytes, Bytes: []byte("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa")},
},
wantLen: 0,
},
{
name: "read with adjacent JSON input is rejected",
fields: []agProtoField{
{Number: 1, Wire: pbWireBytes, Bytes: []byte("read")},
{Number: 2, Wire: pbWireBytes, Bytes: []byte(`{"path": "/tmp/x"}`)},
},
wantLen: 0,
},
{
name: "known Antigravity tool view_file always accepted",
fields: []agProtoField{
{Number: 1, Wire: pbWireBytes, Bytes: []byte("view_file")},
},
wantLen: 1,
},
{
name: "known Antigravity tool invoke_subagent always accepted",
fields: []agProtoField{
{Number: 1, Wire: pbWireBytes, Bytes: []byte("invoke_subagent")},
},
wantLen: 1,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
calls := extractAntigravityToolCalls(0, tc.fields)
assert.Len(t, calls, tc.wantLen)
})
}
}
func TestAntigravityAdjacentToolCallsIntervening(t *testing.T) {
// Tool call N has no UUID. Tool call N+1 has a UUID.
// Call N should not steal Call N+1's UUID or JSON input across the intervening tool name.
fields := []agProtoField{
{Number: 1, Wire: pbWireBytes, Bytes: []byte("run_command")},
{Number: 2, Wire: pbWireBytes, Bytes: []byte("run_command")},
{Number: 3, Wire: pbWireBytes, Bytes: []byte("11111111-1111-1111-1111-111111111111")},
{Number: 4, Wire: pbWireBytes, Bytes: []byte(`{"command": "pwd"}`)},
}
calls := extractAntigravityToolCalls(0, fields)
require.Len(t, calls, 2)
// First call should have synthetic ID and NO JSON input (it is blocked by the second tool name)
assert.Equal(t, "run_command", calls[0].ToolName)
assert.Contains(t, calls[0].ToolUseID, "ag-step-")
assert.Empty(t, calls[0].InputJSON)
// Second call should have the UUID and JSON input
assert.Equal(t, "run_command", calls[1].ToolName)
assert.Equal(t, "11111111-1111-1111-1111-111111111111", calls[1].ToolUseID)
assert.Equal(t, `{"command": "pwd"}`, calls[1].InputJSON)
}
// TestDecodeAntigravityStepToolOnlyAssistantStep verifies that an
// assistant step containing only tool calls (no displayable prose)
// is still emitted as a ParsedMessage with HasToolUse=true rather
// than being silently dropped.
func TestDecodeAntigravityStepToolOnlyAssistantStep(t *testing.T) {
// An assistant step (stepType=15) with only a tool name and a UUID.
// agProtoCollectStrings with minLen=20 would miss the short strings,
// and the UUID would be filtered by cleanAntigravityStepStrings,
// leaving no displayable content. Previously this caused the step
// to be dropped entirely; now it should still be emitted with the
// tool call intact.
inner := encodePB([]pbField{
{num: 1, wire: pbWireVarint, varint: 1779326586},
})
payload := encodePB([]pbField{
{num: 17, wire: pbWireBytes, bytes: []byte("view_file")},
{num: 18, wire: pbWireBytes, bytes: []byte("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa")},
{num: 5, wire: pbWireBytes, bytes: inner},
})
msg, ok := decodeAntigravityStep(0, 15, payload)
require.True(t, ok, "tool-only assistant step should not be dropped")
assert.Equal(t, RoleAssistant, msg.Role)
assert.True(t, msg.HasToolUse, "HasToolUse should be true")
require.Len(t, msg.ToolCalls, 1)
assert.Equal(t, "view_file", msg.ToolCalls[0].ToolName)
}
func TestAntigravityCLITranscriptFidelity(t *testing.T) {
t.Run("covering sidecar is full", func(t *testing.T) {
root := t.TempDir()
id := "11111111-2222-3333-4444-555555555555"
mustMkdir(t, filepath.Join(root, "conversations"))
dbPath := filepath.Join(root, "conversations", id+".db")
createAntigravityTestDB(t, dbPath) // 2 raw steps
writeAntigravityTestSidecar(t, root, id, 2)
sess, _, _, status, err := parseAntigravityCLITestSessionWithStatus(
t, dbPath, "", "test-machine",
)
require.NoError(t, err)
assert.False(t, status.NeedsRetry)
assert.Equal(t, TranscriptFidelityFull, sess.TranscriptFidelity)
})
t.Run("heuristic db decode (no covering sidecar) is summary", func(t *testing.T) {
root := t.TempDir()
id := "22222222-3333-4444-5555-666666666666"
mustMkdir(t, filepath.Join(root, "conversations"))
dbPath := filepath.Join(root, "conversations", id+".db")
createAntigravityTestDB(t, dbPath) // 2 raw steps
writeAntigravityTestSidecar(t, root, id, 1) // lags -> db decode wins
mustWrite(t, filepath.Join(root, "history.jsonl"),
[]byte(`{"display":"history prompt","timestamp":1779000000000,`+
`"workspace":"/tmp/db-proj","conversationId":"`+id+`"}`))
sess, _, _, status, err := parseAntigravityCLITestSessionWithStatus(
t, dbPath, "", "test-machine",
)
require.NoError(t, err)
assert.False(t, status.NeedsRetry)
assert.Equal(t, TranscriptFidelitySummary, sess.TranscriptFidelity)
})
t.Run("partial sidecar over undecodable db is summary", func(t *testing.T) {
root := t.TempDir()
id := "33333333-4444-5555-6666-777777777777"
mustMkdir(t, filepath.Join(root, "conversations"))
dbPath := filepath.Join(root, "conversations", id+".db")
createAntigravityUndecodableDB(t, dbPath, 3)
writeAntigravityTestSidecar(t, root, id, 2) // partial
sess, _, _, status, err := parseAntigravityCLITestSessionWithStatus(
t, dbPath, "", "test-machine",
)
require.NoError(t, err)
assert.True(t, status.NeedsRetry)
assert.Equal(t, TranscriptFidelitySummary, sess.TranscriptFidelity)
})
t.Run("legacy .pb sidecar is full", func(t *testing.T) {
root := t.TempDir()
id := "44444444-5555-6666-7777-888888888888"
mustMkdir(t, filepath.Join(root, "conversations"))
pbPath := filepath.Join(root, "conversations", id+".pb")
mustWrite(t, pbPath, []byte("pb-stub"))
writeAntigravityTestSidecar(t, root, id, 2)
sess, _, err := parseAntigravityCLITestSession(t, pbPath, "", "test-machine")
require.NoError(t, err)
assert.Equal(t, TranscriptFidelityFull, sess.TranscriptFidelity)
})
t.Run("history+brain only is summary", func(t *testing.T) {
root := t.TempDir()
id := "55555555-6666-7777-8888-999999999999"
mustMkdir(t, filepath.Join(root, "conversations"))
pbPath := filepath.Join(root, "conversations", id+".pb")
mustWrite(t, pbPath, []byte("pb-stub")) // no sidecar, no key
mustWrite(t, filepath.Join(root, "history.jsonl"),
[]byte(`{"display":"hi","timestamp":1779000000000,`+
`"workspace":"/tmp/p","conversationId":"`+id+`"}`))
sess, _, err := parseAntigravityCLITestSession(t, pbPath, "", "test-machine")
require.NoError(t, err)
assert.Equal(t, TranscriptFidelitySummary, sess.TranscriptFidelity)
})
}
// ---- IDE trajectory sidecar (agy-reader) ----------------------
//
// The IDE parser mirrors the CLI sidecar-wins path: when a
// conversations/<uuid>.trajectory.json sidecar covers the raw .db step
// count it replaces the heuristic decode and marks the transcript full;
// otherwise the heuristic decode stands exactly as before. These tests
// reuse the CLI sidecar fixtures (writeAntigravityTestSidecar) since the
// sidecar format and on-disk location are identical.
func TestAntigravityIDEPrefersSidecarWithCoverage(t *testing.T) {
root := t.TempDir()
id := "1a1a1a1a-2b2b-3c3c-4d4d-5e5e5e5e5e5e"
mustMkdir(t, filepath.Join(root, "conversations"))
dbPath := filepath.Join(root, "conversations", id+".db")
createAntigravityTestDB(t, dbPath) // 2 raw steps
writeAntigravityTestSidecar(t, root, id, 2)
sess, msgs, _, err := parseAntigravityTestSession(t, dbPath, "", "test-machine")
require.NoError(t, err)
// Structured sidecar transcript wins over the heuristic decode.
require.Len(t, msgs, 2)
assert.Equal(t, RoleUser, msgs[0].Role)
assert.Equal(t, "sidecar prompt", msgs[0].Content)
assert.Equal(t, RoleAssistant, msgs[1].Role)
assert.Equal(t, "sidecar assistant reply", msgs[1].Content)
assert.True(t, msgs[1].HasThinking)
require.Len(t, msgs[1].ToolCalls, 1)
assert.Equal(t, "tc-1", msgs[1].ToolCalls[0].ToolUseID)
assert.Equal(t, "sidecar prompt", sess.FirstMessage)
assert.Equal(t, TranscriptFidelityFull, sess.TranscriptFidelity)
}
func TestAntigravityIDEHeuristicFallbackWhenSidecarMissing(t *testing.T) {
root := t.TempDir()
id := "2a2a2a2a-3b3b-4c4c-5d5d-6e6e6e6e6e6e"
mustMkdir(t, filepath.Join(root, "conversations"))
dbPath := filepath.Join(root, "conversations", id+".db")
createAntigravityTestDB(t, dbPath) // 2 raw steps, no sidecar
sess, msgs, _, err := parseAntigravityTestSession(t, dbPath, "", "test-machine")
require.NoError(t, err)
require.Len(t, msgs, 2)
assert.Equal(t, "user prompt text goes here", msgs[0].Content)
assert.Contains(t, msgs[1].Content, "assistant reply content body")
// Fidelity unchanged from prior IDE behavior: empty (treated as full).
assert.Equal(t, "", sess.TranscriptFidelity)
}
func TestAntigravityIDEKeepsDBDecodeWhenSidecarLags(t *testing.T) {
root := t.TempDir()
id := "3a3a3a3a-4b4b-5c5c-6d6d-7e7e7e7e7e7e"
mustMkdir(t, filepath.Join(root, "conversations"))
dbPath := filepath.Join(root, "conversations", id+".db")
createAntigravityTestDB(t, dbPath) // 2 raw steps
// Sidecar covers only 1 of 2 steps: a live session agy-reader has
// not caught up with yet. The fuller heuristic decode must win.
writeAntigravityTestSidecar(t, root, id, 1)
sess, msgs, _, err := parseAntigravityTestSession(t, dbPath, "", "test-machine")
require.NoError(t, err)
require.Len(t, msgs, 2)
assert.Equal(t, "user prompt text goes here", msgs[0].Content)
assert.Contains(t, msgs[1].Content, "assistant reply content body")
assert.Equal(t, "", sess.TranscriptFidelity)
}
func TestAntigravityIDEMalformedSidecarFallsBack(t *testing.T) {
root := t.TempDir()
id := "4a4a4a4a-5b5b-6c6c-7d7d-8e8e8e8e8e8e"
mustMkdir(t, filepath.Join(root, "conversations"))
dbPath := filepath.Join(root, "conversations", id+".db")
createAntigravityTestDB(t, dbPath) // 2 raw steps
mustWrite(t,
filepath.Join(root, "conversations", id+".trajectory.json"),
[]byte(`{not valid json`))
sess, msgs, _, err := parseAntigravityTestSession(t, dbPath, "", "test-machine")
require.NoError(t, err)
require.Len(t, msgs, 2)
assert.Equal(t, "user prompt text goes here", msgs[0].Content)
assert.Contains(t, msgs[1].Content, "assistant reply content body")
assert.Equal(t, "", sess.TranscriptFidelity)
}
// TestAntigravityIDESidecarWinsKeepsGenMetadataUsage verifies the
// gen_metadata-wins merge: a covering sidecar replaces the transcript,
// but usage still comes from the .db gen_metadata (source "generation")
// rather than being double counted from the sidecar.
func TestAntigravityIDESidecarWinsKeepsGenMetadataUsage(t *testing.T) {
root := t.TempDir()
id := "5a5a5a5a-6b6b-7c7c-8d8d-9e9e9e9e9e9e"
mustMkdir(t, filepath.Join(root, "conversations"))
dbPath := filepath.Join(root, "conversations", id+".db")
db, err := sql.Open("sqlite3", dbPath)
require.NoError(t, err)
createAntigravityStepTables(t, db)
mustExec(t, db, `CREATE TABLE gen_metadata (idx integer, data blob, size integer, PRIMARY KEY (idx))`)
tsEarly := encodePB([]pbField{{num: 1, wire: pbWireVarint, varint: 1779000000}})
userPayload := encodePB([]pbField{
{num: 5, wire: pbWireBytes, bytes: tsEarly},
{num: 17, wire: pbWireBytes, bytes: []byte("db user prompt that is long enough")},
})
tsLate := encodePB([]pbField{{num: 1, wire: pbWireVarint, varint: 1779000100}})
asstPayload := encodePB([]pbField{
{num: 5, wire: pbWireBytes, bytes: tsLate},
{num: 17, wire: pbWireBytes, bytes: []byte("db assistant reply that is long enough")},
})
mustExec(t, db, `INSERT INTO steps (idx, step_type, step_payload) VALUES (0, 14, ?)`, userPayload)
mustExec(t, db, `INSERT INTO steps (idx, step_type, step_payload) VALUES (1, 17, ?)`, asstPayload)
genData := createAntigravityMockGenMetadata(t, 2400, 180, 0, "Test Gemini 3.5")
mustExec(t, db, `INSERT INTO gen_metadata (idx, data, size) VALUES (1, ?, ?)`, genData, len(genData))
require.NoError(t, db.Close())
// Covering sidecar with DIFFERENT generatorMetadata usage; the .db
// gen_metadata event must still win so the generation is counted once.
genJSON := `[{
"stepIndices": [1],
"chatModel": {
"model": "MODEL_PLACEHOLDER_M20",
"usage": {"inputTokens": "9999", "outputTokens": "888"}
}
}]`
writeAntigravityTestSidecarWithGenMetadata(t, root, id, 2, genJSON)
sess, msgs, usageEvents, err := parseAntigravityTestSession(t, dbPath, "", "test-machine")
require.NoError(t, err)
// Sidecar transcript wins.
require.Len(t, msgs, 2)
assert.Equal(t, "sidecar prompt", msgs[0].Content)
assert.Equal(t, TranscriptFidelityFull, sess.TranscriptFidelity)
// Exactly one usage event, from the .db gen_metadata (not the sidecar).
require.Len(t, usageEvents, 1)
assert.Equal(t, "generation", usageEvents[0].Source)
assert.Equal(t, 2400, usageEvents[0].InputTokens)
assert.Equal(t, 180, usageEvents[0].OutputTokens)
assert.Equal(t, sess.ID, usageEvents[0].SessionID)
}
// TestAntigravityIDEWithoutGenMetadataGetsSidecarUsage verifies the
// gap-fill: when the .db has no gen_metadata, a covering sidecar's
// generatorMetadata supplies usage events (source "sidecar").
func TestAntigravityIDEWithoutGenMetadataGetsSidecarUsage(t *testing.T) {
root := t.TempDir()
id := "6a6a6a6a-7b7b-8c8c-9d9d-aeaeaeaeaeae"
mustMkdir(t, filepath.Join(root, "conversations"))
dbPath := filepath.Join(root, "conversations", id+".db")
createAntigravityTestDB(t, dbPath) // 2 raw steps, no gen_metadata table
genJSON := `[{
"stepIndices": [1],
"chatModel": {
"model": "MODEL_PLACEHOLDER_M20",
"usage": {
"inputTokens": "1500",
"outputTokens": "77",
"thinkingOutputTokens": "20",
"cacheReadTokens": "300"
}
}
}]`
writeAntigravityTestSidecarWithGenMetadata(t, root, id, 2, genJSON)
sess, msgs, usageEvents, err := parseAntigravityTestSession(t, dbPath, "", "test-machine")
require.NoError(t, err)
require.NotEmpty(t, msgs)
assert.Equal(t, "sidecar prompt", msgs[0].Content)
assert.Equal(t, TranscriptFidelityFull, sess.TranscriptFidelity)
require.Len(t, usageEvents, 1)
assert.Equal(t, "sidecar", usageEvents[0].Source)
assert.Equal(t, "MODEL_PLACEHOLDER_M20", usageEvents[0].Model)
assert.Equal(t, 1500, usageEvents[0].InputTokens)
assert.Equal(t, 77, usageEvents[0].OutputTokens)
assert.Equal(t, 20, usageEvents[0].ReasoningTokens)
assert.Equal(t, 300, usageEvents[0].CacheReadInputTokens)
assert.Equal(t, sess.ID, usageEvents[0].SessionID)
}
// TestAntigravityIDENonCoveringSidecarUsageRejected verifies that a
// lagging sidecar loses both the transcript and its usage: the usage
// gap-fill is gated on the same coverage check as the transcript.
func TestAntigravityIDENonCoveringSidecarUsageRejected(t *testing.T) {
root := t.TempDir()
id := "7a7a7a7a-8b8b-9c9c-adad-bebebebebebe"
mustMkdir(t, filepath.Join(root, "conversations"))
dbPath := filepath.Join(root, "conversations", id+".db")
createAntigravityTestDB(t, dbPath) // 2 raw steps, no gen_metadata table
genJSON := `[{
"stepIndices": [1],
"chatModel": {
"model": "MODEL_PLACEHOLDER_M20",
"usage": {"inputTokens": "1500", "outputTokens": "77"}
}
}]`
// Sidecar covers only 1 of 2 raw steps.
writeAntigravityTestSidecarWithGenMetadata(t, root, id, 1, genJSON)
sess, msgs, usageEvents, err := parseAntigravityTestSession(t, dbPath, "", "test-machine")
require.NoError(t, err)
// Heuristic decode wins; sidecar usage rejected like its transcript.
require.Len(t, msgs, 2)
assert.Equal(t, "user prompt text goes here", msgs[0].Content)
assert.Equal(t, "", sess.TranscriptFidelity)
assert.Empty(t, usageEvents,
"non-covering sidecar usage must be rejected like its transcript")
}
// TestAntigravityIDEUnreadableStepsWithoutSidecarStillFails verifies the
// step-load error is still surfaced when no sidecar can rescue the
// session, preserving prior behavior.
func TestAntigravityIDEUnreadableStepsWithoutSidecarStillFails(t *testing.T) {
root := t.TempDir()
id := "9a9a9a9a-abab-bcbc-cdcd-dededededede"
mustMkdir(t, filepath.Join(root, "conversations"))
dbPath := filepath.Join(root, "conversations", id+".db")
db, err := sql.Open("sqlite3", dbPath)
require.NoError(t, err)
mustExec(t, db, `CREATE TABLE trajectory_meta (
trajectory_id text, PRIMARY KEY (trajectory_id))`)
require.NoError(t, db.Close())
_, _, _, err = parseAntigravityTestSession(t, dbPath, "", "test-machine")
require.Error(t, err,
"unreadable steps without a rescuing sidecar must fail the parse")
assert.Contains(t, err.Error(), "steps")
}
// TestAntigravityIDEUnreadableStepsFailsClosed verifies that a
// step-load failure always surfaces the error, no matter what other
// data exists -- including a covering displayable sidecar: coverage is
// unprovable without the DB's step count, and the sync engine
// unconditionally full-replaces stored Antigravity messages on any
// successful parse, so persisting sidecar, usage-only, or brain-only
// content would clobber a previously stored full transcript whenever
// the steps query fails transiently.
func TestAntigravityIDEUnreadableStepsFailsClosed(t *testing.T) {
makeNoStepsDB := func(t *testing.T, root, id string) string {
t.Helper()
mustMkdir(t, filepath.Join(root, "conversations"))
dbPath := filepath.Join(root, "conversations", id+".db")
db, err := sql.Open("sqlite3", dbPath)
require.NoError(t, err)
mustExec(t, db, `CREATE TABLE trajectory_meta (
trajectory_id text, PRIMARY KEY (trajectory_id))`)
require.NoError(t, db.Close())
return dbPath
}
t.Run("covering displayable sidecar does not rescue", func(t *testing.T) {
root := t.TempDir()
id := "8a8a8a8a-9b9b-acac-bdbd-cececececece"
dbPath := makeNoStepsDB(t, root, id)
// A displayable sidecar that would win over a readable DB. With
// the steps query failing, coverage is unprovable (the sidecar
// may lag a live session) and this provider force-replaces on
// success, so a rescue could overwrite a previously complete
// stored transcript. The parse must fail instead.
writeAntigravityTestSidecar(t, root, id, 2)
sess, msgs, usageEvents, err := parseAntigravityTestSession(
t, dbPath, "", "test-machine",
)
require.Error(t, err,
"a covering displayable sidecar must not rescue an unreadable DB")
assert.Contains(t, err.Error(), "steps")
assert.Nil(t, sess)
assert.Empty(t, msgs)
assert.Empty(t, usageEvents)
})
t.Run("usage-only sidecar does not persist a degraded row", func(t *testing.T) {
root := t.TempDir()
id := "1b1b1b1b-2c2c-3d3d-4e4e-5f5f5f5f5f5f"
dbPath := makeNoStepsDB(t, root, id)
// Sidecar with zero steps: no displayable transcript, only
// generatorMetadata usage. It must NOT rescue the parse.
genJSON := `[{
"stepIndices": [1],
"chatModel": {
"model": "MODEL_PLACEHOLDER_M20",
"usage": {"inputTokens": "1500", "outputTokens": "77"}
}
}]`
writeAntigravityTestSidecarWithGenMetadata(t, root, id, 0, genJSON)
sess, msgs, usageEvents, err := parseAntigravityTestSession(
t, dbPath, "", "test-machine",
)
require.Error(t, err,
"usage-only sidecar must not persist a degraded row")
assert.Contains(t, err.Error(), "steps")
assert.Nil(t, sess)
assert.Empty(t, msgs)
assert.Empty(t, usageEvents)
})
t.Run("brain artifacts do not persist a degraded row", func(t *testing.T) {
root := t.TempDir()
id := "3b3b3b3b-4c4c-5d5d-6e6e-7f7f7f7f7f7f"
dbPath := makeNoStepsDB(t, root, id)
mustMkdir(t, filepath.Join(root, "brain", id))
mustWrite(t, filepath.Join(root, "brain", id, "plan.md"),
[]byte("# Recovered plan"))
sess, msgs, usageEvents, err := parseAntigravityTestSession(
t, dbPath, "", "test-machine",
)
require.Error(t, err,
"brain-only data must not persist a degraded row")
assert.Contains(t, err.Error(), "steps")
assert.Nil(t, sess)
assert.Empty(t, msgs)
assert.Empty(t, usageEvents)
})
t.Run("gen_metadata usage does not persist a degraded row", func(t *testing.T) {
root := t.TempDir()
id := "4b4b4b4b-5c5c-6d6d-7e7e-8f8f8f8f8f8f"
dbPath := makeNoStepsDB(t, root, id)
// Readable gen_metadata with decodable usage, no sidecar: the
// usage alone must not persist a degraded row either.
db, err := sql.Open("sqlite3", dbPath)
require.NoError(t, err)
mustExec(t, db, `CREATE TABLE gen_metadata (
idx integer, data blob, size integer, PRIMARY KEY (idx))`)
genData := createAntigravityMockGenMetadata(t, 2400, 180, 0, "Test Gemini 3.5")
mustExec(t, db,
`INSERT INTO gen_metadata (idx, data, size) VALUES (1, ?, ?)`,
genData, len(genData))
require.NoError(t, db.Close())
sess, msgs, usageEvents, err := parseAntigravityTestSession(
t, dbPath, "", "test-machine",
)
require.Error(t, err,
"gen_metadata usage alone must not persist a degraded row")
assert.Contains(t, err.Error(), "steps")
assert.Nil(t, sess)
assert.Empty(t, msgs)
assert.Empty(t, usageEvents)
})
}