codeburn/tests/providers/cursor.test.ts
Resham Joshi f7f64a01ab
Add new providers, fix menubar tabs, accent color picker (#167)
* Add Kiro provider and transparent auto-model naming

- Add Kiro IDE provider: parses .chat JSON files, estimates tokens,
  normalizes dot-versioned model IDs for cost lookup
- Show "Cursor (auto)", "Copilot (auto)", "Kiro (auto)" in CLI
  dashboard instead of pretending to know which model was used
- Route auto model names through BUILTIN_ALIASES for cost estimation

* Fix menubar tabs: add missing providers, show period-scoped costs

- Add Kiro, OMP to ProviderFilter enum so installed providers appear as tabs
- Merge Cursor + Cursor Agent into single Cursor tab
- Tab costs now reflect the selected period (7d/30d/month/all) instead
  of always showing today
- Tab visibility still uses today's provider list so tabs don't
  disappear when switching to periods with no data

* Add accent color picker to menubar with Apple system presets

- 9 presets using Apple's exact macOS dark-mode accent colors
  (Ember, Blue, Purple, Pink, Red, Orange, Yellow, Green, Graphite)
- Color picker in header, persisted via UserDefaults
- "Burn" text stays fixed ember regardless of accent
- ThemeState is MainActor-isolated for thread safety
- Picker state lifted to AppStore so it survives .id() tree rebuild
- Accessibility labels on all color swatches
- Renamed brandAccentDark/brandEmberDeep/brandEmberGlow to match
  their actual light/deep/glow semantics

* Fix review findings: case-sensitive cost lookup, Kiro timestamp guard, cache versioning

- Normalize provider dictionary keys to lowercase in tab cost lookup
  so "Cursor Agent" (title-case from CLI) matches providerKeys
- Guard against missing/invalid/epoch startTime in Kiro parser to
  prevent RangeError crash or 1970-01-01 ghost entries
- Bump DAILY_CACHE_VERSION to 4 so upgraded users get a clean
  recompute with the new auto-model naming (cursor-auto vs default)
- Add version field to cursor-results.json cache to invalidate stale
  entries that still use the old 'default' model name
2026-04-27 19:46:30 -07:00

77 lines
2.8 KiB
TypeScript

import { describe, it, expect, beforeEach } from 'vitest'
import { getAllProviders } from '../../src/providers/index.js'
import type { Provider } from '../../src/providers/types.js'
describe('cursor provider', () => {
let cursorProvider: Provider
beforeEach(async () => {
const all = await getAllProviders()
cursorProvider = all.find(p => p.name === 'cursor')!
})
it('is registered', () => {
expect(cursorProvider).toBeDefined()
expect(cursorProvider.name).toBe('cursor')
expect(cursorProvider.displayName).toBe('Cursor')
})
describe('model display names', () => {
it('maps cursor-auto to Cursor (auto) label', () => {
expect(cursorProvider.modelDisplayName('cursor-auto')).toBe('Cursor (auto)')
})
it('maps known models to readable names', () => {
expect(cursorProvider.modelDisplayName('claude-4.5-opus-high-thinking')).toBe('Opus 4.5 (Thinking)')
expect(cursorProvider.modelDisplayName('claude-4-sonnet-thinking')).toBe('Sonnet 4 (Thinking)')
expect(cursorProvider.modelDisplayName('grok-code-fast-1')).toBe('Grok Code Fast')
expect(cursorProvider.modelDisplayName('gemini-3-pro')).toBe('Gemini 3 Pro')
expect(cursorProvider.modelDisplayName('gpt-5')).toBe('GPT-5')
expect(cursorProvider.modelDisplayName('composer-1')).toBe('Composer 1')
})
it('returns raw name for unknown models', () => {
expect(cursorProvider.modelDisplayName('some-future-model')).toBe('some-future-model')
})
})
describe('tool display names', () => {
it('returns raw tool name as identity', () => {
expect(cursorProvider.toolDisplayName('some_tool')).toBe('some_tool')
})
})
describe('session discovery', () => {
it('returns empty when sqlite is not available', async () => {
const sessions = await cursorProvider.discoverSessions()
expect(Array.isArray(sessions)).toBe(true)
})
it('returns empty when db does not exist', async () => {
const sessions = await cursorProvider.discoverSessions()
expect(sessions.every(s => s.provider === 'cursor')).toBe(true)
})
})
})
describe('cursor sqlite adapter', () => {
it('reports availability', async () => {
const { isSqliteAvailable } = await import('../../src/sqlite.js')
const available = isSqliteAvailable()
expect(typeof available).toBe('boolean')
})
it('provides error message when not available', async () => {
const { getSqliteLoadError } = await import('../../src/sqlite.js')
const error = getSqliteLoadError()
expect(typeof error).toBe('string')
expect(error.length).toBeGreaterThan(0)
})
})
describe('cursor cache', () => {
it('returns null when no cache exists', async () => {
const { readCachedResults } = await import('../../src/cursor-cache.js')
const result = await readCachedResults('/nonexistent/path.db')
expect(result).toBeNull()
})
})