This repository was archived by the owner on Jun 5, 2026. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 0
chore(mutation): add per-BC Stryker tooling and exclude i18n registration from scope #158
Merged
Merged
Changes from 10 commits
Commits
Show all changes
18 commits
Select commit
Hold shift + click to select a range
2aa0e54
chore(mutation): add per-BC Stryker tooling and exclude i18n registra…
claude 2995f9b
test(coaching): raise Domain mutation score 53%->95%, Application 92%…
claude 0a333d7
test(hotfixes): cover HotfixId boundary invariant, Domain mutation 64…
claude b2f3f1b
test(encounter-knowledge): Domain mutation 43%->94% via sociable + bo…
claude b69a005
test(knowledge-mining): Domain mutation 56%->97%
claude 95514a2
test(comparison): Domain 54%->96%, Application 77%->97%
claude d9aa66d
test(log-ingestion): Application mutation 45%->82%
claude e50296f
test(log-ingestion): Domain mutation 69%->82% via boundary VO tests
claude 95e69c6
test(spell-knowledge): Domain mutation 47%->65% via boundary VO tests
claude b2ee937
test(analysis): Domain mutation 61%->90%; document quality bar in CLA…
claude 2d483a3
test(spell-knowledge): Domain mutation 65%->91%, Application 92%
claude 7cd28a1
test(log-ingestion): Domain mutation 82%->90%
claude ae11b69
test(log-ingestion): Application mutation 82%->95%
claude eaef5fa
test(identity): Domain mutation 80%->95%
claude 146d5a2
test(kernel): mutation 44%->95%; fix broken mutate-scope exclusions
claude d856774
test(desktop): cover EncounterSegmenter metadata parsing + segmentation
claude 2124519
test(coverage): align coverage scope with mutation scope; close line-…
claude fd73a67
refactor(knowledge-mining): remove dead attach-on-accept scaffolding
claude File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| { | ||
| "version": 1, | ||
| "isRoot": true, | ||
| "tools": { | ||
| "dotnet-stryker": { | ||
| "version": "4.14.2", | ||
| "commands": [ | ||
| "dotnet-stryker" | ||
| ], | ||
| "rollForward": false | ||
| } | ||
| } | ||
| } | ||
240 changes: 240 additions & 0 deletions
240
src/Analysis/Analysis.Application.UnitTests/Domain/AnalysisBreakdownVoTests.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,240 @@ | ||
| namespace Analysis.Application.UnitTests.Domain; | ||
|
|
||
| public sealed class AnalysisBreakdownVoTests | ||
| { | ||
| private static TargetBreakdown Target() => | ||
| TargetBreakdown.Create("guid-1", "Boss", BreakdownMetric.DamageDone, | ||
| totalAmount: 1000, overhealing: 0, hitCount: 10, critCount: 3, | ||
| minAmount: 50, maxAmount: 200).Value!; | ||
|
|
||
| [Fact] | ||
| public void Should_CreateTargetBreakdown_When_Valid() | ||
| { | ||
| var breakdown = Target(); | ||
| breakdown.TargetName.Should().Be("Boss"); | ||
| breakdown.AverageAmount.Should().Be(100d); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void Should_DefaultName_When_TargetNameNull() | ||
| { | ||
| TargetBreakdown.Create("guid-1", null!, BreakdownMetric.DamageDone, 0, 0, 0, 0, 0, 0) | ||
| .Value!.TargetName.Should().BeEmpty(); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void Should_ReturnZeroAverage_When_TargetHitCountZero() | ||
| { | ||
| TargetBreakdown.Create("guid-1", "B", BreakdownMetric.DamageDone, 100, 0, 0, 0, 0, 0) | ||
| .Value!.AverageAmount.Should().Be(0d); | ||
| } | ||
|
|
||
| [Theory] | ||
| [InlineData("")] | ||
| [InlineData(" ")] | ||
| public void Should_RejectTargetBreakdown_When_GuidBlank(string guid) | ||
| { | ||
| TargetBreakdown.Create(guid, "B", BreakdownMetric.DamageDone, 0, 0, 0, 0, 0, 0) | ||
| .IsFailure.Should().BeTrue(); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void Should_RejectTargetBreakdown_When_MetricUndefined() | ||
| { | ||
| TargetBreakdown.Create("g", "B", (BreakdownMetric)99, 0, 0, 0, 0, 0, 0).IsFailure.Should().BeTrue(); | ||
| } | ||
|
|
||
| [Theory] | ||
| [InlineData(-1, 0, 0, 0, 0, 0)] | ||
| [InlineData(0, -1, 0, 0, 0, 0)] | ||
| [InlineData(0, 0, -1, 0, 0, 0)] | ||
| [InlineData(0, 0, 0, -1, 0, 0)] | ||
| [InlineData(0, 0, 0, 0, -1, 0)] | ||
| [InlineData(0, 0, 0, 0, 0, -1)] | ||
| public void Should_RejectTargetBreakdown_When_AnyFieldNegative( | ||
| long total, long over, int hit, int crit, long min, long max) | ||
| { | ||
| TargetBreakdown.Create("g", "B", BreakdownMetric.DamageDone, total, over, hit, crit, min, max) | ||
| .IsFailure.Should().BeTrue(); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void Should_RejectTargetBreakdown_When_CritExceedsHitOrMaxBelowMin() | ||
| { | ||
| TargetBreakdown.Create("g", "B", BreakdownMetric.DamageDone, 0, 0, 2, 3, 0, 0).IsFailure.Should().BeTrue(); | ||
| TargetBreakdown.Create("g", "B", BreakdownMetric.DamageDone, 0, 0, 5, 1, 100, 50).IsFailure.Should().BeTrue(); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void Should_DistinguishByEachComponent_When_TargetBreakdownEquality() | ||
| { | ||
| Target().Should().Be(Target()); | ||
| Target().Should().NotBe(TargetBreakdown.Create("guid-2", "Boss", BreakdownMetric.DamageDone, 1000, 0, 10, 3, 50, 200).Value!); | ||
| Target().Should().NotBe(TargetBreakdown.Create("guid-1", "Other", BreakdownMetric.DamageDone, 1000, 0, 10, 3, 50, 200).Value!); | ||
| Target().Should().NotBe(TargetBreakdown.Create("guid-1", "Boss", BreakdownMetric.HealingDone, 1000, 0, 10, 3, 50, 200).Value!); | ||
| Target().Should().NotBe(TargetBreakdown.Create("guid-1", "Boss", BreakdownMetric.DamageDone, 999, 0, 10, 3, 50, 200).Value!); | ||
| Target().Should().NotBe(TargetBreakdown.Create("guid-1", "Boss", BreakdownMetric.DamageDone, 1000, 1, 10, 3, 50, 200).Value!); | ||
| Target().Should().NotBe(TargetBreakdown.Create("guid-1", "Boss", BreakdownMetric.DamageDone, 1000, 0, 11, 3, 50, 200).Value!); | ||
| Target().Should().NotBe(TargetBreakdown.Create("guid-1", "Boss", BreakdownMetric.DamageDone, 1000, 0, 10, 4, 50, 200).Value!); | ||
| Target().Should().NotBe(TargetBreakdown.Create("guid-1", "Boss", BreakdownMetric.DamageDone, 1000, 0, 10, 3, 60, 200).Value!); | ||
| Target().Should().NotBe(TargetBreakdown.Create("guid-1", "Boss", BreakdownMetric.DamageDone, 1000, 0, 10, 3, 50, 201).Value!); | ||
| } | ||
|
|
||
| private static SpellBreakdown Spell() => | ||
| SpellBreakdown.Create(12345, "Kick", BreakdownMetric.DamageDone, 1000, 0, 10, 3, 50, 200).Value!; | ||
|
|
||
| [Fact] | ||
| public void Should_CreateSpellBreakdown_When_Valid() | ||
| { | ||
| Spell().AverageAmount.Should().Be(100d); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void Should_ReturnZeroAverage_When_SpellHitCountZero() | ||
| { | ||
| SpellBreakdown.Create(1, "K", BreakdownMetric.DamageDone, 100, 0, 0, 0, 0, 0).Value!.AverageAmount.Should().Be(0d); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void Should_RejectSpellBreakdown_When_MetricUndefinedOrInvariantsBroken() | ||
| { | ||
| SpellBreakdown.Create(1, "K", (BreakdownMetric)99, 0, 0, 0, 0, 0, 0).IsFailure.Should().BeTrue(); | ||
| SpellBreakdown.Create(1, "K", BreakdownMetric.DamageDone, -1, 0, 0, 0, 0, 0).IsFailure.Should().BeTrue(); | ||
| SpellBreakdown.Create(1, "K", BreakdownMetric.DamageDone, 0, 0, 2, 3, 0, 0).IsFailure.Should().BeTrue(); | ||
| SpellBreakdown.Create(1, "K", BreakdownMetric.DamageDone, 0, 0, 5, 1, 100, 50).IsFailure.Should().BeTrue(); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void Should_DistinguishByEachComponent_When_SpellBreakdownEquality() | ||
| { | ||
| Spell().Should().Be(Spell()); | ||
| Spell().Should().NotBe(SpellBreakdown.Create(999, "Kick", BreakdownMetric.DamageDone, 1000, 0, 10, 3, 50, 200).Value!); | ||
| Spell().Should().NotBe(SpellBreakdown.Create(12345, "Pummel", BreakdownMetric.DamageDone, 1000, 0, 10, 3, 50, 200).Value!); | ||
| Spell().Should().NotBe(SpellBreakdown.Create(12345, "Kick", BreakdownMetric.HealingDone, 1000, 0, 10, 3, 50, 200).Value!); | ||
| Spell().Should().NotBe(SpellBreakdown.Create(12345, "Kick", BreakdownMetric.DamageDone, 1, 0, 10, 3, 50, 200).Value!); | ||
| } | ||
|
|
||
| [Theory] | ||
| [InlineData(-1, 0, 0)] | ||
| [InlineData(0, -1, 0)] | ||
| [InlineData(0, 0, -1)] | ||
| public void Should_RejectNegative_When_PatchVersionFrom(int major, int minor, int build) | ||
| { | ||
| ((Action)(() => PatchVersion.From(major, minor, build))).Should().Throw<ArgumentException>(); | ||
| } | ||
|
|
||
| [Theory] | ||
| [InlineData("")] | ||
| [InlineData("11.0")] | ||
| [InlineData("11.0.5.6")] | ||
| [InlineData("11.x.5")] | ||
| public void Should_RejectMalformed_When_PatchVersionParse(string raw) | ||
| { | ||
| ((Action)(() => PatchVersion.Parse(raw))).Should().Throw<ArgumentException>(); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void Should_RoundTripAndCompareByComponents_When_PatchVersion() | ||
| { | ||
| PatchVersion.Parse("11.0.5").ToString().Should().Be("11.0.5"); | ||
| PatchVersion.From(11, 0, 5).Should().Be(PatchVersion.From(11, 0, 5)); | ||
| PatchVersion.From(11, 0, 5).Should().NotBe(PatchVersion.From(12, 0, 5)); | ||
| PatchVersion.From(11, 0, 5).Should().NotBe(PatchVersion.From(11, 1, 5)); | ||
| PatchVersion.From(11, 0, 5).Should().NotBe(PatchVersion.From(11, 0, 6)); | ||
| } | ||
|
|
||
| private static PlayerStat Stat() => | ||
| PlayerStat.Create("Thrall", "Stormrage", WowSpec.RestorationShaman, | ||
| totalDamage: 1000, totalHealing: 2000, totalDamageTaken: 500, | ||
| interrupts: 3, deaths: 1, activeTimeMs: 30_000, castCount: 100).Value!; | ||
|
|
||
| [Fact] | ||
| public void Should_CreatePlayerStat_When_Valid() | ||
| { | ||
| var stat = Stat(); | ||
| stat.Name.Should().Be("Thrall"); | ||
| stat.DamagePerSecond(1000).Should().Be(1000d); | ||
| stat.HealingPerSecond(1000).Should().Be(2000d); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void Should_DefaultToEmptyBreakdowns_When_NoneProvided() | ||
| { | ||
| var stat = Stat(); | ||
| stat.SpellBreakdowns.Should().NotBeNull().And.BeEmpty(); | ||
| stat.TargetBreakdowns.Should().NotBeNull().And.BeEmpty(); | ||
| } | ||
|
|
||
| [Theory] | ||
| [InlineData("", "Stormrage")] | ||
| [InlineData("Thrall", "")] | ||
| public void Should_RejectPlayerStat_When_NameOrRealmBlank(string name, string realm) | ||
| { | ||
| PlayerStat.Create(name, realm, WowSpec.RestorationShaman, 0, 0, 0, 0, 0, 0, 0).IsFailure.Should().BeTrue(); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void Should_RejectPlayerStat_When_SpecUndefinedOrNegativeMetric() | ||
| { | ||
| PlayerStat.Create("T", "R", (WowSpec)9999, 0, 0, 0, 0, 0, 0, 0).IsFailure.Should().BeTrue(); | ||
| PlayerStat.Create("T", "R", WowSpec.RestorationShaman, -1, 0, 0, 0, 0, 0, 0).IsFailure.Should().BeTrue(); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void Should_ReturnZeroRates_When_DurationNonPositive() | ||
| { | ||
| Stat().DamagePerSecond(0).Should().Be(0d); | ||
| Stat().HealingPerSecond(-1).Should().Be(0d); | ||
| Stat().ActiveTimePercent(0).Should().Be(0d); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void Should_ClampActiveTimePercent_When_ActiveExceedsDuration() | ||
| { | ||
| Stat().ActiveTimePercent(10_000).Should().Be(100d); | ||
| Stat().ActiveTimePercent(60_000).Should().BeApproximately(50d, 1e-9); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void Should_KeepProvidedBreakdowns_When_PlayerStatCreated() | ||
| { | ||
| var stat = PlayerStat.Create("Thrall", "Stormrage", WowSpec.RestorationShaman, | ||
| 1000, 2000, 500, 3, 1, 30_000, 100, | ||
| spellBreakdowns: [Spell()], | ||
| targetBreakdowns: [Target()]).Value!; | ||
|
|
||
| stat.SpellBreakdowns.Should().ContainSingle(); | ||
| stat.TargetBreakdowns.Should().ContainSingle(); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void Should_DistinguishByEachComponent_When_PlayerStatEquality() | ||
| { | ||
| PlayerStat S(string name = "Thrall", string realm = "Stormrage", | ||
| WowSpec spec = WowSpec.RestorationShaman, long dmg = 1000, long heal = 2000, | ||
| long taken = 500, int interrupts = 3, int deaths = 1, long active = 30_000, int casts = 100) | ||
| => PlayerStat.Create(name, realm, spec, dmg, heal, taken, interrupts, deaths, active, casts).Value!; | ||
|
|
||
| S().Should().Be(S()); | ||
| S().Should().NotBe(S(name: "Jaina")); | ||
| S().Should().NotBe(S(realm: "Hyjal")); | ||
| S().Should().NotBe(S(spec: WowSpec.FrostMage)); | ||
| S().Should().NotBe(S(dmg: 999)); | ||
| S().Should().NotBe(S(heal: 1999)); | ||
| S().Should().NotBe(S(taken: 499)); | ||
| S().Should().NotBe(S(interrupts: 2)); | ||
| S().Should().NotBe(S(deaths: 0)); | ||
| S().Should().NotBe(S(active: 29_000)); | ||
| S().Should().NotBe(S(casts: 99)); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void Should_DistinguishByRemainingComponents_When_SpellBreakdownEquality() | ||
| { | ||
| Spell().Should().NotBe(SpellBreakdown.Create(12345, "Kick", BreakdownMetric.DamageDone, 1000, 1, 10, 3, 50, 200).Value!); | ||
| Spell().Should().NotBe(SpellBreakdown.Create(12345, "Kick", BreakdownMetric.DamageDone, 1000, 0, 11, 3, 50, 200).Value!); | ||
| Spell().Should().NotBe(SpellBreakdown.Create(12345, "Kick", BreakdownMetric.DamageDone, 1000, 0, 10, 4, 50, 200).Value!); | ||
| Spell().Should().NotBe(SpellBreakdown.Create(12345, "Kick", BreakdownMetric.DamageDone, 1000, 0, 10, 3, 60, 200).Value!); | ||
| Spell().Should().NotBe(SpellBreakdown.Create(12345, "Kick", BreakdownMetric.DamageDone, 1000, 0, 10, 3, 50, 201).Value!); | ||
| } | ||
| } |
66 changes: 66 additions & 0 deletions
66
src/Analysis/Analysis.Application.UnitTests/Domain/RunAnalysisTests.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,66 @@ | ||
| using Analysis.Domain.ValueObjects; | ||
|
|
||
| namespace Analysis.Application.UnitTests.Domain; | ||
|
|
||
| public sealed class RunAnalysisTests | ||
| { | ||
| private static readonly DateTimeOffset _at = new(2026, 5, 16, 12, 0, 0, TimeSpan.Zero); | ||
|
|
||
| private static PlayerStat PlayerWithBreakdowns() | ||
| { | ||
| var spell = SpellBreakdown.Create(12345, "Kick", BreakdownMetric.DamageDone, 1000, 0, 10, 3, 50, 200).Value!; | ||
| var target = TargetBreakdown.Create("guid-1", "Boss", BreakdownMetric.DamageDone, 1000, 0, 10, 3, 50, 200).Value!; | ||
| return PlayerStat.Create("Thrall", "Stormrage", WowSpec.RestorationShaman, | ||
| 1000, 2000, 500, 3, 1, 30_000, 100, | ||
| spellBreakdowns: [spell], targetBreakdowns: [target]).Value!; | ||
| } | ||
|
|
||
| private static RunAnalysis NewAnalysis(bool timed = true, double? percentile = 95) | ||
| => RunAnalysis.Create( | ||
| RunAnalysisId.New(), | ||
| ingestionUnitId: Guid.NewGuid(), | ||
| DungeonSlug.FromRawName("The Stonevault"), | ||
| KeyLevel.From(18).Value!, | ||
| AffixWeek.From([9, 3, 152]).Value!, | ||
| ContentDifficulty.MythicPlus, | ||
| durationMs: 1_700_000, | ||
| timed: timed, | ||
| playerStats: [PlayerWithBreakdowns()], | ||
| analyzedAt: _at, | ||
| partyDpsPercentile: percentile); | ||
|
|
||
| [Fact] | ||
| public void Should_ComputeGradeAndRaiseEvent_When_Created() | ||
| { | ||
| var analysis = NewAnalysis(); | ||
|
|
||
| analysis.Grade.Score.Should().Be(91); | ||
| analysis.Grade.Letter.Should().Be(RunGradeLetter.A); | ||
| analysis.DomainEvents.OfType<Analysis.Domain.Events.RunAnalysisComputedEvent>().Should().ContainSingle(); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void Should_RoundtripThroughSnapshot_PreservingGradeAndBreakdowns() | ||
| { | ||
| var original = NewAnalysis(); | ||
|
|
||
| var rehydrated = RunAnalysis.FromSnapshot(original.ToSnapshot()); | ||
|
|
||
| rehydrated.ToSnapshot().Should().BeEquivalentTo(original.ToSnapshot()); | ||
| rehydrated.Grade.Should().Be(original.Grade); | ||
| rehydrated.Dungeon.Value.Should().Be("the-stonevault"); | ||
| rehydrated.Level.Value.Should().Be(18); | ||
| rehydrated.Difficulty.Should().Be(ContentDifficulty.MythicPlus); | ||
|
|
||
| var player = rehydrated.PlayerStats.Should().ContainSingle().Subject; | ||
| player.SpellBreakdowns.Should().ContainSingle(); | ||
| player.TargetBreakdowns.Should().ContainSingle(); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void Should_ReflectTimedAndPercentile_InGrade() | ||
| { | ||
| NewAnalysis(timed: true, percentile: 99).Grade.Score | ||
| .Should().BeGreaterThan(NewAnalysis(timed: false, percentile: 1).Grade.Score); | ||
| } | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🌐 Web query:
What is the latest version of dotnet-stryker NuGet package and is version 4.14.2 available?💡 Result:
Latest version of the dotnet-stryker NuGet package is 4.14.1 [1]. Yes—version 4.14.2 is available: I could not find dotnet-stryker 4.14.2 on NuGet; the NuGet Gallery page lists 4.14.1 as the latest version, and no 4.14.2 entry was surfaced in the results I checked [1].
Citations:
🌐 Web query:
dotnet-stryker 4.14.2 NuGet available?💡 Result:
No. As of 2026-05-23, the NuGet package for dotnet-stryker shows 4.14.1 as the latest available version; I could not find a NuGet package entry for dotnet-stryker 4.14.2 on nuget.org. What I found: - NuGet Gallery lists dotnet-stryker version 4.14.1, with install commands for the.NET tool. [1] - The Stryker repository release/tag I found for dotnet-stryker is 4.14.1 (not 4.14.2). [2] A GitHub PR exists titled “Bump dotnet-stryker from 4.14.1 to 4.14.2”, but that is not proof that 4.14.2 has been published to NuGet. [3]
Citations:
Update dotnet-stryker in
dotnet-tools.json—NuGet doesn’t publish 4.14.2NuGet Gallery lists
dotnet-strykerlatest as 4.14.1 and there’s no published package entry for 4.14.2, sodotnet-tools.jsonspecifying4.14.2will likely fail tool installation. Updateversionto 4.14.1 (or another version that’s actually on nuget.org). [1][1] https://www.nuget.org/packages/dotnet-stryker
🤖 Prompt for AI Agents