import express from "express"; import request from "supertest"; import { beforeEach, describe, expect, it, vi } from "vitest"; const mockAgentService = vi.hoisted(() => ({ getById: vi.fn(), })); const mockHeartbeatService = vi.hoisted(() => ({ buildRunOutputSilence: vi.fn(), getRunIssueSummary: vi.fn(), getActiveRunIssueSummaryForAgent: vi.fn(), getRunLogAccess: vi.fn(), readLog: vi.fn(), wakeup: vi.fn(), })); const mockIssueService = vi.hoisted(() => ({ getById: vi.fn(), getByIdentifier: vi.fn(), })); const mockInstanceSettingsService = vi.hoisted(() => ({ get: vi.fn(), getExperimental: vi.fn(), getGeneral: vi.fn(), listCompanyIds: vi.fn(), })); const routeAgentId = "11111111-1111-4111-8111-111111111111"; function registerModuleMocks() { vi.doMock("../routes/authz.js", async () => vi.importActual("../routes/authz.js")); vi.doMock("../services/agents.js", () => ({ agentService: () => mockAgentService, })); vi.doMock("../services/heartbeat.js", () => ({ heartbeatService: () => mockHeartbeatService, })); vi.doMock("../services/instance-settings.js", () => ({ instanceSettingsService: () => mockInstanceSettingsService, })); vi.doMock("../services/issues.js", () => ({ issueService: () => mockIssueService, })); vi.doMock("../services/index.js", () => ({ agentService: () => mockAgentService, agentInstructionsService: () => ({}), accessService: () => ({}), approvalService: () => ({}), companySkillService: () => ({ listRuntimeSkillEntries: vi.fn() }), budgetService: () => ({}), heartbeatService: () => mockHeartbeatService, issueApprovalService: () => ({}), issueService: () => mockIssueService, logActivity: vi.fn(), secretService: () => ({}), syncInstructionsBundleConfigFromFilePath: vi.fn((_agent, config) => config), workspaceOperationService: () => ({}), })); vi.doMock("../adapters/index.js", () => ({ findServerAdapter: vi.fn(), listAdapterModels: vi.fn(), detectAdapterModel: vi.fn(), findActiveServerAdapter: vi.fn(), requireServerAdapter: vi.fn(), })); } async function createApp(db: Record = {}) { const [{ agentRoutes }, { errorHandler }] = await Promise.all([ vi.importActual("../routes/agents.js"), vi.importActual("../middleware/index.js"), ]); const app = express(); app.use(express.json()); app.use((req, _res, next) => { (req as any).actor = { type: "board", userId: "local-board", companyIds: ["company-1"], source: "local_implicit", isInstanceAdmin: false, }; next(); }); app.use("/api", agentRoutes(db as any)); app.use(errorHandler); return app; } function createLiveRunsDbStub(rows: Array>) { const limit = vi.fn(async (value: number) => rows.slice(0, value)); const orderedQuery = { limit, then: (resolve: (value: Array>) => unknown) => Promise.resolve(rows).then(resolve), }; const query = { from: vi.fn().mockReturnThis(), innerJoin: vi.fn().mockReturnThis(), where: vi.fn().mockReturnThis(), orderBy: vi.fn().mockReturnValue(orderedQuery), }; return { db: { select: vi.fn().mockReturnValue(query), }, limit, }; } async function requestApp( app: express.Express, buildRequest: (baseUrl: string) => request.Test, ) { const { createServer } = await vi.importActual("node:http"); const server = createServer(app); try { await new Promise((resolve) => { server.listen(0, "127.0.0.1", resolve); }); const address = server.address(); if (!address || typeof address === "string") { throw new Error("Expected HTTP server to listen on a TCP port"); } return await buildRequest(`http://127.0.0.1:${address.port}`); } finally { if (server.listening) { await new Promise((resolve, reject) => { server.close((error) => { if (error) reject(error); else resolve(); }); }); } } } describe("agent live run routes", () => { beforeEach(() => { vi.resetModules(); vi.doUnmock("../services/agents.js"); vi.doUnmock("../services/heartbeat.js"); vi.doUnmock("../services/index.js"); vi.doUnmock("../services/instance-settings.js"); vi.doUnmock("../services/issues.js"); vi.doUnmock("../adapters/index.js"); vi.doUnmock("../routes/agents.js"); vi.doUnmock("../routes/authz.js"); vi.doUnmock("../middleware/index.js"); registerModuleMocks(); vi.clearAllMocks(); mockIssueService.getByIdentifier.mockResolvedValue({ id: "issue-1", companyId: "company-1", executionRunId: "run-1", assigneeAgentId: "agent-1", status: "in_progress", }); mockIssueService.getById.mockResolvedValue(null); mockAgentService.getById.mockResolvedValue({ id: "agent-1", companyId: "company-1", name: "Builder", adapterType: "codex_local", }); mockInstanceSettingsService.get.mockResolvedValue({ id: "instance-settings-1", general: { censorUsernameInLogs: false, feedbackDataSharingPreference: "prompt", }, }); mockInstanceSettingsService.getExperimental.mockResolvedValue({}); mockInstanceSettingsService.getGeneral.mockResolvedValue({ censorUsernameInLogs: false, feedbackDataSharingPreference: "prompt", }); mockInstanceSettingsService.listCompanyIds.mockResolvedValue(["company-1"]); mockHeartbeatService.buildRunOutputSilence.mockResolvedValue(null); mockHeartbeatService.getRunIssueSummary.mockResolvedValue({ id: "run-1", status: "running", invocationSource: "on_demand", triggerDetail: "manual", contextCommentId: "comment-1", contextWakeCommentId: "comment-1", startedAt: new Date("2026-04-10T09:30:00.000Z"), finishedAt: null, createdAt: new Date("2026-04-10T09:29:59.000Z"), agentId: "agent-1", issueId: "issue-1", }); mockHeartbeatService.getActiveRunIssueSummaryForAgent.mockResolvedValue(null); mockHeartbeatService.buildRunOutputSilence.mockResolvedValue(null); mockHeartbeatService.getRunLogAccess.mockResolvedValue({ id: "run-1", companyId: "company-1", logStore: "local_file", logRef: "logs/run-1.ndjson", }); mockHeartbeatService.readLog.mockResolvedValue({ runId: "run-1", store: "local_file", logRef: "logs/run-1.ndjson", content: "chunk", nextOffset: 5, }); mockHeartbeatService.wakeup.mockResolvedValue({ id: "run-1", companyId: "company-1", agentId: "agent-1", status: "queued", invocationSource: "on_demand", triggerDetail: "manual", }); }); it("returns a compact active run payload for issue polling", async () => { const res = await requestApp( await createApp(), (baseUrl) => request(baseUrl).get("/api/issues/pc1a2-1295/active-run"), ); expect(res.status, JSON.stringify(res.body)).toBe(200); expect(mockIssueService.getByIdentifier).toHaveBeenCalledWith("PC1A2-1295"); expect(mockHeartbeatService.getRunIssueSummary).toHaveBeenCalledWith("run-1"); expect(res.body).toMatchObject({ id: "run-1", status: "running", invocationSource: "on_demand", triggerDetail: "manual", contextCommentId: "comment-1", contextWakeCommentId: "comment-1", startedAt: "2026-04-10T09:30:00.000Z", finishedAt: null, createdAt: "2026-04-10T09:29:59.000Z", agentId: "agent-1", issueId: "issue-1", agentName: "Builder", adapterType: "codex_local", outputSilence: null, }); expect(res.body).not.toHaveProperty("resultJson"); expect(res.body).not.toHaveProperty("contextSnapshot"); expect(res.body).not.toHaveProperty("logRef"); }, 10_000); it("ignores a stale execution run from another issue and falls back to the assignee's matching run", async () => { mockHeartbeatService.getRunIssueSummary.mockResolvedValue({ id: "run-foreign", status: "running", invocationSource: "assignment", triggerDetail: "callback", startedAt: new Date("2026-04-10T10:00:00.000Z"), finishedAt: null, createdAt: new Date("2026-04-10T09:59:00.000Z"), agentId: "agent-1", issueId: "issue-2", }); mockHeartbeatService.getActiveRunIssueSummaryForAgent.mockResolvedValue({ id: "run-1", status: "running", invocationSource: "on_demand", triggerDetail: "manual", startedAt: new Date("2026-04-10T09:30:00.000Z"), finishedAt: null, createdAt: new Date("2026-04-10T09:29:59.000Z"), agentId: "agent-1", issueId: "issue-1", }); const res = await requestApp( await createApp(), (baseUrl) => request(baseUrl).get("/api/issues/PC1A2-1295/active-run"), ); expect(res.status, JSON.stringify(res.body)).toBe(200); expect(mockHeartbeatService.getRunIssueSummary).toHaveBeenCalledWith("run-1"); expect(mockHeartbeatService.getActiveRunIssueSummaryForAgent).toHaveBeenCalledWith("agent-1"); expect(res.body).toMatchObject({ id: "run-1", issueId: "issue-1", agentId: "agent-1", agentName: "Builder", adapterType: "codex_local", }); }); it("uses narrow run log metadata lookups for log polling", async () => { const res = await requestApp( await createApp(), (baseUrl) => request(baseUrl).get("/api/heartbeat-runs/run-1/log?offset=12&limitBytes=64"), ); expect(res.status, JSON.stringify(res.body)).toBe(200); expect(mockHeartbeatService.getRunLogAccess).toHaveBeenCalledWith("run-1"); expect(mockHeartbeatService.readLog).toHaveBeenCalledWith({ id: "run-1", companyId: "company-1", logStore: "local_file", logRef: "logs/run-1.ndjson", }, { offset: 12, limitBytes: 64, }); expect(res.body).toEqual({ runId: "run-1", store: "local_file", logRef: "logs/run-1.ndjson", content: "chunk", nextOffset: 5, }); }); it("caps company live run polling by default", async () => { const rows = Array.from({ length: 75 }, (_, index) => ({ id: `run-${index}`, companyId: "company-1", status: "running", invocationSource: "on_demand", triggerDetail: "manual", startedAt: new Date("2026-04-10T09:30:00.000Z"), finishedAt: null, createdAt: new Date(`2026-04-10T09:${String(index % 60).padStart(2, "0")}:00.000Z`), agentId: "agent-1", agentName: "Builder", adapterType: "codex_local", logBytes: 0, livenessState: "healthy", livenessReason: null, continuationAttempt: 0, lastUsefulActionAt: null, nextAction: null, lastOutputAt: null, lastOutputSeq: null, lastOutputStream: null, lastOutputBytes: 0, processStartedAt: null, issueId: "issue-1", })); const { db, limit } = createLiveRunsDbStub(rows); const res = await requestApp( await createApp(db), (baseUrl) => request(baseUrl).get("/api/companies/company-1/live-runs"), ); expect(res.status, JSON.stringify(res.body)).toBe(200); expect(limit).toHaveBeenCalledWith(50); expect(res.body).toHaveLength(50); expect(mockHeartbeatService.buildRunOutputSilence).toHaveBeenCalledTimes(50); }); it("treats explicit zero or invalid live run limit as the capped default", async () => { const rows = Array.from({ length: 75 }, (_, index) => ({ id: `run-${index}`, companyId: "company-1", status: "running", invocationSource: "on_demand", triggerDetail: "manual", startedAt: new Date("2026-04-10T09:30:00.000Z"), finishedAt: null, createdAt: new Date(`2026-04-10T09:${String(index % 60).padStart(2, "0")}:00.000Z`), agentId: "agent-1", agentName: "Builder", adapterType: "codex_local", logBytes: 0, livenessState: "healthy", livenessReason: null, continuationAttempt: 0, lastUsefulActionAt: null, nextAction: null, lastOutputAt: null, lastOutputSeq: null, lastOutputStream: null, lastOutputBytes: 0, processStartedAt: null, issueId: "issue-1", })); const { db, limit } = createLiveRunsDbStub(rows); const res = await requestApp( await createApp(db), (baseUrl) => request(baseUrl).get("/api/companies/company-1/live-runs?limit=0&minCount=0"), ); expect(res.status, JSON.stringify(res.body)).toBe(200); expect(limit).toHaveBeenCalledWith(50); expect(res.body).toHaveLength(50); }); it("does not pad with recent runs when no minCount is requested", async () => { const liveRows = Array.from({ length: 8 }, (_, index) => ({ id: `run-live-${index}`, companyId: "company-1", status: "running", invocationSource: "on_demand", triggerDetail: "manual", startedAt: new Date("2026-04-10T09:30:00.000Z"), finishedAt: null, createdAt: new Date(`2026-04-10T09:${String(index % 60).padStart(2, "0")}:00.000Z`), agentId: "agent-1", agentName: "Builder", adapterType: "codex_local", logBytes: 0, livenessState: "healthy", livenessReason: null, continuationAttempt: 0, lastUsefulActionAt: null, nextAction: null, lastOutputAt: null, lastOutputSeq: null, lastOutputStream: null, lastOutputBytes: 0, processStartedAt: null, issueId: "issue-1", })); const selectCalls: Array> = []; const db = { select: vi.fn().mockImplementation(() => { const limitFn = vi.fn(async (value: number) => liveRows.slice(0, value)); const orderedQuery = { limit: limitFn, then: (resolve: (value: typeof liveRows) => unknown) => Promise.resolve(liveRows).then(resolve), }; const query = { from: vi.fn().mockReturnThis(), innerJoin: vi.fn().mockReturnThis(), where: vi.fn().mockReturnThis(), orderBy: vi.fn().mockReturnValue(orderedQuery), }; selectCalls.push(limitFn); return query; }), }; const res = await requestApp( await createApp(db), (baseUrl) => request(baseUrl).get("/api/companies/company-1/live-runs"), ); expect(res.status, JSON.stringify(res.body)).toBe(200); expect(res.body).toHaveLength(8); expect(db.select).toHaveBeenCalledTimes(1); }); it("pads with recent runs when minCount is explicitly requested", async () => { const liveRows = Array.from({ length: 2 }, (_, index) => ({ id: `run-live-${index}`, companyId: "company-1", status: "running", invocationSource: "on_demand", triggerDetail: "manual", startedAt: new Date("2026-04-10T09:30:00.000Z"), finishedAt: null, createdAt: new Date(`2026-04-10T09:${String(index % 60).padStart(2, "0")}:00.000Z`), agentId: "agent-1", agentName: "Builder", adapterType: "codex_local", logBytes: 0, livenessState: "healthy", livenessReason: null, continuationAttempt: 0, lastUsefulActionAt: null, nextAction: null, lastOutputAt: null, lastOutputSeq: null, lastOutputStream: null, lastOutputBytes: 0, processStartedAt: null, issueId: "issue-1", })); const recentRows = Array.from({ length: 4 }, (_, index) => ({ id: `run-recent-${index}`, companyId: "company-1", status: "succeeded", invocationSource: "on_demand", triggerDetail: "manual", startedAt: new Date("2026-04-09T09:30:00.000Z"), finishedAt: new Date("2026-04-09T09:35:00.000Z"), createdAt: new Date(`2026-04-09T09:${String(index % 60).padStart(2, "0")}:00.000Z`), agentId: "agent-1", agentName: "Builder", adapterType: "codex_local", logBytes: 0, livenessState: "healthy", livenessReason: null, continuationAttempt: 0, lastUsefulActionAt: null, nextAction: null, lastOutputAt: null, lastOutputSeq: null, lastOutputStream: null, lastOutputBytes: 0, processStartedAt: null, issueId: "issue-1", })); let selectCallCount = 0; const db = { select: vi.fn().mockImplementation(() => { selectCallCount += 1; const rows = selectCallCount === 1 ? liveRows : recentRows; const limitFn = vi.fn(async (value: number) => rows.slice(0, value)); const orderedQuery = { limit: limitFn, then: (resolve: (value: typeof rows) => unknown) => Promise.resolve(rows).then(resolve), }; return { from: vi.fn().mockReturnThis(), innerJoin: vi.fn().mockReturnThis(), where: vi.fn().mockReturnThis(), orderBy: vi.fn().mockReturnValue(orderedQuery), }; }), }; const res = await requestApp( await createApp(db), (baseUrl) => request(baseUrl).get("/api/companies/company-1/live-runs?minCount=4"), ); expect(res.status, JSON.stringify(res.body)).toBe(200); expect(res.body).toHaveLength(4); expect(db.select).toHaveBeenCalledTimes(2); }); it("passes scoped wake fields through the legacy heartbeat invoke route", async () => { const res = await requestApp( await createApp(), (baseUrl) => request(baseUrl) .post(`/api/agents/${routeAgentId}/heartbeat/invoke?companyId=company-1`) .send({ reason: "issue_assigned", payload: { issueId: "issue-1", taskId: "issue-1", taskKey: "issue-1", }, forceFreshSession: true, }), ); expect(res.status, JSON.stringify(res.body)).toBe(202); // The legacy /heartbeat/invoke endpoint forwards only the wake fields the // caller actually supplied so empty-body callers (e.g. e2e suites) match // the original fixed-arg `heartbeat.invoke()` shape exactly. When the // caller supplies reason / payload / forceFreshSession those are // forwarded; idempotencyKey is omitted unless explicitly set. expect(mockHeartbeatService.wakeup).toHaveBeenCalledWith(routeAgentId, { source: "on_demand", triggerDetail: "manual", reason: "issue_assigned", payload: { issueId: "issue-1", taskId: "issue-1", taskKey: "issue-1", }, requestedByActorType: "user", requestedByActorId: "local-board", contextSnapshot: { triggeredBy: "board", actorId: "local-board", forceFreshSession: true, }, }); }); it("calls heartbeat.wakeup with the legacy minimal shape when the body is empty", async () => { const res = await requestApp( await createApp(), (baseUrl) => request(baseUrl) .post(`/api/agents/${routeAgentId}/heartbeat/invoke?companyId=company-1`) .send({}), ); expect(res.status, JSON.stringify(res.body)).toBe(202); expect(mockHeartbeatService.wakeup).toHaveBeenCalledWith(routeAgentId, { source: "on_demand", triggerDetail: "manual", requestedByActorType: "user", requestedByActorId: "local-board", contextSnapshot: { triggeredBy: "board", actorId: "local-board", }, }); }); });