test: increase strategic coverage
This commit is contained in:
243
src/utils/noteListHelpers.extra.test.ts
Normal file
243
src/utils/noteListHelpers.extra.test.ts
Normal 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,
|
||||
})
|
||||
})
|
||||
})
|
||||
63
src/utils/noteOpenPerformance.extra.test.ts
Normal file
63
src/utils/noteOpenPerformance.extra.test.ts
Normal 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()
|
||||
})
|
||||
})
|
||||
84
src/utils/noteOpenPerformance.test.ts
Normal file
84
src/utils/noteOpenPerformance.test.ts
Normal 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',
|
||||
)
|
||||
})
|
||||
})
|
||||
156
src/utils/streamAiAgent.test.ts
Normal file
156
src/utils/streamAiAgent.test.ts
Normal 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)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user