feat(worker,ui): Online Inbox config + auth hub plumbing (Phase 2)
Hub: GetOnlineInboxState / SetOnlineInboxConfig / SetOnlineInboxAuth / ClearOnlineInboxAuth. WorkerConfig.SaveOnlineInbox persists only the online_inbox section. OnlineTokenStore + config registered always so hub methods work when sync is disabled. IWorkerClient surface + all test fakes synced. RedirectUri config (default http://localhost:8765/callback). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -98,4 +98,9 @@ public interface IWorkerClient : INotifyPropertyChanged
|
|||||||
Task<List<WorktreeOverviewDto>> GetWorktreesOverviewAsync(string? listId);
|
Task<List<WorktreeOverviewDto>> GetWorktreesOverviewAsync(string? listId);
|
||||||
Task<(bool Ok, string? Error)> SetWorktreeStateAsync(string taskId, WorktreeState newState);
|
Task<(bool Ok, string? Error)> SetWorktreeStateAsync(string taskId, WorktreeState newState);
|
||||||
Task<ForceRemoveResultDto?> ForceRemoveWorktreeAsync(string taskId);
|
Task<ForceRemoveResultDto?> ForceRemoveWorktreeAsync(string taskId);
|
||||||
|
|
||||||
|
Task<OnlineInboxStateDto?> GetOnlineInboxStateAsync();
|
||||||
|
Task SetOnlineInboxConfigAsync(OnlineInboxConfigInputDto input);
|
||||||
|
Task SetOnlineInboxAuthAsync(string refreshToken);
|
||||||
|
Task ClearOnlineInboxAuthAsync();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -504,6 +504,18 @@ public partial class WorkerClient : ObservableObject, IAsyncDisposable, IWorkerC
|
|||||||
await _hub.InvokeAsync("QueuePlanningSubtasksAsync", parentTaskId, ct);
|
await _hub.InvokeAsync("QueuePlanningSubtasksAsync", parentTaskId, ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Task<OnlineInboxStateDto?> GetOnlineInboxStateAsync()
|
||||||
|
=> TryInvokeAsync<OnlineInboxStateDto>("GetOnlineInboxState");
|
||||||
|
|
||||||
|
public async Task SetOnlineInboxConfigAsync(OnlineInboxConfigInputDto input)
|
||||||
|
=> await _hub.InvokeAsync("SetOnlineInboxConfig", input);
|
||||||
|
|
||||||
|
public async Task SetOnlineInboxAuthAsync(string refreshToken)
|
||||||
|
=> await _hub.InvokeAsync("SetOnlineInboxAuth", refreshToken);
|
||||||
|
|
||||||
|
public async Task ClearOnlineInboxAuthAsync()
|
||||||
|
=> await _hub.InvokeAsync("ClearOnlineInboxAuth");
|
||||||
|
|
||||||
// IWorkerClient explicit implementations (drop typed return values)
|
// IWorkerClient explicit implementations (drop typed return values)
|
||||||
async Task IWorkerClient.StartPlanningSessionAsync(string taskId, CancellationToken ct)
|
async Task IWorkerClient.StartPlanningSessionAsync(string taskId, CancellationToken ct)
|
||||||
=> await StartPlanningSessionAsync(taskId, ct);
|
=> await StartPlanningSessionAsync(taskId, ct);
|
||||||
@@ -568,3 +580,21 @@ public sealed record WorktreeOverviewDto(
|
|||||||
bool PathExistsOnDisk);
|
bool PathExistsOnDisk);
|
||||||
|
|
||||||
public sealed record ForceRemoveResultDto(bool Removed, string? Reason);
|
public sealed record ForceRemoveResultDto(bool Removed, string? Reason);
|
||||||
|
|
||||||
|
public sealed record OnlineInboxStateDto(
|
||||||
|
bool Enabled,
|
||||||
|
string ApiBaseUrl,
|
||||||
|
string Authority,
|
||||||
|
string ClientId,
|
||||||
|
string Scopes,
|
||||||
|
string RedirectUri,
|
||||||
|
bool SignedIn);
|
||||||
|
|
||||||
|
public sealed record OnlineInboxConfigInputDto(
|
||||||
|
bool Enabled,
|
||||||
|
string ApiBaseUrl,
|
||||||
|
int PollIntervalSeconds,
|
||||||
|
string Authority,
|
||||||
|
string ClientId,
|
||||||
|
string Scopes,
|
||||||
|
string RedirectUri);
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Nodes;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
using ClaudeDo.Data;
|
using ClaudeDo.Data;
|
||||||
using ClaudeDo.Worker.Online;
|
using ClaudeDo.Worker.Online;
|
||||||
@@ -74,9 +75,38 @@ public sealed class WorkerConfig
|
|||||||
return cfg;
|
return cfg;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Persists ONLY the <c>online_inbox</c> section back to <paramref name="path"/>
|
||||||
|
/// (defaults to <see cref="DefaultConfigPath"/>) without rewriting any other fields.
|
||||||
|
/// Reads the existing JSON, replaces the <c>online_inbox</c> node, and writes back indented.
|
||||||
|
/// </summary>
|
||||||
|
public void SaveOnlineInbox(string? path = null)
|
||||||
|
{
|
||||||
|
path ??= DefaultConfigPath;
|
||||||
|
|
||||||
|
var root = File.Exists(path)
|
||||||
|
? JsonNode.Parse(File.ReadAllText(path)) as JsonObject ?? new JsonObject()
|
||||||
|
: new JsonObject();
|
||||||
|
|
||||||
|
root["online_inbox"] = JsonSerializer.SerializeToNode(OnlineInbox, InboxSerializerOpts);
|
||||||
|
|
||||||
|
Directory.CreateDirectory(Path.GetDirectoryName(path)!);
|
||||||
|
File.WriteAllText(path, root.ToJsonString(WriteOpts));
|
||||||
|
}
|
||||||
|
|
||||||
private static readonly JsonSerializerOptions JsonOpts = new()
|
private static readonly JsonSerializerOptions JsonOpts = new()
|
||||||
{
|
{
|
||||||
ReadCommentHandling = JsonCommentHandling.Skip,
|
ReadCommentHandling = JsonCommentHandling.Skip,
|
||||||
AllowTrailingCommas = true,
|
AllowTrailingCommas = true,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private static readonly JsonSerializerOptions InboxSerializerOpts = new()
|
||||||
|
{
|
||||||
|
WriteIndented = false,
|
||||||
|
};
|
||||||
|
|
||||||
|
private static readonly JsonSerializerOptions WriteOpts = new()
|
||||||
|
{
|
||||||
|
WriteIndented = true,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,9 @@ using ClaudeDo.Data;
|
|||||||
using ClaudeDo.Data.Models;
|
using ClaudeDo.Data.Models;
|
||||||
using ClaudeDo.Data.Repositories;
|
using ClaudeDo.Data.Repositories;
|
||||||
using ClaudeDo.Worker.Agents;
|
using ClaudeDo.Worker.Agents;
|
||||||
|
using ClaudeDo.Worker.Config;
|
||||||
using ClaudeDo.Worker.Lifecycle;
|
using ClaudeDo.Worker.Lifecycle;
|
||||||
|
using ClaudeDo.Worker.Online;
|
||||||
using ClaudeDo.Worker.Planning;
|
using ClaudeDo.Worker.Planning;
|
||||||
using ClaudeDo.Worker.Prime;
|
using ClaudeDo.Worker.Prime;
|
||||||
using ClaudeDo.Worker.Queue;
|
using ClaudeDo.Worker.Queue;
|
||||||
@@ -65,6 +67,24 @@ public record UpdateTaskAgentSettingsDto(string TaskId, string? Model, string? S
|
|||||||
public record ListConfigDto(string? Model, string? SystemPrompt, string? AgentPath, int? MaxTurns = null);
|
public record ListConfigDto(string? Model, string? SystemPrompt, string? AgentPath, int? MaxTurns = null);
|
||||||
public record SeedResultDto(int Copied, int Skipped);
|
public record SeedResultDto(int Copied, int Skipped);
|
||||||
|
|
||||||
|
public record OnlineInboxStateDto(
|
||||||
|
bool Enabled,
|
||||||
|
string ApiBaseUrl,
|
||||||
|
string Authority,
|
||||||
|
string ClientId,
|
||||||
|
string Scopes,
|
||||||
|
string RedirectUri,
|
||||||
|
bool SignedIn);
|
||||||
|
|
||||||
|
public record OnlineInboxConfigInput(
|
||||||
|
bool Enabled,
|
||||||
|
string ApiBaseUrl,
|
||||||
|
int PollIntervalSeconds,
|
||||||
|
string Authority,
|
||||||
|
string ClientId,
|
||||||
|
string Scopes,
|
||||||
|
string RedirectUri);
|
||||||
|
|
||||||
public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
|
public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
|
||||||
{
|
{
|
||||||
private static readonly string Version =
|
private static readonly string Version =
|
||||||
@@ -89,6 +109,9 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
|
|||||||
private readonly ITaskStateService _state;
|
private readonly ITaskStateService _state;
|
||||||
private readonly IWeekReportService _report;
|
private readonly IWeekReportService _report;
|
||||||
private readonly IRefineRunner _refineRunner;
|
private readonly IRefineRunner _refineRunner;
|
||||||
|
private readonly WorkerConfig _cfg;
|
||||||
|
private readonly OnlineInboxConfig _onlineInboxConfig;
|
||||||
|
private readonly OnlineTokenStore _onlineTokenStore;
|
||||||
|
|
||||||
public WorkerHub(
|
public WorkerHub(
|
||||||
QueueService queue,
|
QueueService queue,
|
||||||
@@ -109,7 +132,10 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
|
|||||||
IPrimeRunner primeRunner,
|
IPrimeRunner primeRunner,
|
||||||
ITaskStateService state,
|
ITaskStateService state,
|
||||||
IWeekReportService report,
|
IWeekReportService report,
|
||||||
IRefineRunner refineRunner)
|
IRefineRunner refineRunner,
|
||||||
|
WorkerConfig cfg,
|
||||||
|
OnlineInboxConfig onlineInboxConfig,
|
||||||
|
OnlineTokenStore onlineTokenStore)
|
||||||
{
|
{
|
||||||
_queue = queue;
|
_queue = queue;
|
||||||
_waker = waker;
|
_waker = waker;
|
||||||
@@ -130,6 +156,9 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
|
|||||||
_state = state;
|
_state = state;
|
||||||
_report = report;
|
_report = report;
|
||||||
_refineRunner = refineRunner;
|
_refineRunner = refineRunner;
|
||||||
|
_cfg = cfg;
|
||||||
|
_onlineInboxConfig = onlineInboxConfig;
|
||||||
|
_onlineTokenStore = onlineTokenStore;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Maps the two exceptions service methods throw into client-facing HubExceptions:
|
// Maps the two exceptions service methods throw into client-facing HubExceptions:
|
||||||
@@ -684,4 +713,41 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
|
|||||||
|
|
||||||
return ids.Count;
|
return ids.Count;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#pragma warning disable CA1416 // ClaudeDo.Worker is Windows-only; DPAPI calls are safe here.
|
||||||
|
public OnlineInboxStateDto GetOnlineInboxState()
|
||||||
|
{
|
||||||
|
var signedIn = _onlineTokenStore.Read() is not null;
|
||||||
|
return new OnlineInboxStateDto(
|
||||||
|
_onlineInboxConfig.Enabled,
|
||||||
|
_onlineInboxConfig.ApiBaseUrl,
|
||||||
|
_onlineInboxConfig.Zitadel.Authority,
|
||||||
|
_onlineInboxConfig.Zitadel.ClientId,
|
||||||
|
_onlineInboxConfig.Zitadel.Scopes,
|
||||||
|
_onlineInboxConfig.RedirectUri,
|
||||||
|
signedIn);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetOnlineInboxConfig(OnlineInboxConfigInput input)
|
||||||
|
{
|
||||||
|
_onlineInboxConfig.Enabled = input.Enabled;
|
||||||
|
_onlineInboxConfig.ApiBaseUrl = input.ApiBaseUrl ?? "";
|
||||||
|
_onlineInboxConfig.PollIntervalSeconds = input.PollIntervalSeconds;
|
||||||
|
_onlineInboxConfig.RedirectUri = input.RedirectUri ?? "http://localhost:8765/callback";
|
||||||
|
_onlineInboxConfig.Zitadel.Authority = input.Authority ?? "";
|
||||||
|
_onlineInboxConfig.Zitadel.ClientId = input.ClientId ?? "";
|
||||||
|
_onlineInboxConfig.Zitadel.Scopes = input.Scopes ?? "openid offline_access";
|
||||||
|
_cfg.SaveOnlineInbox();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetOnlineInboxAuth(string refreshToken)
|
||||||
|
{
|
||||||
|
_onlineTokenStore.Save(refreshToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ClearOnlineInboxAuth()
|
||||||
|
{
|
||||||
|
_onlineTokenStore.Clear();
|
||||||
|
}
|
||||||
|
#pragma warning restore CA1416
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,9 @@ public sealed class OnlineInboxConfig
|
|||||||
[JsonPropertyName("poll_interval_seconds")]
|
[JsonPropertyName("poll_interval_seconds")]
|
||||||
public int PollIntervalSeconds { get; set; } = 60;
|
public int PollIntervalSeconds { get; set; } = 60;
|
||||||
|
|
||||||
|
[JsonPropertyName("redirect_uri")]
|
||||||
|
public string RedirectUri { get; set; } = "http://localhost:8765/callback";
|
||||||
|
|
||||||
[JsonPropertyName("zitadel")]
|
[JsonPropertyName("zitadel")]
|
||||||
public ZitadelClientConfig Zitadel { get; set; } = new();
|
public ZitadelClientConfig Zitadel { get; set; } = new();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -151,14 +151,19 @@ builder.Services.AddMcpServer()
|
|||||||
.WithTools<PlanningMcpService>()
|
.WithTools<PlanningMcpService>()
|
||||||
.WithTools<TaskRunMcpService>();
|
.WithTools<TaskRunMcpService>();
|
||||||
|
|
||||||
// Online Inbox — registered only when enabled.
|
// OnlineInboxConfig and OnlineTokenStore are always registered so hub methods work
|
||||||
|
// even when sync is disabled. The sync stack (api client, auth, hosted service) is
|
||||||
|
// only registered when enabled.
|
||||||
|
builder.Services.AddSingleton(cfg.OnlineInbox);
|
||||||
|
#pragma warning disable CA1416 // ClaudeDo.Worker is Windows-only; DPAPI is fine here.
|
||||||
|
builder.Services.AddSingleton<OnlineTokenStore>();
|
||||||
|
#pragma warning restore CA1416
|
||||||
|
|
||||||
if (cfg.OnlineInbox.Enabled)
|
if (cfg.OnlineInbox.Enabled)
|
||||||
{
|
{
|
||||||
OnlineInboxApiClient.ValidateBaseUrl(cfg.OnlineInbox.ApiBaseUrl);
|
OnlineInboxApiClient.ValidateBaseUrl(cfg.OnlineInbox.ApiBaseUrl);
|
||||||
builder.Services.AddSingleton(cfg.OnlineInbox);
|
|
||||||
builder.Services.AddHttpClient();
|
builder.Services.AddHttpClient();
|
||||||
#pragma warning disable CA1416 // ClaudeDo.Worker is Windows-only; DPAPI is fine here.
|
#pragma warning disable CA1416
|
||||||
builder.Services.AddSingleton<OnlineTokenStore>();
|
|
||||||
builder.Services.AddSingleton<IOnlineAuthProvider, ZitadelAuthProvider>();
|
builder.Services.AddSingleton<IOnlineAuthProvider, ZitadelAuthProvider>();
|
||||||
#pragma warning restore CA1416
|
#pragma warning restore CA1416
|
||||||
builder.Services.AddHttpClient<IOnlineInboxApi, OnlineInboxApiClient>(client =>
|
builder.Services.AddHttpClient<IOnlineInboxApi, OnlineInboxApiClient>(client =>
|
||||||
|
|||||||
@@ -108,5 +108,10 @@ public abstract class StubWorkerClient : IWorkerClient
|
|||||||
public virtual Task<string> GetLastPrepLogAsync() => Task.FromResult(LastPrepLog);
|
public virtual Task<string> GetLastPrepLogAsync() => Task.FromResult(LastPrepLog);
|
||||||
public virtual Task RefineTaskAsync(string taskId) => Task.CompletedTask;
|
public virtual Task RefineTaskAsync(string taskId) => Task.CompletedTask;
|
||||||
|
|
||||||
|
public virtual Task<OnlineInboxStateDto?> GetOnlineInboxStateAsync() => Task.FromResult<OnlineInboxStateDto?>(null);
|
||||||
|
public virtual Task SetOnlineInboxConfigAsync(OnlineInboxConfigInputDto input) => Task.CompletedTask;
|
||||||
|
public virtual Task SetOnlineInboxAuthAsync(string refreshToken) => Task.CompletedTask;
|
||||||
|
public virtual Task ClearOnlineInboxAuthAsync() => Task.CompletedTask;
|
||||||
|
|
||||||
protected void RaisePropertyChanged(string name) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
|
protected void RaisePropertyChanged(string name) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,8 @@ public sealed class ClearMyDayHubTests : IDisposable
|
|||||||
var broadcaster = new HubBroadcaster(new CapturingHubContext());
|
var broadcaster = new HubBroadcaster(new CapturingHubContext());
|
||||||
var hub = new WorkerHub(
|
var hub = new WorkerHub(
|
||||||
null!, null!, null!, null!, broadcaster, _db.CreateFactory(),
|
null!, null!, null!, null!, broadcaster, _db.CreateFactory(),
|
||||||
null!, null!, null!, null!, null!, null!, null!, 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.Online.OnlineTokenStore());
|
||||||
hub.Clients = new FakeHubCallerClients(new RecordingClientProxy());
|
hub.Clients = new FakeHubCallerClients(new RecordingClientProxy());
|
||||||
hub.Context = new FakeHubCallerContext();
|
hub.Context = new FakeHubCallerContext();
|
||||||
return hub;
|
return hub;
|
||||||
|
|||||||
174
tests/ClaudeDo.Worker.Tests/Hub/OnlineInboxHubTests.cs
Normal file
174
tests/ClaudeDo.Worker.Tests/Hub/OnlineInboxHubTests.cs
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
using ClaudeDo.Worker.Config;
|
||||||
|
using ClaudeDo.Worker.Hub;
|
||||||
|
using ClaudeDo.Worker.Online;
|
||||||
|
using ClaudeDo.Worker.Tests.Infrastructure;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Nodes;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace ClaudeDo.Worker.Tests.Hub;
|
||||||
|
|
||||||
|
public sealed class OnlineInboxHubTests : IDisposable
|
||||||
|
{
|
||||||
|
private readonly string _configPath = Path.Combine(
|
||||||
|
Path.GetTempPath(), $"online_inbox_hub_{Guid.NewGuid():N}.json");
|
||||||
|
private readonly string _tokenPath = Path.Combine(
|
||||||
|
Path.GetTempPath(), $"online_inbox_hub_token_{Guid.NewGuid():N}.bin");
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
try { File.Delete(_configPath); } catch { }
|
||||||
|
try { File.Delete(_tokenPath); } catch { }
|
||||||
|
}
|
||||||
|
|
||||||
|
private (WorkerHub hub, OnlineInboxConfig inboxCfg, OnlineTokenStore store) CreateHub(
|
||||||
|
OnlineInboxConfig? inboxCfg = null, OnlineTokenStore? store = null)
|
||||||
|
{
|
||||||
|
var cfg = new WorkerConfig();
|
||||||
|
inboxCfg ??= cfg.OnlineInbox;
|
||||||
|
store ??= new OnlineTokenStore(_tokenPath);
|
||||||
|
var broadcaster = new HubBroadcaster(new CapturingHubContext());
|
||||||
|
var hub = new WorkerHub(
|
||||||
|
null!, null!, null!, null!, broadcaster, null!,
|
||||||
|
null!, null!, null!, null!, null!, null!, null!, null!, null!, null!, null!, null!, null!,
|
||||||
|
cfg, inboxCfg, store);
|
||||||
|
hub.Clients = new FakeHubCallerClients(new RecordingClientProxy());
|
||||||
|
hub.Context = new FakeHubCallerContext();
|
||||||
|
return (hub, inboxCfg, store);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetOnlineInboxState_ReflectsConfigAndNoToken()
|
||||||
|
{
|
||||||
|
var (hub, _, _) = CreateHub();
|
||||||
|
|
||||||
|
var state = hub.GetOnlineInboxState();
|
||||||
|
|
||||||
|
Assert.False(state.Enabled);
|
||||||
|
Assert.False(state.SignedIn);
|
||||||
|
Assert.Equal("", state.ApiBaseUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetOnlineInboxState_SignedIn_WhenTokenPresent()
|
||||||
|
{
|
||||||
|
if (!OperatingSystem.IsWindows()) return; // DPAPI is Windows-only
|
||||||
|
|
||||||
|
var (hub, _, store) = CreateHub();
|
||||||
|
store.Save("my-refresh-token");
|
||||||
|
|
||||||
|
var state = hub.GetOnlineInboxState();
|
||||||
|
|
||||||
|
Assert.True(state.SignedIn);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetOnlineInboxState_ReflectsConfigFields()
|
||||||
|
{
|
||||||
|
var inboxCfg = new OnlineInboxConfig
|
||||||
|
{
|
||||||
|
Enabled = true,
|
||||||
|
ApiBaseUrl = "https://inbox.example.com",
|
||||||
|
RedirectUri = "http://localhost:9999/callback",
|
||||||
|
Zitadel = new ZitadelClientConfig
|
||||||
|
{
|
||||||
|
Authority = "https://auth.example.com",
|
||||||
|
ClientId = "client-abc",
|
||||||
|
Scopes = "openid offline_access",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
var (hub, _, _) = CreateHub(inboxCfg);
|
||||||
|
|
||||||
|
var state = hub.GetOnlineInboxState();
|
||||||
|
|
||||||
|
Assert.True(state.Enabled);
|
||||||
|
Assert.Equal("https://inbox.example.com", state.ApiBaseUrl);
|
||||||
|
Assert.Equal("https://auth.example.com", state.Authority);
|
||||||
|
Assert.Equal("client-abc", state.ClientId);
|
||||||
|
Assert.Equal("openid offline_access", state.Scopes);
|
||||||
|
Assert.Equal("http://localhost:9999/callback", state.RedirectUri);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SetOnlineInboxConfig_UpdatesConfigAndPersistsToFile()
|
||||||
|
{
|
||||||
|
var cfg = new WorkerConfig();
|
||||||
|
// point SaveOnlineInbox to our temp file
|
||||||
|
var inboxCfg = cfg.OnlineInbox;
|
||||||
|
var store = new OnlineTokenStore(_tokenPath);
|
||||||
|
var broadcaster = new HubBroadcaster(new CapturingHubContext());
|
||||||
|
|
||||||
|
// Patch cfg to save to our temp path by writing an initial file there
|
||||||
|
File.WriteAllText(_configPath, "{}");
|
||||||
|
|
||||||
|
// We need to use the internal SaveOnlineInbox with our path.
|
||||||
|
// WorkerHub calls _cfg.SaveOnlineInbox() which uses DefaultConfigPath.
|
||||||
|
// For this test, directly exercise the round-trip via WorkerConfig.SaveOnlineInbox.
|
||||||
|
cfg.OnlineInbox.Enabled = true;
|
||||||
|
cfg.OnlineInbox.ApiBaseUrl = "https://inbox.test.com";
|
||||||
|
cfg.OnlineInbox.PollIntervalSeconds = 120;
|
||||||
|
cfg.OnlineInbox.RedirectUri = "http://localhost:7777/cb";
|
||||||
|
cfg.OnlineInbox.Zitadel.Authority = "https://auth.test.com";
|
||||||
|
cfg.OnlineInbox.Zitadel.ClientId = "test-client";
|
||||||
|
cfg.OnlineInbox.Zitadel.Scopes = "openid offline_access";
|
||||||
|
|
||||||
|
cfg.SaveOnlineInbox(_configPath);
|
||||||
|
|
||||||
|
// Verify file contains only online_inbox node and preserves others
|
||||||
|
var json = File.ReadAllText(_configPath);
|
||||||
|
var root = JsonNode.Parse(json)!.AsObject();
|
||||||
|
|
||||||
|
Assert.NotNull(root["online_inbox"]);
|
||||||
|
var inbox = root["online_inbox"]!.AsObject();
|
||||||
|
Assert.True(inbox["enabled"]!.GetValue<bool>());
|
||||||
|
Assert.Equal("https://inbox.test.com", inbox["api_base_url"]!.GetValue<string>());
|
||||||
|
Assert.Equal(120, inbox["poll_interval_seconds"]!.GetValue<int>());
|
||||||
|
Assert.Equal("http://localhost:7777/cb", inbox["redirect_uri"]!.GetValue<string>());
|
||||||
|
|
||||||
|
// Verify existing file keys are preserved (not overwritten)
|
||||||
|
var roundTripped = WorkerConfig.Load(_configPath);
|
||||||
|
Assert.Equal("https://inbox.test.com", roundTripped.OnlineInbox.ApiBaseUrl);
|
||||||
|
Assert.Equal(true, roundTripped.OnlineInbox.Enabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SetOnlineInboxConfig_PreservesOtherConfigKeys()
|
||||||
|
{
|
||||||
|
// Write a config with a non-inbox key
|
||||||
|
var initial = """{"signalr_port":12345}""";
|
||||||
|
File.WriteAllText(_configPath, initial);
|
||||||
|
|
||||||
|
var cfg = new WorkerConfig();
|
||||||
|
cfg.OnlineInbox.ApiBaseUrl = "https://new.example.com";
|
||||||
|
cfg.SaveOnlineInbox(_configPath);
|
||||||
|
|
||||||
|
var root = JsonNode.Parse(File.ReadAllText(_configPath))!.AsObject();
|
||||||
|
Assert.Equal(12345, root["signalr_port"]!.GetValue<int>());
|
||||||
|
Assert.Equal("https://new.example.com",
|
||||||
|
root["online_inbox"]!["api_base_url"]!.GetValue<string>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SetOnlineInboxAuth_WritesToken()
|
||||||
|
{
|
||||||
|
if (!OperatingSystem.IsWindows()) return;
|
||||||
|
|
||||||
|
var (hub, _, store) = CreateHub();
|
||||||
|
hub.SetOnlineInboxAuth("refresh-abc");
|
||||||
|
|
||||||
|
Assert.Equal("refresh-abc", store.Read());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ClearOnlineInboxAuth_RemovesToken()
|
||||||
|
{
|
||||||
|
if (!OperatingSystem.IsWindows()) return;
|
||||||
|
|
||||||
|
var (hub, _, store) = CreateHub();
|
||||||
|
store.Save("some-token");
|
||||||
|
|
||||||
|
hub.ClearOnlineInboxAuth();
|
||||||
|
|
||||||
|
Assert.Null(store.Read());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -55,7 +55,8 @@ public sealed class PlanningHubTests : IDisposable
|
|||||||
{
|
{
|
||||||
var hub = new WorkerHub(
|
var hub = new WorkerHub(
|
||||||
null!, null!, null!, null!, null!, null!, null!, null!, null!,
|
null!, null!, null!, null!, null!, null!, null!, null!, null!,
|
||||||
_planning, _launcher, null!, null!, null!, null!, null!, null!, null!, null!);
|
_planning, _launcher, null!, null!, null!, null!, null!, null!, null!, null!,
|
||||||
|
null!, new ClaudeDo.Worker.Online.OnlineInboxConfig(), new ClaudeDo.Worker.Online.OnlineTokenStore());
|
||||||
hub.Clients = new FakeHubCallerClients(_proxy);
|
hub.Clients = new FakeHubCallerClients(_proxy);
|
||||||
hub.Context = new FakeHubCallerContext();
|
hub.Context = new FakeHubCallerContext();
|
||||||
return hub;
|
return hub;
|
||||||
|
|||||||
@@ -19,7 +19,8 @@ public sealed class WorktreeStateHubTests : IDisposable
|
|||||||
var broadcaster = new HubBroadcaster(new CapturingHubContext());
|
var broadcaster = new HubBroadcaster(new CapturingHubContext());
|
||||||
var hub = new WorkerHub(
|
var hub = new WorkerHub(
|
||||||
null!, null!, null!, null!, broadcaster, _db.CreateFactory(),
|
null!, null!, null!, null!, broadcaster, _db.CreateFactory(),
|
||||||
null!, null!, null!, null!, null!, null!, null!, 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.Online.OnlineTokenStore());
|
||||||
hub.Clients = new FakeHubCallerClients(new RecordingClientProxy());
|
hub.Clients = new FakeHubCallerClients(new RecordingClientProxy());
|
||||||
hub.Context = new FakeHubCallerContext();
|
hub.Context = new FakeHubCallerContext();
|
||||||
return hub;
|
return hub;
|
||||||
|
|||||||
@@ -112,6 +112,10 @@ sealed class FakeWorkerClient : IWorkerClient
|
|||||||
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 RefineTaskAsync(string taskId) => Task.CompletedTask;
|
public Task RefineTaskAsync(string taskId) => Task.CompletedTask;
|
||||||
|
public Task<OnlineInboxStateDto?> GetOnlineInboxStateAsync() => Task.FromResult<OnlineInboxStateDto?>(null);
|
||||||
|
public Task SetOnlineInboxConfigAsync(OnlineInboxConfigInputDto input) => Task.CompletedTask;
|
||||||
|
public Task SetOnlineInboxAuthAsync(string refreshToken) => Task.CompletedTask;
|
||||||
|
public Task ClearOnlineInboxAuthAsync() => Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Helper to build VM with pre-seeded Items ──────────────────────────────────
|
// ── Helper to build VM with pre-seeded Items ──────────────────────────────────
|
||||||
|
|||||||
Reference in New Issue
Block a user