diff --git a/src/ClaudeDo.App/Program.cs b/src/ClaudeDo.App/Program.cs index 844e014b..33210ab8 100644 --- a/src/ClaudeDo.App/Program.cs +++ b/src/ClaudeDo.App/Program.cs @@ -128,7 +128,6 @@ sealed class Program // Conflict-merge coordinator: single seam the shell wires to its resolver entry. sc.AddSingleton(); - sc.AddSingleton(sp => sp.GetRequiredService()); // ViewModels sc.AddTransient(); @@ -139,7 +138,7 @@ sealed class Program sc.AddTransient(); sc.AddSingleton(); sc.AddSingleton(); - sc.AddSingleton(); + sc.AddSingleton(); sc.AddTransient(); sc.AddTransient(); sc.AddTransient(); @@ -173,7 +172,7 @@ sealed class Program sp.GetRequiredService(), sp, sp.GetRequiredService(), - sp.GetRequiredService())); + sp.GetRequiredService())); sc.AddSingleton(sp => new UsagePillViewModel(sp.GetRequiredService())); sc.AddSingleton(sp => diff --git a/src/ClaudeDo.Data/Filtering/Filters/ReviewFilter.cs b/src/ClaudeDo.Data/Filtering/Filters/ReviewFilter.cs deleted file mode 100644 index 43c23dfe..00000000 --- a/src/ClaudeDo.Data/Filtering/Filters/ReviewFilter.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System.Linq.Expressions; -using ClaudeDo.Data.Models; -using TaskStatus = ClaudeDo.Data.Models.TaskStatus; - -namespace ClaudeDo.Data.Filtering.Filters; - -public sealed class ReviewFilter : TaskListFilterBase -{ - public override string Id => "virtual:review"; - protected override Expression> MatchExpr => t => t.Status == TaskStatus.WaitingForReview; - public override bool ShouldCount(TaskEntity t) => Matches(t); -} diff --git a/src/ClaudeDo.Data/Filtering/Filters/SmartFlagFilter.cs b/src/ClaudeDo.Data/Filtering/Filters/SmartFlagFilter.cs deleted file mode 100644 index a054a43b..00000000 --- a/src/ClaudeDo.Data/Filtering/Filters/SmartFlagFilter.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System.Linq.Expressions; -using ClaudeDo.Data.Models; -using TaskStatus = ClaudeDo.Data.Models.TaskStatus; - -namespace ClaudeDo.Data.Filtering.Filters; - -/// -/// Filter for a smart list keyed off a boolean/nullable task flag -/// (My Day, Important, Planned). Counts only non-done matches. -/// -public sealed class SmartFlagFilter(string id, Expression> flag) : TaskListFilterBase -{ - private readonly Func _flag = flag.Compile(); - - public override string Id => id; - protected override Expression> MatchExpr => flag; - public override bool ShouldCount(TaskEntity t) => _flag(t) && t.Status != TaskStatus.Done; -} diff --git a/src/ClaudeDo.Data/Filtering/Filters/StatusFilter.cs b/src/ClaudeDo.Data/Filtering/Filters/StatusFilter.cs deleted file mode 100644 index abafd8f4..00000000 --- a/src/ClaudeDo.Data/Filtering/Filters/StatusFilter.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System.Linq.Expressions; -using ClaudeDo.Data.Models; -using TaskStatus = ClaudeDo.Data.Models.TaskStatus; - -namespace ClaudeDo.Data.Filtering.Filters; - -/// -/// Virtual list filter matching tasks by a single status (Queued, Running). -/// Planning parents appear contextually when they host a matching child. -/// -public sealed class StatusFilter(string id, TaskStatus status) : TaskListFilterBase -{ - public override string Id => id; - protected override Expression> MatchExpr => t => t.Status == status; - public override bool ShouldCount(TaskEntity t) => t.Status == status; - public override bool MatchesAsContext(TaskEntity t, IReadOnlyList all) => - PlanningRules.IsPlanningParent(t) && - PlanningRules.HasMatchingChild(t, all, c => c.Status == status); -} diff --git a/src/ClaudeDo.Data/Filtering/Filters/TaskListFilterBase.cs b/src/ClaudeDo.Data/Filtering/Filters/TaskListFilterBase.cs deleted file mode 100644 index be0db003..00000000 --- a/src/ClaudeDo.Data/Filtering/Filters/TaskListFilterBase.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System.Linq.Expressions; -using ClaudeDo.Data.Models; - -namespace ClaudeDo.Data.Filtering.Filters; - -/// -/// Base for implementations: subclasses express their -/// primary-match condition once as an expression tree (), which -/// doubles as a SQL-translatable predicate () and, compiled -/// on first use, as the in-memory predicate. -/// -public abstract class TaskListFilterBase : ITaskListFilter -{ - private Func? _compiled; - - public abstract string Id { get; } - - protected abstract Expression> MatchExpr { get; } - - public Expression> MatchExpression => MatchExpr; - - public bool Matches(TaskEntity t) => (_compiled ??= MatchExpr.Compile())(t); - - public abstract bool ShouldCount(TaskEntity t); - - public virtual bool MatchesAsContext(TaskEntity t, IReadOnlyList all) => false; -} diff --git a/src/ClaudeDo.Data/Filtering/Filters/UserListFilter.cs b/src/ClaudeDo.Data/Filtering/Filters/UserListFilter.cs deleted file mode 100644 index d340dcea..00000000 --- a/src/ClaudeDo.Data/Filtering/Filters/UserListFilter.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System.Linq.Expressions; -using ClaudeDo.Data.Models; -using TaskStatus = ClaudeDo.Data.Models.TaskStatus; - -namespace ClaudeDo.Data.Filtering.Filters; - -/// -/// Filter for any user-defined list. Constructed on demand from the list id — -/// one instance per list. -/// -public sealed class UserListFilter : TaskListFilterBase -{ - private readonly string _listId; - - public UserListFilter(string listId) - { - _listId = listId; - Id = $"user:{listId}"; - } - - public override string Id { get; } - protected override Expression> MatchExpr => t => t.ListId == _listId; - public override bool ShouldCount(TaskEntity t) => t.ListId == _listId && t.Status != TaskStatus.Done; -} diff --git a/src/ClaudeDo.Data/Filtering/Interfaces/ITaskListFilter.cs b/src/ClaudeDo.Data/Filtering/Interfaces/ITaskListFilter.cs deleted file mode 100644 index 0782df3d..00000000 --- a/src/ClaudeDo.Data/Filtering/Interfaces/ITaskListFilter.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System.Linq.Expressions; -using ClaudeDo.Data.Models; - -namespace ClaudeDo.Data.Filtering; - -/// -/// Strategy that defines which tasks belong to a single list. One implementation -/// per list kind; consumers (counters, list loader) ask the registry for the -/// right strategy and never branch on the list id themselves. -/// -public interface ITaskListFilter -{ - /// The list id this filter applies to (e.g. "virtual:queued", "user:abc"). - string Id { get; } - - /// True if is a primary citizen of this list — appears as a row. - bool Matches(TaskEntity t); - - /// The primary-match predicate as an expression tree, so EF Core can push it into SQL. - Expression> MatchExpression { get; } - - /// True if should be counted in this list's badge. - bool ShouldCount(TaskEntity t); - - /// - /// True if is shown as a contextual row (not a primary citizen, - /// but appears to host children that match). Default: nothing extra. - /// - bool MatchesAsContext(TaskEntity t, IReadOnlyList all) => false; -} diff --git a/src/ClaudeDo.Data/Filtering/TaskListFilterRegistry.cs b/src/ClaudeDo.Data/Filtering/TaskListFilterRegistry.cs index c817bf4d..b7358484 100644 --- a/src/ClaudeDo.Data/Filtering/TaskListFilterRegistry.cs +++ b/src/ClaudeDo.Data/Filtering/TaskListFilterRegistry.cs @@ -1,8 +1,84 @@ -using ClaudeDo.Data.Filtering.Filters; +using System.Linq.Expressions; +using ClaudeDo.Data.Models; using TaskStatus = ClaudeDo.Data.Models.TaskStatus; namespace ClaudeDo.Data.Filtering; +/// +/// Strategy that defines which tasks belong to a single list. One subclass per list kind; +/// consumers (counters, list loader) ask the registry for the right strategy and never branch +/// on the list id themselves. +/// +/// Subclasses express their primary-match condition once as an expression tree +/// (), which doubles as a SQL-translatable predicate for EF Core +/// and, compiled on first use, as the in-memory predicate. +/// +public abstract class TaskListFilter +{ + private Func? _compiled; + + /// The list id this filter applies to (e.g. "virtual:queued", "user:abc"). + public abstract string Id { get; } + + /// The primary-match predicate as an expression tree, so EF Core can push it into SQL. + public abstract Expression> MatchExpression { get; } + + /// True if is a primary citizen of this list — appears as a row. + public bool Matches(TaskEntity t) => (_compiled ??= MatchExpression.Compile())(t); + + /// True if should be counted in this list's badge. + public abstract bool ShouldCount(TaskEntity t); + + /// + /// True if is shown as a contextual row (not a primary citizen, + /// but appears to host children that match). Default: nothing extra. + /// + public virtual bool MatchesAsContext(TaskEntity t, IReadOnlyList all) => false; +} + +/// +/// Filter for a smart list keyed off a boolean/nullable task flag +/// (My Day, Important, Planned). Counts only non-done matches. +/// +public sealed class SmartFlagFilter(string id, Expression> flag) : TaskListFilter +{ + public override string Id => id; + public override Expression> MatchExpression => flag; + public override bool ShouldCount(TaskEntity t) => Matches(t) && t.Status != TaskStatus.Done; +} + +/// +/// Virtual list filter matching tasks by a single status (Queued, Running). +/// Planning parents appear contextually when they host a matching child. +/// +public sealed class StatusFilter(string id, TaskStatus status) : TaskListFilter +{ + public override string Id => id; + public override Expression> MatchExpression => t => t.Status == status; + public override bool ShouldCount(TaskEntity t) => t.Status == status; + public override bool MatchesAsContext(TaskEntity t, IReadOnlyList all) => + PlanningRules.IsPlanningParent(t) && + PlanningRules.HasMatchingChild(t, all, c => c.Status == status); +} + +public sealed class ReviewFilter : TaskListFilter +{ + public override string Id => "virtual:review"; + public override Expression> MatchExpression => t => t.Status == TaskStatus.WaitingForReview; + public override bool ShouldCount(TaskEntity t) => Matches(t); +} + +/// +/// Filter for any user-defined list. Constructed on demand from the list id — +/// one instance per list. +/// +public sealed class UserListFilter(string listId) : TaskListFilter +{ + public override string Id { get; } = $"user:{listId}"; + public override Expression> MatchExpression => t => t.ListId == listId; + public override bool ShouldCount(TaskEntity t) => t.ListId == listId && t.Status != TaskStatus.Done; +} + /// /// Resolves a list id (e.g. "virtual:queued", "user:abc") to the filter that /// owns its semantics. Smart and virtual filters are singletons; user-list @@ -12,8 +88,8 @@ public sealed class TaskListFilterRegistry { public const string UserListPrefix = "user:"; - private static readonly IReadOnlyDictionary BuiltIn = - new Dictionary(StringComparer.Ordinal) + private static readonly IReadOnlyDictionary BuiltIn = + new Dictionary(StringComparer.Ordinal) { ["smart:my-day"] = new SmartFlagFilter("smart:my-day", t => t.IsMyDay), ["smart:important"] = new SmartFlagFilter("smart:important", t => t.IsStarred), @@ -26,7 +102,7 @@ public sealed class TaskListFilterRegistry /// /// Resolve a filter for a list id, or null if the id is unknown. /// - public ITaskListFilter? Resolve(string listId) + public TaskListFilter? Resolve(string listId) { if (BuiltIn.TryGetValue(listId, out var f)) return f; if (listId.StartsWith(UserListPrefix, StringComparison.Ordinal)) diff --git a/src/ClaudeDo.Ui/Services/Interfaces/IOnlineLoginService.cs b/src/ClaudeDo.Ui/Services/Interfaces/IOnlineLoginService.cs deleted file mode 100644 index 4658847b..00000000 --- a/src/ClaudeDo.Ui/Services/Interfaces/IOnlineLoginService.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace ClaudeDo.Ui.Services; - -public sealed record OnlineLoginResult(bool Success, string? RefreshToken, string? Error, string? Warning = null); - -public interface IOnlineLoginService -{ - Task LoginAsync( - string authority, string clientId, string scope, string redirectUri, - CancellationToken ct = default); -} diff --git a/src/ClaudeDo.Ui/Services/IMergeCoordinator.cs b/src/ClaudeDo.Ui/Services/MergeCoordinator.cs similarity index 62% rename from src/ClaudeDo.Ui/Services/IMergeCoordinator.cs rename to src/ClaudeDo.Ui/Services/MergeCoordinator.cs index 03496977..dd5c2460 100644 --- a/src/ClaudeDo.Ui/Services/IMergeCoordinator.cs +++ b/src/ClaudeDo.Ui/Services/MergeCoordinator.cs @@ -6,20 +6,12 @@ namespace ClaudeDo.Ui.Services; /// /// Single entry point for handing a conflicting merge to the in-app 3-pane resolver. /// Replaces the per-VM RequestConflictResolution Func seams that used to be -/// hand-threaded shell → details → merge-section → diff → merge-modal. The shell wires -/// once at composition; invokers depend only on -/// this interface (injected via DI). -/// -public interface IMergeCoordinator -{ - Task ResolveConflictAsync(string taskId, string targetBranch); -} - -/// +/// hand-threaded shell → details → merge-section → diff → merge-modal. +/// /// DI singleton holding the resolver entry. The holder breaks the shell↔island construction -/// cycle: islands depend on the interface, the shell sets after it is built. +/// cycle: islands depend on this type, the shell sets after it is built. /// -public sealed class MergeCoordinator : IMergeCoordinator +public sealed class MergeCoordinator { /// Set once at composition to the shell's resolver entry. Null (headless/tests) ⇒ no-op. public Func? Handler { get; set; } diff --git a/src/ClaudeDo.Ui/Services/OnlineLoginService.cs b/src/ClaudeDo.Ui/Services/OnlineLoginService.cs index 51fe90fb..ac95ab2a 100644 --- a/src/ClaudeDo.Ui/Services/OnlineLoginService.cs +++ b/src/ClaudeDo.Ui/Services/OnlineLoginService.cs @@ -6,7 +6,9 @@ using Duende.IdentityModel.OidcClient.Browser; namespace ClaudeDo.Ui.Services; -public sealed class OnlineLoginService : IOnlineLoginService +public sealed record OnlineLoginResult(bool Success, string? RefreshToken, string? Error, string? Warning = null); + +public sealed class OnlineLoginService { public async Task LoginAsync( string authority, string clientId, string scope, string redirectUri, diff --git a/src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs b/src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs index 0d16fb59..f33fdf0f 100644 --- a/src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs +++ b/src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs @@ -21,7 +21,7 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable private readonly IWorkerClient _worker; private readonly IServiceProvider _services; private readonly INotesApi _notesApi; - private readonly IMergeCoordinator _merge; + private readonly MergeCoordinator _merge; // ── Section view models ─────────────────────────────────────────────────── public AgentConfigEditorViewModel AgentSettings { get; } @@ -325,7 +325,7 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable IWorkerClient worker, IServiceProvider services, INotesApi notesApi, - IMergeCoordinator merge) + MergeCoordinator merge) { _dbFactory = dbFactory; _worker = worker; diff --git a/src/ClaudeDo.Ui/ViewModels/MissionControl/ConPtyPaneViewModel.cs b/src/ClaudeDo.Ui/ViewModels/MissionControl/ConPtyPaneViewModel.cs index 1aa94df9..6fd7bf82 100644 --- a/src/ClaudeDo.Ui/ViewModels/MissionControl/ConPtyPaneViewModel.cs +++ b/src/ClaudeDo.Ui/ViewModels/MissionControl/ConPtyPaneViewModel.cs @@ -11,7 +11,7 @@ namespace ClaudeDo.Ui.ViewModels.MissionControl; /// Claude session, or an ad-hoc/free session in a user-chosen directory (no task, /// is null — ad-hoc panes are never deduped, unlike task-based ones). /// -public sealed partial class ConPtyPaneViewModel : ViewModelBase, IMissionControlPane, IDisposable +public sealed partial class ConPtyPaneViewModel : ViewModelBase, IDisposable { public string? TaskId { get; } diff --git a/src/ClaudeDo.Ui/ViewModels/MissionControl/IMissionControlPane.cs b/src/ClaudeDo.Ui/ViewModels/MissionControl/IMissionControlPane.cs deleted file mode 100644 index b7fcf281..00000000 --- a/src/ClaudeDo.Ui/ViewModels/MissionControl/IMissionControlPane.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace ClaudeDo.Ui.ViewModels.MissionControl; - -/// -/// Common contract for anything hosted as a pane in the Command Center — currently only -/// , kept as its own abstraction so the grid/tabs layout -/// toggle binds a pane collection rather than a concrete ConPTY-specific type. -/// -public interface IMissionControlPane -{ - string DisplayTitle { get; } -} diff --git a/src/ClaudeDo.Ui/ViewModels/MissionControlViewModel.cs b/src/ClaudeDo.Ui/ViewModels/MissionControlViewModel.cs index a0baa856..93428487 100644 --- a/src/ClaudeDo.Ui/ViewModels/MissionControlViewModel.cs +++ b/src/ClaudeDo.Ui/ViewModels/MissionControlViewModel.cs @@ -35,7 +35,7 @@ public sealed partial class MissionControlViewModel : ViewModelBase, IDisposable // Mirror of ConPtySessions typed as the pane abstraction so the layout toggle (grid/tabs) // binds one contract rather than a ConPTY-specific type. - public ObservableCollection Panes { get; } = new(); + public ObservableCollection Panes { get; } = new(); [ObservableProperty] private int _columnCount = 1; @@ -43,7 +43,7 @@ public sealed partial class MissionControlViewModel : ViewModelBase, IDisposable [NotifyPropertyChangedFor(nameof(LayoutToggleLabel))] private bool _isFocusMode; - [ObservableProperty] private IMissionControlPane? _focusedPane; + [ObservableProperty] private ConPtyPaneViewModel? _focusedPane; public string LayoutToggleLabel => Loc.T(IsFocusMode ? "missionControl.overviewMode" : "missionControl.focusMode"); @@ -403,7 +403,7 @@ public sealed partial class MissionControlViewModel : ViewModelBase, IDisposable _ => 3, }; OnPropertyChanged(nameof(HasPanes)); - if (e.Action == NotifyCollectionChangedAction.Add && e.NewItems?[0] is IMissionControlPane added) + if (e.Action == NotifyCollectionChangedAction.Add && e.NewItems?[0] is ConPtyPaneViewModel added) FocusedPane = added; else if (e.Action == NotifyCollectionChangedAction.Remove && ReferenceEquals(FocusedPane, e.OldItems?[0])) FocusedPane = Panes.LastOrDefault(); diff --git a/src/ClaudeDo.Ui/ViewModels/Modals/MergeModalViewModel.cs b/src/ClaudeDo.Ui/ViewModels/Modals/MergeModalViewModel.cs index cf8e4c0b..0f69ade4 100644 --- a/src/ClaudeDo.Ui/ViewModels/Modals/MergeModalViewModel.cs +++ b/src/ClaudeDo.Ui/ViewModels/Modals/MergeModalViewModel.cs @@ -9,7 +9,7 @@ namespace ClaudeDo.Ui.ViewModels.Modals; public sealed partial class MergeModalViewModel : ViewModelBase { private readonly IWorkerClient _worker; - private readonly IMergeCoordinator _merge; + private readonly MergeCoordinator _merge; public string TaskId { get; set; } = ""; public string TaskTitle { get; set; } = ""; @@ -42,7 +42,7 @@ public sealed partial class MergeModalViewModel : ViewModelBase /// True once a conflict has been handed off to the resolver — also a cue to close the diff window. public bool RoutedToResolver { get; private set; } - public MergeModalViewModel(IWorkerClient worker, IMergeCoordinator merge) + public MergeModalViewModel(IWorkerClient worker, MergeCoordinator merge) { _worker = worker; _merge = merge; diff --git a/src/ClaudeDo.Ui/ViewModels/Modals/Settings/OnlineInboxSettingsViewModel.cs b/src/ClaudeDo.Ui/ViewModels/Modals/Settings/OnlineInboxSettingsViewModel.cs index 2036f5d7..cc27ecf2 100644 --- a/src/ClaudeDo.Ui/ViewModels/Modals/Settings/OnlineInboxSettingsViewModel.cs +++ b/src/ClaudeDo.Ui/ViewModels/Modals/Settings/OnlineInboxSettingsViewModel.cs @@ -8,7 +8,7 @@ namespace ClaudeDo.Ui.ViewModels.Modals.Settings; public sealed partial class OnlineInboxSettingsViewModel : ViewModelBase { private readonly IWorkerClient _worker; - private readonly IOnlineLoginService _loginService; + private readonly OnlineLoginService _loginService; [ObservableProperty] private bool _enabled; [ObservableProperty] private string _apiBaseUrl = ""; @@ -21,7 +21,7 @@ public sealed partial class OnlineInboxSettingsViewModel : ViewModelBase [ObservableProperty] private bool _isBusy; [ObservableProperty] private string _statusMessage = ""; - public OnlineInboxSettingsViewModel(IWorkerClient worker, IOnlineLoginService loginService) + public OnlineInboxSettingsViewModel(IWorkerClient worker, OnlineLoginService loginService) { _worker = worker; _loginService = loginService; diff --git a/src/ClaudeDo.Ui/ViewModels/Modals/SettingsModalViewModel.cs b/src/ClaudeDo.Ui/ViewModels/Modals/SettingsModalViewModel.cs index 3efba36c..4b55b3e6 100644 --- a/src/ClaudeDo.Ui/ViewModels/Modals/SettingsModalViewModel.cs +++ b/src/ClaudeDo.Ui/ViewModels/Modals/SettingsModalViewModel.cs @@ -73,7 +73,7 @@ public sealed partial class SettingsModalViewModel : ViewModelBase public Action? CloseAction { get; set; } public SettingsModalViewModel(IWorkerClient worker, PrimeClaudeTabViewModel prime, - IOnlineLoginService onlineLoginService, + OnlineLoginService onlineLoginService, ILocalizer localizer, AppSettings appSettings, IDbContextFactory dbFactory) { diff --git a/src/ClaudeDo.Ui/ViewModels/Modals/WorktreesOverviewModalViewModel.cs b/src/ClaudeDo.Ui/ViewModels/Modals/WorktreesOverviewModalViewModel.cs index b88be5d8..7185143c 100644 --- a/src/ClaudeDo.Ui/ViewModels/Modals/WorktreesOverviewModalViewModel.cs +++ b/src/ClaudeDo.Ui/ViewModels/Modals/WorktreesOverviewModalViewModel.cs @@ -69,7 +69,7 @@ public sealed partial class WorktreesOverviewModalViewModel : ViewModelBase { private readonly IWorkerClient _worker; private readonly Func _diffVmFactory; - private readonly IMergeCoordinator _merge; + private readonly MergeCoordinator _merge; [ObservableProperty] private string? _listIdFilter; [ObservableProperty] private string _title = "Worktrees"; @@ -104,7 +104,7 @@ public sealed partial class WorktreesOverviewModalViewModel : ViewModelBase public Func? ResolveMergeVm { get; set; } public Func? ShowMergeAction { get; set; } - public WorktreesOverviewModalViewModel(IWorkerClient worker, Func diffVmFactory, IMergeCoordinator merge) + public WorktreesOverviewModalViewModel(IWorkerClient worker, Func diffVmFactory, MergeCoordinator merge) { _worker = worker; _diffVmFactory = diffVmFactory; diff --git a/src/ClaudeDo.Ui/Views/MissionControl/MissionControlView.axaml b/src/ClaudeDo.Ui/Views/MissionControl/MissionControlView.axaml index 26aa73bb..645903f0 100644 --- a/src/ClaudeDo.Ui/Views/MissionControl/MissionControlView.axaml +++ b/src/ClaudeDo.Ui/Views/MissionControl/MissionControlView.axaml @@ -75,7 +75,7 @@ SelectedItem="{Binding FocusedPane}" IsVisible="{Binding IsFocusMode}"> - + diff --git a/src/ClaudeDo.Worker/External/FindingsMcpTools.cs b/src/ClaudeDo.Worker/External/FindingsMcpTools.cs index b21911d9..ce185336 100644 --- a/src/ClaudeDo.Worker/External/FindingsMcpTools.cs +++ b/src/ClaudeDo.Worker/External/FindingsMcpTools.cs @@ -11,10 +11,10 @@ public sealed record SaveFindingResult( [McpServerToolType] public sealed class FindingsMcpTools { - private readonly IFindingsStore _store; - private readonly IFindingsStoreLocator _locator; + private readonly FindingsStore _store; + private readonly FindingsStoreLocator _locator; - public FindingsMcpTools(IFindingsStore store, IFindingsStoreLocator locator) + public FindingsMcpTools(FindingsStore store, FindingsStoreLocator locator) { _store = store; _locator = locator; diff --git a/src/ClaudeDo.Worker/Findings/FindingsStore.cs b/src/ClaudeDo.Worker/Findings/FindingsStore.cs index df9b4c0c..b01fd054 100644 --- a/src/ClaudeDo.Worker/Findings/FindingsStore.cs +++ b/src/ClaudeDo.Worker/Findings/FindingsStore.cs @@ -4,13 +4,29 @@ using ClaudeDo.Worker.Git; namespace ClaudeDo.Worker.Findings; +/// One durable finding, as handed in by a caller. Git and DB lookups happen above this layer. +public sealed record FindingInput( + string Slug, + string Title, + string Body, + string Scope, + string SourceTaskId, + string VerifiedAgainst); + +public sealed record SaveFindingOutcome( + string Slug, + string Path, + bool Created, + int TotalFindings, + bool NearCapacity); + /// /// Owns <working-dir>/.claudedo/ — one markdown file per finding plus a rebuilt INDEX.md. /// Pure filesystem: the caller supplies the head commit and source task id. /// INDEX.md is always regenerated from the files on disk, never patched, so findings the user /// deleted or renamed by hand disappear from the index on the next write. /// -public sealed class FindingsStore : IFindingsStore +public sealed class FindingsStore { /// INDEX.md is read by every run; past this many entries it stops paying for itself. public const int WarnThreshold = 80; diff --git a/src/ClaudeDo.Worker/Findings/FindingsStoreLocator.cs b/src/ClaudeDo.Worker/Findings/FindingsStoreLocator.cs index 2effbe0c..943b8e01 100644 --- a/src/ClaudeDo.Worker/Findings/FindingsStoreLocator.cs +++ b/src/ClaudeDo.Worker/Findings/FindingsStoreLocator.cs @@ -3,12 +3,15 @@ using ClaudeDo.Data.Repositories; namespace ClaudeDo.Worker.Findings; +/// The main checkout a finding belongs to, plus whether its store is committed with the repo. +public sealed record FindingsTarget(string ListId, string ListName, string WorkingDir, bool Tracked); + /// /// Maps an MCP call to the project whose findings store it targets. Always resolves to the list's /// WorkingDir — the main checkout — even when the caller runs inside a worktree, because concurrent /// writes into worktree copies would produce INDEX.md merge conflicts. /// -public sealed class FindingsStoreLocator : IFindingsStoreLocator +public sealed class FindingsStoreLocator { private readonly TaskRepository _tasks; private readonly ListRepository _lists; diff --git a/src/ClaudeDo.Worker/Findings/Interfaces/IFindingsStore.cs b/src/ClaudeDo.Worker/Findings/Interfaces/IFindingsStore.cs deleted file mode 100644 index fe766c8c..00000000 --- a/src/ClaudeDo.Worker/Findings/Interfaces/IFindingsStore.cs +++ /dev/null @@ -1,22 +0,0 @@ -namespace ClaudeDo.Worker.Findings; - -/// One durable finding, as handed in by a caller. Git and DB lookups happen above this layer. -public sealed record FindingInput( - string Slug, - string Title, - string Body, - string Scope, - string SourceTaskId, - string VerifiedAgainst); - -public sealed record SaveFindingOutcome( - string Slug, - string Path, - bool Created, - int TotalFindings, - bool NearCapacity); - -public interface IFindingsStore -{ - Task SaveAsync(string workingDir, FindingInput input, CancellationToken ct, bool tracked = false); -} diff --git a/src/ClaudeDo.Worker/Findings/Interfaces/IFindingsStoreLocator.cs b/src/ClaudeDo.Worker/Findings/Interfaces/IFindingsStoreLocator.cs deleted file mode 100644 index ff075921..00000000 --- a/src/ClaudeDo.Worker/Findings/Interfaces/IFindingsStoreLocator.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace ClaudeDo.Worker.Findings; - -/// The main checkout a finding belongs to, plus whether its store is committed with the repo. -public sealed record FindingsTarget(string ListId, string ListName, string WorkingDir, bool Tracked); - -public interface IFindingsStoreLocator -{ - Task ResolveForTaskAsync(string taskId, CancellationToken ct); - Task ResolveForListAsync(string listIdOrName, CancellationToken ct); -} diff --git a/src/ClaudeDo.Worker/Hub/WorkerHub.cs b/src/ClaudeDo.Worker/Hub/WorkerHub.cs index 7fda04dc..c1bb25ba 100644 --- a/src/ClaudeDo.Worker/Hub/WorkerHub.cs +++ b/src/ClaudeDo.Worker/Hub/WorkerHub.cs @@ -229,11 +229,11 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub private readonly PlanningAggregator _planningAggregator; private readonly PlanningMergeOrchestrator _planningMergeOrchestrator; private readonly PlanningChainCoordinator _planningChain; - private readonly IPrimeScheduleSignal _primeSignal; + private readonly PrimeScheduleSignal _primeSignal; private readonly IPrimeRunner _primeRunner; private readonly ITaskStateService _state; - private readonly IWeekReportService _report; - private readonly IRefineRunner _refineRunner; + private readonly WeekReportService _report; + private readonly RefineRunner _refineRunner; private readonly WorkerConfig _cfg; private readonly OnlineInboxConfig _onlineInboxConfig; private readonly OnlineTokenStore _onlineTokenStore; @@ -264,11 +264,11 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub PlanningAggregator planningAggregator, PlanningMergeOrchestrator planningMergeOrchestrator, PlanningChainCoordinator planningChain, - IPrimeScheduleSignal primeSignal, + PrimeScheduleSignal primeSignal, IPrimeRunner primeRunner, ITaskStateService state, - IWeekReportService report, - IRefineRunner refineRunner, + WeekReportService report, + RefineRunner refineRunner, WorkerConfig cfg, OnlineInboxConfig onlineInboxConfig, OnlineTokenStore onlineTokenStore, diff --git a/src/ClaudeDo.Worker/Online/StaticTokenAuthProvider.cs b/src/ClaudeDo.Worker/Online/StaticTokenAuthProvider.cs deleted file mode 100644 index 6d41a38f..00000000 --- a/src/ClaudeDo.Worker/Online/StaticTokenAuthProvider.cs +++ /dev/null @@ -1,24 +0,0 @@ -using ClaudeDo.Worker.Online.Interfaces; - -namespace ClaudeDo.Worker.Online; - -/// -/// Simple that returns a fixed token supplied at construction. -/// Used as the default DI registration until ZitadelAuthProvider is wired (Phase 2). -/// Also serves as the test double. -/// -public sealed class StaticTokenAuthProvider : IOnlineAuthProvider -{ - private readonly string? _token; - - public StaticTokenAuthProvider(string? token = null) - { - _token = token; - } - - public Task GetAccessTokenAsync(CancellationToken ct = default) - => Task.FromResult(_token); - - public Task GetAccessTokenAsync(bool forceRefresh, CancellationToken ct = default) - => Task.FromResult(_token); -} diff --git a/src/ClaudeDo.Worker/Planning/WindowsTerminalLauncher.cs b/src/ClaudeDo.Worker/Planning/WindowsTerminalLauncher.cs index bb8d388e..cb20f4aa 100644 --- a/src/ClaudeDo.Worker/Planning/WindowsTerminalLauncher.cs +++ b/src/ClaudeDo.Worker/Planning/WindowsTerminalLauncher.cs @@ -86,7 +86,7 @@ public sealed class WindowsTerminalLauncher : ITerminalLauncher return Task.CompletedTask; } - // The raw claude CLI args for a --resume launch, shared with InteractiveLaunchSpecService + // The raw claude CLI args for a --resume launch, shared with IInteractiveLaunchSpecService // (which needs the bare Exe/Args pair for a ConPTY host, not a wrapped pwsh command line). internal static IReadOnlyList BuildResumeArgs(string claudeSessionId) => new[] { "--resume", claudeSessionId }; @@ -99,7 +99,7 @@ public sealed class WindowsTerminalLauncher : ITerminalLauncher BuildPwshCommand(claudePath, BuildPlanningStartArgs(ctx)); // The raw claude CLI args for an interactive planning START, shared with the embedded-ConPTY - // planning path (InteractiveLaunchSpecService), which needs the bare Exe/Args pair rather than + // planning path (IInteractiveLaunchSpecService), which needs the bare Exe/Args pair rather than // a pwsh-wrapped command line. Arg order matters: variadic flags (--allowedTools, --add-dir) // come first; the single-line kickoff prompt is positional, so it must follow a single-value // flag (--append-system-prompt-file) or a variadic flag would swallow it. @@ -176,7 +176,7 @@ public sealed class WindowsTerminalLauncher : ITerminalLauncher ?? throw new TerminalLaunchException("Failed to start Windows Terminal process."); } - // Not private: reused by InteractiveLaunchSpecService to resolve the claude executable + // Not private: reused by IInteractiveLaunchSpecService to resolve the claude executable // for a ConPTY-hosted launch spec the same way a wt-launched terminal resolves it. internal static string? Resolve(string pathOrName) { diff --git a/src/ClaudeDo.Worker/Prime/Interfaces/IPrimeScheduleSignal.cs b/src/ClaudeDo.Worker/Prime/Interfaces/IPrimeScheduleSignal.cs deleted file mode 100644 index c633eb9b..00000000 --- a/src/ClaudeDo.Worker/Prime/Interfaces/IPrimeScheduleSignal.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace ClaudeDo.Worker.Prime; -public interface IPrimeScheduleSignal -{ - void Signal(); - CancellationToken CurrentToken { get; } -} diff --git a/src/ClaudeDo.Worker/Prime/PrimeScheduleSignal.cs b/src/ClaudeDo.Worker/Prime/PrimeScheduleSignal.cs index f7ab7e72..bb9f3753 100644 --- a/src/ClaudeDo.Worker/Prime/PrimeScheduleSignal.cs +++ b/src/ClaudeDo.Worker/Prime/PrimeScheduleSignal.cs @@ -1,6 +1,6 @@ namespace ClaudeDo.Worker.Prime; -public sealed class PrimeScheduleSignal : IPrimeScheduleSignal, IDisposable +public sealed class PrimeScheduleSignal : IDisposable { private CancellationTokenSource _cts = new(); private readonly object _lock = new(); diff --git a/src/ClaudeDo.Worker/Prime/PrimeScheduler.cs b/src/ClaudeDo.Worker/Prime/PrimeScheduler.cs index 0229c13a..6d63962c 100644 --- a/src/ClaudeDo.Worker/Prime/PrimeScheduler.cs +++ b/src/ClaudeDo.Worker/Prime/PrimeScheduler.cs @@ -21,7 +21,7 @@ public sealed class PrimeScheduler : BackgroundService private readonly IDbContextFactory _dbFactory; private readonly IPrimeRunner _runner; private readonly IPrimeClock _clock; - private readonly IPrimeScheduleSignal _signal; + private readonly PrimeScheduleSignal _signal; private readonly IPrimeBroadcaster _broadcaster; private readonly PrimeSchedulerOptions _options; private readonly ILogger _logger; @@ -30,7 +30,7 @@ public sealed class PrimeScheduler : BackgroundService IDbContextFactory dbFactory, IPrimeRunner runner, IPrimeClock clock, - IPrimeScheduleSignal signal, + PrimeScheduleSignal signal, IPrimeBroadcaster broadcaster, PrimeSchedulerOptions options, ILogger logger) diff --git a/src/ClaudeDo.Worker/Program.cs b/src/ClaudeDo.Worker/Program.cs index 43cb445a..db336c68 100644 --- a/src/ClaudeDo.Worker/Program.cs +++ b/src/ClaudeDo.Worker/Program.cs @@ -67,7 +67,7 @@ builder.Services.AddDbContextFactory(opt => builder.Services.AddSingleton(cfg); builder.Services.AddSingleton(); // Singleton so the always-on and in-task MCP servers share the same index write lock. -builder.Services.AddSingleton(); +builder.Services.AddSingleton(); builder.Services.AddHostedService(); builder.Services.AddHostedService(); builder.Services.AddHostedService(); @@ -140,7 +140,7 @@ builder.Services.AddSingleton(); builder.Services.AddSingleton(_ => new ClaudeHistoryReader(Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".claude", "projects"))); -builder.Services.AddSingleton(); +builder.Services.AddSingleton(); // Usage builder.Services.AddSingleton(); @@ -154,14 +154,13 @@ builder.Services.AddSingleton(); // Prime Claude builder.Services.AddSingleton(); builder.Services.AddSingleton(); -builder.Services.AddSingleton(sp => sp.GetRequiredService()); builder.Services.AddSingleton(); builder.Services.AddSingleton(PrimeSchedulerOptions.Default); builder.Services.AddSingleton(sp => sp.GetRequiredService()); builder.Services.AddHostedService(); // Refine -builder.Services.AddSingleton(); +builder.Services.AddSingleton(); builder.Services.AddSingleton(sp => sp.GetRequiredService()); // "Continue on session limit reset" toggle — depends on UsageState, registered below. @@ -199,7 +198,7 @@ builder.Services.AddScoped(sp => builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); -builder.Services.AddScoped(); +builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddMcpServer() .WithHttpTransport() @@ -339,8 +338,8 @@ if (cfg.ExternalMcpPort > 0) externalBuilder.Services.AddSingleton(app.Services.GetRequiredService()); externalBuilder.Services.AddScoped(); externalBuilder.Services.AddScoped(); - externalBuilder.Services.AddSingleton(app.Services.GetRequiredService()); - externalBuilder.Services.AddScoped(); + externalBuilder.Services.AddSingleton(app.Services.GetRequiredService()); + externalBuilder.Services.AddScoped(); externalBuilder.Services.AddScoped(); externalBuilder.Services.AddMcpServer() .WithHttpTransport() diff --git a/src/ClaudeDo.Worker/Refine/Interfaces/IRefineRunner.cs b/src/ClaudeDo.Worker/Refine/Interfaces/IRefineRunner.cs deleted file mode 100644 index 3f166eff..00000000 --- a/src/ClaudeDo.Worker/Refine/Interfaces/IRefineRunner.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace ClaudeDo.Worker.Refine; - -public interface IRefineRunner -{ - Task RefineAsync(string taskId, CancellationToken ct); -} - -public sealed record RefineRunOutcome(bool Success, string Message); diff --git a/src/ClaudeDo.Worker/Refine/RefineRunner.cs b/src/ClaudeDo.Worker/Refine/RefineRunner.cs index 14395321..560e59cd 100644 --- a/src/ClaudeDo.Worker/Refine/RefineRunner.cs +++ b/src/ClaudeDo.Worker/Refine/RefineRunner.cs @@ -7,7 +7,9 @@ using TaskStatus = ClaudeDo.Data.Models.TaskStatus; namespace ClaudeDo.Worker.Refine; -public sealed class RefineRunner : IRefineRunner +public sealed record RefineRunOutcome(bool Success, string Message); + +public sealed class RefineRunner { private static readonly TimeSpan RunTimeout = TimeSpan.FromMinutes(5); private const int MaxTurns = 5; diff --git a/src/ClaudeDo.Worker/Report/Interfaces/IWeekReportService.cs b/src/ClaudeDo.Worker/Report/Interfaces/IWeekReportService.cs deleted file mode 100644 index ebddd929..00000000 --- a/src/ClaudeDo.Worker/Report/Interfaces/IWeekReportService.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace ClaudeDo.Worker.Report.Interfaces; - -public interface IWeekReportService -{ - Task GetStoredAsync(DateOnly start, DateOnly end, CancellationToken ct = default); - Task GenerateAsync(DateOnly start, DateOnly end, CancellationToken ct = default); -} diff --git a/src/ClaudeDo.Worker/Report/WeekReportService.cs b/src/ClaudeDo.Worker/Report/WeekReportService.cs index da051956..77c41e5a 100644 --- a/src/ClaudeDo.Worker/Report/WeekReportService.cs +++ b/src/ClaudeDo.Worker/Report/WeekReportService.cs @@ -8,7 +8,7 @@ using Microsoft.Extensions.Logging; namespace ClaudeDo.Worker.Report; -public sealed class WeekReportService : IWeekReportService +public sealed class WeekReportService { private static readonly string[] DefaultExcludes = { @"C:\Private" }; private const string NoActivity = "_No activity in this period._"; diff --git a/src/ClaudeDo.Worker/Runner/TaskRunFindingsMcpTools.cs b/src/ClaudeDo.Worker/Runner/TaskRunFindingsMcpTools.cs index 5a4340e0..10f0c047 100644 --- a/src/ClaudeDo.Worker/Runner/TaskRunFindingsMcpTools.cs +++ b/src/ClaudeDo.Worker/Runner/TaskRunFindingsMcpTools.cs @@ -11,12 +11,12 @@ public sealed record SaveTaskFindingResult( [McpServerToolType] public sealed class TaskRunFindingsMcpTools { - private readonly IFindingsStore _store; - private readonly IFindingsStoreLocator _locator; + private readonly FindingsStore _store; + private readonly FindingsStoreLocator _locator; private readonly TaskRunMcpContextAccessor _ctx; public TaskRunFindingsMcpTools( - IFindingsStore store, IFindingsStoreLocator locator, TaskRunMcpContextAccessor ctx) + FindingsStore store, FindingsStoreLocator locator, TaskRunMcpContextAccessor ctx) { _store = store; _locator = locator; diff --git a/tests/ClaudeDo.Data.Tests/Filtering/MatchExpressionSqlTests.cs b/tests/ClaudeDo.Data.Tests/Filtering/MatchExpressionSqlTests.cs index 7120c10b..76f24ced 100644 --- a/tests/ClaudeDo.Data.Tests/Filtering/MatchExpressionSqlTests.cs +++ b/tests/ClaudeDo.Data.Tests/Filtering/MatchExpressionSqlTests.cs @@ -1,11 +1,11 @@ -using ClaudeDo.Data.Filtering.Filters; +using ClaudeDo.Data.Filtering; using Microsoft.EntityFrameworkCore; using TaskStatus = ClaudeDo.Data.Models.TaskStatus; namespace ClaudeDo.Data.Tests.Filtering; /// -/// Proves that is a real +/// Proves that is a real /// expression tree that EF Core can translate into SQL, not just an in-memory delegate — the /// whole point of splitting it out from Matches. /// diff --git a/tests/ClaudeDo.Data.Tests/Filtering/SmartFilterTests.cs b/tests/ClaudeDo.Data.Tests/Filtering/SmartFilterTests.cs index 22ddec93..ac74fb00 100644 --- a/tests/ClaudeDo.Data.Tests/Filtering/SmartFilterTests.cs +++ b/tests/ClaudeDo.Data.Tests/Filtering/SmartFilterTests.cs @@ -1,4 +1,4 @@ -using ClaudeDo.Data.Filtering.Filters; +using ClaudeDo.Data.Filtering; using TaskStatus = ClaudeDo.Data.Models.TaskStatus; namespace ClaudeDo.Data.Tests.Filtering; diff --git a/tests/ClaudeDo.Data.Tests/Filtering/TaskFactory.cs b/tests/ClaudeDo.Data.Tests/Filtering/TaskFactory.cs index 1b89b17c..4a0cf138 100644 --- a/tests/ClaudeDo.Data.Tests/Filtering/TaskFactory.cs +++ b/tests/ClaudeDo.Data.Tests/Filtering/TaskFactory.cs @@ -1,3 +1,4 @@ +using ClaudeDo.Data.Filtering; using ClaudeDo.Data.Models; using TaskStatus = ClaudeDo.Data.Models.TaskStatus; diff --git a/tests/ClaudeDo.Data.Tests/Filtering/TaskListFilterRegistryTests.cs b/tests/ClaudeDo.Data.Tests/Filtering/TaskListFilterRegistryTests.cs index c3b99581..e79e4550 100644 --- a/tests/ClaudeDo.Data.Tests/Filtering/TaskListFilterRegistryTests.cs +++ b/tests/ClaudeDo.Data.Tests/Filtering/TaskListFilterRegistryTests.cs @@ -1,5 +1,4 @@ using ClaudeDo.Data.Filtering; -using ClaudeDo.Data.Filtering.Filters; namespace ClaudeDo.Data.Tests.Filtering; diff --git a/tests/ClaudeDo.Data.Tests/Filtering/UserListFilterTests.cs b/tests/ClaudeDo.Data.Tests/Filtering/UserListFilterTests.cs index 3d72d71e..15aca28c 100644 --- a/tests/ClaudeDo.Data.Tests/Filtering/UserListFilterTests.cs +++ b/tests/ClaudeDo.Data.Tests/Filtering/UserListFilterTests.cs @@ -1,4 +1,4 @@ -using ClaudeDo.Data.Filtering.Filters; +using ClaudeDo.Data.Filtering; using TaskStatus = ClaudeDo.Data.Models.TaskStatus; namespace ClaudeDo.Data.Tests.Filtering; diff --git a/tests/ClaudeDo.Data.Tests/Filtering/VirtualFilterTests.cs b/tests/ClaudeDo.Data.Tests/Filtering/VirtualFilterTests.cs index 20d56bd7..4f37de61 100644 --- a/tests/ClaudeDo.Data.Tests/Filtering/VirtualFilterTests.cs +++ b/tests/ClaudeDo.Data.Tests/Filtering/VirtualFilterTests.cs @@ -1,4 +1,4 @@ -using ClaudeDo.Data.Filtering.Filters; +using ClaudeDo.Data.Filtering; using ClaudeDo.Data.Models; using TaskStatus = ClaudeDo.Data.Models.TaskStatus; diff --git a/tests/ClaudeDo.Worker.Tests/Infrastructure/StaticTokenAuthProvider.cs b/tests/ClaudeDo.Worker.Tests/Infrastructure/StaticTokenAuthProvider.cs new file mode 100644 index 00000000..73cab745 --- /dev/null +++ b/tests/ClaudeDo.Worker.Tests/Infrastructure/StaticTokenAuthProvider.cs @@ -0,0 +1,16 @@ +using ClaudeDo.Worker.Online.Interfaces; + +namespace ClaudeDo.Worker.Online; + +/// +/// Test double: an that returns a fixed token supplied at +/// construction. Production uses ZitadelAuthProvider. +/// +public sealed class StaticTokenAuthProvider(string? token = null) : IOnlineAuthProvider +{ + public Task GetAccessTokenAsync(CancellationToken ct = default) + => Task.FromResult(token); + + public Task GetAccessTokenAsync(bool forceRefresh, CancellationToken ct = default) + => Task.FromResult(token); +} diff --git a/tests/ClaudeDo.Worker.Tests/Online/StaticTokenAuthProviderTests.cs b/tests/ClaudeDo.Worker.Tests/Online/StaticTokenAuthProviderTests.cs deleted file mode 100644 index 47537d65..00000000 --- a/tests/ClaudeDo.Worker.Tests/Online/StaticTokenAuthProviderTests.cs +++ /dev/null @@ -1,30 +0,0 @@ -using ClaudeDo.Worker.Online; - -namespace ClaudeDo.Worker.Tests.Online; - -public sealed class StaticTokenAuthProviderTests -{ - [Fact] - public async Task WithToken_Returns_Token() - { - var provider = new StaticTokenAuthProvider("my-token"); - var result = await provider.GetAccessTokenAsync(); - Assert.Equal("my-token", result); - } - - [Fact] - public async Task WithNull_Returns_Null() - { - var provider = new StaticTokenAuthProvider(null); - var result = await provider.GetAccessTokenAsync(); - Assert.Null(result); - } - - [Fact] - public async Task Default_Returns_Null() - { - var provider = new StaticTokenAuthProvider(); - var result = await provider.GetAccessTokenAsync(); - Assert.Null(result); - } -}