Omitted fields on set_task_config/set_list_config used to be indistinguishable from an explicit clear, so any single-field update silently wiped every other override (including SessionSkills, which wasn't even a tool parameter). Now only the fields you pass are changed; clearing an override requires naming it in the new clearFields array (a sentinel string doesn't work uniformly since maxTurns is an int). set_list_config's "all four null deletes the config" case is preserved but only reachable via clearFields. Return values now reflect the full resulting config.
247 lines
8.6 KiB
C#
247 lines
8.6 KiB
C#
using ClaudeDo.Data;
|
|
using ClaudeDo.Data.Models;
|
|
using ClaudeDo.Data.Repositories;
|
|
using ClaudeDo.Worker.External;
|
|
using ClaudeDo.Worker.Hub;
|
|
using ClaudeDo.Worker.Tests.Infrastructure;
|
|
|
|
namespace ClaudeDo.Worker.Tests.External;
|
|
|
|
public sealed class ConfigMcpToolsTests : IDisposable
|
|
{
|
|
private readonly DbFixture _db = new();
|
|
private readonly ClaudeDoDbContext _ctx;
|
|
private readonly ListRepository _lists;
|
|
private readonly TaskRepository _tasks;
|
|
private readonly ConfigMcpTools _sut;
|
|
|
|
public ConfigMcpToolsTests()
|
|
{
|
|
_ctx = _db.CreateContext();
|
|
_lists = new ListRepository(_ctx);
|
|
_tasks = new TaskRepository(_ctx);
|
|
_sut = new ConfigMcpTools(_lists, _tasks, new HubBroadcaster(new CapturingHubContext()), _db.CreateFactory());
|
|
}
|
|
|
|
public void Dispose() { _ctx.Dispose(); _db.Dispose(); }
|
|
|
|
private async Task<string> SeedListAsync()
|
|
{
|
|
var id = Guid.NewGuid().ToString();
|
|
await _lists.AddAsync(new ListEntity { Id = id, Name = "L", CreatedAt = DateTime.UtcNow });
|
|
return id;
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SetAndGetListConfig_RoundTrips()
|
|
{
|
|
var listId = await SeedListAsync();
|
|
|
|
var setResult = await _sut.SetListConfig(listId, "sonnet", "be terse", null, 42, cancellationToken: CancellationToken.None);
|
|
Assert.True(setResult.Ok);
|
|
Assert.Equal(listId, setResult.ListId);
|
|
Assert.NotNull(setResult.Config);
|
|
Assert.Equal("sonnet", setResult.Config!.Model);
|
|
|
|
var cfg = await _sut.GetListConfig(listId, CancellationToken.None);
|
|
|
|
Assert.True(cfg.Found);
|
|
Assert.NotNull(cfg.Config);
|
|
Assert.Equal("sonnet", cfg.Config!.Model);
|
|
Assert.Equal("be terse", cfg.Config.SystemPrompt);
|
|
Assert.Null(cfg.Config.AgentPath);
|
|
Assert.Equal(42, cfg.Config.MaxTurns);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetListConfig_NoConfigSet_ReturnsNotFound()
|
|
{
|
|
var listId = await SeedListAsync();
|
|
|
|
var cfg = await _sut.GetListConfig(listId, CancellationToken.None);
|
|
|
|
Assert.False(cfg.Found);
|
|
Assert.Null(cfg.Config);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SetListConfig_PartialUpdate_LeavesOtherFieldsUntouched()
|
|
{
|
|
var listId = await SeedListAsync();
|
|
await _sut.SetListConfig(listId, "opus", "be terse", "agent.md", 30, cancellationToken: CancellationToken.None);
|
|
|
|
var result = await _sut.SetListConfig(listId, maxTurns: 40, cancellationToken: CancellationToken.None);
|
|
|
|
Assert.Equal("opus", result.Config!.Model);
|
|
Assert.Equal("be terse", result.Config.SystemPrompt);
|
|
Assert.Equal("agent.md", result.Config.AgentPath);
|
|
Assert.Equal(40, result.Config.MaxTurns);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SetListConfig_ClearFields_ClearsOnlyNamedField()
|
|
{
|
|
var listId = await SeedListAsync();
|
|
await _sut.SetListConfig(listId, "opus", "be terse", "agent.md", 30, cancellationToken: CancellationToken.None);
|
|
|
|
var result = await _sut.SetListConfig(listId, clearFields: new[] { "model" }, cancellationToken: CancellationToken.None);
|
|
|
|
Assert.Null(result.Config!.Model);
|
|
Assert.Equal("be terse", result.Config.SystemPrompt);
|
|
Assert.Equal("agent.md", result.Config.AgentPath);
|
|
Assert.Equal(30, result.Config.MaxTurns);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SetListConfig_AllFieldsCleared_DeletesConfig()
|
|
{
|
|
var listId = await SeedListAsync();
|
|
await _sut.SetListConfig(listId, "sonnet", null, null, null, cancellationToken: CancellationToken.None);
|
|
|
|
var clearResult = await _sut.SetListConfig(
|
|
listId, clearFields: new[] { "model", "systemPrompt", "agentPath", "maxTurns" }, cancellationToken: CancellationToken.None);
|
|
|
|
Assert.True(clearResult.Ok);
|
|
Assert.Null(clearResult.Config);
|
|
var cfg = await _sut.GetListConfig(listId, CancellationToken.None);
|
|
Assert.False(cfg.Found);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SetListConfig_UnknownClearField_Throws()
|
|
{
|
|
var listId = await SeedListAsync();
|
|
|
|
await Assert.ThrowsAsync<InvalidOperationException>(() =>
|
|
_sut.SetListConfig(listId, clearFields: new[] { "bogus" }, cancellationToken: CancellationToken.None));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SetTaskConfig_PersistsOverrides()
|
|
{
|
|
var listId = await SeedListAsync();
|
|
var task = new TaskEntity
|
|
{
|
|
Id = Guid.NewGuid().ToString(),
|
|
ListId = listId,
|
|
Title = "t",
|
|
Status = ClaudeDo.Data.Models.TaskStatus.Idle,
|
|
CreatedAt = DateTime.UtcNow,
|
|
CommitType = "chore",
|
|
};
|
|
await _tasks.AddAsync(task);
|
|
|
|
var result = await _sut.SetTaskConfig(task.Id, "opus", null, null, 15, cancellationToken: CancellationToken.None);
|
|
|
|
Assert.True(result.Ok);
|
|
Assert.Equal(task.Id, result.TaskId);
|
|
Assert.Equal("opus", result.Config!.Model);
|
|
Assert.Equal(15, result.Config.MaxTurns);
|
|
|
|
var loaded = await _tasks.GetByIdAsync(task.Id);
|
|
Assert.Equal("opus", loaded!.Model);
|
|
Assert.Equal(15, loaded.MaxTurns);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SetTaskConfig_PartialUpdate_LeavesOtherOverridesUntouched()
|
|
{
|
|
var listId = await SeedListAsync();
|
|
var task = new TaskEntity
|
|
{
|
|
Id = Guid.NewGuid().ToString(),
|
|
ListId = listId,
|
|
Title = "t",
|
|
Status = ClaudeDo.Data.Models.TaskStatus.Idle,
|
|
CreatedAt = DateTime.UtcNow,
|
|
CommitType = "chore",
|
|
};
|
|
await _tasks.AddAsync(task);
|
|
await _sut.SetTaskConfig(task.Id, "opus", "be terse", "agent.md", 15, cancellationToken: CancellationToken.None);
|
|
|
|
var result = await _sut.SetTaskConfig(task.Id, maxTurns: 40, cancellationToken: CancellationToken.None);
|
|
|
|
Assert.True(result.Ok);
|
|
Assert.Equal("opus", result.Config!.Model);
|
|
Assert.Equal("be terse", result.Config.SystemPrompt);
|
|
Assert.Equal("agent.md", result.Config.AgentPath);
|
|
Assert.Equal(40, result.Config.MaxTurns);
|
|
|
|
var loaded = await _tasks.GetByIdAsync(task.Id);
|
|
Assert.Equal("opus", loaded!.Model);
|
|
Assert.Equal("be terse", loaded.SystemPrompt);
|
|
Assert.Equal(40, loaded.MaxTurns);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SetTaskConfig_ClearFields_ClearsOnlyNamedOverride()
|
|
{
|
|
var listId = await SeedListAsync();
|
|
var task = new TaskEntity
|
|
{
|
|
Id = Guid.NewGuid().ToString(),
|
|
ListId = listId,
|
|
Title = "t",
|
|
Status = ClaudeDo.Data.Models.TaskStatus.Idle,
|
|
CreatedAt = DateTime.UtcNow,
|
|
CommitType = "chore",
|
|
};
|
|
await _tasks.AddAsync(task);
|
|
await _sut.SetTaskConfig(task.Id, "opus", "be terse", "agent.md", 15, cancellationToken: CancellationToken.None);
|
|
|
|
var result = await _sut.SetTaskConfig(task.Id, clearFields: new[] { "systemPrompt" }, cancellationToken: CancellationToken.None);
|
|
|
|
Assert.True(result.Ok);
|
|
Assert.Equal("opus", result.Config!.Model);
|
|
Assert.Null(result.Config.SystemPrompt);
|
|
Assert.Equal("agent.md", result.Config.AgentPath);
|
|
Assert.Equal(15, result.Config.MaxTurns);
|
|
|
|
var loaded = await _tasks.GetByIdAsync(task.Id);
|
|
Assert.Equal("opus", loaded!.Model);
|
|
Assert.Null(loaded.SystemPrompt);
|
|
Assert.Equal("agent.md", loaded.AgentPath);
|
|
Assert.Equal(15, loaded.MaxTurns);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SetTaskConfig_UnknownClearField_Throws()
|
|
{
|
|
var listId = await SeedListAsync();
|
|
var task = new TaskEntity
|
|
{
|
|
Id = Guid.NewGuid().ToString(),
|
|
ListId = listId,
|
|
Title = "t",
|
|
Status = ClaudeDo.Data.Models.TaskStatus.Idle,
|
|
CreatedAt = DateTime.UtcNow,
|
|
CommitType = "chore",
|
|
};
|
|
await _tasks.AddAsync(task);
|
|
|
|
await Assert.ThrowsAsync<InvalidOperationException>(() =>
|
|
_sut.SetTaskConfig(task.Id, clearFields: new[] { "bogus" }, cancellationToken: CancellationToken.None));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetTaskConfig_NoOverrideSet_ReturnsNotFound()
|
|
{
|
|
var listId = await SeedListAsync();
|
|
var task = new TaskEntity
|
|
{
|
|
Id = Guid.NewGuid().ToString(),
|
|
ListId = listId,
|
|
Title = "t",
|
|
Status = ClaudeDo.Data.Models.TaskStatus.Idle,
|
|
CreatedAt = DateTime.UtcNow,
|
|
CommitType = "chore",
|
|
};
|
|
await _tasks.AddAsync(task);
|
|
|
|
var cfg = await _sut.GetTaskConfig(task.Id, CancellationToken.None);
|
|
|
|
Assert.False(cfg.Found);
|
|
Assert.Null(cfg.Config);
|
|
}
|
|
}
|