$offsetsInDays */ public function __construct( private readonly string $featureName, private readonly array $offsetsInDays, ) {} public function name(): string { return $this->featureName; } public function valueFor(CarbonInterface $targetMonday): float { return 0.0; } public function sourceDates(CarbonInterface $targetMonday): array { return array_map( fn (int $offset): CarbonInterface => $targetMonday->copy()->addDays($offset), $this->offsetsInDays, ); } }; } it('passes when every feature reads strictly before the target Monday', function () { $spec = new FeatureSpec( modelLabel: 'test', features: [ makeFeature('lag_1w', [-7]), makeFeature('lag_4w', [-7, -14, -21, -28]), ], ); $report = (new LeakDetector)->validate($spec, [Carbon::parse('2024-06-03')]); expect($report)->toBeInstanceOf(LeakReport::class) ->and($report->hasLeaks())->toBeFalse() ->and($report->leaks)->toBe([]); }); it('flags a feature whose source date IS the target Monday', function () { $spec = new FeatureSpec( modelLabel: 'test', features: [makeFeature('same_day', [0])], ); $report = (new LeakDetector)->validate($spec, [Carbon::parse('2024-06-03')]); expect($report->hasLeaks())->toBeTrue() ->and($report->leaks)->toHaveCount(1) ->and($report->leaks[0]['feature'])->toBe('same_day'); }); it('flags a feature whose source date is AFTER the target Monday', function () { $spec = new FeatureSpec( modelLabel: 'test', features: [makeFeature('future', [7])], ); $report = (new LeakDetector)->validate($spec, [Carbon::parse('2024-06-03')]); expect($report->hasLeaks())->toBeTrue() ->and($report->leaks[0]['feature'])->toBe('future') ->and($report->leaks[0]['target_monday'])->toBe('2024-06-03') ->and($report->leaks[0]['source_date'])->toBe('2024-06-10'); }); it('checks every training week, not just the first', function () { $spec = new FeatureSpec( modelLabel: 'test', features: [makeFeature('lag_1w', [-7])], ); $weeks = [ Carbon::parse('2024-06-03'), Carbon::parse('2024-06-10'), Carbon::parse('2024-06-17'), ]; $report = (new LeakDetector)->validate($spec, $weeks); expect($report->hasLeaks())->toBeFalse(); }); it('reports multiple leaks across multiple features', function () { $spec = new FeatureSpec( modelLabel: 'test', features: [ makeFeature('clean', [-7]), makeFeature('leaky_one', [0]), makeFeature('leaky_two', [3]), ], ); $report = (new LeakDetector)->validate($spec, [Carbon::parse('2024-06-03')]); expect($report->hasLeaks())->toBeTrue() ->and($report->leaks)->toHaveCount(2); $featureNames = array_column($report->leaks, 'feature'); expect($featureNames)->toContain('leaky_one', 'leaky_two') ->and($featureNames)->not->toContain('clean'); });