返回 DeepSeek-Reasonix
admission_generation_test.go
根目录 / internal / control / admission_generation_test.go
1 package control
2
3 import (
4 "context"
5 "errors"
6 "sync/atomic"
7 "testing"
8 "time"
9
10 "reasonix/internal/event"
11 "reasonix/internal/extension"
12 )
13
14 func TestAdmitGuardedTurnRejectsDrainingGeneration(t *testing.T) {
15 // Publish generation 2 so gen 1 is stale for admission.
16 owner := extension.NewRuntimeOwner()
17 owner.Gate.Publish(2)
18
19 var notices atomic.Int32
20 var c *Controller
21 c = newOwnedTestController(t, Options{
22 Sink: event.FuncSink(func(ev event.Event) {
23 if ev.Kind == event.Notice {
24 _ = c.RuntimeGeneration() // must not re-enter while Controller.mu is held
25 notices.Add(1)
26 }
27 }),
28 RuntimeGeneration: 1,
29 RuntimeOwner: owner,
30 })
31 // Ensure we don't leak a controller without Close.
32 t.Cleanup(func() { c.Close() })
33
34 result := make(chan admissionResult, 1)
35 go func() {
36 result <- c.runGuarded(func(context.Context) error {
37 t.Error("body must not run on a draining generation")
38 return nil
39 })
40 }()
41 select {
42 case got := <-result:
43 if got != turnDroppedDraining {
44 t.Fatalf("admission = %v, want turnDroppedDraining", got)
45 }
46 case <-time.After(time.Second):
47 t.Fatal("drain notice deadlocked while re-entering the controller")
48 }
49 if notices.Load() == 0 {
50 t.Fatal("expected drain notice")
51 }
52 if err := c.RunTurn(context.Background(), "blocked"); !errors.Is(err, ErrRuntimeDraining) {
53 t.Fatalf("RunTurn error = %v, want ErrRuntimeDraining", err)
54 }
55 if extension.DefaultLifecycleMetrics.AdmissionRejected.Load() == 0 {
56 t.Fatal("expected AdmissionRejected metric")
57 }
58 }
59
60 func TestAdmitGuardedTurnAllowsPublishedGeneration(t *testing.T) {
61 owner := extension.NewRuntimeOwner()
62 owner.Gate.Publish(9)
63 c := newOwnedTestController(t, Options{RuntimeGeneration: 9, RuntimeOwner: owner, Sink: event.Discard})
64 t.Cleanup(func() { c.Close() })
65 done := make(chan struct{})
66 got := c.runGuarded(func(context.Context) error {
67 close(done)
68 return nil
69 })
70 if got != turnStarted {
71 t.Fatalf("admission = %v, want turnStarted", got)
72 }
73 select {
74 case <-done:
75 case <-time.After(2 * time.Second):
76 t.Fatal("turn body did not run")
77 }
78 }
79
80 type runtimeOwnerRunner struct {
81 owner *extension.RuntimeOwner
82 }
83
84 func (r *runtimeOwnerRunner) Run(ctx context.Context, _ string) error {
85 r.owner = extension.RuntimeOwnerFromContext(ctx)
86 return nil
87 }
88
89 func TestRunTurnBindsRuntimeOwnerToRunnerContext(t *testing.T) {
90 owner := extension.NewRuntimeOwner()
91 owner.Gate.Publish(4)
92 runner := &runtimeOwnerRunner{}
93 c := newOwnedTestController(t, Options{Runner: runner, RuntimeGeneration: 4, RuntimeOwner: owner, Sink: event.Discard})
94 t.Cleanup(c.Close)
95
96 if err := c.RunTurn(context.Background(), "hello"); err != nil {
97 t.Fatal(err)
98 }
99 if runner.owner != owner {
100 t.Fatal("turn context did not carry the controller runtime owner")
101 }
102 }
103
103 lines GO