using NeonSprawl.Server.Game.Mastery; using NeonSprawl.Server.Game.PositionState; namespace NeonSprawl.Server.Tests.Game.Skills; /// NEO-42: craft/refine completion hook → refine XP via shared NEO-38 grant path (sourceKind: activity). public sealed class RefineActivitySkillXpGrantTests { [Fact] public async Task GrantOnSuccessfulCraftOrRefine_WhenCatalogAllowsActivity_ShouldIncreaseRefineXp() { // Arrange — dev player is seeded on the in-memory progression store (NEO-38). await using var factory = new InMemoryWebApplicationFactory(); _ = factory.CreateClient(); using var scope = factory.Services.CreateScope(); var registry = scope.ServiceProvider.GetRequiredService(); var xpStore = scope.ServiceProvider.GetRequiredService(); var levelCurve = scope.ServiceProvider.GetRequiredService(); var perkEngine = scope.ServiceProvider.GetRequiredService(); // Act RefineActivitySkillXpGrant.GrantOnSuccessfulCraftOrRefine( "dev-local-1", registry, xpStore, levelCurve, perkEngine); // Assert var totals = xpStore.GetXpTotals("dev-local-1"); Assert.True(totals.TryGetValue("refine", out var xp)); Assert.Equal(RefineSkillXpConstants.ActivityXpPerCraftRefineCompletion, xp); } [Fact] public async Task GrantOnSuccessfulCraftOrRefine_WhenCatalogAllowsActivity_ShouldMatchSkillProgressionGet() { // Arrange await using var factory = new InMemoryWebApplicationFactory(); var client = factory.CreateClient(); var move = new MoveCommandRequest { SchemaVersion = MoveCommandRequest.CurrentSchemaVersion, Target = new PositionVector { X = 0.5, Y = 0.9, Z = 0.5 }, }; await client.PostAsJsonAsync("/game/players/dev-local-1/move", move); using var scope = factory.Services.CreateScope(); var registry = scope.ServiceProvider.GetRequiredService(); var xpStore = scope.ServiceProvider.GetRequiredService(); var levelCurve = scope.ServiceProvider.GetRequiredService(); var perkEngine = scope.ServiceProvider.GetRequiredService(); // Act RefineActivitySkillXpGrant.GrantOnSuccessfulCraftOrRefine( "dev-local-1", registry, xpStore, levelCurve, perkEngine); var snapshot = await client.GetAsync("/game/players/dev-local-1/skill-progression"); // Assert Assert.Equal(HttpStatusCode.OK, snapshot.StatusCode); var body = await snapshot.Content.ReadFromJsonAsync(); Assert.NotNull(body); var refine = body!.Skills!.Single(static s => s.Id == "refine"); Assert.Equal(RefineSkillXpConstants.ActivityXpPerCraftRefineCompletion, refine.Xp); } [Fact] public async Task GrantOnSuccessfulCraftOrRefine_WhenRefineDisallowsActivity_ShouldLeaveRefineXpZero() { // Arrange await using var factory = new RefineActivityDeniedRegistryWebApplicationFactory(); _ = factory.CreateClient(); using var scope = factory.Services.CreateScope(); var registry = scope.ServiceProvider.GetRequiredService(); var xpStore = scope.ServiceProvider.GetRequiredService(); var levelCurve = scope.ServiceProvider.GetRequiredService(); var perkEngine = scope.ServiceProvider.GetRequiredService(); // Act RefineActivitySkillXpGrant.GrantOnSuccessfulCraftOrRefine( "dev-local-1", registry, xpStore, levelCurve, perkEngine); // Assert — deny returns before TryApplyXpDelta; refine row must not appear in stored XP map. var totals = xpStore.GetXpTotals("dev-local-1"); Assert.False(totals.ContainsKey("refine")); } }