Luigit
repositories / will

will

owned by admin

src/service/ops.int.test.ts

Raw
import assert from 'node:assert/strict'
import { mkdtemp, readdir, readFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { test } from 'node:test'
import { migrate, openDatabase } from '../shared/db.ts'
import type { AuditEvent, ChatMessageIn, DeploymentEventMirror } from '../shared/events.ts'
import { fixedClock } from '../shared/time.ts'
import { createOps } from './ops.ts'

async function freshOps(clockIso = '2026-09-12T12:00:00Z') {
  const dir = await mkdtemp(join(tmpdir(), 'will-ops-'))
  const db = openDatabase(join(dir, 'will.db'))
  migrate(db)
  const clock = fixedClock(clockIso)
  const ops = createOps(
    db,
    { dataDir: dir, archiveDir: join(dir, 'archive'), mediaDir: join(dir, 'media') },
    clock,
  )
  return { ops, dir, db }
}

function audit(id: string, ts: string, kind = 'tool'): AuditEvent {
  return {
    id,
    ts,
    kind,
    payload: { note: `payload-${id}` },
    provenance: { revision: 'r1', worktreeHead: 'abc', worktreeDirty: false },
  }
}

test('audit ingest is idempotent by event id and assigns seqs', async () => {
  const { ops } = await freshOps()
  const r1 = ops.ingestAudit([
    audit('a', '2026-09-12T10:00:00Z'),
    audit('b', '2026-09-12T11:00:00Z'),
  ])
  assert.equal(r1.accepted, 2)
  const r2 = ops.ingestAudit([audit('a', '2026-09-12T10:00:00Z')])
  assert.equal(r2.accepted, 0)
  assert.equal(r2.duplicates, 1)
  assert.equal(ops.lastSeq(), 2)
  const since = ops.eventsSince(0, 10)
  assert.equal(since.length, 2)
  assert.equal((since[0]?.payload as { note: string }).note, 'payload-a')
})

test('recall searches beyond the newest page in hot data and archives', async () => {
  const { ops } = await freshOps()
  const recent = Array.from({ length: 20 }, (_, i) =>
    audit(`noise-${i}`, `2026-09-${String(i + 1).padStart(2, '0')}T00:00:00Z`),
  )
  ops.ingestAudit([
    audit('cold-needle', '2026-01-01T00:00:00Z', 'forgotten-audit'),
    audit('hot-needle', '2026-08-01T00:00:00Z', 'remembered-audit'),
    ...recent,
  ])
  ops.ingestChat([
    {
      id: 'chat-cold-needle',
      chatId: -100,
      messageId: 1,
      direction: 'in',
      ts: '2024-01-01T00:00:00Z',
      senderName: 'old friend',
      text: 'remember this phrase',
    },
    ...recent.map((_, i) => ({
      id: `chat-noise-${i}`,
      chatId: -100,
      messageId: i + 2,
      direction: 'in' as const,
      ts: `2026-08-${String(i + 1).padStart(2, '0')}T00:00:00Z`,
      text: 'routine chatter',
    })),
  ])

  const hot = await ops.recall({ kind: 'audit', q: 'remembered-audit', limit: 2 })
  assert.deepEqual(
    (hot.rows as { id: string }[]).map((r) => r.id),
    ['hot-needle'],
  )

  await ops.archiveCycle()
  const coldAudit = await ops.recall({ kind: 'audit', q: 'forgotten-audit', limit: 2 })
  assert.deepEqual(
    (coldAudit.rows as { id: string }[]).map((r) => r.id),
    ['cold-needle'],
  )
  assert.equal(coldAudit.cold, true)
  const coldChat = await ops.recall({ kind: 'chat', q: 'remember this phrase', limit: 2 })
  assert.deepEqual(
    (coldChat.rows as { id: string }[]).map((r) => r.id),
    ['chat-cold-needle'],
  )
  assert.equal(coldChat.cold, true)
})

test('recall orders overlapping archives and late hot rows globally', async () => {
  const { ops } = await freshOps()
  ops.ingestAudit([
    audit('jan-01', '2026-01-01T00:00:00Z', 'shared-needle'),
    audit('jan-30', '2026-01-30T00:00:00Z', 'shared-needle'),
  ])
  await ops.archiveCycle()
  ops.ingestAudit([audit('jan-15', '2026-01-15T00:00:00Z', 'shared-needle')])
  await ops.archiveCycle()
  ops.ingestAudit([audit('late-jan-02', '2026-01-02T00:00:00Z', 'shared-needle')])

  const result = await ops.recall({ kind: 'audit', q: 'shared-needle', limit: 1 })
  assert.equal((result.rows[0] as { id: string }).id, 'jan-30')
  assert.equal(result.cold, true)
})

test('recall has identical Unicode case matching before and after archival', async () => {
  const { ops } = await freshOps()
  ops.ingestAudit([
    audit('kind-unicode', '2026-01-01T00:00:00Z', 'ÄPFEL'),
    audit('ÖL', '2026-01-02T00:00:00Z'),
  ])
  ops.ingestChat([
    {
      id: 'chat-unicode',
      chatId: -100,
      messageId: 1,
      direction: 'in',
      ts: '2024-01-01T00:00:00Z',
      senderName: 'ÄNNE',
      text: 'GRÜSSE',
    },
  ])

  for (const query of [
    { kind: 'audit' as const, q: 'äpfel', id: 'kind-unicode' },
    { kind: 'audit' as const, q: 'öl', id: 'ÖL' },
    { kind: 'chat' as const, q: 'änne', id: 'chat-unicode' },
    { kind: 'chat' as const, q: 'grüsse', id: 'chat-unicode' },
  ]) {
    const hot = await ops.recall({ kind: query.kind, q: query.q, limit: 2 })
    assert.equal((hot.rows[0] as { id: string }).id, query.id)
  }

  await ops.archiveCycle()
  for (const query of [
    { kind: 'audit' as const, q: 'äpfel', id: 'kind-unicode' },
    { kind: 'audit' as const, q: 'öl', id: 'ÖL' },
    { kind: 'chat' as const, q: 'änne', id: 'chat-unicode' },
    { kind: 'chat' as const, q: 'grüsse', id: 'chat-unicode' },
  ]) {
    const cold = await ops.recall({ kind: query.kind, q: query.q, limit: 2 })
    assert.equal((cold.rows[0] as { id: string }).id, query.id)
    assert.equal(cold.cold, true)
  }
})

test('recall tie ordering is limit-prefix consistent before and after archival', async () => {
  const { ops } = await freshOps()
  const ts = '2026-01-01T00:00:00Z'
  ops.ingestAudit([audit('z', ts, 'tie'), audit('a', ts, 'tie')])
  ops.ingestChat([
    { id: 'z', chatId: -100, messageId: 1, direction: 'in', ts, text: 'tie' },
    { id: 'a', chatId: -100, messageId: 2, direction: 'in', ts, text: 'tie' },
  ])

  const assertOrder = async (kind: 'audit' | 'chat') => {
    const one = await ops.recall({ kind, q: 'tie', limit: 1 })
    const two = await ops.recall({ kind, q: 'tie', limit: 2 })
    assert.deepEqual(
      (one.rows as { id: string }[]).map((r) => r.id),
      ['z'],
    )
    assert.deepEqual(
      (two.rows as { id: string }[]).map((r) => r.id),
      ['z', 'a'],
    )
  }
  await assertOrder('audit')
  await assertOrder('chat')
  await ops.archiveCycle()
  await assertOrder('audit')
  await assertOrder('chat')
})

test('chat ingest is idempotent and hide tombstones', async () => {
  const { ops } = await freshOps()
  const msg: ChatMessageIn = {
    id: 'tg-1',
    chatId: -100,
    messageId: 5,
    direction: 'in',
    ts: '2026-09-12T10:00:00Z',
    updateId: 1,
    senderId: '7',
    text: 'hello',
  }
  const r1 = ops.ingestChat([msg])
  assert.equal(r1.accepted, 1)
  const r2 = ops.ingestChat([msg])
  assert.equal(r2.duplicates, 1)

  const hidden = ops.hideMessage({
    messageId: 5,
    requesterId: '7',
    authority: 'sender',
    ts: '2026-09-12T10:05:00Z',
  })
  assert.equal(hidden.alreadyHidden, false)
  assert.equal(
    ops.hideMessage({
      messageId: 5,
      requesterId: '1',
      authority: 'oliver',
      ts: '2026-09-12T10:06:00Z',
    }).alreadyHidden,
    true,
  )

  const page = await ops.chatPage({ limit: 10 })
  assert.equal(page.rows.length, 1)
  assert.equal(page.rows[0]?.hidden, true)
})

test('deployment events mirror idempotently', async () => {
  const { ops } = await freshOps()
  const e: DeploymentEventMirror = {
    id: 'd1',
    ts: '2026-09-12T10:00:00Z',
    kind: 'promote',
    revision: 'rev9',
    detail: { ok: true },
  }
  assert.equal(ops.ingestDeploy([e]).accepted, 1)
  assert.equal(ops.ingestDeploy([e]).duplicates, 1)
  const recall = await ops.recall({ kind: 'deploy', limit: 10 })
  assert.equal(recall.rows.length, 1)
})

test('media store is content-addressed and idempotent', async () => {
  const { ops, dir } = await freshOps()
  const bytes = Buffer.from('hello media')
  const meta1 = await ops.putMedia(bytes, { type: 'text/plain', originalName: 'a.txt' })
  const meta2 = await ops.putMedia(bytes, { type: 'text/plain', originalName: 'b.txt' })
  assert.equal(meta1.digest, meta2.digest)
  assert.equal(meta1.size, bytes.length)
  const found = ops.getMediaMeta(meta1.digest)
  assert.ok(found)
  assert.equal(found.originalName, 'a.txt')
  const files = await readdir(
    join(dir, 'media', meta1.digest.slice(0, 2), meta1.digest.slice(2, 4)),
  )
  assert.deepEqual(files, [meta1.digest])
})

test('analysis cache upserts by (digest, capability, model, schemaVersion)', async () => {
  const { ops } = await freshOps()
  const key = {
    digest: 'd'.repeat(64),
    capability: 'zai_vision',
    model: 'glm-5.3-flash',
    schemaVersion: 1,
  }
  assert.equal(ops.getAnalysis(key), undefined)
  ops.putAnalysis({ ...key, result: { text: 'a cat' } })
  ops.putAnalysis({ ...key, result: { text: 'a cat v2' } })
  const got = ops.getAnalysis(key)
  assert.deepEqual(got?.result, { text: 'a cat v2' })
  const analyses = await ops.recall({ kind: 'analysis', digest: key.digest })
  assert.equal(analyses.rows.length, 1)
})

test('archive cycle moves old audit rows exactly once and cold reads serve them', async () => {
  const { ops, dir } = await freshOps()
  // clock now = 2026-09-12; insert rows older than 90 days and one fresh row
  ops.ingestAudit([
    audit('old1', '2026-01-05T00:00:00Z'),
    audit('old2', '2026-01-20T00:00:00Z'),
    audit('old3', '2026-03-01T00:00:00Z'),
    audit('new1', '2026-09-01T00:00:00Z'),
  ])
  const result = await ops.archiveCycle()
  assert.equal(result.exportedAudit, 3)
  assert.equal(result.segments, 2) // 2026-01 and 2026-03

  // hot table only has the fresh row
  assert.equal(ops.counts().audit, 1)
  assert.equal(ops.eventsSince(0, 100).length, 1)

  // timeline cold fallback pages in archived rows (newest first)
  const page = await ops.timeline({ limit: 10 })
  assert.equal(page.rows.length, 4)
  assert.equal(page.cold, true)
  assert.equal(page.rows[0]?.id, 'new1')
  assert.equal(page.rows[3]?.id, 'old1')
  assert.equal(page.rows[3]?.archived, true)

  // sidecar indexes exist and are versioned
  const indexFiles: string[] = []
  for (const month of ['2026-01', '2026-03']) {
    for (const f of await readdir(join(dir, 'archive', 'audit', month))) {
      if (f.endsWith('.index.json')) indexFiles.push(f)
    }
  }
  assert.equal(indexFiles.length, 2)
  const sidecar = JSON.parse(
    await readFile(join(dir, 'archive', 'audit', '2026-01', indexFiles[0] as string), 'utf8'),
  )
  assert.equal(sidecar.version, 1)
  assert.equal(sidecar.kind, 'audit')
  assert.equal(sidecar.count, 2)

  // second cycle exports nothing new (move exactly once)
  const second = await ops.archiveCycle()
  assert.equal(second.exportedAudit, 0)
})

test('crash between publish and delete does not duplicate rows (exactly once)', async () => {
  const { ops } = await freshOps()
  ops.ingestAudit([audit('old1', '2026-01-05T00:00:00Z')])
  // First cycle archives AND deletes. Simulate crash-after-publish by inserting
  // the same row back (as a crashed cycle would have left it) before cycle 2.
  await ops.archiveCycle()
  ops.ingestAudit([audit('old1', '2026-01-05T00:00:00Z')])
  const second = await ops.archiveCycle()
  assert.equal(second.exportedAudit, 0)
  assert.equal(ops.counts().audit, 0)
  const page = await ops.timeline({ limit: 10 })
  assert.equal(page.rows.length, 1)
})

test('archive cycle moves chat after one year and tombstones keep filtering', async () => {
  const { ops } = await freshOps()
  ops.ingestChat([
    {
      id: 'tg-old',
      chatId: -100,
      messageId: 1,
      direction: 'in',
      ts: '2024-09-01T00:00:00Z',
      text: 'ancient',
    },
    {
      id: 'tg-new',
      chatId: -100,
      messageId: 2,
      direction: 'in',
      ts: '2026-08-01T00:00:00Z',
      text: 'recent',
    },
  ])
  ops.hideMessage({
    messageId: 1,
    requesterId: '1',
    authority: 'oliver',
    ts: '2026-09-12T00:00:00Z',
  })
  const result = await ops.archiveCycle()
  assert.equal(result.exportedChat, 1)
  const page = await ops.chatPage({ limit: 10 })
  assert.equal(page.rows.length, 2)
  // hidden ancient message stays hidden even after archival
  const hiddenRow = page.rows.find((r) => r.id === 'tg-old')
  assert.equal(hiddenRow?.hidden, true)
  const recall = await ops.recall({ kind: 'chat', limit: 10 })
  assert.equal(recall.rows.length, 1) // hidden rows are excluded from recall
})

test('quick check reports ok and jobs are recorded', async () => {
  const { ops } = await freshOps()
  assert.equal(await ops.quickCheck(), 'ok')
  // Default thresholds depend on the host filesystem's free space; pin them
  // so storageState is deterministic regardless of the environment.
  ops.setStorageThresholds(0, 0)
  const status = await ops.status()
  assert.equal(status.schemaVersion, 1)
  assert.ok(status.storage.freePct >= 0)
  assert.equal(status.storageState, 'ok')
  assert.ok(status.lastQuickCheck)
  // Any non-negative freePct sits below huge thresholds: warn, then stop.
  ops.setStorageThresholds(1000, 0)
  assert.equal((await ops.status()).storageState, 'warn')
  ops.setStorageThresholds(1000, 1000)
  assert.equal((await ops.status()).storageState, 'stop')
})

test('vacuum runs without error', async () => {
  const { ops } = await freshOps()
  await ops.vacuum()
  await ops.reconcile()
})