package notify import ( "b612.me/stario" "context" "io" "math" "net" "sync/atomic" "testing" "time" ) func TestClientWriteToTransportUsesRuntimeConn(t *testing.T) { client := NewClient().(*ClientCommon) fallbackLeft, fallbackRight := net.Pipe() defer fallbackLeft.Close() defer fallbackRight.Close() runtimeLeft, runtimeRight := net.Pipe() defer runtimeLeft.Close() defer runtimeRight.Close() client.conn = fallbackLeft runtimeCtx, runtimeCancel := context.WithCancel(context.Background()) defer runtimeCancel() client.setClientSessionRuntime(&clientSessionRuntime{ conn: runtimeLeft, stopCtx: runtimeCtx, stopFn: runtimeCancel, epoch: 1, }) payload := []byte("runtime-conn") recvCh := make(chan []byte, 1) errCh := make(chan error, 1) go func() { buf := make([]byte, len(payload)) _, err := io.ReadFull(runtimeRight, buf) if err != nil { errCh <- err return } recvCh <- buf }() if err := client.writeToTransport(payload); err != nil { t.Fatalf("writeToTransport failed: %v", err) } select { case err := <-errCh: t.Fatalf("runtime conn read failed: %v", err) case got := <-recvCh: if string(got) != string(payload) { t.Fatalf("runtime payload mismatch: got %q want %q", string(got), string(payload)) } case <-time.After(time.Second): t.Fatal("runtime conn did not receive payload") } _ = fallbackRight.SetReadDeadline(time.Now().Add(20 * time.Millisecond)) buf := make([]byte, 1) if _, err := fallbackRight.Read(buf); err == nil { t.Fatal("fallback conn should not receive payload when runtime conn is active") } } func TestClientMarkSessionStoppedUsesRuntimeStopFn(t *testing.T) { client := NewClient().(*ClientCommon) if !client.beginClientSessionStart() { t.Fatal("beginClientSessionStart should succeed") } client.markSessionStarted() runtimeCtx, runtimeCancel := context.WithCancel(context.Background()) defer runtimeCancel() client.setClientSessionRuntime(&clientSessionRuntime{ stopCtx: runtimeCtx, stopFn: runtimeCancel, epoch: 1, }) fallbackCtx, fallbackCancel := context.WithCancel(context.Background()) defer fallbackCancel() client.stopCtx = fallbackCtx client.stopFn = fallbackCancel client.markSessionStopped("runtime stop", nil) select { case <-runtimeCtx.Done(): case <-time.After(time.Second): t.Fatal("runtime stop context should be canceled by markSessionStopped") } select { case <-fallbackCtx.Done(): t.Fatal("fallback owner stop context should not be canceled when runtime stopFn is active") default: } rt := client.clientSessionRuntimeSnapshot() if rt == nil { t.Fatal("runtime snapshot should remain available after stop") } if rt.conn != nil || rt.queue != nil { t.Fatalf("runtime transport should be cleared after stop: %+v", rt) } if rt.stopCtx == nil { t.Fatalf("runtime stop context should be preserved after stop: %+v", rt) } } func TestClientClearSessionRuntimeTransportPreservesStopState(t *testing.T) { client := NewClient().(*ClientCommon) left, right := net.Pipe() defer left.Close() defer right.Close() stopCtx, stopFn := context.WithCancel(context.Background()) defer stopFn() queue := stario.NewQueueCtx(stopCtx, 4, math.MaxUint32) client.setClientSessionRuntime(&clientSessionRuntime{ conn: left, stopCtx: stopCtx, stopFn: stopFn, queue: queue, epoch: 7, }) client.clearClientSessionRuntimeTransport() rt := client.clientSessionRuntimeSnapshot() if rt == nil { t.Fatal("runtime snapshot should remain after transport clear") } if rt.conn != nil { t.Fatalf("runtime conn should be cleared: %+v", rt) } if rt.queue != queue { t.Fatalf("runtime queue should be preserved across pure transport clear: got %v want %v", rt.queue, queue) } if rt.stopCtx != stopCtx || rt.stopFn == nil || rt.epoch != 7 { t.Fatalf("runtime control state should be preserved: %+v", rt) } if client.clientTransportAttachedSnapshot() { t.Fatal("client transport should be marked detached after runtime clear") } if got := client.clientQueueSnapshot(); got != queue { t.Fatalf("client queue snapshot should be preserved after transport clear: got %v want %v", got, queue) } } func TestClientTransportBindingSnapshotUsesRuntimeBinding(t *testing.T) { client := NewClient().(*ClientCommon) left, right := net.Pipe() defer left.Close() defer right.Close() stopCtx, stopFn := context.WithCancel(context.Background()) defer stopFn() queue := stario.NewQueueCtx(stopCtx, 4, math.MaxUint32) client.setClientSessionRuntime(&clientSessionRuntime{ transport: newTransportBinding(left, queue), conn: left, stopCtx: stopCtx, stopFn: stopFn, queue: queue, epoch: 9, }) binding := client.clientTransportBindingSnapshot() if binding == nil { t.Fatal("runtime transport binding should exist") } if got := binding.connSnapshot(); got != left { t.Fatal("runtime transport binding conn should match runtime conn") } if got := binding.queueSnapshot(); got != queue { t.Fatal("runtime transport binding queue should match runtime queue") } } func TestRetireClientSessionRuntimeCancelsTransportOnly(t *testing.T) { client := NewClient().(*ClientCommon) stopCtx, stopFn := context.WithCancel(context.Background()) defer stopFn() queue := stario.NewQueueCtx(stopCtx, 4, math.MaxUint32) left, right := net.Pipe() defer left.Close() defer right.Close() rt := newClientSessionRuntime(left, stopCtx, stopFn, queue, 3) client.setClientSessionRuntime(rt) client.retireClientSessionRuntime(rt, true) transportStopCtx := client.clientTransportStopContextSnapshot() if transportStopCtx == nil { t.Fatal("transport stop context should exist") } select { case <-transportStopCtx.Done(): case <-time.After(time.Second): t.Fatal("transport stop context should be canceled by retireClientSessionRuntime") } select { case <-client.clientStopContextSnapshot().Done(): t.Fatal("logical stop context should remain active when only retiring transport") default: } } func TestClientClearSessionRuntimeTransportPreservesQueueForEncoding(t *testing.T) { client := NewClient().(*ClientCommon) UseLegacySecurityClient(client) left, right := net.Pipe() defer left.Close() defer right.Close() stopCtx, stopFn := context.WithCancel(context.Background()) defer stopFn() queue := stario.NewQueueCtx(stopCtx, 4, math.MaxUint32) client.setClientSessionRuntime(&clientSessionRuntime{ conn: left, stopCtx: stopCtx, stopFn: stopFn, queue: queue, epoch: 8, }) client.markSessionStarted() defer client.markSessionStopped("test done", nil) client.clearClientSessionRuntimeTransport() data, err := client.encodeEnvelope(newSignalAckEnvelope(1003)) if err != nil { t.Fatalf("encodeEnvelope failed after pure transport clear: %v", err) } if len(data) == 0 { t.Fatal("encodeEnvelope should still return framed payload after pure transport clear") } } func TestAttachClientSessionTransportRebindsRuntimeAndDispatchesOnNewConn(t *testing.T) { client := NewClient().(*ClientCommon) UseLegacySecurityClient(client) stopCtx, stopFn := context.WithCancel(context.Background()) defer stopFn() queue := stario.NewQueueCtx(stopCtx, 4, math.MaxUint32) oldLeft, oldRight := net.Pipe() defer oldRight.Close() client.setClientSessionRuntime(&clientSessionRuntime{ conn: oldLeft, stopCtx: stopCtx, stopFn: stopFn, queue: queue, epoch: 11, suppressGoodByeOnStop: &atomic.Bool{}, }) client.markSessionStarted() defer client.markSessionStopped("test done", nil) recvCh := make(chan Message, 1) client.SetLink("reattach", func(message *Message) { recvCh <- *message }) newLeft, newRight := net.Pipe() defer newRight.Close() if err := client.attachClientSessionTransport(newLeft); err != nil { t.Fatalf("attachClientSessionTransport failed: %v", err) } rt := client.clientSessionRuntimeSnapshot() if rt == nil { t.Fatal("runtime snapshot should exist after attach") } if rt.conn != newLeft || !rt.transportAttached || rt.queue != queue || rt.epoch != 11 { t.Fatalf("attached runtime mismatch: %+v", rt) } env, err := wrapTransferMsgEnvelope(TransferMsg{ ID: 42, Key: "reattach", Value: []byte("ok"), Type: MSG_ASYNC, }, client.sequenceEn) if err != nil { t.Fatalf("wrapTransferMsgEnvelope failed: %v", err) } wire, err := client.encodeEnvelope(env) if err != nil { t.Fatalf("encodeEnvelope failed: %v", err) } if _, err := newRight.Write(wire); err != nil { t.Fatalf("new transport write failed: %v", err) } select { case message := <-recvCh: if got, want := message.Key, "reattach"; got != want { t.Fatalf("message key mismatch: got %q want %q", got, want) } if got, want := string(message.Value), "ok"; got != want { t.Fatalf("message value mismatch: got %q want %q", got, want) } case <-time.After(time.Second): t.Fatal("reattached transport did not dispatch message") } } func TestSetClientSessionRuntimeStopsOldBindingWorkersOnReattach(t *testing.T) { client := NewClient().(*ClientCommon) stopCtx, stopFn := context.WithCancel(context.Background()) defer stopFn() queue := stario.NewQueueCtx(stopCtx, 4, math.MaxUint32) oldLeft, oldRight := net.Pipe() defer oldLeft.Close() defer oldRight.Close() oldBinding := newTransportBinding(oldLeft, queue) oldSender := oldBinding.bulkBatchSenderSnapshot() client.setClientSessionRuntime(&clientSessionRuntime{ transport: oldBinding, conn: oldLeft, stopCtx: stopCtx, stopFn: stopFn, queue: queue, epoch: 1, }) newLeft, newRight := net.Pipe() defer newLeft.Close() defer newRight.Close() newBinding := newTransportBinding(newLeft, queue) client.setClientSessionRuntime(&clientSessionRuntime{ transport: newBinding, conn: newLeft, stopCtx: stopCtx, stopFn: stopFn, queue: queue, epoch: 2, }) err := oldSender.submit(context.Background(), []byte("payload")) if err != errTransportDetached { t.Fatalf("old sender submit after reattach = %v, want %v", err, errTransportDetached) } }