Bisher gab es nur AppSettings.DefaultPermissionMode global — ein Task, der plan oder acceptEdits braucht, erzwang das Umstellen der globalen Einstellung. Neue Spalten tasks.permission_mode und list_config.permission_mode, Auflösung task -> list -> global im EffectiveRunConfigResolver (den TaskRunner und get_effective_run_config gemeinsam nutzen), ComboBox mit Inherited-Badge im geteilten Agent-Editor. MigrationBaselineTests: das Fixture baute per EnsureCreated das heutige Schema und stempelte Legacy-History darauf — jede Migration nach dem Squash lief damit in 'duplicate column name'. Es migriert jetzt gezielt bis InitialCreate und prueft 'nichts pending' statt 'InitialCreate ist die einzige Zeile'.
162 lines
6.0 KiB
C#
162 lines
6.0 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;
|
|
using TaskStatus = ClaudeDo.Data.Models.TaskStatus;
|
|
|
|
namespace ClaudeDo.Worker.Tests.External;
|
|
|
|
/// <summary>
|
|
/// get_effective_run_config must report exactly what TaskRunner will actually run with, plus
|
|
/// where each value came from — task/list/preset/global — so a caller can tell a real default
|
|
/// from an override before queuing a run. See docs task "MCP: get_effective_run_config".
|
|
/// </summary>
|
|
public sealed class EffectiveRunConfigTests : IDisposable
|
|
{
|
|
private readonly DbFixture _db = new();
|
|
private readonly ClaudeDoDbContext _ctx;
|
|
private readonly ListRepository _lists;
|
|
private readonly TaskRepository _tasks;
|
|
private readonly ConfigMcpTools _sut;
|
|
|
|
public EffectiveRunConfigTests()
|
|
{
|
|
_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;
|
|
}
|
|
|
|
private async Task<TaskEntity> SeedTaskAsync(string listId, Action<TaskEntity>? configure = null)
|
|
{
|
|
var task = new TaskEntity
|
|
{
|
|
Id = Guid.NewGuid().ToString(),
|
|
ListId = listId,
|
|
Title = "t",
|
|
Status = TaskStatus.Idle,
|
|
CreatedAt = DateTime.UtcNow,
|
|
CommitType = "chore",
|
|
};
|
|
configure?.Invoke(task);
|
|
await _tasks.AddAsync(task);
|
|
return task;
|
|
}
|
|
|
|
[Fact]
|
|
public async Task No_overrides_reports_preset_and_global_sources()
|
|
{
|
|
var listId = await SeedListAsync();
|
|
var task = await SeedTaskAsync(listId);
|
|
|
|
var result = await _sut.GetEffectiveRunConfig(task.Id, CancellationToken.None);
|
|
|
|
Assert.Equal("sonnet", result.Model.Value);
|
|
Assert.Equal("global", result.Model.Source);
|
|
|
|
Assert.Equal("preset", result.MaxTurns.Source);
|
|
Assert.Equal(30, result.MaxTurns.Effective);
|
|
Assert.Equal(30, result.MaxTurns.Requested);
|
|
Assert.False(result.MaxTurns.Clamped);
|
|
|
|
Assert.Equal("high", result.Effort);
|
|
Assert.Equal("auto", result.PermissionMode);
|
|
Assert.Null(result.AgentPath.Value);
|
|
Assert.Null(result.AgentPath.Source);
|
|
Assert.Empty(result.SkillNames);
|
|
}
|
|
|
|
[Fact]
|
|
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: CancellationToken.None);
|
|
var task = await SeedTaskAsync(listId, t => { t.Model = "haiku"; t.MaxTurns = 12; });
|
|
|
|
var result = await _sut.GetEffectiveRunConfig(task.Id, CancellationToken.None);
|
|
|
|
Assert.Equal("haiku", result.Model.Value);
|
|
Assert.Equal("task", result.Model.Source);
|
|
Assert.Equal(12, result.MaxTurns.Effective);
|
|
Assert.Equal("task", result.MaxTurns.Source);
|
|
Assert.Equal("list-agent.md", result.AgentPath.Value);
|
|
Assert.Equal("list", result.AgentPath.Source);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task List_override_wins_when_no_task_override()
|
|
{
|
|
var listId = await SeedListAsync();
|
|
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);
|
|
|
|
Assert.Equal("opus", result.Model.Value);
|
|
Assert.Equal("list", result.Model.Source);
|
|
Assert.Equal(50, result.MaxTurns.Effective);
|
|
Assert.Equal("list", result.MaxTurns.Source);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Requested_max_turns_above_ceiling_is_clamped_and_both_values_reported()
|
|
{
|
|
var listId = await SeedListAsync();
|
|
var task = await SeedTaskAsync(listId, t => t.MaxTurns = 999);
|
|
|
|
var result = await _sut.GetEffectiveRunConfig(task.Id, CancellationToken.None);
|
|
|
|
Assert.Equal(999, result.MaxTurns.Requested);
|
|
Assert.Equal(80, result.MaxTurns.Effective); // AppSettingsEntity.MaxTurnsCeiling default
|
|
Assert.True(result.MaxTurns.Clamped);
|
|
Assert.Equal("task", result.MaxTurns.Source);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task System_prompt_reports_set_and_contributing_layers()
|
|
{
|
|
var listId = await SeedListAsync();
|
|
var task = await SeedTaskAsync(listId, t => t.SystemPrompt = "be terse");
|
|
|
|
var result = await _sut.GetEffectiveRunConfig(task.Id, CancellationToken.None);
|
|
|
|
Assert.True(result.SystemPrompt.Set);
|
|
Assert.Contains("task", result.SystemPrompt.Sources);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Permission_mode_task_override_beats_list_override_beats_global()
|
|
{
|
|
var listId = await SeedListAsync();
|
|
await _lists.SetConfigAsync(new ListConfigEntity { ListId = listId, PermissionMode = "acceptEdits" });
|
|
|
|
var inherits = await SeedTaskAsync(listId);
|
|
Assert.Equal("acceptEdits", (await _sut.GetEffectiveRunConfig(inherits.Id, CancellationToken.None)).PermissionMode);
|
|
|
|
var overrides = await SeedTaskAsync(listId, t => t.PermissionMode = "plan");
|
|
Assert.Equal("plan", (await _sut.GetEffectiveRunConfig(overrides.Id, CancellationToken.None)).PermissionMode);
|
|
|
|
var otherList = await SeedListAsync();
|
|
var global = await SeedTaskAsync(otherList);
|
|
Assert.Equal("auto", (await _sut.GetEffectiveRunConfig(global.Id, CancellationToken.None)).PermissionMode);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Unknown_task_throws()
|
|
{
|
|
await Assert.ThrowsAsync<InvalidOperationException>(
|
|
() => _sut.GetEffectiveRunConfig("nope", CancellationToken.None));
|
|
}
|
|
}
|