From 8eafa71ed329600b73ff43f890f06f00f7ee6645 Mon Sep 17 00:00:00 2001 From: Mika Kuns Date: Tue, 28 Apr 2026 08:30:26 +0200 Subject: [PATCH] fix: restore green test suite across all projects * TaskRepository.UpdateAsync defensively detaches any locally tracked entity with the same Id before attaching the patched copy, preventing EF identity conflicts when callers load via AsNoTracking and write back through the same DbContext (surfaced by ExternalMcpService UpdateTask integration tests). * TasksIslandViewModel auto-collapse now only fires for Finalized planning parents that are not yet Done. Active-phase parents stay expanded while the user is editing the plan, and Done parents stay expanded so all completed children land in CompletedItems alongside the parent. * Update three Ui.Tests fakes (ConflictResolution, PlanningDiff, DetailsIslandPlanning) to implement the two new IWorkerClient members (OpenInteractiveTerminalAsync, QueuePlanningSubtasksAsync). * Rewrite StreamLineFormatterTests to exercise the current assistant/user/result/system message format instead of the legacy stream_event parsing that was removed in the formatter rewrite. * Align AppSettingsRepository seed-default assertion with the permission-mode default that flipped from bypassPermissions to auto. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../Repositories/TaskRepository.cs | 3 ++ .../Islands/TasksIslandViewModel.cs | 4 +- .../Helpers/StreamLineFormatterTests.cs | 44 +++++++++---------- .../ConflictResolutionViewModelTests.cs | 2 + .../ViewModels/DetailsIslandPlanningTests.cs | 2 + .../ViewModels/PlanningDiffViewModelTests.cs | 2 + .../AppSettingsRepositoryTests.cs | 2 +- 7 files changed, 35 insertions(+), 24 deletions(-) diff --git a/src/ClaudeDo.Data/Repositories/TaskRepository.cs b/src/ClaudeDo.Data/Repositories/TaskRepository.cs index bbc4748..a6d9d68 100644 --- a/src/ClaudeDo.Data/Repositories/TaskRepository.cs +++ b/src/ClaudeDo.Data/Repositories/TaskRepository.cs @@ -27,6 +27,9 @@ public sealed class TaskRepository public async Task UpdateAsync(TaskEntity entity, CancellationToken ct = default) { + var tracked = _context.Tasks.Local.FirstOrDefault(t => t.Id == entity.Id); + if (tracked is not null && !ReferenceEquals(tracked, entity)) + _context.Entry(tracked).State = Microsoft.EntityFrameworkCore.EntityState.Detached; _context.Tasks.Update(entity); await _context.SaveChangesAsync(ct); } diff --git a/src/ClaudeDo.Ui/ViewModels/Islands/TasksIslandViewModel.cs b/src/ClaudeDo.Ui/ViewModels/Islands/TasksIslandViewModel.cs index 96e924f..15830b7 100644 --- a/src/ClaudeDo.Ui/ViewModels/Islands/TasksIslandViewModel.cs +++ b/src/ClaudeDo.Ui/ViewModels/Islands/TasksIslandViewModel.cs @@ -244,7 +244,9 @@ public sealed partial class TasksIslandViewModel : ViewModelBase .Where(r => r.IsChild && !string.IsNullOrEmpty(r.ParentTaskId)) .GroupBy(r => r.ParentTaskId!) .ToDictionary(g => g.Key, g => g.ToList()); - foreach (var parent in Items.Where(r => r.IsPlanningParent && !r.IsChild)) + foreach (var parent in Items.Where(r => r.IsPlanningParent && !r.IsChild + && r.PlanningPhase == PlanningPhase.Finalized + && !r.Done)) { if (_expandedState.ContainsKey(parent.Id)) continue; if (childrenByParent.TryGetValue(parent.Id, out var kids) diff --git a/tests/ClaudeDo.Ui.Tests/Helpers/StreamLineFormatterTests.cs b/tests/ClaudeDo.Ui.Tests/Helpers/StreamLineFormatterTests.cs index fdcb601..9718d9a 100644 --- a/tests/ClaudeDo.Ui.Tests/Helpers/StreamLineFormatterTests.cs +++ b/tests/ClaudeDo.Ui.Tests/Helpers/StreamLineFormatterTests.cs @@ -6,38 +6,38 @@ public class StreamLineFormatterTests { private readonly StreamLineFormatter _formatter = new(); - // --- Text deltas --- + // --- Assistant text blocks --- [Fact] - public void FormatLine_TextDelta_ReturnsTextContent() + public void FormatLine_AssistantTextBlock_ReturnsTextContent() { - var line = """{"type":"stream_event","event":{"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Hello world"}}}"""; - Assert.Equal("Hello world", _formatter.FormatLine(line)); + var line = """{"type":"assistant","message":{"content":[{"type":"text","text":"Hello world"}]}}"""; + Assert.Equal("Hello world\n", _formatter.FormatLine(line)); } [Fact] - public void FormatLine_ConsecutiveTextDeltas_ReturnEachDelta() + public void FormatLine_AssistantConsecutiveTextBlocks_ReturnEachAppended() { - var line1 = """{"type":"stream_event","event":{"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Hello "}}}"""; - var line2 = """{"type":"stream_event","event":{"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"world"}}}"""; - Assert.Equal("Hello ", _formatter.FormatLine(line1)); - Assert.Equal("world", _formatter.FormatLine(line2)); + var line1 = """{"type":"assistant","message":{"content":[{"type":"text","text":"Hello "}]}}"""; + var line2 = """{"type":"assistant","message":{"content":[{"type":"text","text":"world"}]}}"""; + Assert.Equal("Hello \n", _formatter.FormatLine(line1)); + Assert.Equal("world\n", _formatter.FormatLine(line2)); } [Fact] - public void FormatLine_ContentBlockStop_ReturnsNewline() + public void FormatLine_AssistantThinkingBlock_IsFiltered() { - var line = """{"type":"stream_event","event":{"type":"content_block_stop","index":0}}"""; - Assert.Equal("\n", _formatter.FormatLine(line)); + var line = """{"type":"assistant","message":{"content":[{"type":"thinking","text":"hidden"}]}}"""; + Assert.Null(_formatter.FormatLine(line)); } // --- Tool use, result, system, fallback --- [Fact] - public void FormatLine_ToolUseStart_ReturnsToolNameLine() + public void FormatLine_AssistantToolUseBlock_ReturnsToolNameLine() { - var line = """{"type":"stream_event","event":{"type":"content_block_start","index":1,"content_block":{"type":"tool_use","id":"x","name":"bash"}}}"""; - Assert.Equal("\n[Tool: bash]\n", _formatter.FormatLine(line)); + var line = """{"type":"assistant","message":{"content":[{"type":"tool_use","id":"x","name":"Bash","input":{"command":"ls"}}]}}"""; + Assert.Equal("[Bash] $ ls\n", _formatter.FormatLine(line)); } [Fact] @@ -58,13 +58,13 @@ public class StreamLineFormatterTests public void FormatLine_ApiRetry_ReturnsRetryNotice() { var line = """{"type":"system","subtype":"api_retry"}"""; - Assert.Equal("\n[Retrying API call...]\n", _formatter.FormatLine(line)); + Assert.Equal("[Retrying API call...]\n", _formatter.FormatLine(line)); } [Fact] - public void FormatLine_SystemNonRetry_ReturnsNull() + public void FormatLine_SystemUnknownSubtype_ReturnsNull() { - var line = """{"type":"system","subtype":"init"}"""; + var line = """{"type":"system","subtype":"some_unknown_subtype"}"""; Assert.Null(_formatter.FormatLine(line)); } @@ -98,8 +98,8 @@ public class StreamLineFormatterTests { var lines = new[] { - """{"type":"stream_event","event":{"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Hello"}}}""", - """{"type":"stream_event","event":{"type":"content_block_start","index":1,"content_block":{"type":"tool_use","id":"x","name":"bash"}}}""", + """{"type":"assistant","message":{"content":[{"type":"text","text":"Hello"}]}}""", + """{"type":"assistant","message":{"content":[{"type":"tool_use","id":"x","name":"Bash","input":{"command":"ls"}}]}}""", """{"type":"result","result":"Done."}""", }; var file = Path.GetTempFileName(); @@ -108,7 +108,7 @@ public class StreamLineFormatterTests File.WriteAllLines(file, lines); var result = _formatter.FormatFile(file); Assert.Contains("Hello", result); - Assert.Contains("[Tool: bash]", result); + Assert.Contains("[Bash]", result); Assert.Contains("Done.", result); } finally @@ -121,7 +121,7 @@ public class StreamLineFormatterTests public void FormatFile_TrimsLargeContent() { var chunk = new string('x', 1000); - var line = "{\"type\":\"stream_event\",\"event\":{\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"" + chunk + "\"}}}"; + var line = "{\"type\":\"assistant\",\"message\":{\"content\":[{\"type\":\"text\",\"text\":\"" + chunk + "\"}]}}"; var lines = Enumerable.Repeat(line, 65).ToArray(); var file = Path.GetTempFileName(); try diff --git a/tests/ClaudeDo.Ui.Tests/ViewModels/ConflictResolutionViewModelTests.cs b/tests/ClaudeDo.Ui.Tests/ViewModels/ConflictResolutionViewModelTests.cs index 5e8e355..6119951 100644 --- a/tests/ClaudeDo.Ui.Tests/ViewModels/ConflictResolutionViewModelTests.cs +++ b/tests/ClaudeDo.Ui.Tests/ViewModels/ConflictResolutionViewModelTests.cs @@ -48,6 +48,8 @@ public class ConflictResolutionViewModelTests public Task BuildPlanningIntegrationBranchAsync(string planningTaskId, string targetBranch) => Task.FromResult(null); public Task MergeAllPlanningAsync(string planningTaskId, string targetBranch) => Task.CompletedTask; + public Task OpenInteractiveTerminalAsync(string taskId, CancellationToken ct = default) => Task.CompletedTask; + public Task QueuePlanningSubtasksAsync(string parentTaskId, CancellationToken ct = default) => Task.CompletedTask; public Task ContinuePlanningMergeAsync(string planningTaskId) { diff --git a/tests/ClaudeDo.Ui.Tests/ViewModels/DetailsIslandPlanningTests.cs b/tests/ClaudeDo.Ui.Tests/ViewModels/DetailsIslandPlanningTests.cs index 11a8e8a..13532fb 100644 --- a/tests/ClaudeDo.Ui.Tests/ViewModels/DetailsIslandPlanningTests.cs +++ b/tests/ClaudeDo.Ui.Tests/ViewModels/DetailsIslandPlanningTests.cs @@ -80,6 +80,8 @@ public class DetailsIslandPlanningTests : IDisposable public Task MergeAllPlanningAsync(string planningTaskId, string targetBranch) => Task.CompletedTask; public Task ContinuePlanningMergeAsync(string planningTaskId) => Task.CompletedTask; public Task AbortPlanningMergeAsync(string planningTaskId) => Task.CompletedTask; + public Task OpenInteractiveTerminalAsync(string taskId, CancellationToken ct = default) => Task.CompletedTask; + public Task QueuePlanningSubtasksAsync(string parentTaskId, CancellationToken ct = default) => Task.CompletedTask; } private sealed class NullServiceProvider : IServiceProvider diff --git a/tests/ClaudeDo.Ui.Tests/ViewModels/PlanningDiffViewModelTests.cs b/tests/ClaudeDo.Ui.Tests/ViewModels/PlanningDiffViewModelTests.cs index 7d693df..8155328 100644 --- a/tests/ClaudeDo.Ui.Tests/ViewModels/PlanningDiffViewModelTests.cs +++ b/tests/ClaudeDo.Ui.Tests/ViewModels/PlanningDiffViewModelTests.cs @@ -47,6 +47,8 @@ public class PlanningDiffViewModelTests public Task MergeAllPlanningAsync(string planningTaskId, string targetBranch) => Task.CompletedTask; public Task ContinuePlanningMergeAsync(string planningTaskId) => Task.CompletedTask; public Task AbortPlanningMergeAsync(string planningTaskId) => Task.CompletedTask; + public Task OpenInteractiveTerminalAsync(string taskId, CancellationToken ct = default) => Task.CompletedTask; + public Task QueuePlanningSubtasksAsync(string parentTaskId, CancellationToken ct = default) => Task.CompletedTask; } [Fact] diff --git a/tests/ClaudeDo.Worker.Tests/Repositories/AppSettingsRepositoryTests.cs b/tests/ClaudeDo.Worker.Tests/Repositories/AppSettingsRepositoryTests.cs index ddb68e5..8880526 100644 --- a/tests/ClaudeDo.Worker.Tests/Repositories/AppSettingsRepositoryTests.cs +++ b/tests/ClaudeDo.Worker.Tests/Repositories/AppSettingsRepositoryTests.cs @@ -21,7 +21,7 @@ public class AppSettingsRepositoryTests : IDisposable Assert.Equal(AppSettingsEntity.SingletonId, row.Id); Assert.Equal("sonnet", row.DefaultModel); Assert.Equal(100, row.DefaultMaxTurns); - Assert.Equal("bypassPermissions", row.DefaultPermissionMode); + Assert.Equal("auto", row.DefaultPermissionMode); Assert.Equal("sibling", row.WorktreeStrategy); Assert.Null(row.CentralWorktreeRoot); Assert.False(row.WorktreeAutoCleanupEnabled);