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
44 changes: 44 additions & 0 deletions edge-server/internal/api/checkpoint_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
})
}
}
130 changes: 130 additions & 0 deletions edge-server/internal/store/checkpoint_ownership_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
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)
}
})
}
})
}
}

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)
}
})
}
}
4 changes: 4 additions & 0 deletions edge-server/internal/store/store_domain.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package store
// Residual pure-helper peel #1144: run/artifact/profile/settings methods. Same package; zero behavior change.

import (
"slices"
"time"
)

Expand Down Expand Up @@ -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
}

Expand Down
Loading