test: isolate accounting fixtures and include all adapter suites

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Devin FoleyandPaperclip committed 2026-10-02 16:35:32 -07:00
1 parent 4abff286c2
commit 7eb38f881e
30 files changed
+98 -5

No files matched your search

@@ -92,17 +92,37 @@ test("a route/authz suite never leaks into the general-server shards", () => {
const shard = dryRunJson(["--mode", "general", "--group", "general-server", "--shard-index", "0", "--shard-count", SHARD_COUNT.toString()]);
for (const file of shard.selectedGeneralServerSuites) {
assert.ok(
!/[^/]*(?:route|routes|authz)[^/]*\.test\.ts$/.test(file),
!(file.startsWith("server/src/__tests__/") && /[^/]*(?:route|routes|authz)[^/]*\.test\.ts$/.test(file)),
`route/authz suite must stay in the serialized lane, not general-server: ${file}`,
);
}
});
test("general shards retain scripts and route-named suites outside the serialized directory", () => {
const general = dryRunJson(["--mode", "general", "--group", "general-server", "--shard-index", "0", "--shard-count", "1"]);
for (const file of [
"server/scripts/verify-runner-vendor-dependencies.test.mjs",
"server/src/services/openrouter-models.test.ts",
"server/src/routes/setup-token-route.test.ts",
]) assert.ok(general.selectedGeneralServerSuites.includes(file), `missing configured server suite: ${file}`);
});
test("shard flags are rejected for the workspaces-b group", () => {
const result = dryRun(["--mode", "general", "--group", "general-workspaces-b", "--shard-index", "0", "--shard-count", "3"]);
assert.notEqual(result.status, 0, "workspaces-b must not accept shard flags");
});
test("workspace lanes cover every non-server project in the root Vitest configuration", () => {
const config = readFileSync(path.join(repoRoot, "vitest.config.ts"), "utf8");
const roots = [...config.matchAll(/^\s+"([^"]+)",?\s*$/gm)].map(match => match[1]);
assert.ok(roots.includes("server"), "expected the explicit root Vitest project list");
const expected = roots.filter(root => root !== "server")
.map(root => JSON.parse(readFileSync(path.join(repoRoot, root, "package.json"), "utf8")).name).sort();
const actual = ["general-workspaces-a", "general-workspaces-b"]
.flatMap(group => dryRunJson(["--mode", "general", "--group", group]).workspaceProjects).sort();
assert.deepEqual(actual, expected, "no configured project may be silently omitted or run twice");
});
test("workspaces-a shards map to Vitest native --shard slices over a stable project list", () => {
const shards = [0, 1].map((index) =>
dryRunJson([
+14 -3
View File
@@ -19,6 +19,7 @@ const serializedShardDurations = loadShardDurations(
const serverRoot = path.join(repoRoot, "server");
const serverSrcDir = path.join(repoRoot, "server", "src");
const serverTestsDir = path.join(repoRoot, "server", "src", "__tests__");
const serverScriptsDir = path.join(repoRoot, "server", "scripts");
const nonServerProjects = [
"@paperclipai/shared",
"@paperclipai/skills-catalog",
@@ -26,9 +27,14 @@ const nonServerProjects = [
"@paperclipai/adapter-utils",
"@paperclipai/adapter-claude-local",
"@paperclipai/adapter-codex-local",
"@paperclipai/adapter-cursor-cloud",
"@paperclipai/adapter-cursor-local",
"@paperclipai/adapter-gemini-local",
"@paperclipai/adapter-grok-local",
"@paperclipai/adapter-kimi-local",
"@paperclipai/adapter-openclaw-gateway",
"@paperclipai/adapter-opencode-local",
"@paperclipai/adapter-pi-local",
"@paperclipai/plugin-daytona",
"@paperclipai/plugin-sdk",
"@paperclipai/create-paperclip-plugin",
@@ -492,10 +498,15 @@ const routeTests = walk(serverTestsDir)
// config pins maxWorkers to 1, so the only way to parallelize is across jobs.
// Suites are partitioned by recorded duration (scripts/general-server-shard.mjs)
// rather than round-robin, so one slow suite cluster can't stretch a single shard.
const generalServerTestFiles = walk(serverSrcDir)
const serializedRepoPaths = new Set(routeTests.map(test => test.repoPath));
const generalServerTestFiles = [
...walk(serverSrcDir).filter(file => file.endsWith(".test.ts")),
...walk(serverScriptsDir).filter(file => file.endsWith(".test.mjs")),
]
.map((file) => toRepoPath(file))
.filter((repoPath) => repoPath.endsWith(".test.ts"))
.filter((repoPath) => !isRouteOrAuthzTest(repoPath))
// Only exclude suites actually assigned to the serialized lane. A name
// such as services/openrouter-models.test.ts is not a serialized route.
.filter((repoPath) => !serializedRepoPaths.has(repoPath))
.sort((a, b) => a.localeCompare(b));
const options = parseCliOptions(process.argv.slice(2));
@@ -1,3 +1,4 @@
import { execFileSync } from "node:child_process";
import { mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import os from "node:os";
import path from "node:path";
@@ -367,6 +368,9 @@ function isTestFile(relPath: string): boolean {
function listGuidanceFiles(rootDir = repoRoot): string[] {
const found: string[] = [];
// Include tracked files and new authored files, while respecting gitignored
// local settings. A tracked file remains scanned even if an ignore matches.
const authored = rootDir === repoRoot ? new Set(execFileSync("git", ["ls-files", "--cached", "--others", "--exclude-standard", "-z"], { cwd: rootDir, encoding: "utf8" }).split("\0")) : null;
function walk(absDir: string, relDir: string): void {
for (const entry of readdirSync(absDir, { withFileTypes: true })) {
@@ -382,7 +386,7 @@ function listGuidanceFiles(rootDir = repoRoot): string[] {
if (!SCAN_EXTENSIONS.has(path.extname(entry.name))) continue;
if (isTestFile(relPath)) continue;
if (SKIP_PATH_PREFIXES.some((prefix) => relPath.startsWith(prefix))) continue;
found.push(relPath);
if (!authored || authored.has(relPath)) found.push(relPath);
}
}
@@ -11,6 +11,7 @@ import {
stopSshEnvLabFixture,
} from "@paperclipai/adapter-utils/ssh";
import {
costEvents,
agents,
builtInManagedResources,
companies,
@@ -219,6 +220,7 @@ describeEmbeddedPostgres("environmentRuntimeService", () => {
}
await db.delete(environmentLeases);
await db.delete(issues);
await db.delete(costEvents);
await db.delete(heartbeatRuns);
await db.delete(agents);
await db.delete(environments);
@@ -2,6 +2,7 @@ import { randomUUID } from "node:crypto";
import { eq } from "drizzle-orm";
import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest";
import {
costEvents,
activityLog,
agents,
agentWakeupRequests,
@@ -40,6 +41,7 @@ describeEmbeddedPostgres("execution lock orphan cleanup", () => {
await db.delete(activityLog);
await db.delete(heartbeatRunEvents);
await db.delete(issues);
await db.delete(costEvents);
await db.delete(heartbeatRuns);
await db.delete(agents);
await db.delete(companies);
@@ -7,6 +7,7 @@ import { promisify } from "node:util";
import { eq, ne } from "drizzle-orm";
import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from "vitest";
import {
costEvents,
activityLog,
agentRuntimeState,
agentTaskSessions,
@@ -136,6 +137,7 @@ describeEmbeddedPostgres("accepted plan workspace refresh", () => {
await db.delete(activityLog);
await db.delete(heartbeatRunEvents);
try {
await db.delete(costEvents);
await db.delete(heartbeatRuns);
break;
} catch (error) {
@@ -1,6 +1,7 @@
import { randomUUID } from "node:crypto";
import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest";
import {
costEvents,
agents,
agentWakeupRequests,
companies,
@@ -35,6 +36,7 @@ describeEmbeddedPostgres("heartbeat archived-company guard", () => {
afterEach(async () => {
await db.delete(heartbeatRunEvents);
await db.delete(costEvents);
await db.delete(heartbeatRuns);
await db.delete(agentWakeupRequests);
await db.delete(issues);
@@ -2,6 +2,7 @@ import { randomUUID } from "node:crypto";
import { and, eq, inArray, sql } from "drizzle-orm";
import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from "vitest";
import {
costEvents,
activityLog,
agents,
agentRuntimeState,
@@ -143,6 +144,7 @@ describeEmbeddedPostgres("heartbeat dependency-aware queued run selection", () =
await db.delete(issues);
await db.delete(heartbeatRunEvents);
await db.delete(activityLog);
await db.delete(costEvents);
await db.delete(heartbeatRuns);
await db.delete(agentWakeupRequests);
await db.delete(agentRuntimeState);
@@ -2,6 +2,7 @@ import { randomUUID } from "node:crypto";
import { and, desc, eq, sql } from "drizzle-orm";
import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from "vitest";
import {
costEvents,
activityLog,
agentRuntimeState,
agentWakeupRequests,
@@ -90,6 +91,7 @@ describeEmbeddedPostgres("heartbeat issue rewake throttle", () => {
await db.delete(issues);
await db.delete(heartbeatRunEvents);
await db.delete(activityLog);
await db.delete(costEvents);
await db.delete(heartbeatRuns);
await db.delete(agentWakeupRequests);
await db.delete(agentRuntimeState);
@@ -2,6 +2,7 @@ import { randomUUID } from "node:crypto";
import { and, eq } from "drizzle-orm";
import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from "vitest";
import {
costEvents,
agents,
agentRuntimeState,
agentWakeupRequests,
@@ -59,6 +60,7 @@ describeEmbeddedPostgres("heartbeat lock release on cross-agent reassignment", (
afterEach(async () => {
await drainHeartbeatRunsToQuiescence(db, heartbeat);
await db.delete(heartbeatRunEvents);
await db.delete(costEvents);
await db.delete(heartbeatRuns);
await db.delete(agentWakeupRequests);
await db.delete(issues);
@@ -2,6 +2,7 @@ import { randomUUID } from "node:crypto";
import { eq } from "drizzle-orm";
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import {
costEvents,
agents,
companies,
createDb,
@@ -62,6 +63,7 @@ describeEmbeddedPostgres("heartbeat sweepOrphanedActiveLeases", () => {
afterEach(async () => {
await db.delete(environmentLeases);
await db.delete(costEvents);
await db.delete(heartbeatRuns);
await db.delete(environments);
await db.delete(agents);
@@ -2,6 +2,7 @@ import { randomUUID } from "node:crypto";
import { eq, sql } from "drizzle-orm";
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import {
costEvents,
activityLog,
agents,
heartbeatRuns,
@@ -71,6 +72,7 @@ describeEmbeddedPostgres("heartbeat sweepPendingCleanupLeases", () => {
afterEach(async () => {
await db.delete(activityLog);
await db.delete(environmentLeases);
await db.delete(costEvents);
await db.delete(heartbeatRuns);
await db.delete(environments);
await db.delete(agents);
@@ -2,6 +2,7 @@ import { randomUUID } from "node:crypto";
import { and, eq } from "drizzle-orm";
import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from "vitest";
import {
costEvents,
activityLog,
agents,
agentRuntimeState,
@@ -62,6 +63,7 @@ async function deleteHeartbeatRunsAfterEvents(db: ReturnType<typeof createDb>) {
for (let attempt = 0; attempt < 5; attempt += 1) {
await db.delete(heartbeatRunEvents);
try {
await db.delete(costEvents);
await db.delete(heartbeatRuns);
return;
} catch (error) {
@@ -2,6 +2,7 @@ import { randomUUID } from "node:crypto";
import { and, eq, inArray, sql } from "drizzle-orm";
import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from "vitest";
import {
costEvents,
agents,
approvals,
issueApprovals,
@@ -164,6 +165,7 @@ describeEmbeddedPostgres("heartbeat bounded retry scheduling", () => {
}
async function cleanupRetryFixtureOnce() {
await db.delete(costEvents);
await db.delete(activityLog);
await db.delete(environmentLeases);
await db.delete(issueRelations);
@@ -2,6 +2,7 @@ import { randomUUID } from "node:crypto";
import { eq } from "drizzle-orm";
import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from "vitest";
import {
costEvents,
agents,
companies,
createDb,
@@ -42,6 +43,7 @@ describeEmbeddedPostgres("heartbeat terminalizeRunOnLeaseRelease", () => {
afterEach(async () => {
await db.delete(heartbeatRunEvents);
await db.delete(issues);
await db.delete(costEvents);
await db.delete(heartbeatRuns);
await db.delete(agents);
await db.delete(companies);
@@ -2,6 +2,7 @@ import { randomUUID } from "node:crypto";
import { eq } from "drizzle-orm";
import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from "vitest";
import {
costEvents,
agents,
companies,
createDb,
@@ -71,6 +72,7 @@ describeEmbeddedPostgres("heartbeat teardown terminalizes the run before releasi
afterEach(async () => {
await db.delete(heartbeatRunEvents);
await db.delete(issues);
await db.delete(costEvents);
await db.delete(heartbeatRuns);
await db.delete(agents);
await db.delete(companies);
@@ -2,6 +2,7 @@ import { randomUUID } from "node:crypto";
import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from "vitest";
import { eq } from "drizzle-orm";
import {
costEvents,
agents,
agentRuntimeState,
agentWakeupRequests,
@@ -52,6 +53,7 @@ describeEmbeddedPostgres("heartbeat runtime state deduplication", () => {
afterEach(async () => {
clearAllHeartbeatRunRuntimeStatuses();
await db.delete(heartbeatRunEvents);
await db.delete(costEvents);
await db.delete(heartbeatRuns);
await db.delete(agentWakeupRequests);
await db.delete(agentRuntimeState);
@@ -2,6 +2,7 @@ import { randomUUID } from "node:crypto";
import { eq, sql } from "drizzle-orm";
import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest";
import {
costEvents,
activityLog,
agents,
agentRuntimeState,
@@ -51,6 +52,7 @@ describeEmbeddedPostgres("heartbeat task-drain admission release", () => {
await db.delete(heartbeatRunEvents);
await db.delete(activityLog);
try {
await db.delete(costEvents);
await db.delete(heartbeatRuns);
return;
} catch (error) {
@@ -7,6 +7,7 @@ import { promisify } from "node:util";
import { and, eq, inArray } from "drizzle-orm";
import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from "vitest";
import {
costEvents,
activityLog,
agentRuntimeState,
agentWakeupRequests,
@@ -179,6 +180,7 @@ async function deleteHeartbeatRunsForCleanup(db: Db) {
await db.delete(heartbeatRunEvents);
await db.delete(activityLog);
try {
await db.delete(costEvents);
await db.delete(heartbeatRuns);
return;
} catch (error) {
@@ -5,6 +5,7 @@ import path from "node:path";
import { and, eq } from "drizzle-orm";
import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from "vitest";
import {
costEvents,
agents,
agentRuntimeState,
agentWakeupRequests,
@@ -151,6 +152,7 @@ describeEmbeddedPostgres("shared-workspace run serialization", () => {
}
async function cleanupFixtureOnce() {
await db.delete(costEvents);
await db.delete(activityLog);
await db.delete(environmentLeases);
await db.delete(issueComments);
@@ -7,6 +7,7 @@ import { promisify } from "node:util";
import { and, asc, eq } from "drizzle-orm";
import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from "vitest";
import {
costEvents,
activityLog,
agentRuntimeState,
agentTaskSessions,
@@ -121,6 +122,7 @@ async function deleteHeartbeatRowsAfterActivityLogDrains(db: Db) {
await db.delete(activityLog);
await db.delete(heartbeatRunEvents);
try {
await db.delete(costEvents);
await db.delete(heartbeatRuns);
await db.delete(agentWakeupRequests);
return;
@@ -2,6 +2,7 @@ import { randomUUID } from "node:crypto";
import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest";
import { eq, sql } from "drizzle-orm";
import {
costEvents,
activityLog,
agents,
agentWakeupRequests,
@@ -52,6 +53,7 @@ describeEmbeddedPostgres("heartbeat worktree suppression", () => {
await db.delete(heartbeatRunEvents);
await db.delete(activityLog);
try {
await db.delete(costEvents);
await db.delete(heartbeatRuns);
return;
} catch (error) {
@@ -3,6 +3,7 @@ import { eq, sql } from "drizzle-orm";
import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest";
import { PROVIDER_QUOTA_MONITOR_SERVICE_NAME } from "@paperclipai/shared";
import {
costEvents,
activityLog,
agentRuntimeState,
agentWakeupRequests,
@@ -111,6 +112,7 @@ describeEmbeddedPostgres("issue monitor scheduler", () => {
await db.delete(environmentLeases);
await db.delete(workspaceRuntimeServices);
await db.delete(issues);
await db.delete(costEvents);
await db.delete(heartbeatRuns);
await db.delete(agentWakeupRequests);
await db.delete(agentRuntimeState);
@@ -4,6 +4,7 @@ import request from "supertest";
import { and, eq } from "drizzle-orm";
import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from "vitest";
import {
costEvents,
activityLog,
agentRuntimeState,
agents,
@@ -94,6 +95,7 @@ describeEmbeddedPostgres("issue create onboarding first-task routes", () => {
await db.delete(issueThreadInteractions);
await db.delete(issueComments);
await db.delete(heartbeatRunEvents);
await db.delete(costEvents);
await db.delete(heartbeatRuns);
await db.delete(agentWakeupRequests);
await db.delete(agentRuntimeState);
@@ -7,6 +7,7 @@ import request from "supertest";
import { and, eq } from "drizzle-orm";
import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from "vitest";
import {
costEvents,
agents,
agentRuntimeState,
authUsers,
@@ -154,6 +155,7 @@ describeEmbeddedPostgres("issue recovery actions", () => {
await db.delete(issueComments);
await db.delete(environmentLeases);
await db.delete(activityLog);
await db.delete(costEvents);
await db.delete(heartbeatRuns);
await db.delete(agentWakeupRequests);
await db.delete(environments);
@@ -4,6 +4,7 @@ import request from "supertest";
import { and, eq } from "drizzle-orm";
import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from "vitest";
import {
costEvents,
activityLog,
agentRuntimeState,
agentWakeupRequests,
@@ -80,6 +81,7 @@ describeEmbeddedPostgres("issue watchdog routes", () => {
await db.delete(activityLog);
await db.delete(issueComments);
await db.delete(heartbeatRunEvents);
await db.delete(costEvents);
await db.delete(heartbeatRuns);
await db.delete(agentWakeupRequests);
await db.delete(agentRuntimeState);
@@ -6,6 +6,7 @@ import { WebSocketServer } from "ws";
import { and, eq } from "drizzle-orm";
import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest";
import {
costEvents,
activityLog,
authUsers,
agentWakeupRequests,
@@ -103,6 +104,7 @@ async function deleteHeartbeatRunsAndWakeupsAfterActivityLogDrains(db: Db) {
await db.delete(activityLog);
try {
await db.delete(heartbeatRunEvents);
await db.delete(costEvents);
await db.delete(heartbeatRuns);
await db.delete(agentWakeupRequests);
return;
@@ -2,6 +2,7 @@ import { randomUUID } from "node:crypto";
import { eq } from "drizzle-orm";
import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from "vitest";
import {
costEvents,
activityLog,
agents,
companies,
@@ -54,6 +55,7 @@ describeEmbeddedPostgres("recovery sweepStaleIssueLocks", () => {
await db.delete(issueRelations);
await db.delete(activityLog);
await db.delete(heartbeatRunEvents);
await db.delete(costEvents);
await db.delete(heartbeatRuns);
await db.delete(completionContracts);
await db.delete(issues);
+7
View File
@@ -21,6 +21,13 @@ type SupertestTestConstructor = {
};
};
// Receipt-spool recovery must never scan a developer's instance during tests.
const paperclipTestHome = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-vitest-home-"));
process.env.PAPERCLIP_HOME = paperclipTestHome;
// Setup-file afterAll hooks run before suites drain asynchronous heartbeats.
// Cleanup at worker exit, after all suite teardown, avoids deleting live state.
process.once("exit", () => fs.rmSync(paperclipTestHome, { recursive: true, force: true, maxRetries: 8, retryDelay: 50 }));
const require = createRequire(import.meta.url);
const SupertestTest = require("supertest/lib/test.js") as SupertestTestConstructor;
@@ -12,6 +12,7 @@ it("runs every active nested/parameterized fixture case exactly once through the
try {
const tests = path.join(root, "server/src/__tests__");
mkdirSync(tests, { recursive: true });
mkdirSync(path.join(root, "server/scripts"), { recursive: true });
symlinkSync(path.join(repoRoot, "node_modules"), path.join(root, "node_modules"), "junction");
writeFileSync(path.join(root, "package.json"), JSON.stringify({ private: true }));
writeFileSync(path.join(root, "vitest.config.mjs"), `export default {