diff --git a/src/lib/scheduler-service.test.ts b/src/lib/scheduler-service.test.ts index 8af4cef..c31e2fb 100644 --- a/src/lib/scheduler-service.test.ts +++ b/src/lib/scheduler-service.test.ts @@ -58,6 +58,51 @@ describe("Scheduler Service - Ignored Repository Handling", () => { expect(shouldMirrorRepository(oldSyncedRepo)).toBe(true); }); + test("auto-mirror filter respects autoMirror and autoMirrorStarred independently", () => { + // Mirrors the inline filter at scheduler-service.ts L228-233 / L609-614: + // a repo is "starred from another owner" iff isStarred && owner !== githubOwner. + // Such repos are gated by autoMirrorStarred; everything else is gated by autoMirror. + const githubOwner = "Alice".toLowerCase(); + const filterRepos = ( + repos: Array<{ name: string; isStarred: boolean; owner: string }>, + autoMirror: boolean, + autoMirrorStarred: boolean, + ) => + repos.filter(repo => { + const isStarredFromOther = repo.isStarred && repo.owner.toLowerCase() !== githubOwner; + return isStarredFromOther ? autoMirrorStarred : autoMirror; + }); + + // "ALICE" tests case-insensitive owner match — GitHub usernames are case-insensitive, + // so a self-starred repo stored with different casing must still count as owned. + const repos = [ + { name: "owned-repo", isStarred: false, owner: "alice" }, + { name: "self-starred", isStarred: true, owner: "ALICE" }, + { name: "starred-from-bob", isStarred: true, owner: "bob" }, + ]; + + // Both off: nothing mirrors + expect(filterRepos(repos, false, false).map(r => r.name)).toEqual([]); + + // Only autoMirror: owned + self-starred, not third-party stars + expect(filterRepos(repos, true, false).map(r => r.name)).toEqual([ + "owned-repo", + "self-starred", + ]); + + // Only autoMirrorStarred: just third-party stars (the bug fix — used to be empty) + expect(filterRepos(repos, false, true).map(r => r.name)).toEqual([ + "starred-from-bob", + ]); + + // Both on: everything + expect(filterRepos(repos, true, true).map(r => r.name)).toEqual([ + "owned-repo", + "self-starred", + "starred-from-bob", + ]); + }); + test("should validate all repository status enum values", () => { const validStatuses = [ "imported", diff --git a/src/lib/scheduler-service.ts b/src/lib/scheduler-service.ts index 9c43211..5c05326 100644 --- a/src/lib/scheduler-service.ts +++ b/src/lib/scheduler-service.ts @@ -203,10 +203,14 @@ async function runScheduledSync(config: any): Promise { } } - // Auto-mirror: Mirror imported/pending/failed repositories if enabled - if (scheduleConfig.autoMirror) { + // Auto-mirror: Mirror imported/pending/failed repositories if enabled. + // autoMirror covers owned repos; autoMirrorStarred covers starred repos from other owners. + // Either flag on its own is enough to enter this phase. + const autoMirrorOwned = !!scheduleConfig.autoMirror; + const autoMirrorStarred = !!config.githubConfig?.autoMirrorStarred; + if (autoMirrorOwned || autoMirrorStarred) { try { - console.log(`[Scheduler] Auto-mirror enabled - checking for repositories to mirror for user ${userId}...`); + console.log(`[Scheduler] Auto-mirror enabled (owned=${autoMirrorOwned}, starred=${autoMirrorStarred}) - checking for repositories to mirror for user ${userId}...`); let reposNeedingMirror = await db .select() .from(repositories) @@ -221,17 +225,16 @@ async function runScheduledSync(config: any): Promise { ) ); - // Filter out starred repos from auto-mirror when autoMirrorStarred is disabled - if (!config.githubConfig?.autoMirrorStarred) { - const githubOwner = config.githubConfig?.owner || ''; - const beforeCount = reposNeedingMirror.length; - reposNeedingMirror = reposNeedingMirror.filter( - repo => !repo.isStarred || repo.owner === githubOwner - ); - const skippedCount = beforeCount - reposNeedingMirror.length; - if (skippedCount > 0) { - console.log(`[Scheduler] Skipped ${skippedCount} starred repositories from auto-mirror (autoMirrorStarred is disabled)`); - } + const githubOwner = (config.githubConfig?.owner || '').toLowerCase(); + const beforeCount = reposNeedingMirror.length; + reposNeedingMirror = reposNeedingMirror.filter(repo => { + // GitHub usernames are case-insensitive; lowercase both sides to avoid misclassifying self-starred repos. + const isStarredFromOther = repo.isStarred && repo.owner.toLowerCase() !== githubOwner; + return isStarredFromOther ? autoMirrorStarred : autoMirrorOwned; + }); + const skippedCount = beforeCount - reposNeedingMirror.length; + if (skippedCount > 0) { + console.log(`[Scheduler] Skipped ${skippedCount} repositories from auto-mirror (autoMirror=${autoMirrorOwned}, autoMirrorStarred=${autoMirrorStarred})`); } if (reposNeedingMirror.length > 0) { @@ -574,10 +577,12 @@ async function performInitialAutoStart(): Promise { continue; } - // Step 2: Trigger mirror for all repositories that need mirroring - // Only auto-mirror if autoMirror is enabled in schedule config - if (!config.scheduleConfig?.autoMirror) { - console.log(`[Scheduler] Step 2: Skipping initial mirror - autoMirror is disabled for user ${config.userId}`); + // Step 2: Trigger mirror for all repositories that need mirroring. + // autoMirror covers owned repos; autoMirrorStarred covers starred repos from other owners. + const autoMirrorOwned = !!config.scheduleConfig?.autoMirror; + const autoMirrorStarred = !!config.githubConfig?.autoMirrorStarred; + if (!autoMirrorOwned && !autoMirrorStarred) { + console.log(`[Scheduler] Step 2: Skipping initial mirror - autoMirror and autoMirrorStarred are both disabled for user ${config.userId}`); // Still update schedule config timestamps const currentTime2 = new Date(); @@ -587,7 +592,7 @@ async function performInitialAutoStart(): Promise { continue; } - console.log(`[Scheduler] Step 2: Triggering mirror for repositories that need mirroring...`); + console.log(`[Scheduler] Step 2: Triggering mirror for repositories that need mirroring (owned=${autoMirrorOwned}, starred=${autoMirrorStarred})...`); let reposNeedingMirror = await db .select() .from(repositories) @@ -602,17 +607,16 @@ async function performInitialAutoStart(): Promise { ) ); - // Filter out starred repos from auto-mirror when autoMirrorStarred is disabled - if (!config.githubConfig?.autoMirrorStarred) { - const githubOwner = config.githubConfig?.owner || ''; - const beforeCount = reposNeedingMirror.length; - reposNeedingMirror = reposNeedingMirror.filter( - repo => !repo.isStarred || repo.owner === githubOwner - ); - const skippedCount = beforeCount - reposNeedingMirror.length; - if (skippedCount > 0) { - console.log(`[Scheduler] Skipped ${skippedCount} starred repositories from initial auto-mirror (autoMirrorStarred is disabled)`); - } + const githubOwner = (config.githubConfig?.owner || '').toLowerCase(); + const beforeCount = reposNeedingMirror.length; + reposNeedingMirror = reposNeedingMirror.filter(repo => { + // GitHub usernames are case-insensitive; lowercase both sides to avoid misclassifying self-starred repos. + const isStarredFromOther = repo.isStarred && repo.owner.toLowerCase() !== githubOwner; + return isStarredFromOther ? autoMirrorStarred : autoMirrorOwned; + }); + const skippedCount = beforeCount - reposNeedingMirror.length; + if (skippedCount > 0) { + console.log(`[Scheduler] Skipped ${skippedCount} repositories from initial auto-mirror (autoMirror=${autoMirrorOwned}, autoMirrorStarred=${autoMirrorStarred})`); } if (reposNeedingMirror.length > 0) {