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

359 lines
11 KiB
TypeScript

import { mkdtemp, rm } from 'fs/promises'
import { mkdirSync } from 'fs'
import { join } from 'path'
import { tmpdir } from 'os'
import { createRequire } from 'node:module'
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
import { createWarpProvider } from '../../src/providers/warp.js'
import { isSqliteAvailable } from '../../src/sqlite.js'
import type { ParsedProviderCall } from '../../src/providers/types.js'
const requireForTest = createRequire(import.meta.url)
type TestDb = {
exec(sql: string): void
prepare(sql: string): { run(...params: unknown[]): void }
close(): void
}
type QueryFixture = {
exchangeId: string
conversationId: string
startTs: string
input: string
outputStatus?: string
modelId?: string
workingDirectory?: string | null
}
type BlockFixture = {
blockId: string
conversationId: string
startTs: string
completedTs: string
exitCode: number
command: string
}
let tmpDir: string
beforeEach(async () => {
tmpDir = await mkdtemp(join(tmpdir(), 'warp-provider-test-'))
})
afterEach(async () => {
await rm(tmpDir, { recursive: true, force: true })
})
function createWarpDb(dir: string): string {
mkdirSync(dir, { recursive: true })
const dbPath = join(dir, 'warp.sqlite')
const { DatabaseSync: Database } = requireForTest('node:sqlite')
const db = new Database(dbPath)
db.exec(`
CREATE TABLE IF NOT EXISTS agent_conversations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
conversation_id TEXT NOT NULL,
conversation_data TEXT NOT NULL,
last_modified_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
)
`)
db.exec(`
CREATE TABLE IF NOT EXISTS ai_queries (
id INTEGER PRIMARY KEY AUTOINCREMENT,
exchange_id TEXT NOT NULL,
conversation_id TEXT NOT NULL,
start_ts DATETIME NOT NULL,
input TEXT NOT NULL,
working_directory TEXT,
output_status TEXT NOT NULL,
model_id TEXT NOT NULL DEFAULT '',
planning_model_id TEXT NOT NULL DEFAULT '',
coding_model_id TEXT NOT NULL DEFAULT ''
)
`)
db.exec(`
CREATE TABLE IF NOT EXISTS blocks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
pane_leaf_uuid BLOB NOT NULL,
stylized_command BLOB NOT NULL,
stylized_output BLOB NOT NULL,
pwd TEXT,
git_branch TEXT,
virtual_env TEXT,
conda_env TEXT,
exit_code INTEGER NOT NULL,
did_execute BOOLEAN NOT NULL,
completed_ts DATETIME,
start_ts DATETIME,
ps1 TEXT,
honor_ps1 BOOLEAN NOT NULL DEFAULT 0,
shell TEXT,
user TEXT,
host TEXT,
is_background BOOLEAN NOT NULL DEFAULT 0,
rprompt TEXT,
prompt_snapshot TEXT,
block_id TEXT NOT NULL DEFAULT '',
ai_metadata TEXT,
is_local BOOLEAN,
agent_view_visibility TEXT,
git_branch_name TEXT
)
`)
db.close()
return dbPath
}
function withTestDb(dbPath: string, fn: (db: TestDb) => void): void {
const { DatabaseSync: Database } = requireForTest('node:sqlite')
const db = new Database(dbPath)
try {
fn(db)
} finally {
db.close()
}
}
function insertConversation(
db: TestDb,
conversationId: string,
conversationData: unknown,
lastModifiedAt = '2026-05-18 10:10:00',
): void {
db.prepare(
'INSERT INTO agent_conversations (conversation_id, conversation_data, last_modified_at) VALUES (?, ?, ?)',
).run(conversationId, JSON.stringify(conversationData), lastModifiedAt)
}
function insertQuery(db: TestDb, q: QueryFixture): void {
db.prepare(
`INSERT INTO ai_queries (
exchange_id, conversation_id, start_ts, input, working_directory, output_status, model_id, planning_model_id, coding_model_id
) VALUES (?, ?, ?, ?, ?, ?, ?, '', '')`,
).run(
q.exchangeId,
q.conversationId,
q.startTs,
q.input,
q.workingDirectory ?? null,
q.outputStatus ?? '"Completed"',
q.modelId ?? 'auto-efficient',
)
}
function insertBlock(db: TestDb, b: BlockFixture): void {
db.prepare(
`INSERT INTO blocks (
pane_leaf_uuid, stylized_command, stylized_output, exit_code, did_execute,
completed_ts, start_ts, block_id, ai_metadata
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
).run(
Buffer.from([0]),
b.command,
'',
b.exitCode,
1,
b.completedTs,
b.startTs,
b.blockId,
JSON.stringify({
requested_command_action_id: `call-${b.blockId}`,
conversation_id: b.conversationId,
}),
)
}
async function collectCalls(
dbPath: string,
conversationId: string,
seenKeys = new Set<string>(),
): Promise<ParsedProviderCall[]> {
const provider = createWarpProvider(dbPath)
const source = {
path: `${dbPath}:${conversationId}`,
project: 'warp',
provider: 'warp',
}
const calls: ParsedProviderCall[] = []
for await (const call of provider.createSessionParser(source, seenKeys).parse()) {
calls.push(call)
}
return calls
}
const skipUnlessSqlite = isSqliteAvailable() ? describe : describe.skip
skipUnlessSqlite('warp provider', () => {
it('discovers sessions and sanitizes project names from working_directory', async () => {
const dbPath = createWarpDb(tmpDir)
withTestDb(dbPath, (db) => {
insertConversation(db, 'conv-1', { conversation_usage_metadata: { token_usage: [] } })
insertQuery(db, {
exchangeId: 'ex-1',
conversationId: 'conv-1',
startTs: '2026-05-18 10:00:00.000000',
input: '[]',
workingDirectory: '/Users/test/project-a',
})
})
const provider = createWarpProvider(dbPath)
const sessions = await provider.discoverSessions()
expect(sessions).toHaveLength(1)
expect(sessions[0]!.provider).toBe('warp')
expect(sessions[0]!.path).toBe(`${dbPath}:conv-1`)
expect(sessions[0]!.project).toBe('Users-test-project-a')
})
it('parses one call per completed exchange and estimates tokens from primary-agent totals', async () => {
const dbPath = createWarpDb(tmpDir)
withTestDb(dbPath, (db) => {
insertConversation(db, 'conv-1', {
conversation_usage_metadata: {
token_usage: [
{
model_id: 'GPT-5.3 Codex (medium reasoning)',
warp_tokens: 300,
byok_tokens: 0,
warp_token_usage_by_category: { primary_agent: 300 },
byok_token_usage_by_category: {},
},
{
model_id: 'Claude Haiku 4.5',
warp_tokens: 90,
byok_tokens: 0,
warp_token_usage_by_category: { full_terminal_use: 90 },
byok_token_usage_by_category: {},
},
],
},
})
insertQuery(db, {
exchangeId: 'ex-1',
conversationId: 'conv-1',
startTs: '2026-05-18 10:00:00.000000',
input: JSON.stringify([{ Query: { text: 'short prompt' } }]),
modelId: 'auto-efficient',
workingDirectory: '/Users/test/project-a',
})
insertQuery(db, {
exchangeId: 'ex-2',
conversationId: 'conv-1',
startTs: '2026-05-18 10:03:00.000000',
input: JSON.stringify([{ Query: { text: 'longer prompt with substantially more detail for weighting' } }]),
modelId: 'auto-efficient',
workingDirectory: '/Users/test/project-a',
})
})
const calls = await collectCalls(dbPath, 'conv-1')
expect(calls).toHaveLength(2)
expect(calls.map(call => call.deduplicationKey)).toEqual([
'warp:conv-1:ex-1',
'warp:conv-1:ex-2',
])
expect(calls.map(call => call.userMessage)).toEqual([
'short prompt',
'longer prompt with substantially more detail for weighting',
])
expect(calls.every(call => call.model === 'gpt-5.3-codex')).toBe(true)
expect(calls.every(call => call.costIsEstimated === true)).toBe(true)
expect(calls.reduce((sum, call) => sum + call.inputTokens, 0)).toBe(300)
expect(calls[1]!.inputTokens).toBeGreaterThan(calls[0]!.inputTokens)
expect(calls[0]!.projectPath).toBe('/Users/test/project-a')
expect(calls[0]!.project).toBe('Users-test-project-a')
})
it('attributes command blocks to the nearest preceding exchange and extracts Bash commands', async () => {
const dbPath = createWarpDb(tmpDir)
withTestDb(dbPath, (db) => {
insertConversation(db, 'conv-2', {
conversation_usage_metadata: {
token_usage: [
{
model_id: 'GPT-5.3 Codex (medium reasoning)',
warp_tokens: 120,
byok_tokens: 0,
warp_token_usage_by_category: { primary_agent: 120 },
byok_token_usage_by_category: {},
},
],
},
})
insertQuery(db, {
exchangeId: 'ex-a',
conversationId: 'conv-2',
startTs: '2026-05-18 11:00:00.000000',
input: JSON.stringify([{ Query: { text: 'run tests' } }]),
})
insertQuery(db, {
exchangeId: 'ex-b',
conversationId: 'conv-2',
startTs: '2026-05-18 11:05:00.000000',
input: JSON.stringify([{ Query: { text: 'summarize results' } }]),
})
insertBlock(db, {
blockId: 'block-1',
conversationId: 'conv-2',
startTs: '2026-05-18 11:01:00.000000',
completedTs: '2026-05-18 11:01:04.000000',
exitCode: 0,
command: 'npm test && git status',
})
})
const calls = await collectCalls(dbPath, 'conv-2')
expect(calls).toHaveLength(2)
expect(calls[0]!.tools).toEqual(['Bash'])
expect(calls[0]!.bashCommands).toEqual(['npm', 'git'])
expect(calls[1]!.tools).toEqual([])
expect(calls[1]!.bashCommands).toEqual([])
})
it('skips pending or invalid exchanges and does not poison seenKeys for skipped rows', async () => {
const dbPath = createWarpDb(tmpDir)
withTestDb(dbPath, (db) => {
insertConversation(db, 'conv-3', {
conversation_usage_metadata: {
token_usage: [
{
model_id: 'GPT-5.3 Codex (medium reasoning)',
warp_tokens: 42,
byok_tokens: 0,
warp_token_usage_by_category: { primary_agent: 42 },
byok_token_usage_by_category: {},
},
],
},
})
insertQuery(db, {
exchangeId: 'ex-skip-seen',
conversationId: 'conv-3',
startTs: '2026-05-18 12:00:00.000000',
input: JSON.stringify([{ Query: { text: 'already seen' } }]),
})
insertQuery(db, {
exchangeId: 'ex-pending',
conversationId: 'conv-3',
startTs: '2026-05-18 12:01:00.000000',
input: JSON.stringify([{ Query: { text: 'still running' } }]),
outputStatus: '"Pending"',
})
insertQuery(db, {
exchangeId: 'ex-invalid-ts',
conversationId: 'conv-3',
startTs: 'not-a-timestamp',
input: JSON.stringify([{ Query: { text: 'bad timestamp' } }]),
})
})
const seen = new Set<string>(['warp:conv-3:ex-skip-seen'])
const calls = await collectCalls(dbPath, 'conv-3', seen)
expect(calls).toEqual([])
expect(seen.has('warp:conv-3:ex-invalid-ts')).toBe(false)
expect(seen.has('warp:conv-3:ex-pending')).toBe(false)
})
})