package windsurf import ( "context" "net/http" "net/http/httptest" "strings" "sync" "testing" ) // Simulates a Windsurf LS that rejects SendUserCascadeMessage with // "panel state not found". Verifies that allowRecreate=false bubbles the // error up (so chatCascade can rebuild full-history text) while // allowRecreate=true still triggers the internal recreate path. func TestSendUserCascadeMessage_AllowRecreateGatesSilentRetry(t *testing.T) { tests := []struct { name string allowRecreate bool wantErr bool // When allowRecreate=true the client tries ForceWarmup + StartCascade. // When allowRecreate=false we expect no such attempts. wantStartCascadeCalled bool }{ {"disabled bubbles error", false, true, false}, {"enabled attempts recreate", true, true, true}, // test LS keeps rejecting so overall errors, but StartCascade must be attempted } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { var mu sync.Mutex var paths []string server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { mu.Lock() paths = append(paths, r.URL.Path) mu.Unlock() // Every RPC returns panel-not-found so we can detect whether // StartCascade was attempted after the initial failure. w.Header().Set("Content-Type", "application/grpc") w.Header().Set("grpc-status", "5") w.Header().Set("grpc-message", "panel state not found for session abc") w.WriteHeader(http.StatusOK) })) defer server.Close() client := NewLocalLSClient(42099, "csrf") client.BaseURL = server.URL client.HTTP = server.Client() // Pre-mark as warmed so the first SendUserCascadeMessage doesn't trigger // warmup on its own path — keeps the test focused. client.Warmed = true _, err := client.SendUserCascadeMessage( context.Background(), "token", "existing-cascade-id", "hello", "claude-sonnet-4", "", 0, nil, tt.allowRecreate, ) if (err != nil) != tt.wantErr { t.Fatalf("err = %v, wantErr = %v", err, tt.wantErr) } mu.Lock() defer mu.Unlock() startCascadeCalled := false for _, p := range paths { if strings.HasSuffix(p, "/StartCascade") { startCascadeCalled = true break } } if startCascadeCalled != tt.wantStartCascadeCalled { t.Fatalf("StartCascade called = %v, want %v (paths: %v)", startCascadeCalled, tt.wantStartCascadeCalled, paths) } }) } }