DecayTest.php 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100
  1. <?php
  2. declare(strict_types=1);
  3. namespace App\Tests\Unit\Reputation;
  4. use App\Domain\Reputation\Decay;
  5. use App\Domain\Reputation\DecayFunction;
  6. use PHPUnit\Framework\TestCase;
  7. /**
  8. * Hand-computed reference values for both decay shapes. The exponential
  9. * cases hit half-life multiples so the answers are clean fractions.
  10. */
  11. final class DecayTest extends TestCase
  12. {
  13. public function testLinearAtZeroReturnsFullWeight(): void
  14. {
  15. self::assertSame(1.0, Decay::value(DecayFunction::Linear, 0.0, 30.0));
  16. }
  17. public function testLinearMidwayReturnsHalf(): void
  18. {
  19. self::assertEqualsWithDelta(0.5, Decay::value(DecayFunction::Linear, 15.0, 30.0), 1e-9);
  20. }
  21. public function testLinearAtOrPastDecayParamClampsToZero(): void
  22. {
  23. self::assertSame(0.0, Decay::value(DecayFunction::Linear, 30.0, 30.0));
  24. self::assertSame(0.0, Decay::value(DecayFunction::Linear, 100.0, 30.0));
  25. }
  26. public function testExponentialAtZeroReturnsFullWeight(): void
  27. {
  28. self::assertSame(1.0, Decay::value(DecayFunction::Exponential, 0.0, 14.0));
  29. }
  30. public function testExponentialAtOneHalfLifeReturnsHalf(): void
  31. {
  32. self::assertEqualsWithDelta(0.5, Decay::value(DecayFunction::Exponential, 14.0, 14.0), 1e-9);
  33. }
  34. public function testExponentialAtTwoHalfLivesReturnsQuarter(): void
  35. {
  36. self::assertEqualsWithDelta(0.25, Decay::value(DecayFunction::Exponential, 28.0, 14.0), 1e-9);
  37. }
  38. public function testNegativeAgeClampsToFullWeight(): void
  39. {
  40. // Future-dated reports shouldn't happen, but if they do they count
  41. // at full weight rather than blowing up.
  42. self::assertSame(1.0, Decay::value(DecayFunction::Linear, -5.0, 30.0));
  43. self::assertSame(1.0, Decay::value(DecayFunction::Exponential, -5.0, 14.0));
  44. }
  45. public function testZeroDecayParamReturnsZero(): void
  46. {
  47. self::assertSame(0.0, Decay::value(DecayFunction::Linear, 5.0, 0.0));
  48. self::assertSame(0.0, Decay::value(DecayFunction::Exponential, 5.0, 0.0));
  49. }
  50. public function testDaysUntilThresholdLinearHalfwayCase(): void
  51. {
  52. // Linear decay, T=30 days. Score=2.0, threshold=1.0 → score reaches
  53. // threshold when 2.0 * (1 − Δ/30) = 1.0, i.e. Δ = 15.
  54. self::assertEqualsWithDelta(
  55. 15.0,
  56. Decay::daysUntilThreshold(DecayFunction::Linear, 30.0, 2.0, 1.0) ?? -1.0,
  57. 1e-9,
  58. );
  59. }
  60. public function testDaysUntilThresholdExponentialHalfLife(): void
  61. {
  62. // Exponential half-life=14 days. Score=2.0, threshold=1.0 → Δ = 14.
  63. self::assertEqualsWithDelta(
  64. 14.0,
  65. Decay::daysUntilThreshold(DecayFunction::Exponential, 14.0, 2.0, 1.0) ?? -1.0,
  66. 1e-9,
  67. );
  68. // Two half-lives: 4.0 → 1.0 takes 28 days.
  69. self::assertEqualsWithDelta(
  70. 28.0,
  71. Decay::daysUntilThreshold(DecayFunction::Exponential, 14.0, 4.0, 1.0) ?? -1.0,
  72. 1e-9,
  73. );
  74. }
  75. public function testDaysUntilThresholdReturnsZeroWhenAlreadyBelow(): void
  76. {
  77. self::assertSame(0.0, Decay::daysUntilThreshold(DecayFunction::Linear, 30.0, 0.5, 1.0));
  78. self::assertSame(0.0, Decay::daysUntilThreshold(DecayFunction::Exponential, 14.0, 1.0, 1.0));
  79. }
  80. public function testDaysUntilThresholdReturnsNullForNonPositiveThreshold(): void
  81. {
  82. self::assertNull(Decay::daysUntilThreshold(DecayFunction::Linear, 30.0, 5.0, 0.0));
  83. self::assertNull(Decay::daysUntilThreshold(DecayFunction::Exponential, 14.0, 5.0, -0.5));
  84. }
  85. }