176 lines
5.5 KiB
TypeScript
176 lines
5.5 KiB
TypeScript
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');
|
|
});
|
|
});
|