| 1 | //go:build windows |
| 2 | |
| 3 | package taskmonitor |
| 4 | |
| 5 | import ( |
| 6 | "context" |
| 7 | "os" |
| 8 | "path/filepath" |
| 9 | "testing" |
| 10 | "time" |
| 11 | ) |
| 12 | |
| 13 | func TestFileStoreSaveTaskWaitsForTransientSnapshotReader(t *testing.T) { |
| 14 | dir := t.TempDir() |
| 15 | store := NewFileStore(".reasonix/tasks") |
| 16 | ctx := context.Background() |
| 17 | now := time.Now() |
| 18 | snapshot := TaskSnapshot{SchemaVersion: 1, TaskID: "t1", SessionID: "s1", State: TaskStateRunning, Version: 1, CreatedAt: now, UpdatedAt: now} |
| 19 | if err := store.SaveTask(ctx, dir, snapshot); err != nil { |
| 20 | t.Fatal(err) |
| 21 | } |
| 22 | path := filepath.Join(dir, ".reasonix", "tasks", "t1", "snapshot.json") |
| 23 | reader, err := os.Open(path) // ordinary Windows readers omit delete sharing |
| 24 | if err != nil { |
| 25 | t.Fatal(err) |
| 26 | } |
| 27 | defer reader.Close() |
| 28 | snapshot.Version = 2 |
| 29 | done := make(chan error, 1) |
| 30 | go func() { done <- store.SaveTask(ctx, dir, snapshot) }() |
| 31 | // Hold a real OS read handle across the first publication attempts. The |
| 32 | // operation must survive this bounded sharing violation, not fail the task |
| 33 | // control command or fall back to truncating the visible snapshot. |
| 34 | select { |
| 35 | case err := <-done: |
| 36 | t.Fatalf("save completed while the snapshot reader excluded replacement: %v", err) |
| 37 | case <-time.After(100 * time.Millisecond): |
| 38 | } |
| 39 | if err := reader.Close(); err != nil { |
| 40 | t.Fatal(err) |
| 41 | } |
| 42 | select { |
| 43 | case err := <-done: |
| 44 | if err != nil { |
| 45 | t.Fatal(err) |
| 46 | } |
| 47 | case <-time.After(5 * time.Second): |
| 48 | t.Fatal("snapshot publication did not resume after the reader closed") |
| 49 | } |
| 50 | got, err := store.GetTask(ctx, dir, "t1") |
| 51 | if err != nil || got == nil || got.Version != 2 { |
| 52 | t.Fatalf("published snapshot = %+v, err=%v", got, err) |
| 53 | } |
| 54 | } |
| 55 |