2026-04-02 09:11:49 -05:00
|
|
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
|
|
|
|
|
|
|
const {
|
|
|
|
|
createAppMock,
|
|
|
|
|
createDbMock,
|
|
|
|
|
detectPortMock,
|
|
|
|
|
feedbackExportServiceMock,
|
|
|
|
|
feedbackServiceFactoryMock,
|
|
|
|
|
fakeServer,
|
|
|
|
|
} = vi.hoisted(() => {
|
|
|
|
|
const createAppMock = vi.fn(async () => ((_: unknown, __: unknown) => {}) as never);
|
|
|
|
|
const createDbMock = vi.fn(() => ({}) as never);
|
|
|
|
|
const detectPortMock = vi.fn(async (port: number) => port);
|
|
|
|
|
const feedbackExportServiceMock = {
|
|
|
|
|
flushPendingFeedbackTraces: vi.fn(async () => ({ attempted: 0, sent: 0, failed: 0 })),
|
|
|
|
|
};
|
|
|
|
|
const feedbackServiceFactoryMock = vi.fn(() => feedbackExportServiceMock);
|
|
|
|
|
const fakeServer = {
|
|
|
|
|
once: vi.fn().mockReturnThis(),
|
|
|
|
|
off: vi.fn().mockReturnThis(),
|
|
|
|
|
listen: vi.fn((_port: number, _host: string, callback?: () => void) => {
|
|
|
|
|
callback?.();
|
|
|
|
|
return fakeServer;
|
|
|
|
|
}),
|
|
|
|
|
close: vi.fn(),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
createAppMock,
|
|
|
|
|
createDbMock,
|
|
|
|
|
detectPortMock,
|
|
|
|
|
feedbackExportServiceMock,
|
|
|
|
|
feedbackServiceFactoryMock,
|
|
|
|
|
fakeServer,
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
vi.mock("node:http", () => ({
|
|
|
|
|
createServer: vi.fn(() => fakeServer),
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
vi.mock("detect-port", () => ({
|
|
|
|
|
default: detectPortMock,
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
vi.mock("@paperclipai/db", () => ({
|
|
|
|
|
createDb: createDbMock,
|
|
|
|
|
ensurePostgresDatabase: vi.fn(),
|
|
|
|
|
getPostgresDataDirectory: vi.fn(),
|
|
|
|
|
inspectMigrations: vi.fn(async () => ({ status: "upToDate" })),
|
|
|
|
|
applyPendingMigrations: vi.fn(),
|
|
|
|
|
reconcilePendingMigrationHistory: vi.fn(async () => ({ repairedMigrations: [] })),
|
|
|
|
|
formatDatabaseBackupResult: vi.fn(() => "ok"),
|
|
|
|
|
runDatabaseBackup: vi.fn(),
|
|
|
|
|
authUsers: {},
|
|
|
|
|
companies: {},
|
|
|
|
|
companyMemberships: {},
|
|
|
|
|
instanceUserRoles: {},
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
vi.mock("../app.js", () => ({
|
|
|
|
|
createApp: createAppMock,
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
vi.mock("../config.js", () => ({
|
|
|
|
|
loadConfig: vi.fn(() => ({
|
|
|
|
|
deploymentMode: "authenticated",
|
|
|
|
|
deploymentExposure: "private",
|
2026-04-10 07:32:16 -05:00
|
|
|
bind: "loopback",
|
|
|
|
|
customBindHost: undefined,
|
2026-04-02 09:11:49 -05:00
|
|
|
host: "127.0.0.1",
|
|
|
|
|
port: 3210,
|
|
|
|
|
allowedHostnames: [],
|
|
|
|
|
authBaseUrlMode: "auto",
|
|
|
|
|
authPublicBaseUrl: undefined,
|
|
|
|
|
authDisableSignUp: false,
|
|
|
|
|
databaseMode: "postgres",
|
|
|
|
|
databaseUrl: "postgres://paperclip:paperclip@127.0.0.1:5432/paperclip",
|
|
|
|
|
embeddedPostgresDataDir: "/tmp/paperclip-test-db",
|
|
|
|
|
embeddedPostgresPort: 54329,
|
|
|
|
|
databaseBackupEnabled: false,
|
|
|
|
|
databaseBackupIntervalMinutes: 60,
|
|
|
|
|
databaseBackupRetentionDays: 30,
|
|
|
|
|
databaseBackupDir: "/tmp/paperclip-test-backups",
|
|
|
|
|
serveUi: false,
|
|
|
|
|
uiDevMiddleware: false,
|
|
|
|
|
secretsProvider: "local_encrypted",
|
|
|
|
|
secretsStrictMode: false,
|
|
|
|
|
secretsMasterKeyFilePath: "/tmp/paperclip-master.key",
|
|
|
|
|
storageProvider: "local_disk",
|
|
|
|
|
storageLocalDiskBaseDir: "/tmp/paperclip-storage",
|
|
|
|
|
storageS3Bucket: "paperclip-test",
|
|
|
|
|
storageS3Region: "us-east-1",
|
|
|
|
|
storageS3Endpoint: undefined,
|
|
|
|
|
storageS3Prefix: "",
|
|
|
|
|
storageS3ForcePathStyle: false,
|
|
|
|
|
feedbackExportBackendUrl: "https://telemetry.example.com",
|
|
|
|
|
feedbackExportBackendToken: "telemetry-token",
|
|
|
|
|
heartbeatSchedulerEnabled: false,
|
|
|
|
|
heartbeatSchedulerIntervalMs: 30000,
|
|
|
|
|
companyDeletionEnabled: false,
|
|
|
|
|
})),
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
vi.mock("../middleware/logger.js", () => ({
|
|
|
|
|
logger: {
|
|
|
|
|
info: vi.fn(),
|
|
|
|
|
warn: vi.fn(),
|
|
|
|
|
error: vi.fn(),
|
|
|
|
|
},
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
vi.mock("../realtime/live-events-ws.js", () => ({
|
|
|
|
|
setupLiveEventsWebSocketServer: vi.fn(),
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
vi.mock("../services/index.js", () => ({
|
|
|
|
|
feedbackService: feedbackServiceFactoryMock,
|
|
|
|
|
heartbeatService: vi.fn(() => ({
|
|
|
|
|
reapOrphanedRuns: vi.fn(async () => undefined),
|
|
|
|
|
resumeQueuedRuns: vi.fn(async () => undefined),
|
[codex] Harden execution reliability and heartbeat tooling (#3679)
## Thinking Path
> - Paperclip orchestrates AI agents for zero-human companies
> - Reliable execution depends on heartbeat routing, issue lifecycle
semantics, telemetry, and a fast enough local verification loop to keep
regressions visible
> - The remaining commits on this branch were mostly server/runtime
correctness fixes plus test and documentation follow-ups in that area
> - Those changes are logically separate from the UI-focused
issue-detail and workspace/navigation branches even when they touch
overlapping issue APIs
> - This pull request groups the execution reliability, heartbeat,
telemetry, and tooling changes into one standalone branch
> - The benefit is a focused review of the control-plane correctness
work, including the follow-up fix that restored the implicit
comment-reopen helpers after branch splitting
## What Changed
- Hardened issue/heartbeat execution behavior, including self-review
stage skipping, deferred mention wakes during active execution, stranded
execution recovery, active-run scoping, assignee resolution, and
blocked-to-todo wake resumption
- Reduced noisy polling/logging overhead by trimming issue run payloads,
compacting persisted run logs, silencing high-volume request logs, and
capping heartbeat-run queries in dashboard/inbox surfaces
- Expanded telemetry and status semantics with adapter/model fields on
task completion plus clearer status guidance in docs/onboarding material
- Updated test infrastructure and verification defaults with faster
route-test module isolation, cheaper default `pnpm test`, e2e isolation
from local state, and repo verification follow-ups
- Included docs/release housekeeping from the branch and added a small
follow-up commit restoring the implicit comment-reopen helpers that were
dropped during branch reconstruction
## Verification
- `pnpm vitest run
server/src/__tests__/issue-comment-reopen-routes.test.ts
server/src/__tests__/issue-telemetry-routes.test.ts`
- `pnpm vitest run server/src/__tests__/http-log-policy.test.ts
server/src/__tests__/heartbeat-run-log.test.ts
server/src/__tests__/health.test.ts`
- `server/src/__tests__/activity-service.test.ts`,
`server/src/__tests__/heartbeat-comment-wake-batching.test.ts`, and
`server/src/__tests__/heartbeat-process-recovery.test.ts` were attempted
on this host but the embedded Postgres harness reported
init-script/data-dir problems and skipped or failed to start, so they
are noted as environment-limited
## Risks
- Medium: this branch changes core issue/heartbeat routing and
reopen/wakeup behavior, so regressions would affect agent execution flow
rather than isolated UI polish
- Because it also updates verification infrastructure, reviewers should
pay attention to whether the new tests are asserting the right failure
modes and not just reshaping harness behavior
## Model Used
- OpenAI Codex coding agent (GPT-5-class runtime in Codex CLI; exact
deployed model ID is not exposed in this environment), reasoning
enabled, tool use and local code execution enabled
## 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)
- [ ] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [ ] If this change affects the UI, I have included before/after
screenshots
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge
---------
Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-04-14 13:34:52 -05:00
|
|
|
reconcileStrandedAssignedIssues: vi.fn(async () => ({
|
|
|
|
|
dispatchRequeued: 0,
|
|
|
|
|
continuationRequeued: 0,
|
|
|
|
|
escalated: 0,
|
|
|
|
|
skipped: 0,
|
|
|
|
|
issueIds: [],
|
|
|
|
|
})),
|
2026-04-02 09:11:49 -05:00
|
|
|
tickTimers: vi.fn(async () => ({ enqueued: 0 })),
|
|
|
|
|
})),
|
|
|
|
|
reconcilePersistedRuntimeServicesOnStartup: vi.fn(async () => ({ reconciled: 0 })),
|
|
|
|
|
routineService: vi.fn(() => ({
|
|
|
|
|
tickScheduledTriggers: vi.fn(async () => ({ triggered: 0 })),
|
|
|
|
|
})),
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
vi.mock("../storage/index.js", () => ({
|
|
|
|
|
createStorageServiceFromConfig: vi.fn(() => ({ id: "storage-service" })),
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
vi.mock("../services/feedback-share-client.js", () => ({
|
|
|
|
|
createFeedbackTraceShareClientFromConfig: vi.fn(() => ({ id: "feedback-share-client" })),
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
vi.mock("../startup-banner.js", () => ({
|
|
|
|
|
printStartupBanner: vi.fn(),
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
vi.mock("../board-claim.js", () => ({
|
|
|
|
|
getBoardClaimWarningUrl: vi.fn(() => null),
|
|
|
|
|
initializeBoardClaimChallenge: vi.fn(async () => undefined),
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
vi.mock("../auth/better-auth.js", () => ({
|
|
|
|
|
createBetterAuthHandler: vi.fn(() => undefined),
|
|
|
|
|
createBetterAuthInstance: vi.fn(() => ({})),
|
|
|
|
|
deriveAuthTrustedOrigins: vi.fn(() => []),
|
|
|
|
|
resolveBetterAuthSession: vi.fn(async () => null),
|
|
|
|
|
resolveBetterAuthSessionFromHeaders: vi.fn(async () => null),
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
import { startServer } from "../index.ts";
|
|
|
|
|
|
|
|
|
|
describe("startServer feedback export wiring", () => {
|
|
|
|
|
beforeEach(() => {
|
|
|
|
|
vi.clearAllMocks();
|
|
|
|
|
process.env.BETTER_AUTH_SECRET = "test-secret";
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("passes the feedback export service into createApp so pending traces flush in runtime", async () => {
|
|
|
|
|
const started = await startServer();
|
|
|
|
|
|
|
|
|
|
expect(started.server).toBe(fakeServer);
|
|
|
|
|
expect(feedbackServiceFactoryMock).toHaveBeenCalledTimes(1);
|
|
|
|
|
expect(createAppMock).toHaveBeenCalledTimes(1);
|
|
|
|
|
expect(createAppMock.mock.calls[0]?.[1]).toMatchObject({
|
|
|
|
|
feedbackExportService: feedbackExportServiceMock,
|
|
|
|
|
storageService: { id: "storage-service" },
|
|
|
|
|
serverPort: 3210,
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
});
|