mirror of
https://github.com/ivuorinen/hiha-arvio.git
synced 2026-01-26 03:14:00 +00:00
Critical fixes per spec.md requirements: - Restructure EstimateService with two-pool algorithm (gentle vs hard shake) - Expand all estimate pools to 5x spec size for more variety: * Work gentle: 7 → 35 estimates * Work hard: 12 → 60 estimates * Generic gentle: 8 → 40 estimates * Generic hard: 15 → 75 estimates * Humorous: 9 → 45 estimates - Add NSMotionUsageDescription to iOS Info.plist (required for accelerometer) - Add code coverage enforcement to test workflow (95% minimum per spec) - Update all tests to match new two-pool selection algorithm - Use 0.5 intensity threshold to choose between gentle/hard pools All 193 tests passing. Addresses critical spec deviations identified in code review.
350 lines
12 KiB
C#
350 lines
12 KiB
C#
using FluentAssertions;
|
|
using HihaArvio.Models;
|
|
using HihaArvio.Services;
|
|
using HihaArvio.Services.Interfaces;
|
|
|
|
namespace HihaArvio.Tests.Services;
|
|
|
|
public class EstimateServiceTests
|
|
{
|
|
private readonly IEstimateService _service;
|
|
|
|
public EstimateServiceTests()
|
|
{
|
|
_service = new EstimateService();
|
|
}
|
|
|
|
#region Easter Egg Tests (>15 seconds → Humorous mode)
|
|
|
|
[Fact]
|
|
public void GenerateEstimate_WhenDurationExceeds15Seconds_ShouldForceHumorousMode()
|
|
{
|
|
// Arrange
|
|
var duration = TimeSpan.FromSeconds(16);
|
|
|
|
// Act
|
|
var result = _service.GenerateEstimate(0.5, duration, EstimateMode.Work);
|
|
|
|
// Assert
|
|
result.Mode.Should().Be(EstimateMode.Humorous);
|
|
// Verify result is from the expanded humorous pool (45 items)
|
|
result.EstimateText.Should().NotBeNullOrEmpty();
|
|
}
|
|
|
|
[Fact]
|
|
public void GenerateEstimate_WhenDurationExactly15Seconds_ShouldNotTriggerEasterEgg()
|
|
{
|
|
// Arrange
|
|
var duration = TimeSpan.FromSeconds(15);
|
|
|
|
// Act
|
|
var result = _service.GenerateEstimate(0.5, duration, EstimateMode.Work);
|
|
|
|
// Assert
|
|
result.Mode.Should().Be(EstimateMode.Work);
|
|
}
|
|
|
|
[Fact]
|
|
public void GenerateEstimate_WhenDurationBelowThreshold_ShouldRespectOriginalMode()
|
|
{
|
|
// Arrange
|
|
var duration = TimeSpan.FromSeconds(10);
|
|
|
|
// Act
|
|
var result = _service.GenerateEstimate(0.5, duration, EstimateMode.Generic);
|
|
|
|
// Assert
|
|
result.Mode.Should().Be(EstimateMode.Generic);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Two-Pool Selection Tests (Per Spec: Gentle vs Hard)
|
|
|
|
[Theory]
|
|
[InlineData(0.0, EstimateMode.Work)] // Lowest intensity → gentle pool
|
|
[InlineData(0.1, EstimateMode.Work)]
|
|
[InlineData(0.3, EstimateMode.Work)]
|
|
[InlineData(0.49, EstimateMode.Work)] // Just below threshold
|
|
[InlineData(0.0, EstimateMode.Generic)]
|
|
[InlineData(0.2, EstimateMode.Generic)]
|
|
[InlineData(0.4, EstimateMode.Generic)]
|
|
public void GenerateEstimate_WithLowIntensity_ShouldSelectFromGentlePool(double intensity, EstimateMode mode)
|
|
{
|
|
// Arrange
|
|
var duration = TimeSpan.FromSeconds(5);
|
|
|
|
// Act - Generate multiple estimates to verify pool selection
|
|
var results = Enumerable.Range(0, 50)
|
|
.Select(_ => _service.GenerateEstimate(intensity, duration, mode))
|
|
.ToList();
|
|
|
|
// Assert - All results should be from gentle pool
|
|
results.Should().AllSatisfy(r =>
|
|
{
|
|
r.Mode.Should().Be(mode);
|
|
r.ShakeIntensity.Should().Be(intensity);
|
|
r.EstimateText.Should().NotBeNullOrEmpty();
|
|
});
|
|
|
|
// Should have good variety from the expanded pool
|
|
var uniqueEstimates = results.Select(r => r.EstimateText).Distinct().Count();
|
|
uniqueEstimates.Should().BeGreaterThan(5, "gentle pool should have variety");
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(0.5, EstimateMode.Work)] // At threshold → hard pool
|
|
[InlineData(0.6, EstimateMode.Work)]
|
|
[InlineData(0.8, EstimateMode.Work)]
|
|
[InlineData(1.0, EstimateMode.Work)] // Maximum intensity
|
|
[InlineData(0.5, EstimateMode.Generic)]
|
|
[InlineData(0.7, EstimateMode.Generic)]
|
|
[InlineData(0.9, EstimateMode.Generic)]
|
|
public void GenerateEstimate_WithHighIntensity_ShouldSelectFromHardPool(double intensity, EstimateMode mode)
|
|
{
|
|
// Arrange
|
|
var duration = TimeSpan.FromSeconds(5);
|
|
|
|
// Act
|
|
var results = Enumerable.Range(0, 100)
|
|
.Select(_ => _service.GenerateEstimate(intensity, duration, mode))
|
|
.ToList();
|
|
|
|
// Assert
|
|
results.Should().AllSatisfy(r =>
|
|
{
|
|
r.Mode.Should().Be(mode);
|
|
r.ShakeIntensity.Should().Be(intensity);
|
|
r.EstimateText.Should().NotBeNullOrEmpty();
|
|
});
|
|
|
|
// Hard pool should have maximum variety (larger pool)
|
|
var uniqueEstimates = results.Select(r => r.EstimateText).Distinct().Count();
|
|
uniqueEstimates.Should().BeGreaterThan(10, "hard pool should have extensive variety");
|
|
}
|
|
|
|
[Fact]
|
|
public void GenerateEstimate_ThresholdAt0Point5_ShouldProduceDistinctPoolSelections()
|
|
{
|
|
// Arrange
|
|
var duration = TimeSpan.FromSeconds(5);
|
|
|
|
// Act - Sample both sides of the threshold
|
|
var gentleResults = Enumerable.Range(0, 50)
|
|
.Select(_ => _service.GenerateEstimate(0.49, duration, EstimateMode.Work))
|
|
.Select(r => r.EstimateText)
|
|
.Distinct()
|
|
.ToHashSet();
|
|
|
|
var hardResults = Enumerable.Range(0, 50)
|
|
.Select(_ => _service.GenerateEstimate(0.5, duration, EstimateMode.Work))
|
|
.Select(r => r.EstimateText)
|
|
.Distinct()
|
|
.ToHashSet();
|
|
|
|
// Assert - The pools should have some different estimates
|
|
// (They're different pools, so overlap might be minimal or none)
|
|
var overlap = gentleResults.Intersect(hardResults).Count();
|
|
var combined = gentleResults.Union(hardResults).Count();
|
|
|
|
// With 35 gentle + 60 hard = 95 total unique estimates in Work mode
|
|
combined.Should().BeGreaterThan(20, "combined selections from both pools should show variety");
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Expanded Pool Tests (5x Spec)
|
|
|
|
[Fact]
|
|
public void GenerateEstimate_WorkMode_ShouldHaveExpandedPoolSize()
|
|
{
|
|
// Arrange
|
|
var duration = TimeSpan.FromSeconds(5);
|
|
|
|
// Act - Generate many samples to discover pool diversity
|
|
var gentleResults = Enumerable.Range(0, 200)
|
|
.Select(_ => _service.GenerateEstimate(0.3, duration, EstimateMode.Work))
|
|
.Select(r => r.EstimateText)
|
|
.Distinct()
|
|
.Count();
|
|
|
|
var hardResults = Enumerable.Range(0, 300)
|
|
.Select(_ => _service.GenerateEstimate(0.8, duration, EstimateMode.Work))
|
|
.Select(r => r.EstimateText)
|
|
.Distinct()
|
|
.Count();
|
|
|
|
// Assert - Should discover most of the expanded pools
|
|
// Gentle: 35 items (5x spec's 7), Hard: 60 items (5x spec's 12)
|
|
gentleResults.Should().BeGreaterThan(20, "Work gentle pool should have expanded size");
|
|
hardResults.Should().BeGreaterThan(30, "Work hard pool should have expanded size");
|
|
}
|
|
|
|
[Fact]
|
|
public void GenerateEstimate_GenericMode_ShouldHaveExpandedPoolSize()
|
|
{
|
|
// Arrange
|
|
var duration = TimeSpan.FromSeconds(5);
|
|
|
|
// Act
|
|
var gentleResults = Enumerable.Range(0, 200)
|
|
.Select(_ => _service.GenerateEstimate(0.3, duration, EstimateMode.Generic))
|
|
.Select(r => r.EstimateText)
|
|
.Distinct()
|
|
.Count();
|
|
|
|
var hardResults = Enumerable.Range(0, 300)
|
|
.Select(_ => _service.GenerateEstimate(0.8, duration, EstimateMode.Generic))
|
|
.Select(r => r.EstimateText)
|
|
.Distinct()
|
|
.Count();
|
|
|
|
// Assert
|
|
// Gentle: 40 items (5x spec's 8), Hard: 75 items (5x spec's 15)
|
|
gentleResults.Should().BeGreaterThan(20, "Generic gentle pool should have expanded size");
|
|
hardResults.Should().BeGreaterThan(35, "Generic hard pool should have expanded size");
|
|
}
|
|
|
|
[Fact]
|
|
public void GenerateEstimate_HumorousMode_ShouldHaveExpandedPoolSize()
|
|
{
|
|
// Arrange
|
|
var duration = TimeSpan.FromSeconds(16); // Trigger humorous via easter egg
|
|
|
|
// Act
|
|
var results = Enumerable.Range(0, 200)
|
|
.Select(_ => _service.GenerateEstimate(0.5, duration, EstimateMode.Work))
|
|
.Select(r => r.EstimateText)
|
|
.Distinct()
|
|
.Count();
|
|
|
|
// Assert - Humorous: 45 items (5x spec's 9)
|
|
results.Should().BeGreaterThan(30, "Humorous pool should have expanded size");
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region EstimateResult Metadata Tests
|
|
|
|
[Fact]
|
|
public void GenerateEstimate_ShouldSetAllEstimateResultProperties()
|
|
{
|
|
// Arrange
|
|
var intensity = 0.75;
|
|
var duration = TimeSpan.FromSeconds(8);
|
|
var mode = EstimateMode.Work;
|
|
var before = DateTimeOffset.UtcNow;
|
|
|
|
// Act
|
|
var result = _service.GenerateEstimate(intensity, duration, mode);
|
|
var after = DateTimeOffset.UtcNow;
|
|
|
|
// Assert
|
|
result.Id.Should().NotBeEmpty();
|
|
result.Timestamp.Should().BeOnOrAfter(before).And.BeOnOrBefore(after);
|
|
result.EstimateText.Should().NotBeNullOrEmpty();
|
|
result.Mode.Should().Be(mode);
|
|
result.ShakeIntensity.Should().Be(intensity);
|
|
result.ShakeDuration.Should().Be(duration);
|
|
}
|
|
|
|
[Fact]
|
|
public void GenerateEstimate_ShouldGenerateUniqueIds()
|
|
{
|
|
// Act
|
|
var result1 = _service.GenerateEstimate(0.5, TimeSpan.FromSeconds(5), EstimateMode.Work);
|
|
var result2 = _service.GenerateEstimate(0.5, TimeSpan.FromSeconds(5), EstimateMode.Work);
|
|
|
|
// Assert
|
|
result1.Id.Should().NotBe(result2.Id);
|
|
}
|
|
|
|
[Fact]
|
|
public void GenerateEstimate_ShouldProduceRandomResults()
|
|
{
|
|
// Act - Generate many estimates to verify randomness
|
|
var results = Enumerable.Range(0, 100)
|
|
.Select(_ => _service.GenerateEstimate(0.8, TimeSpan.FromSeconds(5), EstimateMode.Work))
|
|
.Select(r => r.EstimateText)
|
|
.ToList();
|
|
|
|
// Assert - Should have multiple different estimates (not always the same)
|
|
var uniqueCount = results.Distinct().Count();
|
|
uniqueCount.Should().BeGreaterThan(5, "service should produce varied random results");
|
|
}
|
|
|
|
[Fact]
|
|
public void GenerateEstimate_ShouldUseCryptographicallySecureRNG()
|
|
{
|
|
// Act - Generate large sample to test distribution
|
|
var results = Enumerable.Range(0, 1000)
|
|
.Select(_ => _service.GenerateEstimate(0.8, TimeSpan.FromSeconds(5), EstimateMode.Work))
|
|
.Select(r => r.EstimateText)
|
|
.GroupBy(x => x)
|
|
.Select(g => g.Count())
|
|
.ToList();
|
|
|
|
// Assert - Distribution should be reasonably uniform (no single value dominates)
|
|
var maxFrequency = results.Max();
|
|
var avgFrequency = results.Average();
|
|
|
|
// No single estimate should appear more than 3x the average
|
|
(maxFrequency / avgFrequency).Should().BeLessThan(3, "RNG should produce reasonably uniform distribution");
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Edge Cases
|
|
|
|
[Fact]
|
|
public void GenerateEstimate_WithZeroIntensity_ShouldWork()
|
|
{
|
|
// Act
|
|
var result = _service.GenerateEstimate(0.0, TimeSpan.FromSeconds(5), EstimateMode.Work);
|
|
|
|
// Assert
|
|
result.Should().NotBeNull();
|
|
result.EstimateText.Should().NotBeNullOrEmpty();
|
|
result.ShakeIntensity.Should().Be(0.0);
|
|
}
|
|
|
|
[Fact]
|
|
public void GenerateEstimate_WithMaxIntensity_ShouldWork()
|
|
{
|
|
// Act
|
|
var result = _service.GenerateEstimate(1.0, TimeSpan.FromSeconds(5), EstimateMode.Work);
|
|
|
|
// Assert
|
|
result.Should().NotBeNull();
|
|
result.EstimateText.Should().NotBeNullOrEmpty();
|
|
result.ShakeIntensity.Should().Be(1.0);
|
|
}
|
|
|
|
[Fact]
|
|
public void GenerateEstimate_WithZeroDuration_ShouldWork()
|
|
{
|
|
// Act
|
|
var result = _service.GenerateEstimate(0.5, TimeSpan.Zero, EstimateMode.Work);
|
|
|
|
// Assert
|
|
result.Should().NotBeNull();
|
|
result.ShakeDuration.Should().Be(TimeSpan.Zero);
|
|
}
|
|
|
|
[Fact]
|
|
public void GenerateEstimate_AllModes_ShouldReturnValidEstimates()
|
|
{
|
|
// Act & Assert for each mode
|
|
foreach (EstimateMode mode in Enum.GetValues(typeof(EstimateMode)))
|
|
{
|
|
var result = _service.GenerateEstimate(0.5, TimeSpan.FromSeconds(5), mode);
|
|
|
|
result.Should().NotBeNull();
|
|
result.EstimateText.Should().NotBeNullOrEmpty();
|
|
result.Mode.Should().Be(mode);
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
}
|