test: increase strategic coverage

This commit is contained in:
lucaronin
2026-04-21 16:54:52 +02:00
parent 8ae1ade220
commit b785c53ef9
36 changed files with 7194 additions and 93 deletions

View File

@@ -0,0 +1,243 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { APP_STORAGE_KEYS, LEGACY_APP_STORAGE_KEYS } from '../constants/appStorage'
import { allSelection, makeEntry } from '../test-utils/noteListTestUtils'
import {
clearListSortFromLocalStorage,
countInboxByPeriod,
extractSortableProperties,
filterEntries,
filterInboxEntries,
formatSearchSubtitle,
formatSubtitle,
getSortComparator,
getSortOptionLabel,
loadSortPreferences,
parseSortConfig,
relativeDate,
saveSortPreferences,
serializeSortConfig,
} from './noteListHelpers'
const localStorageMock = (() => {
let store: Record<string, string> = {}
return {
getItem: (key: string) => store[key] ?? null,
setItem: (key: string, value: string) => { store[key] = value },
removeItem: (key: string) => { delete store[key] },
clear: () => { store = {} },
}
})()
Object.defineProperty(globalThis, 'localStorage', {
value: localStorageMock,
writable: true,
})
describe('noteListHelpers extra coverage', () => {
beforeEach(() => {
vi.useFakeTimers()
vi.setSystemTime(new Date('2026-04-21T12:00:00Z'))
localStorage.clear()
})
afterEach(() => {
vi.useRealTimers()
})
it('formats relative dates across future, recent, and older timestamps', () => {
const nowSeconds = Math.floor(Date.now() / 1000)
expect(relativeDate(nowSeconds + 86400)).toBe('Apr 22')
expect(relativeDate(nowSeconds - 30)).toBe('just now')
expect(relativeDate(nowSeconds - 5 * 60)).toBe('5m ago')
expect(relativeDate(nowSeconds - 2 * 3600)).toBe('2h ago')
expect(relativeDate(nowSeconds - 3 * 86400)).toBe('3d ago')
expect(relativeDate(nowSeconds - 10 * 86400)).toBe('Apr 11')
})
it('builds note subtitles for empty, linked, and edited notes', () => {
const modifiedEntry = makeEntry({
title: 'Project',
modifiedAt: Math.floor(Date.now() / 1000) - 3600,
createdAt: Math.floor(Date.now() / 1000) - 86400 * 2,
wordCount: 1200,
outgoingLinks: ['alpha', 'beta'],
})
const emptyEntry = makeEntry({
title: 'Empty',
modifiedAt: null,
createdAt: null,
wordCount: 0,
outgoingLinks: [],
})
expect(formatSubtitle(modifiedEntry)).toBe('1h ago · 1,200 words · 2 links')
expect(formatSubtitle(emptyEntry)).toBe('Empty')
expect(formatSearchSubtitle(modifiedEntry)).toBe('1h ago · Created 2d ago · 1,200 words · 2 links')
})
it('extracts sortable properties and labels custom property sort keys', () => {
const entries = [
makeEntry({ properties: { Priority: 'High', Owner: 'Luca' } }),
makeEntry({ properties: { Estimate: 3, Priority: 'Low' } }),
]
expect(extractSortableProperties(entries)).toEqual(['Estimate', 'Owner', 'Priority'])
expect(getSortOptionLabel('property:Priority')).toBe('Priority')
expect(getSortOptionLabel('title')).toBe('Title')
})
it('sorts entries by built-in and custom property comparators', () => {
const entries = [
makeEntry({
title: 'Gamma',
createdAt: 10,
modifiedAt: 30,
status: 'Done',
properties: { Score: 5, Start: '2026-04-18', Enabled: true },
}),
makeEntry({
title: 'Alpha',
createdAt: 20,
modifiedAt: 20,
status: 'Active',
properties: { Score: 2, Start: '2026-04-15', Enabled: false },
}),
makeEntry({
title: 'Beta',
createdAt: 15,
modifiedAt: 25,
status: null,
properties: { Score: 8, Start: 'not-a-date', Enabled: true },
}),
]
expect([...entries].sort(getSortComparator('title', 'asc')).map((entry) => entry.title)).toEqual(['Alpha', 'Beta', 'Gamma'])
expect([...entries].sort(getSortComparator('created', 'desc')).map((entry) => entry.title)).toEqual(['Alpha', 'Beta', 'Gamma'])
expect([...entries].sort(getSortComparator('status', 'asc')).map((entry) => entry.title)).toEqual(['Alpha', 'Gamma', 'Beta'])
expect([...entries].sort(getSortComparator('property:Score', 'asc')).map((entry) => entry.title)).toEqual(['Alpha', 'Gamma', 'Beta'])
expect([...entries].sort(getSortComparator('property:Start', 'asc')).map((entry) => entry.title)).toEqual(['Alpha', 'Gamma', 'Beta'])
expect([...entries].sort(getSortComparator('property:Enabled', 'asc')).map((entry) => entry.title)).toEqual(['Alpha', 'Gamma', 'Beta'])
})
it('serializes, parses, loads, and saves sort preferences with migration support', () => {
const serialized = serializeSortConfig({ option: 'property:Priority', direction: 'desc' })
expect(serialized).toBe('property:Priority:desc')
expect(parseSortConfig(serialized)).toEqual({ option: 'property:Priority', direction: 'desc' })
expect(parseSortConfig('broken')).toBeNull()
expect(parseSortConfig('title:sideways')).toBeNull()
localStorage.setItem(APP_STORAGE_KEYS.sortPreferences, JSON.stringify({
'__list__': 'title',
'type:Project': { option: 'created', direction: 'asc' },
}))
expect(loadSortPreferences()).toEqual({
'__list__': { option: 'title', direction: 'asc' },
'type:Project': { option: 'created', direction: 'asc' },
})
saveSortPreferences({
'__list__': { option: 'modified', direction: 'desc' },
})
expect(localStorage.getItem(APP_STORAGE_KEYS.sortPreferences)).toBe(JSON.stringify({
'__list__': { option: 'modified', direction: 'desc' },
}))
expect(localStorage.getItem(LEGACY_APP_STORAGE_KEYS.sortPreferences)).toBeNull()
clearListSortFromLocalStorage()
expect(localStorage.getItem(APP_STORAGE_KEYS.sortPreferences)).toBeNull()
expect(localStorage.getItem(LEGACY_APP_STORAGE_KEYS.sortPreferences)).toBeNull()
})
it('filters view, folder, favorites, and pulse selections', () => {
const entries = [
makeEntry({
path: '/vault/notes/alpha.md',
title: 'Alpha',
fileKind: 'markdown',
favorite: true,
}),
makeEntry({
path: '/vault/projects/beta.md',
title: 'Beta',
fileKind: 'markdown',
}),
makeEntry({
path: '/vault/attachments/diagram.png',
title: 'Diagram',
fileKind: 'binary',
}),
]
const views = [{
filename: 'work.view',
definition: {
name: 'Work',
icon: null,
color: null,
sort: null,
filters: {
all: [{ field: 'title', op: 'contains', value: 'Alpha' }],
},
},
}]
expect(filterEntries(entries, { kind: 'view', filename: 'work.view' }, undefined, views).map((entry) => entry.title)).toEqual(['Alpha'])
expect(filterEntries(entries, { kind: 'folder', path: 'projects' }).map((entry) => entry.title)).toEqual(['Beta'])
expect(filterEntries(entries, { kind: 'filter', filter: 'favorites' }).map((entry) => entry.title)).toEqual(['Alpha'])
expect(filterEntries(entries, { kind: 'filter', filter: 'pulse' })).toEqual([])
expect(filterEntries(entries, allSelection).map((entry) => entry.title)).toEqual(['Alpha', 'Beta'])
})
it('filters inbox entries by period and counts them', () => {
const nowSeconds = Math.floor(Date.now() / 1000)
const entries = [
makeEntry({
title: 'This Week',
organized: false,
archived: false,
isA: 'Note',
createdAt: nowSeconds - 2 * 86400,
}),
makeEntry({
title: 'This Month',
organized: false,
archived: false,
isA: 'Note',
createdAt: nowSeconds - 20 * 86400,
}),
makeEntry({
title: 'This Quarter',
organized: false,
archived: false,
isA: 'Note',
createdAt: nowSeconds - 80 * 86400,
}),
makeEntry({
title: 'Organized',
organized: true,
archived: false,
isA: 'Note',
createdAt: nowSeconds - 2 * 86400,
}),
makeEntry({
title: 'Type document',
organized: false,
archived: false,
isA: 'Type',
createdAt: nowSeconds - 2 * 86400,
}),
]
expect(filterInboxEntries(entries, 'week').map((entry) => entry.title)).toEqual(['This Week'])
expect(filterInboxEntries(entries, 'month').map((entry) => entry.title)).toEqual(['This Week', 'This Month'])
expect(filterInboxEntries(entries, 'quarter').map((entry) => entry.title)).toEqual(['This Week', 'This Month', 'This Quarter'])
expect(countInboxByPeriod(entries)).toEqual({
week: 1,
month: 2,
quarter: 3,
all: 3,
})
})
})

