ConusAI vendor v0.1.0-dev.20261013+85463d22

This commit is contained in:
ConusAI vendor v0.1.0-dev.20261013+85463d22
2026-10-13 20:45:57 +00:00
parent c3c6526952
commit 0da8a2a81b
67 changed files with 1510 additions and 10153 deletions
+37 -2
View File
@@ -3,12 +3,19 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
const mocks = vi.hoisted(() => ({
exportToS3: vi.fn(async () => 'exports/visitor-1/now.json' as string | null),
notes: [{ id: 'n-1', title: 'Plan' }],
loggerInfo: vi.fn(),
loggerRecordException: vi.fn(),
log: vi.fn(async () => {}),
captureException: vi.fn(),
}))
vi.mock('@/shared/api', () => ({
db: () => ({
select: () => ({ from: () => ({ where: () => ({ orderBy: () => ({ limit: async () => mocks.notes }) }) }) }),
}),
logger: () => ({ info: mocks.loggerInfo, recordException: mocks.loggerRecordException }),
audit: () => ({ log: mocks.log }),
serverErrors: () => ({ captureException: mocks.captureException }),
}))
vi.mock('@/shared/lib/optional', () => ({ exportToS3: mocks.exportToS3 }))
vi.mock('@/shared/lib/visitor-session', () => ({ ensureVisitor: async () => 'visitor-1' }))
@@ -18,11 +25,39 @@ import { exportNotes } from '@/features/export-notes/api/actions'
describe('exportNotes', () => {
beforeEach(() => {
mocks.exportToS3.mockClear()
mocks.loggerInfo.mockClear()
mocks.loggerRecordException.mockClear()
mocks.log.mockClear()
mocks.captureException.mockClear()
})
it('writes the visitor’s own notes to S3 once', async () => {
await exportNotes()
it('writes the visitor’s own notes to S3 once, logs the outcome, and audits notes.exported with the key', async () => {
const result = await exportNotes(null, new FormData())
expect(mocks.exportToS3).toHaveBeenCalledTimes(1)
expect(mocks.exportToS3).toHaveBeenCalledWith('visitor-1', mocks.notes)
expect(result).toEqual({ ok: true, key: 'exports/visitor-1/now.json', size: expect.any(Number) })
expect(mocks.loggerInfo).toHaveBeenCalledWith('notes.exported', { 'actor.id': 'visitor-1', 'export.count': 1, outcome: 'ok' })
expect(mocks.log).toHaveBeenCalledWith('notes.exported', {
actor: { kind: 'human', id: 'visitor-1' },
target: { id: 'exports/visitor-1/now.json' },
})
expect(mocks.loggerRecordException).not.toHaveBeenCalled()
})
it('reports S3 as not linked, logs it as skipped, and audits nothing, when exportToS3 returns null', async () => {
mocks.exportToS3.mockResolvedValueOnce(null)
const result = await exportNotes(null, new FormData())
expect(result).toEqual({ ok: false, message: 'S3 is not linked; nothing was exported.' })
expect(mocks.loggerInfo).toHaveBeenCalledWith('notes.exported', { 'actor.id': 'visitor-1', 'export.count': 1, outcome: 'skipped' })
expect(mocks.log).not.toHaveBeenCalled()
})
it('reports the error, logs and captures it, when the S3 write throws', async () => {
mocks.exportToS3.mockRejectedValueOnce(new Error('bucket unreachable'))
const result = await exportNotes(null, new FormData())
expect(result).toEqual({ ok: false, message: 'bucket unreachable' })
expect(mocks.loggerRecordException).toHaveBeenCalledWith(expect.any(Error), { 'conusai.action': 'notes.exported', 'actor.id': 'visitor-1' })
expect(mocks.captureException).toHaveBeenCalledTimes(1)
expect(mocks.log).not.toHaveBeenCalled()
})
})
+24 -4
View File
@@ -1,13 +1,33 @@
'use server'
import { noteRepository } from '@/entities/note'
import { db } from '@/shared/api'
import { audit, db, logger, serverErrors } from '@/shared/api'
import { exportToS3 } from '@/shared/lib/optional'
import { ensureVisitor } from '@/shared/lib/visitor-session'
/** Writes the visitor's notes to the S3 bucket as one JSON object (S3 module). */
export async function exportNotes(): Promise<void> {
export type ExportResult = { ok: true; key: string; size: number } | { ok: false; message: string }
/**
* Writes the visitor's notes to the S3 bucket as one JSON object (S3 module).
*
* The caller (a `useActionState` form) needs the object key/size or the error to show
* the visitor, and the write needs an audit event like every other action here — before
* this, the button gave no feedback at all and nothing landed in the audit timeline
* (stage sweep, 2026-10-11). The `ExportResult | null` / `FormData` signature is
* `useActionState`'s action shape, not a plain server action.
*/
export async function exportNotes(_state: ExportResult | null, _form: FormData): Promise<ExportResult> {
const actor = await ensureVisitor()
const notes = await noteRepository(db()).listByAuthor(actor)
await exportToS3(actor, notes)
try {
const key = await exportToS3(actor, notes)
logger()?.info('notes.exported', { 'actor.id': actor, 'export.count': notes.length, outcome: key ? 'ok' : 'skipped' })
if (!key) return { ok: false, message: 'S3 is not linked; nothing was exported.' }
await audit().log('notes.exported', { actor: { kind: 'human', id: actor }, target: { id: key } })
return { ok: true, key, size: Buffer.byteLength(JSON.stringify(notes)) }
} catch (error) {
logger()?.recordException(error, { 'conusai.action': 'notes.exported', 'actor.id': actor })
serverErrors().captureException(error)
return { ok: false, message: error instanceof Error ? error.message : 'the export failed' }
}
}