Files
ClaudeDo/tests/ClaudeDo.Data.Tests/ModelPresetsTests.cs
T
mika kuns 7d636c60dd fix(worker): normalize model alias before preset lookup, wire DefaultMaxTurns as fallback
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.
2026-08-05 08:56:23 +02:00

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"));
}
}