codeburn/tests/editor-data-paths.test.ts
Uttam Deb 2f2a14fb70
Some checks are pending
CI / semgrep (push) Waiting to run
Tests / test (22) (push) Waiting to run
Tests / test (24) (push) Waiting to run
Tests / test (26) (push) Waiting to run
Tests / test-platforms (macos-latest) (push) Waiting to run
Tests / test (22.13.0) (push) Waiting to run
Tests / test-platforms (windows-latest) (push) Waiting to run
Tests / desktop (push) Waiting to run
Honor redirected Copilot and Cursor storage roots (#1624)
* Add regressions for redirected editor storage roots

* Honor redirected data roots for Copilot and Cursor

* Verify redirected Copilot OTel discovery on Linux

* Reproduce Cursor cache collisions across storage roots

* Bind Cursor result caches to their database and backfill redirected history

* Verify redirected discovery against the latest daily cache

* Update env guard and document editor storage overrides
2026-10-04 22:17:28 +02:00

141 lines
8 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'
import { join, posix, win32 } from 'node:path'
import { tmpdir } from 'node:os'
import * as os from 'os'
import { createRequire } from 'node:module'
import { createCopilotProvider, getVSCodeGlobalStorageDirs, getVSCodeWorkspaceStorageDirs } from '../src/providers/copilot.js'
import { clearCursorWorkspaceMapCache, createCursorProvider } from '../src/providers/cursor.js'
import { computeEnvFingerprint } from '../src/session-cache.js'
import { isSqliteAvailable } from '../src/sqlite.js'
const requireForTest = createRequire(import.meta.url)
vi.mock('os', async importOriginal => {
const actual = await importOriginal<typeof import('os')>()
return { ...actual, platform: vi.fn(actual.platform) }
})
let root: string
beforeEach(async () => {
root = await mkdtemp(join(tmpdir(), 'codeburn-editor-roots-'))
vi.stubEnv('APPDATA', root)
vi.stubEnv('XDG_CONFIG_HOME', root)
vi.stubEnv('CODEBURN_COPILOT_DISABLE_OTEL', '1')
clearCursorWorkspaceMapCache()
})
afterEach(async () => {
vi.restoreAllMocks()
vi.unstubAllEnvs()
clearCursorWorkspaceMapCache()
await rm(root, { recursive: true, force: true })
})
describe('redirected editor data paths', () => {
it.each([
['win32', 'C:\\Users\\fixture', 'D:\\redirected-roaming', win32.join],
['linux', '/home/fixture', '/mnt/redirected-config', posix.join],
] as const)('uses the %s application-data override for every VS Code variant', (os, home, redirected, j) => {
vi.stubEnv(os === 'win32' ? 'APPDATA' : 'XDG_CONFIG_HOME', redirected)
for (const [getDirs, storage] of [
[getVSCodeWorkspaceStorageDirs, 'workspaceStorage'],
[getVSCodeGlobalStorageDirs, 'globalStorage'],
] as const) {
expect(getDirs(home, os)).toEqual(['Code', 'Code - Insiders', 'VSCodium'].map(editor => j(redirected, editor, 'User', storage)))
}
})
it.each([undefined, ''])('keeps platform defaults for absent or empty overrides (%s)', value => {
vi.stubEnv('APPDATA', value)
vi.stubEnv('XDG_CONFIG_HOME', value)
expect(getVSCodeWorkspaceStorageDirs('C:\\Users\\fixture', 'win32')[0]).toBe(win32.join('C:\\Users\\fixture', 'AppData', 'Roaming', 'Code', 'User', 'workspaceStorage'))
expect(getVSCodeGlobalStorageDirs('/home/fixture', 'linux')[0]).toBe('/home/fixture/.config/Code/User/globalStorage')
})
it('keeps macOS paths independent of APPDATA and XDG_CONFIG_HOME', () => {
expect(getVSCodeGlobalStorageDirs('/Users/fixture', 'darwin')).toEqual(['Code', 'Code - Insiders', 'VSCodium'].map(editor => posix.join('/Users/fixture', 'Library', 'Application Support', editor, 'User', 'globalStorage')))
})
it.skipIf(process.platform === 'darwin')('discovers Copilot journals and watches both redirected storage folders', async () => {
const workspace = join(root, 'Code', 'User', 'workspaceStorage')
const global = join(root, 'Code', 'User', 'globalStorage')
const wsChats = join(workspace, 'hash', 'chatSessions')
const globalChats = join(global, 'emptyWindowChatSessions')
await mkdir(wsChats, { recursive: true })
await mkdir(globalChats, { recursive: true })
await writeFile(join(workspace, 'hash', 'workspace.json'), JSON.stringify({ folder: 'file:///work/redirected' }))
const journal = JSON.stringify({ kind: 0, v: { sessionId: 'redirected', requests: [] } }) + '\n'
const wsPath = join(wsChats, 'workspace.jsonl')
const globalPath = join(globalChats, 'empty.jsonl')
await writeFile(wsPath, journal)
await writeFile(globalPath, journal)
const provider = createCopilotProvider(join(root, 'no-cli'), undefined, undefined, join(root, 'no-jb'), join(root, 'no-store'))
expect((await provider.discoverSessions()).map(s => s.path).sort()).toEqual([wsPath, globalPath].sort())
expect((await provider.probeRoots!()).map(r => r.path)).toEqual(expect.arrayContaining([workspace, global]))
})
it.skipIf(process.platform === 'darwin' || !isSqliteAvailable())('discovers the redirected Cursor database and its sibling workspace mapping', async () => {
const { DatabaseSync } = requireForTest('node:sqlite') as typeof import('node:sqlite')
const global = join(root, 'Cursor', 'User', 'globalStorage')
const workspace = join(root, 'Cursor', 'User', 'workspaceStorage', 'hash')
await mkdir(global, { recursive: true })
await mkdir(workspace, { recursive: true })
const dbPath = join(global, 'state.vscdb')
const globalDb = new DatabaseSync(dbPath)
globalDb.exec('CREATE TABLE ItemTable (key TEXT PRIMARY KEY, value TEXT)')
globalDb.close()
const workspaceDb = new DatabaseSync(join(workspace, 'state.vscdb'))
workspaceDb.exec('CREATE TABLE ItemTable (key TEXT PRIMARY KEY, value TEXT)')
workspaceDb.prepare('INSERT INTO ItemTable VALUES (?, ?)').run('composer.composerData', JSON.stringify({ allComposers: [{ composerId: 'redirected-composer' }] }))
workspaceDb.close()
await writeFile(join(workspace, 'workspace.json'), JSON.stringify({ folder: 'file:///work/redirected' }))
const provider = createCursorProvider()
expect(await provider.probeRoots!()).toEqual([{ path: dbPath, label: 'db' }])
expect((await provider.discoverSessions()).map(s => s.project)).toContain('-work-redirected')
})
it('keeps constructor storage overrides ahead of application-data defaults', async () => {
const workspace = join(root, 'explicit-ws')
const global = join(root, 'explicit-global')
const provider = createCopilotProvider(join(root, 'no-cli'), workspace, global, join(root, 'no-jb'), join(root, 'no-store'))
const roots = (await provider.probeRoots!()).map(r => r.path)
expect(roots).toEqual(expect.arrayContaining([workspace, global]))
expect(roots).not.toContain(join(root, 'Code', 'User', 'workspaceStorage'))
expect(await createCursorProvider(join(root, 'explicit.vscdb')).probeRoots!()).toEqual([{ path: join(root, 'explicit.vscdb'), label: 'db' }])
})
it.skipIf(!isSqliteAvailable())('discovers OTel in a redirected Linux global-storage root', async () => {
vi.stubEnv('APPDATA', join(root, 'unused-windows-root'))
const { DatabaseSync } = requireForTest('node:sqlite') as typeof import('node:sqlite')
// posix.join can operate on this temporary Windows path too: Node's fs
// accepts its forward slashes, so Linux discovery is exercised on any host.
const global = getVSCodeGlobalStorageDirs(root, 'linux')[0]!
const otelDir = posix.join(global, 'github.copilot-chat')
const dbPath = posix.join(otelDir, 'agent-traces.db')
await mkdir(otelDir, { recursive: true })
const db = new DatabaseSync(dbPath)
db.exec(`
CREATE TABLE spans (span_id TEXT PRIMARY KEY, trace_id TEXT, operation_name TEXT, start_time_ms INTEGER, response_model TEXT);
CREATE TABLE span_attributes (id INTEGER PRIMARY KEY, span_id TEXT, key TEXT, value TEXT);
INSERT INTO spans VALUES ('span', 'trace', 'chat', 1780157113020, 'gpt-4o');
INSERT INTO span_attributes VALUES (1, 'span', 'gen_ai.conversation.id', 'redirected-otel');
INSERT INTO span_attributes VALUES (2, 'span', 'gen_ai.usage.input_tokens', '100');
INSERT INTO span_attributes VALUES (3, 'span', 'gen_ai.usage.output_tokens', '20');
`)
db.close()
vi.spyOn(os, 'platform').mockReturnValue('linux')
vi.stubEnv('CODEBURN_COPILOT_DISABLE_OTEL', '')
const provider = createCopilotProvider(join(root, 'no-cli'), undefined, undefined, join(root, 'no-jb'), join(root, 'no-store'))
expect((await provider.discoverSessions()).map(s => s.path)).toContain(join(dbPath))
expect((await provider.probeRoots!()).map(r => r.path)).toContain(join(otelDir))
})
it.each(['APPDATA', 'XDG_CONFIG_HOME'])('invalidates Cursor caches on %s changes without discarding durable Copilot history', variable => {
const cursorBefore = computeEnvFingerprint('cursor')
const copilotBefore = computeEnvFingerprint('copilot')
vi.stubEnv(variable, join(root, 'moved'))
expect(computeEnvFingerprint('cursor')).not.toBe(cursorBefore)
expect(computeEnvFingerprint('copilot')).toBe(copilotBefore)
})
})