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
7 changes: 2 additions & 5 deletions src/mcp/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -907,7 +907,7 @@ export class GittensoryMcp {
server.registerTool(
"gittensory_get_burden_forecast",
{
description: "Return the cached or freshly-computed maintainer burden forecast for a repo, including projected review load, queue growth risk, stale PR signals, and a freshness marker.",
description: "Return the cached maintainer burden forecast for a repo, including projected review load, queue growth risk, stale PR signals, and a freshness marker.",
inputSchema: ownerRepoShape,
outputSchema: freshnessResponseOutputSchema,
},
Expand Down Expand Up @@ -1577,10 +1577,7 @@ export class GittensoryMcp {
};
}
return {
summary:
response.source === "snapshot"
? `Gittensory burden forecast for ${fullName} (cached, ${response.freshness}).`
: `Gittensory burden forecast for ${fullName} (computed from cached metadata).`,
summary: `Gittensory burden forecast for ${fullName} (cached, ${response.freshness}).`,
data: response as unknown as Record<string, unknown>,
};
}
Expand Down
27 changes: 3 additions & 24 deletions src/services/burden-forecast.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,5 @@
import {
getBurdenForecast,
getRepository,
listIssueSignalSample,
listOpenPullRequests,
listRecentMergedPullRequests,
} from "../db/repositories";
import { buildBurdenForecast, buildCollisionReport, type BurdenForecast } from "../signals/engine";
import { getBurdenForecast, getRepository } from "../db/repositories";
import type { BurdenForecast } from "../signals/engine";

export const BURDEN_FORECAST_MAX_AGE_MS = 6 * 60 * 60 * 1000;

Expand Down Expand Up @@ -39,22 +33,7 @@ export async function loadOrComputeBurdenForecastResponse(env: Env, fullName: st
report: cached.payload as unknown as BurdenForecast,
};
}
const [issues, pullRequests, recentMergedPullRequests] = await Promise.all([
listIssueSignalSample(env, repoFullName),
listOpenPullRequests(env, repoFullName),
listRecentMergedPullRequests(env, repoFullName),
]);
const collisions = buildCollisionReport(repoFullName, issues, pullRequests, recentMergedPullRequests);
const report = buildBurdenForecast(repo, issues, pullRequests, collisions, 30);
return {
status: "ready",
source: "computed",
repoFullName,
generatedAt: report.generatedAt,
ageSeconds: 0,
freshness: "fresh",
report,
};
return null;
}

