fix(sync): honor per-spoon auto-sync settings

This commit is contained in:
Gabriel Brown
2026-07-10 17:15:20 -04:00
parent 57c175aecd
commit bdb2964e34
5 changed files with 231 additions and 3 deletions
@@ -0,0 +1,167 @@
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<string, never>,
{ 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);
});
});