diff --git a/setup/register.test.ts b/setup/register.test.ts index 2f67cd4..7cf5949 100644 --- a/setup/register.test.ts +++ b/setup/register.test.ts @@ -19,16 +19,19 @@ function createTestPool(): pg.Pool { } as pg.QueryResult; } - if (/INSERT INTO registered_groups/i.test(sql)) { + if (/INSERT INTO REGISTERED_GROUPS/i.test(s)) { const jid = params![0] as string; + const hasJailConfigParam = params!.length >= 7; + const jailConfig = hasJailConfigParam ? params![5] : null; + const requiresTrigger = hasJailConfigParam ? params![6] : params![5]; groups.set(jid, { jid, name: params![1], folder: params![2], trigger_pattern: params![3], added_at: params![4], - jail_config: params![5], - requires_trigger: params![6], + jail_config: jailConfig, + requires_trigger: requiresTrigger, }); return { rows: [], @@ -39,8 +42,18 @@ function createTestPool(): pg.Pool { } as pg.QueryResult; } - if (/SELECT \* FROM registered_groups/i.test(sql)) { - if (sql.includes('WHERE jid = $1')) { + if (s.includes('SELECT COUNT')) { + return { + rows: [{ count: String(groups.size) }], + rowCount: 1, + command: 'SELECT', + oid: 0, + fields: [], + } as pg.QueryResult; + } + + if (s.includes('FROM REGISTERED_GROUPS')) { + if (s.includes('WHERE JID = $1')) { const row = groups.get(params![0] as string); return { rows: row ? [row] : [], @@ -59,16 +72,6 @@ function createTestPool(): pg.Pool { } as pg.QueryResult; } - if (/SELECT COUNT/i.test(sql)) { - return { - rows: [{ count: String(groups.size) }], - rowCount: 1, - command: 'SELECT', - oid: 0, - fields: [], - } as pg.QueryResult; - } - return { rows: [], rowCount: 0, diff --git a/src/db-identifiers.test.ts b/src/db-identifiers.test.ts new file mode 100644 index 0000000..7ff0d39 --- /dev/null +++ b/src/db-identifiers.test.ts @@ -0,0 +1,58 @@ +import { describe, expect, it } from 'vitest'; + +import { + defaultMemoryDbName, + defaultMemoryDbUser, + defaultOpsDbName, + defaultOpsDbUser, + defaultSkillsDbName, + defaultSkillsDbUser, + isEnvTemplateValue, + normalizeDbIdentifierBase, + usableEnvValue, +} from './db-identifiers.js'; + +describe('normalizeDbIdentifierBase', () => { + it('normalizes to lowercase and underscores', () => { + expect(normalizeDbIdentifierBase('Clawdie AI')).toBe('clawdie_ai'); + }); + + it('trims extra separators', () => { + expect(normalizeDbIdentifierBase('---Clawdie---AI---')).toBe('clawdie_ai'); + }); + + it('falls back to clawdie for empty values', () => { + expect(normalizeDbIdentifierBase(' ')).toBe('clawdie'); + }); +}); + +describe('default db identifiers', () => { + it('builds skills identifiers', () => { + expect(defaultSkillsDbUser('Clawdie AI')).toBe('clawdie_ai_reader'); + expect(defaultSkillsDbName('Clawdie AI')).toBe('clawdie_ai_skills'); + }); + + it('builds memory identifiers', () => { + expect(defaultMemoryDbUser('Clawdie AI')).toBe('clawdie_ai_brain'); + expect(defaultMemoryDbName('Clawdie AI')).toBe('clawdie_ai_brain'); + }); + + it('builds ops identifiers', () => { + expect(defaultOpsDbUser('Clawdie AI')).toBe('clawdie_ai_ops'); + expect(defaultOpsDbName('Clawdie AI')).toBe('clawdie_ai_ops'); + }); +}); + +describe('env template helpers', () => { + it('detects env template placeholders', () => { + expect(isEnvTemplateValue('${OPS_DB_URL}')).toBe(true); + expect(isEnvTemplateValue('postgres://user@host/db')).toBe(false); + }); + + it('filters template placeholders', () => { + expect(usableEnvValue('${OPS_DB_URL}')).toBeUndefined(); + expect(usableEnvValue('postgres://user@host/db')).toBe( + 'postgres://user@host/db', + ); + }); +}); diff --git a/src/test-helpers.ts b/src/test-helpers.ts index 4216c54..1f86030 100644 --- a/src/test-helpers.ts +++ b/src/test-helpers.ts @@ -46,7 +46,7 @@ class MockPool { } as QueryResult; } - if (/INSERT INTO chats/i.test(s)) { + if (/INSERT INTO CHATS/i.test(s)) { const [jid, name, last_message_time, channel, is_group] = (params || []) as [string, string, string, string | null, number | null]; const existing = this.chats.get(jid); @@ -92,17 +92,20 @@ class MockPool { } as QueryResult; } - if (/SELECT.*FROM chats/i.test(s) && !/COUNT/i.test(s)) { + if (/SELECT[\s\S]*FROM CHATS/i.test(s) && !/COUNT/i.test(s)) { let rows = [...this.chats.values()]; - if (s.includes('jid = $1') && params?.[0]) { + if (s.includes('JID = $1') && params?.[0]) { rows = rows.filter((r) => r.jid === params[0]); } - if (s.includes("jid <> '__group_sync__'") && s.includes('name <> jid')) { + if ( + s.includes("JID <> '__GROUP_SYNC__'") && + s.includes('NAME <> JID') + ) { rows = rows.filter( (r) => r.jid !== '__group_sync__' && r.name !== r.jid, ); } - if (s.includes('ORDER BY last_message_time DESC')) { + if (s.includes('ORDER BY LAST_MESSAGE_TIME DESC')) { rows.sort((a, b) => (b.last_message_time as string).localeCompare( a.last_message_time as string, @@ -121,9 +124,9 @@ class MockPool { } as QueryResult; } - if (/SELECT COUNT.*FROM chats/i.test(s)) { + if (/SELECT COUNT[\s\S]*FROM CHATS/i.test(s)) { let rows = [...this.chats.values()]; - if (s.includes("jid <> '__group_sync__'")) { + if (s.includes("JID <> '__GROUP_SYNC__'")) { rows = rows.filter((r) => r.jid !== '__group_sync__'); } return { @@ -135,7 +138,7 @@ class MockPool { } as QueryResult; } - if (/INSERT INTO messages/i.test(s)) { + if (/INSERT INTO MESSAGES/i.test(s)) { const [ id, chat_jid, @@ -175,22 +178,22 @@ class MockPool { } as QueryResult; } - if (/SELECT.*FROM messages/i.test(s)) { + if (/SELECT[\s\S]*FROM MESSAGES/i.test(s)) { let rows = [...this.messages.values()]; - if (s.includes('chat_jid = $1')) { + if (s.includes('CHAT_JID = $1')) { rows = rows.filter((r) => r.chat_jid === params![0]); } - if (s.includes('chat_jid IN (')) { + if (s.includes('CHAT_JID IN (')) { const jids = params!.slice(1, -1) as string[]; rows = rows.filter((r) => jids.includes(r.chat_jid as string)); } - if (s.includes('timestamp > $1') || s.includes('timestamp >')) { - const tsParam = s.includes('chat_jid = $1') + if (s.includes('TIMESTAMP > $1') || s.includes('TIMESTAMP >')) { + const tsParam = s.includes('CHAT_JID = $1') ? (params![1] as string) : (params![0] as string); rows = rows.filter((r) => (r.timestamp as string) > tsParam); } - if (s.includes('is_bot_message = 0')) { + if (s.includes('IS_BOT_MESSAGE = 0')) { rows = rows.filter((r) => r.is_bot_message === 0); } const botPrefixParam = s.includes('NOT LIKE $3') @@ -217,7 +220,7 @@ class MockPool { } as QueryResult; } - if (/INSERT INTO scheduled_tasks/i.test(s)) { + if (/INSERT INTO SCHEDULED_TASKS/i.test(s)) { const [ id, group_folder, @@ -264,9 +267,9 @@ class MockPool { } as QueryResult; } - if (/SELECT.*FROM scheduled_tasks/i.test(s) && !/COUNT/i.test(s)) { + if (/SELECT[\s\S]*FROM SCHEDULED_TASKS/i.test(s) && !/COUNT/i.test(s)) { let rows = [...this.tasks.values()]; - if (s.includes('WHERE id = $1')) { + if (s.includes('WHERE ID = $1')) { rows = rows.filter((r) => r.id === params![0]); return { rows, @@ -276,10 +279,10 @@ class MockPool { fields: [], } as QueryResult; } - if (s.includes('WHERE group_folder = $1')) { + if (s.includes('WHERE GROUP_FOLDER = $1')) { rows = rows.filter((r) => r.group_folder === params![0]); } - if (s.includes("status = 'active'") && s.includes('next_run')) { + if (s.includes("STATUS = 'ACTIVE'") && s.includes('NEXT_RUN')) { const now = params![0] as string; rows = rows.filter( (r) => @@ -288,12 +291,12 @@ class MockPool { (r.next_run as string) <= now, ); } - if (s.includes('ORDER BY created_at DESC')) { + if (s.includes('ORDER BY CREATED_AT DESC')) { rows.sort((a, b) => (b.created_at as string).localeCompare(a.created_at as string), ); } - if (s.includes('ORDER BY next_run')) { + if (s.includes('ORDER BY NEXT_RUN')) { rows.sort((a, b) => ((a.next_run as string) || '').localeCompare( (b.next_run as string) || '', @@ -309,17 +312,17 @@ class MockPool { } as QueryResult; } - if (/UPDATE scheduled_tasks/i.test(s)) { + if (/UPDATE SCHEDULED_TASKS/i.test(s)) { const lastParam = params![params!.length - 1] as string; const task = this.tasks.get(lastParam); if (task) { - if (s.includes('prompt =')) task.prompt = params!.shift(); - if (s.includes('status =') && s.includes("'paused'") === false) { - const statusIdx = s.indexOf('status ='); + if (s.includes('PROMPT =')) task.prompt = params!.shift(); + if (s.includes('STATUS =') && s.includes("'PAUSED'") === false) { + const statusIdx = s.indexOf('STATUS ='); if (statusIdx > -1 && params!.length >= 2) task.status = params![params!.length - 2]; } - if (s.includes('SET status = CASE') && s.includes("'completed'")) { + if (s.includes('SET STATUS = CASE') && s.includes("'COMPLETED'")) { // updateTaskAfterRun: complex, parse from params } const id = params![params!.length - 1] as string; @@ -448,7 +451,7 @@ class MockPool { } as QueryResult; } - if (/INSERT INTO registered_groups/i.test(s)) { + if (/INSERT INTO REGISTERED_GROUPS/i.test(s)) { const [ jid, name, @@ -484,8 +487,8 @@ class MockPool { } as QueryResult; } - if (/SELECT.*FROM registered_groups/i.test(s)) { - if (s.includes('WHERE jid = $1')) { + if (/SELECT[\s\S]*FROM REGISTERED_GROUPS/i.test(s)) { + if (s.includes('WHERE JID = $1')) { const row = this.registeredGroups.get(params![0] as string); return { rows: row ? [row] : [], @@ -504,7 +507,7 @@ class MockPool { } as QueryResult; } - if (/SELECT COUNT.*FROM registered_groups/i.test(s)) { + if (/SELECT COUNT[\s\S]*FROM REGISTERED_GROUPS/i.test(s)) { return { rows: [{ count: String(this.registeredGroups.size) }], command: 'SELECT', @@ -514,7 +517,7 @@ class MockPool { } as QueryResult; } - if (/UPDATE registered_groups/i.test(s)) { + if (/UPDATE REGISTERED_GROUPS/i.test(s)) { return { rows: [], command: '', @@ -534,6 +537,13 @@ class MockPool { } async end(): Promise {} + + async connect(): Promise<{ query: MockPool['query']; release: () => void }> { + return { + query: this.query.bind(this), + release: () => {}, + }; + } } export function createMockPool(): Pool {