mirror of
https://github.com/AgentSeal/codeburn.git
synced 2026-08-21 06:24:32 +00:00
Add doctor probeRoots coverage for the remaining fixed-location providers while keeping discovery and diagnostics on the same shared root-resolution logic.
This commit is contained in:
parent
45d98a373f
commit
08e6c99d3b
8 changed files with 170 additions and 19 deletions
|
|
@ -2,8 +2,8 @@ import { stat } from 'fs/promises'
|
|||
import { homedir } from 'os'
|
||||
import { basename, join } from 'path'
|
||||
|
||||
import { discoverClineTasks, createClineParser, getVSCodeGlobalStoragePaths } from './vscode-cline-parser.js'
|
||||
import type { Provider, SessionSource, SessionParser } from './types.js'
|
||||
import { discoverClineTasks, createClineParser, clineTaskRoots } from './vscode-cline-parser.js'
|
||||
import type { ProbeRoot, Provider, SessionSource, SessionParser } from './types.js'
|
||||
|
||||
const EXTENSION_ID = 'saoudrizwan.claude-dev'
|
||||
|
||||
|
|
@ -38,6 +38,14 @@ async function dedupeTaskSources(sources: SessionSource[]): Promise<SessionSourc
|
|||
|
||||
export function createClineProvider(overrideDirs?: string | string[]): Provider {
|
||||
const configuredDirs = normalizeOverrideDirs(overrideDirs)
|
||||
// Cline may be installed in any VS Code variant (stable, Insiders, VSCodium),
|
||||
// so every globalStorage root is scanned - same as the Roo Code and KiloCode
|
||||
// siblings - plus Cline's own home-data root. Shared by discovery and
|
||||
// probeRoots so doctor can never report a root discovery does not read.
|
||||
const taskRoots = (): string[] => configuredDirs ?? [
|
||||
...clineTaskRoots(EXTENSION_ID),
|
||||
getClineDataPath(),
|
||||
]
|
||||
|
||||
return {
|
||||
name: 'cline',
|
||||
|
|
@ -51,14 +59,12 @@ export function createClineProvider(overrideDirs?: string | string[]): Provider
|
|||
return rawTool
|
||||
},
|
||||
|
||||
async probeRoots(): Promise<ProbeRoot[]> {
|
||||
return taskRoots().map(path => ({ path, label: 'tasks' }))
|
||||
},
|
||||
|
||||
async discoverSessions(): Promise<SessionSource[]> {
|
||||
// Cline may be installed in any VS Code variant (stable, Insiders,
|
||||
// VSCodium), so every globalStorage root is scanned - same as the Roo Code
|
||||
// and KiloCode siblings - plus Cline's own home-data root.
|
||||
const baseDirs = configuredDirs ?? [
|
||||
...getVSCodeGlobalStoragePaths(EXTENSION_ID),
|
||||
getClineDataPath(),
|
||||
]
|
||||
const baseDirs = taskRoots()
|
||||
|
||||
return dedupeTaskSources(await discoverClineTasks(EXTENSION_ID, 'cline', 'Cline', baseDirs))
|
||||
},
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { homedir } from 'os'
|
|||
import { readSessionFile } from '../fs-utils.js'
|
||||
import { calculateCost, getShortModelName } from '../models.js'
|
||||
import { extractBashCommands } from '../bash-utils.js'
|
||||
import type { Provider, SessionSource, SessionParser, ParsedProviderCall } from './types.js'
|
||||
import type { ProbeRoot, Provider, SessionSource, SessionParser, ParsedProviderCall } from './types.js'
|
||||
|
||||
// Grok Build (xAI's coding CLI) stores one session per directory at
|
||||
// <grok-home>/sessions/<url-encoded-cwd>/<uuid>/, where grok-home is $GROK_HOME
|
||||
|
|
@ -257,6 +257,10 @@ export function createGrokProvider(sessionsDir?: string): Provider {
|
|||
name: 'grok',
|
||||
displayName: 'Grok Build',
|
||||
|
||||
async probeRoots(): Promise<ProbeRoot[]> {
|
||||
return [{ path: dir, label: 'sessions' }]
|
||||
},
|
||||
|
||||
modelDisplayName(model: string): string {
|
||||
if (model.startsWith('grok-build')) return 'Grok Build'
|
||||
return getShortModelName(model)
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import { join } from 'path'
|
||||
import { homedir } from 'os'
|
||||
|
||||
import { discoverClineTasks, createClineParser } from './vscode-cline-parser.js'
|
||||
import { discoverClineTasks, createClineParser, clineTaskRoots } from './vscode-cline-parser.js'
|
||||
import { discoverSqliteSessions, createSqliteSessionParser, type SqliteProviderConfig } from './sqlite-session-parser.js'
|
||||
import type { Provider, SessionSource, SessionParser } from './types.js'
|
||||
import type { ProbeRoot, Provider, SessionSource, SessionParser } from './types.js'
|
||||
|
||||
const EXTENSION_ID = 'kilocode.kilo-code'
|
||||
const PROVIDER_NAME = 'kilo-code'
|
||||
|
|
@ -33,6 +33,14 @@ export function createKiloCodeProvider(overrideDir?: string | string[]): Provide
|
|||
return rawTool
|
||||
},
|
||||
|
||||
async probeRoots(): Promise<ProbeRoot[]> {
|
||||
// Both halves of discovery: the legacy task tree and the SQLite store.
|
||||
return [
|
||||
...clineTaskRoots(EXTENSION_ID, overrideDir).map(path => ({ path, label: 'tasks' })),
|
||||
{ path: sqliteConfig.dbDir, label: 'sqlite' },
|
||||
]
|
||||
},
|
||||
|
||||
async discoverSessions(): Promise<SessionSource[]> {
|
||||
const [oldSessions, dbSessions] = await Promise.all([
|
||||
discoverClineTasks(EXTENSION_ID, PROVIDER_NAME, 'KiloCode', overrideDir),
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import { homedir } from 'os'
|
|||
import { extractBashCommands } from '../bash-utils.js'
|
||||
import { readSessionLines } from '../fs-utils.js'
|
||||
import { calculateCost, getShortModelName } from '../models.js'
|
||||
import type { ParsedProviderCall, Provider, SessionParser, SessionSource } from './types.js'
|
||||
import type { ProbeRoot, ParsedProviderCall, Provider, SessionParser, SessionSource } from './types.js'
|
||||
|
||||
type JsonObject = Record<string, unknown>
|
||||
|
||||
|
|
@ -346,6 +346,10 @@ export function createKimiProvider(overrideDir?: string): Provider {
|
|||
name: 'kimi',
|
||||
displayName: 'Kimi',
|
||||
|
||||
async probeRoots(): Promise<ProbeRoot[]> {
|
||||
return [{ path: join(shareDir, 'sessions'), label: 'sessions' }]
|
||||
},
|
||||
|
||||
modelDisplayName(model: string): string {
|
||||
return getShortModelName(model)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import { readSessionFile, readSessionLines } from '../fs-utils.js'
|
|||
import { calculateCost } from '../models.js'
|
||||
import { extractBashCommands } from '../bash-utils.js'
|
||||
import { normalizeContentBlocks } from '../content-utils.js'
|
||||
import type { Provider, SessionSource, SessionParser, ParsedProviderCall } from './types.js'
|
||||
import type { ProbeRoot, Provider, SessionSource, SessionParser, ParsedProviderCall } from './types.js'
|
||||
|
||||
const modelDisplayNames: Record<string, string> = {
|
||||
'gpt-5.4': 'GPT-5.4',
|
||||
|
|
@ -272,6 +272,10 @@ export function createPiProvider(sessionsDir?: string): Provider {
|
|||
|
||||
return {
|
||||
name: 'pi',
|
||||
|
||||
async probeRoots(): Promise<ProbeRoot[]> {
|
||||
return [{ path: dir, label: 'sessions' }]
|
||||
},
|
||||
displayName: 'Pi',
|
||||
|
||||
modelDisplayName(model: string): string {
|
||||
|
|
@ -302,6 +306,10 @@ export function createOmpProvider(sessionsDir?: string): Provider {
|
|||
|
||||
return {
|
||||
name: 'omp',
|
||||
|
||||
async probeRoots(): Promise<ProbeRoot[]> {
|
||||
return [{ path: dir, label: 'sessions' }]
|
||||
},
|
||||
displayName: 'OMP',
|
||||
|
||||
modelDisplayName(model: string): string {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { discoverClineTasks, createClineParser } from './vscode-cline-parser.js'
|
||||
import type { Provider, SessionSource, SessionParser } from './types.js'
|
||||
import { discoverClineTasks, createClineParser, clineTaskRoots } from './vscode-cline-parser.js'
|
||||
import type { ProbeRoot, Provider, SessionSource, SessionParser } from './types.js'
|
||||
|
||||
const EXTENSION_ID = 'rooveterinaryinc.roo-cline'
|
||||
|
||||
|
|
@ -16,6 +16,10 @@ export function createRooCodeProvider(overrideDir?: string | string[]): Provider
|
|||
return rawTool
|
||||
},
|
||||
|
||||
async probeRoots(): Promise<ProbeRoot[]> {
|
||||
return clineTaskRoots(EXTENSION_ID, overrideDir).map(path => ({ path, label: 'tasks' }))
|
||||
},
|
||||
|
||||
async discoverSessions(): Promise<SessionSource[]> {
|
||||
return discoverClineTasks(EXTENSION_ID, 'roo-code', 'Roo Code', overrideDir)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -42,11 +42,18 @@ export function getVSCodeGlobalStoragePath(extensionId: string): string {
|
|||
return getVSCodeGlobalStoragePaths(extensionId)[0]!
|
||||
}
|
||||
|
||||
export async function discoverClineTasks(extensionId: string, providerName: string, displayName: string, overrideDir?: string | string[]): Promise<SessionSource[]> {
|
||||
const baseDirs = overrideDir
|
||||
// The roots discoverClineTasks scans: an explicit override wins, otherwise
|
||||
// every VS Code variant's globalStorage. Exported so a provider's probeRoots()
|
||||
// can report exactly what discovery reads by calling the same function, rather
|
||||
// than mirroring this logic and drifting from it.
|
||||
export function clineTaskRoots(extensionId: string, overrideDir?: string | string[]): string[] {
|
||||
return overrideDir
|
||||
? (Array.isArray(overrideDir) ? overrideDir : [overrideDir])
|
||||
: getVSCodeGlobalStoragePaths(extensionId)
|
||||
return discoverClineTasksInBaseDirs(baseDirs, providerName, displayName)
|
||||
}
|
||||
|
||||
export async function discoverClineTasks(extensionId: string, providerName: string, displayName: string, overrideDir?: string | string[]): Promise<SessionSource[]> {
|
||||
return discoverClineTasksInBaseDirs(clineTaskRoots(extensionId, overrideDir), providerName, displayName)
|
||||
}
|
||||
|
||||
export async function discoverClineTasksInBaseDirs(baseDirs: string[], providerName: string, displayName: string): Promise<SessionSource[]> {
|
||||
|
|
|
|||
110
tests/provider-probe-roots-tier2.test.ts
Normal file
110
tests/provider-probe-roots-tier2.test.ts
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
import { describe, it, expect } from 'vitest'
|
||||
import { isAbsolute, join } from 'path'
|
||||
import { homedir } from 'os'
|
||||
|
||||
import { createClineProvider, getClineDataPath } from '../src/providers/cline.js'
|
||||
import { createRooCodeProvider } from '../src/providers/roo-code.js'
|
||||
import { createKiloCodeProvider } from '../src/providers/kilo-code.js'
|
||||
import { createGrokProvider } from '../src/providers/grok.js'
|
||||
import { createPiProvider, createOmpProvider } from '../src/providers/pi.js'
|
||||
import { createKimiProvider } from '../src/providers/kimi.js'
|
||||
import {
|
||||
clineTaskRoots,
|
||||
discoverClineTasks,
|
||||
getVSCodeGlobalStoragePaths,
|
||||
} from '../src/providers/vscode-cline-parser.js'
|
||||
|
||||
// #899 Tier 2, batch 1. probeRoots() must report the roots discovery actually
|
||||
// reads: a probe pointing somewhere discovery never looks is worse than none,
|
||||
// because it looks authoritative. Assertions pin exact root sets rather than
|
||||
// substrings, so a wrong-but-similar path cannot pass.
|
||||
//
|
||||
// This file is separate from the Tier 1 suite only because #903 introduces
|
||||
// that one and is still open; fold the two together once it lands.
|
||||
|
||||
const CLINE_EXTENSION = 'saoudrizwan.claude-dev'
|
||||
const ROO_EXTENSION = 'rooveterinaryinc.roo-cline'
|
||||
|
||||
describe('probeRoots mirrors discovery resolution (Tier 2, batch 1)', () => {
|
||||
it('cline reports exactly the roots discovery scans', async () => {
|
||||
// The provider whose silence motivated #874: four places to look, and until
|
||||
// now no way to see which of them CodeBurn actually read.
|
||||
const roots = await createClineProvider().probeRoots!()
|
||||
expect(roots).toEqual([
|
||||
...clineTaskRoots(CLINE_EXTENSION).map(path => ({ path, label: 'tasks' })),
|
||||
{ path: getClineDataPath(), label: 'tasks' },
|
||||
])
|
||||
expect(roots).toHaveLength(4)
|
||||
for (const root of roots) expect(isAbsolute(root.path)).toBe(true)
|
||||
})
|
||||
|
||||
it('cline reports the configured dirs verbatim when overridden', async () => {
|
||||
expect(await createClineProvider(['/tmp/cline-a', '/tmp/cline-b']).probeRoots!()).toEqual([
|
||||
{ path: '/tmp/cline-a', label: 'tasks' },
|
||||
{ path: '/tmp/cline-b', label: 'tasks' },
|
||||
])
|
||||
})
|
||||
|
||||
it('roo-code reports the override, or exactly the VS Code variant roots', async () => {
|
||||
expect(await createRooCodeProvider('/tmp/roo-a').probeRoots!()).toEqual([
|
||||
{ path: '/tmp/roo-a', label: 'tasks' },
|
||||
])
|
||||
expect(await createRooCodeProvider().probeRoots!()).toEqual(
|
||||
getVSCodeGlobalStoragePaths(ROO_EXTENSION).map(path => ({ path, label: 'tasks' })),
|
||||
)
|
||||
})
|
||||
|
||||
// Regression: an earlier draft mirrored the resolution in a local helper that
|
||||
// detected "no override" with `=== undefined`, while discoverClineTasks uses
|
||||
// truthiness. An empty-string override made doctor report [""] while
|
||||
// discovery scanned the three default roots. Both now call one resolver.
|
||||
it('an empty-string override resolves the same for probeRoots and discovery', async () => {
|
||||
const probed = (await createRooCodeProvider('').probeRoots!()).map(r => r.path)
|
||||
expect(probed).toEqual(clineTaskRoots(ROO_EXTENSION, ''))
|
||||
expect(probed).toEqual(getVSCodeGlobalStoragePaths(ROO_EXTENSION))
|
||||
// discoverClineTasks resolves through the same function, so an empty
|
||||
// override cannot send discovery somewhere probeRoots did not report.
|
||||
expect(await discoverClineTasks(ROO_EXTENSION, 'roo-code', 'Roo Code', '')).toEqual([])
|
||||
})
|
||||
|
||||
it('kilo-code reports both halves of its discovery: tasks and the sqlite store', async () => {
|
||||
const roots = await createKiloCodeProvider('/tmp/kilo-a').probeRoots!()
|
||||
expect(roots[0]).toEqual({ path: '/tmp/kilo-a', label: 'tasks' })
|
||||
const sqlite = roots.filter(r => r.label === 'sqlite')
|
||||
expect(sqlite).toHaveLength(1)
|
||||
// The same dbDir discoverSqliteSessions reads, not a lookalike.
|
||||
expect(sqlite[0]!.path).toBe(
|
||||
join(process.env['XDG_DATA_HOME'] ?? join(homedir(), '.local', 'share'), 'kilo'),
|
||||
)
|
||||
})
|
||||
|
||||
it('grok reports exactly its resolved sessions dir', async () => {
|
||||
expect(await createGrokProvider('/tmp/grok-a').probeRoots!()).toEqual([
|
||||
{ path: '/tmp/grok-a', label: 'sessions' },
|
||||
])
|
||||
expect(await createGrokProvider().probeRoots!()).toEqual([
|
||||
{ path: join(homedir(), '.grok', 'sessions'), label: 'sessions' },
|
||||
])
|
||||
})
|
||||
|
||||
it('pi and omp each report their own sessions dir', async () => {
|
||||
expect(await createPiProvider('/tmp/pi-a').probeRoots!()).toEqual([
|
||||
{ path: '/tmp/pi-a', label: 'sessions' },
|
||||
])
|
||||
expect(await createOmpProvider('/tmp/omp-a').probeRoots!()).toEqual([
|
||||
{ path: '/tmp/omp-a', label: 'sessions' },
|
||||
])
|
||||
// Same module, two providers: the roots must not collide.
|
||||
const [piRoot] = await createPiProvider().probeRoots!()
|
||||
const [ompRoot] = await createOmpProvider().probeRoots!()
|
||||
expect(piRoot!.path).not.toBe(ompRoot!.path)
|
||||
})
|
||||
|
||||
it('kimi reports the sessions dir under its share root, not the share root itself', async () => {
|
||||
// Discovery walks <shareDir>/sessions; reporting shareDir would point doctor
|
||||
// at a directory that exists even when no sessions do.
|
||||
expect(await createKimiProvider('/tmp/kimi-a').probeRoots!()).toEqual([
|
||||
{ path: join('/tmp/kimi-a', 'sessions'), label: 'sessions' },
|
||||
])
|
||||
})
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue