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
46 changes: 30 additions & 16 deletions internal/daytona/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -213,24 +213,38 @@ func (c *Client) GetSnapshot(ctx context.Context, id string) (*Snapshot, error)
return &s, json.NewDecoder(resp.Body).Decode(&s)
}

// ListSnapshots returns all Daytona snapshots.
// ListSnapshots returns all Daytona snapshots. v0.189 paginates GET /api/snapshots
// as {items, total, page, totalPages} with a default page size of 100, so walk every
// page rather than returning only the first — otherwise the list silently truncates
// once the registry holds more than one page of snapshots.
func (c *Client) ListSnapshots(ctx context.Context) ([]Snapshot, error) {
resp, err := c.do(ctx, http.MethodGet, "/api/snapshots", nil)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
b, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("daytona list snapshots: %s", b)
}
var page struct {
Items []Snapshot `json:"items"`
}
if err := json.NewDecoder(resp.Body).Decode(&page); err != nil {
return nil, fmt.Errorf("decode snapshots: %w", err)
var all []Snapshot
for page := 1; ; page++ {
path := fmt.Sprintf("/api/snapshots?limit=100&page=%d", page)
resp, err := c.do(ctx, http.MethodGet, path, nil)
if err != nil {
return nil, err
}
body, err := io.ReadAll(resp.Body)
resp.Body.Close()
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("daytona list snapshots: %s", body)
}
var paged struct {
Items []Snapshot `json:"items"`
TotalPages int `json:"totalPages"`
}
if err := json.Unmarshal(body, &paged); err != nil {
return nil, fmt.Errorf("decode snapshots: %w", err)
}
all = append(all, paged.Items...)
if len(paged.Items) == 0 || page >= paged.TotalPages {
return all, nil
}
}
return page.Items, nil
}

// BaseURL returns the configured base URL (used by reverse proxy).
Expand Down
60 changes: 60 additions & 0 deletions internal/daytona/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,66 @@ func TestListSandboxes_SetsAuthHeader(t *testing.T) {
}
}

// ── ListSnapshots ─────────────────────────────────────────────────────────────

func TestListSnapshots_WalksAllPages(t *testing.T) {
var gotPages []string
srv := mockServer(t, func(w http.ResponseWriter, r *http.Request) {
gotPages = append(gotPages, r.URL.Query().Get("page"))
w.Header().Set("Content-Type", "application/json")
switch r.URL.Query().Get("page") {
case "1":
w.Write([]byte(`{"items":[{"id":"s1"},{"id":"s2"}],"total":3,"page":1,"totalPages":2}`))
case "2":
w.Write([]byte(`{"items":[{"id":"s3"}],"total":3,"page":2,"totalPages":2}`))
default:
t.Errorf("unexpected page request: %q", r.URL.Query().Get("page"))
}
})

c := NewClient(srv.URL, "key")
got, err := c.ListSnapshots(context.Background())
if err != nil {
t.Fatalf("ListSnapshots: %v", err)
}
if len(got) != 3 {
t.Fatalf("length: got %d want 3 (pagination truncated?)", len(got))
}
if got[0].ID != "s1" || got[2].ID != "s3" {
t.Errorf("snapshot IDs: got %v", got)
}
if len(gotPages) != 2 || gotPages[0] != "1" || gotPages[1] != "2" {
t.Errorf("expected page requests [1 2], got %v", gotPages)
}
}

func TestListSnapshots_SinglePage(t *testing.T) {
srv := mockServer(t, func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(`{"items":[{"id":"only"}],"total":1,"page":1,"totalPages":1}`))
})

c := NewClient(srv.URL, "key")
got, err := c.ListSnapshots(context.Background())
if err != nil {
t.Fatalf("ListSnapshots: %v", err)
}
if len(got) != 1 || got[0].ID != "only" {
t.Errorf("got %v want single snapshot 'only'", got)
}
}

func TestListSnapshots_NonOK_ReturnsError(t *testing.T) {
srv := mockServer(t, func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusBadGateway)
})

c := NewClient(srv.URL, "key")
if _, err := c.ListSnapshots(context.Background()); err == nil {
t.Fatal("expected error for non-200, got nil")
}
}

// ── StopSandbox ───────────────────────────────────────────────────────────────

func TestStopSandbox_OK(t *testing.T) {
Expand Down