HuggingClaw-MissionControl / src /lib /__tests__ /db-helpers.test.ts
Nyk
feat: add validated @mentions for tasks and comments
200c64b
Raw
History Blame Contribute Delete
4.91 kB
import { describe, it, expect, vi, beforeEach } from 'vitest'
// Use vi.hoisted() so mock variables are available inside vi.mock() factories
const { mockBroadcast, mockRun, mockGet, mockPrepare } = vi.hoisted(() => {
const mockRun = vi.fn(() => ({ lastInsertRowid: 1, changes: 1 }))
const mockGet = vi.fn((): any => ({ count: 1 }))
const mockPrepare = vi.fn(() => ({
run: mockRun,
get: mockGet,
all: vi.fn(() => []),
}))
const mockBroadcast = vi.fn()
return { mockBroadcast, mockRun, mockGet, mockPrepare }
})
// Mock better-sqlite3 native module to avoid needing compiled bindings
vi.mock('better-sqlite3', () => {
return {
default: vi.fn(() => ({
prepare: mockPrepare,
pragma: vi.fn(),
exec: vi.fn(),
close: vi.fn(),
})),
}
})
vi.mock('@/lib/config', () => ({
config: { dbPath: ':memory:' },
ensureDirExists: vi.fn(),
}))
vi.mock('@/lib/migrations', () => ({
runMigrations: vi.fn(),
}))
vi.mock('@/lib/password', () => ({
hashPassword: vi.fn((p: string) => `hashed:${p}`),
verifyPassword: vi.fn(() => false),
}))
vi.mock('@/lib/logger', () => ({
logger: { info: vi.fn(), error: vi.fn(), warn: vi.fn(), debug: vi.fn() },
}))
vi.mock('@/lib/event-bus', () => ({
eventBus: { broadcast: mockBroadcast, on: vi.fn(), emit: vi.fn(), setMaxListeners: vi.fn() },
}))
// Import after mocks — the real db_helpers will use our mocked getDatabase
import { db_helpers } from '@/lib/db'
describe('parseMentions', () => {
it('extracts multiple mentions', () => {
expect(db_helpers.parseMentions('@alice hello @bob')).toEqual(['alice', 'bob'])
})
it('returns empty array when no mentions', () => {
expect(db_helpers.parseMentions('no mentions here')).toEqual([])
})
it('extracts single mention', () => {
expect(db_helpers.parseMentions('hey @alice')).toEqual(['alice'])
})
it('handles @@double — captures word chars after @', () => {
const result = db_helpers.parseMentions('@@double')
expect(result).toContain('double')
})
it('handles mentions at start and end of string', () => {
expect(db_helpers.parseMentions('@start and @end')).toEqual(['start', 'end'])
})
it('supports hyphen, underscore, and dots in handles', () => {
expect(db_helpers.parseMentions('ping @agent-code_reviewer.v2 now')).toEqual(['agent-code_reviewer.v2'])
})
it('deduplicates repeated mentions case-insensitively', () => {
expect(db_helpers.parseMentions('@Alice please sync with @alice')).toEqual(['Alice'])
})
it('returns empty array for empty string', () => {
expect(db_helpers.parseMentions('')).toEqual([])
})
})
describe('logActivity', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('inserts activity into database and broadcasts event', () => {
db_helpers.logActivity('task_created', 'task', 1, 'alice', 'Created task')
expect(mockPrepare).toHaveBeenCalled()
expect(mockRun).toHaveBeenCalledWith(
'task_created', 'task', 1, 'alice', 'Created task', null, 1,
)
expect(mockBroadcast).toHaveBeenCalledWith(
'activity.created',
expect.objectContaining({
type: 'task_created',
entity_type: 'task',
entity_id: 1,
actor: 'alice',
}),
)
})
it('stringifies data when provided', () => {
const data = { key: 'value' }
db_helpers.logActivity('update', 'agent', 2, 'bob', 'Updated agent', data)
expect(mockRun).toHaveBeenCalledWith(
'update', 'agent', 2, 'bob', 'Updated agent', JSON.stringify(data), 1,
)
})
})
describe('createNotification', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('inserts notification and broadcasts event', () => {
db_helpers.createNotification('alice', 'mention', 'Mentioned', 'You were mentioned')
expect(mockRun).toHaveBeenCalledWith(
'alice', 'mention', 'Mentioned', 'You were mentioned', undefined, undefined, 1,
)
expect(mockBroadcast).toHaveBeenCalledWith(
'notification.created',
expect.objectContaining({
recipient: 'alice',
type: 'mention',
title: 'Mentioned',
}),
)
})
it('passes source_type and source_id when provided', () => {
db_helpers.createNotification('bob', 'alert', 'Alert', 'CPU high', 'agent', 5)
expect(mockRun).toHaveBeenCalledWith(
'bob', 'alert', 'Alert', 'CPU high', 'agent', 5, 1,
)
})
})
describe('updateAgentStatus', () => {
beforeEach(() => {
vi.clearAllMocks()
mockGet.mockReturnValue({ id: 42 })
})
it('updates agent status in database and broadcasts', () => {
db_helpers.updateAgentStatus('worker-1', 'busy', 'Processing task')
expect(mockPrepare).toHaveBeenCalled()
expect(mockBroadcast).toHaveBeenCalledWith(
'agent.status_changed',
expect.objectContaining({
id: 42,
name: 'worker-1',
status: 'busy',
}),
)
})
})