Files
hermes-ui/tests/server/hermes-schemas.test.ts
toor 77b1858163 Remove the API relay link and the ESP32 device feature
Two pieces of upstream promotion came out of this deployment.

The sidebar "apiRelay" tab only opened a registration page on a third-party
site, so it goes with no replacement.

The circuit badge next to Settings was not what it looked like. It opened an
ESP32 device manager: add a device, show LAN status, remote connect/disconnect,
rename, delete, backed by six REST routes and an mcu_devices table. The purchase
button inside that dialog was the promotional part; the rest was real device
management. Since the hardware is out of scope here, the entire feature is
removed rather than just the link that pointed upstream - UI entry, client API,
routes, controller, store, schema, locale strings, and the one GlobalAgentServer
method that only existed to answer its status column.

Table creation is dropped but no DROP TABLE was added, so an already-provisioned
database keeps its rows as harmless orphans instead of losing data on upgrade.

DevicesView and the hermes.devices route are untouched: that is LAN device
discovery, a separate feature.

AGENTS.md now records the removal so the badge does not come back.
2026-10-19 01:32:51 +08:00

120 lines
5.7 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 } =
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)
// 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)
})
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()
})
})