A full model id like claude-sonnet-4-6 in list_config.model never matched any ModelPresets row (only bare aliases did), so every run under it landed on the hardcoded 30-turn/DefaultLevel fallback instead of the sonnet preset row - this is what killed two autonomous tasks at the 30-turn limit. ModelRegistry.TryNormalizeAlias (non-throwing: exact match, then substring match against a full model id) lets ModelPresets.For resolve a full model id to its alias's preset row. For a model that still doesn't resolve, the hardcoded 30 is replaced by a caller-supplied fallbackMaxTurns, and TaskRunner now passes AppSettings.DefaultMaxTurns there - so that setting has a real effect instead of being dead, matching the direction already noted in docs/open.md.
130 lines
4.3 KiB
C#
130 lines
4.3 KiB
C#
using ClaudeDo.Data.Models;
|
|
|
|
namespace ClaudeDo.Data.Tests;
|
|
|
|
public class ModelPresetsTests
|
|
{
|
|
[Fact]
|
|
public void Defaults_cover_every_known_alias()
|
|
{
|
|
Assert.Equal(
|
|
ModelRegistry.Aliases.OrderBy(a => a),
|
|
ModelPresets.Defaults.Select(p => p.Model).OrderBy(m => m));
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(null)]
|
|
[InlineData("")]
|
|
[InlineData(" ")]
|
|
[InlineData("not json at all")]
|
|
public void Parse_falls_back_to_defaults_when_unusable(string? json)
|
|
{
|
|
Assert.Equal(ModelPresets.Defaults, ModelPresets.Parse(json));
|
|
}
|
|
|
|
[Fact]
|
|
public void Serialize_roundtrips()
|
|
{
|
|
var edited = new[] { new ModelPreset("opus", "max", 55) };
|
|
var parsed = ModelPresets.Parse(ModelPresets.Serialize(edited));
|
|
|
|
Assert.Equal("max", ModelPresets.For(parsed, "opus").Effort);
|
|
Assert.Equal(55, ModelPresets.For(parsed, "opus").MaxTurns);
|
|
}
|
|
|
|
[Fact]
|
|
public void Serialize_fills_missing_aliases_from_defaults()
|
|
{
|
|
var parsed = ModelPresets.Parse(ModelPresets.Serialize(new[] { new ModelPreset("opus", "max", 55) }));
|
|
|
|
Assert.Equal(ModelRegistry.Aliases.Count, parsed.Count);
|
|
Assert.Equal(ModelPresets.For(ModelPresets.Defaults, "haiku"), ModelPresets.For(parsed, "haiku"));
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("nonsense", 30)] // unknown effort → the default row's effort
|
|
[InlineData("high", 0)] // turns out of range → the default row's turns
|
|
[InlineData("high", 999)]
|
|
public void Serialize_rejects_out_of_range_values(string effort, int turns)
|
|
{
|
|
var fallback = ModelPresets.For(ModelPresets.Defaults, "sonnet");
|
|
var parsed = ModelPresets.Parse(ModelPresets.Serialize(new[] { new ModelPreset("sonnet", effort, turns) }));
|
|
var row = ModelPresets.For(parsed, "sonnet");
|
|
|
|
if (effort == "nonsense") Assert.Equal(fallback.Effort, row.Effort);
|
|
if (turns is 0 or 999) Assert.Equal(fallback.MaxTurns, row.MaxTurns);
|
|
}
|
|
|
|
[Fact]
|
|
public void For_unknown_model_yields_a_usable_row()
|
|
{
|
|
var row = ModelPresets.For(ModelPresets.Defaults, "some-future-model");
|
|
|
|
Assert.Equal("some-future-model", row.Model);
|
|
Assert.Contains(row.Effort, EffortRegistry.Levels);
|
|
Assert.True(row.MaxTurns > 0);
|
|
}
|
|
|
|
[Fact]
|
|
public void For_unknown_model_uses_the_supplied_fallback_max_turns_instead_of_a_hardcoded_number()
|
|
{
|
|
var row = ModelPresets.For(ModelPresets.Defaults, "some-future-model", fallbackMaxTurns: 100);
|
|
|
|
Assert.Equal(100, row.MaxTurns);
|
|
}
|
|
|
|
[Fact]
|
|
public void For_a_full_model_id_resolves_to_its_alias_preset_row_not_the_unknown_fallback()
|
|
{
|
|
var sonnetPreset = ModelPresets.For(ModelPresets.Defaults, "sonnet");
|
|
|
|
var row = ModelPresets.For(ModelPresets.Defaults, "claude-sonnet-4-6");
|
|
|
|
Assert.Equal(sonnetPreset.Effort, row.Effort);
|
|
Assert.Equal(sonnetPreset.MaxTurns, row.MaxTurns);
|
|
}
|
|
|
|
[Fact]
|
|
public void For_a_full_model_id_honors_a_customized_preset_row_for_its_alias()
|
|
{
|
|
var parsed = ModelPresets.Parse(ModelPresets.Serialize(new[] { new ModelPreset("sonnet", "xhigh", 77) }));
|
|
|
|
var row = ModelPresets.For(parsed, "claude-sonnet-4-6");
|
|
|
|
Assert.Equal("xhigh", row.Effort);
|
|
Assert.Equal(77, row.MaxTurns);
|
|
}
|
|
|
|
[Fact]
|
|
public void For_unknown_model_never_throws_and_still_starts_a_run()
|
|
{
|
|
var exception = Record.Exception(() => ModelPresets.For(ModelPresets.Defaults, "totally-unrecognized-model"));
|
|
|
|
Assert.Null(exception);
|
|
}
|
|
|
|
[Fact]
|
|
public void For_null_model_uses_the_default_alias()
|
|
{
|
|
Assert.Equal(
|
|
ModelPresets.For(ModelPresets.Defaults, ModelRegistry.DefaultAlias),
|
|
ModelPresets.For(ModelPresets.Defaults, null));
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("low", "low")]
|
|
[InlineData("XHIGH", "xhigh")]
|
|
[InlineData(" max ", "max")]
|
|
public void NormalizeLevel_canonicalizes(string input, string expected)
|
|
{
|
|
Assert.Equal(expected, EffortRegistry.NormalizeLevel(input));
|
|
}
|
|
|
|
[Fact]
|
|
public void NormalizeLevel_blank_means_unset_and_unknown_throws()
|
|
{
|
|
Assert.Null(EffortRegistry.NormalizeLevel(null));
|
|
Assert.Throws<ArgumentException>(() => EffortRegistry.NormalizeLevel("gigantic"));
|
|
}
|
|
}
|