using AxCopilot.Models; using AxCopilot.Services; using AxCopilot.Services.Agent; using FluentAssertions; using Xunit; namespace AxCopilot.Tests.Services; public class ChatSessionStateServiceTests { [Fact] public void AppendExecutionEvent_CreatesConversationAndTrimsToLatest400() { var session = new ChatSessionStateService(); for (var i = 0; i < 405; i++) { session.AppendExecutionEvent("Code", new AgentEvent { RunId = $"run-{i}", Type = AgentEventType.ToolResult, ToolName = "file_read", Summary = $"event-{i}", Timestamp = DateTime.Now.AddSeconds(i), Success = true, }); } session.CurrentConversation.Should().NotBeNull(); session.CurrentConversation!.Tab.Should().Be("Code"); session.CurrentConversation.ExecutionEvents.Should().HaveCount(400); session.CurrentConversation.ExecutionEvents[0].RunId.Should().Be("run-5"); session.CurrentConversation.ExecutionEvents[^1].RunId.Should().Be("run-404"); } [Fact] public void AppendExecutionEvent_MergesNearDuplicateEvents() { var session = new ChatSessionStateService(); var now = DateTime.Now; session.AppendExecutionEvent("Code", new AgentEvent { RunId = "run-dup", Type = AgentEventType.ToolResult, ToolName = "file_read", Summary = "same summary", Timestamp = now, Success = true, StepCurrent = 1, StepTotal = 3, InputTokens = 12, OutputTokens = 8, ElapsedMs = 20, }); session.AppendExecutionEvent("Code", new AgentEvent { RunId = "run-dup", Type = AgentEventType.ToolResult, ToolName = "file_read", Summary = "same summary", Timestamp = now.AddSeconds(1), Success = true, StepCurrent = 2, StepTotal = 3, InputTokens = 18, OutputTokens = 11, ElapsedMs = 40, }); session.CurrentConversation.Should().NotBeNull(); session.CurrentConversation!.ExecutionEvents.Should().HaveCount(1); session.CurrentConversation.ExecutionEvents[0].StepCurrent.Should().Be(2); session.CurrentConversation.ExecutionEvents[0].ElapsedMs.Should().Be(40); session.CurrentConversation.ExecutionEvents[0].InputTokens.Should().Be(18); } [Fact] public void AppendAgentRun_KeepsLatestTwelveRuns() { var session = new ChatSessionStateService(); for (var i = 0; i < 15; i++) { session.AppendAgentRun("Cowork", new AgentEvent { RunId = $"run-{i}", Type = AgentEventType.Complete, Summary = $"summary-{i}", Timestamp = DateTime.Now.AddMinutes(-i), Iteration = i, }, "completed", $"done-{i}"); } session.CurrentConversation.Should().NotBeNull(); session.CurrentConversation!.AgentRunHistory.Should().HaveCount(12); session.CurrentConversation.AgentRunHistory[0].RunId.Should().Be("run-14"); session.CurrentConversation.AgentRunHistory[^1].RunId.Should().Be("run-3"); } [Fact] public void AppendAgentRun_UpsertsByRunIdKeepingLatestState() { var session = new ChatSessionStateService(); var baseTime = DateTime.Now; session.AppendAgentRun("Code", new AgentEvent { RunId = "run-dup", Type = AgentEventType.Thinking, Summary = "running", Timestamp = baseTime, Iteration = 1, }, "running", "running"); session.AppendAgentRun("Code", new AgentEvent { RunId = "run-dup", Type = AgentEventType.Complete, Summary = "completed", Timestamp = baseTime.AddSeconds(5), Iteration = 2, }, "completed", "completed"); session.CurrentConversation.Should().NotBeNull(); session.CurrentConversation!.AgentRunHistory.Should().HaveCount(1); session.CurrentConversation.AgentRunHistory[0].RunId.Should().Be("run-dup"); session.CurrentConversation.AgentRunHistory[0].Status.Should().Be("completed"); session.CurrentConversation.AgentRunHistory[0].LastIteration.Should().Be(2); } [Fact] public void AppendExecutionEvent_WithExplicitConversation_DoesNotReplaceVisibleConversation_WhenRememberDisabled() { var session = new ChatSessionStateService(); var visibleConversation = new ChatConversation { Id = $"visible-{Guid.NewGuid():N}", Tab = "Code", Title = "visible", Messages = [new ChatMessage { Role = "user", Content = "keep me selected" }] }; var runningConversation = new ChatConversation { Id = $"running-{Guid.NewGuid():N}", Tab = "Code", Title = "running", Messages = [new ChatMessage { Role = "user", Content = "background run" }] }; session.SetCurrentConversation("Code", visibleConversation); session.RememberConversation("Code", visibleConversation.Id); session.AppendExecutionEvent( "Code", runningConversation, new AgentEvent { RunId = "run-background", Type = AgentEventType.ToolResult, ToolName = "file_read", Summary = "background step", Timestamp = DateTime.Now, Success = true, }, rememberConversation: false); session.CurrentConversation.Should().BeSameAs(visibleConversation); session.GetConversationId("Code").Should().Be(visibleConversation.Id); runningConversation.ExecutionEvents.Should().ContainSingle(); runningConversation.ExecutionEvents[0].RunId.Should().Be("run-background"); } [Fact] public void EnqueueDraft_AndToggleExecutionHistory_UpdateConversationState() { var session = new ChatSessionStateService(); var item = session.EnqueueDraft("Chat", " follow up draft ", "now"); var visible = session.ToggleExecutionHistory("Chat"); item.Should().NotBeNull(); session.CurrentConversation.Should().NotBeNull(); session.CurrentConversation!.DraftQueueItems.Should().ContainSingle(); session.CurrentConversation.DraftQueueItems[0].Text.Should().Be("follow up draft"); session.CurrentConversation.DraftQueueItems[0].Priority.Should().Be("now"); visible.Should().BeFalse(); session.CurrentConversation.ShowExecutionHistory.Should().BeFalse(); } [Fact] public void GetDraftQueueItems_ReturnsSnapshotOfCurrentConversationQueue() { var session = new ChatSessionStateService(); session.EnqueueDraft("Chat", "first draft", "next"); session.EnqueueDraft("Chat", "second draft", "later"); var items = session.GetDraftQueueItems("Chat"); items.Should().HaveCount(2); items[0].Text.Should().Be("first draft"); items[1].Priority.Should().Be("later"); } [Fact] public void ScheduleDraftRetry_RequeuesBeforeMaxAttempts() { var session = new ChatSessionStateService(); var item = session.EnqueueDraft("Chat", "retry me", "next"); session.MarkDraftRunning("Chat", item!.Id); var scheduled = session.ScheduleDraftRetry("Chat", item.Id, "temporary", maxAutoRetries: 3); scheduled.Should().BeTrue(); session.CurrentConversation!.DraftQueueItems[0].State.Should().Be("queued"); session.CurrentConversation!.DraftQueueItems[0].NextRetryAt.Should().NotBeNull(); } [Fact] public void SaveConversationListPreferences_PersistsFilterFlags() { var session = new ChatSessionStateService(); session.SaveConversationListPreferences("Chat", failedOnly: true, runningOnly: true, sortByRecent: true); session.CurrentConversation.Should().NotBeNull(); session.CurrentConversation!.ConversationFailedOnlyFilter.Should().BeTrue(); session.CurrentConversation.ConversationRunningOnlyFilter.Should().BeTrue(); session.CurrentConversation.ConversationSortMode.Should().Be("recent"); } [Fact] public void ClearCurrentConversation_RemovesRememberedConversationIdForTab() { var session = new ChatSessionStateService(); session.RememberConversation("Code", "conv-1"); session.ClearCurrentConversation("Code"); session.GetConversationId("Code").Should().BeNull(); session.CurrentConversation.Should().BeNull(); } [Fact] public void SaveCurrentConversation_RemembersConversationId_WhenOnlyExecutionHistoryExists() { var session = new ChatSessionStateService(); var storage = new ChatStorageService(); var conv = session.EnsureCurrentConversation("Code"); conv.Messages.Clear(); conv.ExecutionEvents = new List { new() { RunId = "run-1", Type = "ToolResult", ToolName = "file_read", Summary = "read", Timestamp = DateTime.Now } }; conv.AgentRunHistory.Clear(); conv.DraftQueueItems.Clear(); session.SaveCurrentConversation(storage, "Code"); session.GetConversationId("Code").Should().Be(conv.Id); } [Fact] public void SaveCurrentConversation_DoesNotPersistEmptyFreshConversation() { var session = new ChatSessionStateService(); var storage = new ChatStorageService(); var conv = session.EnsureCurrentConversation("Cowork"); conv.Messages.Clear(); conv.ExecutionEvents.Clear(); conv.AgentRunHistory.Clear(); conv.DraftQueueItems.Clear(); conv.Preview = ""; conv.WorkFolder = ""; conv.SystemCommand = ""; conv.Permission = null; conv.DataUsage = null; conv.OutputFormat = null; conv.Mood = null; conv.Category = ChatCategory.General; conv.Title = "새 대화"; session.SaveCurrentConversation(storage, "Cowork"); session.GetConversationId("Cowork").Should().BeNull(); storage.Load(conv.Id).Should().BeNull(); } [Fact] public void SaveCurrentConversation_RemembersConversationId_WhenOnlyAgentRunHistoryExists() { var session = new ChatSessionStateService(); var storage = new ChatStorageService(); var conv = session.EnsureCurrentConversation("Code"); conv.Messages.Clear(); conv.ExecutionEvents.Clear(); conv.AgentRunHistory = new List { new() { RunId = "run-2", Status = "completed", Summary = "done", UpdatedAt = DateTime.Now } }; conv.DraftQueueItems.Clear(); session.SaveCurrentConversation(storage, "Code"); session.GetConversationId("Code").Should().Be(conv.Id); } [Fact] public void SaveCurrentConversation_RemembersConversationId_WhenOnlyDraftQueueExists() { var session = new ChatSessionStateService(); var storage = new ChatStorageService(); var conv = session.EnsureCurrentConversation("Code"); conv.Messages.Clear(); conv.ExecutionEvents.Clear(); conv.AgentRunHistory.Clear(); conv.DraftQueueItems = new List { new() { Id = "draft-1", Text = "todo", Priority = "next", State = "queued", CreatedAt = DateTime.Now } }; session.SaveCurrentConversation(storage, "Code"); session.GetConversationId("Code").Should().Be(conv.Id); } [Fact] public void SaveCurrentConversation_UsesConversationTabToAvoidCrossTabContamination() { var session = new ChatSessionStateService(); var storage = new ChatStorageService(); var conv = new ChatConversation { Tab = "Code", Messages = [new ChatMessage { Role = "user", Content = "code message" }] }; session.CurrentConversation = conv; session.SaveCurrentConversation(storage, "Chat"); session.GetConversationId("Code").Should().Be(conv.Id); session.GetConversationId("Chat").Should().BeNull(); } [Fact] public void SetCurrentConversation_UpdatesCurrentConversationAndRememberedId() { var session = new ChatSessionStateService(); var conversation = new ChatConversation { Id = "conv-42", Tab = "", Title = "test title" }; var current = session.SetCurrentConversation("Cowork", conversation); current.Tab.Should().Be("Cowork"); session.CurrentConversation.Should().BeSameAs(conversation); session.GetConversationId("Cowork").Should().Be("conv-42"); } [Fact] public void SetCurrentConversation_ForcesConversationTabToRequestedTab() { var session = new ChatSessionStateService(); var conversation = new ChatConversation { Id = "conv-99", Tab = "Chat", Title = "wrong tab" }; var current = session.SetCurrentConversation("Code", conversation); current.Tab.Should().Be("Code"); session.GetConversationId("Code").Should().Be("conv-99"); } [Fact] public void EnsureCurrentConversation_WhenTabDiffers_CreatesIsolatedConversation() { var session = new ChatSessionStateService(); session.CurrentConversation = new ChatConversation { Tab = "Chat", Messages = [new ChatMessage { Role = "user", Content = "chat message" }] }; var codeConversation = session.EnsureCurrentConversation("Code"); codeConversation.Tab.Should().Be("Code"); codeConversation.Messages.Should().BeEmpty(); } [Fact] public void LoadOrCreateConversation_WhenRememberedIdPointsToDifferentTab_CreatesFreshConversation() { var session = new ChatSessionStateService(); var storage = new ChatStorageService(); var settings = new SettingsService(); var chatConversation = new ChatConversation { Id = "conv-chat-tab", Tab = "Chat", Title = "chat only" }; storage.Save(chatConversation); session.RememberConversation("Code", chatConversation.Id); var loaded = session.LoadOrCreateConversation("Code", storage, settings); loaded.Tab.Should().Be("Code"); loaded.Id.Should().NotBe(chatConversation.Id); session.GetConversationId("Code").Should().BeNull(); } [Fact] [Trait("Suite", "ReplayStability")] public void LoadOrCreateConversation_NormalizesHistoryOrderAndCompactsSize() { var session = new ChatSessionStateService(); var storage = new ChatStorageService(); var settings = new SettingsService(); var baseTime = new DateTime(2026, 4, 3, 1, 0, 0, DateTimeKind.Local); var conversation = new ChatConversation { Id = $"conv-history-normalize-{Guid.NewGuid():N}", Tab = "Code", Title = "history normalize", }; for (var i = 0; i < 420; i++) { conversation.ExecutionEvents.Add(new ChatExecutionEvent { RunId = $"run-{i % 20}", Type = "ToolResult", ToolName = "file_read", Summary = $"event-{i}", Timestamp = baseTime.AddSeconds(420 - i), // 역순 저장 }); } for (var i = 0; i < 20; i++) { conversation.AgentRunHistory.Add(new ChatAgentRunRecord { RunId = $"run-{i}", Status = i % 2 == 0 ? "completed" : "failed", Summary = $"summary-{i}", UpdatedAt = baseTime.AddMinutes(i - 20), // 오래된 순 저장 StartedAt = baseTime.AddMinutes(i - 21), }); } storage.Save(conversation); session.RememberConversation("Code", conversation.Id); var loaded = session.LoadOrCreateConversation("Code", storage, settings); loaded.ExecutionEvents.Should().HaveCount(400); loaded.ExecutionEvents.First().Timestamp.Should().BeBefore(loaded.ExecutionEvents.Last().Timestamp); loaded.AgentRunHistory.Should().HaveCount(12); loaded.AgentRunHistory.First().RunId.Should().Be("run-19"); loaded.AgentRunHistory.Last().RunId.Should().Be("run-8"); } [Fact] [Trait("Suite", "ReplayStability")] public void LoadOrCreateConversation_NormalizesAgentRunDuplicatesByRunId() { var session = new ChatSessionStateService(); var storage = new ChatStorageService(); var settings = new SettingsService(); var baseTime = new DateTime(2026, 4, 3, 1, 0, 0, DateTimeKind.Local); var conversation = new ChatConversation { Id = $"conv-run-dedupe-{Guid.NewGuid():N}", Tab = "Code", Title = "run dedupe", AgentRunHistory = [ new ChatAgentRunRecord { RunId = "run-a", Status = "running", Summary = "old", UpdatedAt = baseTime.AddMinutes(-2), StartedAt = baseTime.AddMinutes(-3), LastIteration = 1 }, new ChatAgentRunRecord { RunId = "run-a", Status = "completed", Summary = "new", UpdatedAt = baseTime.AddMinutes(-1), StartedAt = baseTime.AddMinutes(-3), LastIteration = 2 }, new ChatAgentRunRecord { RunId = "run-b", Status = "failed", Summary = "other", UpdatedAt = baseTime, StartedAt = baseTime.AddMinutes(-1), LastIteration = 1 }, ] }; storage.Save(conversation); session.RememberConversation("Code", conversation.Id); var loaded = session.LoadOrCreateConversation("Code", storage, settings); loaded.AgentRunHistory.Should().HaveCount(2); loaded.AgentRunHistory[0].RunId.Should().Be("run-b"); loaded.AgentRunHistory[1].RunId.Should().Be("run-a"); loaded.AgentRunHistory[1].Status.Should().Be("completed"); loaded.AgentRunHistory[1].Summary.Should().Be("new"); } [Fact] public void Load_NormalizesLegacyAndCaseInsensitiveTabKeys() { var session = new ChatSessionStateService(); var settings = new SettingsService(); settings.Settings.Llm.LastActiveTab = "code"; settings.Settings.Llm.LastConversationIds = new Dictionary(StringComparer.OrdinalIgnoreCase) { ["coworkcode"] = "conv-cowork-legacy", ["code"] = "conv-code-lower", ["chat"] = "conv-chat-lower", }; session.Load(settings); session.ActiveTab.Should().Be("Code"); session.GetConversationId("Cowork").Should().Be("conv-cowork-legacy"); session.GetConversationId("Code").Should().Be("conv-code-lower"); session.GetConversationId("Chat").Should().Be("conv-chat-lower"); } [Fact] public void AppendMessage_FirstUserMessageUpdatesConversationTitle() { var session = new ChatSessionStateService(); session.AppendMessage("Chat", new ChatMessage { Role = "user", Content = "first user request" }, useForTitle: true); session.AppendMessage("Chat", new ChatMessage { Role = "assistant", Content = "answer" }); session.CurrentConversation.Should().NotBeNull(); session.CurrentConversation!.Messages.Should().HaveCount(2); session.CurrentConversation.Title.Should().Be("first user request"); } [Fact] public void AppendMessage_RemembersConversationIdForActiveTab() { var session = new ChatSessionStateService(); session.AppendMessage("Code", new ChatMessage { Role = "user", Content = "run build" }, useForTitle: true); session.CurrentConversation.Should().NotBeNull(); session.CurrentConversation!.Tab.Should().Be("Code"); session.GetConversationId("Code").Should().Be(session.CurrentConversation.Id); } [Fact] public void UpdateConversationMetadata_UpdatesCurrentConversationFields() { var session = new ChatSessionStateService(); session.EnsureCurrentConversation("Code"); session.UpdateConversationMetadata("Code", conv => { conv.Title = "new title"; conv.Category = ChatCategory.Product; conv.SystemCommand = "system prompt"; }); session.CurrentConversation.Should().NotBeNull(); session.CurrentConversation!.Title.Should().Be("new title"); session.CurrentConversation.Category.Should().Be(ChatCategory.Product); session.CurrentConversation.SystemCommand.Should().Be("system prompt"); } [Fact] public void SaveConversationSettings_UpdatesConversationScopedPreferences() { var session = new ChatSessionStateService(); session.SaveConversationSettings("Cowork", "Ask", "active", "markdown", "modern"); session.CurrentConversation.Should().NotBeNull(); session.CurrentConversation!.Permission.Should().Be("Ask"); session.CurrentConversation.DataUsage.Should().Be("active"); session.CurrentConversation.OutputFormat.Should().Be("markdown"); session.CurrentConversation.Mood.Should().Be("modern"); } [Fact] public void RemoveLastAssistantMessage_RemovesOnlyAssistantTail() { var session = new ChatSessionStateService(); session.AppendMessage("Chat", new ChatMessage { Role = "user", Content = "u1" }); session.AppendMessage("Chat", new ChatMessage { Role = "assistant", Content = "a1" }); var removed = session.RemoveLastAssistantMessage("Chat"); removed.Should().BeTrue(); session.CurrentConversation.Should().NotBeNull(); session.CurrentConversation!.Messages.Should().ContainSingle(); session.CurrentConversation.Messages[0].Role.Should().Be("user"); } [Fact] public void UpdateUserMessageAndTrim_RewritesTailFromTargetIndex() { var session = new ChatSessionStateService(); session.AppendMessage("Chat", new ChatMessage { Role = "user", Content = "u1" }); session.AppendMessage("Chat", new ChatMessage { Role = "assistant", Content = "a1" }); session.AppendMessage("Chat", new ChatMessage { Role = "user", Content = "u2" }); session.AppendMessage("Chat", new ChatMessage { Role = "assistant", Content = "a2" }); var updated = session.UpdateUserMessageAndTrim("Chat", 2, "u2-edited"); updated.Should().BeTrue(); session.CurrentConversation.Should().NotBeNull(); session.CurrentConversation!.Messages.Should().HaveCount(3); session.CurrentConversation.Messages[2].Content.Should().Be("u2-edited"); } [Fact] public void UpdateMessageFeedback_UpdatesStoredMessageFeedback() { var session = new ChatSessionStateService(); var message = new ChatMessage { Role = "assistant", Content = "answer" }; session.AppendMessage("Chat", message); var updated = session.UpdateMessageFeedback("Chat", message, "like"); updated.Should().BeTrue(); session.CurrentConversation.Should().NotBeNull(); session.CurrentConversation!.Messages[0].Feedback.Should().Be("like"); } [Fact] public void CreateFreshConversation_AppliesDefaultWorkFolderOutsideChatTab() { var session = new ChatSessionStateService(); var settings = new SettingsService(); settings.Settings.Llm.WorkFolder = @"E:\workspace"; var conversation = session.CreateFreshConversation("Code", settings); conversation.Tab.Should().Be("Code"); conversation.WorkFolder.Should().Be(@"E:\workspace"); session.CurrentConversation.Should().BeSameAs(conversation); } [Fact] public void CreateFreshConversation_PrefersTabSpecificWorkFolderOutsideChatTab() { var session = new ChatSessionStateService(); var settings = new SettingsService(); settings.Settings.Llm.WorkFolder = @"E:\global"; settings.Settings.Llm.CodeWorkFolder = @"E:\code"; var conversation = session.CreateFreshConversation("Code", settings); conversation.WorkFolder.Should().Be(@"E:\code"); } [Fact] public void CreateBranchConversation_ClonesConversationContextUpToBranchPoint() { var session = new ChatSessionStateService(); var source = new ChatConversation { Id = "source-1", Title = "Main", Tab = "Code", Category = ChatCategory.Product, WorkFolder = @"E:\workspace", SystemCommand = "system", ConversationFailedOnlyFilter = true, ConversationRunningOnlyFilter = true, ConversationSortMode = "recent", AgentRunHistory = [ new ChatAgentRunRecord { RunId = "run-1", Status = "completed", Summary = "done", LastIteration = 2 } ], Messages = [ new ChatMessage { Role = "user", Content = "u1", Timestamp = DateTime.Now.AddMinutes(-3), QueryPreviewContent = "preview-1" }, new ChatMessage { Role = "assistant", Content = "a1", Timestamp = DateTime.Now.AddMinutes(-2), MetaKind = "meta", MetaRunId = "run-1" }, new ChatMessage { Role = "user", Content = "u2", Timestamp = DateTime.Now.AddMinutes(-1) } ] }; var branch = session.CreateBranchConversation(source, 1, 2, "follow-up", "context message", "run-ctx"); branch.ParentId.Should().Be("source-1"); branch.Tab.Should().Be("Code"); branch.WorkFolder.Should().Be(@"E:\workspace"); branch.BranchLabel.Should().Contain("2"); branch.Messages.Should().HaveCount(3); branch.Messages[0].QueryPreviewContent.Should().Be("preview-1"); branch.Messages[1].MetaRunId.Should().Be("run-1"); branch.Messages[2].MetaKind.Should().Be("branch_context"); branch.AgentRunHistory.Should().ContainSingle(); } [Fact] public void LoadOrCreateConversation_RestoresMissingToolResultPreviewFromPersistedMessages() { var storage = new ChatStorageService(); var settings = new SettingsService(); var conversationId = $"conv-preview-{Guid.NewGuid():N}"; var conversation = new ChatConversation { Id = conversationId, Tab = "Code", Title = "preview restore", Messages = [ new ChatMessage { Role = "user", Content = """{"type":"tool_result","tool_use_id":"call-restore","tool_name":"file_read","content":"long output"}""", QueryPreviewContent = """{"type":"tool_result","tool_use_id":"call-restore","tool_name":"file_read","content":"preview"}""" }, new ChatMessage { Role = "user", Content = """{"type":"tool_result","tool_use_id":"call-restore","tool_name":"file_read","content":"long output"}""" } ] }; storage.Save(conversation); var session = new ChatSessionStateService(); session.RememberConversation("Code", conversationId); var loaded = session.LoadOrCreateConversation("Code", storage, settings); loaded.Messages.Should().HaveCount(2); loaded.Messages[1].QueryPreviewContent.Should().Be(loaded.Messages[0].QueryPreviewContent); } [Fact] public void DraftStateHelpers_SelectAndTransitionQueuedItems() { var session = new ChatSessionStateService(); var first = session.EnqueueDraft("Chat", "first", "next"); var second = session.EnqueueDraft("Chat", "second", "now"); var next = session.GetNextQueuedDraft("Chat"); next.Should().NotBeNull(); next!.Id.Should().Be(second!.Id); session.MarkDraftRunning("Chat", second.Id).Should().BeTrue(); session.MarkDraftFailed("Chat", second.Id, "error").Should().BeTrue(); session.MarkDraftCompleted("Chat", first!.Id).Should().BeTrue(); session.CurrentConversation.Should().NotBeNull(); session.CurrentConversation!.DraftQueueItems.Should().Contain(x => x.Id == second.Id && x.State == "failed" && x.LastError == "error"); session.CurrentConversation.DraftQueueItems.Should().Contain(x => x.Id == first.Id && x.State == "completed"); } [Fact] public void DraftStateHelpers_ResetAndRemoveDraft() { var session = new ChatSessionStateService(); var item = session.EnqueueDraft("Chat", "queued item", "next"); session.MarkDraftRunning("Chat", item!.Id).Should().BeTrue(); session.MarkDraftFailed("Chat", item.Id, "boom").Should().BeTrue(); session.ResetDraftToQueued("Chat", item.Id).Should().BeTrue(); session.RemoveDraft("Chat", item.Id).Should().BeTrue(); session.CurrentConversation.Should().NotBeNull(); session.CurrentConversation!.DraftQueueItems.Should().NotContain(x => x.Id == item.Id); } [Fact] public void GetDraftQueueSummary_ReturnsConversationQueueSnapshot() { var session = new ChatSessionStateService(); var first = session.EnqueueDraft("Chat", "first", "next"); var second = session.EnqueueDraft("Chat", "second", "now"); session.MarkDraftRunning("Chat", second!.Id); session.MarkDraftCompleted("Chat", second.Id); var summary = session.GetDraftQueueSummary("Chat"); summary.TotalCount.Should().Be(2); summary.QueuedCount.Should().Be(1); summary.CompletedCount.Should().Be(1); summary.NextItem.Should().NotBeNull(); summary.NextItem!.Id.Should().Be(first!.Id); } [Fact] public void RememberConversation_NormalizesCoworkAliasesToSingleBucket() { var session = new ChatSessionStateService(); session.RememberConversation("Cowork Code", "conv-1"); session.RememberConversation("cowork/code", "conv-2"); session.RememberConversation("코워크/코드", "conv-3"); session.GetConversationId("Cowork").Should().Be("conv-3"); session.GetConversationId("Code").Should().BeNull(); session.GetConversationId("Chat").Should().BeNull(); } [Fact] public void LoadOrCreateConversation_RestoresRunHistoryAndExecutionEventsAfterRestart() { var storage = new ChatStorageService(); var settings = new SettingsService(); var sessionA = new ChatSessionStateService(); var conv = new ChatConversation { Id = $"conv-replay-{Guid.NewGuid():N}", Tab = "Code", Title = "restore test", Messages = [new ChatMessage { Role = "user", Content = "build it" }], ExecutionEvents = [ new ChatExecutionEvent { RunId = "run-restore-1", Type = "ToolResult", ToolName = "build_run", Summary = "build ok", Timestamp = DateTime.Now } ], AgentRunHistory = [ new ChatAgentRunRecord { RunId = "run-restore-1", Status = "completed", Summary = "done", UpdatedAt = DateTime.Now } ] }; sessionA.SetCurrentConversation("Code", conv, storage); sessionA.SaveCurrentConversation(storage, "Code"); sessionA.Save(settings); var sessionB = new ChatSessionStateService(); sessionB.Load(settings); var restored = sessionB.LoadOrCreateConversation("Code", storage, settings); restored.Id.Should().Be(conv.Id); restored.Tab.Should().Be("Code"); restored.ExecutionEvents.Should().ContainSingle(); restored.AgentRunHistory.Should().ContainSingle(); restored.AgentRunHistory[0].RunId.Should().Be("run-restore-1"); } }