oxios 1.5.2

Oxios Agent OS — Agent Operating System powered by oxi-sdk
import { render } from '@testing-library/react'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { EmbeddingCanvas } from '@/components/memory/embedding-canvas'
import type { MemoryMapEntry } from '@/types/memory'

// ---- Mocks ----
//
// We need to control the d3-force simulation lifecycle to assert that
// the Animate toggle restarts/stops it. d3-zoom and ResizeObserver are
// stubbed because jsdom has no layout, and we provide a no-op canvas
// getContext so the draw loop does not blow up.

type SimInstance = {
  force: ReturnType<typeof vi.fn>
  alpha: ReturnType<typeof vi.fn>
  alphaDecay: ReturnType<typeof vi.fn>
  restart: ReturnType<typeof vi.fn>
  stop: ReturnType<typeof vi.fn>
  on: ReturnType<typeof vi.fn>
}

function makeFakeSim(): SimInstance {
  const sim: SimInstance = {
    force: vi.fn().mockReturnThis(),
    alpha: vi.fn(function (this: SimInstance, _v: number) {
      return this
    }),
    alphaDecay: vi.fn().mockReturnThis(),
    restart: vi.fn(function (this: SimInstance) {
      return this
    }),
    stop: vi.fn(),
    on: vi.fn(function (this: SimInstance, _event: string, _cb: () => void) {
      return this
    }),
  }
  return sim
}

const lastSim = vi.hoisted(() => ({ current: null as SimInstance | null }))

vi.mock('d3-force', () => {
  const chainable = () => {
    const obj: Record<string, unknown> = {}
    ;(Object.keys({ id: 1, distance: 1, strength: 1, radius: 1 }) as string[]).forEach((k) => {
      obj[k] = vi.fn(() => obj)
    })
    return obj
  }
  return {
    forceCenter: vi.fn(() => chainable()),
    forceCollide: vi.fn(() => chainable()),
    forceLink: vi.fn(() => chainable()),
    forceManyBody: vi.fn(() => chainable()),
    forceSimulation: vi.fn(() => {
      const s = makeFakeSim()
      lastSim.current = s
      return s
    }),
    forceX: vi.fn(() => chainable()),
    forceY: vi.fn(() => chainable()),
  }
})

vi.mock('d3-selection', () => {
  const noopSel = {
    call: vi.fn().mockReturnThis(),
    on: vi.fn().mockReturnThis(),
  }
  return { select: vi.fn(() => noopSel) }
})

vi.mock('d3-zoom', () => {
  const noopZoom = {
    scaleExtent: vi.fn().mockReturnThis(),
    on: vi.fn().mockReturnThis(),
    transform: vi.fn(),
  }
  return {
    zoom: vi.fn(() => noopZoom),
    zoomIdentity: { translate: vi.fn().mockReturnThis(), scale: vi.fn().mockReturnThis() },
  }
})

// ResizeObserver is not in jsdom.
class ResizeObserverMock {
  observe = vi.fn()
  unobserve = vi.fn()
  disconnect = vi.fn()
}
globalThis.ResizeObserver = ResizeObserverMock as unknown as typeof ResizeObserver

// jsdom does not implement canvas 2D context.
const fakeContext = {
  setTransform: vi.fn(),
  clearRect: vi.fn(),
  translate: vi.fn(),
  scale: vi.fn(),
  beginPath: vi.fn(),
  arc: vi.fn(),
  rect: vi.fn(),
  moveTo: vi.fn(),
  lineTo: vi.fn(),
  closePath: vi.fn(),
  fill: vi.fn(),
  stroke: vi.fn(),
  save: vi.fn(),
  restore: vi.fn(),
}
HTMLCanvasElement.prototype.getContext = vi.fn(
  () => fakeContext,
) as unknown as typeof HTMLCanvasElement.prototype.getContext

// We don't mock requestAnimationFrame; jsdom polyfills it as a
// microtask, so the canvas component's draw scheduling still runs.
beforeEach(() => {
  lastSim.current = null
})

afterEach(() => {
  vi.clearAllMocks()
})

const sampleEntry = (id: string, x = 0.1, y = 0.2): MemoryMapEntry => ({
  id,
  tier: 'hot',
  mem_type: 'fact',
  content_preview: `preview-${id}`,
  created_at: '2026-06-04T00:00:00Z',
  access_count: 1,
  coords_2d: [x, y],
  top_neighbors: [],
})

const ENTRIES: MemoryMapEntry[] = [
  sampleEntry('a', 0.1, 0.2),
  sampleEntry('b', -0.3, 0.4),
  sampleEntry('c', 0.0, -0.5),
]

describe('EmbeddingCanvas — Animate toggle (P0-2)', () => {
  it('starts the simulation with non-zero alpha on first mount', () => {
    render(<EmbeddingCanvas entries={ENTRIES} animate={true} />)
    expect(lastSim.current).not.toBeNull()
    // alpha() must have been called with 0.6 at least once during the
    // graph build (animate=true).
    expect(lastSim.current?.alpha).toHaveBeenCalled()
    const alphas = (lastSim.current?.alpha.mock.calls ?? []).map((c) => c[0])
    expect(alphas).toContain(0.6)
  })

  it('restart() is called when animate flips from false to true', () => {
    const { rerender } = render(<EmbeddingCanvas entries={ENTRIES} animate={false} />)
    expect(lastSim.current).not.toBeNull()
    const sim = lastSim.current!
    // After mount with animate=false, the sim should be stopped (not
    // restarted). The .alpha(0.001) used at graph-build time is fine.
    expect(sim.stop).toHaveBeenCalled()
    sim.stop.mockClear()
    sim.restart.mockClear()
    sim.alpha.mockClear()

    rerender(<EmbeddingCanvas entries={ENTRIES} animate={true} />)

    // The Animate effect must call sim.alpha(0.6).restart().
    expect(sim.alpha).toHaveBeenCalledWith(0.6)
    expect(sim.restart).toHaveBeenCalled()
  })

  it('stop() is called when animate flips from true to false', () => {
    const { rerender } = render(<EmbeddingCanvas entries={ENTRIES} animate={true} />)
    expect(lastSim.current).not.toBeNull()
    const sim = lastSim.current!
    sim.stop.mockClear()

    rerender(<EmbeddingCanvas entries={ENTRIES} animate={false} />)
    expect(sim.stop).toHaveBeenCalled()
  })
})