github.com/anthropics/anthropic-sdk-go@v1.70.1/lib/environments/memories_two_pass_test.go (about)

     1  package environments
     2  
     3  // The sync's two passes: a content-free listing, then targeted fetches.
     4  //
     5  // A sync lists shas only (basic view) and fetches a memory's body just
     6  // before writing it to disk; only the download takes full pages, since it
     7  // needs every memory anyway. These tests pin that fetch discipline: a sync
     8  // that writes nothing fetches nothing, a changed path fetches exactly
     9  // itself, fetches fan out rather than queue, and a fetch that fails — or
    10  // races a server-side delete — leaves the old state for the next sync.
    11  
    12  import (
    13  	"context"
    14  	"net/http"
    15  	"net/http/httptest"
    16  	"path/filepath"
    17  	"strings"
    18  	"testing"
    19  	"time"
    20  
    21  	anthropic "github.com/anthropics/anthropic-sdk-go"
    22  	"github.com/anthropics/anthropic-sdk-go/option"
    23  	"github.com/stretchr/testify/require"
    24  )
    25  
    26  func TestTwoPass_ASyncThatPullsNothingFetchesNoContent(t *testing.T) {
    27  	local, server, stores := downloadedStores(t, map[string]string{"a.md": "v1", "b.md": "v1", "c.md": "v1"}, "", nil, nil)
    28  	ctx := context.Background()
    29  	// The download took full listing pages: no single-memory fetches at all.
    30  	require.Empty(t, server.ContentFetchesSnapshot())
    31  
    32  	writeLocal(t, local, "a.md", "v2 from agent") // a push needs no remote content
    33  	runSync(ctx, stores)
    34  
    35  	require.Empty(t, server.ContentFetchesSnapshot())
    36  	require.Equal(t, "v2 from agent", server.Files["a.md"])
    37  }
    38  
    39  func TestTwoPass_OnlyTheChangedMemoryIsFetched(t *testing.T) {
    40  	local, server, stores := downloadedStores(t, map[string]string{"a.md": "v1", "b.md": "v1"}, "", nil, nil)
    41  	ctx := context.Background()
    42  	server.ClearContentFetches()
    43  
    44  	server.Write("a.md", "v2 from server")
    45  	runSync(ctx, stores)
    46  
    47  	require.Equal(t, []string{"a.md"}, server.ContentFetchesSnapshot())
    48  	require.Equal(t, "v2 from server", readLocal(t, local, "a.md"))
    49  	require.Equal(t, "v1", readLocal(t, local, "b.md"))
    50  }
    51  
    52  func TestTwoPass_AFileAlreadyHoldingTheRemoteContentIsAdoptedWithoutAFetch(t *testing.T) {
    53  	local, server, stores := downloadedStores(t, map[string]string{"a.md": "v1"}, "", nil, nil)
    54  	ctx := context.Background()
    55  	server.ClearContentFetches()
    56  
    57  	// Both sides landed on the same bytes independently.
    58  	writeLocal(t, local, "a.md", "v2 on both")
    59  	server.Write("a.md", "v2 on both")
    60  	runSync(ctx, stores)
    61  
    62  	require.Empty(t, server.ContentFetchesSnapshot())
    63  	require.Empty(t, server.ReceivedSnapshot())
    64  
    65  	// The baseline advanced to the adopted sha: the next edit pushes guarded by it.
    66  	writeLocal(t, local, "a.md", "v3 from agent")
    67  	runSync(ctx, stores)
    68  	require.Equal(t, []string{updated("a.md", "v3 from agent", "v2 on both")}, server.ReceivedSnapshot())
    69  }
    70  
    71  func TestTwoPass_ContentFetchesFanOutWithinAStore(t *testing.T) {
    72  	// The first memory's fetch completes only after the second's starts —
    73  	// only parallel fetches satisfy that; serial ones would time out.
    74  	server := newMemoryServer(t, map[string]string{"a.md": "v1", "b.md": "v1"}, "")
    75  	secondStarted := make(chan struct{})
    76  	httpSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    77  		isFetch := r.Method == http.MethodGet && strings.Contains(r.URL.Path, "/memories/")
    78  		if isFetch {
    79  			if strings.HasSuffix(r.URL.Path, "/"+memID("a.md")) {
    80  				select {
    81  				case <-secondStarted:
    82  				case <-time.After(10 * time.Second):
    83  					t.Error("the second fetch never started: fetches ran serially")
    84  				}
    85  			} else {
    86  				close(secondStarted)
    87  			}
    88  		}
    89  		server.serveHTTP(w, r)
    90  	}))
    91  	t.Cleanup(httpSrv.Close)
    92  	client := anthropic.NewClient(option.WithBaseURL(httpSrv.URL), option.WithAPIKey("k"), option.WithMaxRetries(0))
    93  	workdir := t.TempDir()
    94  	stores := mustStores(t, client, SessionMemoryStoresOptions{Workdir: workdir, Logger: silentLogger})
    95  	ctx := context.Background()
    96  	require.NoError(t, stores.Download(ctx, fetchSession(t, client)))
    97  	local := filepath.Join(workdir, "memory", "notes")
    98  
    99  	server.Write("a.md", "v2 from server")
   100  	server.Write("b.md", "v2 from server")
   101  	runSync(ctx, stores)
   102  
   103  	require.Equal(t, "v2 from server", readLocal(t, local, "a.md"))
   104  	require.Equal(t, "v2 from server", readLocal(t, local, "b.md"))
   105  }
   106  
   107  func TestTwoPass_AFailedFetchKeepsTheOldStateAndTheNextSyncRetries(t *testing.T) {
   108  	local, server, stores := downloadedStores(t, map[string]string{"a.md": "v1"}, "", nil, nil)
   109  	ctx := context.Background()
   110  	server.Write("a.md", "v2 from server")
   111  
   112  	server.SetOnFetch(func(string) int { return 500 })
   113  	runSync(ctx, stores)
   114  	require.Equal(t, "v1", readLocal(t, local, "a.md"))
   115  
   116  	server.SetOnFetch(nil)
   117  	runSync(ctx, stores)
   118  	require.Equal(t, "v2 from server", readLocal(t, local, "a.md"))
   119  	// The stale baseline drove no uploads or deletes along the way.
   120  	require.Empty(t, server.ReceivedSnapshot())
   121  }
   122  
   123  func TestTwoPass_AMemoryDeletedBetweenListingAndFetchWaitsForTheNextSync(t *testing.T) {
   124  	local, server, stores := downloadedStores(t, map[string]string{"a.md": "v1", "keep.md": "v1"}, "", nil, nil)
   125  	ctx := context.Background()
   126  	server.Write("a.md", "v2 from server")
   127  
   128  	// Called with the server mutex held, so Files is safe to touch.
   129  	server.SetOnFetch(func(path string) int {
   130  		delete(server.Files, path)
   131  		return 0
   132  	})
   133  	runSync(ctx, stores)
   134  	// The fetch 404ed: disk and baseline keep the old version for now.
   135  	require.Equal(t, "v1", readLocal(t, local, "a.md"))
   136  
   137  	server.SetOnFetch(nil)
   138  	runSync(ctx, stores)
   139  	// The next sync saw the server-side delete and took the local copy with it.
   140  	require.NoFileExists(t, filepath.Join(local, "a.md"))
   141  	require.Empty(t, server.ReceivedSnapshot())
   142  }
   143  
   144  func TestTwoPass_ListingsAskForTheLargestPageTheViewAllows(t *testing.T) {
   145  	// The download lists full pages at the server's cap for that view; the
   146  	// sync lists basic pages at the API maximum.
   147  	server := newMemoryServer(t, map[string]string{"a.md": "v1"}, "")
   148  	var listQueries []string
   149  	httpSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   150  		if r.Method == http.MethodGet && strings.HasSuffix(r.URL.Path, "/memories") {
   151  			q := r.URL.Query()
   152  			listQueries = append(listQueries, "view="+q.Get("view")+" limit="+q.Get("limit"))
   153  		}
   154  		server.serveHTTP(w, r)
   155  	}))
   156  	t.Cleanup(httpSrv.Close)
   157  	client := anthropic.NewClient(option.WithBaseURL(httpSrv.URL), option.WithAPIKey("k"), option.WithMaxRetries(0))
   158  	stores := mustStores(t, client, SessionMemoryStoresOptions{Workdir: t.TempDir(), Logger: silentLogger})
   159  	ctx := context.Background()
   160  	require.NoError(t, stores.Download(ctx, fetchSession(t, client)))
   161  	runSync(ctx, stores)
   162  
   163  	require.Equal(t, []string{"view=full limit=20", "view=basic limit=100"}, listQueries)
   164  }