| 1 | package workspacestate |
| 2 | |
| 3 | import ( |
| 4 | "encoding/json" |
| 5 | "errors" |
| 6 | "path/filepath" |
| 7 | "testing" |
| 8 | ) |
| 9 | |
| 10 | func TestTopicRemovalJournalSurvivesFailedEffectAndClonesEvidence(t *testing.T) { |
| 11 | s := NewStore(filepath.Join(t.TempDir(), "state.json")) |
| 12 | failed := errors.New("external metadata unavailable") |
| 13 | err := s.TransitionTopicRemoval(t.Context(), "remove", func(_ State, item *TopicRemoval, checkpoint func() error) error { |
| 14 | *item = TopicRemoval{ID: "remove", TopicID: "topic", WorkspaceID: GlobalWorkspaceID, Phase: "prepared", Snapshot: json.RawMessage(`{"title":"keep"}`), Metadata: json.RawMessage(`{"future":[1,2]}`)} |
| 15 | if err := checkpoint(); err != nil { |
| 16 | return err |
| 17 | } |
| 18 | item.Phase = "committed" |
| 19 | return failed |
| 20 | }) |
| 21 | if !errors.Is(err, failed) { |
| 22 | t.Fatal(err) |
| 23 | } |
| 24 | state, err := s.Load(t.Context()) |
| 25 | if err != nil { |
| 26 | t.Fatal(err) |
| 27 | } |
| 28 | if state.TopicRemovals["remove"].Phase != "prepared" { |
| 29 | t.Fatal("failure reported as durable commit") |
| 30 | } |
| 31 | state.TopicRemovals["remove"].Snapshot[0] = '!' |
| 32 | state.TopicRemovals["remove"].Metadata[0] = '!' |
| 33 | again, err := s.Load(t.Context()) |
| 34 | if err != nil { |
| 35 | t.Fatal(err) |
| 36 | } |
| 37 | if string(again.TopicRemovals["remove"].Snapshot) != `{"title":"keep"}` || string(again.TopicRemovals["remove"].Metadata) != `{"future":[1,2]}` { |
| 38 | t.Fatal("snapshot aliases private reader cache") |
| 39 | } |
| 40 | before := again.Generation |
| 41 | if err := s.TransitionTopicRemoval(t.Context(), "remove", func(_ State, item *TopicRemoval, _ func() error) error { item.Phase = "committed"; return nil }); err != nil { |
| 42 | t.Fatal(err) |
| 43 | } |
| 44 | again, _ = s.Load(t.Context()) |
| 45 | if again.Generation <= before { |
| 46 | t.Fatal("completion did not invalidate list cursors") |
| 47 | } |
| 48 | } |
| 49 | |
| 50 | func TestTopicRemovalUnknownFieldsRoundTrip(t *testing.T) { |
| 51 | var item TopicRemoval |
| 52 | if err := json.Unmarshal([]byte(`{"id":"remove","phase":"prepared","future":{"keep":[1,2]}}`), &item); err != nil { |
| 53 | t.Fatal(err) |
| 54 | } |
| 55 | item.Phase = "committed" |
| 56 | body, err := json.Marshal(item) |
| 57 | if err != nil { |
| 58 | t.Fatal(err) |
| 59 | } |
| 60 | var fields map[string]json.RawMessage |
| 61 | if err := json.Unmarshal(body, &fields); err != nil { |
| 62 | t.Fatal(err) |
| 63 | } |
| 64 | if string(fields["future"]) != `{"keep":[1,2]}` { |
| 65 | t.Fatal("future writer evidence lost") |
| 66 | } |
| 67 | state := newState() |
| 68 | body, _ = json.Marshal(state) |
| 69 | if err := json.Unmarshal(body, &fields); err != nil { |
| 70 | t.Fatal(err) |
| 71 | } |
| 72 | if _, exists := fields["topicRemovals"]; exists { |
| 73 | t.Fatal("empty collection changed old-data shape") |
| 74 | } |
| 75 | } |
| 76 |