import { convexTest } from 'convex-test'; import { describe, expect, test } from 'vitest'; import { internal } from '../../convex/_generated/api'; import schema from '../../convex/schema'; const modules = import.meta.glob('../../convex/**/*.*s'); describe('notifications schema', () => { test('inserts and reads back a notifications row', async () => { const t = convexTest(schema, modules); const { notificationId, userId } = await t.run(async (ctx) => { const userId = await ctx.db.insert('users', { email: 'a@b.c', name: 'A', }); const now = Date.now(); const notificationId = await ctx.db.insert('notifications', { userId, kind: 'agent_turn_finished', title: 'Agent finished', body: 'Your agent completed its turn.', link: '/threads/abc', createdAt: now, }); return { notificationId, userId }; }); const notification = await t.run( async (ctx) => await ctx.db.get(notificationId), ); expect(notification).not.toBeNull(); expect(notification?.userId).toBe(userId); expect(notification?.kind).toBe('agent_turn_finished'); expect(notification?.title).toBe('Agent finished'); expect(notification?.body).toBe('Your agent completed its turn.'); expect(notification?.link).toBe('/threads/abc'); expect(notification?.readAt).toBeUndefined(); expect(notification?.emailedAt).toBeUndefined(); expect(typeof notification?.createdAt).toBe('number'); }); test('inserts and reads back a notificationPreferences row', async () => { const t = convexTest(schema, modules); const { preferenceId, userId } = await t.run(async (ctx) => { const userId = await ctx.db.insert('users', { email: 'prefs@b.c', name: 'Prefs', }); const preferenceId = await ctx.db.insert('notificationPreferences', { userId, emailEnabled: true, emailSyncFailed: false, updatedAt: Date.now(), }); return { preferenceId, userId }; }); const preference = await t.run( async (ctx) => await ctx.db.get(preferenceId), ); expect(preference).not.toBeNull(); expect(preference?.userId).toBe(userId); expect(preference?.emailEnabled).toBe(true); expect(preference?.emailSyncFailed).toBe(false); expect(preference?.emailMaintenance).toBeUndefined(); expect(typeof preference?.updatedAt).toBe('number'); }); }); describe('emitNotification', () => { test('inserts an unread notification row for the user', async () => { const t = convexTest(schema, modules); const userId = await t.run( async (ctx) => await ctx.db.insert('users', { email: 'emit@b.c', name: 'Emit' }), ); const notificationId = await t.mutation(internal.notifications.emit, { userId, kind: 'agent_turn_finished', title: 'Agent finished', body: 'Your agent completed its turn.', link: '/threads/abc', }); const notification = await t.run( async (ctx) => await ctx.db.get(notificationId), ); expect(notification).not.toBeNull(); expect(notification?.userId).toBe(userId); expect(notification?.kind).toBe('agent_turn_finished'); expect(notification?.title).toBe('Agent finished'); expect(notification?.body).toBe('Your agent completed its turn.'); expect(notification?.link).toBe('/threads/abc'); expect(notification?.readAt).toBeUndefined(); expect(notification?.emailedAt).toBeUndefined(); expect(typeof notification?.createdAt).toBe('number'); }); test('does not email when emailEnabled is false', async () => { const t = convexTest(schema, modules); const userId = await t.run(async (ctx) => { const userId = await ctx.db.insert('users', { email: 'noemail@b.c', name: 'NoEmail', }); await ctx.db.insert('notificationPreferences', { userId, emailEnabled: false, updatedAt: Date.now(), }); return userId; }); const notificationId = await t.mutation(internal.notifications.emit, { userId, kind: 'sync_failed', title: 'Sync failed', body: 'Your sync failed.', }); // No email should have been scheduled; draining is a safe no-op. await t.finishAllScheduledFunctions(() => undefined); const notification = await t.run( async (ctx) => await ctx.db.get(notificationId), ); expect(notification?.emailedAt).toBeUndefined(); }); test('getForEmail returns the recipient shape and markEmailed patches emailedAt', async () => { const t = convexTest(schema, modules); const { notificationId } = await t.run(async (ctx) => { const userId = await ctx.db.insert('users', { email: 'foremail@b.c', name: 'ForEmail', }); const notificationId = await ctx.db.insert('notifications', { userId, kind: 'agent_needs_input', title: 'Needs input', body: 'The agent needs your input.', link: '/threads/xyz', createdAt: Date.now(), }); return { notificationId }; }); const forEmail = await t.query(internal.notifications.getForEmail, { notificationId, }); expect(forEmail).toEqual({ to: 'foremail@b.c', title: 'Needs input', body: 'The agent needs your input.', link: '/threads/xyz', }); await t.mutation(internal.notifications.markEmailed, { notificationId }); const notification = await t.run( async (ctx) => await ctx.db.get(notificationId), ); expect(typeof notification?.emailedAt).toBe('number'); }); });