diff --git a/server/src/__tests__/issue-list-assignee-filter-routes.test.ts b/server/src/__tests__/issue-list-assignee-filter-routes.test.ts index 3c88bd3beb14..19cb261e8b0c 100644 --- a/server/src/__tests__/issue-list-assignee-filter-routes.test.ts +++ b/server/src/__tests__/issue-list-assignee-filter-routes.test.ts @@ -2,7 +2,7 @@ import { randomUUID } from "node:crypto"; import express from "express"; import request from "supertest"; import { eq } from "drizzle-orm"; -import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from "vitest"; +import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest"; import { activityLog, agents, companies, companyMemberships, createDb, heartbeatRuns, issues, principalPermissionGrants } from "@paperclipai/db"; import { getEmbeddedPostgresTestSupport, @@ -12,6 +12,7 @@ import { errorHandler } from "../middleware/index.js"; import { __clearIssueListResponseCacheForTests, __getIssueListResponseCacheSizeForTests, + __setIssueListResponseCacheEntryForTests, ISSUE_LIST_SERVER_CACHE_MAX_ENTRIES, issueRoutes, } from "../routes/issues.js"; @@ -550,40 +551,24 @@ describeEmbeddedPostgres("issue list routes assigneeAgentId filter", () => { expect(second.headers["x-paperclip-request-cache"]).toBe("hit"); }); - it("bounds compact issue-list server cache entries", async () => { - const companyId = randomUUID(); - const issueId = randomUUID(); - - await db.insert(companies).values({ - id: companyId, - name: "Paperclip", - issuePrefix: uniqueIssuePrefix(), - requireBoardApprovalForNewAgents: false, - }); - await seedCloudTenantMember(companyId); - await db.insert(issues).values({ - id: issueId, - companyId, - title: "Bounded cache issue", - status: "todo", - priority: "medium", - }); - - const app = createApp(companyId); - const fixedNow = Date.now(); - const nowSpy = vi.spyOn(Date, "now").mockReturnValue(fixedNow); - try { - for (let index = 0; index < ISSUE_LIST_SERVER_CACHE_MAX_ENTRIES + 5; index += 1) { - const res = await request(app) - .get(`/api/companies/${companyId}/issues`) - .query({ view: "compact", limit: "20", q: `cache-key-${index}` }); - expect(res.status, JSON.stringify(res.body)).toBe(200); - } - - expect(__getIssueListResponseCacheSizeForTests()).toBe(ISSUE_LIST_SERVER_CACHE_MAX_ENTRIES); - } finally { - nowSpy.mockRestore(); + it("bounds compact issue-list server cache entries", () => { + // Drive the exact same insert-and-trim path a real request takes + // (setIssueListResponseCacheEntry), but with synthetic entries inserted + // synchronously instead of 261 real HTTP round trips against Postgres. + // No DB, no HTTP, no awaits — nothing here can time out under a loaded + // runner, and there's no module-global override left to leak into a + // later test if it did. + const syntheticEntry = { + response: { kind: "compact" as const, body: [], etag: "test-etag", cacheControl: "no-store" }, + expiresAt: Date.now() + 1_000, + staleUntil: Date.now() + 5_000, + }; + + for (let index = 0; index < ISSUE_LIST_SERVER_CACHE_MAX_ENTRIES + 5; index += 1) { + __setIssueListResponseCacheEntryForTests(`cache-key-${index}`, syntheticEntry); } + + expect(__getIssueListResponseCacheSizeForTests()).toBe(ISSUE_LIST_SERVER_CACHE_MAX_ENTRIES); }); it("logs request_storm_detected for identical in-flight compact issue-list fanout without query values", async () => { diff --git a/server/src/routes/issues.ts b/server/src/routes/issues.ts index 63138e4a756c..4465ac828e22 100644 --- a/server/src/routes/issues.ts +++ b/server/src/routes/issues.ts @@ -2531,6 +2531,10 @@ function setIssueListResponseCacheEntry(key: string, entry: IssueListCacheEntry) trimIssueListResponseCache(); } +export function __setIssueListResponseCacheEntryForTests(key: string, entry: IssueListCacheEntry) { + setIssueListResponseCacheEntry(key, entry); +} + function decrementIssueListActorClientInflight(actorClientKey: string) { const next = (issueListActorClientInflight.get(actorClientKey) ?? 1) - 1; if (next <= 0) issueListActorClientInflight.delete(actorClientKey);