diff --git a/tests/providers/cursor-workspace-breakdown.test.ts b/tests/providers/cursor-workspace-breakdown.test.ts index 8e666b41..93c1863e 100644 --- a/tests/providers/cursor-workspace-breakdown.test.ts +++ b/tests/providers/cursor-workspace-breakdown.test.ts @@ -71,9 +71,14 @@ function createGlobalDb(composerIds: string[]): string { return dbPath } +type WorkspaceComposerRow = { + key: 'composer.composerData' | 'composer.composerHeaders' + composerIds: string[] +} + /// Creates one workspaceStorage// subdir with workspace.json (folder URI) -/// and state.vscdb (composer.composerData listing the supplied composerIds). -function createWorkspaceDir(hash: string, folderUri: string, composerIds: string[]): void { +/// and state.vscdb composer rows listing the supplied composerIds. +function createWorkspaceDirWithRows(hash: string, folderUri: string, rows: WorkspaceComposerRow[]): void { const dir = join(userDir, 'workspaceStorage', hash) mkdirSync(dir, { recursive: true }) @@ -89,20 +94,25 @@ function createWorkspaceDir(hash: string, folderUri: string, composerIds: string const { DatabaseSync: Database } = requireForTest('node:sqlite') const db = new Database(wsDbPath) db.exec(`CREATE TABLE ItemTable (key TEXT UNIQUE, value BLOB)`) - const composerData = { - allComposers: composerIds.map(id => ({ - composerId: id, - name: 'session-' + id.slice(0, 6), - unifiedMode: 'agent', - })), + const insert = db.prepare(`INSERT INTO ItemTable (key, value) VALUES (?, ?)`) + for (const row of rows) { + const composerData = { + allComposers: row.composerIds.map(id => ({ + composerId: id, + name: 'session-' + id.slice(0, 6), + unifiedMode: 'agent', + })), + } + insert.run(row.key, JSON.stringify(composerData)) } - db.prepare(`INSERT INTO ItemTable (key, value) VALUES (?, ?)`).run( - 'composer.composerData', - JSON.stringify(composerData), - ) db.close() } +/// Legacy helper: most fixtures only need the original composer.composerData key. +function createWorkspaceDir(hash: string, folderUri: string, composerIds: string[]): void { + createWorkspaceDirWithRows(hash, folderUri, [{ key: 'composer.composerData', composerIds }]) +} + async function collect(parser: { parse(): AsyncGenerator }): Promise { const out: ParsedProviderCall[] = [] for await (const call of parser.parse()) out.push(call) @@ -158,6 +168,33 @@ describe('cursor provider — per-project breakdown (#196)', () => { expect(personalComposerIds).toEqual(new Set(['composer-personal-1'])) }) + it('merges legacy composerData and composerHeaders rows for the same workspace', async () => { + if (!isSqliteAvailable()) return + + const dbPath = createGlobalDb([ + 'composer-legacy', + 'composer-headers', + 'composer-orphan', + ]) + createWorkspaceDirWithRows('hash-mixed', 'file:///Users/me/mixed-app', [ + { key: 'composer.composerData', composerIds: ['composer-legacy'] }, + { key: 'composer.composerHeaders', composerIds: ['composer-headers'] }, + ]) + + const provider = createCursorProvider(dbPath) + const sources = await provider.discoverSessions() + const mixedSource = sources.find(s => s.project === '-Users-me-mixed-app')! + const orphanSource = sources.find(s => s.project === 'cursor')! + + const mixedCalls = await collect(provider.createSessionParser(mixedSource, new Set())) + const mixedIds = new Set(mixedCalls.map(c => c.sessionId)) + expect(mixedIds).toEqual(new Set(['composer-legacy', 'composer-headers'])) + + const orphanCalls = await collect(provider.createSessionParser(orphanSource, new Set())) + const orphanIds = new Set(orphanCalls.map(c => c.sessionId)) + expect(orphanIds).toEqual(new Set(['composer-orphan'])) + }) + it('orphan source captures composers not registered in any workspace', async () => { if (!isSqliteAvailable()) return