mirror of
https://github.com/alkimake/paperclip.git
synced 2026-06-17 03:10:38 +09:00
Persist non-issue inbox dismissals
This commit is contained in:
parent
1de5fb9316
commit
5640d29ab0
23 changed files with 13623 additions and 54 deletions
212
server/src/__tests__/inbox-dismissals.test.ts
Normal file
212
server/src/__tests__/inbox-dismissals.test.ts
Normal file
|
|
@ -0,0 +1,212 @@
|
|||
import { randomUUID } from "node:crypto";
|
||||
import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest";
|
||||
import {
|
||||
agents,
|
||||
approvals,
|
||||
companies,
|
||||
createDb,
|
||||
heartbeatRuns,
|
||||
inboxDismissals,
|
||||
invites,
|
||||
joinRequests,
|
||||
} from "@paperclipai/db";
|
||||
import {
|
||||
getEmbeddedPostgresTestSupport,
|
||||
startEmbeddedPostgresTestDatabase,
|
||||
} from "./helpers/embedded-postgres.js";
|
||||
import { inboxDismissalService } from "../services/inbox-dismissals.ts";
|
||||
import { sidebarBadgeService } from "../services/sidebar-badges.ts";
|
||||
|
||||
const embeddedPostgresSupport = await getEmbeddedPostgresTestSupport();
|
||||
const describeEmbeddedPostgres = embeddedPostgresSupport.supported ? describe : describe.skip;
|
||||
|
||||
if (!embeddedPostgresSupport.supported) {
|
||||
console.warn(
|
||||
`Skipping embedded Postgres inbox dismissal tests on this host: ${embeddedPostgresSupport.reason ?? "unsupported environment"}`,
|
||||
);
|
||||
}
|
||||
|
||||
describeEmbeddedPostgres("inbox dismissals", () => {
|
||||
let db!: ReturnType<typeof createDb>;
|
||||
let dismissalsSvc!: ReturnType<typeof inboxDismissalService>;
|
||||
let badgesSvc!: ReturnType<typeof sidebarBadgeService>;
|
||||
let tempDb: Awaited<ReturnType<typeof startEmbeddedPostgresTestDatabase>> | null = null;
|
||||
|
||||
beforeAll(async () => {
|
||||
tempDb = await startEmbeddedPostgresTestDatabase("paperclip-inbox-dismissals-");
|
||||
db = createDb(tempDb.connectionString);
|
||||
dismissalsSvc = inboxDismissalService(db);
|
||||
badgesSvc = sidebarBadgeService(db);
|
||||
}, 20_000);
|
||||
|
||||
afterEach(async () => {
|
||||
await db.delete(inboxDismissals);
|
||||
await db.delete(joinRequests);
|
||||
await db.delete(invites);
|
||||
await db.delete(heartbeatRuns);
|
||||
await db.delete(approvals);
|
||||
await db.delete(agents);
|
||||
await db.delete(companies);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await tempDb?.cleanup();
|
||||
});
|
||||
|
||||
it("upserts a single dismissal record per user and inbox item key", async () => {
|
||||
const companyId = randomUUID();
|
||||
const userId = "board-user";
|
||||
const firstDismissedAt = new Date("2026-03-11T01:00:00.000Z");
|
||||
const secondDismissedAt = new Date("2026-03-11T02:00:00.000Z");
|
||||
|
||||
await db.insert(companies).values({
|
||||
id: companyId,
|
||||
name: "Paperclip",
|
||||
issuePrefix: "PAP",
|
||||
requireBoardApprovalForNewAgents: false,
|
||||
});
|
||||
|
||||
await dismissalsSvc.dismiss(companyId, userId, "approval:approval-1", firstDismissedAt);
|
||||
await dismissalsSvc.dismiss(companyId, userId, "approval:approval-1", secondDismissedAt);
|
||||
|
||||
const dismissals = await dismissalsSvc.list(companyId, userId);
|
||||
|
||||
expect(dismissals).toHaveLength(1);
|
||||
expect(dismissals[0]?.itemKey).toBe("approval:approval-1");
|
||||
expect(new Date(dismissals[0]?.dismissedAt ?? 0).toISOString()).toBe(secondDismissedAt.toISOString());
|
||||
});
|
||||
|
||||
it("honors dismissal timestamps and resurfaces approvals with newer activity", async () => {
|
||||
const companyId = randomUUID();
|
||||
const userId = "board-user";
|
||||
const primaryAgentId = randomUUID();
|
||||
const secondaryAgentId = randomUUID();
|
||||
const hiddenApprovalId = randomUUID();
|
||||
const resurfacedApprovalId = randomUUID();
|
||||
const inviteId = randomUUID();
|
||||
const hiddenJoinRequestId = randomUUID();
|
||||
const hiddenRunId = randomUUID();
|
||||
const visibleRunId = randomUUID();
|
||||
|
||||
await db.insert(companies).values({
|
||||
id: companyId,
|
||||
name: "Paperclip",
|
||||
issuePrefix: "PAP",
|
||||
requireBoardApprovalForNewAgents: false,
|
||||
});
|
||||
|
||||
await db.insert(agents).values([
|
||||
{
|
||||
id: primaryAgentId,
|
||||
companyId,
|
||||
name: "Primary",
|
||||
role: "engineer",
|
||||
status: "active",
|
||||
adapterType: "codex_local",
|
||||
adapterConfig: {},
|
||||
runtimeConfig: {},
|
||||
permissions: {},
|
||||
},
|
||||
{
|
||||
id: secondaryAgentId,
|
||||
companyId,
|
||||
name: "Secondary",
|
||||
role: "engineer",
|
||||
status: "active",
|
||||
adapterType: "codex_local",
|
||||
adapterConfig: {},
|
||||
runtimeConfig: {},
|
||||
permissions: {},
|
||||
},
|
||||
]);
|
||||
|
||||
await db.insert(approvals).values([
|
||||
{
|
||||
id: hiddenApprovalId,
|
||||
companyId,
|
||||
type: "hire_agent",
|
||||
status: "pending",
|
||||
payload: {},
|
||||
updatedAt: new Date("2026-03-11T01:00:00.000Z"),
|
||||
},
|
||||
{
|
||||
id: resurfacedApprovalId,
|
||||
companyId,
|
||||
type: "hire_agent",
|
||||
status: "revision_requested",
|
||||
payload: {},
|
||||
updatedAt: new Date("2026-03-11T03:00:00.000Z"),
|
||||
},
|
||||
]);
|
||||
|
||||
await db.insert(invites).values({
|
||||
id: inviteId,
|
||||
companyId,
|
||||
inviteType: "company_join",
|
||||
tokenHash: "hash-1",
|
||||
allowedJoinTypes: "both",
|
||||
expiresAt: new Date("2026-03-12T00:00:00.000Z"),
|
||||
});
|
||||
|
||||
await db.insert(joinRequests).values({
|
||||
id: hiddenJoinRequestId,
|
||||
inviteId,
|
||||
companyId,
|
||||
requestType: "human",
|
||||
status: "pending_approval",
|
||||
requestIp: "127.0.0.1",
|
||||
createdAt: new Date("2026-03-11T01:00:00.000Z"),
|
||||
updatedAt: new Date("2026-03-11T01:00:00.000Z"),
|
||||
});
|
||||
|
||||
await db.insert(heartbeatRuns).values([
|
||||
{
|
||||
id: hiddenRunId,
|
||||
companyId,
|
||||
agentId: primaryAgentId,
|
||||
invocationSource: "assignment",
|
||||
status: "failed",
|
||||
createdAt: new Date("2026-03-11T01:00:00.000Z"),
|
||||
updatedAt: new Date("2026-03-11T01:00:00.000Z"),
|
||||
},
|
||||
{
|
||||
id: visibleRunId,
|
||||
companyId,
|
||||
agentId: secondaryAgentId,
|
||||
invocationSource: "assignment",
|
||||
status: "timed_out",
|
||||
createdAt: new Date("2026-03-11T04:00:00.000Z"),
|
||||
updatedAt: new Date("2026-03-11T04:00:00.000Z"),
|
||||
},
|
||||
]);
|
||||
|
||||
await dismissalsSvc.dismiss(companyId, userId, `approval:${hiddenApprovalId}`, new Date("2026-03-11T02:00:00.000Z"));
|
||||
await dismissalsSvc.dismiss(companyId, userId, `approval:${resurfacedApprovalId}`, new Date("2026-03-11T02:00:00.000Z"));
|
||||
await dismissalsSvc.dismiss(companyId, userId, `join:${hiddenJoinRequestId}`, new Date("2026-03-11T02:00:00.000Z"));
|
||||
await dismissalsSvc.dismiss(companyId, userId, `run:${hiddenRunId}`, new Date("2026-03-11T02:00:00.000Z"));
|
||||
|
||||
const dismissedAtByKey = new Map(
|
||||
(await dismissalsSvc.list(companyId, userId)).map((dismissal) => [
|
||||
dismissal.itemKey,
|
||||
new Date(dismissal.dismissedAt).getTime(),
|
||||
]),
|
||||
);
|
||||
|
||||
const badges = await badgesSvc.get(companyId, {
|
||||
dismissals: dismissedAtByKey,
|
||||
joinRequests: [{
|
||||
id: hiddenJoinRequestId,
|
||||
createdAt: new Date("2026-03-11T01:00:00.000Z"),
|
||||
updatedAt: new Date("2026-03-11T01:00:00.000Z"),
|
||||
}],
|
||||
unreadTouchedIssues: 1,
|
||||
});
|
||||
|
||||
expect(badges).toEqual({
|
||||
inbox: 3,
|
||||
approvals: 1,
|
||||
failedRuns: 1,
|
||||
joinRequests: 0,
|
||||
});
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue