-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathinvoke_machine_test.go
More file actions
326 lines (275 loc) · 7.79 KB
/
invoke_machine_test.go
File metadata and controls
326 lines (275 loc) · 7.79 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
package statekit_test
import (
"testing"
"time"
"go.klarlabs.de/statekit"
"go.klarlabs.de/statekit/internal/ir"
)
// Helper to create a child machine that processes work
func createWorkerChildMachine() *ir.MachineConfig[struct{}] {
machine, err := statekit.NewMachine[struct{}]("worker").
WithInitial("working").
State("working").
On("FINISH").Target("completed").End().
Done().
State("completed").Final().
Done().
Build()
if err != nil {
panic(err)
}
return machine
}
func TestInvokeMachine_Basic(t *testing.T) {
t.Parallel()
type ctx struct{}
// Create child machine
childMachine := createWorkerChildMachine()
// Create parent machine that invokes child
parent, err := statekit.NewMachine[ctx]("parent").
WithInitial("idle").
WithChildMachine("worker", func(parentCtx ctx, parentSend func(statekit.Event) error) ir.ChildInterpreter {
interp := statekit.NewInterpreter(childMachine)
return interp
}).
State("idle").
On("START").Target("processing").End().
Done().
State("processing").
InvokeMachine("worker").
OnDone("completed").
End().
Done().
State("completed").Final().
Done().
Build()
if err != nil {
t.Fatal(err)
}
interp := statekit.NewInterpreter(parent)
interp.Start()
if interp.State().Value != "idle" {
t.Errorf("expected idle, got %s", interp.State().Value)
}
// Transition to processing - this starts the child machine
interp.Send(statekit.Event{Type: "START"})
if interp.State().Value != "processing" {
t.Errorf("expected processing, got %s", interp.State().Value)
}
// The child machine should have started
// Give it time to start (async)
time.Sleep(50 * time.Millisecond)
// The child is not yet done, so parent should still be in processing
if interp.State().Value != "processing" {
t.Errorf("expected processing, got %s", interp.State().Value)
}
interp.Stop()
}
func TestInvokeMachine_OnDone(t *testing.T) {
t.Parallel()
type ctx struct{}
// Create a child machine that starts in "working" and can transition to "done" (final)
childMachine, err := statekit.NewMachine[ctx]("child").
WithInitial("working").
State("working").
On("COMPLETE").Target("done").End().
Done().
State("done").Final().
Done().
Build()
if err != nil {
t.Fatal(err)
}
var childInterp *statekit.Interpreter[ctx]
// Create parent machine
parent, err := statekit.NewMachine[ctx]("parent").
WithInitial("processing").
WithChildMachine("worker", func(parentCtx ctx, parentSend func(statekit.Event) error) ir.ChildInterpreter {
childInterp = statekit.NewInterpreter(childMachine)
return childInterp
}).
State("processing").
InvokeMachine("worker").
OnDone("completed").
End().
Done().
State("completed").Final().
Done().
Build()
if err != nil {
t.Fatal(err)
}
interp := statekit.NewInterpreter(parent)
interp.Start()
// Should start in processing and child should be running
time.Sleep(50 * time.Millisecond)
if interp.State().Value != "processing" {
t.Errorf("expected processing, got %s", interp.State().Value)
}
// Send COMPLETE to child to make it reach final state
if childInterp != nil {
childInterp.Send(statekit.Event{Type: "COMPLETE"})
}
// Wait for OnDone transition
time.Sleep(100 * time.Millisecond)
// Parent should have transitioned to completed
if interp.State().Value != "completed" {
t.Errorf("expected completed, got %s", interp.State().Value)
}
if !interp.Done() {
t.Error("expected parent to be in final state")
}
}
func TestInvokeMachine_StopOnParentExit(t *testing.T) {
t.Parallel()
type ctx struct{}
childMachine, err := statekit.NewMachine[ctx]("child").
WithInitial("working").
State("working").
On("FINISH").Target("done").End().
Done().
State("done").Final().
Done().
Build()
if err != nil {
t.Fatal(err)
}
// Create parent machine
parent, err := statekit.NewMachine[ctx]("parent").
WithInitial("processing").
WithChildMachine("worker", func(parentCtx ctx, parentSend func(statekit.Event) error) ir.ChildInterpreter {
return statekit.NewInterpreter(childMachine)
}).
State("processing").
InvokeMachine("worker").End().
On("CANCEL").Target("cancelled").End().
Done().
State("cancelled").Final().
Done().
Build()
if err != nil {
t.Fatal(err)
}
interp := statekit.NewInterpreter(parent)
interp.Start()
// Give child time to start
time.Sleep(50 * time.Millisecond)
// Exit the processing state - this should stop the child
interp.Send(statekit.Event{Type: "CANCEL"})
// Give time for cleanup
time.Sleep(50 * time.Millisecond)
// Child should have been stopped (exit action called)
// Note: We can't directly verify the child was stopped, but we verify
// the parent transitioned correctly
if interp.State().Value != "cancelled" {
t.Errorf("expected cancelled, got %s", interp.State().Value)
}
}
func TestInvokeMachine_WithID(t *testing.T) {
t.Parallel()
type ctx struct{}
childMachine, err := statekit.NewMachine[ctx]("child").
WithInitial("active").
State("active").
On("DONE").Target("finished").End().
Done().
State("finished").Final().
Done().
Build()
if err != nil {
t.Fatal(err)
}
// Create parent machine with explicit invoke ID
parent, err := statekit.NewMachine[ctx]("parent").
WithInitial("processing").
WithChildMachine("childRef", func(parentCtx ctx, parentSend func(statekit.Event) error) ir.ChildInterpreter {
return statekit.NewInterpreter(childMachine)
}).
State("processing").
InvokeMachine("childRef").
ID("myWorker").
OnDone("completed").
End().
Done().
State("completed").Final().
Done().
Build()
if err != nil {
t.Fatal(err)
}
interp := statekit.NewInterpreter(parent)
interp.Start()
if interp.State().Value != "processing" {
t.Errorf("expected processing, got %s", interp.State().Value)
}
interp.Stop()
}
func TestInvokeMachine_MultipleInvocations(t *testing.T) {
t.Parallel()
type ctx struct{}
childMachine, err := statekit.NewMachine[ctx]("child").
WithInitial("active").
State("active").
On("DONE").Target("finished").End().
Done().
State("finished").Final().
Done().
Build()
if err != nil {
t.Fatal(err)
}
// Create parent with multiple child invocations
parent, err := statekit.NewMachine[ctx]("parent").
WithInitial("processing").
WithChildMachine("worker1", func(parentCtx ctx, parentSend func(statekit.Event) error) ir.ChildInterpreter {
return statekit.NewInterpreter(childMachine)
}).
WithChildMachine("worker2", func(parentCtx ctx, parentSend func(statekit.Event) error) ir.ChildInterpreter {
return statekit.NewInterpreter(childMachine)
}).
State("processing").
InvokeMachine("worker1").ID("w1").End().
InvokeMachine("worker2").ID("w2").End().
On("STOP").Target("stopped").End().
Done().
State("stopped").Final().
Done().
Build()
if err != nil {
t.Fatal(err)
}
interp := statekit.NewInterpreter(parent)
interp.Start()
// Give children time to start
time.Sleep(50 * time.Millisecond)
if interp.State().Value != "processing" {
t.Errorf("expected processing, got %s", interp.State().Value)
}
// Stop should clean up both children
interp.Send(statekit.Event{Type: "STOP"})
time.Sleep(50 * time.Millisecond)
if interp.State().Value != "stopped" {
t.Errorf("expected stopped, got %s", interp.State().Value)
}
}
func TestInvokeMachine_BuilderValidation(t *testing.T) {
t.Parallel()
type ctx struct{}
// Build should succeed even if child machine ref doesn't exist
// (validation happens at runtime when invoking)
machine, err := statekit.NewMachine[ctx]("parent").
WithInitial("processing").
State("processing").
InvokeMachine("nonexistent").OnDone("done").End().
Done().
State("done").Final().
Done().
Build()
if err != nil {
t.Fatalf("build should succeed: %v", err)
}
// Machine should be created (validation is at runtime)
if machine == nil {
t.Error("expected machine to be non-nil")
}
}