From 25e2d2f3904455de7550fd61f073bc099b052c4f Mon Sep 17 00:00:00 2001 From: Andy Dixon Date: Sun, 15 Feb 2026 18:15:47 +0000 Subject: [PATCH] Keep one snapshot per session instead of one globally Indexing used to discard every existing snapshot, so concurrent users wiped out each other's landscapes. Only the same session's previous snapshot and workspace are replaced now. --- main.go | 10 ++++++---- main_test.go | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 4 deletions(-) diff --git a/main.go b/main.go index e62b5f2..444102a 100644 --- a/main.go +++ b/main.go @@ -519,13 +519,15 @@ func (s *Server) indexJob(id, root string) { s.jobsMu.Unlock() snapshot.ID = uniqueID("snapshot") - // Only the latest snapshot is kept; older workspaces are deleted. + // Each session keeps only its latest snapshot; its older workspaces are deleted. s.snapshotsMu.Lock() var oldRoots []string - for _, old := range s.snapshots { - oldRoots = append(oldRoots, old.Root) + for key, old := range s.snapshots { + if old.Owner == snapshot.Owner { + oldRoots = append(oldRoots, old.Root) + delete(s.snapshots, key) + } } - clear(s.snapshots) s.snapshots[snapshot.ID] = snapshot s.snapshotsMu.Unlock() for _, old := range oldRoots { diff --git a/main_test.go b/main_test.go index e283a5b..795dc59 100644 --- a/main_test.go +++ b/main_test.go @@ -286,3 +286,40 @@ func TestSessionCookieFlags(t *testing.T) { } } } + +// indexLocal uploads files as a local job and waits for the snapshot. +func (c client) indexLocal(files map[string]string) (jobID, snapshotID string) { + c.t.Helper() + var created struct{ JobID string } + c.json("POST", "/api/jobs", `{"kind":"local"}`, 201, &created) + for path, content := range files { + c.json("POST", "/api/jobs/"+created.JobID+"/files?path="+path, content, 200, nil) + } + c.json("POST", "/api/jobs/"+created.JobID+"/commit", "", 202, nil) + status, raw := c.do("GET", "/api/jobs/"+created.JobID+"/events", "") + var job Job + for line := range strings.Lines(string(raw)) { + if data, ok := strings.CutPrefix(line, "data: "); ok { + json.Unmarshal([]byte(data), &job) + } + } + if status != 200 || job.SnapshotID == nil { + c.t.Fatalf("job did not finish: %d %+v", status, job) + } + return created.JobID, *job.SnapshotID +} + +func TestSessionsKeepIndependentSnapshots(t *testing.T) { + server, alice := newTestServer(t) + bob := alice.stranger() + _, aliceFirst := alice.indexLocal(map[string]string{"a.go": "package a"}) + _, bobSnapshot := bob.indexLocal(map[string]string{"b.go": "package b"}) + _, aliceSecond := alice.indexLocal(map[string]string{"c.go": "package c"}) + + alice.json("GET", "/api/snapshots/"+aliceFirst, "", 404, nil) + alice.json("GET", "/api/snapshots/"+aliceSecond, "", 200, nil) + bob.json("GET", "/api/snapshots/"+bobSnapshot, "", 200, nil) + if len(server.snapshots) != 2 { + t.Fatalf("snapshots = %d, want 2", len(server.snapshots)) + } +}