View File

@@ -0,0 +1,63 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import {
beginNoteOpenTrace,
failNoteOpenTrace,
finishNoteOpenTrace,
logKeyboardNavigationTrace,
markNoteOpenTrace,
} from './noteOpenPerformance'
const VITEST_WORKER_DESCRIPTOR = Object.getOwnPropertyDescriptor(globalThis, '__vitest_worker__')
describe('noteOpenPerformance additional coverage', () => {
beforeEach(() => {
vi.clearAllMocks()
if (VITEST_WORKER_DESCRIPTOR) {
Reflect.deleteProperty(globalThis, '__vitest_worker__')
}
})
afterEach(() => {
if (VITEST_WORKER_DESCRIPTOR) {
Object.defineProperty(globalThis, '__vitest_worker__', VITEST_WORKER_DESCRIPTOR)
}
})
it('logs n/a durations and cache misses when optional marks are absent', () => {
const debugSpy = vi.spyOn(console, 'debug').mockImplementation(() => {})
vi.spyOn(performance, 'now')
.mockReturnValueOnce(10)
.mockReturnValueOnce(35)
beginNoteOpenTrace('/vault/missing-stages.md', 'quick-open')
finishNoteOpenTrace('/vault/missing-stages.md')
expect(debugSpy).toHaveBeenCalledWith(
'[perf] noteOpen path=/vault/missing-stages.md source=quick-open total=25.0ms beforeNavigate=n/a contentLoad=n/a editorSwap=25.0ms cache=miss',
)
})
it('ignores trace updates when running under the vitest runtime flag', () => {
const debugSpy = vi.spyOn(console, 'debug').mockImplementation(() => {})
Object.defineProperty(globalThis, '__vitest_worker__', {
configurable: true,
value: 'worker-1',
})
beginNoteOpenTrace('/vault/ignored.md', 'sidebar')
markNoteOpenTrace('/vault/ignored.md', 'cacheReady')
failNoteOpenTrace('/vault/ignored.md', 'ignored')
finishNoteOpenTrace('/vault/ignored.md')
logKeyboardNavigationTrace('down', 999, 12)
expect(debugSpy).not.toHaveBeenCalled()
})
it('does not log quiet keyboard traces when both thresholds stay below the cutoff', () => {
const debugSpy = vi.spyOn(console, 'debug').mockImplementation(() => {})
logKeyboardNavigationTrace('down', 499, 3.9)
expect(debugSpy).not.toHaveBeenCalled()
})
})

