diff --git a/src/api/routes.ts b/src/api/routes.ts index 8db71ad86c..7eaff6955f 100644 --- a/src/api/routes.ts +++ b/src/api/routes.ts @@ -1595,7 +1595,9 @@ export function createApp() { getOrCreateScoringModelSnapshot(c.env), getContributorEvidence(c.env, parsed.data.contributorLogin), ]); - const preview = buildScorePreview({ input: parsed.data, repo, snapshot, contributorEvidence: evidence }); + // Time-decay (#703) is an owner-gated global, injected server-side (not caller-controllable). + const input = { ...parsed.data, applyTimeDecay: isTimeDecayEnabled(c.env) }; + const preview = buildScorePreview({ input, repo, snapshot, contributorEvidence: evidence }); return c.json(explainScoreBreakdown(preview)); }); diff --git a/src/mcp/server.ts b/src/mcp/server.ts index ec60cea129..8c9b02d761 100644 --- a/src/mcp/server.ts +++ b/src/mcp/server.ts @@ -2094,7 +2094,9 @@ export class GittensoryMcp { getOrCreateScoringModelSnapshot(this.env), getContributorEvidence(this.env, input.contributorLogin), ]); - const preview = buildScorePreview({ input, repo, snapshot, contributorEvidence: evidence }); + // Time-decay (#703) is an owner-gated global, injected server-side (not caller-controllable). + const scoreInput = { ...input, applyTimeDecay: isTimeDecayEnabled(this.env) }; + const preview = buildScorePreview({ input: scoreInput, repo, snapshot, contributorEvidence: evidence }); const breakdown = explainScoreBreakdown(preview); return { summary: `Private Gittensory score breakdown for ${input.contributorLogin} in ${input.repoFullName}. Highest leverage: ${breakdown.highestLeverageLever.component}.`, diff --git a/test/integration/api.test.ts b/test/integration/api.test.ts index c98a0d0d1d..8e4e10724e 100644 --- a/test/integration/api.test.ts +++ b/test/integration/api.test.ts @@ -1627,29 +1627,44 @@ describe("api routes", () => { ); expect(noContributorScorePreview.status).toBe(200); + const agedScoreInput = { + repoFullName: "entrius/allways-ui", + contributorLogin: "oktofeesh1", + sourceTokenScore: 42, + totalTokenScore: 60, + sourceLines: 40, + openPrCount: 1, + linkedIssueMode: "standard", + prAgeHours: 240, + }; + env.SCORING_TIME_DECAY_ENABLED = "true"; + const agedScorePreview = await app.request( + "/v1/scoring/preview", + { method: "POST", headers: apiHeaders(env), body: JSON.stringify(agedScoreInput) }, + env, + ); + expect(agedScorePreview.status).toBe(200); + const agedScorePreviewBody = (await agedScorePreview.json()) as { input: { applyTimeDecay?: boolean; prAgeHours?: number }; result: { effectiveEstimatedScore: number; scoreEstimate: { timeDecayMultiplier: number } } }; + const scoreBreakdown = await app.request( "/v1/scoring/explain-breakdown", { method: "POST", headers: apiHeaders(env), - body: JSON.stringify({ - repoFullName: "entrius/allways-ui", - contributorLogin: "oktofeesh1", - sourceTokenScore: 42, - totalTokenScore: 60, - sourceLines: 40, - openPrCount: 1, - linkedIssueMode: "standard", - }), + body: JSON.stringify(agedScoreInput), }, env, ); expect(scoreBreakdown.status).toBe(200); - await expect(scoreBreakdown.json()).resolves.toMatchObject({ + const scoreBreakdownBody = (await scoreBreakdown.json()) as { effectiveEstimatedScore: number }; + expect(scoreBreakdownBody).toMatchObject({ repoFullName: "entrius/allways-ui", components: expect.arrayContaining([expect.objectContaining({ component: expect.any(String), lever: expect.any(String) })]), highestLeverageLever: expect.objectContaining({ component: expect.any(String), lever: expect.any(String) }), }); + expect(agedScorePreviewBody.input).toMatchObject({ applyTimeDecay: true, prAgeHours: 240 }); + expect(agedScorePreviewBody.result.scoreEstimate.timeDecayMultiplier).toBeLessThan(1); + expect(scoreBreakdownBody.effectiveEstimatedScore).toBe(agedScorePreviewBody.result.effectiveEstimatedScore); const missingContributorBreakdown = await app.request( "/v1/scoring/explain-breakdown",