Files
PaperClipAI/server/src/__tests__/github-connection-events.test.ts
1477d1ecea test: remove the no-op sequential describe modifier (#15286)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Paperclip runs the server and runner test suites with Vitest.
> - Vitest 5 removes the deprecated `describe.sequential` property, so
the pending Vitest 5 upgrade fails the type-check and test jobs.
> - `describe.sequential` only changes behaviour inside a
`describe.concurrent` suite, or when `sequence.concurrent` is on.
> - This repository has neither, so the modifier changed nothing at run
time.
> - The benefit is that the Vitest 5 upgrade can land, and the test
files lose a modifier that did no work.

## Linked Issues or Issue Description

Refs: #12969

## What Changed

- Replace every `describe.sequential` use with a plain `describe` call.
- Drop the `{ concurrent: false }` suite option from the two runner test
files.
- Add a comment to `server/vitest.config.ts` that records why these
suites must run one test at a time.
- Leave the package manifests and the lockfile unchanged.

## Why the modifier did nothing

The Vitest documentation states that `describe.sequential` is useful to
run tests in sequence inside a `describe.concurrent` suite, or with the
`--sequence.concurrent` option. `sequence.concurrent` defaults to
`false`.

This repository satisfies neither condition:

- No test file uses `describe.concurrent`, `it.concurrent`, or
`test.concurrent`.
- `server/vitest.config.ts` sets `sequence.concurrent: false`, with
`maxWorkers: 1`, `maxConcurrency: 1`, and `isolate: true`.
- `packages/paperclip-runner/vitest.config.ts` sets no `sequence` block,
so the `false` default applies.

`packages/db` and `cli` already run the same embedded-Postgres suites
with a plain `describe`, and those jobs are green. The server package
was the only outlier.

The modifier did carry one real piece of knowledge: these suites need
their tests to run one at a time. The new comment in
`server/vitest.config.ts` records that reason next to the setting that
enforces it.

## Verification

- `git grep` for `describe.sequential` returns nothing outside
`node_modules`.
- The author ran the changed server test files under the installed
Vitest 4, and the results match the results without this change.
- Two very large embedded-Postgres test files exceeded the author's
local memory limit, so the CI test jobs cover those two.
- The two changed runner test files have pre-existing local failures
caused by a missing Rust toolchain and a missing global `pnpm` binary.
The failures are identical with and without this change.
- The author type-checked the changed files and found no new error.
- CI must pass the typecheck, build, server test, and runner verify
jobs.

## Risks

- Low risk. Suite execution stays serial, because the Vitest config
enforces it.
- The change adds no dependency and changes no package manifest or
lockfile.
- A future change that turns `sequence.concurrent` on would break these
suites. The new config comment warns against it.

## Model Used

- Claude Sonnet 5 — code edits and local verification.
- OpenAI Codex, GPT-5 — the earlier revision of this branch.

## Test plan

- [x] Every CI check reaches a terminal green state. A pending or queued
check is not a pass.
- [x] The `Typecheck + Release Registry` job passes. This change must
not introduce a type error.
- [x] The `Build` job passes.
- [x] The server test jobs and the runner verify jobs pass.
- [x] Greptile re-reviews this commit set and posts a passing verdict.
The dependabot waiver does not apply to this pull request.
- [x] `mergeable` reads `MERGEABLE` as a terminal value.

## 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 linked the related public issue with `Refs: #12969`
- [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 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: Priya Raman <priya.raman@paperclip.ing>

---------

Co-authored-by: Priya Raman <priya.raman@paperclip.ing>
Co-authored-by: Paperclip <noreply@paperclip.ing>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: nickyleach <331803+nickyleach@users.noreply.github.com>
2026-10-06 07:19:15 -07:00

324 lines
12 KiB
TypeScript

import { randomUUID } from "node:crypto";
import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from "vitest";
import {
activityLog,
companies,
connectionEventDeliveries,
connectionGrants,
createDb,
externalObjects,
toolApplications,
toolConnections,
} from "@paperclipai/db";
import { eq } from "drizzle-orm";
import { githubConnectionEventService } from "../services/github-connection-events.js";
import { subscribeCompanyLiveEvents } from "../services/live-events.js";
import type { PaperclipCloudConnector } from "../services/paperclip-cloud-connector.js";
import {
getEmbeddedPostgresTestSupport,
startEmbeddedPostgresTestDatabase,
} from "./helpers/embedded-postgres.js";
const embeddedPostgresSupport = await getEmbeddedPostgresTestSupport();
const describeEmbeddedPostgres = embeddedPostgresSupport.supported ? describe : describe.skip;
describeEmbeddedPostgres("GitHub connection event delivery", () => {
let db!: ReturnType<typeof createDb>;
let tempDb: Awaited<ReturnType<typeof startEmbeddedPostgresTestDatabase>> | null = null;
beforeAll(async () => {
tempDb = await startEmbeddedPostgresTestDatabase("paperclip-github-events-");
db = createDb(tempDb.connectionString);
}, 20_000);
afterEach(async () => {
await db.delete(activityLog);
await db.delete(connectionEventDeliveries);
await db.delete(externalObjects);
await db.delete(connectionGrants);
await db.delete(toolConnections);
await db.delete(toolApplications);
await db.delete(companies);
});
afterAll(async () => {
await tempDb?.cleanup();
});
it("applies a normalized merged PR once, updates the snapshot, and acknowledges Cloud", async () => {
const companyId = randomUUID();
const userId = `github-owner-${randomUUID()}`;
const applicationId = randomUUID();
const connectionId = randomUUID();
const grantId = randomUUID();
await db.insert(companies).values({ id: companyId, name: "Paperclip", issuePrefix: "GHE" });
await db.insert(toolApplications).values({
id: applicationId,
companyId,
applicationKey: `github-${randomUUID()}`,
name: "GitHub",
type: "mcp_server",
status: "active",
});
await db.insert(toolConnections).values({
id: connectionId,
companyId,
applicationId,
name: "GitHub",
uid: `github-${randomUUID()}`,
transport: "mcp_remote",
authKind: "oauth",
credentialPolicy: "per_user",
status: "active",
enabled: true,
config: { sourceTemplateKey: "github", oauth: { connectorProfile: "github.code" } },
transportConfig: {},
});
await db.insert(connectionGrants).values({
id: grantId,
companyId,
connectionId,
kind: "user",
subjectUserId: userId,
status: "active",
isDefault: false,
providerTenant: {
oauth: { strategy: "paperclip_cloud_connector", accessTokenExpiresAt: null },
github: {
userId: "42",
login: "octocat",
installationCount: 1,
repositoryCount: 3,
repositorySelection: "selected",
installationIds: ["101"],
installationOwnerLogins: ["paperclipai"],
repositories: [{ id: "203", fullName: "paperclipai/removed", installationId: "101" }],
webhookHealth: "pending",
},
},
});
const externalObjectId = randomUUID();
await db.insert(externalObjects).values({
id: externalObjectId,
companyId,
providerKey: "github",
objectType: "pull_request",
externalId: "paperclipai/paperclip#pull/123",
statusCategory: "open",
statusTone: "info",
data: { provider: "github", marker: "preserved" },
});
const leasedEvent = {
id: "delivery_merged_123",
provider: "github" as const,
event: "pull_request",
action: "closed",
installationId: "101",
repositoryId: "99",
createdAt: "2026-09-04T12:00:00.000Z",
bindingIds: [`${grantId}_101`],
payload: {
repository: "paperclipai/paperclip",
number: 123,
state: "closed",
merged: true,
mergedAt: "2026-09-04T11:59:00.000Z",
updatedAt: "2026-09-04T11:59:01.000Z",
url: "https://github.com/paperclipai/paperclip/pull/123",
headRef: "feature",
headSha: "a".repeat(40),
baseRef: "master",
baseSha: "b".repeat(40),
body: "private pull request body",
comments: [{ body: "private review comment" }],
accessToken: "ghu_must_not_be_persisted",
arbitraryNested: { credential: "also-must-not-be-persisted" },
},
};
let poll = 0;
const connector = {
getCapabilities: vi.fn(async () => ["github.code" as const]),
startAuthorization: vi.fn(),
claim: vi.fn(),
refresh: vi.fn(),
revoke: vi.fn(),
setWebhookBinding: vi.fn(async () => undefined),
leaseEvents: vi.fn(async () => ({ leaseId: `lease-${++poll}`, events: [leasedEvent] })),
acknowledgeEvents: vi.fn(async () => 1),
} as unknown as PaperclipCloudConnector;
let currentTime = new Date("2026-09-04T12:00:05.000Z");
const service = githubConnectionEventService(db, { connector, now: () => currentTime });
await expect(service.pollOnce()).resolves.toMatchObject({ leased: 1, processed: 1, duplicate: 0, failed: 0 });
const [snapshot] = await db.select().from(externalObjects).where(eq(externalObjects.id, externalObjectId));
expect(snapshot).toMatchObject({
statusKey: "merged",
statusLabel: "Merged",
statusCategory: "succeeded",
statusTone: "success",
isTerminal: true,
remoteVersion: "2026-09-04T11:59:01.000Z",
data: expect.objectContaining({ marker: "preserved", merged: true, headRef: "feature", baseSha: "b".repeat(40) }),
});
const [grant] = await db.select().from(connectionGrants).where(eq(connectionGrants.id, grantId));
expect(grant?.providerTenant?.github).toMatchObject({ webhookHealth: "healthy", lastWebhookAt: currentTime.toISOString() });
const [receipt] = await db.select().from(connectionEventDeliveries).where(eq(
connectionEventDeliveries.providerDeliveryId,
leasedEvent.id,
));
expect(receipt).toMatchObject({
status: "processed",
attempts: 1,
provider: "github",
normalizedPayload: {
repository: "paperclipai/paperclip",
number: 123,
state: "closed",
merged: true,
mergedAt: "2026-09-04T11:59:00.000Z",
updatedAt: "2026-09-04T11:59:01.000Z",
url: "https://github.com/paperclipai/paperclip/pull/123",
headRef: "feature",
headSha: "a".repeat(40),
baseRef: "master",
baseSha: "b".repeat(40),
},
});
expect(JSON.stringify(receipt)).not.toMatch(/private pull request|private review|ghu_|also-must-not/);
const [activity] = await db.select().from(activityLog).where(eq(activityLog.action, "tool_connection.webhook_processed"));
expect(activity?.details).toEqual({
provider: "github",
event: "pull_request",
action: "closed",
deliveryId: leasedEvent.id,
installationId: "101",
repositoryId: "99",
});
expect(connector.acknowledgeEvents).toHaveBeenCalledTimes(1);
currentTime = new Date(currentTime.getTime() + 6_000);
await expect(service.pollOnce()).resolves.toMatchObject({ leased: 1, processed: 0, duplicate: 1, failed: 0 });
const [duplicateReceipt] = await db.select().from(connectionEventDeliveries).where(eq(
connectionEventDeliveries.providerDeliveryId,
leasedEvent.id,
));
expect(duplicateReceipt?.attempts).toBe(1);
expect(connector.acknowledgeEvents).toHaveBeenCalledTimes(2);
});
it.each([false, true])("applies installation events once without discarding newer verified access (refreshed: %s)", async (refreshed) => {
const companyId = randomUUID();
const applicationId = randomUUID();
const connectionId = randomUUID();
const grantId = randomUUID();
await db.insert(companies).values({ id: companyId, name: "Paperclip", issuePrefix: "GHI" });
await db.insert(toolApplications).values({
id: applicationId,
companyId,
applicationKey: `github-${randomUUID()}`,
name: "GitHub",
type: "mcp_server",
status: "active",
});
await db.insert(toolConnections).values({
id: connectionId,
companyId,
applicationId,
name: "GitHub",
uid: `github-${randomUUID()}`,
transport: "mcp_remote",
authKind: "oauth",
credentialPolicy: "per_user",
status: "active",
enabled: true,
config: { sourceTemplateKey: "github", oauth: { connectorProfile: "github.code" } },
transportConfig: {},
});
await db.insert(connectionGrants).values({
id: grantId,
companyId,
connectionId,
kind: "user",
subjectUserId: `github-owner-${randomUUID()}`,
status: "active",
isDefault: false,
providerTenant: {
oauth: { strategy: "paperclip_cloud_connector", accessTokenExpiresAt: null },
github: {
userId: "42",
login: "octocat",
installationCount: 1,
repositoryCount: 3,
repositorySelection: "selected",
installationIds: ["101"],
installationOwnerLogins: ["paperclipai"],
repositories: [{ id: "203", fullName: "paperclipai/removed", installationId: "101" }],
webhookHealth: "pending",
},
},
});
const leasedEvent = {
id: "delivery_repository_change_101",
provider: "github" as const,
event: "installation_repositories",
action: "added",
installationId: "101",
repositoryId: null,
createdAt: "2026-09-04T12:00:00.000Z",
bindingIds: [`${grantId}_101`],
payload: {
repositorySelection: "selected",
repositoriesAdded: ["201", "202"],
repositoriesRemoved: ["203"],
},
};
let poll = 0;
const connector = {
getCapabilities: vi.fn(async () => ["github.code" as const]),
startAuthorization: vi.fn(),
claim: vi.fn(),
refresh: vi.fn(),
revoke: vi.fn(),
setWebhookBinding: vi.fn(async () => undefined),
leaseEvents: vi.fn(async () => {
if (refreshed) {
const [latest] = await db.select().from(connectionGrants).where(eq(connectionGrants.id, grantId));
await db.update(connectionGrants).set({ providerTenant: {
...latest!.providerTenant,
github: { ...latest!.providerTenant!.github!, lastAccessRefreshAt: "2026-09-04T12:00:02.000Z" },
} }).where(eq(connectionGrants.id, grantId));
}
return ({ leaseId: `lease-${++poll}`, events: [leasedEvent] });
}),
acknowledgeEvents: vi.fn(async () => 1),
} as unknown as PaperclipCloudConnector;
let currentTime = new Date("2026-09-04T12:00:05.000Z");
const service = githubConnectionEventService(db, { connector, now: () => currentTime });
const unsubscribe = subscribeCompanyLiveEvents(companyId, () => {
throw new Error("fixture live subscriber failed");
});
await expect(service.pollOnce()).resolves.toMatchObject({ processed: 1, duplicate: 0, failed: 0 });
unsubscribe();
let [grant] = await db.select().from(connectionGrants).where(eq(connectionGrants.id, grantId));
expect(grant?.providerTenant?.github).toMatchObject({ repositoryCount: refreshed ? 3 : 4, webhookHealth: "healthy" });
if (refreshed) {
expect(grant?.providerTenant?.github?.repositories).toHaveLength(1);
} else {
expect(grant?.providerTenant?.github?.repositories).toBeUndefined();
}
currentTime = new Date(currentTime.getTime() + 6_000);
await expect(service.pollOnce()).resolves.toMatchObject({ processed: 0, duplicate: 1, failed: 0 });
[grant] = await db.select().from(connectionGrants).where(eq(connectionGrants.id, grantId));
expect(grant?.providerTenant?.github?.repositoryCount).toBe(refreshed ? 3 : 4);
const [receipt] = await db.select().from(connectionEventDeliveries).where(eq(
connectionEventDeliveries.providerDeliveryId,
leasedEvent.id,
));
expect(receipt).toMatchObject({ status: "processed", attempts: 1 });
});
});