Import the client, server, skills, tests, and build scripts as the starting point for the rebuild, then trim the surfaces this project does not ship: Electron desktop packaging, the marketing website, and the ESP32 firmware tree. CI keeps the web build, e2e, lockfile, release, and Docker workflows. Rename the package and CLI entry points to hermes-ui, drop the prepare hook so npm ci no longer triggers a build, and drop the removed tsconfig and nodemon references. Rewrite AGENTS.md, ARCHITECTURE.md, DEVELOPMENT.md, and the READMEs so the documented package map and hard rules match this repository. reference_code/ is ignored: it is a read-only snapshot, not an input to the build.
128 lines
6.3 KiB
TypeScript
128 lines
6.3 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
|
|
describe('Hermes schema initialization', () => {
|
|
let db: any = null
|
|
|
|
beforeEach(async () => {
|
|
vi.resetModules()
|
|
const { DatabaseSync } = await import('node:sqlite')
|
|
db = new DatabaseSync(':memory:')
|
|
vi.doMock('../../packages/server/src/db/index', () => ({
|
|
getDb: () => db,
|
|
getStoragePath: () => ':memory:',
|
|
}))
|
|
})
|
|
|
|
afterEach(() => {
|
|
db?.close()
|
|
db = null
|
|
vi.doUnmock('../../packages/server/src/db/index')
|
|
vi.resetModules()
|
|
})
|
|
|
|
it('initializes all tables with correct schemas', async () => {
|
|
const { initAllHermesTables, USAGE_TABLE, SESSIONS_TABLE, MESSAGES_TABLE, GC_ROOMS_TABLE, USERS_TABLE, USER_PROFILES_TABLE, DEVICES_TABLE, MCU_DEVICES_TABLE } =
|
|
await import('../../packages/server/src/db/hermes/schemas')
|
|
|
|
expect(() => initAllHermesTables()).not.toThrow()
|
|
|
|
// Verify core tables exist
|
|
const tables = db.prepare(`SELECT name FROM sqlite_master WHERE type='table'`).all() as Array<{ name: string }>
|
|
expect(tables.map(t => t.name)).toContain(USAGE_TABLE)
|
|
expect(tables.map(t => t.name)).toContain(SESSIONS_TABLE)
|
|
expect(tables.map(t => t.name)).toContain(MESSAGES_TABLE)
|
|
expect(tables.map(t => t.name)).toContain(GC_ROOMS_TABLE)
|
|
expect(tables.map(t => t.name)).toContain(USERS_TABLE)
|
|
expect(tables.map(t => t.name)).toContain(USER_PROFILES_TABLE)
|
|
expect(tables.map(t => t.name)).toContain(DEVICES_TABLE)
|
|
expect(tables.map(t => t.name)).toContain(MCU_DEVICES_TABLE)
|
|
|
|
// Verify USAGE_TABLE structure
|
|
const usageCols = db.prepare(`PRAGMA table_info("${USAGE_TABLE}")`).all() as Array<{ name: string }>
|
|
expect(usageCols.some(c => c.name === 'id')).toBe(true)
|
|
expect(usageCols.some(c => c.name === 'session_id')).toBe(true)
|
|
expect(usageCols.some(c => c.name === 'input_tokens')).toBe(true)
|
|
expect(usageCols.some(c => c.name === 'output_tokens')).toBe(true)
|
|
|
|
const sessionCols = db.prepare(`PRAGMA table_info("${SESSIONS_TABLE}")`).all() as Array<{ name: string }>
|
|
expect(sessionCols.some(c => c.name === 'source')).toBe(true)
|
|
expect(sessionCols.some(c => c.name === 'agent')).toBe(true)
|
|
|
|
const userCols = db.prepare(`PRAGMA table_info("${USERS_TABLE}")`).all() as Array<{ name: string }>
|
|
expect(userCols.some(c => c.name === 'id')).toBe(true)
|
|
expect(userCols.some(c => c.name === 'username')).toBe(true)
|
|
expect(userCols.some(c => c.name === 'password_hash')).toBe(true)
|
|
expect(userCols.some(c => c.name === 'role')).toBe(true)
|
|
|
|
const profileCols = db.prepare(`PRAGMA table_info("${USER_PROFILES_TABLE}")`).all() as Array<{ name: string }>
|
|
expect(profileCols.some(c => c.name === 'user_id')).toBe(true)
|
|
expect(profileCols.some(c => c.name === 'profile_name')).toBe(true)
|
|
expect(profileCols.some(c => c.name === 'is_default')).toBe(true)
|
|
|
|
const deviceCols = db.prepare(`PRAGMA table_info("${DEVICES_TABLE}")`).all() as Array<{ name: string }>
|
|
expect(deviceCols.some(c => c.name === 'id')).toBe(true)
|
|
expect(deviceCols.some(c => c.name === 'status')).toBe(true)
|
|
expect(deviceCols.some(c => c.name === 'device_public_key')).toBe(true)
|
|
|
|
const mcuDeviceCols = db.prepare(`PRAGMA table_info("${MCU_DEVICES_TABLE}")`).all() as Array<{ name: string }>
|
|
expect(mcuDeviceCols.some(c => c.name === 'id')).toBe(true)
|
|
expect(mcuDeviceCols.some(c => c.name === 'name')).toBe(true)
|
|
expect(mcuDeviceCols.some(c => c.name === 'device_code')).toBe(true)
|
|
expect(mcuDeviceCols.some(c => c.name === 'is_official')).toBe(true)
|
|
expect(mcuDeviceCols.some(c => c.name === 'created_at')).toBe(true)
|
|
})
|
|
|
|
it('preserves existing data when adding safe schema columns', async () => {
|
|
const { initAllHermesTables, USAGE_TABLE, USAGE_SCHEMA } =
|
|
await import('../../packages/server/src/db/hermes/schemas')
|
|
|
|
// Create table with minimal schema
|
|
db.exec(`CREATE TABLE "${USAGE_TABLE}" (id INTEGER PRIMARY KEY AUTOINCREMENT, session_id TEXT NOT NULL, created_at INTEGER NOT NULL)`)
|
|
|
|
// Insert test data
|
|
db.prepare(`INSERT INTO "${USAGE_TABLE}" (session_id, created_at) VALUES (?, ?)`).run('test-session', Date.now())
|
|
|
|
// Run initialization (should add safe missing columns)
|
|
expect(() => initAllHermesTables()).not.toThrow()
|
|
|
|
// Verify data is preserved
|
|
const row = db.prepare(`SELECT * FROM "${USAGE_TABLE}" WHERE session_id = ?`).get('test-session')
|
|
expect(row).toBeTruthy()
|
|
expect(row.session_id).toBe('test-session')
|
|
|
|
// Verify safe new columns were added
|
|
const cols = db.prepare(`PRAGMA table_info("${USAGE_TABLE}")`).all() as Array<{ name: string }>
|
|
expect(cols.some(c => c.name === 'input_tokens')).toBe(true)
|
|
expect(cols.some(c => c.name === 'output_tokens')).toBe(true)
|
|
})
|
|
|
|
it('handles single-column primary key tables correctly', async () => {
|
|
const { initAllHermesTables, GC_ROOM_AGENTS_TABLE } =
|
|
await import('../../packages/server/src/db/hermes/schemas')
|
|
|
|
expect(() => initAllHermesTables()).not.toThrow()
|
|
|
|
// Verify table has primary key and required columns
|
|
const tableInfo = db.prepare(`SELECT sql FROM sqlite_master WHERE type='table' AND name=?`).get(GC_ROOM_AGENTS_TABLE) as { sql: string }
|
|
expect(tableInfo.sql).toContain('PRIMARY KEY')
|
|
expect(tableInfo.sql).toContain('id')
|
|
expect(tableInfo.sql).toContain('roomId')
|
|
expect(tableInfo.sql).toContain('agentId')
|
|
|
|
// Verify we can insert multiple entries with unique id
|
|
db.prepare(`INSERT INTO "${GC_ROOM_AGENTS_TABLE}" (id, roomId, agentId, profile, name, description, invited) VALUES (?, ?, ?, ?, ?, ?, ?)`)
|
|
.run('agent-1', 'room-1', 'agent-1', 'default', 'Agent 1', '', 0)
|
|
db.prepare(`INSERT INTO "${GC_ROOM_AGENTS_TABLE}" (id, roomId, agentId, profile, name, description, invited) VALUES (?, ?, ?, ?, ?, ?, ?)`)
|
|
.run('agent-2', 'room-1', 'agent-2', 'default', 'Agent 2', '', 0)
|
|
|
|
const count = db.prepare(`SELECT COUNT(*) as count FROM "${GC_ROOM_AGENTS_TABLE}"`).get() as { count: number }
|
|
expect(count.count).toBe(2)
|
|
|
|
// Verify duplicate primary key is rejected
|
|
expect(() => {
|
|
db.prepare(`INSERT INTO "${GC_ROOM_AGENTS_TABLE}" (id, roomId, agentId, profile, name, description, invited) VALUES (?, ?, ?, ?, ?, ?, ?)`)
|
|
.run('agent-1', 'room-1', 'agent-1', 'default', 'Agent 1 Duplicate', '', 0)
|
|
}).toThrow()
|
|
})
|
|
})
|