feat: Claude client + Voyage AI embed helper

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FFDuBhMKvoWjrWT3ZnGmmr
This commit is contained in:
2026-07-11 16:10:26 +08:00
co-authored by Claude Sonnet 4.6
parent 0e53d5c771
commit ebaa98d9f4
6 changed files with 101 additions and 2 deletions
+37
View File
@@ -0,0 +1,37 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
describe('embedText', () => {
beforeEach(() => {
process.env.VOYAGE_API_KEY = 'test-key'
})
afterEach(() => {
vi.restoreAllMocks()
})
it('returns 1024-element embedding array', async () => {
const mockEmbedding = Array.from({ length: 1024 }, (_, i) => i / 1024)
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
ok: true,
json: () => Promise.resolve({ data: [{ embedding: mockEmbedding }] }),
}))
const { embedText } = await import('@/lib/claude/embed')
const result = await embedText('forklift hit racking in zone B')
expect(result).toHaveLength(1024)
expect(result[0]).toBeCloseTo(0)
expect(result[1023]).toBeCloseTo(1023 / 1024)
})
it('throws on non-ok response', async () => {
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ ok: false, status: 401 }))
const { embedText } = await import('@/lib/claude/embed')
await expect(embedText('test')).rejects.toThrow('Voyage embed failed: 401')
})
it('throws when VOYAGE_API_KEY is missing', async () => {
delete process.env.VOYAGE_API_KEY
vi.resetModules()
const { embedText } = await import('@/lib/claude/embed')
await expect(embedText('test')).rejects.toThrow('VOYAGE_API_KEY')
})
})