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) <noreply@anthropic.com>
This commit is contained in:
Mika Kuns
2026-04-28 08:30:26 +02:00
parent dc3fc443b4
commit 8eafa71ed3
7 changed files with 35 additions and 24 deletions

View File

@@ -27,6 +27,9 @@ public sealed class TaskRepository
public async Task UpdateAsync(TaskEntity entity, CancellationToken ct = default) 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); _context.Tasks.Update(entity);
await _context.SaveChangesAsync(ct); await _context.SaveChangesAsync(ct);
} }

View File

@@ -244,7 +244,9 @@ public sealed partial class TasksIslandViewModel : ViewModelBase
.Where(r => r.IsChild && !string.IsNullOrEmpty(r.ParentTaskId)) .Where(r => r.IsChild && !string.IsNullOrEmpty(r.ParentTaskId))
.GroupBy(r => r.ParentTaskId!) .GroupBy(r => r.ParentTaskId!)
.ToDictionary(g => g.Key, g => g.ToList()); .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 (_expandedState.ContainsKey(parent.Id)) continue;
if (childrenByParent.TryGetValue(parent.Id, out var kids) if (childrenByParent.TryGetValue(parent.Id, out var kids)

View File

@@ -6,38 +6,38 @@ public class StreamLineFormatterTests
{ {
private readonly StreamLineFormatter _formatter = new(); private readonly StreamLineFormatter _formatter = new();
// --- Text deltas --- // --- Assistant text blocks ---
[Fact] [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"}}}"""; var line = """{"type":"assistant","message":{"content":[{"type":"text","text":"Hello world"}]}}""";
Assert.Equal("Hello world", _formatter.FormatLine(line)); Assert.Equal("Hello world\n", _formatter.FormatLine(line));
} }
[Fact] [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 line1 = """{"type":"assistant","message":{"content":[{"type":"text","text":"Hello "}]}}""";
var line2 = """{"type":"stream_event","event":{"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"world"}}}"""; var line2 = """{"type":"assistant","message":{"content":[{"type":"text","text":"world"}]}}""";
Assert.Equal("Hello ", _formatter.FormatLine(line1)); Assert.Equal("Hello \n", _formatter.FormatLine(line1));
Assert.Equal("world", _formatter.FormatLine(line2)); Assert.Equal("world\n", _formatter.FormatLine(line2));
} }
[Fact] [Fact]
public void FormatLine_ContentBlockStop_ReturnsNewline() public void FormatLine_AssistantThinkingBlock_IsFiltered()
{ {
var line = """{"type":"stream_event","event":{"type":"content_block_stop","index":0}}"""; var line = """{"type":"assistant","message":{"content":[{"type":"thinking","text":"hidden"}]}}""";
Assert.Equal("\n", _formatter.FormatLine(line)); Assert.Null(_formatter.FormatLine(line));
} }
// --- Tool use, result, system, fallback --- // --- Tool use, result, system, fallback ---
[Fact] [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"}}}"""; var line = """{"type":"assistant","message":{"content":[{"type":"tool_use","id":"x","name":"Bash","input":{"command":"ls"}}]}}""";
Assert.Equal("\n[Tool: bash]\n", _formatter.FormatLine(line)); Assert.Equal("[Bash] $ ls\n", _formatter.FormatLine(line));
} }
[Fact] [Fact]
@@ -58,13 +58,13 @@ public class StreamLineFormatterTests
public void FormatLine_ApiRetry_ReturnsRetryNotice() public void FormatLine_ApiRetry_ReturnsRetryNotice()
{ {
var line = """{"type":"system","subtype":"api_retry"}"""; 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] [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)); Assert.Null(_formatter.FormatLine(line));
} }
@@ -98,8 +98,8 @@ public class StreamLineFormatterTests
{ {
var lines = new[] var lines = new[]
{ {
"""{"type":"stream_event","event":{"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Hello"}}}""", """{"type":"assistant","message":{"content":[{"type":"text","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":"tool_use","id":"x","name":"Bash","input":{"command":"ls"}}]}}""",
"""{"type":"result","result":"Done."}""", """{"type":"result","result":"Done."}""",
}; };
var file = Path.GetTempFileName(); var file = Path.GetTempFileName();
@@ -108,7 +108,7 @@ public class StreamLineFormatterTests
File.WriteAllLines(file, lines); File.WriteAllLines(file, lines);
var result = _formatter.FormatFile(file); var result = _formatter.FormatFile(file);
Assert.Contains("Hello", result); Assert.Contains("Hello", result);
Assert.Contains("[Tool: bash]", result); Assert.Contains("[Bash]", result);
Assert.Contains("Done.", result); Assert.Contains("Done.", result);
} }
finally finally
@@ -121,7 +121,7 @@ public class StreamLineFormatterTests
public void FormatFile_TrimsLargeContent() public void FormatFile_TrimsLargeContent()
{ {
var chunk = new string('x', 1000); 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 lines = Enumerable.Repeat(line, 65).ToArray();
var file = Path.GetTempFileName(); var file = Path.GetTempFileName();
try try

View File

@@ -48,6 +48,8 @@ public class ConflictResolutionViewModelTests
public Task<CombinedDiffResultDto?> BuildPlanningIntegrationBranchAsync(string planningTaskId, string targetBranch) => public Task<CombinedDiffResultDto?> BuildPlanningIntegrationBranchAsync(string planningTaskId, string targetBranch) =>
Task.FromResult<CombinedDiffResultDto?>(null); Task.FromResult<CombinedDiffResultDto?>(null);
public Task MergeAllPlanningAsync(string planningTaskId, string targetBranch) => Task.CompletedTask; 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) public Task ContinuePlanningMergeAsync(string planningTaskId)
{ {

View File

@@ -80,6 +80,8 @@ public class DetailsIslandPlanningTests : IDisposable
public Task MergeAllPlanningAsync(string planningTaskId, string targetBranch) => Task.CompletedTask; public Task MergeAllPlanningAsync(string planningTaskId, string targetBranch) => Task.CompletedTask;
public Task ContinuePlanningMergeAsync(string planningTaskId) => Task.CompletedTask; public Task ContinuePlanningMergeAsync(string planningTaskId) => Task.CompletedTask;
public Task AbortPlanningMergeAsync(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 private sealed class NullServiceProvider : IServiceProvider

View File

@@ -47,6 +47,8 @@ public class PlanningDiffViewModelTests
public Task MergeAllPlanningAsync(string planningTaskId, string targetBranch) => Task.CompletedTask; public Task MergeAllPlanningAsync(string planningTaskId, string targetBranch) => Task.CompletedTask;
public Task ContinuePlanningMergeAsync(string planningTaskId) => Task.CompletedTask; public Task ContinuePlanningMergeAsync(string planningTaskId) => Task.CompletedTask;
public Task AbortPlanningMergeAsync(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] [Fact]

View File

@@ -21,7 +21,7 @@ public class AppSettingsRepositoryTests : IDisposable
Assert.Equal(AppSettingsEntity.SingletonId, row.Id); Assert.Equal(AppSettingsEntity.SingletonId, row.Id);
Assert.Equal("sonnet", row.DefaultModel); Assert.Equal("sonnet", row.DefaultModel);
Assert.Equal(100, row.DefaultMaxTurns); Assert.Equal(100, row.DefaultMaxTurns);
Assert.Equal("bypassPermissions", row.DefaultPermissionMode); Assert.Equal("auto", row.DefaultPermissionMode);
Assert.Equal("sibling", row.WorktreeStrategy); Assert.Equal("sibling", row.WorktreeStrategy);
Assert.Null(row.CentralWorktreeRoot); Assert.Null(row.CentralWorktreeRoot);
Assert.False(row.WorktreeAutoCleanupEnabled); Assert.False(row.WorktreeAutoCleanupEnabled);