mirror of
https://github.com/EKKOLearnAI/hermes-web-ui.git
synced 2026-07-13 20:20:46 +00:00
404 lines
12 KiB
TypeScript
404 lines
12 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
|
|
describe('STT provider settings schema', () => {
|
|
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()
|
|
})
|
|
|
|
async function initStores(): Promise<void> {
|
|
const { initAllStores } = await import('../../packages/server/src/db/hermes/init')
|
|
initAllStores()
|
|
}
|
|
|
|
function tableNames(): string[] {
|
|
return db.prepare("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name").all().map((row: any) => row.name)
|
|
}
|
|
|
|
it('creates stt_provider_settings table during store init', async () => {
|
|
await initStores()
|
|
|
|
expect(tableNames()).toContain('stt_provider_settings')
|
|
const columns = db.prepare('PRAGMA table_info(stt_provider_settings)').all().map((row: any) => row.name)
|
|
expect(columns).toEqual(expect.arrayContaining([
|
|
'id',
|
|
'user_id',
|
|
'provider',
|
|
'settings_json',
|
|
'secrets_json',
|
|
'created_at',
|
|
'updated_at',
|
|
]))
|
|
})
|
|
|
|
it('rejects duplicate provider settings for the same user', async () => {
|
|
await initStores()
|
|
|
|
const insert = db.prepare('INSERT INTO stt_provider_settings (user_id, provider) VALUES (?, ?)')
|
|
insert.run(1, 'openai')
|
|
|
|
expect(() => insert.run(1, 'openai')).toThrow(/UNIQUE constraint failed/)
|
|
})
|
|
|
|
it('allows the same provider for different users', async () => {
|
|
await initStores()
|
|
|
|
const insert = db.prepare('INSERT INTO stt_provider_settings (user_id, provider) VALUES (?, ?)')
|
|
insert.run(1, 'openai')
|
|
|
|
expect(() => insert.run(2, 'openai')).not.toThrow()
|
|
expect(db.prepare('SELECT COUNT(*) AS count FROM stt_provider_settings WHERE provider = ?').get('openai').count).toBe(2)
|
|
})
|
|
|
|
it('migrates legacy stt_provider_settings tables that still default user_id to 0', async () => {
|
|
db.exec(`
|
|
CREATE TABLE stt_provider_settings (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
user_id INTEGER NOT NULL DEFAULT 0,
|
|
provider TEXT NOT NULL,
|
|
settings_json TEXT NOT NULL DEFAULT '{}',
|
|
secrets_json TEXT NOT NULL DEFAULT '{}',
|
|
created_at INTEGER NOT NULL DEFAULT (strftime('%s','now')),
|
|
updated_at INTEGER NOT NULL DEFAULT (strftime('%s','now'))
|
|
)
|
|
`)
|
|
db.exec('CREATE INDEX idx_stt_provider_settings_user ON stt_provider_settings(user_id)')
|
|
db.exec('CREATE UNIQUE INDEX idx_stt_provider_settings_user_provider ON stt_provider_settings(user_id, provider)')
|
|
db.prepare(
|
|
'INSERT INTO stt_provider_settings (user_id, provider, settings_json, secrets_json, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)'
|
|
).run(7, 'deepgram', '{"language":"en"}', '{"apiKey":"secret"}', 1710000000, 1710000100)
|
|
|
|
await initStores()
|
|
|
|
const userIdColumn = db.prepare('PRAGMA table_info(stt_provider_settings)').all().find((row: any) => row.name === 'user_id')
|
|
expect(userIdColumn?.dflt_value).toBeNull()
|
|
|
|
const insertWithoutUserId = db.prepare('INSERT INTO stt_provider_settings (provider) VALUES (?)')
|
|
expect(() => insertWithoutUserId.run('openai')).toThrow(/NOT NULL constraint failed: stt_provider_settings\.user_id/)
|
|
|
|
const preservedRow = db.prepare('SELECT * FROM stt_provider_settings WHERE user_id = ? AND provider = ?').get(7, 'deepgram')
|
|
expect(preservedRow).toMatchObject({
|
|
user_id: 7,
|
|
provider: 'deepgram',
|
|
settings_json: '{"language":"en"}',
|
|
secrets_json: '{"apiKey":"secret"}',
|
|
created_at: 1710000000,
|
|
updated_at: 1710000100,
|
|
})
|
|
})
|
|
|
|
it('rejects inserts that omit user_id', async () => {
|
|
await initStores()
|
|
|
|
const insertWithoutUserId = db.prepare('INSERT INTO stt_provider_settings (provider) VALUES (?)')
|
|
|
|
expect(() => insertWithoutUserId.run('openai')).toThrow(/NOT NULL constraint failed: stt_provider_settings\.user_id/)
|
|
})
|
|
})
|
|
|
|
describe('stt settings store', () => {
|
|
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()
|
|
})
|
|
|
|
async function initStore() {
|
|
const schemas = await import('../../packages/server/src/db/hermes/schemas')
|
|
schemas.initAllHermesTables()
|
|
return {
|
|
schemas,
|
|
store: await import('../../packages/server/src/db/hermes/stt-settings-store'),
|
|
}
|
|
}
|
|
|
|
it('stores settings and masks secrets on read', async () => {
|
|
const { store } = await initStore()
|
|
|
|
const saved = store.saveSttProviderSetting('default', 'openai', {
|
|
settings: {
|
|
baseUrl: 'https://api.openai.com/v1/audio/transcriptions',
|
|
model: 'gpt-4o-transcribe',
|
|
language: 'en',
|
|
},
|
|
secrets: {
|
|
apiKey: 'secret-value',
|
|
},
|
|
})
|
|
|
|
expect(saved).toMatchObject({
|
|
profile: 'default',
|
|
provider: 'openai',
|
|
settings: {
|
|
baseUrl: 'https://api.openai.com/v1/audio/transcriptions',
|
|
model: 'gpt-4o-transcribe',
|
|
language: 'en',
|
|
},
|
|
secrets: {
|
|
apiKey: '[stored]',
|
|
},
|
|
})
|
|
expect(JSON.stringify(saved)).not.toContain('secret-value')
|
|
|
|
const fetched = store.getSttProviderSetting('default', 'openai')
|
|
expect(fetched).toEqual(saved)
|
|
expect(JSON.stringify(fetched)).not.toContain('secret-value')
|
|
})
|
|
|
|
it('returns raw secrets only for backend callers that opt in', async () => {
|
|
const { store } = await initStore()
|
|
|
|
store.saveSttProviderSetting('default', 'openai', {
|
|
settings: {
|
|
baseUrl: 'https://api.openai.com/v1/audio/transcriptions',
|
|
model: 'gpt-4o-transcribe',
|
|
},
|
|
secrets: {
|
|
apiKey: 'secret-value',
|
|
},
|
|
})
|
|
|
|
expect(store.getSttProviderSetting('default', 'openai', { includeSecrets: true })).toMatchObject({
|
|
profile: 'default',
|
|
provider: 'openai',
|
|
settings: {
|
|
baseUrl: 'https://api.openai.com/v1/audio/transcriptions',
|
|
model: 'gpt-4o-transcribe',
|
|
},
|
|
secrets: {
|
|
apiKey: 'secret-value',
|
|
},
|
|
})
|
|
})
|
|
|
|
it('preserves existing raw secrets when the stored marker is submitted again', async () => {
|
|
const { store } = await initStore()
|
|
|
|
store.saveSttProviderSetting('default', 'openai', {
|
|
settings: {
|
|
baseUrl: 'https://api.openai.com/v1/audio/transcriptions',
|
|
model: 'gpt-4o-transcribe',
|
|
},
|
|
secrets: {
|
|
apiKey: 'secret-value',
|
|
},
|
|
})
|
|
|
|
const updated = store.saveSttProviderSetting('default', 'openai', {
|
|
settings: {
|
|
language: 'en',
|
|
prompt: 'Transcribe carefully',
|
|
},
|
|
secrets: {
|
|
apiKey: '[stored]',
|
|
},
|
|
})
|
|
|
|
expect(updated).toMatchObject({
|
|
profile: 'default',
|
|
provider: 'openai',
|
|
settings: {
|
|
baseUrl: 'https://api.openai.com/v1/audio/transcriptions',
|
|
model: 'gpt-4o-transcribe',
|
|
language: 'en',
|
|
prompt: 'Transcribe carefully',
|
|
},
|
|
secrets: {
|
|
apiKey: '[stored]',
|
|
},
|
|
})
|
|
|
|
expect(store.getSttProviderSetting('default', 'openai', { includeSecrets: true })?.secrets).toEqual({
|
|
apiKey: 'secret-value',
|
|
})
|
|
})
|
|
|
|
it('trims and bounds prompt length to 1000 characters', async () => {
|
|
const { store } = await initStore()
|
|
const trimmedPrompt = 'x'.repeat(1000)
|
|
|
|
const saved = store.saveSttProviderSetting('default', 'openai', {
|
|
settings: {
|
|
prompt: ` ${trimmedPrompt}extra `,
|
|
},
|
|
})
|
|
|
|
expect(saved.settings.prompt).toBe(trimmedPrompt)
|
|
expect(saved.settings.prompt).toHaveLength(1000)
|
|
|
|
const storedRow = db.prepare(
|
|
'SELECT settings_json FROM stt_profile_provider_settings WHERE profile = ? AND provider = ?'
|
|
).get('default', 'openai') as { settings_json: string }
|
|
|
|
expect(JSON.parse(storedRow.settings_json)).toMatchObject({
|
|
prompt: trimmedPrompt,
|
|
})
|
|
})
|
|
|
|
it('clears one stored secret without deleting settings', async () => {
|
|
const { store } = await initStore()
|
|
|
|
store.saveSttProviderSetting('default', 'openai', {
|
|
settings: {
|
|
baseUrl: 'https://api.openai.com/v1/audio/transcriptions',
|
|
model: 'gpt-4o-transcribe',
|
|
language: 'en',
|
|
},
|
|
secrets: {
|
|
apiKey: 'secret-value',
|
|
},
|
|
})
|
|
|
|
const cleared = store.clearStoredSttSecret('default', 'openai', 'apiKey')
|
|
expect(cleared).toMatchObject({
|
|
profile: 'default',
|
|
provider: 'openai',
|
|
settings: {
|
|
baseUrl: 'https://api.openai.com/v1/audio/transcriptions',
|
|
model: 'gpt-4o-transcribe',
|
|
language: 'en',
|
|
},
|
|
secrets: {},
|
|
})
|
|
|
|
expect(store.getSttProviderSetting('default', 'openai')).toEqual(cleared)
|
|
expect(store.getSttProviderSetting('default', 'openai', { includeSecrets: true })?.secrets).toEqual({})
|
|
})
|
|
|
|
it('lists only settings for the requested profile', async () => {
|
|
const { store } = await initStore()
|
|
|
|
const expected = store.saveSttProviderSetting('default', 'openai', {
|
|
settings: {
|
|
baseUrl: 'https://api.openai.com/v1/audio/transcriptions',
|
|
model: 'gpt-4o-transcribe',
|
|
},
|
|
secrets: {
|
|
apiKey: 'secret-value',
|
|
},
|
|
})
|
|
|
|
store.saveSttProviderSetting('research', 'custom', {
|
|
settings: {
|
|
baseUrl: 'https://transcribe.example.com/v1',
|
|
model: 'custom-whisper',
|
|
},
|
|
secrets: {
|
|
apiKey: 'other-secret',
|
|
},
|
|
})
|
|
|
|
expect(store.listSttProviderSettings('default')).toEqual([expected])
|
|
})
|
|
|
|
it('ignores unsupported legacy provider rows when listing settings', async () => {
|
|
const { store } = await initStore()
|
|
|
|
const expected = store.saveSttProviderSetting('default', 'openai', {
|
|
settings: {
|
|
baseUrl: 'https://api.openai.com/v1/audio/transcriptions',
|
|
model: 'gpt-4o-transcribe',
|
|
},
|
|
secrets: {
|
|
apiKey: 'secret-value',
|
|
},
|
|
})
|
|
|
|
db.prepare(
|
|
'INSERT INTO stt_profile_provider_settings (profile, provider, settings_json, secrets_json, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)'
|
|
).run('default', 'deepgram', '{"language":"en"}', '{"apiKey":"legacy-secret"}', 1710000000, 1710000100)
|
|
|
|
expect(() => store.listSttProviderSettings('default')).not.toThrow()
|
|
expect(store.listSttProviderSettings('default')).toEqual([expected])
|
|
expect(() => store.getSttProviderSetting('default', 'deepgram' as any)).toThrow(/unknown STT provider/i)
|
|
})
|
|
|
|
it('rejects unsupported providers and secret names', async () => {
|
|
const { store } = await initStore()
|
|
|
|
expect(() => {
|
|
store.saveSttProviderSetting('default', 'deepgram', {
|
|
settings: {
|
|
model: 'nova-2',
|
|
},
|
|
})
|
|
}).toThrow(/unknown STT provider/i)
|
|
|
|
expect(() => {
|
|
store.saveSttProviderSetting('default', 'openai', {
|
|
secrets: {
|
|
token: 'secret-value',
|
|
},
|
|
})
|
|
}).toThrow(/unknown STT provider secret/i)
|
|
})
|
|
|
|
it.each([
|
|
'http://10.0.0.1:8000/v1/audio/transcriptions',
|
|
'http://172.16.0.1:8000/v1/audio/transcriptions',
|
|
'http://192.168.1.1:8000/v1/audio/transcriptions',
|
|
'http://127.0.0.1:8000/v1/audio/transcriptions',
|
|
'http://169.254.169.254/latest/meta-data',
|
|
'http://[::1]:8000/v1/audio/transcriptions',
|
|
'http://[fd00::1]:8000/v1/audio/transcriptions',
|
|
'http://[fe90::1]:8000/v1/audio/transcriptions',
|
|
])('allows local or private base urls using the shared url rules: %s', async (baseUrl) => {
|
|
const { store } = await initStore()
|
|
|
|
const saved = store.saveSttProviderSetting('default', 'openai', {
|
|
settings: {
|
|
baseUrl,
|
|
},
|
|
})
|
|
|
|
expect(saved.settings.baseUrl).toContain(new URL(baseUrl).origin)
|
|
})
|
|
|
|
it('stores only supported audio transcode modes', async () => {
|
|
const { store } = await initStore()
|
|
|
|
const saved = store.saveSttProviderSetting('default', 'custom', {
|
|
settings: {
|
|
baseUrl: 'http://127.0.0.1:8000/v1',
|
|
model: 'whisper-1',
|
|
audioTranscode: 'ffmpeg',
|
|
},
|
|
})
|
|
expect(saved.settings.audioTranscode).toBe('ffmpeg')
|
|
|
|
const ignored = store.saveSttProviderSetting('default', 'custom', {
|
|
settings: {
|
|
audioTranscode: 'auto',
|
|
},
|
|
})
|
|
expect(ignored.settings.audioTranscode).toBe('ffmpeg')
|
|
})
|
|
})
|