function forecastAgeMs(generatedAt: string): number {
Expand Down
31 changes: 12 additions & 19 deletions test/integration/api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1718,13 +1718,12 @@ describe("api routes", () => {
expect(snapshotIntelligenceBody.burdenForecastFreshness?.ageSeconds).toBeLessThan(Math.floor((BURDEN_FORECAST_MAX_AGE_MS + 120_000) / 1000));

await upsertRepositoryFromGitHub(env, { name: "uncached-burden", full_name: "entrius/uncached-burden", private: false, owner: { login: "entrius" }, default_branch: "main" });
const computedIntelligence = await app.request("/v1/repos/entrius/uncached-burden/intelligence", { headers: apiHeaders(env) }, env);
expect(computedIntelligence.status).toBe(200);
await expect(computedIntelligence.json()).resolves.toMatchObject({
source: "computed",
burdenForecast: { repoFullName: "entrius/uncached-burden", level: "low" },
burdenForecastFreshness: { source: "computed", freshness: "fresh", ageSeconds: 0 },
});
const uncachedIntelligence = await app.request("/v1/repos/entrius/uncached-burden/intelligence", { headers: apiHeaders(env) }, env);
expect(uncachedIntelligence.status).toBe(200);
const uncachedIntelligenceBody = (await uncachedIntelligence.json()) as Record<string, unknown>;
expect(uncachedIntelligenceBody).toMatchObject({ source: "computed" });
expect(uncachedIntelligenceBody.burdenForecast).toBeUndefined();
expect(uncachedIntelligenceBody.burdenForecastFreshness).toBeUndefined();

const degradedForecastEnv = withBurdenForecastReadFailure(env);
const degradedIntelligence = await app.request("/v1/repos/entrius/allways-ui/intelligence", { headers: apiHeaders(env) }, degradedForecastEnv);
Expand Down Expand Up @@ -5049,26 +5048,20 @@ describe("api routes", () => {
},
});

await upsertRepositoryFromGitHub(env, { name: "mcp-computed-burden", full_name: "entrius/mcp-computed-burden", private: false, owner: { login: "entrius" }, default_branch: "main" });
const computedBurdenForecast = await app.request(
await upsertRepositoryFromGitHub(env, { name: "mcp-uncached-burden", full_name: "entrius/mcp-uncached-burden", private: false, owner: { login: "entrius" }, default_branch: "main" });
const uncachedBurdenForecast = await app.request(
"/mcp",
{
method: "POST",
headers: mcpHeaders(env),
body: JSON.stringify({ jsonrpc: "2.0", id: "computed-burden", method: "tools/call", params: { name: "gittensory_get_burden_forecast", arguments: { owner: "entrius", repo: "mcp-computed-burden" } } }),
body: JSON.stringify({ jsonrpc: "2.0", id: "uncached-burden", method: "tools/call", params: { name: "gittensory_get_burden_forecast", arguments: { owner: "entrius", repo: "mcp-uncached-burden" } } }),
},
env,
);
expect(computedBurdenForecast.status).toBe(200);
await expect(mcpJson(computedBurdenForecast)).resolves.toMatchObject({
expect(uncachedBurdenForecast.status).toBe(200);
await expect(mcpJson(uncachedBurdenForecast)).resolves.toMatchObject({
result: {
structuredContent: {
status: "ready",
source: "computed",
repoFullName: "entrius/mcp-computed-burden",
freshness: "fresh",
report: { repoFullName: "entrius/mcp-computed-burden", level: "low" },
},
structuredContent: { status: "not_found", repoFullName: "entrius/mcp-uncached-burden" },
},
});

Expand Down
19 changes: 7 additions & 12 deletions test/unit/burden-forecast.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,17 +146,13 @@ describe("loadOrComputeBurdenForecastResponse", () => {
expect(response?.ageSeconds).toBe(Number.POSITIVE_INFINITY);
});

it("falls back to a computed forecast when no snapshot exists but the repo is known", async () => {
it("returns null when no snapshot exists for a known repo", async () => {
const env = createTestEnv();
await upsertRepositoryFromGitHub(env, { name: "uncached", full_name: "owner/uncached", private: false, owner: { login: "owner" }, default_branch: "main" });

const response = await loadOrComputeBurdenForecastResponse(env, "owner/uncached");
expect(response).toMatchObject({
status: "ready",
source: "computed",
freshness: "fresh",
ageSeconds: 0,
});
expect(response?.report).toMatchObject({ repoFullName: "owner/uncached", level: "low" });

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

it("does not call broad request-time listers when a cached forecast exists", async () => {
Expand All @@ -180,7 +176,7 @@ describe("loadOrComputeBurdenForecastResponse", () => {
}
});

it("uses only the bounded per-repo listers when computing a missing forecast", async () => {
it("does not call broad request-time listers when no cached forecast exists", async () => {
const env = createTestEnv();
await upsertRepositoryFromGitHub(env, { name: "computed-perf", full_name: "owner/computed-perf", private: false, owner: { login: "owner" }, default_branch: "main" });
const repositoriesModule = await import("../../src/db/repositories");
Expand All @@ -192,10 +188,9 @@ describe("loadOrComputeBurdenForecastResponse", () => {

const response = await loadOrComputeBurdenForecastResponse(env, "owner/computed-perf");

expect(response).toMatchObject({ source: "computed", report: { repoFullName: "owner/computed-perf" } });
expect(response).toBeNull();
for (const spy of spies) {
expect(spy).toHaveBeenCalledTimes(1);
expect(spy).toHaveBeenCalledWith(env, "owner/computed-perf");
expect(spy).not.toHaveBeenCalled();
spy.mockRestore();
}
});
Expand Down
Loading