fix(mcp): make set_task_config/set_list_config patch instead of overwrite
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.
This commit is contained in:
+100
-11
@@ -37,7 +37,7 @@ public sealed class ConfigMcpToolsTests : IDisposable
|
||||
{
|
||||
var listId = await SeedListAsync();
|
||||
|
||||
var setResult = await _sut.SetListConfig(listId, "sonnet", "be terse", null, 42, CancellationToken.None);
|
||||
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);
|
||||
@@ -65,12 +65,41 @@ public sealed class ConfigMcpToolsTests : IDisposable
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SetListConfig_AllNull_ClearsConfig()
|
||||
public async Task SetListConfig_PartialUpdate_LeavesOtherFieldsUntouched()
|
||||
{
|
||||
var listId = await SeedListAsync();
|
||||
await _sut.SetListConfig(listId, "sonnet", null, null, null, CancellationToken.None);
|
||||
await _sut.SetListConfig(listId, "opus", "be terse", "agent.md", 30, cancellationToken: CancellationToken.None);
|
||||
|
||||
var clearResult = await _sut.SetListConfig(listId, null, null, null, null, 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);
|
||||
@@ -78,6 +107,15 @@ public sealed class ConfigMcpToolsTests : IDisposable
|
||||
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()
|
||||
{
|
||||
@@ -93,7 +131,7 @@ public sealed class ConfigMcpToolsTests : IDisposable
|
||||
};
|
||||
await _tasks.AddAsync(task);
|
||||
|
||||
var result = await _sut.SetTaskConfig(task.Id, "opus", null, null, 15, CancellationToken.None);
|
||||
var result = await _sut.SetTaskConfig(task.Id, "opus", null, null, 15, cancellationToken: CancellationToken.None);
|
||||
|
||||
Assert.True(result.Ok);
|
||||
Assert.Equal(task.Id, result.TaskId);
|
||||
@@ -106,7 +144,7 @@ public sealed class ConfigMcpToolsTests : IDisposable
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SetTaskConfig_NullField_ClearsThatOverride()
|
||||
public async Task SetTaskConfig_PartialUpdate_LeavesOtherOverridesUntouched()
|
||||
{
|
||||
var listId = await SeedListAsync();
|
||||
var task = new TaskEntity
|
||||
@@ -119,19 +157,70 @@ public sealed class ConfigMcpToolsTests : IDisposable
|
||||
CommitType = "chore",
|
||||
};
|
||||
await _tasks.AddAsync(task);
|
||||
await _sut.SetTaskConfig(task.Id, "opus", "be terse", "agent.md", 15, CancellationToken.None);
|
||||
await _sut.SetTaskConfig(task.Id, "opus", "be terse", "agent.md", 15, cancellationToken: CancellationToken.None);
|
||||
|
||||
var result = await _sut.SetTaskConfig(task.Id, "opus", null, "agent.md", null, 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.Null(result.Config.MaxTurns);
|
||||
Assert.Equal(15, result.Config.MaxTurns);
|
||||
|
||||
var loaded = await _tasks.GetByIdAsync(task.Id);
|
||||
Assert.Null(loaded!.SystemPrompt);
|
||||
Assert.Null(loaded.MaxTurns);
|
||||
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]
|
||||
|
||||
@@ -81,7 +81,7 @@ public sealed class EffectiveRunConfigTests : IDisposable
|
||||
public async Task Task_override_beats_list_override_beats_preset()
|
||||
{
|
||||
var listId = await SeedListAsync();
|
||||
await _sut.SetListConfig(listId, "opus", null, "list-agent.md", 50, CancellationToken.None);
|
||||
await _sut.SetListConfig(listId, "opus", null, "list-agent.md", 50, cancellationToken: CancellationToken.None);
|
||||
var task = await SeedTaskAsync(listId, t => { t.Model = "haiku"; t.MaxTurns = 12; });
|
||||
|
||||
var result = await _sut.GetEffectiveRunConfig(task.Id, CancellationToken.None);
|
||||
@@ -98,7 +98,7 @@ public sealed class EffectiveRunConfigTests : IDisposable
|
||||
public async Task List_override_wins_when_no_task_override()
|
||||
{
|
||||
var listId = await SeedListAsync();
|
||||
await _sut.SetListConfig(listId, "opus", null, null, 50, CancellationToken.None);
|
||||
await _sut.SetListConfig(listId, "opus", null, null, 50, cancellationToken: CancellationToken.None);
|
||||
var task = await SeedTaskAsync(listId);
|
||||
|
||||
var result = await _sut.GetEffectiveRunConfig(task.Id, CancellationToken.None);
|
||||
|
||||
Reference in New Issue
Block a user