import { convexTest } from 'convex-test'; import { makeFunctionReference } from 'convex/server'; import { describe, expect, test } from 'vitest'; import { internal } from '../../convex/_generated/api.js'; import schema from '../../convex/schema'; import { shouldAutoSync, shouldCreateReviewThread, } from '../../convex/syncGating'; const modules = import.meta.glob('../../convex/**/*.*s'); const backfillAutoSyncDefaults = makeFunctionReference< 'mutation', Record, { updated: number } >('migrations:backfillAutoSyncDefaults'); describe('syncGating', () => { test('auto-sync requires the flag and a clean behind compare', () => { expect( shouldAutoSync( { autoSyncEnabled: false, requireCleanCompareForSync: true }, { status: 'behind', forkAheadBy: 0 }, ), ).toBe(false); expect( shouldAutoSync( { autoSyncEnabled: true, requireCleanCompareForSync: true }, { status: 'behind', forkAheadBy: 0 }, ), ).toBe(true); expect( shouldAutoSync( { autoSyncEnabled: true, requireCleanCompareForSync: true }, { status: 'behind', forkAheadBy: 2 }, ), ).toBe(false); expect( shouldAutoSync( { autoSyncEnabled: true, requireCleanCompareForSync: true }, { status: 'diverged', forkAheadBy: 0 }, ), ).toBe(false); }); test('review thread requires autoReviewEnabled and diverged', () => { expect( shouldCreateReviewThread( { autoReviewEnabled: false }, { status: 'diverged' }, ), ).toBe(false); expect( shouldCreateReviewThread( { autoReviewEnabled: true }, { status: 'diverged' }, ), ).toBe(true); expect( shouldCreateReviewThread( { autoReviewEnabled: true }, { status: 'behind' }, ), ).toBe(false); }); test('backfill enables auto-sync only for disabled GitHub spoons', async () => { const t = convexTest(schema, modules); const seeded = await t.mutation(async (ctx) => { const now = Date.now(); const ownerId = await ctx.db.insert('users', { email: 'owner@example.com', name: 'Owner', }); const createSpoon = async ( name: string, provider: 'github' | 'gitea', autoSyncEnabled: boolean, ) => { const spoonId = await ctx.db.insert('spoons', { ownerId, name, provider, upstreamOwner: 'upstream', upstreamRepo: name, upstreamDefaultBranch: 'main', upstreamUrl: `https://example.com/upstream/${name}`, forkOwner: 'owner', forkRepo: name, forkUrl: `https://example.com/owner/${name}`, visibility: 'private', maintenanceMode: 'watch', syncCadence: 'daily', productionRefStrategy: 'default_branch', status: 'active', createdAt: now, updatedAt: now, }); const settingsId = await ctx.db.insert('spoonSettings', { spoonId, ownerId, autoRefreshEnabled: true, autoReviewEnabled: true, autoSyncEnabled, requireAiLowRiskForSync: true, requireCleanCompareForSync: true, ignoredFilePatterns: [], importantFilePatterns: [], createdAt: now, updatedAt: now, }); return settingsId; }; return { disabledGithub: await createSpoon('disabled-github', 'github', false), enabledGithub: await createSpoon('enabled-github', 'github', true), disabledGitea: await createSpoon('disabled-gitea', 'gitea', false), }; }); const result = await t.mutation(backfillAutoSyncDefaults, {}); const settings = await t.run(async (ctx) => ({ disabledGithub: await ctx.db.get(seeded.disabledGithub), enabledGithub: await ctx.db.get(seeded.enabledGithub), disabledGitea: await ctx.db.get(seeded.disabledGitea), })); expect(result).toEqual({ updated: 1 }); expect(settings.disabledGithub?.autoSyncEnabled).toBe(true); expect(settings.enabledGithub?.autoSyncEnabled).toBe(true); expect(settings.disabledGitea?.autoSyncEnabled).toBe(false); }); test('new GitHub spoons default auto-sync on', async () => { const t = convexTest(schema, modules); const ownerId = await t.mutation(async (ctx) => { return await ctx.db.insert('users', { email: 'new-owner@example.com', name: 'New owner', }); }); const spoonId = await t.mutation(internal.github.createForkSpoonRecord, { ownerId, name: 'github-spoon', upstreamOwner: 'upstream', upstreamRepo: 'repo', upstreamDefaultBranch: 'main', upstreamUrl: 'https://github.com/upstream/repo', forkOwner: 'new-owner', forkRepo: 'repo', forkDefaultBranch: 'main', forkUrl: 'https://github.com/new-owner/repo', visibility: 'private', }); const settings = await t.run(async (ctx) => { return await ctx.db .query('spoonSettings') .withIndex('by_spoon', (q) => q.eq('spoonId', spoonId)) .unique(); }); expect(settings?.autoSyncEnabled).toBe(true); }); });