Luigit
repositories / will

will

owned by admin

src/service/main.int.test.ts

Raw
import assert from 'node:assert/strict'
import { mkdir, mkdtemp, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { test } from 'node:test'
import { fileURLToPath } from 'node:url'
import { type RunningService, startService } from './main.ts'

const here = fileURLToPath(new URL('.', import.meta.url))

async function startTestService(): Promise<{
  service: RunningService
  base: (path: string) => string
  internal: (path: string) => string
}> {
  const dataDir = await mkdtemp(join(tmpdir(), 'will-svc-'))
  const webDist = join(dataDir, 'web-dist')
  const docsDist = join(dataDir, 'docs-dist')
  await mkdir(webDist, { recursive: true })
  await mkdir(docsDist, { recursive: true })
  await writeFile(join(webDist, 'index.html'), '<!doctype html><title>will</title>')
  await writeFile(join(docsDist, 'manifest.json'), JSON.stringify({ pages: ['architecture'] }))
  await writeFile(join(docsDist, 'architecture.html'), '<h1>architecture</h1>')
  // ephemeral ports
  const publicPort = 0
  const service = await startService({
    dataDir,
    publicAddr: `127.0.0.1:${publicPort}`,
    internalAddr: '127.0.0.1:0',
    webDist,
    docsDist,
    mediaMaxBytes: 1000 << 10,
    // 0/0 pins storageState to 'ok' for every non-negative freePct; tests
    // that need warn/stop patch the service meta thresholds explicitly.
    storageWarnPct: 0,
    storageStopPct: 0,
    workerUrl: join(here, 'worker.ts'),
  })
  const pubAddr = service.publicServer.address()
  const intAddr = service.internalServer.address()
  if (
    pubAddr === null ||
    typeof pubAddr !== 'object' ||
    intAddr === null ||
    typeof intAddr !== 'object'
  ) {
    throw new Error('no listen address')
  }
  return {
    service,
    base: (p: string) => `http://127.0.0.1:${pubAddr.port}${p}`,
    internal: (p: string) => `http://127.0.0.1:${intAddr.port}${p}`,
  }
}

test('healthz ok, index served, docs manifest served', async () => {
  const { service, base } = await startTestService()
  try {
    const health = await fetch(base('/healthz'))
    assert.equal(health.status, 200)
    assert.equal(((await health.json()) as { ok: boolean }).ok, true)

    const index = await fetch(base('/'))
    assert.match(await index.text(), /<title>will<\/title>/)

    const docs = await fetch(base('/api/docs'))
    assert.deepEqual(((await docs.json()) as { pages: string[] }).pages, ['architecture'])
    const doc = await fetch(base('/docs/architecture'))
    assert.match(await doc.text(), /architecture/)
  } finally {
    await service.close()
  }
})

test('path traversal is rejected', async () => {
  const { service, base } = await startTestService()
  try {
    const res = await fetch(base('/docs/..%2F..%2Fetc%2Fpasswd'))
    assert.equal(res.status, 404)
    const res2 = await fetch(base('/assets/../../etc/passwd'))
    assert.ok(res2.status === 403 || res2.status === 404)
  } finally {
    await service.close()
  }
})

test('audit ingest -> timeline -> SSE replay with Last-Event-ID', async () => {
  const { service, base, internal } = await startTestService()
  try {
    for (let i = 1; i <= 3; i++) {
      const res = await fetch(internal('/v1/audit/ingest'), {
        method: 'POST',
        headers: { 'content-type': 'application/json' },
        body: JSON.stringify({
          events: [{ id: `e${i}`, ts: new Date().toISOString(), kind: 'tool', payload: { i } }],
        }),
      })
      assert.equal(res.status, 200)
      const body = (await res.json()) as { accepted: number }
      assert.equal(body.accepted, 1)
    }
    const page = await fetch(base('/api/timeline?limit=10'))
    const timeline = (await page.json()) as { rows: { id: string; seq: number }[] }
    assert.equal(timeline.rows.length, 3)

    // SSE replay from seq 1 misses nothing after seq 1
    const controller = new AbortController()
    const stream = await fetch(base('/api/stream?lastEventId=1'), {
      signal: controller.signal,
      headers: { accept: 'text/event-stream' },
    })
    assert.equal(stream.status, 200)
    const reader = stream.body?.getReader()
    if (!reader) throw new Error('no stream body')
    const decoder = new TextDecoder()
    let buf = ''
    const ids: number[] = []
    while (ids.length < 2) {
      const { value, done } = await reader.read()
      if (done) break
      buf += decoder.decode(value, { stream: true })
      for (const m of buf.matchAll(/^id: (\d+)$/gm)) ids.push(Number(m[1]))
      buf = buf.split('\n\n').pop() ?? ''
    }
    controller.abort()
    assert.deepEqual(ids, [2, 3])

    // live notification: ingest while stream open
    const liveController = new AbortController()
    const live = await fetch(base('/api/stream'), { signal: liveController.signal })
    const liveReader = live.body?.getReader()
    if (!liveReader) throw new Error('no live stream body')
    await fetch(internal('/v1/audit/ingest'), {
      method: 'POST',
      headers: { 'content-type': 'application/json' },
      body: JSON.stringify({
        events: [{ id: 'e4', ts: new Date().toISOString(), kind: 'tool', payload: {} }],
      }),
    })
    let liveBuf = ''
    let sawE4 = false
    for (let i = 0; i < 20 && !sawE4; i++) {
      const { value, done } = await liveReader.read()
      if (done) break
      liveBuf += decoder.decode(value, { stream: true })
      if (liveBuf.includes('e4')) sawE4 = true
    }
    liveController.abort()
    controller.abort()
    assert.ok(sawE4, 'live SSE delivery after ingest')
  } finally {
    await service.close()
  }
})

test('media upload, meta, public download round-trip', async () => {
  const { service, base, internal } = await startTestService()
  try {
    const bytes = Buffer.from('media-bytes-here')
    const up = (await fetch(internal('/v1/media'), {
      method: 'POST',
      headers: {
        'content-type': 'application/octet-stream',
        'x-will-media-type': 'image/png',
        'x-will-media-name': 'x.png',
      },
      body: new Uint8Array(bytes),
    })) as Response
    assert.equal(up.status, 201)
    const meta = (await up.json()) as { digest: string; size: number }
    assert.equal(meta.size, bytes.length)

    const metaRes = await fetch(internal(`/v1/media/${meta.digest}/meta`))
    assert.equal(((await metaRes.json()) as { originalName: string }).originalName, 'x.png')

    const dl = await fetch(base(`/api/media/${meta.digest}`))
    assert.equal(dl.status, 200)
    assert.equal(dl.headers.get('content-type'), 'image/png')
    assert.deepEqual(Buffer.from(await dl.arrayBuffer()), bytes)

    const missing = await fetch(base(`/api/media/${'0'.repeat(64)}`))
    assert.equal(missing.status, 404)
  } finally {
    await service.close()
  }
})

test('chat ingest, hide via internal API, tombstone filters public chat', async () => {
  const { service, base, internal } = await startTestService()
  try {
    const msg = {
      id: 'tg-1',
      chatId: -100,
      messageId: 9,
      direction: 'in',
      ts: new Date().toISOString(),
      senderId: '7',
      senderName: 'friend',
      text: 'hello will',
      triggerReason: 'mention',
    }
    const ingest = await fetch(internal('/v1/chat/ingest'), {
      method: 'POST',
      headers: { 'content-type': 'application/json' },
      body: JSON.stringify({ messages: [msg] }),
    })
    assert.equal(ingest.status, 200)

    const before = await fetch(base('/api/chat'))
    const rows1 = (await before.json()) as { rows: { hidden: boolean }[] }
    assert.equal(rows1.rows.length, 1)
    assert.equal(rows1.rows[0]?.hidden, false)

    const hide = await fetch(internal('/v1/chat/hide'), {
      method: 'POST',
      headers: { 'content-type': 'application/json' },
      body: JSON.stringify({
        messageId: 9,
        requesterId: '7',
        authority: 'sender',
        ts: new Date().toISOString(),
      }),
    })
    assert.equal(hide.status, 200)

    const after = await fetch(base('/api/chat'))
    const rows2 = (await after.json()) as { rows: { hidden: boolean }[] }
    assert.equal(rows2.rows[0]?.hidden, true)
  } finally {
    await service.close()
  }
})

test('status reports schema, counts and storage; recall works over HTTP', async () => {
  const { service, base, internal } = await startTestService()
  try {
    await fetch(internal('/v1/audit/ingest'), {
      method: 'POST',
      headers: { 'content-type': 'application/json' },
      body: JSON.stringify({
        events: [{ id: 'x', ts: new Date().toISOString(), kind: 'boot', payload: {} }],
      }),
    })
    const status = await fetch(base('/api/status'))
    const body = (await status.json()) as {
      schemaVersion: number
      counts: { audit: number }
      storageState: string
    }
    assert.equal(body.schemaVersion, 1)
    assert.equal(body.counts.audit, 1)
    assert.equal(body.storageState, 'ok')

    const recall = await fetch(internal('/v1/recall'), {
      method: 'POST',
      headers: { 'content-type': 'application/json' },
      body: JSON.stringify({ kind: 'audit', q: 'boot', limit: 5 }),
    })
    const rbody = (await recall.json()) as { rows: unknown[] }
    assert.equal(rbody.rows.length, 1)
  } finally {
    await service.close()
  }
})

test('usage view aggregates turn_usage rows into daily cost', async () => {
  const { service, base, internal } = await startTestService()
  try {
    const ingest = (events: unknown[]) =>
      fetch(internal('/v1/audit/ingest'), {
        method: 'POST',
        headers: { 'content-type': 'application/json' },
        body: JSON.stringify({ events }),
      })
    const today = new Date().toISOString().slice(0, 11)
    const res = await ingest([
      {
        id: 'u1',
        ts: `${today}00:00:00Z`,
        kind: 'turn_usage',
        payload: {
          provider: 'zai',
          model: 'glm-5.3-flash',
          input: 2_000_000,
          output: 100_000,
          cacheRead: 1_000_000,
          cacheWrite: 0,
          responses: 3,
        },
      },
      {
        id: 'u2',
        ts: `${today}09:00:00Z`,
        kind: 'turn_usage',
        payload: { provider: 'other', model: 'mystery', input: 5, responses: 1 },
      },
      { id: 'u3', ts: new Date().toISOString(), kind: 'agent_end', payload: {} },
    ])
    assert.equal(res.status, 200)

    const page = await fetch(base('/api/usage'))
    assert.equal(page.status, 200)
    const report = (await page.json()) as {
      days: { date: string; input: number; responses: number; costUsd: number | null }[]
      totals: { input: number; costUsd: number | null; responses: number }
      projection: { todayUsd: number | null; projectedEomUsd: number | null }
    }
    assert.equal(report.days.length, 1)
    assert.equal(report.days[0]?.input, 2_000_005)
    assert.equal(report.days[0]?.responses, 4)
    // one unpriced model among the day's rows => cost withheld for that day
    assert.equal(report.days[0]?.costUsd, null)
    assert.equal(report.projection.projectedEomUsd, null)
  } finally {
    await service.close()
  }
})