feat(progression): use cases (slice 6)

ProgressionService holds the loaded Profile, active Playthrough and
active Attempt, so "exactly one active Attempt" has a testable home.

EnterLevel always announces LevelStarted - the reason ghost recording
and splits never worked on level 1 or after a Continue. Refuses
unreachable levels. Does not touch campaign position, so a Replay
leaves it alone.

SkillCatalogue returns null for a skill with no authored upgrades,
killing the Upgrades[0].Cost overrun. PurchaseSkill reads the current
level from the ledger first, so one Attempt can unlock then upgrade.
HandlePlayerDeath now persists - death was free if you quit.

SkillLevel.First/Next driven out here by a real caller. 53 tests.
This commit is contained in:
2026-07-28 19:10:06 +02:00
parent 003ec4b010
commit 330e10880c
8 changed files with 433 additions and 6 deletions

View File

@@ -156,4 +156,18 @@ public class PlaythroughTests
Assert.False(playthrough.HasCleared(First));
Assert.Equal(First, playthrough.CurrentLevel);
}
[Fact]
public void Committing_an_attempt_that_overspent_the_bank_is_an_impossible_state()
{
// PurchaseSkill's affordability check makes this unreachable through the application layer.
// Asserted directly so the guard is covered rather than being dead code nobody has run — if
// it ever fires, the bug is a missing affordability check, not this line.
var campaign = TwoLevels();
var playthrough = StartWith(Lives.Of(3));
var attempt = new Attempt(First);
attempt.TryRecordPurchase(SkillId.Of("brick_throw"), SkillLevel.First, Coins.Of(999));
Assert.Throws<InvalidOperationException>(() => playthrough.SettleCompletion(attempt, campaign));
}
}