Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions layers/registry/server/utils/sync-repo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -567,8 +567,10 @@ export function unchangedRepoStatus(input: {
const { existing, hasAdmittedSkills, headTreeSha, repoPushedAt } = input
if (!hasAdmittedSkills || !existing?.last_tree_sha)
return null
if (headTreeSha && existing.last_tree_sha === headTreeSha)
return 'skipped-tree-sha'
// A known tree SHA is the content identity. Use pushed_at only when
// GitHub did not provide it.
if (headTreeSha)
return existing.last_tree_sha === headTreeSha ? 'skipped-tree-sha' : null
if (existing.pushed_at != null && repoPushedAt != null && existing.pushed_at >= repoPushedAt)
return 'skipped-pushed-at'
return null
Expand Down
12 changes: 12 additions & 0 deletions test/unit/sync-prefetch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,18 @@ describe('prefetchUnchangedRepos', () => {
.toEqual({ repo_meta_synced_at: null })
})

it('queues a changed tree even when the push timestamp has not advanced', async () => {
harness.raw.prepare(`UPDATE repos SET pushed_at = ? WHERE owner = 'acme' AND repo = 'changed'`)
.run(Date.parse('2026-10-01T00:00:00Z') / 1000)
vi.stubGlobal('fetch', vi.fn(graphqlAnswer))

const result = await prefetchUnchangedRepos({ db: harness.db, bindings, now: NOW }, [candidate('changed')])

expect(result.queue).toEqual([candidate('changed')])
expect(harness.raw.prepare(`SELECT repo_meta_synced_at FROM repos WHERE owner = 'acme' AND repo = 'changed'`).get())
.toEqual({ repo_meta_synced_at: null })
})

it('queues every candidate when GitHub does not answer the batch', async () => {
vi.stubGlobal('fetch', vi.fn(async () => new Response('bad gateway', { status: 502 })))

Expand Down
23 changes: 23 additions & 0 deletions test/unit/sync-repo-freshness.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,29 @@ describe('syncRepo freshness cursor', () => {
expect(statements.some(sql => sql.includes('SELECT name, current_sha'))).toBe(false)
})

it.each([1783857600, 1783857601])('reads a changed tree when the stored push timestamp is %s', async (pushedAt) => {
sqlite.prepare(`UPDATE repos SET pushed_at = ?`).run(pushedAt)
github.getRepoSummary.mockResolvedValue(repoSummary('changed-tree'))
github.getTree.mockResolvedValue({ status: 503, data: null, rateLimit: null, notModified: false })

const result = await syncRepo('acme', 'skills', {}, db)

expect(result).toMatchObject({ status: 'failed', reason: 'tree_fetch_failed:503' })
expect(github.getTree.mock.calls[0]?.[2]).toBe('changed-tree')
expect(sqlite.prepare(`SELECT repo_meta_synced_at FROM repos`).pluck().get()).toBe(75)
})

it('uses the push timestamp when GitHub gives no head tree', async () => {
sqlite.prepare(`UPDATE repos SET pushed_at = 1783857600`).run()
github.getRepoSummary.mockResolvedValue(repoSummary(null))

const result = await syncRepo('acme', 'skills', {}, db)

expect(result.status).toBe('skipped-pushed-at')
expect(github.getTree).not.toHaveBeenCalled()
expect(sqlite.prepare(`SELECT repo_meta_synced_at FROM repos`).pluck().get()).toBe(1783900800)
})

it('clears a stale too-large verdict once the repo syncs clean', async () => {
sqlite.prepare(`UPDATE repos SET tree_truncated_at = 123`).run()

Expand Down
Loading