1package coordinator
2
3import (
4 "strings"
5 "sync"
6 "testing"
7
8 "github.com/bnema/dumber/internal/domain/entity"
9)
10
11func testLeafNode(id string) *entity.PaneNode {
12 return &entity.PaneNode{
13 ID: id,
14 Pane: entity.NewPane(entity.PaneID(id)),
15 }
16}
17
18func testSplitNode(id string, left, right *entity.PaneNode) *entity.PaneNode {
19 node := &entity.PaneNode{
20 ID: id,
21 SplitDir: entity.SplitHorizontal,
22 Children: []*entity.PaneNode{left, right},
23 }
24 if left != nil {
25 left.Parent = node
26 }
27 if right != nil {
28 right.Parent = node
29 }
30 return node
31}
32
33func TestDeriveIncrementalCloseTreeContext_ValidSplit(t *testing.T) {
34 closing := testLeafNode("closing")
35 sibling := testLeafNode("sibling")
36 parent := testSplitNode("parent", closing, sibling)
37 other := testLeafNode("other")
38 grand := testSplitNode("grand", parent, other)
39
40 ctx, err := deriveIncrementalCloseTreeContext(closing)
41 if err != nil {
42 t.Fatalf("unexpected error: %v", err)
43 }
44
45 if ctx.parentNode != parent {
46 t.Fatalf("parent mismatch")
47 }
48 if ctx.siblingNode != sibling {
49 t.Fatalf("sibling mismatch")
50 }
51 if ctx.grandparentNode != grand {
52 t.Fatalf("grandparent mismatch")
53 }
54 if ctx.siblingIsStartChild {
55 t.Fatalf("sibling should be end child")
56 }
57 if !ctx.parentIsStartInGrand {
58 t.Fatalf("parent should be start child in grandparent")
59 }
60}
61
62func TestDeriveIncrementalCloseTreeContext_InvalidPreconditions(t *testing.T) {
63 tests := []struct {
64 name string
65 closingPane *entity.PaneNode
66 wantText string
67 }{
68 {
69 name: "nil closing pane",
70 closingPane: nil,
71 wantText: "closing pane missing",
72 },
73 {
74 name: "closing pane has no parent",
75 closingPane: testLeafNode("closing"),
76 wantText: "closing pane has no parent",
77 },
78 {
79 name: "parent is not split",
80 closingPane: func() *entity.PaneNode {
81 closing := testLeafNode("closing")
82 parent := &entity.PaneNode{ID: "parent", Children: []*entity.PaneNode{closing}}
83 closing.Parent = parent
84 return closing
85 }(),
86 wantText: "parent node is not split",
87 },
88 }
89
90 for _, tt := range tests {
91 t.Run(tt.name, func(t *testing.T) {
92 _, err := deriveIncrementalCloseTreeContext(tt.closingPane)
93 if err == nil {
94 t.Fatalf("expected error")
95 }
96 if !strings.Contains(err.Error(), tt.wantText) {
97 t.Fatalf("expected %q error, got: %v", tt.wantText, err)
98 }
99 })
100 }
101}
102
103func TestDeriveIncrementalCloseTreeContext_MissingSibling(t *testing.T) {
104 closing := testLeafNode("closing")
105 parent := testSplitNode("parent", closing, nil)
106
107 _, err := deriveIncrementalCloseTreeContext(closing)
108 if err == nil {
109 t.Fatalf("expected invariant error")
110 }
111 if !strings.Contains(err.Error(), "sibling") && !strings.Contains(err.Error(), "nil child") {
112 t.Fatalf("expected sibling/nil-child error, got: %v", err)
113 }
114
115 if got := paneNodeID(parent); got != "parent" {
116 t.Fatalf("unexpected parent id helper output: %s", got)
117 }
118}
119
120func TestDeriveIncrementalCloseTreeContext_ParentDoesNotContainClosingPane(t *testing.T) {
121 closing := testLeafNode("closing")
122 left := testLeafNode("left")
123 right := testLeafNode("right")
124 parent := testSplitNode("parent", left, right)
125 closing.Parent = parent
126
127 _, err := deriveIncrementalCloseTreeContext(closing)
128 if err == nil {
129 t.Fatalf("expected invariant error")
130 }
131 if !strings.Contains(err.Error(), "not found under parent") {
132 t.Fatalf("unexpected error: %v", err)
133 }
134}
135
136func TestDeriveIncrementalCloseTreeContext_ParentMissingFromGrandparent(t *testing.T) {
137 closing := testLeafNode("closing")
138 sibling := testLeafNode("sibling")
139 parent := testSplitNode("parent", closing, sibling)
140 grand := testSplitNode("grand", testLeafNode("other-left"), testLeafNode("other-right"))
141 parent.Parent = grand
142
143 _, err := deriveIncrementalCloseTreeContext(closing)
144 if err == nil {
145 t.Fatalf("expected invariant error")
146 }
147 if !strings.Contains(err.Error(), "parent not found under grandparent") {
148 t.Fatalf("unexpected error: %v", err)
149 }
150}
151
152func TestCaptureIncrementalCloseContext_MissingSiblingSetsPrecheckReason(t *testing.T) {
153 closing := testLeafNode("closing")
154 testSplitNode("parent", closing, nil)
155
156 coord := &WorkspaceCoordinator{}
157 ctx := coord.captureIncrementalCloseContext(nil, closing)
158 if ctx.precheckReason == "" {
159 t.Fatalf("expected precheck reason")
160 }
161}
162
163func TestDeriveIncrementalCloseTreeContext_ConcurrentPaneAndTabCloseSnapshots(t *testing.T) {
164 paneCloseNode := testLeafNode("pane-close")
165 paneCloseSibling := testLeafNode("pane-close-sibling")
166 testSplitNode("pane-parent", paneCloseNode, paneCloseSibling)
167
168 tabCloseNode := testLeafNode("tab-close")
169 tabCloseSibling := testLeafNode("tab-close-sibling")
170 testSplitNode("tab-parent", tabCloseSibling, tabCloseNode)
171
172 run := func(node *entity.PaneNode, wg *sync.WaitGroup, errCh chan<- error) {
173 defer wg.Done()
174 for range 200 {
175 if _, err := deriveIncrementalCloseTreeContext(node); err != nil {
176 errCh <- err
177 return
178 }
179 }
180 }
181
182 errCh := make(chan error, 2)
183 var wg sync.WaitGroup
184 wg.Add(2)
185 go run(paneCloseNode, &wg, errCh)
186 go run(tabCloseNode, &wg, errCh)
187 wg.Wait()
188 close(errCh)
189
190 for err := range errCh {
191 if err != nil {
192 t.Fatalf("unexpected concurrent derive error: %v", err)
193 }
194 }
195}