From cef3c2a21911c9abf79fbbdc5bab0436d9715654 Mon Sep 17 00:00:00 2001 From: Khaliq Date: Sun, 20 Sep 2026 08:32:33 -0700 Subject: [PATCH] fix(mount): ignore stale GitHub head records in strict bootstrap --- internal/mountsync/syncer.go | 28 +++++++++++++ internal/mountsync/syncer_test.go | 67 +++++++++++++++++++++++++++++++ 2 files changed, 95 insertions(+) diff --git a/internal/mountsync/syncer.go b/internal/mountsync/syncer.go index 3124dba0..e62d2b67 100644 --- a/internal/mountsync/syncer.go +++ b/internal/mountsync/syncer.go @@ -7584,6 +7584,11 @@ func (s *Syncer) pullRemoteFullTree(ctx context.Context, conflicted map[string]s } continue } + if strictCompleteGithubSource && + (page.Entries[i].Type == remoteTypeFile || page.Entries[i].Type == remoteTypeSymlink) && + s.githubWorkingTreeRemotePathHasStaleHead(page.Entries[i].Path) { + continue + } if remainingFileBudget >= 0 && fileEntriesThisChunk >= remainingFileBudget { entryEnd = i break @@ -7618,6 +7623,11 @@ func (s *Syncer) pullRemoteFullTree(ctx context.Context, conflicted map[string]s if !isUnderRemoteRoot(s.remoteRoot, remotePath) { continue } + if strictCompleteGithubSource && + (entry.Type == remoteTypeFile || entry.Type == remoteTypeSymlink) && + s.githubWorkingTreeRemotePathHasStaleHead(remotePath) { + continue + } if strictCompleteGithubSource && (entry.Type == remoteTypeFile || entry.Type == remoteTypeSymlink) && runtimeRoot == "" { strictFilesThisChunk++ } @@ -12121,6 +12131,24 @@ func (s *Syncer) githubWorkingTreeRemotePathMatchesHead(remotePath string) bool return strings.HasSuffix(normalizeRemotePath(remotePath), "@"+headSHA+".json") } +func (s *Syncer) githubWorkingTreeRemotePathHasStaleHead(remotePath string) bool { + if s.githubWorkingTree == nil { + return false + } + headSHA := strings.TrimSpace(s.githubWorkingTree.HeadSHA) + if headSHA == "" { + return false + } + normalized := normalizeRemotePath(remotePath) + if strings.HasSuffix(normalized, "@"+headSHA+".json") { + return false + } + if _, ok := s.githubWorkingTree.remotePathToWorkingTreeRel(normalized); !ok { + return false + } + return true +} + func safeLocalPath(localRoot, rel string) (string, error) { localRoot = filepath.Clean(localRoot) rel = filepath.ToSlash(strings.TrimSpace(rel)) diff --git a/internal/mountsync/syncer_test.go b/internal/mountsync/syncer_test.go index 8242967d..057cd1fa 100644 --- a/internal/mountsync/syncer_test.go +++ b/internal/mountsync/syncer_test.go @@ -3398,6 +3398,73 @@ func TestGithubWorkingTreeLocalMappingPrefersCurrentHeadSHA(t *testing.T) { } } +func TestCompleteGithubTreeStrictTraversalIgnoresStaleHeadRecords(t *testing.T) { + localDir := t.TempDir() + contentsRoot := "/github/repos/AgentWorkforce/cloud/contents" + headSHA := "head123" + readme := []byte("# Cloud\n") + currentApp := []byte("export const ok = true;\n") + staleApp := []byte("export const ok = false;\n") + readmeRemote := contentsRoot + "/README.md@" + headSHA + ".json" + currentAppRemote := contentsRoot + "/src/app.ts@" + headSHA + ".json" + staleAppRemote := contentsRoot + "/src/app.ts@oldsha.json" + client := &fakeClient{files: map[string]RemoteFile{ + readmeRemote: { + Path: readmeRemote, + Revision: "rev_1", + Content: string(readme), + ContentHash: hashBytes(readme), + }, + currentAppRemote: { + Path: currentAppRemote, + Revision: "rev_2", + Content: string(currentApp), + ContentHash: hashBytes(currentApp), + }, + staleAppRemote: { + Path: staleAppRemote, + Revision: "rev_999", + Content: string(staleApp), + ContentHash: hashBytes(staleApp), + }, + }} + syncer, err := NewSyncer(client, SyncerOptions{ + WorkspaceID: "ws_complete_strict_stale_head", + RemoteRoot: contentsRoot, + LocalRoot: localDir, + StateFile: filepath.Join(localDir, ".relayfile-mount-state.json"), + WebSocket: boolPtr(false), + FullPullEvery: -1, + }) + if err != nil { + t.Fatalf("NewSyncer failed: %v", err) + } + syncer.githubWorkingTree.HeadSHA = headSHA + expected := 2 + syncer.state.GithubWorkingTreeSourceProfile = "complete-v1" + syncer.state.GithubWorkingTreeFilesExpected = &expected + + if err := syncer.pullRemoteFullTree(context.Background(), nil, bootstrapProgress{}); err != nil { + t.Fatalf("strict complete-v1 traversal should ignore stale head records: %v", err) + } + if !syncer.state.BootstrapComplete { + t.Fatal("strict complete-v1 traversal did not complete") + } + if _, tracked := syncer.state.Files[staleAppRemote]; tracked { + t.Fatalf("stale head record was tracked in mount state") + } + if got := len(syncer.state.Files); got != expected { + t.Fatalf("tracked file count=%d, want %d", got, expected) + } + gotApp, err := os.ReadFile(filepath.Join(localDir, "src", "app.ts")) + if err != nil { + t.Fatalf("read app.ts: %v", err) + } + if !bytes.Equal(gotApp, currentApp) { + t.Fatalf("app.ts content = %q, want current head content %q", string(gotApp), string(currentApp)) + } +} + func TestGithubWorkingTreeTarSeedRejectsDuplicateEntries(t *testing.T) { localDir := t.TempDir() contentsRoot := "/github/repos/AgentWorkforce/cloud/contents"