feat(store): fetch contributions incrementally and persist history in SQLite
Bound each provider's startFetch()/resolveFetch() to an optional since/until window, wire a SQLite-backed ContributionStore into ContributionAggregator (fetch only the trailing window past the last stored date, merge into the store, prune by CONTRIBUTIONS_RETENTION_DAYS), and add a graph:contributions:refetch command to force a full or ranged re-fetch in <=365-day chunks. This is the root fix for the full 365-day-refetch timeout that used to hit on every cache miss.
This commit is contained in:
@@ -29,6 +29,7 @@ final class GitHubProviderTest extends TestCase
|
||||
);
|
||||
}
|
||||
|
||||
/** @param array<int, array{contributionDays: array<int, array{date: string, contributionCount: int}>}> $weeks */
|
||||
private function stubGraphqlResponse(array $weeks): ResponseInterface
|
||||
{
|
||||
$response = $this->createStub(ResponseInterface::class);
|
||||
@@ -131,4 +132,27 @@ final class GitHubProviderTest extends TestCase
|
||||
|
||||
$this->assertSame([], $result);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function it_narrows_the_graphql_query_window_when_since_and_until_are_given(): void
|
||||
{
|
||||
$since = new \DateTimeImmutable('2024-01-01');
|
||||
$until = new \DateTimeImmutable('2024-01-31');
|
||||
$capturedQuery = null;
|
||||
|
||||
$client = $this->createMock(HttpClientInterface::class);
|
||||
$client->method('request')->willReturnCallback(
|
||||
function (string $method, string $url, array $options) use (&$capturedQuery): ResponseInterface {
|
||||
$capturedQuery = $options['json']['query'];
|
||||
|
||||
return $this->stubGraphqlResponse([]);
|
||||
}
|
||||
);
|
||||
|
||||
$provider = $this->makeProvider(client: $client);
|
||||
$provider->startFetch($since, $until);
|
||||
|
||||
$this->assertStringContainsString('2024-01-01T00:00:00Z', $capturedQuery);
|
||||
$this->assertStringContainsString('2024-01-31T23:59:59Z', $capturedQuery);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ final class GitLabProviderTest extends TestCase
|
||||
);
|
||||
}
|
||||
|
||||
/** @param array<int, array<string, int|string>> $data raw JSON body, e.g. events or the user lookup */
|
||||
private function stubResponse(array $data): ResponseInterface
|
||||
{
|
||||
$response = $this->createStub(ResponseInterface::class);
|
||||
@@ -128,4 +129,27 @@ final class GitLabProviderTest extends TestCase
|
||||
$this->assertSame(100, $result['2024-06-10']);
|
||||
$this->assertSame(1, $result['2024-06-11']);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function it_sends_after_and_before_query_params_when_since_and_until_are_given(): void
|
||||
{
|
||||
$capturedQuery = null;
|
||||
|
||||
$client = $this->createMock(HttpClientInterface::class);
|
||||
$client->method('request')->willReturnCallback(
|
||||
function (string $method, string $url, array $options = []) use (&$capturedQuery): ResponseInterface {
|
||||
if (str_contains($url, '/events')) {
|
||||
$capturedQuery = $options['query'];
|
||||
}
|
||||
|
||||
return $this->stubResponse(str_contains($url, '/events') ? [] : [['id' => 42]]);
|
||||
}
|
||||
);
|
||||
|
||||
$provider = $this->makeProvider(client: $client);
|
||||
$provider->startFetch(new \DateTimeImmutable('2024-01-01'), new \DateTimeImmutable('2024-01-31'));
|
||||
|
||||
$this->assertSame('2024-01-01', $capturedQuery['after']);
|
||||
$this->assertSame('2024-01-31', $capturedQuery['before']);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ final class GiteaProviderTest extends TestCase
|
||||
);
|
||||
}
|
||||
|
||||
/** @param array<int, array{timestamp: int, contributions: int}> $data */
|
||||
private function stubResponse(array $data): ResponseInterface
|
||||
{
|
||||
$response = $this->createStub(ResponseInterface::class);
|
||||
@@ -111,4 +112,44 @@ final class GiteaProviderTest extends TestCase
|
||||
|
||||
$this->assertSame([], $result);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function it_filters_out_entries_before_since(): void
|
||||
{
|
||||
$since = new \DateTimeImmutable('2024-01-10');
|
||||
$before = $since->modify('-1 day')->getTimestamp();
|
||||
$after = $since->modify('+1 day')->getTimestamp();
|
||||
|
||||
$client = $this->createStub(HttpClientInterface::class);
|
||||
$client->method('request')->willReturn($this->stubResponse([
|
||||
['timestamp' => $before, 'contributions' => 3],
|
||||
['timestamp' => $after, 'contributions' => 2],
|
||||
]));
|
||||
|
||||
$provider = $this->makeProvider(client: $client);
|
||||
$result = $provider->resolveFetch($provider->startFetch($since));
|
||||
|
||||
$this->assertArrayNotHasKey(date('Y-m-d', $before), $result);
|
||||
$this->assertSame(2, $result[date('Y-m-d', $after)]);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function it_filters_out_entries_after_until(): void
|
||||
{
|
||||
$until = new \DateTimeImmutable('-10 days');
|
||||
$before = $until->modify('-1 day')->getTimestamp();
|
||||
$after = $until->modify('+1 day')->getTimestamp();
|
||||
|
||||
$client = $this->createStub(HttpClientInterface::class);
|
||||
$client->method('request')->willReturn($this->stubResponse([
|
||||
['timestamp' => $before, 'contributions' => 3],
|
||||
['timestamp' => $after, 'contributions' => 2],
|
||||
]));
|
||||
|
||||
$provider = $this->makeProvider(client: $client);
|
||||
$result = $provider->resolveFetch($provider->startFetch(null, $until));
|
||||
|
||||
$this->assertSame(3, $result[date('Y-m-d', $before)]);
|
||||
$this->assertArrayNotHasKey(date('Y-m-d', $after), $result);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ final class ProbeTraitTest extends TestCase
|
||||
public function isConfigured(): bool { return $this->configured; }
|
||||
public function getName(): string { return 'test'; }
|
||||
public function ping(): void { ($this->ping)(); }
|
||||
public function startFetch(): mixed { return null; }
|
||||
public function startFetch(?\DateTimeImmutable $since = null, ?\DateTimeImmutable $until = null): mixed { return null; }
|
||||
public function resolveFetch(mixed $handle): array { return []; }
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user