mirror of
https://github.com/AgentSeal/codeburn.git
synced 2026-08-26 00:44:41 +00:00
Merge pull request #1122 from avs-io/local/899-proberoots
feat(doctor): probeRoots for five fixed-location providers (#899 Tier 2, batch 2)
This commit is contained in:
commit
7e23ed8083
7 changed files with 280 additions and 66 deletions
|
|
@ -43,6 +43,8 @@
|
|||
- **The resident `codeburn serve` child.** The first real panel request is also the cache warm-up, so startup never runs an artificial warm-up query beside a duplicate one-shot child; each served command carries its own read-only option allowlist, and anything outside it falls back to a normal spawn; the child exits when its stdin closes, so it can never outlive the app. Requests whose response exceeds the 16 MiB frame limit still replace the child, but that deliberate kill no longer spends the resident's unexpected-death budget. (#972)
|
||||
|
||||
### Fixed
|
||||
- **`codeburn doctor` now probes the five remaining fixed-location providers.** `codebuff`, `devin`, `gemini`, `kiro`, and `mistral-vibe` implement `probeRoots()` through the same resolvers discovery uses, so a silent zero is distinguishable from a missing install. Codebuff reports all three manicode channels unless a factory or `CODEBUFF_DATA_DIR` pins one; Devin reports `transcripts` plus `sessions.db`, not the parent; Gemini reports only `~/.gemini/tmp`; Kiro reports pre-filter candidates (empty CLI/v2 skipped, empty agent/workspace fall back); Mistral Vibe reports the joined sessions dir. Missing defaults still appear. Thanks @therickfactr. (#899)
|
||||
|
||||
- **Subscription SKUs are classified from real product ids, and a false-positive built-in can be opted out.** `codex-auto-review` consumes ordinary Codex usage ([openai/codex#32224](https://github.com/openai/codex/issues/32224)) and is priced as GPT-5.5 on #1056, so treating it as $0 hid real spend — it left the flat-rate list. Warp's product id is `auto`, not the synthetic `warp`. `kimi-for-coding-highspeed` (the SKU #968 was filed around) is now honestly $0. `big-pickle` was dropped: it appears under OpenCode, not as a cited ClinePass codename. `codeburn model-flat-rate --remove` now opts out of a built-in, so a wrong classifier entry can warn again without waiting for a release. The daily-cache config hash now always includes the flat-rate section (even when empty), so the first run after upgrade re-derives every stored day once from the warm session cache. (#968, #1050)
|
||||
- **Codex MCP and skill usage is attributed from every shape Codex records a shell command in.** `mcp-cli call <server> <tool>` was only recognized when the command arrived as `function_call` arguments (#656). Codex has two other shapes for the same exec: its custom-tool transport records the shell tool as a `custom_tool_call` whose payload is an `input` program rather than `arguments`, and its item model repeats a finished command as `event_msg`/`item_completed` carrying a `CommandExecution` item with an argv `command`. Both reached the Bash counter and neither reached the matcher, so a CLI-wrapped MCP call stayed absent from the MCP breakdown exactly as before the fix. All three shapes now feed one classification pipeline. The same pipeline learns skills: Codex has no skill tool, so loading one is a shell read of the skill's `SKILL.md`, and those reads landed entirely under Bash with the Skills dimension empty. A read counts as a skill load only when the command segment starts with a file-reading binary (`cat`/`bat`/`sed`/`head`/`tail`/`less`/`more`) and the path it reads ends in `<name>/SKILL.md`; the skill is `<name>`, the same key `pi` derives for a native skill read (#588) and the same vocabulary the Claude parser records from the `Skill` tool. A `grep`/`rg`/`ls` that merely mentions a `SKILL.md` is a search near the file, not a skill load, and stays plain Bash. This is attribution only — no call, token or cost figure moves, and a command carried by both a response item and an item-model item is attributed once. On a 1,397-rollout corpus: Skills went from empty to 7 skills over 35 turns (55 attributions), Bash was unchanged at 42,170, and cost, calls, tokens, sessions, daily, models and projects came back identical. Cached Codex sessions re-parse once (`CODEX_CACHE_VERSION` 13 → 14 and the codex parse version both move; without them the fix is invisible on a warm cache). Thanks @chr-evensen. (#478)
|
||||
- **`gpt-5.6-codex` and `gpt-5.6-codex-max` now have their own pricing rows.** Neither id is in LiteLLM yet, and both were missing from the bundled snapshot — flagged during #1075 verification on a real corpus (285 sessions, 5,446 calls). `getModelCosts` already resolved both through the `gpt-5.6` prefix fallback, so live pricing was already correct once a session priced fresh; every prior Codex-suffixed id LiteLLM does carry bills identically to its bare-model sibling of the same generation (`gpt-5-codex` == `gpt-5`, `gpt-5.1-codex` == `gpt-5.1-codex-max` == `gpt-5.1`, `gpt-5.2-codex` == `gpt-5.2`, `gpt-5.3-codex` == `gpt-5.3`), which is the evidence both new rows mirror rather than inventing a rate. The gap that does not self-heal is the daily cache: it has no per-provider invalidation, so a day finalized while either id had no billable rate keeps that $0 forever. Raising `MIN_SUPPORTED_VERSION` (v23 -> v24) forces the one-time re-derivation, a lossless no-op for days already correct. (#1077)
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { homedir } from 'os'
|
|||
|
||||
import { calculateCost } 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'
|
||||
|
||||
// Codebuff (formerly Manicode) uses a credit-based billing system. The local
|
||||
// chat-messages.json doesn't record per-call token counts the way Claude Code
|
||||
|
|
@ -123,11 +123,15 @@ type CodebuffChatMessage = {
|
|||
metadata?: CodebuffMetadata
|
||||
}
|
||||
|
||||
function getCodebuffBaseDir(override?: string): string {
|
||||
if (override && override.trim()) return override
|
||||
// Shared by discoverSessions and probeRoots. Factory / CODEBUFF_DATA_DIR win
|
||||
// as a single root; otherwise discovery walks every CHANNEL before existence
|
||||
// filtering. Empty / blank factory is unset (same as #938 truthiness).
|
||||
export function getCodebuffRootSet(override?: string): string[] {
|
||||
if (override && override.trim()) return [override]
|
||||
const envPath = process.env['CODEBUFF_DATA_DIR']
|
||||
if (envPath && envPath.trim()) return envPath
|
||||
return join(homedir(), '.config', 'manicode')
|
||||
if (envPath && envPath.trim()) return [envPath]
|
||||
const configDir = join(homedir(), '.config')
|
||||
return CHANNELS.map(channel => join(configDir, channel))
|
||||
}
|
||||
|
||||
function pickNumber(...vals: Array<number | undefined>): number | undefined {
|
||||
|
|
@ -294,23 +298,9 @@ async function discoverChannel(root: string): Promise<SessionSource[]> {
|
|||
return sources
|
||||
}
|
||||
|
||||
async function discoverSessionsInBase(baseDir: string): Promise<SessionSource[]> {
|
||||
async function discoverSessionsInRoots(roots: string[]): Promise<SessionSource[]> {
|
||||
const results: SessionSource[] = []
|
||||
|
||||
// Honor an explicit override: walk only the provided directory even if it
|
||||
// matches one of the channel names literally.
|
||||
if (process.env['CODEBUFF_DATA_DIR'] || baseDir !== join(homedir(), '.config', 'manicode')) {
|
||||
const rootStat = await stat(baseDir).catch(() => null)
|
||||
if (!rootStat?.isDirectory()) return results
|
||||
results.push(...await discoverChannel(baseDir))
|
||||
return results
|
||||
}
|
||||
|
||||
const configDir = join(homedir(), '.config')
|
||||
for (const channel of CHANNELS) {
|
||||
const root = join(configDir, channel)
|
||||
const rootStat = await stat(root).catch(() => null)
|
||||
if (!rootStat?.isDirectory()) continue
|
||||
for (const root of roots) {
|
||||
results.push(...await discoverChannel(root))
|
||||
}
|
||||
return results
|
||||
|
|
@ -433,7 +423,7 @@ function createParser(source: SessionSource, seenKeys: Set<string>): SessionPars
|
|||
}
|
||||
|
||||
export function createCodebuffProvider(baseDir?: string): Provider {
|
||||
const dir = getCodebuffBaseDir(baseDir)
|
||||
const roots = getCodebuffRootSet(baseDir)
|
||||
|
||||
return {
|
||||
name: 'codebuff',
|
||||
|
|
@ -447,8 +437,12 @@ export function createCodebuffProvider(baseDir?: string): Provider {
|
|||
return toolNameMap[rawTool] ?? rawTool
|
||||
},
|
||||
|
||||
async probeRoots(): Promise<ProbeRoot[]> {
|
||||
return roots.map(path => ({ path, label: 'chats' }))
|
||||
},
|
||||
|
||||
async discoverSessions(): Promise<SessionSource[]> {
|
||||
return discoverSessionsInBase(dir)
|
||||
return discoverSessionsInRoots(roots)
|
||||
},
|
||||
|
||||
createSessionParser(source: SessionSource, seenKeys: Set<string>): SessionParser {
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { getShortModelName } from "../models.js";
|
|||
import { openDatabase } from "../sqlite.js";
|
||||
import { readConfig } from "../config.js";
|
||||
import type {
|
||||
ProbeRoot,
|
||||
Provider,
|
||||
SessionParser,
|
||||
SessionSource,
|
||||
|
|
@ -524,8 +525,24 @@ class DevinSessionParser implements SessionParser {
|
|||
}
|
||||
}
|
||||
|
||||
export function createDevinProvider(cliDir: string): Provider {
|
||||
const sessionsDbPath = join(cliDir, DEVIN_SESSIONS_DB);
|
||||
function resolveDevinCliDir(override?: string): string {
|
||||
return override && override.trim() ? override : DEFAULT_DEVIN_CLI_DIR;
|
||||
}
|
||||
|
||||
function getDevinDiscoveryRoots(cliDir: string): {
|
||||
transcriptsDir: string;
|
||||
sessionsDbPath: string;
|
||||
} {
|
||||
return {
|
||||
transcriptsDir: join(cliDir, DEVIN_TRANSCRIPTS_SUBDIR),
|
||||
sessionsDbPath: join(cliDir, DEVIN_SESSIONS_DB),
|
||||
};
|
||||
}
|
||||
|
||||
export function createDevinProvider(cliDir?: string): Provider {
|
||||
const resolvedCliDir = resolveDevinCliDir(cliDir);
|
||||
const { transcriptsDir, sessionsDbPath } =
|
||||
getDevinDiscoveryRoots(resolvedCliDir);
|
||||
let sessionMetadata: Map<string, DevinSessionMetadata> | null = null;
|
||||
|
||||
const getSessionMetadata = () => {
|
||||
|
|
@ -545,10 +562,16 @@ export function createDevinProvider(cliDir: string): Provider {
|
|||
return rawTool;
|
||||
},
|
||||
|
||||
async probeRoots(): Promise<ProbeRoot[]> {
|
||||
return [
|
||||
{ path: transcriptsDir, label: "transcripts" },
|
||||
{ path: sessionsDbPath, label: "sessions.db" },
|
||||
];
|
||||
},
|
||||
|
||||
async discoverSessions(): Promise<SessionSource[]> {
|
||||
if ((await getCostFactor()) === null) return [];
|
||||
|
||||
const transcriptsDir = join(cliDir, DEVIN_TRANSCRIPTS_SUBDIR);
|
||||
const entries = await readdir(transcriptsDir).catch(() => []);
|
||||
const metadata = getSessionMetadata();
|
||||
const sources: SessionSource[] = [];
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { homedir } from 'os'
|
|||
import { readSessionFile } from '../fs-utils.js'
|
||||
import { calculateCost } 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'
|
||||
|
||||
const toolNameMap: Record<string, string> = {
|
||||
read_file: 'Read',
|
||||
|
|
@ -213,7 +213,7 @@ function createParser(source: SessionSource, seenKeys: Set<string>): SessionPars
|
|||
}
|
||||
}
|
||||
|
||||
function getGeminiTmpDir(): string {
|
||||
export function getGeminiTmpDir(): string {
|
||||
return join(homedir(), '.gemini', 'tmp')
|
||||
}
|
||||
|
||||
|
|
@ -272,6 +272,10 @@ export function createGeminiProvider(): Provider {
|
|||
return toolNameMap[rawTool] ?? rawTool
|
||||
},
|
||||
|
||||
async probeRoots(): Promise<ProbeRoot[]> {
|
||||
return [{ path: getGeminiTmpDir(), label: 'tmp' }]
|
||||
},
|
||||
|
||||
async discoverSessions(): Promise<SessionSource[]> {
|
||||
return discoverSessions()
|
||||
},
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import { flatSlice, flatString } from '../content-utils.js'
|
|||
import { calculateCost } from '../models.js'
|
||||
import { estimateTokensFromChars } from '../token-estimate.js'
|
||||
import type { ToolCall } from '../types.js'
|
||||
import type { Provider, SessionSource, SessionParser, ParsedProviderCall } from './types.js'
|
||||
import type { ProbeRoot, Provider, SessionSource, SessionParser, ParsedProviderCall } from './types.js'
|
||||
|
||||
// Kiro bills in credits: individual plans are $20/mo for 1,000 credits and
|
||||
// overage is billed at $0.04 per additional credit. We price credits at the
|
||||
|
|
@ -925,7 +925,10 @@ function createParser(source: SessionSource, seenKeys: Set<string>): SessionPars
|
|||
|
||||
// --- Discovery ---
|
||||
|
||||
function getKiroAgentDir(override?: string): string[] {
|
||||
// Pre-filter candidate list shared by probeRoots and discovery. Must not
|
||||
// existsSync-filter: Linux discovery still trims in getKiroAgentDir, but
|
||||
// doctor has to report missing defaults. Empty / blank override is unset.
|
||||
export function getKiroAgentDirCandidates(override?: string): string[] {
|
||||
if (override) return [override]
|
||||
if (process.platform === 'darwin') {
|
||||
return [join(homedir(), 'Library', 'Application Support', 'Kiro', 'User', 'globalStorage', 'kiro.kiroagent')]
|
||||
|
|
@ -933,17 +936,21 @@ function getKiroAgentDir(override?: string): string[] {
|
|||
if (process.platform === 'win32') {
|
||||
return [join(homedir(), 'AppData', 'Roaming', 'Kiro', 'User', 'globalStorage', 'kiro.kiroagent')]
|
||||
}
|
||||
return [
|
||||
join(homedir(), '.kiro-server', 'data', 'User', 'globalStorage', 'kiro.kiroagent'),
|
||||
join(homedir(), '.config', 'Kiro', 'User', 'globalStorage', 'kiro.kiroagent'),
|
||||
]
|
||||
}
|
||||
|
||||
function getKiroAgentDir(override?: string): string[] {
|
||||
const candidates = getKiroAgentDirCandidates(override)
|
||||
if (override || process.platform !== 'linux') return candidates
|
||||
// On Linux, scan both ~/.kiro-server/data/... (remote dev boxes) and
|
||||
// ~/.config/Kiro/... (local installs). Both can have data simultaneously
|
||||
// if the user switches between local and remote, or if .kiro-server exists
|
||||
// but is stale while .config/Kiro has current sessions.
|
||||
const paths: string[] = []
|
||||
const kiroServer = join(homedir(), '.kiro-server', 'data', 'User', 'globalStorage', 'kiro.kiroagent')
|
||||
const kiroConfig = join(homedir(), '.config', 'Kiro', 'User', 'globalStorage', 'kiro.kiroagent')
|
||||
if (existsSync(kiroServer)) paths.push(kiroServer)
|
||||
if (existsSync(kiroConfig)) paths.push(kiroConfig)
|
||||
// Fallback to config path if neither exists (will just find nothing)
|
||||
return paths.length > 0 ? paths : [kiroConfig]
|
||||
const existing = candidates.filter(p => existsSync(p))
|
||||
return existing.length > 0 ? existing : [candidates[candidates.length - 1]!]
|
||||
}
|
||||
|
||||
function getKiroWorkspaceStorageDir(override?: string): string {
|
||||
|
|
@ -986,26 +993,29 @@ async function resolveWorkspaceProject(agentDir: string, workspaceStorageDir: st
|
|||
return workspaceHash
|
||||
}
|
||||
|
||||
async function discoverSessions(agentDir: string, workspaceStorageDir: string, cliSessionsDir: string): Promise<SessionSource[]> {
|
||||
async function discoverSessions(agentDir: string, workspaceStorageDir: string, cliSessionsDir: string | undefined): Promise<SessionSource[]> {
|
||||
const sources: SessionSource[] = []
|
||||
|
||||
// --- Kiro CLI sessions (~/.kiro/sessions/cli/) ---
|
||||
try {
|
||||
const cliEntries = await readdir(cliSessionsDir, { withFileTypes: true })
|
||||
for (const entry of cliEntries) {
|
||||
if (!entry.isFile() || !entry.name.endsWith('.jsonl')) continue
|
||||
const jsonlPath = join(cliSessionsDir, entry.name)
|
||||
// Derive project from companion .json
|
||||
const metaPath = jsonlPath.replace(/\.jsonl$/, '.json')
|
||||
let project = 'kiro-cli'
|
||||
try {
|
||||
const raw = await readFile(metaPath, 'utf-8')
|
||||
const meta = JSON.parse(raw) as { cwd?: string }
|
||||
if (meta.cwd) project = basename(meta.cwd)
|
||||
} catch {}
|
||||
sources.push({ path: jsonlPath, project, provider: 'kiro' })
|
||||
}
|
||||
} catch {}
|
||||
// Empty CLI is skipped (do not readdir '' or '.'). Same skip in probeRoots.
|
||||
if (cliSessionsDir) {
|
||||
try {
|
||||
const cliEntries = await readdir(cliSessionsDir, { withFileTypes: true })
|
||||
for (const entry of cliEntries) {
|
||||
if (!entry.isFile() || !entry.name.endsWith('.jsonl')) continue
|
||||
const jsonlPath = join(cliSessionsDir, entry.name)
|
||||
// Derive project from companion .json
|
||||
const metaPath = jsonlPath.replace(/\.jsonl$/, '.json')
|
||||
let project = 'kiro-cli'
|
||||
try {
|
||||
const raw = await readFile(metaPath, 'utf-8')
|
||||
const meta = JSON.parse(raw) as { cwd?: string }
|
||||
if (meta.cwd) project = basename(meta.cwd)
|
||||
} catch {}
|
||||
sources.push({ path: jsonlPath, project, provider: 'kiro' })
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
// --- Kiro IDE sessions ---
|
||||
let workspaceDirs: string[]
|
||||
|
|
@ -1123,18 +1133,24 @@ async function discoverV2Sessions(sessionsRoot: string): Promise<SessionSource[]
|
|||
}
|
||||
|
||||
export function createKiroProvider(agentDirOverride?: string, workspaceStorageDirOverride?: string, cliSessionsDirOverride?: string, v2SessionsRootOverride?: string): Provider {
|
||||
const agentCandidates = getKiroAgentDirCandidates(agentDirOverride)
|
||||
const agentDirs = getKiroAgentDir(agentDirOverride)
|
||||
const wsDir = getKiroWorkspaceStorageDir(workspaceStorageDirOverride)
|
||||
// When overrides are provided (tests), don't scan real CLI sessions unless explicitly given
|
||||
const cliDir = cliSessionsDirOverride ?? (agentDirOverride ? join(agentDirOverride, '..', 'cli-sessions') : join(process.env['KIRO_HOME'] || join(homedir(), '.kiro'), 'sessions', 'cli'))
|
||||
const defaultCliDir = join(process.env['KIRO_HOME'] || join(homedir(), '.kiro'), 'sessions', 'cli')
|
||||
// Empty CLI is skipped (do not report '' or '.'). Same skip in discoverSessions.
|
||||
const cliDir = cliSessionsDirOverride === ''
|
||||
? undefined
|
||||
: cliSessionsDirOverride ?? (agentDirOverride ? join(agentDirOverride, '..', 'cli-sessions') : defaultCliDir)
|
||||
// v2 IDE sessions live under ~/.kiro/sessions/<hash>/sess_*/, a sibling of the
|
||||
// CLI store (.../sessions/cli). Derive the root from cliDir ONLY when cliDir was
|
||||
// itself explicit (default path or cliSessionsDirOverride). When only
|
||||
// agentDirOverride is set (tests), the derived cliDir parent would point at an
|
||||
// arbitrary directory (e.g. the system tmpdir) — scan nothing in that case.
|
||||
const v2Root = v2SessionsRootOverride ??
|
||||
(cliSessionsDirOverride ? dirname(cliSessionsDirOverride) :
|
||||
agentDirOverride ? undefined : dirname(cliDir))
|
||||
// CLI store (.../sessions/cli). Derive the root from an explicit CLI override
|
||||
// or the default CLI parent. Empty CLI does not drop default v2; empty v2
|
||||
// is skipped on its own. When only agentDirOverride is set (tests), do not
|
||||
// derive v2 from the test tmpdir.
|
||||
const v2Root = v2SessionsRootOverride === ''
|
||||
? undefined
|
||||
: v2SessionsRootOverride ??
|
||||
(cliSessionsDirOverride ? dirname(cliSessionsDirOverride) :
|
||||
agentDirOverride ? undefined : dirname(defaultCliDir))
|
||||
|
||||
return {
|
||||
name: 'kiro',
|
||||
|
|
@ -1153,6 +1169,16 @@ export function createKiroProvider(agentDirOverride?: string, workspaceStorageDi
|
|||
return toolNameMap[rawTool] ?? rawTool
|
||||
},
|
||||
|
||||
async probeRoots(): Promise<ProbeRoot[]> {
|
||||
const roots: ProbeRoot[] = [
|
||||
...agentCandidates.map(path => ({ path, label: 'agent' })),
|
||||
{ path: wsDir, label: 'workspace' },
|
||||
]
|
||||
if (cliDir) roots.push({ path: cliDir, label: 'cli' })
|
||||
if (v2Root) roots.push({ path: v2Root, label: 'v2' })
|
||||
return roots
|
||||
},
|
||||
|
||||
async discoverSessions(): Promise<SessionSource[]> {
|
||||
const allSources: SessionSource[] = []
|
||||
for (const agentDir of agentDirs) {
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { homedir } from 'os'
|
|||
import { readSessionFile, readSessionLines } from '../fs-utils.js'
|
||||
import { calculateCost } 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'
|
||||
import { safeNumber } from '../parser.js'
|
||||
|
||||
const METADATA_FILENAME = 'meta.json'
|
||||
|
|
@ -82,7 +82,7 @@ type VibeMessage = {
|
|||
tool_calls?: VibeToolCall[] | null
|
||||
}
|
||||
|
||||
function getMistralVibeSessionsDir(override?: string): string {
|
||||
export function getMistralVibeSessionsDir(override?: string): string {
|
||||
if (override) return override
|
||||
const configuredHome = process.env['VIBE_HOME']
|
||||
const vibeHome = configuredHome ? expandHome(configuredHome) : join(homedir(), '.vibe')
|
||||
|
|
@ -409,6 +409,10 @@ export function createMistralVibeProvider(sessionsDir?: string): Provider {
|
|||
return toolNameMap[rawTool] ?? rawTool
|
||||
},
|
||||
|
||||
async probeRoots(): Promise<ProbeRoot[]> {
|
||||
return [{ path: dir, label: 'sessions' }]
|
||||
},
|
||||
|
||||
async discoverSessions(): Promise<SessionSource[]> {
|
||||
const dirs = await discoverSessionDirs(dir)
|
||||
const sources: SessionSource[] = []
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { describe, it, expect } from 'vitest'
|
||||
import { afterEach, describe, it, expect } from 'vitest'
|
||||
import { isAbsolute, join } from 'path'
|
||||
import { homedir } from 'os'
|
||||
|
||||
|
|
@ -13,6 +13,11 @@ import {
|
|||
discoverClineTasks,
|
||||
getVSCodeGlobalStoragePaths,
|
||||
} from '../src/providers/vscode-cline-parser.js'
|
||||
import { createCodebuffProvider, getCodebuffRootSet } from '../src/providers/codebuff.js'
|
||||
import { createDevinProvider } from '../src/providers/devin.js'
|
||||
import { createGeminiProvider, getGeminiTmpDir } from '../src/providers/gemini.js'
|
||||
import { createKiroProvider, getKiroAgentDirCandidates } from '../src/providers/kiro.js'
|
||||
import { createMistralVibeProvider, getMistralVibeSessionsDir } from '../src/providers/mistral-vibe.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,
|
||||
|
|
@ -108,3 +113,159 @@ describe('probeRoots mirrors discovery resolution (Tier 2, batch 1)', () => {
|
|||
])
|
||||
})
|
||||
})
|
||||
|
||||
// #899 Tier 2, batch 2. Same contract as batch 1: probeRoots() is the exact
|
||||
// discovery-root set after the same resolution, pinned as full objects.
|
||||
describe('probeRoots mirrors discovery resolution (Tier 2, batch 2)', () => {
|
||||
const originalCodebuffDataDir = process.env['CODEBUFF_DATA_DIR']
|
||||
const originalVibeHome = process.env['VIBE_HOME']
|
||||
|
||||
afterEach(() => {
|
||||
if (originalCodebuffDataDir === undefined) delete process.env['CODEBUFF_DATA_DIR']
|
||||
else process.env['CODEBUFF_DATA_DIR'] = originalCodebuffDataDir
|
||||
if (originalVibeHome === undefined) delete process.env['VIBE_HOME']
|
||||
else process.env['VIBE_HOME'] = originalVibeHome
|
||||
})
|
||||
|
||||
it('codebuff reports all three CHANNELS on default, and empty factory is unset', async () => {
|
||||
const expected = [
|
||||
{ path: join(homedir(), '.config', 'manicode'), label: 'chats' },
|
||||
{ path: join(homedir(), '.config', 'manicode-dev'), label: 'chats' },
|
||||
{ path: join(homedir(), '.config', 'manicode-staging'), label: 'chats' },
|
||||
]
|
||||
expect(await createCodebuffProvider().probeRoots!()).toEqual(expected)
|
||||
expect(await createCodebuffProvider('').probeRoots!()).toEqual(expected)
|
||||
expect(getCodebuffRootSet()).toEqual(expected.map(r => r.path))
|
||||
expect(getCodebuffRootSet('')).toEqual(expected.map(r => r.path))
|
||||
for (const root of expected) expect(isAbsolute(root.path)).toBe(true)
|
||||
})
|
||||
|
||||
it('codebuff reports the factory root, or CODEBUFF_DATA_DIR when factory is empty', async () => {
|
||||
expect(await createCodebuffProvider('/tmp/codebuff-a').probeRoots!()).toEqual([
|
||||
{ path: '/tmp/codebuff-a', label: 'chats' },
|
||||
])
|
||||
process.env['CODEBUFF_DATA_DIR'] = '/tmp/codebuff-env'
|
||||
expect(await createCodebuffProvider().probeRoots!()).toEqual([
|
||||
{ path: '/tmp/codebuff-env', label: 'chats' },
|
||||
])
|
||||
expect(await createCodebuffProvider('').probeRoots!()).toEqual([
|
||||
{ path: '/tmp/codebuff-env', label: 'chats' },
|
||||
])
|
||||
// Non-blank factory wins over the env root.
|
||||
expect(await createCodebuffProvider('/tmp/codebuff-a').probeRoots!()).toEqual([
|
||||
{ path: '/tmp/codebuff-a', label: 'chats' },
|
||||
])
|
||||
})
|
||||
|
||||
it('devin reports transcripts + sessions.db, not the parent, and empty factory is default', async () => {
|
||||
expect(await createDevinProvider('/tmp/probe-devin').probeRoots!()).toEqual([
|
||||
{ path: join('/tmp/probe-devin', 'transcripts'), label: 'transcripts' },
|
||||
{ path: join('/tmp/probe-devin', 'sessions.db'), label: 'sessions.db' },
|
||||
])
|
||||
const defaults = [
|
||||
{ path: join(homedir(), '.local', 'share', 'devin', 'cli', 'transcripts'), label: 'transcripts' },
|
||||
{ path: join(homedir(), '.local', 'share', 'devin', 'cli', 'sessions.db'), label: 'sessions.db' },
|
||||
]
|
||||
expect(await createDevinProvider().probeRoots!()).toEqual(defaults)
|
||||
expect(await createDevinProvider('').probeRoots!()).toEqual(defaults)
|
||||
for (const root of defaults) expect(isAbsolute(root.path)).toBe(true)
|
||||
})
|
||||
|
||||
it('gemini reports only the shared tmp parent', async () => {
|
||||
const tmpDir = getGeminiTmpDir()
|
||||
expect(tmpDir).toBe(join(homedir(), '.gemini', 'tmp'))
|
||||
expect(await createGeminiProvider().probeRoots!()).toEqual([
|
||||
{ path: tmpDir, label: 'tmp' },
|
||||
])
|
||||
expect(tmpDir).not.toContain(`${join('tmp', 'chats')}`)
|
||||
expect(isAbsolute(tmpDir)).toBe(true)
|
||||
})
|
||||
|
||||
it('kiro reports the override set exactly and never the developer Application Support tree', async () => {
|
||||
const agent = '/tmp/kiro-agent'
|
||||
const workspace = '/tmp/kiro-workspace'
|
||||
const cli = '/tmp/kiro-cli'
|
||||
const v2 = '/tmp/kiro-v2'
|
||||
const roots = await createKiroProvider(agent, workspace, cli, v2).probeRoots!()
|
||||
expect(roots).toEqual([
|
||||
{ path: agent, label: 'agent' },
|
||||
{ path: workspace, label: 'workspace' },
|
||||
{ path: cli, label: 'cli' },
|
||||
{ path: v2, label: 'v2' },
|
||||
])
|
||||
for (const root of roots) {
|
||||
expect(isAbsolute(root.path)).toBe(true)
|
||||
expect(root.path).not.toContain('Application Support/Kiro')
|
||||
}
|
||||
})
|
||||
|
||||
it('kiro empty-string table matches discovery: agent/workspace fall back, empty CLI and v2 are skipped', async () => {
|
||||
const workspace = '/tmp/kiro-ws'
|
||||
const cli = '/tmp/kiro-cli'
|
||||
const v2 = '/tmp/kiro-v2'
|
||||
|
||||
const unsetAgent = await createKiroProvider(undefined, workspace, cli, v2).probeRoots!()
|
||||
const emptyAgent = await createKiroProvider('', workspace, cli, v2).probeRoots!()
|
||||
expect(emptyAgent).toEqual(unsetAgent)
|
||||
expect(emptyAgent.map(r => r.path)).toEqual([
|
||||
...getKiroAgentDirCandidates(''),
|
||||
workspace,
|
||||
cli,
|
||||
v2,
|
||||
])
|
||||
expect(emptyAgent.filter(r => r.label === 'agent').map(r => r.path)).toEqual(
|
||||
getKiroAgentDirCandidates(),
|
||||
)
|
||||
|
||||
const unsetWorkspace = await createKiroProvider('/tmp/kiro-agent', undefined, cli, v2).probeRoots!()
|
||||
const emptyWorkspace = await createKiroProvider('/tmp/kiro-agent', '', cli, v2).probeRoots!()
|
||||
expect(emptyWorkspace).toEqual(unsetWorkspace)
|
||||
expect(emptyWorkspace.find(r => r.label === 'workspace')!.path).not.toBe('')
|
||||
|
||||
const emptyCli = await createKiroProvider('/tmp/kiro-agent', workspace, '', v2).probeRoots!()
|
||||
expect(emptyCli).toEqual([
|
||||
{ path: '/tmp/kiro-agent', label: 'agent' },
|
||||
{ path: workspace, label: 'workspace' },
|
||||
{ path: v2, label: 'v2' },
|
||||
])
|
||||
expect(emptyCli.map(r => r.path)).not.toContain('')
|
||||
expect(emptyCli.map(r => r.path)).not.toContain('.')
|
||||
|
||||
const emptyV2 = await createKiroProvider('/tmp/kiro-agent', workspace, cli, '').probeRoots!()
|
||||
expect(emptyV2).toEqual([
|
||||
{ path: '/tmp/kiro-agent', label: 'agent' },
|
||||
{ path: workspace, label: 'workspace' },
|
||||
{ path: cli, label: 'cli' },
|
||||
])
|
||||
expect(emptyV2.some(r => r.label === 'v2')).toBe(false)
|
||||
|
||||
const emptyCliAndV2 = await createKiroProvider('/tmp/kiro-agent', workspace, '', '').probeRoots!()
|
||||
expect(emptyCliAndV2).toEqual([
|
||||
{ path: '/tmp/kiro-agent', label: 'agent' },
|
||||
{ path: workspace, label: 'workspace' },
|
||||
])
|
||||
|
||||
const skipped = await createKiroProvider('/tmp/kiro-agent', workspace, '', '').discoverSessions()
|
||||
expect(skipped).toEqual([])
|
||||
})
|
||||
|
||||
it('mistral-vibe reports the same joined sessions dir discovery uses', async () => {
|
||||
expect(await createMistralVibeProvider('/tmp/vibe-sessions').probeRoots!()).toEqual([
|
||||
{ path: getMistralVibeSessionsDir('/tmp/vibe-sessions'), label: 'sessions' },
|
||||
])
|
||||
expect(getMistralVibeSessionsDir('/tmp/vibe-sessions')).toBe('/tmp/vibe-sessions')
|
||||
|
||||
const defaults = await createMistralVibeProvider().probeRoots!()
|
||||
expect(defaults).toEqual([
|
||||
{ path: getMistralVibeSessionsDir(), label: 'sessions' },
|
||||
])
|
||||
expect(defaults[0]!.path).toBe(join(homedir(), '.vibe', 'logs', 'session'))
|
||||
expect(await createMistralVibeProvider('').probeRoots!()).toEqual(defaults)
|
||||
|
||||
process.env['VIBE_HOME'] = '/tmp/vibe-home'
|
||||
expect(await createMistralVibeProvider().probeRoots!()).toEqual([
|
||||
{ path: join('/tmp/vibe-home', 'logs', 'session'), label: 'sessions' },
|
||||
])
|
||||
expect(getMistralVibeSessionsDir()).toBe(join('/tmp/vibe-home', 'logs', 'session'))
|
||||
})
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue