Initial implementation of Telegram AI customer support bot

Cloudflare Workers + Hono + D1 + KV + R2 stack with 4 specialized AI agents
(onboarding, troubleshoot, asset, billing), OpenAI function calling with
7 tool definitions, human escalation, pending action approval workflow,
feedback collection, audit logging, i18n (ko/en), and Workers AI fallback.

43 source files, 45 tests passing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
kappa
2026-02-11 13:21:38 +09:00
commit 1d6b64c9e4
58 changed files with 12857 additions and 0 deletions

120
tests/setup.ts Normal file
View File

@@ -0,0 +1,120 @@
/**
* Vitest test setup
*
* Miniflare-based D1 + KV simulation for integration tests.
*/
import { readFileSync } from 'fs';
import { join } from 'path';
import { beforeAll, afterEach } from 'vitest';
import { Miniflare } from 'miniflare';
let mf: Miniflare | null = null;
let db: D1Database | null = null;
declare global {
var getMiniflareBindings: () => {
DB: D1Database;
RATE_LIMIT_KV: KVNamespace;
SESSION_KV: KVNamespace;
CACHE_KV: KVNamespace;
};
}
beforeAll(async () => {
mf = new Miniflare({
modules: true,
script: 'export default { fetch() { return new Response("test"); } }',
d1Databases: {
DB: '__test_db__',
},
kvNamespaces: ['RATE_LIMIT_KV', 'SESSION_KV', 'CACHE_KV'],
});
db = await mf.getD1Database('DB');
(global as any).getMiniflareBindings = () => ({
DB: db as D1Database,
RATE_LIMIT_KV: {} as KVNamespace,
SESSION_KV: {} as KVNamespace,
CACHE_KV: {} as KVNamespace,
});
// Schema initialization
const schemaPath = join(__dirname, '../schema.sql');
const schema = readFileSync(schemaPath, 'utf-8');
const cleanSchema = schema
.split('\n')
.filter(line => !line.trim().startsWith('--'))
.join('\n');
const statements = cleanSchema
.split(';')
.map(s => s.replace(/\s+/g, ' ').trim())
.filter(s => s.length > 0);
try {
for (const statement of statements) {
await db.exec(statement + ';');
}
} catch (error) {
console.error('Schema initialization failed:', error);
throw error;
}
});
afterEach(async () => {
if (!db) return;
// Delete child tables first, then parent tables (FK order)
// 1. No FK dependencies between each other
await db.exec('DELETE FROM feedback');
await db.exec('DELETE FROM pending_actions');
await db.exec('DELETE FROM audit_logs');
// 2. bank_notifications refs transactions
await db.exec('DELETE FROM bank_notifications');
// 3. transactions refs users
await db.exec('DELETE FROM transactions');
// 4. wallets refs users
await db.exec('DELETE FROM wallets');
// 5. Asset tables ref users
await db.exec('DELETE FROM domains');
await db.exec('DELETE FROM servers');
await db.exec('DELETE FROM services_ddos');
await db.exec('DELETE FROM services_vpn');
// 6. Conversation tables ref users
await db.exec('DELETE FROM conversations');
await db.exec('DELETE FROM conversation_archives');
// 7. Standalone tables (no FKs)
await db.exec('DELETE FROM knowledge_articles');
await db.exec('DELETE FROM d2_cache');
// 8. Session tables (no FKs to users)
await db.exec('DELETE FROM onboarding_sessions');
await db.exec('DELETE FROM troubleshoot_sessions');
await db.exec('DELETE FROM asset_sessions');
await db.exec('DELETE FROM billing_sessions');
// 9. users last (parent table)
await db.exec('DELETE FROM users');
});
/**
* Create a test user and return its auto-incremented id.
*/
export async function createTestUser(
telegramId: string,
username?: string
): Promise<number> {
const bindings = getMiniflareBindings();
const result = await bindings.DB.prepare(
'INSERT INTO users (telegram_id, username) VALUES (?, ?)'
).bind(telegramId, username || null).run();
return Number(result.meta?.last_row_id || 0);
}
/**
* Get the test D1Database binding.
*/
export function getTestDB(): D1Database {
return getMiniflareBindings().DB;
}