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>
121 lines
3.3 KiB
TypeScript
121 lines
3.3 KiB
TypeScript
/**
|
|
* 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;
|
|
}
|