View File

@@ -0,0 +1,84 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import {
beginNoteOpenTrace,
failNoteOpenTrace,
finishNoteOpenTrace,
logKeyboardNavigationTrace,
markNoteOpenTrace,
} from './noteOpenPerformance'
const VITEST_WORKER_DESCRIPTOR = Object.getOwnPropertyDescriptor(globalThis, '__vitest_worker__')
describe('noteOpenPerformance', () => {
beforeEach(() => {
vi.clearAllMocks()
if (VITEST_WORKER_DESCRIPTOR) {
Reflect.deleteProperty(globalThis, '__vitest_worker__')
}
})
afterEach(() => {
if (VITEST_WORKER_DESCRIPTOR) {
Object.defineProperty(globalThis, '__vitest_worker__', VITEST_WORKER_DESCRIPTOR)
}
})
it('logs a completed note-open trace with detailed stage timing', () => {
const debugSpy = vi.spyOn(console, 'debug').mockImplementation(() => {})
const nowSpy = vi.spyOn(performance, 'now')
nowSpy
.mockReturnValueOnce(100)
.mockReturnValueOnce(120)
.mockReturnValueOnce(150)
.mockReturnValueOnce(170)
.mockReturnValueOnce(200)
.mockReturnValueOnce(245)
.mockReturnValueOnce(290)
beginNoteOpenTrace('/vault/note.md', 'sidebar')
markNoteOpenTrace('/vault/note.md', 'beforeNavigateStart')
markNoteOpenTrace('/vault/note.md', 'beforeNavigateEnd')
markNoteOpenTrace('/vault/note.md', 'cacheReady')
markNoteOpenTrace('/vault/note.md', 'contentLoadStart')
markNoteOpenTrace('/vault/note.md', 'contentLoadEnd')
finishNoteOpenTrace('/vault/note.md')
expect(debugSpy).toHaveBeenCalledWith(
'[perf] noteOpen path=/vault/note.md source=sidebar total=190.0ms beforeNavigate=30.0ms contentLoad=45.0ms editorSwap=45.0ms cache=hit',
)
})
it('logs when an in-flight note open is superseded by another one', () => {
const debugSpy = vi.spyOn(console, 'debug').mockImplementation(() => {})
const nowSpy = vi.spyOn(performance, 'now')
nowSpy
.mockReturnValueOnce(10)
.mockReturnValueOnce(35)
.mockReturnValueOnce(55)
beginNoteOpenTrace('/vault/alpha.md', 'sidebar')
beginNoteOpenTrace('/vault/beta.md', 'search')
failNoteOpenTrace('/vault/beta.md', 'cleanup')
expect(debugSpy).toHaveBeenNthCalledWith(
1,
'[perf] noteOpen cancel path=/vault/alpha.md source=sidebar total=25.0ms reason=superseded',
)
expect(debugSpy).toHaveBeenNthCalledWith(
2,
'[perf] noteOpen cancel path=/vault/beta.md source=search total=20.0ms reason=cleanup',
)
})
it('only logs keyboard traces when the list is large or slow enough', () => {
const debugSpy = vi.spyOn(console, 'debug').mockImplementation(() => {})
logKeyboardNavigationTrace('down', 100, 3)
logKeyboardNavigationTrace('up', 600, 5)
expect(debugSpy).toHaveBeenCalledTimes(1)
expect(debugSpy).toHaveBeenCalledWith(
'[perf] noteListKeyboard direction=up items=600 move=5.0ms',
)
})
})

