refactor(ui): remove the stale-worker banner
The banner compared the running worker's build SHA against the selected list's HEAD via a git ancestor check on every list switch and reconnect. It added a git-rev-parse MSBuild target, a hub call, and a poll loop for a corner case that hasn't come up in practice — not worth the moving parts.
This commit is contained in:
@@ -13,8 +13,6 @@ namespace ClaudeDo.Data.Wire;
|
|||||||
|
|
||||||
public record ActiveTaskDto(string Slot, string TaskId, DateTime StartedAt);
|
public record ActiveTaskDto(string Slot, string TaskId, DateTime StartedAt);
|
||||||
|
|
||||||
public record WorkerBuildInfoDto(string? BuildSha);
|
|
||||||
|
|
||||||
public record AppSettingsDto(
|
public record AppSettingsDto(
|
||||||
string DefaultClaudeInstructions,
|
string DefaultClaudeInstructions,
|
||||||
string DefaultModel,
|
string DefaultModel,
|
||||||
|
|||||||
@@ -720,9 +720,6 @@
|
|||||||
"updateNow": "Jetzt aktualisieren",
|
"updateNow": "Jetzt aktualisieren",
|
||||||
"dismiss": "Ausblenden"
|
"dismiss": "Ausblenden"
|
||||||
},
|
},
|
||||||
"staleWorker": {
|
|
||||||
"message": "Der Worker läuft auf einem älteren Stand als der gemergte main-Branch dieses Repos — Neustart nötig, damit gemergte Änderungen wirken."
|
|
||||||
},
|
|
||||||
"externalMerge": {
|
"externalMerge": {
|
||||||
"banner": "Eine Claude-Session löst gerade einen Merge-Konflikt auf — bitte keine Dateien im Repository bearbeiten.",
|
"banner": "Eine Claude-Session löst gerade einen Merge-Konflikt auf — bitte keine Dateien im Repository bearbeiten.",
|
||||||
"open": "Resolver öffnen"
|
"open": "Resolver öffnen"
|
||||||
|
|||||||
@@ -720,9 +720,6 @@
|
|||||||
"updateNow": "Update now",
|
"updateNow": "Update now",
|
||||||
"dismiss": "Dismiss"
|
"dismiss": "Dismiss"
|
||||||
},
|
},
|
||||||
"staleWorker": {
|
|
||||||
"message": "The worker is running an older build than this repo's merged main — restart it so your merged changes take effect."
|
|
||||||
},
|
|
||||||
"externalMerge": {
|
"externalMerge": {
|
||||||
"banner": "A Claude session is resolving a merge conflict — don't edit files in the repository.",
|
"banner": "A Claude session is resolving a merge conflict — don't edit files in the repository.",
|
||||||
"open": "Open resolver"
|
"open": "Open resolver"
|
||||||
|
|||||||
@@ -183,9 +183,6 @@ public interface IWorkerClient : INotifyPropertyChanged
|
|||||||
Task<string> GetLastPrepLogAsync();
|
Task<string> GetLastPrepLogAsync();
|
||||||
Task<IReadOnlyList<WorkerLogEntry>> GetRecentLogsAsync();
|
Task<IReadOnlyList<WorkerLogEntry>> GetRecentLogsAsync();
|
||||||
|
|
||||||
/// <summary>Git SHA the running worker was built from (null when offline or the build isn't stamped).</summary>
|
|
||||||
Task<WorkerBuildInfoDto?> GetWorkerBuildInfoAsync();
|
|
||||||
|
|
||||||
Task<List<PrimeScheduleDto>> GetPrimeSchedulesAsync();
|
Task<List<PrimeScheduleDto>> GetPrimeSchedulesAsync();
|
||||||
Task<PrimeScheduleDto?> UpsertPrimeScheduleAsync(PrimeScheduleDto dto);
|
Task<PrimeScheduleDto?> UpsertPrimeScheduleAsync(PrimeScheduleDto dto);
|
||||||
Task DeletePrimeScheduleAsync(Guid id);
|
Task DeletePrimeScheduleAsync(Guid id);
|
||||||
|
|||||||
@@ -492,9 +492,6 @@ public partial class WorkerClient : ObservableObject, IAsyncDisposable, IWorkerC
|
|||||||
public async Task<string> GetLastPrepLogAsync()
|
public async Task<string> GetLastPrepLogAsync()
|
||||||
=> await TryInvokeAsync<string>("GetLastPrepLog") ?? string.Empty;
|
=> await TryInvokeAsync<string>("GetLastPrepLog") ?? string.Empty;
|
||||||
|
|
||||||
public Task<WorkerBuildInfoDto?> GetWorkerBuildInfoAsync()
|
|
||||||
=> TryInvokeAsync<WorkerBuildInfoDto>("GetWorkerBuildInfo");
|
|
||||||
|
|
||||||
public async Task<IReadOnlyList<WorkerLogEntry>> GetRecentLogsAsync()
|
public async Task<IReadOnlyList<WorkerLogEntry>> GetRecentLogsAsync()
|
||||||
=> await TryInvokeAsync<List<WorkerLogEntry>>("GetRecentLogs") ?? new List<WorkerLogEntry>();
|
=> await TryInvokeAsync<List<WorkerLogEntry>>("GetRecentLogs") ?? new List<WorkerLogEntry>();
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ using System.Threading.Tasks;
|
|||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
using CommunityToolkit.Mvvm.Input;
|
using CommunityToolkit.Mvvm.Input;
|
||||||
using ClaudeDo.Data;
|
using ClaudeDo.Data;
|
||||||
using ClaudeDo.Data.Git;
|
|
||||||
using ClaudeDo.Data.Models;
|
using ClaudeDo.Data.Models;
|
||||||
using ClaudeDo.Ui.Localization;
|
using ClaudeDo.Ui.Localization;
|
||||||
using ClaudeDo.Ui.Services;
|
using ClaudeDo.Ui.Services;
|
||||||
@@ -52,7 +51,6 @@ public sealed partial class IslandsShellViewModel : ViewModelBase, IDisposable
|
|||||||
private readonly UpdateCheckService _updateCheck = null!;
|
private readonly UpdateCheckService _updateCheck = null!;
|
||||||
private readonly InstallerLocator _installerLocator = null!;
|
private readonly InstallerLocator _installerLocator = null!;
|
||||||
private readonly WorkerLocator _workerLocator = null!;
|
private readonly WorkerLocator _workerLocator = null!;
|
||||||
private readonly GitService? _git;
|
|
||||||
private readonly IDbContextFactory<ClaudeDoDbContext>? _dbFactory;
|
private readonly IDbContextFactory<ClaudeDoDbContext>? _dbFactory;
|
||||||
private readonly Func<WorktreesOverviewModalViewModel> _worktreesOverviewVmFactory = () => null!;
|
private readonly Func<WorktreesOverviewModalViewModel> _worktreesOverviewVmFactory = () => null!;
|
||||||
private readonly Func<WeeklyReportModalViewModel> _weeklyReportVmFactory = () => null!;
|
private readonly Func<WeeklyReportModalViewModel> _weeklyReportVmFactory = () => null!;
|
||||||
@@ -121,11 +119,6 @@ public sealed partial class IslandsShellViewModel : ViewModelBase, IDisposable
|
|||||||
[ObservableProperty] private string? _updateBannerLatestVersion;
|
[ObservableProperty] private string? _updateBannerLatestVersion;
|
||||||
private bool _bannerDismissedThisSession;
|
private bool _bannerDismissedThisSession;
|
||||||
|
|
||||||
// Persistent (not auto-clearing) banner: the running worker predates the selected list's
|
|
||||||
// merged HEAD, so "verified" claims made against the current process are stale. ClaudeDo-repo
|
|
||||||
// only — see RefreshStaleWorkerCheckAsync.
|
|
||||||
[ObservableProperty] private bool _isStaleWorkerBannerVisible;
|
|
||||||
|
|
||||||
// planningTaskId -> subtaskId, for unit-merge conflicts an MCP session (not the UI) started.
|
// planningTaskId -> subtaskId, for unit-merge conflicts an MCP session (not the UI) started.
|
||||||
// Kept as a dictionary so a later conflict on the same planning task updates in place instead
|
// Kept as a dictionary so a later conflict on the same planning task updates in place instead
|
||||||
// of piling up duplicate entries.
|
// of piling up duplicate entries.
|
||||||
@@ -328,8 +321,7 @@ public sealed partial class IslandsShellViewModel : ViewModelBase, IDisposable
|
|||||||
Func<MergeModalViewModel> mergeVmFactory,
|
Func<MergeModalViewModel> mergeVmFactory,
|
||||||
Func<RepoImportModalViewModel> repoImportVmFactory,
|
Func<RepoImportModalViewModel> repoImportVmFactory,
|
||||||
MissionControlViewModel missionControl,
|
MissionControlViewModel missionControl,
|
||||||
UsagePillViewModel usagePill,
|
UsagePillViewModel usagePill)
|
||||||
GitService? git = null)
|
|
||||||
{
|
{
|
||||||
Lists = lists; Tasks = tasks; Details = details; Worker = worker;
|
Lists = lists; Tasks = tasks; Details = details; Worker = worker;
|
||||||
MissionControl = missionControl;
|
MissionControl = missionControl;
|
||||||
@@ -349,9 +341,7 @@ public sealed partial class IslandsShellViewModel : ViewModelBase, IDisposable
|
|||||||
_usageMonitorVmFactory = usageMonitorVmFactory;
|
_usageMonitorVmFactory = usageMonitorVmFactory;
|
||||||
_mergeVmFactory = mergeVmFactory;
|
_mergeVmFactory = mergeVmFactory;
|
||||||
_repoImportVmFactory = repoImportVmFactory;
|
_repoImportVmFactory = repoImportVmFactory;
|
||||||
_git = git;
|
|
||||||
Lists.SelectionChanged += (_, _) => Tasks.LoadForList(Lists.SelectedList);
|
Lists.SelectionChanged += (_, _) => Tasks.LoadForList(Lists.SelectedList);
|
||||||
Lists.SelectionChanged += (_, _) => _ = RefreshStaleWorkerCheckAsync();
|
|
||||||
Tasks.SelectionChanged += (_, _) => Details.Bind(Tasks.SelectedTask, Tasks.SelectionSource);
|
Tasks.SelectionChanged += (_, _) => Details.Bind(Tasks.SelectedTask, Tasks.SelectionSource);
|
||||||
Tasks.PrepRequested += () => Details.ShowPrep();
|
Tasks.PrepRequested += () => Details.ShowPrep();
|
||||||
Tasks.PropertyChanged += (_, e) =>
|
Tasks.PropertyChanged += (_, e) =>
|
||||||
@@ -418,7 +408,6 @@ public sealed partial class IslandsShellViewModel : ViewModelBase, IDisposable
|
|||||||
};
|
};
|
||||||
Worker.OperationProgressEvent += OnOperationProgress;
|
Worker.OperationProgressEvent += OnOperationProgress;
|
||||||
Worker.WorkerLogReceivedEvent += OnWorkerLogReceived;
|
Worker.WorkerLogReceivedEvent += OnWorkerLogReceived;
|
||||||
Worker.ConnectionRestoredEvent += () => _ = RefreshStaleWorkerCheckAsync();
|
|
||||||
Worker.PlanningMergeConflictEvent += OnPlanningMergeConflict;
|
Worker.PlanningMergeConflictEvent += OnPlanningMergeConflict;
|
||||||
Worker.PlanningMergeAbortedEvent += OnPlanningMergeAborted;
|
Worker.PlanningMergeAbortedEvent += OnPlanningMergeAborted;
|
||||||
Worker.PlanningCompletedEvent += OnPlanningMergeCompleted;
|
Worker.PlanningCompletedEvent += OnPlanningMergeCompleted;
|
||||||
@@ -471,8 +460,6 @@ public sealed partial class IslandsShellViewModel : ViewModelBase, IDisposable
|
|||||||
_connectTimer.Dispose();
|
_connectTimer.Dispose();
|
||||||
_primeStatusTimer.Stop();
|
_primeStatusTimer.Stop();
|
||||||
_primeStatusTimer.Dispose();
|
_primeStatusTimer.Dispose();
|
||||||
_staleWorkerCts?.Cancel();
|
|
||||||
_staleWorkerCts?.Dispose();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void RefreshBannerFromStatus()
|
private void RefreshBannerFromStatus()
|
||||||
@@ -492,57 +479,6 @@ public sealed partial class IslandsShellViewModel : ViewModelBase, IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private CancellationTokenSource? _staleWorkerCts;
|
|
||||||
|
|
||||||
// Re-evaluates the stale-worker banner for the currently selected list. Cheap (one hub call +
|
|
||||||
// up to two git subprocesses) and only ever runs for a git-backed list, so it's fine to fire on
|
|
||||||
// every selection change / reconnect rather than caching.
|
|
||||||
private async Task RefreshStaleWorkerCheckAsync()
|
|
||||||
{
|
|
||||||
_staleWorkerCts?.Cancel();
|
|
||||||
var cts = new CancellationTokenSource();
|
|
||||||
_staleWorkerCts = cts;
|
|
||||||
|
|
||||||
var stale = await ComputeIsStaleWorkerAsync(Lists?.SelectedList?.WorkingDir, cts.Token);
|
|
||||||
if (cts.IsCancellationRequested) return;
|
|
||||||
IsStaleWorkerBannerVisible = stale;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<bool> ComputeIsStaleWorkerAsync(string? workingDir, CancellationToken ct)
|
|
||||||
{
|
|
||||||
if (_git is null || Worker is null || string.IsNullOrWhiteSpace(workingDir)) return false;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var buildInfo = await Worker.GetWorkerBuildInfoAsync();
|
|
||||||
var buildSha = buildInfo?.BuildSha;
|
|
||||||
if (string.IsNullOrWhiteSpace(buildSha)) return false;
|
|
||||||
if (!await _git.IsGitRepoAsync(workingDir, ct)) return false;
|
|
||||||
|
|
||||||
var head = await _git.RevParseHeadAsync(workingDir, ct);
|
|
||||||
var isAncestor = string.Equals(buildSha, head, StringComparison.OrdinalIgnoreCase)
|
|
||||||
? (bool?)false // equal — never "stale" on a match, and no need to ask git
|
|
||||||
: await _git.IsAncestorAsync(workingDir, buildSha, head, ct);
|
|
||||||
return ShouldShowStaleWorkerBanner(buildSha, head, isAncestor);
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
// Worker offline, dir no longer a repo, etc. — unknown, so stay quiet.
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pure decision extracted for testability. isAncestor is the tri-state result of
|
|
||||||
// `git merge-base --is-ancestor buildSha head`: true = worker predates head (stale), false =
|
|
||||||
// equal or diverged (never claim "stale" on a match or an unrelated history), null = unknown
|
|
||||||
// (e.g. buildSha isn't a commit this repo knows about — never treat "unknown" as "stale").
|
|
||||||
internal static bool ShouldShowStaleWorkerBanner(string? buildSha, string? headSha, bool? isAncestor)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(buildSha) || string.IsNullOrWhiteSpace(headSha)) return false;
|
|
||||||
if (string.Equals(buildSha, headSha, StringComparison.OrdinalIgnoreCase)) return false;
|
|
||||||
return isAncestor == true;
|
|
||||||
}
|
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
private void OpenMissionControl()
|
private void OpenMissionControl()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -259,29 +259,6 @@
|
|||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
<!-- Stale-worker notice — bottom-right corner, above the update popup. Persistent
|
|
||||||
(no auto-dismiss timer): the running worker predates the selected list's merged
|
|
||||||
HEAD, so a restart is needed before "verified" claims against it are trustworthy. -->
|
|
||||||
<Border Grid.Row="0" Grid.RowSpan="3"
|
|
||||||
VerticalAlignment="Bottom" HorizontalAlignment="Right"
|
|
||||||
Margin="0,0,16,100"
|
|
||||||
ZIndex="100"
|
|
||||||
IsVisible="{Binding IsStaleWorkerBannerVisible}"
|
|
||||||
Background="{DynamicResource DeepBrush}"
|
|
||||||
BorderBrush="{DynamicResource LineBrush}"
|
|
||||||
BorderThickness="1"
|
|
||||||
CornerRadius="6"
|
|
||||||
Padding="14,10">
|
|
||||||
<StackPanel Spacing="8" Width="260">
|
|
||||||
<TextBlock Classes="body" Text="{loc:Tr shell.staleWorker.message}" TextWrapping="Wrap"/>
|
|
||||||
<StackPanel Orientation="Horizontal" Spacing="8" HorizontalAlignment="Right">
|
|
||||||
<Button Classes="btn"
|
|
||||||
Content="{loc:Tr shell.menu.restartWorker}"
|
|
||||||
Command="{Binding RestartWorkerCommand}"/>
|
|
||||||
</StackPanel>
|
|
||||||
</StackPanel>
|
|
||||||
</Border>
|
|
||||||
|
|
||||||
<!-- External merge popup card — bottom-left corner, while an MCP session owns an
|
<!-- External merge popup card — bottom-left corner, while an MCP session owns an
|
||||||
unresolved unit-merge conflict. Does not auto-dismiss; stays until the merge ends. -->
|
unresolved unit-merge conflict. Does not auto-dismiss; stays until the merge ends. -->
|
||||||
<Border Grid.Row="0" Grid.RowSpan="3"
|
<Border Grid.Row="0" Grid.RowSpan="3"
|
||||||
|
|||||||
@@ -35,18 +35,4 @@
|
|||||||
<InternalsVisibleTo Include="ClaudeDo.Worker.Tests" />
|
<InternalsVisibleTo Include="ClaudeDo.Worker.Tests" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<!-- Stamps this worker's exact commit into AssemblyInformationalVersion as "+{sha}" so a
|
|
||||||
running instance can report which commit it was built from (see WorkerHub.GetWorkerBuildInfo).
|
|
||||||
Independent of MinVer: MinVerVersionOverride (release builds) replaces the base version but
|
|
||||||
doesn't touch SourceRevisionId, and the SDK appends it to InformationalVersion regardless. -->
|
|
||||||
<Target Name="SetBuildRevisionFromGit" BeforeTargets="GetAssemblyVersion" Condition="'$(SourceRevisionId)' == ''">
|
|
||||||
<Exec Command="git rev-parse HEAD"
|
|
||||||
WorkingDirectory="$(MSBuildProjectDirectory)"
|
|
||||||
ConsoleToMSBuild="true"
|
|
||||||
StandardOutputImportance="low"
|
|
||||||
ContinueOnError="WarnAndContinue">
|
|
||||||
<Output TaskParameter="ConsoleOutput" PropertyName="SourceRevisionId" />
|
|
||||||
</Exec>
|
|
||||||
</Target>
|
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -35,20 +35,6 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
|
|||||||
private static readonly string Version =
|
private static readonly string Version =
|
||||||
Assembly.GetExecutingAssembly().GetName().Version?.ToString(3) ?? "0.0.0";
|
Assembly.GetExecutingAssembly().GetName().Version?.ToString(3) ?? "0.0.0";
|
||||||
|
|
||||||
// SourceRevisionId (set by ClaudeDo.Worker.csproj's git-rev-parse build target) is appended
|
|
||||||
// by the SDK to InformationalVersion as "+{sha}" -- take the segment after the LAST '+' so
|
|
||||||
// this stays correct even when MinVer's own pre-release metadata already contains a '+'.
|
|
||||||
private static readonly string? BuildSha = ParseBuildSha(
|
|
||||||
Assembly.GetExecutingAssembly().GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion);
|
|
||||||
|
|
||||||
internal static string? ParseBuildSha(string? informationalVersion)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(informationalVersion)) return null;
|
|
||||||
var plusIndex = informationalVersion.LastIndexOf('+');
|
|
||||||
if (plusIndex < 0 || plusIndex == informationalVersion.Length - 1) return null;
|
|
||||||
return informationalVersion[(plusIndex + 1)..];
|
|
||||||
}
|
|
||||||
|
|
||||||
private readonly QueueService _queue;
|
private readonly QueueService _queue;
|
||||||
private readonly IQueueWaker _waker;
|
private readonly IQueueWaker _waker;
|
||||||
private readonly AgentFileService _agentService;
|
private readonly AgentFileService _agentService;
|
||||||
@@ -233,11 +219,6 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
|
|||||||
|
|
||||||
public string Ping() => $"pong v{Version}";
|
public string Ping() => $"pong v{Version}";
|
||||||
|
|
||||||
/// <summary>Lets the UI detect a worker that's still running an older build than the code it's
|
|
||||||
/// comparing against (e.g. a just-merged main) — a separate diagnostic rather than extending
|
|
||||||
/// Ping's "pong vX.Y.Z" text, since that format isn't ours to break for any existing caller.</summary>
|
|
||||||
public WorkerBuildInfoDto GetWorkerBuildInfo() => new(BuildSha);
|
|
||||||
|
|
||||||
public IReadOnlyList<ActiveTaskDto> GetActive()
|
public IReadOnlyList<ActiveTaskDto> GetActive()
|
||||||
{
|
{
|
||||||
return _queue.GetActive()
|
return _queue.GetActive()
|
||||||
|
|||||||
@@ -1,47 +0,0 @@
|
|||||||
using ClaudeDo.Ui.ViewModels;
|
|
||||||
using Xunit;
|
|
||||||
|
|
||||||
namespace ClaudeDo.Ui.Tests;
|
|
||||||
|
|
||||||
public class StaleWorkerBannerDecisionTests
|
|
||||||
{
|
|
||||||
private const string ShaA = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
|
|
||||||
private const string ShaB = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void True_when_build_is_a_strict_ancestor_of_head()
|
|
||||||
{
|
|
||||||
Assert.True(IslandsShellViewModel.ShouldShowStaleWorkerBanner(ShaA, ShaB, isAncestor: true));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void False_when_build_equals_head_even_if_ancestor_check_says_true()
|
|
||||||
{
|
|
||||||
Assert.False(IslandsShellViewModel.ShouldShowStaleWorkerBanner(ShaA, ShaA, isAncestor: true));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void False_when_ancestor_check_says_no()
|
|
||||||
{
|
|
||||||
Assert.False(IslandsShellViewModel.ShouldShowStaleWorkerBanner(ShaA, ShaB, isAncestor: false));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void False_when_ancestor_check_is_unknown()
|
|
||||||
{
|
|
||||||
// Unknown must never be treated as stale.
|
|
||||||
Assert.False(IslandsShellViewModel.ShouldShowStaleWorkerBanner(ShaA, ShaB, isAncestor: null));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void False_when_build_sha_missing()
|
|
||||||
{
|
|
||||||
Assert.False(IslandsShellViewModel.ShouldShowStaleWorkerBanner(null, ShaB, isAncestor: true));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void False_when_head_sha_missing()
|
|
||||||
{
|
|
||||||
Assert.False(IslandsShellViewModel.ShouldShowStaleWorkerBanner(ShaA, null, isAncestor: true));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -170,8 +170,6 @@ public abstract class StubWorkerClient : IWorkerClient
|
|||||||
public virtual Task DeleteDailyNoteAsync(string id) => Task.CompletedTask;
|
public virtual Task DeleteDailyNoteAsync(string id) => Task.CompletedTask;
|
||||||
public string LastPrepLog = "";
|
public string LastPrepLog = "";
|
||||||
public virtual Task<string> GetLastPrepLogAsync() => Task.FromResult(LastPrepLog);
|
public virtual Task<string> GetLastPrepLogAsync() => Task.FromResult(LastPrepLog);
|
||||||
public WorkerBuildInfoDto? WorkerBuildInfo;
|
|
||||||
public virtual Task<WorkerBuildInfoDto?> GetWorkerBuildInfoAsync() => Task.FromResult(WorkerBuildInfo);
|
|
||||||
public virtual Task RefineTaskAsync(string taskId) => Task.CompletedTask;
|
public virtual Task RefineTaskAsync(string taskId) => Task.CompletedTask;
|
||||||
|
|
||||||
public virtual Task<ClaudeBinDto?> GetClaudeBinAsync() => Task.FromResult<ClaudeBinDto?>(null);
|
public virtual Task<ClaudeBinDto?> GetClaudeBinAsync() => Task.FromResult<ClaudeBinDto?>(null);
|
||||||
|
|||||||
@@ -1,36 +0,0 @@
|
|||||||
using ClaudeDo.Worker.Hub;
|
|
||||||
using Xunit;
|
|
||||||
|
|
||||||
namespace ClaudeDo.Worker.Tests.Hub;
|
|
||||||
|
|
||||||
public sealed class WorkerBuildInfoHubTests
|
|
||||||
{
|
|
||||||
[Theory]
|
|
||||||
[InlineData(null, null)]
|
|
||||||
[InlineData("", null)]
|
|
||||||
[InlineData("1.2.3", null)]
|
|
||||||
[InlineData("1.2.3+", null)]
|
|
||||||
[InlineData("1.2.3+abc1234", "abc1234")]
|
|
||||||
// MinVer's own pre-release metadata can already contain a '+' (e.g. "+devsha"); our
|
|
||||||
// SourceRevisionId is appended last, so only the segment after the LAST '+' is authoritative.
|
|
||||||
[InlineData("0.0.0-alpha.0.4+devsha+f5b1a2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9", "f5b1a2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9")]
|
|
||||||
public void ParseBuildSha_ExtractsSegmentAfterLastPlus(string? informationalVersion, string? expected)
|
|
||||||
{
|
|
||||||
Assert.Equal(expected, WorkerHub.ParseBuildSha(informationalVersion));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void GetWorkerBuildInfo_ReturnsDto()
|
|
||||||
{
|
|
||||||
var hub = new WorkerHub(
|
|
||||||
null!, null!, null!, null!, null!, null!,
|
|
||||||
null!, null!, null!, null!, null!, null!, null!, null!, null!, null!, null!, null!, null!,
|
|
||||||
null!, new ClaudeDo.Worker.Online.OnlineInboxConfig(), new ClaudeDo.Worker.OnlineRefreshTokenStore(new ClaudeDo.Worker.DpapiTokenStore("unused.token")),
|
|
||||||
new ClaudeDo.Worker.Runner.PendingQuestionRegistry(), null!);
|
|
||||||
|
|
||||||
// No assertion on the actual SHA value (depends on the build environment) — just that
|
|
||||||
// the call doesn't throw and always returns a DTO, never null.
|
|
||||||
var info = hub.GetWorkerBuildInfo();
|
|
||||||
Assert.NotNull(info);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -159,7 +159,6 @@ sealed class FakeWorkerClient : IWorkerClient
|
|||||||
public Task UpdateDailyNoteAsync(string id, string text) => Task.CompletedTask;
|
public Task UpdateDailyNoteAsync(string id, string text) => Task.CompletedTask;
|
||||||
public Task DeleteDailyNoteAsync(string id) => Task.CompletedTask;
|
public Task DeleteDailyNoteAsync(string id) => Task.CompletedTask;
|
||||||
public Task<string> GetLastPrepLogAsync() => Task.FromResult(string.Empty);
|
public Task<string> GetLastPrepLogAsync() => Task.FromResult(string.Empty);
|
||||||
public Task<WorkerBuildInfoDto?> GetWorkerBuildInfoAsync() => Task.FromResult<WorkerBuildInfoDto?>(null);
|
|
||||||
public Task RefineTaskAsync(string taskId) => Task.CompletedTask;
|
public Task RefineTaskAsync(string taskId) => Task.CompletedTask;
|
||||||
public Task<ClaudeBinDto?> GetClaudeBinAsync() => Task.FromResult<ClaudeBinDto?>(null);
|
public Task<ClaudeBinDto?> GetClaudeBinAsync() => Task.FromResult<ClaudeBinDto?>(null);
|
||||||
public Task<ClaudeBinDto?> SetClaudeBinAsync(string? value) => Task.FromResult<ClaudeBinDto?>(null);
|
public Task<ClaudeBinDto?> SetClaudeBinAsync(string? value) => Task.FromResult<ClaudeBinDto?>(null);
|
||||||
|
|||||||
Reference in New Issue
Block a user