diff --git a/mobile/__tests__/services/gitService.test.ts b/mobile/__tests__/services/gitService.test.ts index c0a6759..07e3fed 100644 --- a/mobile/__tests__/services/gitService.test.ts +++ b/mobile/__tests__/services/gitService.test.ts @@ -703,6 +703,70 @@ describe('GitService', () => { }) ); }); + + it('should download missing blobs even when metadata commit already matches remote tip', async () => { + const localPath = 'file:///mock/documents/vault/repo'; + const sharedCommitSha = 'remote-tip-sha'; + const metadata = { + version: 1, + transport: 'github-api', + repoUrl: 'https://github.com/test/repo', + owner: 'test', + repo: 'repo', + branch: 'main', + commitSha: sharedCommitSha, + treeSha: 'tree-sha-1', + files: { + 'README.md': { sha: 'blob-readme', mode: '100644', type: 'blob' }, + }, + }; + + (AsyncStorage.getItem as jest.Mock).mockResolvedValueOnce( + JSON.stringify({ ['https://github.com/test/repo']: { username: 'token', token: 'ghp_testtoken123' } }) + ); + + (FileSystem.getInfoAsync as jest.Mock).mockImplementation(async (path: string) => { + if (path === 'file:///mock/documents/vault/repo/.synapse/repo.json') return { exists: true, isDirectory: false, size: 0 }; + if (path === 'file:///mock/documents/vault/repo/README.md') return { exists: true, isDirectory: false, size: 0 }; + if (path === 'file:///mock/documents/vault/repo/NEW.md') return { exists: false, isDirectory: false, size: 0 }; + return { exists: false, isDirectory: false, size: 0 }; + }); + + (FileSystem.readAsStringAsync as jest.Mock).mockImplementation(async (path: string, options?: any) => { + if (path === 'file:///mock/documents/vault/repo/.synapse/repo.json') return JSON.stringify(metadata); + return ''; + }); + + mockFetch + .mockResolvedValueOnce({ ok: true, json: async () => ({ commit: { sha: sharedCommitSha } }) }) + .mockResolvedValueOnce({ ok: true, json: async () => ({ tree: { sha: 'tree-sha-1' } }) }) + .mockResolvedValueOnce({ + ok: true, + json: async () => ({ + tree: [ + { path: 'README.md', type: 'blob', mode: '100644', sha: 'blob-readme' }, + { path: 'NEW.md', type: 'blob', mode: '100644', sha: 'blob-new' }, + ], + }), + }) + .mockResolvedValueOnce({ ok: true, json: async () => ({ content: 'TkVXIEZJTEUK', encoding: 'base64' }) }); + + await GitService.refreshRemote(localPath); + + expect(FileSystem.writeAsStringAsync).toHaveBeenCalledWith( + 'file:///mock/documents/vault/repo/NEW.md', + 'TkVXIEZJTEUK', + { encoding: 'base64' } + ); + const metaWrite = (FileSystem.writeAsStringAsync as jest.Mock).mock.calls.find( + (call: string[]) => call[0] === 'file:///mock/documents/vault/repo/.synapse/repo.json' + ); + expect(metaWrite).toBeDefined(); + const saved = JSON.parse(metaWrite![1] as string); + expect(saved.files['NEW.md']).toEqual( + expect.objectContaining({ sha: 'blob-new', mode: '100644', type: 'blob' }) + ); + }); }); describe('Error Handling', () => { diff --git a/mobile/src/services/gitService.ts b/mobile/src/services/gitService.ts index 48ef0b3..742bce9 100644 --- a/mobile/src/services/gitService.ts +++ b/mobile/src/services/gitService.ts @@ -877,11 +877,6 @@ export class GitService { const remoteState = await this.fetchGitHubApiRemoteState(metadata); - // Nothing changed since last sync — skip all file work - if (remoteState.commitSha === metadata.commitSha) { - return; - } - const remoteBlobs = remoteState.tree.filter((entry) => entry.type === 'blob'); const nextFiles: RepoMetadataFile['files'] = { ...metadata.files };