Files
2026-07-13 12:26:02 +08:00

609 lines
22 KiB
TypeScript

import { mkdtemp, mkdir, readFile, rm, writeFile } from 'fs/promises'
import { tmpdir } from 'os'
import { join } from 'path'
import { createRequire } from 'node:module'
import { describe, expect, it } from 'vitest'
import { isSqliteAvailable } from '../../src/sqlite.js'
import {
antigravityAppDataDirFromSourcePath,
antigravityCascadeIdFromPath,
createAntigravityProvider,
discoverAntigravitySessionSources,
extractAntigravityAppDataDirFromLine,
extractAntigravityGeneratorMetadata,
extractAntigravityModelMap,
getAntigravityStatusLineEventsPath,
parseAntigravityServerInfo,
parseAntigravityServerInfoFromLine,
recordAntigravityStatusLinePayload,
shouldReparseAntigravitySource,
} from '../../src/providers/antigravity.js'
import type { ParsedProviderCall } from '../../src/providers/types.js'
const requireForTest = createRequire(import.meta.url)
type CurrentCliFixture = {
conversationId: string
rows: Array<{ idx: number; hex: string }>
}
type TestDb = {
exec(sql: string): void
prepare(sql: string): { run(...params: unknown[]): void }
close(): void
}
function createCurrentAntigravityCliDb(dbPath: string, fixture: CurrentCliFixture): void {
const { DatabaseSync: Database } = requireForTest('node:sqlite')
const db = new Database(dbPath) as TestDb
try {
db.exec('CREATE TABLE gen_metadata (idx integer, data blob, size integer NOT NULL DEFAULT 0, PRIMARY KEY (idx))')
db.exec('CREATE TABLE trajectory_metadata_blob (id text DEFAULT "main", data blob, PRIMARY KEY (id))')
db.prepare('INSERT INTO trajectory_metadata_blob (id, data) VALUES (?, ?)').run(
'main',
Buffer.from('file:///Users/example/private-project'),
)
for (const row of fixture.rows) {
const data = Buffer.from(row.hex, 'hex')
db.prepare('INSERT INTO gen_metadata (idx, data, size) VALUES (?, ?, ?)').run(row.idx, data, data.length)
}
} finally {
db.close()
}
}
async function collectAntigravityCalls(source: { path: string; project: string; provider: string }): Promise<ParsedProviderCall[]> {
const parser = createAntigravityProvider().createSessionParser(source, new Set())
const calls: ParsedProviderCall[] = []
for await (const call of parser.parse()) calls.push(call)
return calls
}
describe('antigravity provider helpers', () => {
it('parses legacy https server flags from POSIX process args', () => {
const server = parseAntigravityServerInfoFromLine(
'/Applications/Antigravity.app/language_server_macos_arm --app_data_dir antigravity --https_server_port 57101 --csrf_token 01234567-89ab-cdef-0123-456789abcdef',
)
expect(server).toEqual({
port: 57101,
csrfToken: '01234567-89ab-cdef-0123-456789abcdef',
})
})
it('parses Windows extension server flags and equals syntax', () => {
const server = parseAntigravityServerInfoFromLine(
'C:\\Users\\Admin\\AppData\\Local\\Programs\\Antigravity\\resources\\app\\extensions\\antigravity\\bin\\language_server_windows_x64.exe --extension_server_port=62225 --extension_server_csrf_token=abcdef01-2345-6789-abcd-ef0123456789',
)
expect(server).toEqual({
port: 62225,
csrfToken: 'abcdef01-2345-6789-abcd-ef0123456789',
})
})
it('parses Windows extension server flags and space syntax', () => {
const server = parseAntigravityServerInfo([
'node something-unrelated',
'language_server_windows_x64.exe --app_data_dir C:\\Users\\Admin\\.gemini\\antigravity --extension_server_port 62300 --extension_server_csrf_token fedcba98-7654-3210-fedc-ba9876543210',
])
expect(server).toEqual({
port: 62300,
csrfToken: 'fedcba98-7654-3210-fedc-ba9876543210',
})
})
it('parses quoted flag values', () => {
const server = parseAntigravityServerInfoFromLine(
'Antigravity language_server_windows_x64.exe --extension_server_port "62301" --extension_server_csrf_token "fedcba98-7654-3210-fedc-ba9876543211"',
)
expect(server).toEqual({
port: 62301,
csrfToken: 'fedcba98-7654-3210-fedc-ba9876543211',
})
})
it('normalizes app_data_dir from app and CLI process args', () => {
expect(extractAntigravityAppDataDirFromLine(
'language_server --app_data_dir antigravity --https_server_port 0 --csrf_token 01234567-89ab-cdef-0123-456789abcdef',
)).toBe('antigravity')
expect(extractAntigravityAppDataDirFromLine(
'language_server --app_data_dir /Users/dev/.gemini/antigravity-cli --https_server_port 0 --csrf_token 01234567-89ab-cdef-0123-456789abcdef',
)).toBe('antigravity-cli')
expect(extractAntigravityAppDataDirFromLine(
'language_server.exe --app_data_dir "C:\\Users\\Admin\\.gemini\\antigravity-cli" --extension_server_port 62225 --extension_server_csrf_token abcdef01-2345-6789-abcd-ef0123456789',
)).toBe('antigravity-cli')
expect(extractAntigravityAppDataDirFromLine(
'language_server_windows_x64.exe --app_data_dir antigravity-ide --extension_server_port 8720 --extension_server_csrf_token 39800f1b-343a-40b0-8eb5-850702450346',
)).toBe('antigravity-ide')
})
it('accepts Antigravity 2 ephemeral port zero', () => {
const server = parseAntigravityServerInfoFromLine(
'antigravity language_server_macos_arm --https_server_port 0 --csrf_token 01234567-89ab-cdef-0123-456789abcdef',
)
expect(server).toEqual({
port: 0,
csrfToken: '01234567-89ab-cdef-0123-456789abcdef',
})
})
it('matches language-server and antigravity markers case-insensitively', () => {
const server = parseAntigravityServerInfoFromLine(
'ANTIGRAVITY LANGUAGE_SERVER_WINDOWS_X64.EXE --extension_server_port 62302 --extension_server_csrf_token fedcba98-7654-3210-fedc-ba9876543212',
)
expect(server).toEqual({
port: 62302,
csrfToken: 'fedcba98-7654-3210-fedc-ba9876543212',
})
})
it('ignores process args without an antigravity marker', () => {
expect(parseAntigravityServerInfoFromLine(
'language_server --extension_server_port 62300 --extension_server_csrf_token fedcba98-7654-3210-fedc-ba9876543210',
)).toBeNull()
})
it('ignores invalid ports', () => {
expect(parseAntigravityServerInfoFromLine(
'antigravity language_server --extension_server_port 99999 --extension_server_csrf_token fedcba98-7654-3210-fedc-ba9876543210',
)).toBeNull()
})
it('ignores chained flag names as values', () => {
expect(parseAntigravityServerInfoFromLine(
'antigravity language_server --extension_server_port=--extension_server_csrf_token --extension_server_csrf_token fedcba98-7654-3210-fedc-ba9876543210',
)).toBeNull()
})
it('ignores implausibly short CSRF tokens', () => {
expect(parseAntigravityServerInfoFromLine(
'antigravity language_server --extension_server_port 62300 --extension_server_csrf_token short',
)).toBeNull()
})
it('extracts model maps from wrapped and unwrapped RPC responses', () => {
expect(extractAntigravityModelMap({
response: { models: { high: { model: 'MODEL_PLACEHOLDER_M7' } } },
})).toEqual({ MODEL_PLACEHOLDER_M7: 'high' })
expect(extractAntigravityModelMap({
models: { low: { model: 'MODEL_PLACEHOLDER_M8' } },
})).toEqual({ MODEL_PLACEHOLDER_M8: 'low' })
expect(extractAntigravityModelMap({
models: { bad: null, good: { model: 'MODEL_PLACEHOLDER_M9' } },
})).toEqual({ MODEL_PLACEHOLDER_M9: 'good' })
expect(extractAntigravityModelMap({
models: { 'gemini-3-flash-agent': { model: 'MODEL_PLACEHOLDER_M133', displayName: 'Gemini 3.5 Flash (High)' } },
})).toEqual({ MODEL_PLACEHOLDER_M133: 'gemini-3.5-flash-high' })
expect(extractAntigravityModelMap(null)).toEqual({})
})
it('extracts generator metadata from wrapped and unwrapped RPC responses', () => {
const metadata = [{
chatModel: {
model: 'gemini-3-pro',
usage: {
model: 'gemini-3-pro',
inputTokens: '10',
outputTokens: '4',
apiProvider: 'google',
},
},
}]
expect(extractAntigravityGeneratorMetadata({ response: { generatorMetadata: metadata } })).toEqual(metadata)
expect(extractAntigravityGeneratorMetadata({ generatorMetadata: metadata })).toEqual(metadata)
expect(extractAntigravityGeneratorMetadata({ response: { generatorMetadata: null } })).toEqual([])
expect(extractAntigravityGeneratorMetadata(null)).toEqual([])
})
it('derives cascade ids from legacy .pb and Antigravity 2 .db files', () => {
expect(antigravityCascadeIdFromPath('/tmp/123.pb')).toBe('123')
expect(antigravityCascadeIdFromPath('/tmp/456.db')).toBe('456')
expect(antigravityCascadeIdFromPath('/tmp/789.db-wal')).toBe('789.db-wal')
})
it('routes app and CLI source paths to matching Antigravity app data dirs', () => {
expect(antigravityAppDataDirFromSourcePath(
'/Users/dev/.gemini/antigravity/conversations/session.db',
)).toBe('antigravity')
expect(antigravityAppDataDirFromSourcePath(
'/Users/dev/.gemini/antigravity-cli/conversations/session.pb',
)).toBe('antigravity-cli')
expect(antigravityAppDataDirFromSourcePath(
'C:\\Users\\Admin\\.gemini\\antigravity-cli\\implicit\\session.pb',
)).toBe('antigravity-cli')
expect(antigravityAppDataDirFromSourcePath(
'/Users/dev/.gemini/antigravity-ide/conversations/session.db',
)).toBe('antigravity-ide')
expect(antigravityAppDataDirFromSourcePath(
'C:\\Users\\Admin\\.gemini\\antigravity-ide\\implicit\\session.pb',
)).toBe('antigravity-ide')
})
it('discovers legacy .pb files and Antigravity 2 .db files only', async () => {
const dir = await mkdtemp(join(tmpdir(), 'codeburn-antigravity-'))
try {
await writeFile(join(dir, 'legacy.pb'), '')
await writeFile(join(dir, 'antigravity-2.db'), '')
await writeFile(join(dir, 'uppercase.DB'), '')
await writeFile(join(dir, 'antigravity-2.db-wal'), '')
await mkdir(join(dir, 'directory.pb'))
const sources = await discoverAntigravitySessionSources([{
dir,
project: 'test-project',
extensions: ['.pb', '.db'],
}])
expect(sources).toEqual([
{ path: join(dir, 'antigravity-2.db'), project: 'test-project', provider: 'antigravity' },
{ path: join(dir, 'legacy.pb'), project: 'test-project', provider: 'antigravity' },
{ path: join(dir, 'uppercase.DB'), project: 'test-project', provider: 'antigravity' },
])
} finally {
await rm(dir, { recursive: true, force: true })
}
})
it('discovers antigravity-ide conversation and implicit files', async () => {
const tempHome = await mkdtemp(join(tmpdir(), 'codeburn-home-'))
const conversationsDir = join(tempHome, '.gemini', 'antigravity-ide', 'conversations')
const implicitDir = join(tempHome, '.gemini', 'antigravity-ide', 'implicit')
await mkdir(conversationsDir, { recursive: true })
await mkdir(implicitDir, { recursive: true })
await writeFile(join(conversationsDir, 'session1.db'), '')
await writeFile(join(implicitDir, 'session2.pb'), '')
const roots = [
{
dir: conversationsDir,
project: 'antigravity-ide',
extensions: ['.pb', '.db'] as const,
},
{
dir: implicitDir,
project: 'antigravity-ide',
extensions: ['.pb'] as const,
},
]
const sources = await discoverAntigravitySessionSources(roots)
expect(sources).toEqual([
{ path: join(conversationsDir, 'session1.db'), project: 'antigravity-ide', provider: 'antigravity' },
{ path: join(implicitDir, 'session2.pb'), project: 'antigravity-ide', provider: 'antigravity' },
])
await rm(tempHome, { recursive: true, force: true })
})
it('displays Gemini 3.5 Flash thinking variants as the base model', () => {
const provider = createAntigravityProvider()
expect(provider.modelDisplayName('gemini-3.5-flash')).toBe('Gemini 3.5 Flash')
expect(provider.modelDisplayName('gemini-3.5-flash-high')).toBe('Gemini 3.5 Flash')
expect(provider.modelDisplayName('gemini-3.5-flash-medium')).toBe('Gemini 3.5 Flash')
expect(provider.modelDisplayName('gemini-3.5-flash-low')).toBe('Gemini 3.5 Flash')
expect(provider.modelDisplayName('Gemini 3.5 Flash (High)')).toBe('Gemini 3.5 Flash')
})
it('captures exact Antigravity CLI statusLine usage as fallback calls', async () => {
const dir = await mkdtemp(join(tmpdir(), 'codeburn-antigravity-statusline-'))
process.env['CODEBURN_CACHE_DIR'] = dir
try {
const payload = {
conversation_id: 'ce061468-2e2b-4c6f-bf4f-e072bd5fa986',
session_id: 'session-1',
cwd: '/workspace/project',
model: {
id: 'Gemini 3.5 Flash (High)',
display_name: 'Gemini 3.5 Flash (High)',
},
context_window: {
current_usage: {
input_tokens: 28407,
output_tokens: 137,
cache_creation_input_tokens: 0,
cache_read_input_tokens: 0,
},
},
}
expect(await recordAntigravityStatusLinePayload(payload)).toBe(true)
expect(await recordAntigravityStatusLinePayload(payload)).toBe(true)
const recorded = await readFile(getAntigravityStatusLineEventsPath(), 'utf-8')
expect(recorded).not.toContain('/workspace/project')
expect(JSON.parse(recorded.split(/\r?\n/)[0]!)).not.toHaveProperty('cwd')
const source = {
path: getAntigravityStatusLineEventsPath(),
project: 'antigravity-cli',
provider: 'antigravity',
}
const parser = createAntigravityProvider().createSessionParser(source, new Set())
const calls = []
for await (const call of parser.parse()) calls.push(call)
expect(calls).toHaveLength(1)
expect(calls[0]).toMatchObject({
provider: 'antigravity',
model: 'Gemini 3.5 Flash (High)',
inputTokens: 28407,
outputTokens: 137,
cacheCreationInputTokens: 0,
cacheReadInputTokens: 0,
cachedInputTokens: 0,
sessionId: 'ce061468-2e2b-4c6f-bf4f-e072bd5fa986',
project: 'antigravity-cli',
})
expect(calls[0]!.projectPath).toBeUndefined()
expect(calls[0]!.costUSD).toBeGreaterThan(0)
} finally {
await rm(dir, { recursive: true, force: true })
}
})
it('skips statusLine fallback calls when RPC cache already covered the conversation', async () => {
const dir = await mkdtemp(join(tmpdir(), 'codeburn-antigravity-statusline-rpc-dedup-'))
process.env['CODEBURN_CACHE_DIR'] = dir
try {
expect(await recordAntigravityStatusLinePayload({
conversation_id: 'rpc-covered-conversation',
session_id: 'session-1',
model: 'Gemini 3.5 Flash (High)',
context_window: {
current_usage: {
input_tokens: 1000,
output_tokens: 100,
cache_creation_input_tokens: 0,
cache_read_input_tokens: 0,
},
},
})).toBe(true)
const parser = createAntigravityProvider().createSessionParser({
path: getAntigravityStatusLineEventsPath(),
project: 'antigravity-cli',
provider: 'antigravity',
}, new Set(['antigravity:rpc-covered-conversation:0']))
const calls = []
for await (const call of parser.parse()) calls.push(call)
expect(calls).toEqual([])
} finally {
await rm(dir, { recursive: true, force: true })
}
})
it('skips singleton statusLine snapshots and deltas monotonic usage', async () => {
const dir = await mkdtemp(join(tmpdir(), 'codeburn-antigravity-statusline-runs-'))
process.env['CODEBURN_CACHE_DIR'] = dir
const basePayload = {
conversation_id: 'statusline-runs',
session_id: 'session-1',
model: 'Gemini 3.5 Flash (High)',
}
const withUsage = (
input_tokens: number,
output_tokens: number,
cache_read_input_tokens = 0,
) => ({
...basePayload,
context_window: {
current_usage: {
input_tokens,
output_tokens,
cache_creation_input_tokens: 0,
cache_read_input_tokens,
},
},
})
try {
expect(await recordAntigravityStatusLinePayload(withUsage(100, 10))).toBe(true)
expect(await recordAntigravityStatusLinePayload(withUsage(200, 20))).toBe(true)
expect(await recordAntigravityStatusLinePayload(withUsage(200, 20))).toBe(true)
expect(await recordAntigravityStatusLinePayload(withUsage(300, 30, 50))).toBe(true)
const parser = createAntigravityProvider().createSessionParser({
path: getAntigravityStatusLineEventsPath(),
project: 'antigravity-cli',
provider: 'antigravity',
}, new Set())
const calls = []
for await (const call of parser.parse()) calls.push(call)
expect(calls).toHaveLength(2)
expect(calls.map(call => [call.inputTokens, call.outputTokens, call.cacheReadInputTokens])).toEqual([
[200, 20, 0],
[100, 10, 50],
])
expect(calls.map(call => call.cachedInputTokens)).toEqual([0, 0])
} finally {
await rm(dir, { recursive: true, force: true })
}
})
it('treats non-monotonic statusLine usage as a new request snapshot', async () => {
const dir = await mkdtemp(join(tmpdir(), 'codeburn-antigravity-statusline-reset-'))
process.env['CODEBURN_CACHE_DIR'] = dir
const payload = (
input_tokens: number,
output_tokens: number,
cache_read_input_tokens = 0,
) => ({
conversation_id: 'statusline-reset',
session_id: 'session-1',
model: 'Gemini 3.5 Flash (High)',
context_window: {
current_usage: {
input_tokens,
output_tokens,
cache_creation_input_tokens: 0,
cache_read_input_tokens,
},
},
})
try {
expect(await recordAntigravityStatusLinePayload(payload(1000, 100))).toBe(true)
expect(await recordAntigravityStatusLinePayload(payload(1000, 100))).toBe(true)
expect(await recordAntigravityStatusLinePayload(payload(200, 30, 500))).toBe(true)
const parser = createAntigravityProvider().createSessionParser({
path: getAntigravityStatusLineEventsPath(),
project: 'antigravity-cli',
provider: 'antigravity',
}, new Set())
const calls = []
for await (const call of parser.parse()) calls.push(call)
expect(calls).toHaveLength(2)
expect(calls.map(call => [call.inputTokens, call.outputTokens, call.cacheReadInputTokens])).toEqual([
[1000, 100, 0],
[200, 30, 500],
])
} finally {
await rm(dir, { recursive: true, force: true })
}
})
it('always reparses append-only statusLine sources but not unchanged cached cascades', () => {
const statusLinePath = getAntigravityStatusLineEventsPath()
expect(shouldReparseAntigravitySource(statusLinePath, 1)).toBe(true)
expect(shouldReparseAntigravitySource('/tmp/antigravity/conversation.pb', 0)).toBe(true)
expect(shouldReparseAntigravitySource('/tmp/antigravity/conversation.pb', 1)).toBe(false)
})
it('parses current Antigravity CLI SQLite conversations with non-zero token usage', async () => {
if (!isSqliteAvailable()) return
const tempHome = await mkdtemp(join(tmpdir(), 'codeburn-antigravity-current-cli-'))
const cacheDir = join(tempHome, 'cache')
const previousCacheDir = process.env['CODEBURN_CACHE_DIR']
process.env['CODEBURN_CACHE_DIR'] = cacheDir
try {
const fixture = JSON.parse(await readFile(
new URL('../fixtures/antigravity-cli-current/gen-metadata.json', import.meta.url),
'utf-8',
)) as CurrentCliFixture
const conversationsDir = join(tempHome, '.gemini', 'antigravity-cli', 'conversations')
const logsDir = join(
tempHome,
'.gemini',
'antigravity-cli',
'brain',
fixture.conversationId,
'.system_generated',
'logs',
)
await mkdir(conversationsDir, { recursive: true })
await mkdir(logsDir, { recursive: true })
await writeFile(
join(logsDir, 'transcript.jsonl'),
await readFile(
new URL(
'../fixtures/antigravity-cli-current/brain/fixture-current-cli/.system_generated/logs/transcript.jsonl',
import.meta.url,
),
'utf-8',
),
)
const dbPath = join(conversationsDir, `${fixture.conversationId}.db`)
createCurrentAntigravityCliDb(dbPath, fixture)
const sources = await discoverAntigravitySessionSources([{
dir: conversationsDir,
project: 'antigravity-cli',
extensions: ['.pb', '.db'],
}])
expect(sources).toEqual([{ path: dbPath, project: 'antigravity-cli', provider: 'antigravity' }])
const calls = await collectAntigravityCalls(sources[0]!)
expect(calls.length).toBeGreaterThanOrEqual(1)
expect(calls[0]).toMatchObject({
provider: 'antigravity',
model: 'gemini-3.1-pro-high',
inputTokens: 30265,
outputTokens: 659,
reasoningTokens: 71,
sessionId: fixture.conversationId,
project: 'antigravity-cli',
})
expect(calls[0]!.projectPath).toBeUndefined()
expect(calls[0]!.costUSD).toBeGreaterThan(0)
} finally {
if (previousCacheDir === undefined) delete process.env['CODEBURN_CACHE_DIR']
else process.env['CODEBURN_CACHE_DIR'] = previousCacheDir
await rm(tempHome, { recursive: true, force: true })
}
})
it('deduplicates current SQLite rows against RPC response ids with hyphens', async () => {
if (!isSqliteAvailable()) return
const tempHome = await mkdtemp(join(tmpdir(), 'codeburn-antigravity-current-cli-dedup-'))
const cacheDir = join(tempHome, 'cache')
const previousCacheDir = process.env['CODEBURN_CACHE_DIR']
process.env['CODEBURN_CACHE_DIR'] = cacheDir
try {
const fixture = JSON.parse(await readFile(
new URL('../fixtures/antigravity-cli-current/gen-metadata.json', import.meta.url),
'utf-8',
)) as CurrentCliFixture
const conversationsDir = join(tempHome, '.gemini', 'antigravity-cli', 'conversations')
await mkdir(conversationsDir, { recursive: true })
const dbPath = join(conversationsDir, `${fixture.conversationId}.db`)
createCurrentAntigravityCliDb(dbPath, fixture)
const parser = createAntigravityProvider().createSessionParser({
path: dbPath,
project: 'antigravity-cli',
provider: 'antigravity',
}, new Set([`antigravity:${fixture.conversationId}:fixture-response-1`]))
const calls = []
for await (const call of parser.parse()) calls.push(call)
expect(calls).toEqual([])
} finally {
if (previousCacheDir === undefined) delete process.env['CODEBURN_CACHE_DIR']
else process.env['CODEBURN_CACHE_DIR'] = previousCacheDir
await rm(tempHome, { recursive: true, force: true })
}
})
})