feat(worker): session skills SignalR surface + per-level persistence
This commit is contained in:
@@ -64,6 +64,7 @@ public sealed class AppSettingsRepository
|
|||||||
? null : updated.ReportExcludedPaths;
|
? null : updated.ReportExcludedPaths;
|
||||||
row.StandupWeekday = updated.StandupWeekday;
|
row.StandupWeekday = updated.StandupWeekday;
|
||||||
row.DailyPrepMaxTasks = updated.DailyPrepMaxTasks < 1 ? 1 : updated.DailyPrepMaxTasks;
|
row.DailyPrepMaxTasks = updated.DailyPrepMaxTasks < 1 ? 1 : updated.DailyPrepMaxTasks;
|
||||||
|
row.SessionSkills = string.IsNullOrWhiteSpace(updated.SessionSkills) ? null : updated.SessionSkills;
|
||||||
|
|
||||||
await _context.SaveChangesAsync(ct);
|
await _context.SaveChangesAsync(ct);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -77,6 +77,7 @@ public sealed class ListRepository
|
|||||||
existing.SystemPrompt = config.SystemPrompt;
|
existing.SystemPrompt = config.SystemPrompt;
|
||||||
existing.AgentPath = config.AgentPath;
|
existing.AgentPath = config.AgentPath;
|
||||||
existing.MaxTurns = config.MaxTurns;
|
existing.MaxTurns = config.MaxTurns;
|
||||||
|
existing.SessionSkills = config.SessionSkills;
|
||||||
}
|
}
|
||||||
await _context.SaveChangesAsync(ct);
|
await _context.SaveChangesAsync(ct);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -189,6 +189,7 @@ public sealed class TaskRepository
|
|||||||
string? systemPrompt,
|
string? systemPrompt,
|
||||||
string? agentPath,
|
string? agentPath,
|
||||||
int? maxTurns = null,
|
int? maxTurns = null,
|
||||||
|
string? sessionSkills = null,
|
||||||
CancellationToken ct = default)
|
CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
await _context.Tasks
|
await _context.Tasks
|
||||||
@@ -197,7 +198,8 @@ public sealed class TaskRepository
|
|||||||
.SetProperty(t => t.Model, model)
|
.SetProperty(t => t.Model, model)
|
||||||
.SetProperty(t => t.SystemPrompt, systemPrompt)
|
.SetProperty(t => t.SystemPrompt, systemPrompt)
|
||||||
.SetProperty(t => t.AgentPath, agentPath)
|
.SetProperty(t => t.AgentPath, agentPath)
|
||||||
.SetProperty(t => t.MaxTurns, maxTurns), ct);
|
.SetProperty(t => t.MaxTurns, maxTurns)
|
||||||
|
.SetProperty(t => t.SessionSkills, sessionSkills), ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|||||||
@@ -64,6 +64,10 @@ public interface IWorkerClient : INotifyPropertyChanged
|
|||||||
Task<SeedResultDto?> RestoreDefaultAgentsAsync();
|
Task<SeedResultDto?> RestoreDefaultAgentsAsync();
|
||||||
Task<ListConfigDto?> GetListConfigAsync(string listId);
|
Task<ListConfigDto?> GetListConfigAsync(string listId);
|
||||||
Task UpdateTaskAgentSettingsAsync(UpdateTaskAgentSettingsDto dto);
|
Task UpdateTaskAgentSettingsAsync(UpdateTaskAgentSettingsDto dto);
|
||||||
|
Task<List<SessionSkillDto>> GetSessionSkillsAsync();
|
||||||
|
Task<List<string>> InstallSessionSkillAsync(string url);
|
||||||
|
Task UpdateSessionSkillAsync(string sourceUrl);
|
||||||
|
Task RemoveSessionSkillAsync(string sourceUrl);
|
||||||
Task SetTaskStatusAsync(string taskId, TaskStatus status);
|
Task SetTaskStatusAsync(string taskId, TaskStatus status);
|
||||||
Task<MergeResultDto?> ApproveReviewAsync(string taskId, string targetBranch);
|
Task<MergeResultDto?> ApproveReviewAsync(string taskId, string targetBranch);
|
||||||
Task<MergePreviewDto?> PreviewMergeAsync(string taskId, string targetBranch);
|
Task<MergePreviewDto?> PreviewMergeAsync(string taskId, string targetBranch);
|
||||||
|
|||||||
@@ -481,6 +481,18 @@ public partial class WorkerClient : ObservableObject, IAsyncDisposable, IWorkerC
|
|||||||
await _hub.InvokeAsync("UpdateTaskAgentSettings", dto);
|
await _hub.InvokeAsync("UpdateTaskAgentSettings", dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<List<SessionSkillDto>> GetSessionSkillsAsync()
|
||||||
|
=> await TryInvokeAsync<List<SessionSkillDto>>("GetSessionSkills") ?? [];
|
||||||
|
|
||||||
|
public Task<List<string>> InstallSessionSkillAsync(string url)
|
||||||
|
=> _hub.InvokeAsync<List<string>>("InstallSessionSkill", url);
|
||||||
|
|
||||||
|
public Task UpdateSessionSkillAsync(string sourceUrl)
|
||||||
|
=> _hub.InvokeAsync("UpdateSessionSkill", sourceUrl);
|
||||||
|
|
||||||
|
public Task RemoveSessionSkillAsync(string sourceUrl)
|
||||||
|
=> _hub.InvokeAsync("RemoveSessionSkill", sourceUrl);
|
||||||
|
|
||||||
public async Task SetTaskStatusAsync(string taskId, ClaudeDo.Data.Models.TaskStatus status)
|
public async Task SetTaskStatusAsync(string taskId, ClaudeDo.Data.Models.TaskStatus status)
|
||||||
{
|
{
|
||||||
await _hub.InvokeAsync("SetTaskStatus", taskId, status.ToString());
|
await _hub.InvokeAsync("SetTaskStatus", taskId, status.ToString());
|
||||||
@@ -627,7 +639,15 @@ public sealed record AppSettingsDto(
|
|||||||
int WorktreeAutoCleanupDays,
|
int WorktreeAutoCleanupDays,
|
||||||
string? ReportExcludedPaths,
|
string? ReportExcludedPaths,
|
||||||
int StandupWeekday,
|
int StandupWeekday,
|
||||||
int DailyPrepMaxTasks);
|
int DailyPrepMaxTasks,
|
||||||
|
List<string>? SessionSkills = null);
|
||||||
|
|
||||||
|
public sealed record SessionSkillDto(
|
||||||
|
string Name,
|
||||||
|
string Description,
|
||||||
|
string SourceUrl,
|
||||||
|
string PinnedRef,
|
||||||
|
DateTimeOffset AddedAt);
|
||||||
|
|
||||||
public sealed record WorktreeCleanupDto(int Removed);
|
public sealed record WorktreeCleanupDto(int Removed);
|
||||||
public sealed record WorktreeResetDto(int Removed, int TasksAffected, bool Blocked, int RunningTasks);
|
public sealed record WorktreeResetDto(int Removed, int TasksAffected, bool Blocked, int RunningTasks);
|
||||||
@@ -638,9 +658,9 @@ public record MergeConflictDocumentsDto(string TaskId, IReadOnlyList<ConflictDoc
|
|||||||
public record ConflictDocumentDto(string Path, bool IsBinary, IReadOnlyList<MergeSegmentDto> Segments);
|
public record ConflictDocumentDto(string Path, bool IsBinary, IReadOnlyList<MergeSegmentDto> Segments);
|
||||||
public record MergeSegmentDto(bool IsConflict, string Text, string Ours, string? Base, string Theirs);
|
public record MergeSegmentDto(bool IsConflict, string Text, string Ours, string? Base, string Theirs);
|
||||||
public sealed record UpdateListDto(string Id, string Name, string? WorkingDir, string DefaultCommitType);
|
public sealed record UpdateListDto(string Id, string Name, string? WorkingDir, string DefaultCommitType);
|
||||||
public sealed record UpdateListConfigDto(string ListId, string? Model, string? SystemPrompt, string? AgentPath, int? MaxTurns = null);
|
public sealed record UpdateListConfigDto(string ListId, string? Model, string? SystemPrompt, string? AgentPath, int? MaxTurns = null, List<string>? SessionSkills = null);
|
||||||
public sealed record UpdateTaskAgentSettingsDto(string TaskId, string? Model, string? SystemPrompt, string? AgentPath, int? MaxTurns = null);
|
public sealed record UpdateTaskAgentSettingsDto(string TaskId, string? Model, string? SystemPrompt, string? AgentPath, int? MaxTurns = null, List<string>? SessionSkills = null);
|
||||||
public sealed record ListConfigDto(string? Model, string? SystemPrompt, string? AgentPath, int? MaxTurns = null);
|
public sealed record ListConfigDto(string? Model, string? SystemPrompt, string? AgentPath, int? MaxTurns = null, List<string>? SessionSkills = null);
|
||||||
public sealed record SeedResultDto(int Copied, int Skipped);
|
public sealed record SeedResultDto(int Copied, int Skipped);
|
||||||
|
|
||||||
public sealed record WorktreeOverviewDto(
|
public sealed record WorktreeOverviewDto(
|
||||||
|
|||||||
+1
-1
@@ -58,7 +58,7 @@ public sealed class ConfigMcpTools
|
|||||||
_ = await _tasks.GetByIdAsync(taskId, cancellationToken)
|
_ = await _tasks.GetByIdAsync(taskId, cancellationToken)
|
||||||
?? throw new InvalidOperationException($"Task {taskId} not found.");
|
?? throw new InvalidOperationException($"Task {taskId} not found.");
|
||||||
|
|
||||||
await _tasks.UpdateAgentSettingsAsync(taskId, model.NullIfBlank(), systemPrompt.NullIfBlank(), agentPath.NullIfBlank(), maxTurns, cancellationToken);
|
await _tasks.UpdateAgentSettingsAsync(taskId, model.NullIfBlank(), systemPrompt.NullIfBlank(), agentPath.NullIfBlank(), maxTurns, ct: cancellationToken);
|
||||||
await _broadcaster.TaskUpdated(taskId);
|
await _broadcaster.TaskUpdated(taskId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,8 +15,10 @@ using ClaudeDo.Worker.Queue;
|
|||||||
using ClaudeDo.Worker.Refine;
|
using ClaudeDo.Worker.Refine;
|
||||||
using ClaudeDo.Worker.Report;
|
using ClaudeDo.Worker.Report;
|
||||||
using ClaudeDo.Worker.Report.Interfaces;
|
using ClaudeDo.Worker.Report.Interfaces;
|
||||||
|
using ClaudeDo.Worker.Skills;
|
||||||
using ClaudeDo.Worker.State;
|
using ClaudeDo.Worker.State;
|
||||||
using ClaudeDo.Worker.Worktrees;
|
using ClaudeDo.Worker.Worktrees;
|
||||||
|
using System.Text.Json;
|
||||||
using TaskStatus = ClaudeDo.Data.Models.TaskStatus;
|
using TaskStatus = ClaudeDo.Data.Models.TaskStatus;
|
||||||
using Microsoft.AspNetCore.SignalR;
|
using Microsoft.AspNetCore.SignalR;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
@@ -37,7 +39,15 @@ public record AppSettingsDto(
|
|||||||
int WorktreeAutoCleanupDays,
|
int WorktreeAutoCleanupDays,
|
||||||
string? ReportExcludedPaths,
|
string? ReportExcludedPaths,
|
||||||
int StandupWeekday,
|
int StandupWeekday,
|
||||||
int DailyPrepMaxTasks);
|
int DailyPrepMaxTasks,
|
||||||
|
List<string>? SessionSkills = null);
|
||||||
|
|
||||||
|
public record SessionSkillDto(
|
||||||
|
string Name,
|
||||||
|
string Description,
|
||||||
|
string SourceUrl,
|
||||||
|
string PinnedRef,
|
||||||
|
DateTimeOffset AddedAt);
|
||||||
|
|
||||||
public record WorktreeCleanupDto(int Removed);
|
public record WorktreeCleanupDto(int Removed);
|
||||||
public record WorktreeResetDto(int Removed, int TasksAffected, bool Blocked, int RunningTasks);
|
public record WorktreeResetDto(int Removed, int TasksAffected, bool Blocked, int RunningTasks);
|
||||||
@@ -65,9 +75,9 @@ public record MergeConflictDocumentsDto(string TaskId, IReadOnlyList<ConflictDoc
|
|||||||
public record ConflictDocumentDto(string Path, bool IsBinary, IReadOnlyList<MergeSegmentDto> Segments);
|
public record ConflictDocumentDto(string Path, bool IsBinary, IReadOnlyList<MergeSegmentDto> Segments);
|
||||||
public record MergeSegmentDto(bool IsConflict, string Text, string Ours, string? Base, string Theirs);
|
public record MergeSegmentDto(bool IsConflict, string Text, string Ours, string? Base, string Theirs);
|
||||||
public record UpdateListDto(string Id, string Name, string? WorkingDir, string DefaultCommitType);
|
public record UpdateListDto(string Id, string Name, string? WorkingDir, string DefaultCommitType);
|
||||||
public record UpdateListConfigDto(string ListId, string? Model, string? SystemPrompt, string? AgentPath, int? MaxTurns = null);
|
public record UpdateListConfigDto(string ListId, string? Model, string? SystemPrompt, string? AgentPath, int? MaxTurns = null, List<string>? SessionSkills = null);
|
||||||
public record UpdateTaskAgentSettingsDto(string TaskId, string? Model, string? SystemPrompt, string? AgentPath, int? MaxTurns = null);
|
public record UpdateTaskAgentSettingsDto(string TaskId, string? Model, string? SystemPrompt, string? AgentPath, int? MaxTurns = null, List<string>? SessionSkills = null);
|
||||||
public record ListConfigDto(string? Model, string? SystemPrompt, string? AgentPath, int? MaxTurns = null);
|
public record ListConfigDto(string? Model, string? SystemPrompt, string? AgentPath, int? MaxTurns = null, List<string>? SessionSkills = null);
|
||||||
public record SeedResultDto(int Copied, int Skipped);
|
public record SeedResultDto(int Copied, int Skipped);
|
||||||
|
|
||||||
public record OnlineInboxStateDto(
|
public record OnlineInboxStateDto(
|
||||||
@@ -119,6 +129,7 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
|
|||||||
private readonly Runner.PendingQuestionRegistry _pendingQuestions;
|
private readonly Runner.PendingQuestionRegistry _pendingQuestions;
|
||||||
private readonly InteractiveSessionService _interactive;
|
private readonly InteractiveSessionService _interactive;
|
||||||
private readonly LogRingBuffer? _logBuffer;
|
private readonly LogRingBuffer? _logBuffer;
|
||||||
|
private readonly ISessionSkillRegistry _skillRegistry;
|
||||||
|
|
||||||
public WorkerHub(
|
public WorkerHub(
|
||||||
QueueService queue,
|
QueueService queue,
|
||||||
@@ -145,6 +156,7 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
|
|||||||
OnlineTokenStore onlineTokenStore,
|
OnlineTokenStore onlineTokenStore,
|
||||||
Runner.PendingQuestionRegistry pendingQuestions,
|
Runner.PendingQuestionRegistry pendingQuestions,
|
||||||
InteractiveSessionService interactive,
|
InteractiveSessionService interactive,
|
||||||
|
ISessionSkillRegistry skillRegistry,
|
||||||
LogRingBuffer? logBuffer = null)
|
LogRingBuffer? logBuffer = null)
|
||||||
{
|
{
|
||||||
_queue = queue;
|
_queue = queue;
|
||||||
@@ -171,9 +183,23 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
|
|||||||
_onlineTokenStore = onlineTokenStore;
|
_onlineTokenStore = onlineTokenStore;
|
||||||
_pendingQuestions = pendingQuestions;
|
_pendingQuestions = pendingQuestions;
|
||||||
_interactive = interactive;
|
_interactive = interactive;
|
||||||
|
_skillRegistry = skillRegistry;
|
||||||
_logBuffer = logBuffer;
|
_logBuffer = logBuffer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Persistence boundary for the session_skills JSON-array columns (task/list/global).
|
||||||
|
// A null/empty selection persists as null so "inherit / none" stays clean, and the
|
||||||
|
// shape matches TaskRunner.UnionSkillNames's expected JSON string array.
|
||||||
|
private static string? SkillsToJson(List<string>? names) =>
|
||||||
|
names is null or { Count: 0 } ? null : JsonSerializer.Serialize(names);
|
||||||
|
|
||||||
|
private static List<string>? SkillsFromJson(string? json)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(json)) return null;
|
||||||
|
try { return JsonSerializer.Deserialize<List<string>>(json); }
|
||||||
|
catch (JsonException) { return null; }
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Deliver the user's answer to a question a running task raised via AskUser.
|
/// <summary>Deliver the user's answer to a question a running task raised via AskUser.
|
||||||
/// Returns false if no matching question is still pending (already answered or timed out).</summary>
|
/// Returns false if no matching question is still pending (already answered or timed out).</summary>
|
||||||
public bool AnswerTaskQuestion(string taskId, string questionId, string answer) =>
|
public bool AnswerTaskQuestion(string taskId, string questionId, string answer) =>
|
||||||
@@ -288,7 +314,8 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
|
|||||||
row.WorktreeAutoCleanupDays,
|
row.WorktreeAutoCleanupDays,
|
||||||
row.ReportExcludedPaths,
|
row.ReportExcludedPaths,
|
||||||
row.StandupWeekday,
|
row.StandupWeekday,
|
||||||
row.DailyPrepMaxTasks);
|
row.DailyPrepMaxTasks,
|
||||||
|
SkillsFromJson(row.SessionSkills));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task UpdateAppSettings(AppSettingsDto dto)
|
public async Task UpdateAppSettings(AppSettingsDto dto)
|
||||||
@@ -310,9 +337,28 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
|
|||||||
ReportExcludedPaths = dto.ReportExcludedPaths,
|
ReportExcludedPaths = dto.ReportExcludedPaths,
|
||||||
StandupWeekday = dto.StandupWeekday is >= 0 and <= 6 ? dto.StandupWeekday : (int)DayOfWeek.Wednesday,
|
StandupWeekday = dto.StandupWeekday is >= 0 and <= 6 ? dto.StandupWeekday : (int)DayOfWeek.Wednesday,
|
||||||
DailyPrepMaxTasks = dto.DailyPrepMaxTasks,
|
DailyPrepMaxTasks = dto.DailyPrepMaxTasks,
|
||||||
|
SessionSkills = SkillsToJson(dto.SessionSkills),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<List<SessionSkillDto>> GetSessionSkills()
|
||||||
|
{
|
||||||
|
var rows = await _skillRegistry.ListAsync(Context.ConnectionAborted);
|
||||||
|
return rows.Select(r => new SessionSkillDto(r.Name, r.Description, r.SourceUrl, r.PinnedRef, r.AddedAt)).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<List<string>> InstallSessionSkill(string url) => HubGuard(async () =>
|
||||||
|
{
|
||||||
|
var installed = await _skillRegistry.InstallAsync(url, Context.ConnectionAborted);
|
||||||
|
return installed.ToList();
|
||||||
|
});
|
||||||
|
|
||||||
|
public Task UpdateSessionSkill(string sourceUrl) => HubGuard(
|
||||||
|
() => _skillRegistry.UpdateAsync(sourceUrl, Context.ConnectionAborted));
|
||||||
|
|
||||||
|
public Task RemoveSessionSkill(string sourceUrl) => HubGuard(
|
||||||
|
() => _skillRegistry.RemoveAsync(sourceUrl, Context.ConnectionAborted));
|
||||||
|
|
||||||
public async Task<WorktreeCleanupDto> CleanupFinishedWorktrees(string? listId = null)
|
public async Task<WorktreeCleanupDto> CleanupFinishedWorktrees(string? listId = null)
|
||||||
{
|
{
|
||||||
var result = await _wtMaintenance.CleanupFinishedAsync(listId, Context.ConnectionAborted);
|
var result = await _wtMaintenance.CleanupFinishedAsync(listId, Context.ConnectionAborted);
|
||||||
@@ -456,8 +502,9 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
|
|||||||
var model = dto.Model.NullIfBlank();
|
var model = dto.Model.NullIfBlank();
|
||||||
var systemPrompt = dto.SystemPrompt.NullIfBlank();
|
var systemPrompt = dto.SystemPrompt.NullIfBlank();
|
||||||
var agentPath = dto.AgentPath.NullIfBlank();
|
var agentPath = dto.AgentPath.NullIfBlank();
|
||||||
|
var sessionSkills = SkillsToJson(dto.SessionSkills);
|
||||||
|
|
||||||
if (model is null && systemPrompt is null && agentPath is null && dto.MaxTurns is null)
|
if (model is null && systemPrompt is null && agentPath is null && dto.MaxTurns is null && sessionSkills is null)
|
||||||
{
|
{
|
||||||
await repo.DeleteConfigAsync(dto.ListId);
|
await repo.DeleteConfigAsync(dto.ListId);
|
||||||
}
|
}
|
||||||
@@ -470,6 +517,7 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
|
|||||||
SystemPrompt = systemPrompt,
|
SystemPrompt = systemPrompt,
|
||||||
AgentPath = agentPath,
|
AgentPath = agentPath,
|
||||||
MaxTurns = dto.MaxTurns,
|
MaxTurns = dto.MaxTurns,
|
||||||
|
SessionSkills = sessionSkills,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -482,7 +530,7 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
|
|||||||
var repo = new ListRepository(ctx);
|
var repo = new ListRepository(ctx);
|
||||||
var config = await repo.GetConfigAsync(listId);
|
var config = await repo.GetConfigAsync(listId);
|
||||||
if (config is null) return null;
|
if (config is null) return null;
|
||||||
return new ListConfigDto(config.Model, config.SystemPrompt, config.AgentPath, config.MaxTurns);
|
return new ListConfigDto(config.Model, config.SystemPrompt, config.AgentPath, config.MaxTurns, SkillsFromJson(config.SessionSkills));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task SetTaskStatus(string taskId, string status)
|
public async Task SetTaskStatus(string taskId, string status)
|
||||||
@@ -543,7 +591,8 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
|
|||||||
dto.Model.NullIfBlank(),
|
dto.Model.NullIfBlank(),
|
||||||
dto.SystemPrompt.NullIfBlank(),
|
dto.SystemPrompt.NullIfBlank(),
|
||||||
dto.AgentPath.NullIfBlank(),
|
dto.AgentPath.NullIfBlank(),
|
||||||
dto.MaxTurns);
|
dto.MaxTurns,
|
||||||
|
SkillsToJson(dto.SessionSkills));
|
||||||
|
|
||||||
await _broadcaster.TaskUpdated(dto.TaskId);
|
await _broadcaster.TaskUpdated(dto.TaskId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -86,6 +86,10 @@ public abstract class StubWorkerClient : IWorkerClient
|
|||||||
public virtual Task<SeedResultDto?> RestoreDefaultAgentsAsync() => Task.FromResult<SeedResultDto?>(null);
|
public virtual Task<SeedResultDto?> RestoreDefaultAgentsAsync() => Task.FromResult<SeedResultDto?>(null);
|
||||||
public virtual Task<ListConfigDto?> GetListConfigAsync(string listId) => Task.FromResult<ListConfigDto?>(null);
|
public virtual Task<ListConfigDto?> GetListConfigAsync(string listId) => Task.FromResult<ListConfigDto?>(null);
|
||||||
public virtual Task UpdateTaskAgentSettingsAsync(UpdateTaskAgentSettingsDto dto) => Task.CompletedTask;
|
public virtual Task UpdateTaskAgentSettingsAsync(UpdateTaskAgentSettingsDto dto) => Task.CompletedTask;
|
||||||
|
public virtual Task<List<SessionSkillDto>> GetSessionSkillsAsync() => Task.FromResult(new List<SessionSkillDto>());
|
||||||
|
public virtual Task<List<string>> InstallSessionSkillAsync(string url) => Task.FromResult(new List<string>());
|
||||||
|
public virtual Task UpdateSessionSkillAsync(string sourceUrl) => Task.CompletedTask;
|
||||||
|
public virtual Task RemoveSessionSkillAsync(string sourceUrl) => Task.CompletedTask;
|
||||||
public virtual Task SetTaskStatusAsync(string taskId, TaskStatus status) => Task.CompletedTask;
|
public virtual Task SetTaskStatusAsync(string taskId, TaskStatus status) => Task.CompletedTask;
|
||||||
public virtual Task<MergeResultDto?> ApproveReviewAsync(string taskId, string targetBranch) => Task.FromResult<MergeResultDto?>(null);
|
public virtual Task<MergeResultDto?> ApproveReviewAsync(string taskId, string targetBranch) => Task.FromResult<MergeResultDto?>(null);
|
||||||
public virtual Task<MergePreviewDto?> PreviewMergeAsync(string taskId, string targetBranch) => Task.FromResult<MergePreviewDto?>(null);
|
public virtual Task<MergePreviewDto?> PreviewMergeAsync(string taskId, string targetBranch) => Task.FromResult<MergePreviewDto?>(null);
|
||||||
|
|||||||
@@ -624,7 +624,7 @@ public sealed class ExternalMcpServiceTests : IDisposable
|
|||||||
{
|
{
|
||||||
var listId = await SeedListAsync();
|
var listId = await SeedListAsync();
|
||||||
var task = await SeedTaskAsync(listId);
|
var task = await SeedTaskAsync(listId);
|
||||||
await _tasks.UpdateAgentSettingsAsync(task.Id, "claude-sonnet-4-6", "be concise", null, 10, CancellationToken.None);
|
await _tasks.UpdateAgentSettingsAsync(task.Id, "claude-sonnet-4-6", "be concise", null, 10, ct: CancellationToken.None);
|
||||||
var sut = BuildConfigSut();
|
var sut = BuildConfigSut();
|
||||||
|
|
||||||
var result = await sut.GetTaskConfig(task.Id, CancellationToken.None);
|
var result = await sut.GetTaskConfig(task.Id, CancellationToken.None);
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ public sealed class ClearMyDayHubTests : IDisposable
|
|||||||
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(),
|
null!, new ClaudeDo.Worker.Online.OnlineInboxConfig(), new ClaudeDo.Worker.Online.OnlineTokenStore(),
|
||||||
new ClaudeDo.Worker.Runner.PendingQuestionRegistry(), null!);
|
new ClaudeDo.Worker.Runner.PendingQuestionRegistry(), null!, null!);
|
||||||
hub.Clients = new FakeHubCallerClients(new RecordingClientProxy());
|
hub.Clients = new FakeHubCallerClients(new RecordingClientProxy());
|
||||||
hub.Context = new FakeHubCallerContext();
|
hub.Context = new FakeHubCallerContext();
|
||||||
return hub;
|
return hub;
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ public sealed class OnlineInboxHubTests : IDisposable
|
|||||||
var hub = new WorkerHub(
|
var hub = new WorkerHub(
|
||||||
null!, null!, null!, null!, broadcaster, null!,
|
null!, null!, null!, null!, broadcaster, 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!,
|
||||||
cfg, inboxCfg, store, new ClaudeDo.Worker.Runner.PendingQuestionRegistry(), null!);
|
cfg, inboxCfg, store, new ClaudeDo.Worker.Runner.PendingQuestionRegistry(), null!, null!);
|
||||||
hub.Clients = new FakeHubCallerClients(new RecordingClientProxy());
|
hub.Clients = new FakeHubCallerClients(new RecordingClientProxy());
|
||||||
hub.Context = new FakeHubCallerContext();
|
hub.Context = new FakeHubCallerContext();
|
||||||
return (hub, inboxCfg, store);
|
return (hub, inboxCfg, store);
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ public sealed class PlanningHubTests : IDisposable
|
|||||||
null!, null!, null!, null!, null!, _db.CreateFactory(), null!, null!, null!,
|
null!, null!, null!, null!, null!, _db.CreateFactory(), 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(),
|
null!, new ClaudeDo.Worker.Online.OnlineInboxConfig(), new ClaudeDo.Worker.Online.OnlineTokenStore(),
|
||||||
new ClaudeDo.Worker.Runner.PendingQuestionRegistry(), null!);
|
new ClaudeDo.Worker.Runner.PendingQuestionRegistry(), null!, null!);
|
||||||
hub.Clients = new FakeHubCallerClients(_proxy);
|
hub.Clients = new FakeHubCallerClients(_proxy);
|
||||||
hub.Context = new FakeHubCallerContext();
|
hub.Context = new FakeHubCallerContext();
|
||||||
return hub;
|
return hub;
|
||||||
|
|||||||
@@ -0,0 +1,191 @@
|
|||||||
|
using ClaudeDo.Data.Models;
|
||||||
|
using ClaudeDo.Data.Repositories;
|
||||||
|
using ClaudeDo.Worker.Hub;
|
||||||
|
using ClaudeDo.Worker.Skills;
|
||||||
|
using ClaudeDo.Worker.Tests.Infrastructure;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace ClaudeDo.Worker.Tests.Hub;
|
||||||
|
|
||||||
|
public sealed class SessionSkillsHubTests : IDisposable
|
||||||
|
{
|
||||||
|
private readonly DbFixture _db = new();
|
||||||
|
|
||||||
|
public void Dispose() => _db.Dispose();
|
||||||
|
|
||||||
|
private sealed class FakeSessionSkillRegistry : ISessionSkillRegistry
|
||||||
|
{
|
||||||
|
public List<SessionSkillEntity> Skills { get; } = new();
|
||||||
|
public string? InstallUrl { get; private set; }
|
||||||
|
public string? UpdateSourceUrl { get; private set; }
|
||||||
|
public string? RemoveSourceUrl { get; private set; }
|
||||||
|
public Exception? ThrowOnInstall { get; set; }
|
||||||
|
|
||||||
|
public Task<IReadOnlyList<string>> InstallAsync(string url, CancellationToken ct)
|
||||||
|
{
|
||||||
|
InstallUrl = url;
|
||||||
|
if (ThrowOnInstall is not null) throw ThrowOnInstall;
|
||||||
|
return Task.FromResult<IReadOnlyList<string>>(new List<string> { "ponytail", "ponytail-help" });
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task UpdateAsync(string sourceUrl, CancellationToken ct)
|
||||||
|
{
|
||||||
|
UpdateSourceUrl = sourceUrl;
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task RemoveAsync(string sourceUrl, CancellationToken ct)
|
||||||
|
{
|
||||||
|
RemoveSourceUrl = sourceUrl;
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<IReadOnlyList<SessionSkillEntity>> ListAsync(CancellationToken ct)
|
||||||
|
=> Task.FromResult<IReadOnlyList<SessionSkillEntity>>(Skills);
|
||||||
|
}
|
||||||
|
|
||||||
|
private (WorkerHub hub, FakeSessionSkillRegistry registry) CreateHub()
|
||||||
|
{
|
||||||
|
var registry = new FakeSessionSkillRegistry();
|
||||||
|
var broadcaster = new HubBroadcaster(new CapturingHubContext());
|
||||||
|
var hub = new WorkerHub(
|
||||||
|
null!, null!, null!, null!, broadcaster, _db.CreateFactory(),
|
||||||
|
null!, null!, null!, null!, null!, null!, null!, null!, null!, null!, null!, null!, null!,
|
||||||
|
null!, new ClaudeDo.Worker.Online.OnlineInboxConfig(), new ClaudeDo.Worker.Online.OnlineTokenStore(),
|
||||||
|
new ClaudeDo.Worker.Runner.PendingQuestionRegistry(), null!, registry);
|
||||||
|
hub.Clients = new FakeHubCallerClients(new RecordingClientProxy());
|
||||||
|
hub.Context = new FakeHubCallerContext();
|
||||||
|
return (hub, registry);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetSessionSkills_maps_registry_rows_to_dtos()
|
||||||
|
{
|
||||||
|
var (hub, registry) = CreateHub();
|
||||||
|
registry.Skills.Add(new SessionSkillEntity
|
||||||
|
{
|
||||||
|
Name = "ponytail",
|
||||||
|
SourceUrl = "https://example.com/skills.git",
|
||||||
|
PinnedRef = "abc123",
|
||||||
|
Subpath = "skills/ponytail",
|
||||||
|
Description = "A skill",
|
||||||
|
AddedAt = DateTimeOffset.UtcNow,
|
||||||
|
});
|
||||||
|
|
||||||
|
var result = await hub.GetSessionSkills();
|
||||||
|
|
||||||
|
var dto = Assert.Single(result);
|
||||||
|
Assert.Equal("ponytail", dto.Name);
|
||||||
|
Assert.Equal("A skill", dto.Description);
|
||||||
|
Assert.Equal("https://example.com/skills.git", dto.SourceUrl);
|
||||||
|
Assert.Equal("abc123", dto.PinnedRef);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task InstallSessionSkill_returns_installed_names_and_forwards_url()
|
||||||
|
{
|
||||||
|
var (hub, registry) = CreateHub();
|
||||||
|
|
||||||
|
var installed = await hub.InstallSessionSkill("https://example.com/skills.git");
|
||||||
|
|
||||||
|
Assert.Equal(new List<string> { "ponytail", "ponytail-help" }, installed);
|
||||||
|
Assert.Equal("https://example.com/skills.git", registry.InstallUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task InstallSessionSkill_wraps_InvalidOperationException_as_HubException()
|
||||||
|
{
|
||||||
|
var (hub, registry) = CreateHub();
|
||||||
|
registry.ThrowOnInstall = new InvalidOperationException("boom");
|
||||||
|
|
||||||
|
var ex = await Assert.ThrowsAsync<Microsoft.AspNetCore.SignalR.HubException>(
|
||||||
|
() => hub.InstallSessionSkill("https://example.com/skills.git"));
|
||||||
|
Assert.Equal("boom", ex.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task UpdateSessionSkill_forwards_source_url()
|
||||||
|
{
|
||||||
|
var (hub, registry) = CreateHub();
|
||||||
|
|
||||||
|
await hub.UpdateSessionSkill("https://example.com/skills.git");
|
||||||
|
|
||||||
|
Assert.Equal("https://example.com/skills.git", registry.UpdateSourceUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task RemoveSessionSkill_forwards_source_url()
|
||||||
|
{
|
||||||
|
var (hub, registry) = CreateHub();
|
||||||
|
|
||||||
|
await hub.RemoveSessionSkill("https://example.com/skills.git");
|
||||||
|
|
||||||
|
Assert.Equal("https://example.com/skills.git", registry.RemoveSourceUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task UpdateAppSettings_then_GetAppSettings_RoundTrips_SessionSkills()
|
||||||
|
{
|
||||||
|
var (hub, _) = CreateHub();
|
||||||
|
var current = await hub.GetAppSettings();
|
||||||
|
|
||||||
|
await hub.UpdateAppSettings(current with { SessionSkills = new List<string> { "ponytail", "ponytail-help" } });
|
||||||
|
var reloaded = await hub.GetAppSettings();
|
||||||
|
|
||||||
|
Assert.Equal(new List<string> { "ponytail", "ponytail-help" }, reloaded.SessionSkills);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task UpdateAppSettings_EmptySessionSkills_PersistsAsNull()
|
||||||
|
{
|
||||||
|
var (hub, _) = CreateHub();
|
||||||
|
var current = await hub.GetAppSettings();
|
||||||
|
|
||||||
|
await hub.UpdateAppSettings(current with { SessionSkills = new List<string> { "ponytail" } });
|
||||||
|
await hub.UpdateAppSettings(current with { SessionSkills = new List<string>() });
|
||||||
|
var reloaded = await hub.GetAppSettings();
|
||||||
|
|
||||||
|
Assert.Null(reloaded.SessionSkills);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task UpdateListConfig_then_GetListConfig_RoundTrips_SessionSkills()
|
||||||
|
{
|
||||||
|
var (hub, _) = CreateHub();
|
||||||
|
var listId = Guid.NewGuid().ToString();
|
||||||
|
using (var ctx = _db.CreateContext())
|
||||||
|
{
|
||||||
|
await new ListRepository(ctx).AddAsync(new ListEntity { Id = listId, Name = "L", CreatedAt = DateTime.UtcNow });
|
||||||
|
}
|
||||||
|
|
||||||
|
await hub.UpdateListConfig(new UpdateListConfigDto(listId, null, null, null, null, new List<string> { "ponytail" }));
|
||||||
|
var config = await hub.GetListConfig(listId);
|
||||||
|
|
||||||
|
Assert.NotNull(config);
|
||||||
|
Assert.Equal(new List<string> { "ponytail" }, config!.SessionSkills);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task UpdateTaskAgentSettings_Persists_SessionSkills()
|
||||||
|
{
|
||||||
|
var (hub, _) = CreateHub();
|
||||||
|
var listId = Guid.NewGuid().ToString();
|
||||||
|
var taskId = Guid.NewGuid().ToString();
|
||||||
|
using (var ctx = _db.CreateContext())
|
||||||
|
{
|
||||||
|
await new ListRepository(ctx).AddAsync(new ListEntity { Id = listId, Name = "L", CreatedAt = DateTime.UtcNow });
|
||||||
|
await new TaskRepository(ctx).AddAsync(new TaskEntity
|
||||||
|
{
|
||||||
|
Id = taskId, ListId = listId, Title = "T", CreatedAt = DateTime.UtcNow,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await hub.UpdateTaskAgentSettings(new UpdateTaskAgentSettingsDto(
|
||||||
|
taskId, null, null, null, null, new List<string> { "ponytail", "ponytail-help" }));
|
||||||
|
|
||||||
|
using var readCtx = _db.CreateContext();
|
||||||
|
var entity = await new TaskRepository(readCtx).GetByIdAsync(taskId);
|
||||||
|
Assert.NotNull(entity);
|
||||||
|
Assert.Equal("[\"ponytail\",\"ponytail-help\"]", entity!.SessionSkills);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,7 +21,7 @@ public sealed class WorktreeStateHubTests : IDisposable
|
|||||||
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(),
|
null!, new ClaudeDo.Worker.Online.OnlineInboxConfig(), new ClaudeDo.Worker.Online.OnlineTokenStore(),
|
||||||
new ClaudeDo.Worker.Runner.PendingQuestionRegistry(), null!);
|
new ClaudeDo.Worker.Runner.PendingQuestionRegistry(), null!, null!);
|
||||||
hub.Clients = new FakeHubCallerClients(new RecordingClientProxy());
|
hub.Clients = new FakeHubCallerClients(new RecordingClientProxy());
|
||||||
hub.Context = new FakeHubCallerContext();
|
hub.Context = new FakeHubCallerContext();
|
||||||
return hub;
|
return hub;
|
||||||
|
|||||||
@@ -72,6 +72,43 @@ public class AppSettingsRepositoryTests : IDisposable
|
|||||||
Assert.Null(row.CentralWorktreeRoot);
|
Assert.Null(row.CentralWorktreeRoot);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task UpdateAsync_Persists_SessionSkills_Json()
|
||||||
|
{
|
||||||
|
using (var ctx = _db.CreateContext())
|
||||||
|
{
|
||||||
|
var repo = new AppSettingsRepository(ctx);
|
||||||
|
await repo.UpdateAsync(new AppSettingsEntity
|
||||||
|
{
|
||||||
|
SessionSkills = "[\"ponytail\",\"ponytail-help\"]",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
using var readCtx = _db.CreateContext();
|
||||||
|
var row = await new AppSettingsRepository(readCtx).GetAsync();
|
||||||
|
Assert.Equal("[\"ponytail\",\"ponytail-help\"]", row.SessionSkills);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task UpdateAsync_Blank_SessionSkills_Stored_As_Null()
|
||||||
|
{
|
||||||
|
using (var ctx = _db.CreateContext())
|
||||||
|
{
|
||||||
|
var repo = new AppSettingsRepository(ctx);
|
||||||
|
await repo.UpdateAsync(new AppSettingsEntity { SessionSkills = "[\"a\"]" });
|
||||||
|
}
|
||||||
|
|
||||||
|
using (var ctx = _db.CreateContext())
|
||||||
|
{
|
||||||
|
var repo = new AppSettingsRepository(ctx);
|
||||||
|
await repo.UpdateAsync(new AppSettingsEntity { SessionSkills = null });
|
||||||
|
}
|
||||||
|
|
||||||
|
using var readCtx = _db.CreateContext();
|
||||||
|
var row = await new AppSettingsRepository(readCtx).GetAsync();
|
||||||
|
Assert.Null(row.SessionSkills);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task DailyPrepMaxTasks_defaults_to_5_and_persists()
|
public async Task DailyPrepMaxTasks_defaults_to_5_and_persists()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -60,6 +60,42 @@ public sealed class ListRepositoryConfigTests : IDisposable
|
|||||||
Assert.Equal("haiku-4-5", fetched.Model);
|
Assert.Equal("haiku-4-5", fetched.Model);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task SetConfig_Persists_SessionSkills_On_Insert()
|
||||||
|
{
|
||||||
|
await _repo.SetConfigAsync(new ListConfigEntity
|
||||||
|
{
|
||||||
|
ListId = _listId,
|
||||||
|
SessionSkills = "[\"ponytail\"]",
|
||||||
|
});
|
||||||
|
|
||||||
|
var fetched = await _repo.GetConfigAsync(_listId);
|
||||||
|
Assert.NotNull(fetched);
|
||||||
|
Assert.Equal("[\"ponytail\"]", fetched.SessionSkills);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task SetConfig_Persists_SessionSkills_On_Update()
|
||||||
|
{
|
||||||
|
await _repo.SetConfigAsync(new ListConfigEntity { ListId = _listId, SessionSkills = "[\"a\"]" });
|
||||||
|
await _repo.SetConfigAsync(new ListConfigEntity { ListId = _listId, SessionSkills = "[\"b\",\"c\"]" });
|
||||||
|
|
||||||
|
var fetched = await _repo.GetConfigAsync(_listId);
|
||||||
|
Assert.NotNull(fetched);
|
||||||
|
Assert.Equal("[\"b\",\"c\"]", fetched.SessionSkills);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task SetConfig_Null_SessionSkills_Clears_On_Update()
|
||||||
|
{
|
||||||
|
await _repo.SetConfigAsync(new ListConfigEntity { ListId = _listId, SessionSkills = "[\"a\"]" });
|
||||||
|
await _repo.SetConfigAsync(new ListConfigEntity { ListId = _listId, SessionSkills = null });
|
||||||
|
|
||||||
|
var fetched = await _repo.GetConfigAsync(_listId);
|
||||||
|
Assert.NotNull(fetched);
|
||||||
|
Assert.Null(fetched.SessionSkills);
|
||||||
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
_ctx.Dispose();
|
_ctx.Dispose();
|
||||||
|
|||||||
@@ -72,4 +72,33 @@ public sealed class TaskRepositoryAgentSettingsTests : IDisposable
|
|||||||
Assert.Null(entity.SystemPrompt);
|
Assert.Null(entity.SystemPrompt);
|
||||||
Assert.Null(entity.AgentPath);
|
Assert.Null(entity.AgentPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task UpdateAgentSettingsAsync_Persists_SessionSkills()
|
||||||
|
{
|
||||||
|
var taskId = await SeedTaskAsync();
|
||||||
|
|
||||||
|
await _repo.UpdateAgentSettingsAsync(taskId, null, null, null, sessionSkills: "[\"ponytail\",\"ponytail-help\"]");
|
||||||
|
|
||||||
|
var entity = await _repo.GetByIdAsync(taskId);
|
||||||
|
Assert.NotNull(entity);
|
||||||
|
Assert.Equal("[\"ponytail\",\"ponytail-help\"]", entity!.SessionSkills);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task UpdateAgentSettingsAsync_Null_SessionSkills_ClearsColumn()
|
||||||
|
{
|
||||||
|
var taskId = await SeedTaskAsync();
|
||||||
|
|
||||||
|
using (var ctx = _db.CreateContext())
|
||||||
|
{
|
||||||
|
await new TaskRepository(ctx).UpdateAgentSettingsAsync(taskId, null, null, null, sessionSkills: "[\"a\"]");
|
||||||
|
}
|
||||||
|
|
||||||
|
await _repo.UpdateAgentSettingsAsync(taskId, null, null, null, sessionSkills: null);
|
||||||
|
|
||||||
|
var entity = await _repo.GetByIdAsync(taskId);
|
||||||
|
Assert.NotNull(entity);
|
||||||
|
Assert.Null(entity!.SessionSkills);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,6 +55,10 @@ sealed class FakeWorkerClient : IWorkerClient
|
|||||||
public Task<SeedResultDto?> RestoreDefaultAgentsAsync() => Task.FromResult<SeedResultDto?>(null);
|
public Task<SeedResultDto?> RestoreDefaultAgentsAsync() => Task.FromResult<SeedResultDto?>(null);
|
||||||
public Task<ListConfigDto?> GetListConfigAsync(string listId) => Task.FromResult<ListConfigDto?>(null);
|
public Task<ListConfigDto?> GetListConfigAsync(string listId) => Task.FromResult<ListConfigDto?>(null);
|
||||||
public Task UpdateTaskAgentSettingsAsync(UpdateTaskAgentSettingsDto dto) => Task.CompletedTask;
|
public Task UpdateTaskAgentSettingsAsync(UpdateTaskAgentSettingsDto dto) => Task.CompletedTask;
|
||||||
|
public Task<List<SessionSkillDto>> GetSessionSkillsAsync() => Task.FromResult(new List<SessionSkillDto>());
|
||||||
|
public Task<List<string>> InstallSessionSkillAsync(string url) => Task.FromResult(new List<string>());
|
||||||
|
public Task UpdateSessionSkillAsync(string sourceUrl) => Task.CompletedTask;
|
||||||
|
public Task RemoveSessionSkillAsync(string sourceUrl) => Task.CompletedTask;
|
||||||
public Task SetTaskStatusAsync(string taskId, TaskStatus status) => Task.CompletedTask;
|
public Task SetTaskStatusAsync(string taskId, TaskStatus status) => Task.CompletedTask;
|
||||||
public Task<MergeResultDto?> ApproveReviewAsync(string taskId, string targetBranch) => Task.FromResult<MergeResultDto?>(null);
|
public Task<MergeResultDto?> ApproveReviewAsync(string taskId, string targetBranch) => Task.FromResult<MergeResultDto?>(null);
|
||||||
public Task<MergePreviewDto?> PreviewMergeAsync(string taskId, string targetBranch) => Task.FromResult<MergePreviewDto?>(null);
|
public Task<MergePreviewDto?> PreviewMergeAsync(string taskId, string targetBranch) => Task.FromResult<MergePreviewDto?>(null);
|
||||||
|
|||||||
Reference in New Issue
Block a user