mirror of
https://github.com/AgentSeal/codeburn.git
synced 2026-07-18 13:34:24 +00:00
Some checks are pending
CI / semgrep (push) Waiting to run
* Add Forge provider support * Add Forge provider documentation * Avoid loading Forge contexts during discovery
275 lines
9.2 KiB
TypeScript
275 lines
9.2 KiB
TypeScript
import { mkdtemp, readFile, rm } from 'fs/promises'
|
|
import { join } from 'path'
|
|
import { tmpdir } from 'os'
|
|
import { createRequire } from 'node:module'
|
|
|
|
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
|
import { isSqliteAvailable } from '../../src/sqlite.js'
|
|
import { createForgeProvider } from '../../src/providers/forge.js'
|
|
import { getProvider } from '../../src/providers/index.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
|
|
}
|
|
|
|
let tmpRoot: string
|
|
|
|
beforeEach(async () => {
|
|
tmpRoot = await mkdtemp(join(tmpdir(), 'forge-test-'))
|
|
})
|
|
|
|
afterEach(async () => {
|
|
await rm(tmpRoot, { recursive: true, force: true })
|
|
})
|
|
|
|
function createForgeDb(): string {
|
|
const dbPath = join(tmpRoot, 'forge.db')
|
|
const { DatabaseSync: Database } = requireForTest('node:sqlite')
|
|
const db = new Database(dbPath)
|
|
db.exec(`
|
|
CREATE TABLE conversations(
|
|
conversation_id TEXT PRIMARY KEY NOT NULL,
|
|
title TEXT,
|
|
workspace_id BIGINT NOT NULL,
|
|
context TEXT,
|
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at TIMESTAMP,
|
|
metrics 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 insertConversationRow(db: TestDb, overrides: {
|
|
conversationId?: string
|
|
title?: string | null
|
|
workspaceId?: number | string
|
|
context?: string | null
|
|
createdAt?: string
|
|
updatedAt?: string | null
|
|
} = {}): void {
|
|
db.prepare(`
|
|
INSERT INTO conversations (conversation_id, title, workspace_id, context, created_at, updated_at, metrics)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
`).run(
|
|
overrides.conversationId ?? 'conv-1',
|
|
'title' in overrides ? overrides.title : 'Forge Project',
|
|
overrides.workspaceId ?? 123,
|
|
overrides.context ?? null,
|
|
overrides.createdAt ?? '2026-05-06 15:00:00',
|
|
'updatedAt' in overrides ? overrides.updatedAt : '2026-05-06 15:20:41.379094',
|
|
null,
|
|
)
|
|
}
|
|
|
|
function insertConversation(db: TestDb): void {
|
|
const context = {
|
|
conversation_id: 'conv-1',
|
|
messages: [
|
|
{ message: { text: { role: 'User', content: 'implement forge' } } },
|
|
{
|
|
message: {
|
|
text: {
|
|
role: 'Assistant',
|
|
content: '',
|
|
model: 'claude-opus-4-6',
|
|
tool_calls: [
|
|
{ name: 'shell', call_id: 'call-1', arguments: { command: 'git status && npm test' } },
|
|
{ name: 'Read', call_id: 'call-2', arguments: { file_path: '/tmp/a' } },
|
|
],
|
|
},
|
|
},
|
|
usage: {
|
|
prompt_tokens: { actual: 1200 },
|
|
completion_tokens: { actual: 300 },
|
|
total_tokens: { actual: 1500 },
|
|
cached_tokens: { actual: 200 },
|
|
},
|
|
},
|
|
],
|
|
}
|
|
|
|
insertConversationRow(db, { context: JSON.stringify(context) })
|
|
}
|
|
|
|
async function collect(parser: { parse(): AsyncGenerator<ParsedProviderCall> }): Promise<ParsedProviderCall[]> {
|
|
const out: ParsedProviderCall[] = []
|
|
for await (const call of parser.parse()) out.push(call)
|
|
return out
|
|
}
|
|
|
|
describe('forge provider', () => {
|
|
it('discovers conversations with context and parses assistant usage/tool calls', async () => {
|
|
if (!isSqliteAvailable()) return
|
|
|
|
const dbPath = createForgeDb()
|
|
withTestDb(dbPath, insertConversation)
|
|
|
|
const provider = createForgeProvider(dbPath)
|
|
const sources = await provider.discoverSessions()
|
|
|
|
expect(sources).toEqual([
|
|
{
|
|
path: `${dbPath}:conv-1`,
|
|
project: 'Forge Project',
|
|
provider: 'forge',
|
|
},
|
|
])
|
|
|
|
const seenKeys = new Set<string>()
|
|
const calls = await collect(provider.createSessionParser(sources[0]!, seenKeys))
|
|
|
|
expect(calls).toHaveLength(1)
|
|
expect(calls[0]).toMatchObject({
|
|
provider: 'forge',
|
|
model: 'claude-opus-4-6',
|
|
inputTokens: 1000,
|
|
outputTokens: 300,
|
|
cacheReadInputTokens: 200,
|
|
cachedInputTokens: 200,
|
|
cacheCreationInputTokens: 0,
|
|
tools: ['Bash', 'Read'],
|
|
bashCommands: ['git', 'npm'],
|
|
userMessage: 'implement forge',
|
|
sessionId: 'conv-1',
|
|
timestamp: '2026-05-06T15:20:41.379Z',
|
|
deduplicationKey: 'forge:conv-1:call-1',
|
|
})
|
|
|
|
const duplicates = await collect(provider.createSessionParser(sources[0]!, seenKeys))
|
|
expect(duplicates).toEqual([])
|
|
})
|
|
|
|
it('does not select conversation context while discovering sessions', async () => {
|
|
const source = await readFile(new URL('../../src/providers/forge.ts', import.meta.url), 'utf8')
|
|
const discoverySql = source.match(/async function discoverFromDb[\s\S]*?db\.query<[^>]+>\(\s*`([\s\S]*?)`/)?.[1]
|
|
const selectedColumns = discoverySql?.split('FROM conversations')[0] ?? ''
|
|
|
|
expect(discoverySql).toContain('WHERE context IS NOT NULL')
|
|
expect(selectedColumns).not.toMatch(/\bcontext\b/)
|
|
expect(selectedColumns).not.toMatch(/\bcreated_at\b|\bupdated_at\b/)
|
|
})
|
|
|
|
it('returns no sessions when the database is missing', async () => {
|
|
const provider = createForgeProvider(join(tmpRoot, 'missing.db'))
|
|
|
|
await expect(provider.discoverSessions()).resolves.toEqual([])
|
|
})
|
|
|
|
it('skips zero-token assistant usage', async () => {
|
|
if (!isSqliteAvailable()) return
|
|
|
|
const dbPath = createForgeDb()
|
|
const context = {
|
|
messages: [
|
|
{ message: { text: { role: 'User', content: 'zero tokens' } } },
|
|
{
|
|
message: { text: { role: 'Assistant', model: 'claude-opus-4-6' } },
|
|
usage: {
|
|
prompt_tokens: { actual: 0 },
|
|
completion_tokens: { actual: 0 },
|
|
cached_tokens: { actual: 0 },
|
|
},
|
|
},
|
|
],
|
|
}
|
|
withTestDb(dbPath, db => insertConversationRow(db, { context: JSON.stringify(context) }))
|
|
|
|
const provider = createForgeProvider(dbPath)
|
|
const sources = await provider.discoverSessions()
|
|
const calls = await collect(provider.createSessionParser(sources[0]!, new Set()))
|
|
|
|
expect(calls).toEqual([])
|
|
})
|
|
|
|
it('parses multiple assistant messages with the nearest previous user prompt', async () => {
|
|
if (!isSqliteAvailable()) return
|
|
|
|
const dbPath = createForgeDb()
|
|
const context = {
|
|
messages: [
|
|
{ message: { text: { role: 'User', content: 'first request' } } },
|
|
{
|
|
message: { text: { role: 'Assistant', model: 'claude-opus-4-6', tool_calls: [{ name: 'shell', call_id: 'call-1', arguments: { command: 'npm test' } }] } },
|
|
usage: { prompt_tokens: { actual: 100 }, completion_tokens: { actual: 20 } },
|
|
},
|
|
{ message: { text: { role: 'User', content: 'second request' } } },
|
|
{
|
|
message: { text: { role: 'Assistant', model: 'claude-sonnet-4-6', tool_calls: [{ name: 'Read', call_id: 'call-2', arguments: { file_path: '/tmp/a' } }] } },
|
|
usage: { prompt_tokens: { actual: 200 }, completion_tokens: { actual: 30 } },
|
|
},
|
|
],
|
|
}
|
|
withTestDb(dbPath, db => insertConversationRow(db, { context: JSON.stringify(context) }))
|
|
|
|
const provider = createForgeProvider(dbPath)
|
|
const sources = await provider.discoverSessions()
|
|
const calls = await collect(provider.createSessionParser(sources[0]!, new Set()))
|
|
|
|
expect(calls).toHaveLength(2)
|
|
expect(calls.map(call => call.userMessage)).toEqual(['first request', 'second request'])
|
|
expect(calls.map(call => call.deduplicationKey)).toEqual(['forge:conv-1:call-1', 'forge:conv-1:call-2'])
|
|
})
|
|
|
|
it('uses workspace_id as project when title is null', async () => {
|
|
if (!isSqliteAvailable()) return
|
|
|
|
const dbPath = createForgeDb()
|
|
withTestDb(dbPath, db => insertConversationRow(db, { title: null, workspaceId: 'workspace-1', context: '{}' }))
|
|
|
|
const provider = createForgeProvider(dbPath)
|
|
const sources = await provider.discoverSessions()
|
|
|
|
expect(sources[0]?.project).toBe('workspace-1')
|
|
})
|
|
|
|
it('uses large integer workspace_id values as strings when title is null', async () => {
|
|
if (!isSqliteAvailable()) return
|
|
|
|
const dbPath = createForgeDb()
|
|
withTestDb(dbPath, db => {
|
|
db.exec(`
|
|
INSERT INTO conversations (conversation_id, title, workspace_id, context, created_at, updated_at, metrics)
|
|
VALUES ('conv-1', NULL, 8549909960051246556, '{}', '2026-05-06 15:00:00', '2026-05-06 15:20:41.379094', NULL)
|
|
`)
|
|
})
|
|
|
|
const provider = createForgeProvider(dbPath)
|
|
const sources = await provider.discoverSessions()
|
|
|
|
expect(sources[0]?.project).toBe('8549909960051246556')
|
|
})
|
|
|
|
it('does not throw and yields no calls for invalid JSON context', async () => {
|
|
if (!isSqliteAvailable()) return
|
|
|
|
const dbPath = createForgeDb()
|
|
withTestDb(dbPath, db => insertConversationRow(db, { context: '{invalid' }))
|
|
|
|
const provider = createForgeProvider(dbPath)
|
|
const sources = await provider.discoverSessions()
|
|
|
|
await expect(collect(provider.createSessionParser(sources[0]!, new Set()))).resolves.toEqual([])
|
|
})
|
|
|
|
it('is available through the provider registry', async () => {
|
|
const provider = await getProvider('forge')
|
|
expect(provider?.name).toBe('forge')
|
|
})
|
|
})
|