View File

@@ -0,0 +1,156 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
const {
getAiAgentDefinitionMock,
invokeMock,
isTauriState,
listenMock,
} = vi.hoisted(() => ({
getAiAgentDefinitionMock: vi.fn((agent: string) => ({
label: agent === 'codex' ? 'Codex' : 'Claude Code',
})),
invokeMock: vi.fn(),
isTauriState: { value: false },
listenMock: vi.fn(),
}))
vi.mock('../mock-tauri', () => ({
isTauri: () => isTauriState.value,
}))
vi.mock('../lib/aiAgents', () => ({
getAiAgentDefinition: getAiAgentDefinitionMock,
}))
vi.mock('@tauri-apps/api/core', () => ({
invoke: invokeMock,
}))
vi.mock('@tauri-apps/api/event', () => ({
listen: listenMock,
}))
import { streamAiAgent } from './streamAiAgent'
describe('streamAiAgent', () => {
beforeEach(() => {
vi.clearAllMocks()
isTauriState.value = false
})
afterEach(() => {
vi.useRealTimers()
})
it('uses the mock response when Tauri is unavailable', async () => {
vi.useFakeTimers()
const callbacks = {
onText: vi.fn(),
onThinking: vi.fn(),
onToolStart: vi.fn(),
onToolDone: vi.fn(),
onError: vi.fn(),
onDone: vi.fn(),
}
const promise = streamAiAgent({
agent: 'codex',
message: '<conversation_history>\n[user]: first\n\n[user]: latest\n</conversation_history>',
vaultPath: '/vault',
callbacks,
})
await vi.advanceTimersByTimeAsync(300)
await promise
expect(callbacks.onText).toHaveBeenCalledWith(
'[mock-codex turns=2] You asked: "latest" — This note is related to [[Build Laputa App]] and [[Matteo Cellini]].',
)
expect(callbacks.onDone).toHaveBeenCalledTimes(1)
expect(listenMock).not.toHaveBeenCalled()
expect(invokeMock).not.toHaveBeenCalled()
})
it('forwards streamed Tauri events and invokes the backend request', async () => {
isTauriState.value = true
const unlistenMock = vi.fn()
let eventHandler: ((event: { payload: unknown }) => void) | undefined
listenMock.mockImplementation(async (_eventName: string, handler: typeof eventHandler) => {
eventHandler = handler
return unlistenMock
})
invokeMock.mockImplementation(async () => {
eventHandler?.({ payload: { kind: 'Init', session_id: 'session-1' } })
eventHandler?.({ payload: { kind: 'ThinkingDelta', text: 'thinking...' } })
eventHandler?.({ payload: { kind: 'TextDelta', text: 'answer' } })
eventHandler?.({ payload: { kind: 'ToolStart', tool_name: 'Write', tool_id: 'tool-1', input: '{"path":"/vault/note.md"}' } })
eventHandler?.({ payload: { kind: 'ToolDone', tool_id: 'tool-1', output: 'saved' } })
eventHandler?.({ payload: { kind: 'Done' } })
return 'session-1'
})
const callbacks = {
onText: vi.fn(),
onThinking: vi.fn(),
onToolStart: vi.fn(),
onToolDone: vi.fn(),
onError: vi.fn(),
onDone: vi.fn(),
}
const promise = streamAiAgent({
agent: 'claude_code',
message: 'Explain this',
systemPrompt: 'SYSTEM',
vaultPath: '/vault',
callbacks,
})
await promise
expect(listenMock).toHaveBeenCalledWith('ai-agent-stream', expect.any(Function))
expect(invokeMock).toHaveBeenCalledWith('stream_ai_agent', {
request: {
agent: 'claude_code',
message: 'Explain this',
system_prompt: 'SYSTEM',
vault_path: '/vault',
},
})
expect(callbacks.onThinking).toHaveBeenCalledWith('thinking...')
expect(callbacks.onText).toHaveBeenCalledWith('answer')
expect(callbacks.onToolStart).toHaveBeenCalledWith('Write', 'tool-1', '{"path":"/vault/note.md"}')
expect(callbacks.onToolDone).toHaveBeenCalledWith('tool-1', 'saved')
expect(callbacks.onDone).toHaveBeenCalledTimes(1)
expect(unlistenMock).toHaveBeenCalledTimes(1)
})
it('surfaces backend invocation failures and still closes the stream', async () => {
isTauriState.value = true
const unlistenMock = vi.fn()
listenMock.mockResolvedValue(unlistenMock)
invokeMock.mockRejectedValue(new Error('backend boom'))
const callbacks = {
onText: vi.fn(),
onThinking: vi.fn(),
onToolStart: vi.fn(),
onToolDone: vi.fn(),
onError: vi.fn(),
onDone: vi.fn(),
}
await streamAiAgent({
agent: 'codex',
message: 'Explain this',
vaultPath: '/vault',
callbacks,
})
expect(callbacks.onError).toHaveBeenCalledWith('backend boom')
expect(callbacks.onDone).toHaveBeenCalledTimes(1)
expect(unlistenMock).toHaveBeenCalledTimes(1)
})
})