codeburn/tests/providers/quickdesk.test.ts
ozymandiashh bdb1475251 feat(quickdesk): Amazon Quick Desktop provider, parse ~/.quickwork sessions and metrics (#707)
Adds an eager provider for Amazon Quick Desktop (aws.amazon.com/quick/desktop),
which stores local data under ~/.quickwork. Usage source of truth is the
per-day EMF metrics JSONL (Model, InputTokens, OutputTokens, CostUSD,
session_id); real CostUSD passes through the cache like kiro/devin/hermes.
sessions.db (SQLite, read-only) enriches calls with titles, first user
message, and tool names, and provides estimated usage (quickdesk-auto,
costIsEstimated) for sessions that predate metrics coverage. Multi-profile
layout via profiles.json entries plus the migrated legacy root; honors
QUICKWORK_HOME; schema-tolerant (sqlite_master and PRAGMA introspection,
per-line JSONL error isolation). The on-disk schema is reverse-engineered and
documented as such in docs/providers/quickdesk.md.
2026-07-18 00:02:01 +03:00

445 lines
18 KiB
TypeScript

import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'
import { createRequire } from 'node:module'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
import { calculateCost, getShortModelName } from '../../src/models.js'
import { providers } from '../../src/providers/index.js'
import { quickdesk } from '../../src/providers/quickdesk.js'
import { isSqliteAvailable } from '../../src/sqlite.js'
import type { ParsedProviderCall, SessionSource } 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
}
let quickworkHome: string
let originalQuickworkHome: string | undefined
let externalDirs: string[]
beforeEach(async () => {
quickworkHome = await mkdtemp(join(tmpdir(), 'quickdesk-provider-test-'))
originalQuickworkHome = process.env['QUICKWORK_HOME']
process.env['QUICKWORK_HOME'] = quickworkHome
externalDirs = []
})
afterEach(async () => {
if (originalQuickworkHome === undefined) delete process.env['QUICKWORK_HOME']
else process.env['QUICKWORK_HOME'] = originalQuickworkHome
await rm(quickworkHome, { recursive: true, force: true })
for (const path of externalDirs) await rm(path, { recursive: true, force: true })
})
async function writeMetrics(basePath: string, date: string, lines: Array<Record<string, unknown> | string>): Promise<string> {
const metricsDir = join(basePath, 'metrics')
await mkdir(metricsDir, { recursive: true })
const path = join(metricsDir, `metrics-${date}.jsonl`)
await writeFile(path, lines.map(line => typeof line === 'string' ? line : JSON.stringify(line)).join('\n') + '\n')
return path
}
async function writeProfiles(entries: Array<{ id: string, data_path: string }>): Promise<void> {
await writeFile(join(quickworkHome, 'profiles.json'), JSON.stringify({ last_active: entries[0]?.id, entries }))
}
async function createSessionsDb(basePath: string, withMessages = true): Promise<string> {
const sessionsDir = join(basePath, 'sessions')
await mkdir(sessionsDir, { recursive: true })
const dbPath = join(sessionsDir, 'sessions.db')
const { DatabaseSync: Database } = requireForTest('node:sqlite') as {
DatabaseSync: new (path: string) => TestDb
}
const db = new Database(dbPath)
db.exec('PRAGMA journal_mode = WAL')
db.exec(`
CREATE TABLE sessions (
id TEXT PRIMARY KEY,
title TEXT,
created_at REAL,
updated_at REAL,
message_count INTEGER,
agent_mode TEXT,
deleted_at REAL
)
`)
if (withMessages) {
db.exec(`
CREATE TABLE session_messages (
session_id TEXT,
role TEXT,
content TEXT,
timestamp REAL,
tool_names TEXT
)
`)
}
db.close()
return dbPath
}
function withDb(dbPath: string, fn: (db: TestDb) => void): void {
const { DatabaseSync: Database } = requireForTest('node:sqlite') as {
DatabaseSync: new (path: string) => TestDb
}
const db = new Database(dbPath)
try {
fn(db)
} finally {
db.close()
}
}
async function collectCalls(sources: SessionSource[]): Promise<ParsedProviderCall[]> {
const calls: ParsedProviderCall[] = []
const seenKeys = new Set<string>()
for (const source of sources) {
for await (const call of quickdesk.createSessionParser(source, seenKeys).parse()) calls.push(call)
}
return calls
}
describe('quickdesk pure metrics', () => {
it('parses metrics without requiring a SQLite fixture', async () => {
await writeMetrics(quickworkHome, '2026-07-17', [
{ Model: 'claude-sonnet-4-5', InputTokens: 12, OutputTokens: 6, CostUSD: 0.002 },
])
const calls = await collectCalls(await quickdesk.discoverSessions())
expect(calls).toHaveLength(1)
expect(calls[0]).toMatchObject({
provider: 'quickdesk',
inputTokens: 12,
outputTokens: 6,
costUSD: 0.002,
costIsEstimated: false,
})
})
it('discovers an absolute data_path outside the store root', async () => {
const absoluteBase = await mkdtemp(join(tmpdir(), 'quickdesk-absolute-profile-'))
externalDirs.push(absoluteBase)
await writeProfiles([{ id: 'absolute-profile', data_path: absoluteBase }])
const metricsPath = await writeMetrics(absoluteBase, '2026-07-18', [
{ Model: 'claude-sonnet-4-5', InputTokens: 9, OutputTokens: 3, CostUSD: 0.001 },
])
expect(await quickdesk.discoverSessions()).toEqual([{
path: metricsPath,
project: 'absolute-profile',
provider: 'quickdesk',
sourceId: 'metrics',
sourcePath: absoluteBase,
}])
expect(await quickdesk.probeRoots?.()).toEqual([{ path: absoluteBase, label: 'absolute-profile' }])
})
it('namespaces session-less metric keys by profile', async () => {
const firstBase = join(quickworkHome, 'profiles', 'first')
const secondBase = join(quickworkHome, 'profiles', 'second')
await writeProfiles([
{ id: 'first-profile', data_path: 'profiles/first' },
{ id: 'second-profile', data_path: 'profiles/second' },
])
const row = { Model: 'claude-sonnet-4-5', InputTokens: 20, OutputTokens: 5, CostUSD: 0.003 }
await writeMetrics(firstBase, '2026-07-19', [row])
await writeMetrics(secondBase, '2026-07-19', [row])
const calls = await collectCalls(await quickdesk.discoverSessions())
expect(calls).toHaveLength(2)
expect(calls.map(call => call.deduplicationKey)).toEqual([
'quickdesk:first-profile:metrics-2026-07-19.jsonl:2026-07-19T00:00:00.000Z:claude-sonnet-4-5:20:5',
'quickdesk:second-profile:metrics-2026-07-19.jsonl:2026-07-19T00:00:00.000Z:claude-sonnet-4-5:20:5',
])
})
})
const skipUnlessSqlite = isSqliteAvailable() ? describe : describe.skip
skipUnlessSqlite('quickdesk provider', () => {
it('discovers every profile data_path and parses EMF usage, linked tools, real cost, and malformed lines', async () => {
const firstBase = join(quickworkHome, 'stores', 'alpha-data')
const secondBase = join(quickworkHome, 'custom-location', 'beta-data')
await writeProfiles([
{ id: 'profile-alpha', data_path: 'stores/alpha-data' },
{ id: 'profile-beta', data_path: 'custom-location/beta-data' },
])
await writeMetrics(firstBase, '2026-07-14', [
'{malformed json',
{ _aws: { Timestamp: 1783987200000 } },
{ session_id: 'session-alpha', ToolName: 'read_file', ToolCallCount: 1 },
{
_aws: { Timestamp: 1783987200123 },
session_id: 'session-alpha',
thread_id: 'thread-1',
Model: 'claude-sonnet-4-5',
InputTokens: 120,
OutputTokens: 30,
CostUSD: 0.0042,
},
])
await writeMetrics(secondBase, '2026-07-15', [
{ session_id: 'session-beta', Model: 'claude-sonnet-4-5', InputTokens: 40, OutputTokens: 10 },
])
const sources = await quickdesk.discoverSessions()
expect(sources.map(source => [source.project, source.sourcePath])).toEqual([
['profile-alpha', firstBase],
['profile-beta', secondBase],
])
expect(await quickdesk.probeRoots?.()).toEqual([
{ path: firstBase, label: 'profile-alpha' },
{ path: secondBase, label: 'profile-beta' },
])
const calls = await collectCalls(sources)
expect(calls).toHaveLength(2)
expect(calls[0]).toMatchObject({
provider: 'quickdesk',
model: 'claude-sonnet-4-5',
inputTokens: 120,
outputTokens: 30,
costUSD: 0.0042,
costIsEstimated: false,
tools: ['Read'],
timestamp: '2026-07-14T00:00:00.123Z',
sessionId: 'session-alpha',
project: 'profile-alpha',
})
expect(calls[0]!.deduplicationKey).toBe(
'quickdesk:session-alpha:2026-07-14T00:00:00.123Z:claude-sonnet-4-5:120:30',
)
expect(calls[1]).toMatchObject({
inputTokens: 40,
outputTokens: 10,
costIsEstimated: true,
timestamp: '2026-07-15T00:00:00.000Z',
project: 'profile-beta',
})
expect(calls[1]!.costUSD).toBe(calculateCost('claude-sonnet-4-5', 40, 10, 0, 0, 0))
})
it('falls back to the legacy store root when profiles.json is absent', async () => {
const metricsPath = await writeMetrics(quickworkHome, '2026-07-16', [
{ Model: 'claude-sonnet-4-5', InputTokens: 8, OutputTokens: 4, CostUSD: 0.001 },
])
const sources = await quickdesk.discoverSessions()
expect(sources).toEqual([{
path: metricsPath,
project: 'default',
provider: 'quickdesk',
sourceId: 'metrics',
sourcePath: quickworkHome,
}])
expect(await quickdesk.probeRoots?.()).toEqual([{ path: quickworkHome, label: 'default' }])
const calls = await collectCalls(sources)
expect(calls[0]).toMatchObject({
sessionId: 'metrics-2026-07-16.jsonl',
project: 'default',
userMessage: '',
})
})
it('emits sessions from active profiles and a coexisting migrated legacy root database', async () => {
const profileBase = join(quickworkHome, 'profiles', 'active-data')
await writeProfiles([{ id: 'active-profile', data_path: 'profiles/active-data' }])
const profileDbPath = await createSessionsDb(profileBase)
const legacyDbPath = await createSessionsDb(quickworkHome)
withDb(profileDbPath, db => {
db.prepare(
'INSERT INTO sessions (id, title, created_at, updated_at, message_count, agent_mode, deleted_at) VALUES (?, ?, ?, ?, ?, ?, ?)',
).run('profile-session', 'Profile session', 1783987200, 1783987300, 2, 'agent', null)
db.prepare(
'INSERT INTO session_messages (session_id, role, content, timestamp, tool_names) VALUES (?, ?, ?, ?, ?)',
).run('profile-session', 'user', 'profile prompt', 1783987201, null)
db.prepare(
'INSERT INTO session_messages (session_id, role, content, timestamp, tool_names) VALUES (?, ?, ?, ?, ?)',
).run('profile-session', 'assistant', 'profile answer', 1783987202, null)
})
withDb(legacyDbPath, db => {
db.prepare(
'INSERT INTO sessions (id, title, created_at, updated_at, message_count, agent_mode, deleted_at) VALUES (?, ?, ?, ?, ?, ?, ?)',
).run('legacy-session', 'Legacy session', 1777507200, 1777507300, 2, 'agent', null)
db.prepare(
'INSERT INTO session_messages (session_id, role, content, timestamp, tool_names) VALUES (?, ?, ?, ?, ?)',
).run('legacy-session', 'user', 'legacy prompt', 1777507201, null)
db.prepare(
'INSERT INTO session_messages (session_id, role, content, timestamp, tool_names) VALUES (?, ?, ?, ?, ?)',
).run('legacy-session', 'assistant', 'legacy answer', 1777507202, null)
})
const sources = await quickdesk.discoverSessions()
expect(sources.map(source => [source.project, source.path])).toEqual([
['active-profile', profileDbPath],
['default', legacyDbPath],
])
const calls = await collectCalls(sources)
expect(calls.map(call => [call.project, call.sessionId])).toEqual([
['active-profile', 'profile-session'],
['default', 'legacy-session'],
])
})
it('suppresses database estimates globally when profile metrics contain the migrated session id', async () => {
const profileBase = join(quickworkHome, 'profiles', 'active-data')
await writeProfiles([{ id: 'active-profile', data_path: 'profiles/active-data' }])
const profileDbPath = await createSessionsDb(profileBase)
const legacyDbPath = await createSessionsDb(quickworkHome)
for (const dbPath of [profileDbPath, legacyDbPath]) {
withDb(dbPath, db => {
db.prepare(
'INSERT INTO sessions (id, title, created_at, updated_at, message_count, agent_mode, deleted_at) VALUES (?, ?, ?, ?, ?, ?, ?)',
).run('shared-session', 'Shared session', 1783987200, 1783987300, 2, 'agent', null)
db.prepare(
'INSERT INTO session_messages (session_id, role, content, timestamp, tool_names) VALUES (?, ?, ?, ?, ?)',
).run('shared-session', 'user', 'shared prompt', 1783987201, null)
db.prepare(
'INSERT INTO session_messages (session_id, role, content, timestamp, tool_names) VALUES (?, ?, ?, ?, ?)',
).run('shared-session', 'assistant', 'shared answer', 1783987202, null)
})
}
await writeMetrics(profileBase, '2026-07-14', [{
session_id: 'shared-session',
Model: 'claude-sonnet-4-5',
InputTokens: 40,
OutputTokens: 10,
CostUSD: 0.004,
}])
const calls = await collectCalls(await quickdesk.discoverSessions())
expect(calls).toHaveLength(1)
expect(calls[0]).toMatchObject({
sessionId: 'shared-session',
costUSD: 0.004,
costIsEstimated: false,
})
expect(calls.filter(call => call.costIsEstimated)).toHaveLength(0)
})
it('enriches metrics and estimates only non-deleted sessions absent from all metrics', async () => {
const dbPath = await createSessionsDb(quickworkHome)
withDb(dbPath, db => {
const insertSession = db.prepare(
'INSERT INTO sessions (id, title, created_at, updated_at, message_count, agent_mode, deleted_at) VALUES (?, ?, ?, ?, ?, ?, ?)',
)
insertSession.run('metered', 'Metered session', 1783900800, 1783900900, 2, 'agent', null)
insertSession.run('fallback', 'Fallback session', 1783987200, 1783987300, 3, 'agent', null)
insertSession.run('deleted', 'Deleted session', 1784073600, 1784073700, 2, 'agent', 1784073800)
const insertMessage = db.prepare(
'INSERT INTO session_messages (session_id, role, content, timestamp, tool_names) VALUES (?, ?, ?, ?, ?)',
)
insertMessage.run('metered', 'user', 'metered prompt', 1783900801, null)
insertMessage.run('metered', 'assistant', 'metered answer', 1783900802, '["write_file"]')
insertMessage.run('fallback', 'user', '12345678', 1783987201, null)
insertMessage.run('fallback', 'tool', '1234', 1783987202, '["run_command"]')
insertMessage.run('fallback', 'assistant', '123456789', 1783987203, null)
insertMessage.run('deleted', 'user', 'must not appear', 1784073601, null)
insertMessage.run('deleted', 'assistant', 'deleted answer', 1784073602, null)
})
await writeMetrics(quickworkHome, '2026-07-13', [
{ session_id: 'metered', thread_id: 'thread-a', ToolName: 'read_file' },
{
session_id: 'metered',
thread_id: 'thread-a',
Model: 'claude-sonnet-4-5',
InputTokens: 50,
OutputTokens: 20,
CostUSD: 0.003,
},
{
session_id: 'deleted',
Model: 'claude-sonnet-4-5',
InputTokens: 99,
OutputTokens: 99,
CostUSD: 0.9,
},
])
const calls = await collectCalls(await quickdesk.discoverSessions())
expect(calls.map(call => call.sessionId)).toEqual(['metered', 'fallback'])
expect(calls[0]).toMatchObject({
userMessage: 'metered prompt',
tools: ['Read', 'Edit'],
costIsEstimated: false,
})
expect(calls[1]).toMatchObject({
model: 'quickdesk-auto',
inputTokens: 3,
outputTokens: 3,
costIsEstimated: true,
tools: ['Bash'],
timestamp: '2026-07-14T00:00:00.000Z',
deduplicationKey: 'quickdesk-est:fallback',
userMessage: '12345678',
})
expect(calls[1]!.costUSD).toBeGreaterThan(0)
})
it('treats millisecond sessions.created_at values as milliseconds', async () => {
const dbPath = await createSessionsDb(quickworkHome)
withDb(dbPath, db => {
db.prepare(
'INSERT INTO sessions (id, title, created_at, updated_at, message_count, agent_mode, deleted_at) VALUES (?, ?, ?, ?, ?, ?, ?)',
).run('milliseconds-session', 'Milliseconds', 1783987200000, 1783987300000, 2, 'agent', null)
db.prepare(
'INSERT INTO session_messages (session_id, role, content, timestamp, tool_names) VALUES (?, ?, ?, ?, ?)',
).run('milliseconds-session', 'user', 'millisecond prompt', 1783987201, null)
db.prepare(
'INSERT INTO session_messages (session_id, role, content, timestamp, tool_names) VALUES (?, ?, ?, ?, ?)',
).run('milliseconds-session', 'assistant', 'millisecond answer', 1783987202, null)
})
const calls = await collectCalls(await quickdesk.discoverSessions())
expect(calls).toHaveLength(1)
expect(calls[0]).toMatchObject({
sessionId: 'milliseconds-session',
timestamp: '2026-07-14T00:00:00.000Z',
costIsEstimated: true,
})
})
it('keeps metrics when sessions.db has no session_messages table', async () => {
const dbPath = await createSessionsDb(quickworkHome, false)
withDb(dbPath, db => {
db.prepare(
'INSERT INTO sessions (id, title, created_at, updated_at, message_count, agent_mode, deleted_at) VALUES (?, ?, ?, ?, ?, ?, ?)',
).run('metrics-only', 'Metrics only', 1783987200, 1783987300, 0, 'agent', null)
})
await writeMetrics(quickworkHome, '2026-07-14', [
{
session_id: 'metrics-only',
Model: 'claude-sonnet-4-5',
InputTokens: 16,
OutputTokens: 4,
CostUSD: 0.002,
},
])
await expect(collectCalls(await quickdesk.discoverSessions())).resolves.toMatchObject([{
sessionId: 'metrics-only',
inputTokens: 16,
outputTokens: 4,
userMessage: '',
}])
})
it('uses raw display values outside the small obvious maps', () => {
expect(providers).toContain(quickdesk)
expect(quickdesk.modelDisplayName('quickdesk-auto')).toBe('Quick Desktop (auto)')
expect(getShortModelName('quickdesk-auto')).toBe('Quick Desktop (auto)')
expect(quickdesk.modelDisplayName('claude-sonnet-4-5')).toBe('Sonnet 4.5')
expect(quickdesk.modelDisplayName('future-model')).toBe('future-model')
expect(quickdesk.toolDisplayName('read_file')).toBe('Read')
expect(quickdesk.toolDisplayName('future_tool')).toBe('future_tool')
})
})