feat: polish inbox and issue list workflows

This commit is contained in:
Dotta
2026-04-10 22:26:21 -05:00
parent 548721248e
commit dab95740be
37 changed files with 1674 additions and 411 deletions
@@ -1,6 +1,8 @@
import express from "express";
import request from "supertest";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { issueRoutes } from "../routes/issues.js";
import { errorHandler } from "../middleware/index.js";
const issueId = "11111111-1111-4111-8111-111111111111";
const companyId = "22222222-2222-4222-8222-222222222222";
@@ -50,11 +52,7 @@ vi.mock("../services/index.js", () => ({
workProductService: () => ({}),
}));
async function createApp() {
const [{ issueRoutes }, { errorHandler }] = await Promise.all([
import("../routes/issues.js"),
import("../middleware/index.js"),
]);
function createApp() {
const app = express();
app.use(express.json());
app.use((req, _res, next) => {
@@ -74,7 +72,6 @@ async function createApp() {
describe("issue document revision routes", () => {
beforeEach(() => {
vi.resetModules();
vi.resetAllMocks();
mockIssueService.getById.mockResolvedValue({
id: issueId,
@@ -125,10 +122,9 @@ describe("issue document revision routes", () => {
});
it("returns revision snapshots including title and format", async () => {
const res = await request(await createApp()).get(`/api/issues/${issueId}/documents/plan/revisions`);
const res = await request(createApp()).get(`/api/issues/${issueId}/documents/plan/revisions`);
expect(res.status).toBe(200);
expect(mockDocumentsService.listIssueDocumentRevisions).toHaveBeenCalledWith(issueId, "plan");
expect(res.body).toEqual([
expect.objectContaining({
revisionNumber: 2,
@@ -140,7 +136,7 @@ describe("issue document revision routes", () => {
});
it("restores a revision through the append-only route and logs the action", async () => {
const res = await request(await createApp())
const res = await request(createApp())
.post(`/api/issues/${issueId}/documents/plan/revisions/revision-1/restore`)
.send({});
@@ -172,7 +168,7 @@ describe("issue document revision routes", () => {
});
it("rejects invalid document keys before attempting restore", async () => {
const res = await request(await createApp())
const res = await request(createApp())
.post(`/api/issues/${issueId}/documents/INVALID KEY/revisions/revision-1/restore`)
.send({});
+49 -50
View File
@@ -26,56 +26,53 @@ import {
getEmbeddedPostgresTestSupport,
startEmbeddedPostgresTestDatabase,
} from "./helpers/embedded-postgres.js";
import { errorHandler } from "../middleware/index.js";
import { accessService } from "../services/access.js";
function registerServiceMocks() {
vi.doMock("../services/index.js", async () => {
const actual = await vi.importActual<typeof import("../services/index.js")>("../services/index.js");
vi.mock("../services/index.js", async () => {
const actual = await vi.importActual<typeof import("../services/index.js")>("../services/index.js");
return {
...actual,
routineService: (db: any) =>
actual.routineService(db, {
heartbeat: {
wakeup: async (agentId: string, wakeupOpts: any) => {
const issueId =
(typeof wakeupOpts?.payload?.issueId === "string" && wakeupOpts.payload.issueId) ||
(typeof wakeupOpts?.contextSnapshot?.issueId === "string" && wakeupOpts.contextSnapshot.issueId) ||
null;
if (!issueId) return null;
return {
...actual,
routineService: (db: any) =>
actual.routineService(db, {
heartbeat: {
wakeup: async (agentId: string, wakeupOpts: any) => {
const issueId =
(typeof wakeupOpts?.payload?.issueId === "string" && wakeupOpts.payload.issueId) ||
(typeof wakeupOpts?.contextSnapshot?.issueId === "string" && wakeupOpts.contextSnapshot.issueId) ||
null;
if (!issueId) return null;
const issue = await db
.select({ companyId: issues.companyId })
.from(issues)
.where(eq(issues.id, issueId))
.then((rows: Array<{ companyId: string }>) => rows[0] ?? null);
if (!issue) return null;
const issue = await db
.select({ companyId: issues.companyId })
.from(issues)
.where(eq(issues.id, issueId))
.then((rows: Array<{ companyId: string }>) => rows[0] ?? null);
if (!issue) return null;
const queuedRunId = randomUUID();
await db.insert(heartbeatRuns).values({
id: queuedRunId,
companyId: issue.companyId,
agentId,
invocationSource: wakeupOpts?.source ?? "assignment",
triggerDetail: wakeupOpts?.triggerDetail ?? null,
status: "queued",
contextSnapshot: { ...(wakeupOpts?.contextSnapshot ?? {}), issueId },
});
await db
.update(issues)
.set({
executionRunId: queuedRunId,
executionLockedAt: new Date(),
})
.where(eq(issues.id, issueId));
return { id: queuedRunId };
},
const queuedRunId = randomUUID();
await db.insert(heartbeatRuns).values({
id: queuedRunId,
companyId: issue.companyId,
agentId,
invocationSource: wakeupOpts?.source ?? "assignment",
triggerDetail: wakeupOpts?.triggerDetail ?? null,
status: "queued",
contextSnapshot: { ...(wakeupOpts?.contextSnapshot ?? {}), issueId },
});
await db
.update(issues)
.set({
executionRunId: queuedRunId,
executionLockedAt: new Date(),
})
.where(eq(issues.id, issueId));
return { id: queuedRunId };
},
}),
};
});
}
},
}),
};
});
const embeddedPostgresSupport = await getEmbeddedPostgresTestSupport();
const describeEmbeddedPostgres = embeddedPostgresSupport.supported ? describe : describe.skip;
@@ -95,11 +92,6 @@ describeEmbeddedPostgres("routine routes end-to-end", () => {
db = createDb(tempDb.connectionString);
}, 20_000);
beforeEach(() => {
vi.resetModules();
registerServiceMocks();
});
afterEach(async () => {
await db.delete(activityLog);
await db.delete(routineRuns);
@@ -123,8 +115,15 @@ describeEmbeddedPostgres("routine routes end-to-end", () => {
await tempDb?.cleanup();
});
beforeEach(() => {
vi.resetModules();
});
async function createApp(actor: Record<string, unknown>) {
const { routineRoutes } = await import("../routes/routines.js");
const [{ routineRoutes }, { errorHandler }] = await Promise.all([
import("../routes/routines.js"),
import("../middleware/index.js"),
]);
const app = express();
app.use(express.json());
app.use((req, _res, next) => {
+23 -29
View File
@@ -1,6 +1,8 @@
import express from "express";
import request from "supertest";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { errorHandler } from "../middleware/index.js";
import { routineRoutes } from "../routes/routines.js";
const companyId = "22222222-2222-4222-8222-222222222222";
const agentId = "11111111-1111-4111-8111-111111111111";
@@ -83,28 +85,22 @@ const mockLogActivity = vi.hoisted(() => vi.fn());
const mockTrackRoutineCreated = vi.hoisted(() => vi.fn());
const mockGetTelemetryClient = vi.hoisted(() => vi.fn());
function registerRouteMocks() {
vi.doMock("@paperclipai/shared/telemetry", () => ({
trackRoutineCreated: mockTrackRoutineCreated,
trackErrorHandlerCrash: vi.fn(),
}));
vi.mock("@paperclipai/shared/telemetry", () => ({
trackRoutineCreated: mockTrackRoutineCreated,
trackErrorHandlerCrash: vi.fn(),
}));
vi.doMock("../telemetry.js", () => ({
getTelemetryClient: mockGetTelemetryClient,
}));
vi.mock("../telemetry.js", () => ({
getTelemetryClient: mockGetTelemetryClient,
}));
vi.doMock("../services/index.js", () => ({
accessService: () => mockAccessService,
logActivity: mockLogActivity,
routineService: () => mockRoutineService,
}));
}
vi.mock("../services/index.js", () => ({
accessService: () => mockAccessService,
logActivity: mockLogActivity,
routineService: () => mockRoutineService,
}));
async function createApp(actor: Record<string, unknown>) {
const [{ routineRoutes }, { errorHandler }] = await Promise.all([
import("../routes/routines.js"),
import("../middleware/index.js"),
]);
function createApp(actor: Record<string, unknown>) {
const app = express();
app.use(express.json());
app.use((req, _res, next) => {
@@ -118,9 +114,7 @@ async function createApp(actor: Record<string, unknown>) {
describe("routine routes", () => {
beforeEach(() => {
vi.resetModules();
registerRouteMocks();
vi.clearAllMocks();
vi.resetAllMocks();
mockGetTelemetryClient.mockReturnValue({ track: vi.fn() });
mockRoutineService.create.mockResolvedValue(routine);
mockRoutineService.get.mockResolvedValue(routine);
@@ -136,7 +130,7 @@ describe("routine routes", () => {
});
it("requires tasks:assign permission for non-admin board routine creation", async () => {
const app = await createApp({
const app = createApp({
type: "board",
userId: "board-user",
source: "session",
@@ -158,7 +152,7 @@ describe("routine routes", () => {
});
it("requires tasks:assign permission to retarget a routine assignee", async () => {
const app = await createApp({
const app = createApp({
type: "board",
userId: "board-user",
source: "session",
@@ -179,7 +173,7 @@ describe("routine routes", () => {
it("requires tasks:assign permission to reactivate a routine", async () => {
mockRoutineService.get.mockResolvedValue(pausedRoutine);
const app = await createApp({
const app = createApp({
type: "board",
userId: "board-user",
source: "session",
@@ -199,7 +193,7 @@ describe("routine routes", () => {
});
it("requires tasks:assign permission to create a trigger", async () => {
const app = await createApp({
const app = createApp({
type: "board",
userId: "board-user",
source: "session",
@@ -221,7 +215,7 @@ describe("routine routes", () => {
});
it("requires tasks:assign permission to update a trigger", async () => {
const app = await createApp({
const app = createApp({
type: "board",
userId: "board-user",
source: "session",
@@ -241,7 +235,7 @@ describe("routine routes", () => {
});
it("requires tasks:assign permission to manually run a routine", async () => {
const app = await createApp({
const app = createApp({
type: "board",
userId: "board-user",
source: "session",
@@ -260,7 +254,7 @@ describe("routine routes", () => {
it("allows routine creation when the board user has tasks:assign", async () => {
mockAccessService.canUser.mockResolvedValue(true);
const app = await createApp({
const app = createApp({
type: "board",
userId: "board-user",
source: "session",