openVikingMemoryRead.test.ts 2.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051
  1. // @vitest-environment node
  2. import { expect, it, vi } from 'vitest'
  3. import { OpenVikingMemoryProvider } from '../../src/server/openVikingMemoryProvider'
  4. import type { OpenVikingService } from '../../src/server/openVikingService'
  5. import type { WorkspaceKnowledge } from '../../src/server/workspaceKnowledge'
  6. function fixture() {
  7. const records = Array.from({ length: 20 }, (_, index) => ({
  8. schemaVersion: 1, revisions: [], memory: {
  9. id: index.toString(16).padStart(32, '0'), scope: 'agent', agentId: 'agent', content: `fact-${index}`,
  10. },
  11. }))
  12. const list = vi.fn(async () => records.map(record => `${record.memory.id}.json`))
  13. const read = vi.fn(async (uri: string) => JSON.stringify(records.find(record => uri.endsWith(`${record.memory.id}.json`))))
  14. const client = { list, read }
  15. const service = { enabled: true, ensureUser: vi.fn(async () => undefined), userClient: vi.fn(() => client) }
  16. const knowledge = { agent: vi.fn(() => ({ id: 'agent' })) }
  17. return { provider: new OpenVikingMemoryProvider(service as unknown as OpenVikingService, knowledge as unknown as WorkspaceKnowledge),
  18. records, read, list }
  19. }
  20. it('overlaps at most eight reads while preserving listing order and scope isolation', async () => {
  21. const f = fixture()
  22. let active = 0, peak = 0
  23. const release: Array<() => void> = []
  24. f.records[3]!.memory.agentId = 'another-agent'
  25. f.read.mockImplementation(async uri => {
  26. active++; peak = Math.max(peak, active)
  27. await new Promise<void>(resolve => release.push(resolve))
  28. active--
  29. return JSON.stringify(f.records.find(record => uri.endsWith(`${record.memory.id}.json`)))
  30. })
  31. const pending = f.provider.list('owner', [{ scope: 'agent', agentId: 'agent' }])
  32. await vi.waitFor(() => expect(release).toHaveLength(8))
  33. // Release batches in reverse order, forcing responses to arrive out of listing order.
  34. while (f.read.mock.calls.length < 20 || active) {
  35. release.splice(0).reverse().forEach(resolve => resolve())
  36. await new Promise(resolve => setTimeout(resolve, 0))
  37. }
  38. expect((await pending).map(memory => memory.content)).toEqual(f.records.filter(record => record.memory.agentId === 'agent').map(record => record.memory.content))
  39. expect(peak).toBe(8)
  40. expect(f.read).toHaveBeenCalledTimes(20)
  41. })
  42. it('keeps upstream failures visible and continues to reject corrupt records', async () => {
  43. const f = fixture()
  44. f.read.mockRejectedValueOnce(new Error('upstream unavailable'))
  45. await expect(f.provider.list('owner', [{ scope: 'agent', agentId: 'agent' }])).rejects.toThrow('upstream unavailable')
  46. f.read.mockResolvedValueOnce('invalid JSON')
  47. await expect(f.provider.list('owner', [{ scope: 'agent', agentId: 'agent' }])).rejects.toMatchObject({ code: 'openviking_memory_corrupt' })
  48. })