import { describe, it, expect, beforeAll, beforeEach, afterAll, vi } from 'vitest' /** * Integration tests for the Valkey availability mirror * (requirement/valkey-online-mirror-plan.md). * * Real Postgres + real Valkey via test/setup.js — no mocks. We assert on both * stores' state after each operation to catch missed mirrors or order bugs. */ vi.mock('../../src/plugins/websocket.js', () => ({ sendToUser: vi.fn(() => false), sendToSessionParticipant: vi.fn(() => false), registerWebSocketPlugin: vi.fn(), registerWebSocketRoute: vi.fn(), isUserOnlineWs: vi.fn(() => false), getSessionConnections: vi.fn(() => ({})), })) vi.mock('../../src/services/notification.service.js', () => ({ sendPushNotification: vi.fn(async () => true), registerDeviceToken: vi.fn(async () => {}), })) const valkey = await import('../../src/plugins/valkey.js') const { setOnline, setOffline, heartbeat, isMitraReachable, recomputeCapacityForMitra, recomputeCapacityBySession, seedFromPostgres, autoOfflineStaleMitras, mirrorHeartbeatsToPostgres, countAvailableMitrasFromCache, invalidateAvailabilityCache, VK_MITRAS_ONLINE, VK_MITRAS_DEACTIVATED, vkCapacityKey, vkHeartbeatKey, } = await import('../../src/services/mitra-status.service.js') const { updateMitraStatus } = await import('../../src/services/mitra.service.js') const { findAvailableMitras, acceptPairingRequest, createPairingRequest } = await import('../../src/services/pairing.service.js') const { createPaymentSession, confirmPaymentSession } = await import('../../src/services/payment.service.js') const { db, resetDb, resetAppConfig } = await import('../helpers/db.js') const { getTestValkey } = await import('../helpers/valkey.js') const { createCustomer, createMitra } = await import('../helpers/fixtures.js') const { SessionStatus, UserType } = await import('../../src/constants.js') const v = () => getTestValkey() describe('mitra-status valkey mirror', () => { beforeAll(async () => { await resetAppConfig() }) beforeEach(async () => { await resetDb() }) // ---------- Seed ---------- describe('seedFromPostgres', () => { it('populates mitras:online from is_online=true rows', async () => { const m1 = await createMitra({ callName: 'M1', isOnline: true }) const m2 = await createMitra({ callName: 'M2', isOnline: true }) await createMitra({ callName: 'M3', isOnline: false }) await v().flushdb() await seedFromPostgres() const members = await v().smembers(VK_MITRAS_ONLINE) expect(members.sort()).toEqual([m1.id, m2.id].sort()) }) it('seeds mitras:deactivated from is_active=false', async () => { const m = await createMitra({ callName: 'Dead', isActive: false }) await createMitra({ callName: 'Alive', isActive: true }) await v().flushdb() await seedFromPostgres() const members = await v().smembers(VK_MITRAS_DEACTIVATED) expect(members).toEqual([m.id]) }) it('seeds heartbeat keys for online mitras with current timestamp', async () => { const m = await createMitra({ callName: 'Live', isOnline: true }) await v().flushdb() const before = Date.now() await seedFromPostgres() const after = Date.now() const ts = await v().get(vkHeartbeatKey(m.id)) expect(ts).toBeTruthy() const seeded = Date.parse(ts) expect(seeded).toBeGreaterThanOrEqual(before) expect(seeded).toBeLessThanOrEqual(after) }) it('seeds capacity counters from chat_sessions', async () => { const c = await createCustomer({ callName: 'C' }) const m = await createMitra({ callName: 'M', isOnline: true }) const sql = db() await sql` INSERT INTO chat_sessions (customer_id, mitra_id, status) VALUES (${c.id}, ${m.id}, ${SessionStatus.ACTIVE}) ` await v().flushdb() await seedFromPostgres() expect(await v().get(vkCapacityKey(m.id))).toBe('1') }) it('is idempotent — running twice yields the same state', async () => { const m = await createMitra({ callName: 'Idem', isOnline: true }) await seedFromPostgres() const first = { online: (await v().smembers(VK_MITRAS_ONLINE)).sort(), heartbeat: await v().get(vkHeartbeatKey(m.id)), } await seedFromPostgres() const second = { online: (await v().smembers(VK_MITRAS_ONLINE)).sort(), heartbeat: await v().get(vkHeartbeatKey(m.id)), } expect(second.online).toEqual(first.online) // Heartbeat is reseeded with NOW each call — must be >= first expect(Date.parse(second.heartbeat)).toBeGreaterThanOrEqual(Date.parse(first.heartbeat)) }) }) // ---------- setOnline / setOffline ---------- describe('setOnline / setOffline write-through', () => { it('setOnline adds to mitras:online + writes heartbeat key', async () => { const m = await createMitra({ callName: 'Toggle', isOnline: false }) await v().flushdb() await setOnline(m.id) expect(await v().sismember(VK_MITRAS_ONLINE, m.id)).toBe(1) expect(await v().get(vkHeartbeatKey(m.id))).toBeTruthy() }) it('setOffline removes from mitras:online + deletes heartbeat key', async () => { const m = await createMitra({ callName: 'Toggle', isOnline: true }) await setOnline(m.id) // ensure heartbeat key exists await setOffline(m.id) expect(await v().sismember(VK_MITRAS_ONLINE, m.id)).toBe(0) expect(await v().get(vkHeartbeatKey(m.id))).toBeNull() }) it('setOffline is no-op when mitra was already offline', async () => { const m = await createMitra({ callName: 'OffAlready', isOnline: false }) const sql = db() const beforeLogs = await sql`SELECT COUNT(*)::int AS c FROM mitra_online_logs WHERE mitra_id=${m.id}` await setOffline(m.id) const afterLogs = await sql`SELECT COUNT(*)::int AS c FROM mitra_online_logs WHERE mitra_id=${m.id}` expect(afterLogs[0].c).toBe(beforeLogs[0].c) }) }) // ---------- heartbeat ---------- describe('heartbeat (Valkey-only)', () => { it('writes Valkey timestamp without touching Postgres last_heartbeat_at', async () => { const m = await createMitra({ callName: 'Pinger', isOnline: true }) const sql = db() const [before] = await sql`SELECT last_heartbeat_at FROM mitra_online_status WHERE mitra_id=${m.id}` const pgBefore = before.last_heartbeat_at // Make sure subsequent NOW() would differ await new Promise(r => setTimeout(r, 50)) await heartbeat(m.id) const [after] = await sql`SELECT last_heartbeat_at FROM mitra_online_status WHERE mitra_id=${m.id}` // Postgres untouched expect(after.last_heartbeat_at).toEqual(pgBefore) // Valkey updated const ts = await v().get(vkHeartbeatKey(m.id)) expect(ts).toBeTruthy() expect(Date.parse(ts)).toBeGreaterThan(pgBefore.getTime()) }) it('advances the heartbeat timestamp on each call', async () => { const m = await createMitra({ callName: 'P', isOnline: true }) await heartbeat(m.id) const t1 = await v().get(vkHeartbeatKey(m.id)) await new Promise(r => setTimeout(r, 20)) await heartbeat(m.id) const t2 = await v().get(vkHeartbeatKey(m.id)) expect(Date.parse(t2)).toBeGreaterThan(Date.parse(t1)) }) }) // ---------- isMitraReachable ---------- describe('isMitraReachable', () => { it('returns true for online mitra with fresh heartbeat', async () => { const m = await createMitra({ callName: 'Reach', isOnline: true }) expect(await isMitraReachable(m.id)).toBe(true) }) it('returns false when mitra is not in mitras:online', async () => { const m = await createMitra({ callName: 'NoReach', isOnline: false }) expect(await isMitraReachable(m.id)).toBe(false) }) it('returns false when heartbeat is stale', async () => { const m = await createMitra({ callName: 'Stale', isOnline: true }) // Force stale heartbeat (one hour ago) const ancient = new Date(Date.now() - 3_600_000).toISOString() await v().set(vkHeartbeatKey(m.id), ancient) expect(await isMitraReachable(m.id)).toBe(false) }) }) // ---------- recomputeCapacity ---------- describe('recomputeCapacityForMitra', () => { it('counts ACTIVE + PENDING_PAYMENT sessions', async () => { const c = await createCustomer({ callName: 'C' }) const c2 = await createCustomer({ callName: 'C2' }) const m = await createMitra({ callName: 'Cap', isOnline: true }) const sql = db() await sql` INSERT INTO chat_sessions (customer_id, mitra_id, status) VALUES (${c.id}, ${m.id}, ${SessionStatus.ACTIVE}), (${c2.id}, ${m.id}, ${SessionStatus.PENDING_PAYMENT}) ` await recomputeCapacityForMitra(m.id) expect(await v().get(vkCapacityKey(m.id))).toBe('2') }) it('excludes ended/closing/extending sessions', async () => { const c = await createCustomer({ callName: 'C' }) const m = await createMitra({ callName: 'Cap', isOnline: true }) const sql = db() await sql` INSERT INTO chat_sessions (customer_id, mitra_id, status) VALUES (${c.id}, ${m.id}, ${SessionStatus.COMPLETED}) ` await recomputeCapacityForMitra(m.id) expect(await v().get(vkCapacityKey(m.id))).toBe('0') }) it('no-op when mitraId is null/undefined', async () => { await recomputeCapacityForMitra(null) // should not throw await recomputeCapacityForMitra(undefined) }) }) // ---------- findAvailableMitras ---------- describe('findAvailableMitras (Valkey-driven)', () => { it('returns online + not-deactivated + under-capacity + fresh-heartbeat mitras', async () => { const ok = await createMitra({ callName: 'OK', isOnline: true }) const deact = await createMitra({ callName: 'Deact', isOnline: true, isActive: false }) const offline = await createMitra({ callName: 'Off', isOnline: false }) const stale = await createMitra({ callName: 'Stale', isOnline: true }) await v().set(vkHeartbeatKey(stale.id), new Date(Date.now() - 3_600_000).toISOString()) const result = await findAvailableMitras() const ids = result.map(r => r.id).sort() expect(ids).toEqual([ok.id].sort()) expect(result.find(r => r.id === ok.id).active_session_count).toBe(0) }) it('excludes a mitra whose capacity is at max', async () => { const m = await createMitra({ callName: 'AtCap', isOnline: true }) // max_customers_per_mitra default is 3 await v().set(vkCapacityKey(m.id), 3) const result = await findAvailableMitras() expect(result.find(r => r.id === m.id)).toBeUndefined() }) it('returns capacity in the result for the blast loop', async () => { const m = await createMitra({ callName: 'WithCap', isOnline: true }) await v().set(vkCapacityKey(m.id), 2) const result = await findAvailableMitras() expect(result.find(r => r.id === m.id).active_session_count).toBe(2) }) }) // ---------- countAvailableMitrasFromCache ---------- describe('countAvailableMitrasFromCache (beacon)', () => { it('caches the snapshot in Valkey with TTL', async () => { await createMitra({ callName: 'On', isOnline: true }) await v().del('availability:snapshot') const first = await countAvailableMitrasFromCache() expect(first.available).toBe(true) expect(first.count).toBe(1) const cached = await v().get('availability:snapshot') expect(cached).toBeTruthy() expect(JSON.parse(cached)).toEqual(first) const ttl = await v().ttl('availability:snapshot') expect(ttl).toBeGreaterThan(0) expect(ttl).toBeLessThanOrEqual(10) }) it('returns cached snapshot on subsequent calls without recompute', async () => { await createMitra({ callName: 'On', isOnline: true }) await countAvailableMitrasFromCache() // primes cache // Manually corrupt SET to prove subsequent call reads cache, not Valkey state await v().flushdb() await v().set('availability:snapshot', JSON.stringify({ available: true, count: 42 }), 'EX', 10) const result = await countAvailableMitrasFromCache() expect(result.count).toBe(42) }) it('invalidateAvailabilityCache deletes the snapshot', async () => { await v().set('availability:snapshot', JSON.stringify({ available: true, count: 1 }), 'EX', 10) await invalidateAvailabilityCache() expect(await v().get('availability:snapshot')).toBeNull() }) }) // ---------- autoOfflineStaleMitras ---------- describe('autoOfflineStaleMitras', () => { it('flips Postgres + cleans Valkey for mitras with stale heartbeat', async () => { const m = await createMitra({ callName: 'WillStale', isOnline: true }) const sql = db() // Force stale heartbeat await v().set(vkHeartbeatKey(m.id), new Date(Date.now() - 3_600_000).toISOString()) const count = await autoOfflineStaleMitras() expect(count).toBe(1) const [row] = await sql`SELECT is_online FROM mitra_online_status WHERE mitra_id=${m.id}` expect(row.is_online).toBe(false) expect(await v().sismember(VK_MITRAS_ONLINE, m.id)).toBe(0) expect(await v().get(vkHeartbeatKey(m.id))).toBeNull() const [log] = await sql` SELECT status FROM mitra_online_logs WHERE mitra_id=${m.id} ORDER BY timestamp DESC LIMIT 1 ` expect(log.status).toBe('offline') }) it('no-op when no mitras are stale', async () => { await createMitra({ callName: 'Fresh', isOnline: true }) const count = await autoOfflineStaleMitras() expect(count).toBe(0) }) it('no-op when require_ping=false', async () => { const sql = db() await sql` UPDATE app_config SET value=${sql.json({ value: false })} WHERE key='require_mitra_ping' ` const m = await createMitra({ callName: 'WouldBeStale', isOnline: true }) await v().set(vkHeartbeatKey(m.id), new Date(Date.now() - 3_600_000).toISOString()) const count = await autoOfflineStaleMitras() expect(count).toBe(0) // Restore for other tests await sql` UPDATE app_config SET value=${sql.json({ value: true })} WHERE key='require_mitra_ping' ` }) }) // ---------- mirrorHeartbeatsToPostgres ---------- describe('mirrorHeartbeatsToPostgres', () => { it('writes Valkey heartbeat timestamps to Postgres last_heartbeat_at in one batch', async () => { const m1 = await createMitra({ callName: 'P1', isOnline: true }) const m2 = await createMitra({ callName: 'P2', isOnline: true }) const sql = db() const ts = new Date(Date.now() - 2_000).toISOString() await v().set(vkHeartbeatKey(m1.id), ts) await v().set(vkHeartbeatKey(m2.id), ts) const count = await mirrorHeartbeatsToPostgres() expect(count).toBe(2) const rows = await sql` SELECT mitra_id, last_heartbeat_at FROM mitra_online_status WHERE mitra_id IN (${m1.id}, ${m2.id}) ` for (const row of rows) { expect(row.last_heartbeat_at.toISOString()).toBe(ts) } }) it('no-op when no mitras are online', async () => { await v().del(VK_MITRAS_ONLINE) const count = await mirrorHeartbeatsToPostgres() expect(count).toBe(0) }) }) // ---------- updateMitraStatus / revokeAllSessions ---------- describe('updateMitraStatus + auth_session revocation', () => { it('deactivation adds to mitras:deactivated AND revokes all auth_sessions', async () => { const m = await createMitra({ callName: 'Banned', isActive: true }) const sql = db() const tokenHash = '$2b$10$abcdefghijklmnopqrstuv' await sql` INSERT INTO auth_sessions (user_type, user_id, refresh_token_hash, expires_at) VALUES (${UserType.MITRA}, ${m.id}, ${tokenHash}, NOW() + INTERVAL '30 days') ` await updateMitraStatus(m.id, false) expect(await v().sismember(VK_MITRAS_DEACTIVATED, m.id)).toBe(1) const [auth] = await sql`SELECT revoked_at FROM auth_sessions WHERE user_id=${m.id}` expect(auth.revoked_at).not.toBeNull() }) it('reactivation removes from mitras:deactivated', async () => { const m = await createMitra({ callName: 'Pardoned', isActive: false }) await v().sadd(VK_MITRAS_DEACTIVATED, m.id) await updateMitraStatus(m.id, true) expect(await v().sismember(VK_MITRAS_DEACTIVATED, m.id)).toBe(0) }) }) // ---------- E2E: blast lifecycle ---------- describe('end-to-end: blast lifecycle drives capacity counter', () => { it('mitra accept → capacity++; session end is covered separately', async () => { const c = await createCustomer({ callName: 'BlastC' }) const m = await createMitra({ callName: 'BlastM', isOnline: true }) const pay = await createPaymentSession({ customerId: c.id, durationMinutes: 15, amount: 30000, }) await confirmPaymentSession(pay.id, c.id) const session = await createPairingRequest(c.id, { paymentRequestId: pay.id }) expect(await v().get(vkCapacityKey(m.id))).toBe('0') // no accept yet await acceptPairingRequest(session.id, m.id) expect(await v().get(vkCapacityKey(m.id))).toBe('1') }) }) // ---------- Reader fallback when Valkey is unavailable ---------- describe('reader fallback', () => { it('isMitraReachable falls back to Postgres on Valkey error', async () => { const m = await createMitra({ callName: 'Fallback', isOnline: true }) // Stub sismember to throw const spy = vi.spyOn(valkey, 'sismember').mockRejectedValue(new Error('valkey down')) try { // Postgres has is_online=true → fallback returns true const result = await isMitraReachable(m.id) expect(result).toBe(true) } finally { spy.mockRestore() } }) it('findAvailableMitras falls back to Postgres JOIN when Valkey sdiff throws', async () => { const m = await createMitra({ callName: 'FallbackBlast', isOnline: true }) const spy = vi.spyOn(valkey, 'sdiff').mockRejectedValue(new Error('valkey down')) try { const result = await findAvailableMitras() expect(result.find(r => r.id === m.id)).toBeDefined() } finally { spy.mockRestore() } }) }) })