@@ -7488,6 +7488,76 @@ func (c *fakeClient) ListTree(ctx context.Context, workspaceID, path string, dep
74887488 }, nil
74897489}
74907490
7491+ type hierarchicalTreeCall struct {
7492+ path string
7493+ depth int
7494+ entries int
7495+ }
7496+
7497+ // hierarchicalTreeClient mirrors the production tree contract: a request
7498+ // returns files and synthesized directories only through the requested depth.
7499+ // It lets traversal tests distinguish pruning before descent from filtering a
7500+ // flat depth=200 response after the server has already enumerated every leaf.
7501+ type hierarchicalTreeClient struct {
7502+ * fakeClient
7503+ calls []hierarchicalTreeCall
7504+ entriesReturned int
7505+ }
7506+
7507+ func (c * hierarchicalTreeClient ) ListTree (_ context.Context , _ string , path string , depth int , cursor string ) (TreeResponse , error ) {
7508+ base := normalizeRemotePath (path )
7509+ if depth <= 0 {
7510+ depth = 1
7511+ }
7512+ entriesByPath := map [string ]TreeEntry {}
7513+ for remotePath , file := range c .files {
7514+ remotePath = normalizeRemotePath (remotePath )
7515+ if ! isUnderRemoteRoot (base , remotePath ) || remotePath == base {
7516+ continue
7517+ }
7518+ relative := strings .TrimPrefix (strings .TrimPrefix (remotePath , base ), "/" )
7519+ parts := strings .Split (relative , "/" )
7520+ levels := depth
7521+ if len (parts ) < levels {
7522+ levels = len (parts )
7523+ }
7524+ for level := 1 ; level <= levels ; level ++ {
7525+ entryPath := normalizeRemotePath (strings .TrimSuffix (base , "/" ) + "/" + strings .Join (parts [:level ], "/" ))
7526+ if level == len (parts ) {
7527+ entriesByPath [entryPath ] = TreeEntry {
7528+ Path : entryPath ,
7529+ Type : "file" ,
7530+ Revision : file .Revision ,
7531+ ContentHash : file .ContentHash ,
7532+ Size : int64 (len (file .Content )),
7533+ }
7534+ continue
7535+ }
7536+ if _ , exists := entriesByPath [entryPath ]; ! exists {
7537+ entriesByPath [entryPath ] = TreeEntry {Path : entryPath , Type : "dir" , Revision : "dir" }
7538+ }
7539+ }
7540+ }
7541+ entries := make ([]TreeEntry , 0 , len (entriesByPath ))
7542+ for _ , entry := range entriesByPath {
7543+ entries = append (entries , entry )
7544+ }
7545+ sort .Slice (entries , func (i , j int ) bool { return entries [i ].Path < entries [j ].Path })
7546+ if cursor != "" {
7547+ start := len (entries )
7548+ for index , entry := range entries {
7549+ if entry .Path == cursor {
7550+ start = index + 1
7551+ break
7552+ }
7553+ }
7554+ entries = entries [start :]
7555+ }
7556+ c .calls = append (c .calls , hierarchicalTreeCall {path : base , depth : depth , entries : len (entries )})
7557+ c .entriesReturned += len (entries )
7558+ return TreeResponse {Path : base , Entries : entries }, nil
7559+ }
7560+
74917561func (c * fakeClient ) LatestEventID (ctx context.Context , workspaceID , provider string ) (string , error ) {
74927562 _ = ctx
74937563 _ = workspaceID
@@ -9943,6 +10013,119 @@ func TestPullRemoteFullTreeSkipsNestedMountRuntimeState(t *testing.T) {
994310013 }
994410014}
994510015
10016+ func TestIsMountRuntimeRelativePathExcludesOnlyReservedRuntimeArtifacts (t * testing.T ) {
10017+ tests := []struct {
10018+ path string
10019+ want bool
10020+ }{
10021+ {path : ".relay" , want : true },
10022+ {path : "slack/channels/C123/messages/1/.relay/state.json" , want : true },
10023+ {path : "slack/channels/C123/messages/1/.relay/outbox/acked/mountcmd_1.json" , want : true },
10024+ {path : "slack/channels/C123/messages/1/.relayfile-mount-state.json" , want : true },
10025+ {path : "slack/channels/C123/messages/1/relay/state.json" , want : false },
10026+ {path : "slack/channels/C123/messages/1/.relay-notes/state.json" , want : false },
10027+ {path : "slack/channels/C123/messages/1/outbox/acked/mountcmd_1.json" , want : false },
10028+ {path : "slack/channels/C123/messages/1/notes/mountcmd_1.json" , want : false },
10029+ {path : "slack/channels/C123/messages/1/.relayfile-mount-state.json.backup" , want : false },
10030+ }
10031+ for _ , tt := range tests {
10032+ t .Run (tt .path , func (t * testing.T ) {
10033+ if got := isMountRuntimeRelativePath (tt .path ); got != tt .want {
10034+ t .Fatalf ("isMountRuntimeRelativePath(%q) = %t, want %t" , tt .path , got , tt .want )
10035+ }
10036+ })
10037+ }
10038+ }
10039+
10040+ func TestPullRemoteFullTreePrunesNestedMountRuntimeBeforeDescendantEnumeration (t * testing.T ) {
10041+ files := map [string ]RemoteFile {
10042+ "/slack/channels/C123/messages/1780145510_376649.json" : {
10043+ Path : "/slack/channels/C123/messages/1780145510_376649.json" , Revision : "rev_msg" , Content : `{"text":"hello"}` ,
10044+ },
10045+ "/slack/channels/C123/messages/1780145510_376649/.relay-notes/keep.md" : {
10046+ Path : "/slack/channels/C123/messages/1780145510_376649/.relay-notes/keep.md" , Revision : "rev_notes" , Content : "real notes" ,
10047+ },
10048+ "/slack/channels/C123/messages/1780145510_376649/outbox/acked/mountcmd_real.json" : {
10049+ Path : "/slack/channels/C123/messages/1780145510_376649/outbox/acked/mountcmd_real.json" , Revision : "rev_real_cmd" , Content : `{"real":true}` ,
10050+ },
10051+ "/slack/channels/C123/messages/1780145510_376649/.relay/state.json" : {
10052+ Path : "/slack/channels/C123/messages/1780145510_376649/.relay/state.json" , Revision : "rev_runtime_state" , Content : `{"pendingWriteback":400}` ,
10053+ },
10054+ }
10055+ for index := 0 ; index < 400 ; index ++ {
10056+ path := fmt .Sprintf ("/slack/channels/C123/messages/1780145510_376649/.relay/outbox/acked/mountcmd_%03d.json" , index )
10057+ files [path ] = RemoteFile {Path : path , Revision : fmt .Sprintf ("rev_runtime_%03d" , index ), Content : `{"runtime":true}` }
10058+ }
10059+ baseClient := & fakeClient {files : files }
10060+ client := & hierarchicalTreeClient {fakeClient : baseClient }
10061+ logger := & captureLogger {}
10062+ localDir := t .TempDir ()
10063+ syncer , err := NewSyncer (client , SyncerOptions {
10064+ WorkspaceID : "ws_prune_nested_relay" ,
10065+ RemoteRoot : "/slack" ,
10066+ LocalRoot : localDir ,
10067+ Logger : logger ,
10068+ })
10069+ if err != nil {
10070+ t .Fatalf ("new syncer failed: %v" , err )
10071+ }
10072+ runtimeDir := filepath .Join (localDir , "channels" , "C123" , "messages" , "1780145510_376649" , ".relay" )
10073+ staleRuntimePath := filepath .Join (runtimeDir , "outbox" , "acked" , "mountcmd_stale.json" )
10074+ if err := os .MkdirAll (filepath .Dir (staleRuntimePath ), 0o755 ); err != nil {
10075+ t .Fatalf ("mkdir stale runtime tree: %v" , err )
10076+ }
10077+ if err := os .WriteFile (staleRuntimePath , []byte (`{"stale":true}` ), 0o644 ); err != nil {
10078+ t .Fatalf ("write stale runtime artifact: %v" , err )
10079+ }
10080+ staleRemotePath := "/slack/channels/C123/messages/1780145510_376649/.relay/outbox/acked/mountcmd_stale.json"
10081+ syncer .state .Files [staleRemotePath ] = trackedFile {Revision : "rev_stale" , Hash : hashString (`{"stale":true}` )}
10082+
10083+ if err := syncer .pullRemoteFullTree (context .Background (), nil , bootstrapProgress {}); err != nil {
10084+ t .Fatalf ("pullRemoteFullTree failed: %v" , err )
10085+ }
10086+
10087+ if client .entriesReturned >= 40 {
10088+ t .Fatalf ("runtime subtree was enumerated before filtering: returned %d entries across calls %#v" , client .entriesReturned , client .calls )
10089+ }
10090+ for _ , call := range client .calls {
10091+ if isMountRuntimeRemotePath (call .path ) {
10092+ t .Fatalf ("ListTree descended into reserved runtime subtree: %#v" , call )
10093+ }
10094+ if call .depth != 1 {
10095+ t .Fatalf ("ListTree depth = %d for %s, want 1 so runtime directories can be pruned before descent" , call .depth , call .path )
10096+ }
10097+ }
10098+ for _ , realPath := range []string {
10099+ filepath .Join (localDir , "channels" , "C123" , "messages" , "1780145510_376649.json" ),
10100+ filepath .Join (localDir , "channels" , "C123" , "messages" , "1780145510_376649" , ".relay-notes" , "keep.md" ),
10101+ filepath .Join (localDir , "channels" , "C123" , "messages" , "1780145510_376649" , "outbox" , "acked" , "mountcmd_real.json" ),
10102+ } {
10103+ if _ , err := os .Stat (realPath ); err != nil {
10104+ t .Fatalf ("expected real workspace content %s to remain mirrored: %v" , realPath , err )
10105+ }
10106+ }
10107+ if _ , err := os .Stat (runtimeDir ); ! errors .Is (err , os .ErrNotExist ) {
10108+ t .Fatalf ("nested runtime subtree should be absent after pruning, stat err=%v" , err )
10109+ }
10110+ if _ , exists := syncer .state .Files [staleRemotePath ]; exists {
10111+ t .Fatalf ("stale nested runtime artifact remained tracked" )
10112+ }
10113+ for path , calls := range baseClient .readFileCallsByPath {
10114+ if isMountRuntimeRemotePath (path ) && calls > 0 {
10115+ t .Fatalf ("runtime artifact %s was read %d time(s)" , path , calls )
10116+ }
10117+ }
10118+ var summary string
10119+ for _ , line := range logger .lines {
10120+ if strings .Contains (line , "mount full-tree traversal summary" ) {
10121+ summary = line
10122+ }
10123+ }
10124+ if ! strings .Contains (summary , "runtime_subtrees_pruned=1" ) {
10125+ t .Fatalf ("expected traversal summary to report one pruned runtime subtree, got %q" , summary )
10126+ }
10127+ }
10128+
994610129func TestPullRemoteIncrementalSkipsNestedMountRuntimeState (t * testing.T ) {
994710130 client := & fakeClient {
994810131 files : map [string ]RemoteFile {
0 commit comments