mirror of
https://github.com/AgentSeal/codeburn.git
synced 2026-07-10 01:29:41 +00:00
fix(cursor): map composers to workspace via composer.composerHeaders
Recent Cursor builds renamed the per-workspace composer list from
ItemTable['composer.composerData'] to 'composer.composerHeaders' (identical
{ allComposers: [{ composerId }] } shape). loadWorkspaceMap only read the old
key, so on these builds the composer->workspace map came back empty and every
composer fell through to the 'cursor' orphan bucket, losing per-project
attribution.
Read both keys and merge their allComposers lists (backward compatible with
older installs). Add a regression test covering the new composer.composerHeaders
key and the legacy composer.composerData key.
Verified against a real workspace state.vscdb: composer.composerData absent,
composer.composerHeaders present with composerIds matching the bubbleId/
composerData rows; 21/22 calls now attribute to the real workspace instead of
the 'cursor' orphan bucket.
AI-Origin: human
This commit is contained in:
parent
174193d503
commit
400ca083a2
2 changed files with 104 additions and 13 deletions
|
|
@ -193,22 +193,31 @@ function loadWorkspaceMap(workspaceStorageDir: string): WorkspaceMapping {
|
|||
continue
|
||||
}
|
||||
try {
|
||||
// Cursor renamed the per-workspace composer list from
|
||||
// 'composer.composerData' to 'composer.composerHeaders' in newer builds
|
||||
// (identical { allComposers: [{ composerId }] } shape). Read both keys
|
||||
// and merge so the composer -> workspace mapping keeps working across
|
||||
// Cursor versions. Without this, on builds that only write
|
||||
// 'composer.composerHeaders' every composer falls through to the
|
||||
// 'cursor' orphan bucket and per-project attribution is lost.
|
||||
const rows = db.query<{ value: string }>(
|
||||
"SELECT value FROM ItemTable WHERE key='composer.composerData'",
|
||||
"SELECT value FROM ItemTable WHERE key IN ('composer.composerData', 'composer.composerHeaders')",
|
||||
)
|
||||
if (rows.length === 0) continue
|
||||
let parsed: { allComposers?: Array<{ composerId?: string }> }
|
||||
try {
|
||||
parsed = JSON.parse(rows[0]!.value)
|
||||
} catch {
|
||||
continue
|
||||
}
|
||||
const project = sanitizeWorkspaceUri(folder)
|
||||
let added = 0
|
||||
for (const c of parsed.allComposers ?? []) {
|
||||
if (typeof c.composerId === 'string') {
|
||||
result.composerToWorkspace.set(c.composerId, folder)
|
||||
added += 1
|
||||
for (const row of rows) {
|
||||
let parsed: { allComposers?: Array<{ composerId?: string }> }
|
||||
try {
|
||||
parsed = JSON.parse(row.value)
|
||||
} catch {
|
||||
continue
|
||||
}
|
||||
for (const c of parsed.allComposers ?? []) {
|
||||
if (typeof c.composerId === 'string') {
|
||||
result.composerToWorkspace.set(c.composerId, folder)
|
||||
added += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
if (added > 0) {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,11 @@
|
|||
import { describe, it, expect, beforeEach } from 'vitest'
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
|
||||
import { mkdtemp, mkdir, writeFile, rm } from 'node:fs/promises'
|
||||
import { join } from 'node:path'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { createRequire } from 'node:module'
|
||||
import { getAllProviders } from '../../src/providers/index.js'
|
||||
import { getCursorTimeFloor } from '../../src/providers/cursor.js'
|
||||
import { getCursorTimeFloor, createCursorProvider, clearCursorWorkspaceMapCache } from '../../src/providers/cursor.js'
|
||||
import { isSqliteAvailable } from '../../src/sqlite.js'
|
||||
import type { Provider } from '../../src/providers/types.js'
|
||||
|
||||
describe('cursor provider', () => {
|
||||
|
|
@ -83,3 +88,80 @@ describe('cursor cache', () => {
|
|||
expect(result).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
// Regression: Cursor renamed the per-workspace composer list key from
|
||||
// 'composer.composerData' to 'composer.composerHeaders'. loadWorkspaceMap must
|
||||
// read both, otherwise every composer orphans into the 'cursor' catch-all and
|
||||
// per-project attribution is lost.
|
||||
describe('cursor workspace mapping (composer.composerHeaders regression)', () => {
|
||||
const requireForTest = createRequire(import.meta.url)
|
||||
type TestDb = {
|
||||
exec(sql: string): void
|
||||
prepare(sql: string): { run(...params: unknown[]): void }
|
||||
close(): void
|
||||
}
|
||||
let root: string
|
||||
|
||||
function writeItemTableDb(dbPath: string, key: string, composerIds: string[]): void {
|
||||
const { DatabaseSync } = requireForTest('node:sqlite') as { DatabaseSync: new (p: string) => TestDb }
|
||||
const db = new DatabaseSync(dbPath)
|
||||
db.exec('CREATE TABLE ItemTable (key TEXT PRIMARY KEY, value TEXT)')
|
||||
if (key) {
|
||||
const value = JSON.stringify({ allComposers: composerIds.map(composerId => ({ composerId })) })
|
||||
db.prepare('INSERT INTO ItemTable (key, value) VALUES (?, ?)').run(key, value)
|
||||
}
|
||||
db.close()
|
||||
}
|
||||
|
||||
async function makeWorkspace(hash: string, folderUri: string, key: string, composerIds: string[]): Promise<void> {
|
||||
const wsDir = join(root, 'User', 'workspaceStorage', hash)
|
||||
await mkdir(wsDir, { recursive: true })
|
||||
await writeFile(join(wsDir, 'workspace.json'), JSON.stringify({ folder: folderUri }))
|
||||
writeItemTableDb(join(wsDir, 'state.vscdb'), key, composerIds)
|
||||
}
|
||||
|
||||
async function makeGlobalDb(): Promise<string> {
|
||||
const gsDir = join(root, 'User', 'globalStorage')
|
||||
await mkdir(gsDir, { recursive: true })
|
||||
const dbPath = join(gsDir, 'state.vscdb')
|
||||
// discoverSessions only needs the global DB to exist; the workspace map is
|
||||
// built from the sibling workspaceStorage dir.
|
||||
writeItemTableDb(dbPath, '', [])
|
||||
return dbPath
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
root = await mkdtemp(join(tmpdir(), 'cursor-ws-test-'))
|
||||
clearCursorWorkspaceMapCache()
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
clearCursorWorkspaceMapCache()
|
||||
await rm(root, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
it.skipIf(!isSqliteAvailable())(
|
||||
'maps composers to their workspace via composer.composerHeaders (new Cursor key)',
|
||||
async () => {
|
||||
await makeWorkspace('ws-headers', 'file:///home/user/myapp', 'composer.composerHeaders', ['comp-1', 'comp-2'])
|
||||
const dbPath = await makeGlobalDb()
|
||||
|
||||
const sources = await createCursorProvider(dbPath).discoverSessions()
|
||||
const projects = sources.map(s => s.project)
|
||||
|
||||
// Before the fix these composers orphaned to the 'cursor' catch-all.
|
||||
expect(projects).toContain('-home-user-myapp')
|
||||
},
|
||||
)
|
||||
|
||||
it.skipIf(!isSqliteAvailable())(
|
||||
'still maps composers via the legacy composer.composerData key',
|
||||
async () => {
|
||||
await makeWorkspace('ws-legacy', 'file:///home/user/legacy', 'composer.composerData', ['old-1'])
|
||||
const dbPath = await makeGlobalDb()
|
||||
|
||||
const sources = await createCursorProvider(dbPath).discoverSessions()
|
||||
expect(sources.map(s => s.project)).toContain('-home-user-legacy')
|
||||
},
|
||||
)
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue