From 35469721dbf054e141f4953cee76183267ba9de4 Mon Sep 17 00:00:00 2001 From: Delicious233 <101502465+DeliciousBuding@users.noreply.github.com> Date: Sun, 6 Sep 2026 18:56:53 +0800 Subject: [PATCH 1/2] test(store): reproduce mutable checkpoint evidence Refs #2341. Capture writer-side aliasing across real backends, durable reopen and the checkpoint preview handler before changing production code. Co-authored-by: Codex --- edge-server/internal/api/checkpoint_test.go | 44 +++++++++ .../store/checkpoint_ownership_test.go | 96 +++++++++++++++++++ 2 files changed, 140 insertions(+) create mode 100644 edge-server/internal/store/checkpoint_ownership_test.go diff --git a/edge-server/internal/api/checkpoint_test.go b/edge-server/internal/api/checkpoint_test.go index 8ac49a12b..99f31f983 100644 --- a/edge-server/internal/api/checkpoint_test.go +++ b/edge-server/internal/api/checkpoint_test.go @@ -139,3 +139,47 @@ func TestGetRunCheckpointFile(t *testing.T) { t.Fatalf("POST status = %d, want 405", rec.Code) } } + +func TestCheckpointPreviewRetainsSavedEvidence(t *testing.T) { + for _, access := range []string{"input", "result"} { + t.Run(access, func(t *testing.T) { + h := newTestHandler() + runID := seedCheckpointRun(t, h) + input, ok := h.Store.GetRunCheckpoint(runID) + if !ok { + t.Fatal("seeded checkpoint missing") + } + // Replace the checkpoint, then reuse the writer-owned file list. + saved, err := h.Store.UpsertRunCheckpoint(input) + if err != nil { + t.Fatal(err) + } + view := input + if access == "result" { + view = saved + } + view.Files[0].Content = "changed after save" + view.Files[0].Hash = "changed-hash" + + req := httptest.NewRequest(http.MethodGet, "/v1/runs/"+runID+"/checkpoint/file?path=src/a.ts", nil) + rec := httptest.NewRecorder() + h.GetRunCheckpointFile(rec, req, runID) + if rec.Code != http.StatusOK { + t.Fatalf("preview status = %d, body=%s", rec.Code, rec.Body.String()) + } + var envelope struct { + Data struct { + Path, Hash, Content string + SizeBytes int + } + } + if err := json.Unmarshal(rec.Body.Bytes(), &envelope); err != nil { + t.Fatalf("decode preview: %v", err) + } + data := envelope.Data + if data.Path != "src/a.ts" || data.Content != "export {}" || data.Hash != "h-a" || data.SizeBytes != 12 { + t.Fatalf("caller %s changed saved preview evidence: %#v", access, data) + } + }) + } +} diff --git a/edge-server/internal/store/checkpoint_ownership_test.go b/edge-server/internal/store/checkpoint_ownership_test.go new file mode 100644 index 000000000..29bfc3273 --- /dev/null +++ b/edge-server/internal/store/checkpoint_ownership_test.go @@ -0,0 +1,96 @@ +package store + +import ( + "path/filepath" + "reflect" + "testing" +) + +func TestRunCheckpointWriteOwnsFiles(t *testing.T) { + t.Parallel() + for _, backend := range []struct { + name string + open func(string) (Repository, error) + }{ + {"memory", func(string) (Repository, error) { return New(), nil }}, + {"file", func(path string) (Repository, error) { return NewFile(path) }}, + {"sqlite", func(path string) (Repository, error) { return NewSQLite(path) }}, + } { + t.Run(backend.name, func(t *testing.T) { + for _, access := range []string{"input", "result"} { + t.Run(access, func(t *testing.T) { + path := filepath.Join(t.TempDir(), "checkpoint.data") + repo, err := backend.open(path) + if err != nil { + t.Fatal(err) + } + t.Cleanup(repo.Close) + if _, err := repo.CreateProject("project", "Checkpoint", ""); err != nil { + t.Fatal(err) + } + if _, err := repo.CreateThread("thread", "project", "Checkpoint", "", "", ""); err != nil { + t.Fatal(err) + } + if _, err := repo.CreateRun("run", "project", "thread"); err != nil { + t.Fatal(err) + } + input := RunCheckpoint{ + ID: "checkpoint", RunID: "run", WorkDir: "workspace", FileCount: 2, TotalBytes: 11, + Files: []CheckpointFile{ + {Path: "z.txt", Size: 6, Hash: "original-z", Content: "before"}, + {Path: "a.txt", Size: 5, Hash: "original-a", Content: "other"}, + }, + } + want := input + want.Files = append([]CheckpointFile(nil), input.Files...) + saved, err := repo.UpsertRunCheckpoint(input) + if err != nil { + t.Fatal(err) + } + want.CreatedAt = saved.CreatedAt + if want.CreatedAt == "" { + t.Fatal("checkpoint creation time was not stamped") + } + view := input + if access == "result" { + view = saved + } + view.Files[0] = CheckpointFile{Path: "caller.txt", Size: 1, Hash: "caller", Content: "x"} + + got, ok := repo.GetRunCheckpoint(input.RunID) + if !ok || !reflect.DeepEqual(got, want) { + t.Errorf("caller %s changed stored checkpoint: got=%#v want=%#v", access, got, want) + } + other := saved + if access == "result" { + other = input + } + if !reflect.DeepEqual(other.Files, want.Files) { + t.Errorf("checkpoint input and result share files: other=%#v", other.Files) + } + if backend.name == "memory" { + return + } + + // An unrelated successful write must not persist caller-side edits. + if _, err := repo.UpsertSettings(map[string]string{"theme": "dark"}); err != nil { + t.Fatal(err) + } + repo.Close() + if err := repo.(interface{ LastPersistError() error }).LastPersistError(); err != nil { + t.Fatalf("close persist: %v", err) + } + restored, err := backend.open(path) + if err != nil { + t.Fatal(err) + } + t.Cleanup(restored.Close) + got, ok = restored.GetRunCheckpoint(input.RunID) + if !ok || !reflect.DeepEqual(got, want) { + t.Errorf("caller %s edits survived reopen: got=%#v want=%#v", access, got, want) + } + }) + } + }) + } +} From 21745169df7f56b14a8583877deaa1bb3f5a624c Mon Sep 17 00:00:00 2001 From: Delicious233 <101502465+DeliciousBuding@users.noreply.github.com> Date: Sun, 6 Sep 2026 19:03:44 +0800 Subject: [PATCH 2/2] fix(store): detach checkpoint input and result file lists Refs #2341. Preserve saved pre-run evidence when writers reuse their input or result. Keep metadata, nil/empty writer results, read behavior and persistence policy unchanged. Co-authored-by: Codex --- .../store/checkpoint_ownership_test.go | 34 +++++++++++++++++++ edge-server/internal/store/store_domain.go | 4 +++ 2 files changed, 38 insertions(+) diff --git a/edge-server/internal/store/checkpoint_ownership_test.go b/edge-server/internal/store/checkpoint_ownership_test.go index 29bfc3273..0a98ad45c 100644 --- a/edge-server/internal/store/checkpoint_ownership_test.go +++ b/edge-server/internal/store/checkpoint_ownership_test.go @@ -94,3 +94,37 @@ func TestRunCheckpointWriteOwnsFiles(t *testing.T) { }) } } + +func TestRunCheckpointWritePreservesEmptyFileLists(t *testing.T) { + s := New() + if _, err := s.CreateProject("project", "Checkpoint", ""); err != nil { + t.Fatal(err) + } + if _, err := s.CreateThread("thread", "project", "Checkpoint", "", "", ""); err != nil { + t.Fatal(err) + } + if _, err := s.CreateRun("run", "project", "thread"); err != nil { + t.Fatal(err) + } + for _, tc := range []struct { + name string + files []CheckpointFile + }{ + {"nil", nil}, + {"empty", []CheckpointFile{}}, + } { + t.Run(tc.name, func(t *testing.T) { + input := RunCheckpoint{ + ID: "checkpoint", RunID: "run", WorkDir: "workspace", + CreatedAt: "2026-09-01T00:00:00Z", Files: tc.files, + } + saved, err := s.UpsertRunCheckpoint(input) + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(saved, input) { + t.Fatalf("upsert changed empty checkpoint metadata or list shape: got=%#v want=%#v", saved, input) + } + }) + } +} diff --git a/edge-server/internal/store/store_domain.go b/edge-server/internal/store/store_domain.go index 698cfb04c..f46f26ffa 100644 --- a/edge-server/internal/store/store_domain.go +++ b/edge-server/internal/store/store_domain.go @@ -3,6 +3,7 @@ package store // Residual pure-helper peel #1144: run/artifact/profile/settings methods. Same package; zero behavior change. import ( + "slices" "time" ) @@ -58,7 +59,10 @@ func (s *Store) UpsertRunCheckpoint(cp RunCheckpoint) (RunCheckpoint, error) { if cp.CreatedAt == "" { cp.CreatedAt = nowString() } + // Neither the input nor the result may share the stored evidence. + cp.Files = slices.Clone(cp.Files) s.checkpoints[cp.RunID] = cp + cp.Files = slices.Clone(cp.Files) return cp, nil }