test(server): reuse comment wakeup route modules (#16410)

## Thinking Path

> - Paperclip manages AI agents and their work.
> - Task routes decide which agent wakes after a comment or assignment
change.
> - The comment-wakeup suite reloads the large route module before each
test.
> - The worker exhausts its memory before all 34 cases finish.
> - This PR loads the module once and resets mock behavior between
cases.
> - The suite keeps every assertion and runs within the existing memory
limit.

## Linked Issues or Issue Description

Refs #16017. Related fixes: #16396 and #16404 cover other route suites.

The [failed serialized
shard](https://github.com/paperclipai/paperclip/actions/runs/38706528509/job/116333628474)
completes 26 of 34 cases before a 4 GiB heap OOM. It also logs
`runner.select is not a function` when real external-object sync reaches
the empty transaction fixture.

A duplicate search found no open PR for this suite's memory failure.

## What Changed

- Move module reset, mock registration, and initial route import into
`beforeAll`.
- Mock external-object sync at its direct service import.
- Reset mock implementations between cases, then restore the fixture
defaults.
- Keep all 34 test cases and every assertion unchanged.

## Verification

- Node 24.21.0, matching the failed CI job.
- Unchanged suite with `NODE_OPTIONS=--max-old-space-size=4096`: worker
receives SIGKILL after 31/34 cases in this 4 GiB container.
- Fixed suite under the same heap limit: 34/34 pass in 5.06 seconds;
peak child RSS is 654 MiB.
- Fixed suite with a 1 GiB heap and shuffled order (seed 394): 34/34
pass.
- Compared all test bodies and assertions against master; they are
byte-for-byte unchanged.
- `git diff --check` passes.
- Workspace typecheck reaches the Rust stage, then stops because this
container has no `cargo`.
- Master has a stale lockfile. Local setup used `pnpm install
--no-lockfile`; Vitest resolved to 5.0.4 and Supertest to 7.3.1.
- Local build stops because Node.js headers are missing for the
SO_PEERCRED addon.
- The local full-suite command was bounded to three minutes and did not
finish.
- [Greptile gives this exact head
5/5](https://github.com/paperclipai/paperclip/pull/16410#issuecomment-6188812731),
with no findings or review threads.
- [CI run 38712230280, attempt
2](https://github.com/paperclipai/paperclip/actions/runs/38712230280/attempts/2)
passed on this head. All nine serialized shards, general tests,
typecheck, build, Rust checks, and e2e checks passed.
- The first attempt hit an unrelated legacy-recovery timestamp flake.
Its [single retry
passed](https://github.com/paperclipai/paperclip/actions/runs/38712230280/job/116349967086).
- Current check inventory: 53 successful check runs, two skipped
Storybook checks, and a successful Snyk commit status.

Reproduce the focused check with:

```sh
NODE_OPTIONS=--max-old-space-size=4096 pnpm exec vitest run --project @paperclipai/server server/src/__tests__/issue-update-comment-wakeup-routes.test.ts --pool=forks --isolate
```


## Risks

Low risk. Only test setup changes. Production routes, workflows, and the
lockfile are unchanged. The shuffled run checks that mock behavior does
not leak between tests.

## Model Used

OpenAI GPT-6 through Codex, with reasoning, repository tools, and code
execution. This runtime does not expose a more specific model ID or
context-window size.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

Co-authored-by: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Devin FoleyandPaperclip authored and GitHub committed 2026-10-15 02:55:49 -07:00
1 parent 11d69d04c7
commit 72ae600742
1 file changed
+21 -6
@@ -1,6 +1,6 @@
import express from "express";
import request from "supertest";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
vi.mock("../services/fast-responses.js", () => ({
enqueueFastResponse: vi.fn(async () => undefined),
@@ -134,6 +134,13 @@ vi.mock("../services/index.js", () => ({
}));
function registerModuleMocks() {
vi.doMock("../services/external-objects.js", () => ({
externalObjectService: () => ({
syncCommentSafely: vi.fn(async () => undefined),
syncIssueSafely: vi.fn(async () => undefined),
}),
}));
vi.doMock("../services/index.js", () => ({
companyService: () => ({
getById: vi.fn(async () => ({ id: "company-1" })),
@@ -205,12 +212,16 @@ function registerModuleMocks() {
}));
}
async function createApp(transaction: (callback: (tx: Record<string, never>) => Promise<unknown>) => Promise<unknown> =
async (callback) => callback({})) {
const [{ errorHandler }, { issueRoutes }] = await Promise.all([
async function loadAppModules() {
return Promise.all([
vi.importActual<typeof import("../middleware/index.js")>("../middleware/index.js"),
vi.importActual<typeof import("../routes/issues.js")>("../routes/issues.js"),
]);
}
async function createApp(transaction: (callback: (tx: Record<string, never>) => Promise<unknown>) => Promise<unknown> =
async (callback) => callback({})) {
const [{ errorHandler }, { issueRoutes }] = await loadAppModules();
const app = express();
app.use(express.json());
app.use((req, _res, next) => {
@@ -253,13 +264,17 @@ function makeIssue(overrides: Record<string, unknown> = {}) {
}
describe("issue update comment wakeups", () => {
beforeEach(() => {
beforeAll(async () => {
vi.resetModules();
vi.doUnmock("../routes/issues.js");
vi.doUnmock("../routes/authz.js");
vi.doUnmock("../middleware/index.js");
registerModuleMocks();
vi.clearAllMocks();
await loadAppModules();
});
beforeEach(() => {
vi.resetAllMocks();
mockAccessDecide.mockImplementation(async (input) => ({ allowed: true, action: input.action, reason: "allow_explicit_grant", explanation: "Allowed by test grant." }));
mockPauseGate.mockResolvedValue(null);
mockIssueService.findMentionedAgents.mockResolvedValue([]);