Files
tolaria/src/utils/ai-chat.test.ts
Luca Rossi 894cb45779 feat: replace Anthropic API with Claude CLI for AI chat and agent panels (#159)
* feat: add claude CLI subprocess backend for AI panels

Add claude_cli.rs module that spawns the local `claude` CLI as a
subprocess instead of calling the Anthropic API directly. This removes
the need for users to configure an API key — the CLI uses the user's
existing Claude authentication.

- find_claude_binary(): discovers claude in PATH or common locations
- check_cli(): returns install/version status for the frontend
- run_chat_stream(): spawns claude -p with stream-json for chat panel
- run_agent_stream(): spawns claude with MCP vault tools for agent panel
- Parses stream-json NDJSON events and emits typed Tauri events
- Remove http:default capability (no longer calling APIs from frontend)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: replace Anthropic API with Claude CLI for AI chat and agent panels

Remove direct Anthropic API calls and the entire tool-use loop from the frontend.
Both AI Chat and AI Agent panels now delegate to the `claude` CLI subprocess via
Tauri commands, streaming NDJSON events back to the UI.

Key changes:
- ai-chat.ts / ai-agent.ts: replace SSE/API streaming with Tauri invoke + listen
- useAIChat / useAiAgent hooks: simplified to use CLI streaming callbacks
- AIChatPanel / AiPanel: remove API key dialogs, model selectors, undo support
- SettingsPanel: remove Anthropic key field (no longer needed)
- claude_cli.rs: add comprehensive tests (37 total, 90% coverage)
- mock-handlers: replace ai_chat with check_claude_cli / stream_claude_* stubs
- Net -432 lines across 17 files

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: add --verbose flag to claude CLI subprocess args

* chore: exclude search.rs and lib.rs from coverage (qmd integration + Tauri setup)

---------

Co-authored-by: Test <test@test.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 19:49:58 +01:00

99 lines
2.7 KiB
TypeScript

import { describe, it, expect, vi } from 'vitest'
// Mock the mock-tauri module before importing ai-chat
vi.mock('../mock-tauri', () => ({
isTauri: () => false,
}))
import {
estimateTokens, buildSystemPrompt,
nextMessageId, checkClaudeCli, streamClaudeChat,
} from './ai-chat'
import type { VaultEntry } from '../types'
// --- estimateTokens ---
describe('estimateTokens', () => {
it('estimates tokens from string length', () => {
expect(estimateTokens('abcd')).toBe(1)
expect(estimateTokens('abcdefgh')).toBe(2)
})
it('accepts a number (char count)', () => {
expect(estimateTokens(100)).toBe(25)
})
})
// --- buildSystemPrompt ---
describe('buildSystemPrompt', () => {
const makeEntry = (path: string, title: string): VaultEntry => ({
path, title, filename: `${title}.md`, isA: 'Note',
aliases: [], belongsTo: [], relatedTo: [],
status: null, owner: null, cadence: null,
modifiedAt: null, createdAt: null, fileSize: 100,
snippet: '', relationships: {},
})
it('returns empty prompt for no notes', () => {
const result = buildSystemPrompt([], {})
expect(result.prompt).toBe('')
expect(result.totalTokens).toBe(0)
expect(result.truncated).toBe(false)
})
it('includes note content in the prompt', () => {
const notes = [makeEntry('/test.md', 'Test Note')]
const content = { '/test.md': '# Test Note\nHello world' }
const result = buildSystemPrompt(notes, content)
expect(result.prompt).toContain('Test Note')
expect(result.prompt).toContain('Hello world')
expect(result.totalTokens).toBeGreaterThan(0)
})
})
// --- nextMessageId ---
describe('nextMessageId', () => {
it('returns unique IDs', () => {
const id1 = nextMessageId()
const id2 = nextMessageId()
expect(id1).not.toBe(id2)
expect(id1).toMatch(/^msg-/)
})
})
// --- checkClaudeCli ---
describe('checkClaudeCli', () => {
it('returns not installed in non-Tauri environment', async () => {
const status = await checkClaudeCli()
expect(status.installed).toBe(false)
expect(status.version).toBeNull()
})
})
// --- streamClaudeChat ---
describe('streamClaudeChat', () => {
it('returns mock session in non-Tauri environment', async () => {
const onText = vi.fn()
const onDone = vi.fn()
const onError = vi.fn()
const sessionId = await streamClaudeChat('hello', undefined, undefined, {
onText,
onError,
onDone,
})
// Wait for the setTimeout mock response
await new Promise(r => setTimeout(r, 400))
expect(sessionId).toBe('mock-session')
expect(onText).toHaveBeenCalledWith(expect.stringContaining('Claude CLI'))
expect(onDone).toHaveBeenCalled()
expect(onError).not.toHaveBeenCalled()
})
})