Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 11 additions & 9 deletions src/services/burden-forecast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,32 +22,34 @@ export type BurdenForecastResponse = {
};

export async function loadOrComputeBurdenForecastResponse(env: Env, fullName: string): Promise<BurdenForecastResponse | null> {
const cached = await getBurdenForecast(env, fullName);
const repo = await getRepository(env, fullName);
if (!repo) return null;

const repoFullName = repo.fullName;
const cached = await getBurdenForecast(env, repoFullName);
if (cached) {
const ageMs = forecastAgeMs(cached.generatedAt);
return {
status: "ready",
source: "snapshot",
repoFullName: fullName,
repoFullName,
generatedAt: cached.generatedAt,
ageSeconds: Math.max(0, Math.floor(ageMs / 1000)),
freshness: ageMs > BURDEN_FORECAST_MAX_AGE_MS ? "stale" : "fresh",
report: cached.payload as unknown as BurdenForecast,
};
}
const repo = await getRepository(env, fullName);
if (!repo) return null;
const [issues, pullRequests, recentMergedPullRequests] = await Promise.all([
listIssueSignalSample(env, fullName),
listOpenPullRequests(env, fullName),
listRecentMergedPullRequests(env, fullName),
listIssueSignalSample(env, repoFullName),
listOpenPullRequests(env, repoFullName),
listRecentMergedPullRequests(env, repoFullName),
]);
const collisions = buildCollisionReport(fullName, issues, pullRequests, recentMergedPullRequests);
const collisions = buildCollisionReport(repoFullName, issues, pullRequests, recentMergedPullRequests);
const report = buildBurdenForecast(repo, issues, pullRequests, collisions, 30);
return {
status: "ready",
source: "computed",
repoFullName: fullName,
repoFullName,
generatedAt: report.generatedAt,
ageSeconds: 0,
freshness: "fresh",
Expand Down
24 changes: 24 additions & 0 deletions test/unit/burden-forecast.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,23 @@ describe("loadOrComputeBurdenForecastResponse", () => {
expect(response).toBeNull();
});

it("does not expose an orphaned cached forecast for an unknown repo", async () => {
const env = createTestEnv();
await upsertBurdenForecast(env, {
repoFullName: "ghost/private-repo",
payload: {
repoFullName: "ghost/private-repo",
level: "critical",
summary: "orphaned private queue fixture",
} as unknown as Record<string, JsonValue>,
generatedAt: new Date(Date.now() - 1000).toISOString(),
});

const response = await loadOrComputeBurdenForecastResponse(env, "ghost/private-repo");

expect(response).toBeNull();
});

it("returns a snapshot envelope with freshness:fresh for a recently persisted forecast", async () => {
const env = createTestEnv();
await upsertRepositoryFromGitHub(env, { name: "fresh", full_name: "owner/fresh", private: false, owner: { login: "owner" }, default_branch: "main" });
Expand Down Expand Up @@ -105,6 +122,13 @@ describe("loadOrComputeBurdenForecastResponse", () => {

it("treats malformed cached forecast timestamps as stale", async () => {
const env = createTestEnv();
await upsertRepositoryFromGitHub(env, {
name: "malformed-time",
full_name: "owner/malformed-time",
private: false,
owner: { login: "owner" },
default_branch: "main",
});
await upsertBurdenForecast(env, {
repoFullName: "owner/malformed-time",
payload: { repoFullName: "owner/malformed-time", level: "medium", summary: "bad timestamp fixture" } as unknown as Record<string, JsonValue>,
Expand Down
8 changes: 6 additions & 2 deletions test/unit/contributor-open-pr-monitor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ const maintainerRole: RoleContext = {
guidance: "maintainer",
};

function daysAgo(days: number): string {
return new Date(Date.now() - days * 86_400_000).toISOString();
}

function pr(overrides: Partial<PullRequestRecord> & Pick<PullRequestRecord, "number">): PullRequestRecord {
return {
repoFullName: "entrius/allways-ui",
Expand All @@ -42,8 +46,8 @@ function pr(overrides: Partial<PullRequestRecord> & Pick<PullRequestRecord, "num
authorLogin: "miner-a",
labels: [],
linkedIssues: [1],
createdAt: "2026-05-20T00:00:00.000Z",
updatedAt: "2026-05-27T00:00:00.000Z",
createdAt: daysAgo(3),
updatedAt: daysAgo(2),
...overrides,
};
}
Expand Down
8 changes: 6 additions & 2 deletions test/unit/pending-pr-scenarios.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ const maintainerRole: RoleContext = {
guidance: "maintainer",
};

function daysAgo(days: number): string {
return new Date(Date.now() - days * 86_400_000).toISOString();
}

function pr(overrides: Partial<PullRequestRecord> & Pick<PullRequestRecord, "number">): PullRequestRecord {
return {
repoFullName: "entrius/allways-ui",
Expand All @@ -42,8 +46,8 @@ function pr(overrides: Partial<PullRequestRecord> & Pick<PullRequestRecord, "num
authorLogin: "miner-a",
labels: [],
linkedIssues: [1],
createdAt: "2026-05-20T00:00:00.000Z",
updatedAt: "2026-05-27T00:00:00.000Z",
createdAt: daysAgo(3),
updatedAt: daysAgo(2),
...overrides,
};
}
Expand Down