返回 DeepSeek-Reasonix
runtime_state_published_test.go
根目录 / internal / control / runtime_state_published_test.go
1 package control
2
3 import (
4 "context"
5 "reflect"
6 "sync"
7 "testing"
8 "time"
9
10 "reasonix/internal/event"
11 )
12
13 func TestPublishedRuntimeStateDoesNotWaitForSamplingLocks(t *testing.T) {
14 isolateControlConfigHome(t)
15 c := newOwnedTestController(t, Options{SessionDir: t.TempDir()})
16 defer c.Close()
17 want := c.RuntimeStateSnapshot()
18 if want.SchemaVersion != 1 || want.Revision == 0 {
19 t.Fatalf("initial state was not committed: %+v", want)
20 }
21 // A producer waiting on a controller/session owner holds the sampling
22 // mutex. Neither that mutex nor the controller lock may gate UI reads.
23 c.runtimeState.mu.Lock()
24 defer c.runtimeState.mu.Unlock()
25 c.mu.Lock()
26 defer c.mu.Unlock()
27 done := make(chan event.RuntimeStateSnapshot, 1)
28 go func() { done <- c.PublishedRuntimeStateSnapshot() }()
29 select {
30 case got := <-done:
31 if !reflect.DeepEqual(got, want) {
32 t.Fatalf("committed state changed while producer is blocked: got=%+v want=%+v", got, want)
33 }
34 case <-time.After(5 * time.Second):
35 t.Fatal("published read waited on an owner or sampling lock")
36 }
37 }
38
39 func TestPublishedRuntimeStateTracksCancellationAndCompletion(t *testing.T) {
40 isolateControlConfigHome(t)
41 sink := &runtimeStateTestSink{Sink: event.FuncSink(func(event.Event) {}), states: make(chan event.RuntimeStateSnapshot, 64)}
42 c := newOwnedTestController(t, Options{SessionDir: t.TempDir(), Sink: sink})
43 defer c.Close()
44 release := make(chan struct{})
45 releaseBody := sync.OnceFunc(func() { close(release) })
46 defer releaseBody()
47 started := c.PublishedRuntimeStateSnapshot()
48 c.runGuarded(func(ctx context.Context) error { <-release; return ctx.Err() })
49 for _, phase := range []string{"executing", "cancelling", "idle"} {
50 switch phase {
51 case "cancelling":
52 c.CancelSession()
53 case "idle":
54 releaseBody()
55 }
56 published := runtimeStateAwait(t, sink.states, func(s event.RuntimeStateSnapshot) bool { return s.Phase == phase })
57 got := c.PublishedRuntimeStateSnapshot()
58 if got.Phase != phase || got.Revision < published.Revision || got.RuntimeEpoch != started.RuntimeEpoch {
59 t.Fatalf("committed state did not follow %s: %+v", phase, got)
60 }
61 if phase == "idle" && got.ActiveWork() {
62 t.Fatalf("completed turn still appears active: %+v", got)
63 }
64 }
65 }
66
67 func TestPublishedRuntimeStateOwnsMutableObservations(t *testing.T) {
68 c := &Controller{}
69 state := event.RuntimeStateSnapshot{
70 Revision: 1, Todos: []event.Todo{{Content: "original"}},
71 Interactions: []event.PendingInteraction{{RequestID: "original"}},
72 Recovery: &event.RecoveryStatus{Reason: "original"},
73 Maintenance: &event.MaintenanceState{OperationID: "original"},
74 }
75 c.runtimeState.commitSnapshot(state)
76 // Neither producer-owned input nor a UI consumer may rewrite a version
77 // another reader has already observed.
78 state.Todos[0].Content = "producer mutation"
79 first := c.PublishedRuntimeStateSnapshot()
80 if first.Todos[0].Content != "original" {
81 t.Fatal("producer retained ownership of committed todos")
82 }
83 first.Todos[0].Content = "consumer mutation"
84 first.Interactions[0].RequestID = "consumer mutation"
85 first.Recovery.Reason = "consumer mutation"
86 first.Maintenance.OperationID = "consumer mutation"
87 second := c.PublishedRuntimeStateSnapshot()
88 if second.Todos[0].Content != "original" || second.Interactions[0].RequestID != "original" ||
89 second.Recovery.Reason != "original" || second.Maintenance.OperationID != "original" {
90 t.Fatalf("a reader mutated the committed version: %+v", second)
91 }
92 }
93
93 lines GO