返回 DeepSeek-Reasonix
extension_ui_gate_test.go
根目录 / internal / boot / extension_ui_gate_test.go
1 package boot
2
3 import (
4 "context"
5 "errors"
6 "sync/atomic"
7 "testing"
8 "time"
9
10 "reasonix/internal/control"
11 )
12
13 // Deterministic coverage for gateExtensionUIRequest: a sidecar's startup
14 // host/ui/request must WAIT for the controller instead of failing outright
15 // (the pre-preflight lifecycle started sidecars only after control.New, so
16 // these requests always found a controller).
17
18 func TestGateExtensionUIRequestWaitsForControllerReady(t *testing.T) {
19 var ctrlRef atomic.Pointer[control.Controller]
20 ready := make(chan struct{})
21 failed := make(chan struct{})
22
23 type result struct {
24 values map[string]any
25 err error
26 }
27 resCh := make(chan result, 1)
28 go func() {
29 values, _, err := gateExtensionUIRequest(context.Background(), ctrlRef.Load, ready, failed,
30 func(c *control.Controller) (map[string]any, bool, error) {
31 return map[string]any{"answer": "yes"}, false, nil
32 })
33 resCh <- result{values, err}
34 }()
35
36 // The request is parked: nothing may come back before readiness.
37 select {
38 case r := <-resCh:
39 t.Fatalf("request resolved before the controller was ready: %+v", r)
40 case <-time.After(50 * time.Millisecond):
41 }
42
43 ctrlRef.Store(&control.Controller{})
44 close(ready)
45 select {
46 case r := <-resCh:
47 if r.err != nil || r.values["answer"] != "yes" {
48 t.Fatalf("request after ready: values=%v err=%v", r.values, r.err)
49 }
50 case <-time.After(5 * time.Second):
51 t.Fatal("request did not resolve after the controller became ready")
52 }
53 }
54
55 func TestGateExtensionUIRequestUnblocksOnBuildFailure(t *testing.T) {
56 var ctrlRef atomic.Pointer[control.Controller]
57 ready := make(chan struct{})
58 failed := make(chan struct{})
59
60 errCh := make(chan error, 1)
61 go func() {
62 _, _, err := gateExtensionUIRequest(context.Background(), ctrlRef.Load, ready, failed,
63 func(c *control.Controller) (map[string]any, bool, error) {
64 return nil, false, errors.New("serve must not run on a failed build")
65 })
66 errCh <- err
67 }()
68
69 close(failed)
70 select {
71 case err := <-errCh:
72 if err == nil {
73 t.Fatal("expected a build-failure error, got nil")
74 }
75 case <-time.After(5 * time.Second):
76 t.Fatal("request stayed parked after the build failed")
77 }
78 }
79
80 func TestGateExtensionUIRequestHonorsRequestCancellation(t *testing.T) {
81 var ctrlRef atomic.Pointer[control.Controller]
82 ready := make(chan struct{})
83 failed := make(chan struct{})
84 ctx, cancel := context.WithCancel(context.Background())
85
86 errCh := make(chan error, 1)
87 go func() {
88 _, _, err := gateExtensionUIRequest(ctx, ctrlRef.Load, ready, failed,
89 func(c *control.Controller) (map[string]any, bool, error) {
90 return nil, false, errors.New("serve must not run on a cancelled request")
91 })
92 errCh <- err
93 }()
94
95 cancel()
96 select {
97 case err := <-errCh:
98 if !errors.Is(err, context.Canceled) {
99 t.Fatalf("expected context.Canceled, got %v", err)
100 }
101 case <-time.After(5 * time.Second):
102 t.Fatal("request stayed parked after its context cancelled")
103 }
104 }
105
105 lines GO