paperclip/server/src/__tests__/attention-service.test.ts

2074 lines
72 KiB
TypeScript

import { randomUUID } from "node:crypto";
import express from "express";
import request from "supertest";
import { eq } from "drizzle-orm";
import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest";
import {
activityLog,
agents,
approvals,
assets,
budgetIncidents,
budgetPolicies,
companies,
createDb,
decisionQueueItems,
decisionQueues,
decisionArchiveNotificationOutbox,
decisionRetention,
decisions,
decisionTriage,
decisionTriageEvents,
documents,
heartbeatRunEvents,
heartbeatRuns,
inboxDismissals,
invites,
issueApprovals,
issueAttachments,
issueDocuments,
issueRecoveryActions,
issueRelations,
issueThreadInteractions,
issues,
joinRequests,
projects,
projectWorkspaces,
} from "@paperclipai/db";
import {
getEmbeddedPostgresTestSupport,
startEmbeddedPostgresTestDatabase,
} from "./helpers/embedded-postgres.js";
import { errorHandler } from "../middleware/index.js";
import { attentionRoutes } from "../routes/attention.js";
import { attentionService } from "../services/attention.js";
import { agentService } from "../services/agents.js";
import { ROUTABLE_BLOCKED_ROLLOUT_AT } from "../services/routable-blocked.js";
const embeddedPostgresSupport = await getEmbeddedPostgresTestSupport();
const describeEmbeddedPostgres = embeddedPostgresSupport.supported ? describe : describe.skip;
if (!embeddedPostgresSupport.supported) {
console.warn(
`Skipping embedded Postgres attention service tests on this host: ${embeddedPostgresSupport.reason ?? "unsupported environment"}`,
);
}
describeEmbeddedPostgres("attention service", () => {
let db!: ReturnType<typeof createDb>;
let tempDb: Awaited<ReturnType<typeof startEmbeddedPostgresTestDatabase>> | null = null;
beforeAll(async () => {
tempDb = await startEmbeddedPostgresTestDatabase("paperclip-attention-service-");
db = createDb(tempDb.connectionString);
}, 30_000);
afterEach(async () => {
await db.delete(inboxDismissals);
await db.delete(decisionArchiveNotificationOutbox);
await db.delete(decisionRetention);
await db.delete(decisionTriageEvents);
await db.delete(decisionTriage);
await db.delete(decisionQueueItems);
await db.delete(decisionQueues);
await db.delete(decisions);
await db.delete(issueThreadInteractions);
await db.delete(issueApprovals);
await db.delete(issueAttachments);
await db.delete(issueDocuments);
await db.delete(heartbeatRunEvents);
await db.delete(heartbeatRuns);
await db.delete(budgetIncidents);
await db.delete(budgetPolicies);
await db.delete(joinRequests);
await db.delete(invites);
await db.delete(issueRecoveryActions);
await db.delete(issueRelations);
await db.delete(activityLog);
await db.delete(approvals);
await db.delete(issues);
await db.delete(assets);
await db.delete(documents);
await db.delete(projectWorkspaces);
await db.delete(projects);
await db.delete(agents);
await db.delete(companies);
});
afterAll(async () => {
await tempDb?.cleanup();
});
async function seedCompany(prefix = "ATN") {
const companyId = randomUUID();
const workerId = randomUUID();
const reviewerId = randomUUID();
const errorAgentId = randomUUID();
await db.insert(companies).values({
id: companyId,
name: `${prefix} Co`,
issuePrefix: prefix,
requireBoardApprovalForNewAgents: false,
});
await db.insert(agents).values([
{
id: workerId,
companyId,
name: "Worker",
role: "engineer",
status: "idle",
adapterType: "codex_local",
adapterConfig: {},
runtimeConfig: {},
permissions: {},
},
{
id: reviewerId,
companyId,
name: "Reviewer",
role: "qa",
status: "idle",
adapterType: "codex_local",
adapterConfig: {},
runtimeConfig: {},
permissions: {},
},
{
id: errorAgentId,
companyId,
name: "Broken Agent",
role: "engineer",
status: "error",
errorReason: "adapter config missing",
adapterType: "codex_local",
adapterConfig: {},
runtimeConfig: {},
permissions: {},
},
]);
return { companyId, workerId, reviewerId, errorAgentId, prefix };
}
async function insertIssue(input: {
companyId: string;
id?: string;
identifier: string;
title: string;
status: string;
priority?: string;
parentId?: string | null;
assigneeAgentId?: string | null;
assigneeUserId?: string | null;
originKind?: string;
originId?: string | null;
originFingerprint?: string;
projectId?: string | null;
projectWorkspaceId?: string | null;
executionState?: Record<string, unknown> | null;
updatedAt?: Date;
createdAt?: Date;
unblockDescriptor?: { owner: { userId: string } | "board"; action: string } | null;
blockedTransitionAt?: Date | null;
harnessKind?: string | null;
reviewPolicy?: "anyone" | "not_creator" | "human_only" | null;
}) {
const id = input.id ?? randomUUID();
await db.insert(issues).values({
id,
companyId: input.companyId,
identifier: input.identifier,
title: input.title,
status: input.status,
priority: input.priority ?? "medium",
reviewPolicy: input.reviewPolicy ?? null,
parentId: input.parentId ?? null,
projectId: input.projectId ?? null,
projectWorkspaceId: input.projectWorkspaceId ?? null,
assigneeAgentId: input.assigneeAgentId ?? null,
assigneeUserId: input.assigneeUserId ?? null,
originKind: input.originKind ?? "manual",
originId: input.originId ?? null,
originFingerprint: input.originFingerprint ?? "default",
executionState: input.executionState ?? null,
unblockDescriptor: input.unblockDescriptor ?? null,
blockedTransitionAt: input.blockedTransitionAt ?? null,
harnessKind: input.harnessKind ?? null,
createdAt: input.createdAt,
updatedAt: input.updatedAt,
});
return id;
}
function pendingUserExecutionState(userId = "board-user") {
return {
status: "pending",
currentStageId: null,
currentStageIndex: null,
currentStageType: "review",
currentParticipant: { type: "user", userId },
returnAssignee: null,
reviewRequest: null,
completedStageIds: [],
lastDecisionId: null,
lastDecisionOutcome: null,
monitor: null,
};
}
function pendingAgentExecutionState(agentId: string) {
return {
...pendingUserExecutionState(),
currentParticipant: { type: "agent", agentId },
};
}
it("excludes internal harness reviews from items, counts, and decision queues", async () => {
const { companyId, workerId } = await seedCompany("ATH");
const harnessIssueId = await insertIssue({
companyId,
identifier: "ATH-1",
title: "Internal harness review",
status: "in_review",
assigneeAgentId: workerId,
harnessKind: "skill_test",
});
const queueId = randomUUID();
await db.insert(decisionQueues).values({
id: queueId,
companyId,
key: "internal-review",
title: "Internal review",
createdByType: "user",
createdByUserId: "board-user",
});
await db.insert(decisionQueueItems).values({
companyId,
queueId,
sourceKind: "review",
sourceId: harnessIssueId,
addedByType: "user",
addedByUserId: "board-user",
});
const feed = await attentionService(db).list(companyId, { userId: "board-user" });
expect(feed.items.some((item) => item.subject.id === harnessIssueId)).toBe(false);
expect(feed.countsBySourceKind.review ?? 0).toBe(0);
expect(feed.items.flatMap((item) => item.queues).some((queue) => queue.key === "internal-review")).toBe(false);
});
it("returns ranked decision-only items for every active source and excludes non-human or transient rows", async () => {
const { companyId, workerId, reviewerId } = await seedCompany("ATN");
const baseTime = new Date("2026-07-09T12:00:00.000Z");
const interactionIssueId = await insertIssue({
companyId,
identifier: "ATN-1",
title: "Needs interaction",
status: "in_progress",
assigneeAgentId: workerId,
updatedAt: baseTime,
});
const recoverySourceIssueId = await insertIssue({
companyId,
identifier: "ATN-2",
title: "Needs recovery",
status: "in_progress",
assigneeAgentId: workerId,
updatedAt: baseTime,
});
const agentRecoverySourceIssueId = await insertIssue({
companyId,
identifier: "ATN-21",
title: "Agent-owned recovery source",
status: "in_progress",
assigneeAgentId: workerId,
updatedAt: baseTime,
});
const productivitySourceIssueId = await insertIssue({
companyId,
identifier: "ATN-3",
title: "Needs productivity review source",
status: "in_progress",
assigneeAgentId: workerId,
updatedAt: baseTime,
});
const agentProductivitySourceIssueId = await insertIssue({
companyId,
identifier: "ATN-31",
title: "Agent productivity review source",
status: "in_progress",
assigneeAgentId: workerId,
updatedAt: baseTime,
});
const blockerParentId = await insertIssue({
companyId,
identifier: "ATN-4",
title: "Blocked parent",
status: "blocked",
blockedTransitionAt: new Date(ROUTABLE_BLOCKED_ROLLOUT_AT.getTime() + 1),
updatedAt: new Date("2026-07-09T12:04:00.000Z"),
});
const blockerLeafId = await insertIssue({
companyId,
identifier: "ATN-5",
title: "Stalled review blocker",
status: "in_review",
assigneeAgentId: reviewerId,
// PAP-16506: the /decisions review card states who may give the verdict,
// so the subject has to carry the issue's opt-in constraint.
reviewPolicy: "human_only",
updatedAt: new Date("2026-07-09T12:05:00.000Z"),
});
await db.insert(issueRelations).values({
companyId,
issueId: blockerLeafId,
relatedIssueId: blockerParentId,
type: "blocks",
});
const reviewUserIssueId = await insertIssue({
companyId,
identifier: "ATN-6",
title: "Human review",
status: "in_review",
executionState: pendingUserExecutionState(),
updatedAt: new Date("2026-07-09T12:06:00.000Z"),
});
await insertIssue({
companyId,
identifier: "ATN-7",
title: "Agent review excluded",
status: "in_review",
executionState: pendingAgentExecutionState(reviewerId),
updatedAt: new Date("2026-07-09T12:07:00.000Z"),
});
const pendingApprovalId = randomUUID();
await db.insert(approvals).values([
{
id: pendingApprovalId,
companyId,
type: "hire_agent",
status: "pending",
payload: { title: "Hire Designer" },
createdAt: new Date("2026-07-09T12:01:00.000Z"),
updatedAt: new Date("2026-07-09T12:01:00.000Z"),
},
{
id: randomUUID(),
companyId,
type: "hire_agent",
status: "revision_requested",
payload: { title: "Revision requested" },
createdAt: new Date("2026-07-09T12:02:00.000Z"),
updatedAt: new Date("2026-07-09T12:02:00.000Z"),
},
]);
await db.insert(issueThreadInteractions).values([
{
id: randomUUID(),
companyId,
issueId: interactionIssueId,
kind: "ask_user_questions",
status: "pending",
continuationPolicy: "wake_assignee",
title: "Pick a launch date",
payload: { version: 1, questions: [] },
createdAt: new Date("2026-07-09T12:03:00.000Z"),
updatedAt: new Date("2026-07-09T12:03:00.000Z"),
},
{
id: randomUUID(),
companyId,
issueId: interactionIssueId,
kind: "ask_user_questions",
status: "pending",
continuationPolicy: "wake_assignee",
addresseeAgentId: reviewerId,
title: "Ask the reviewer privately",
payload: { version: 1, questions: [] },
createdAt: new Date("2026-07-09T12:03:15.000Z"),
updatedAt: new Date("2026-07-09T12:03:15.000Z"),
},
{
id: randomUUID(),
companyId,
issueId: interactionIssueId,
kind: "request_confirmation",
status: "accepted",
continuationPolicy: "wake_assignee",
title: "Already accepted",
payload: { version: 1, prompt: "Already done" },
createdAt: new Date("2026-07-09T12:03:30.000Z"),
updatedAt: new Date("2026-07-09T12:03:30.000Z"),
},
]);
const inviteId = randomUUID();
await db.insert(invites).values({
id: inviteId,
companyId,
tokenHash: `hash-${inviteId}`,
allowedJoinTypes: "both",
expiresAt: new Date("2026-07-10T00:00:00.000Z"),
});
await db.insert(joinRequests).values({
id: randomUUID(),
inviteId,
companyId,
requestType: "human",
status: "pending_approval",
requestIp: "127.0.0.1",
requestEmailSnapshot: "new@paperclip.test",
createdAt: new Date("2026-07-09T12:04:00.000Z"),
updatedAt: new Date("2026-07-09T12:04:00.000Z"),
});
await db.insert(issueRecoveryActions).values([
{
id: randomUUID(),
companyId,
sourceIssueId: recoverySourceIssueId,
kind: "missing_disposition",
status: "escalated",
ownerType: "board",
ownerAgentId: null,
ownerUserId: null,
cause: "missing_disposition",
fingerprint: "human-recovery",
evidence: {},
nextAction: "Choose the final disposition.",
createdAt: new Date("2026-07-09T12:05:00.000Z"),
updatedAt: new Date("2026-07-09T12:05:00.000Z"),
},
{
id: randomUUID(),
companyId,
sourceIssueId: agentRecoverySourceIssueId,
kind: "stranded_assigned_issue",
status: "active",
ownerType: "agent",
ownerAgentId: workerId,
ownerUserId: null,
cause: "stranded",
fingerprint: "agent-recovery",
evidence: {},
nextAction: "Agent should self-heal.",
createdAt: new Date("2026-07-09T12:05:30.000Z"),
updatedAt: new Date("2026-07-09T12:05:30.000Z"),
},
]);
await insertIssue({
companyId,
identifier: "ATN-8",
title: "Human productivity review",
status: "todo",
priority: "high",
parentId: productivitySourceIssueId,
assigneeUserId: "board-user",
originKind: "issue_productivity_review",
originId: productivitySourceIssueId,
originFingerprint: `productivity-review:${productivitySourceIssueId}`,
updatedAt: new Date("2026-07-09T12:08:00.000Z"),
});
await insertIssue({
companyId,
identifier: "ATN-9",
title: "Agent productivity review excluded",
status: "todo",
priority: "high",
parentId: agentProductivitySourceIssueId,
assigneeAgentId: workerId,
originKind: "issue_productivity_review",
originId: agentProductivitySourceIssueId,
originFingerprint: `productivity-review-agent:${agentProductivitySourceIssueId}`,
updatedAt: new Date("2026-07-09T12:08:30.000Z"),
});
const exhaustedRunId = randomUUID();
const transientRunId = randomUUID();
await db.insert(heartbeatRuns).values([
{
id: exhaustedRunId,
companyId,
agentId: workerId,
invocationSource: "automation",
status: "failed",
error: "adapter failed",
errorCode: "adapter_failed",
contextSnapshot: { issueId: reviewUserIssueId },
scheduledRetryAttempt: 4,
scheduledRetryReason: "transient_failure",
createdAt: new Date("2026-07-09T12:09:00.000Z"),
updatedAt: new Date("2026-07-09T12:09:00.000Z"),
finishedAt: new Date("2026-07-09T12:09:00.000Z"),
},
{
id: transientRunId,
companyId,
agentId: reviewerId,
invocationSource: "automation",
status: "failed",
error: "will retry",
errorCode: "provider_quota",
contextSnapshot: { issueId: interactionIssueId },
createdAt: new Date("2026-07-09T12:09:30.000Z"),
updatedAt: new Date("2026-07-09T12:09:30.000Z"),
finishedAt: new Date("2026-07-09T12:09:30.000Z"),
},
]);
await db.insert(heartbeatRunEvents).values({
companyId,
runId: exhaustedRunId,
agentId: workerId,
seq: 1,
eventType: "lifecycle",
message: "Bounded retry exhausted after 4 scheduled attempts; no further automatic retry will be queued",
payload: { retryReason: "transient_failure", maxAttempts: 4 },
createdAt: new Date("2026-07-09T12:09:01.000Z"),
});
const softPolicy85Id = randomUUID();
const softPolicy84Id = randomUUID();
const hardPolicyId = randomUUID();
await db.insert(budgetPolicies).values([
{
id: softPolicy85Id,
companyId,
scopeType: "company",
scopeId: companyId,
metric: "billed_cents",
windowKind: "calendar_month_utc",
amount: 100,
},
{
id: softPolicy84Id,
companyId,
scopeType: "company",
scopeId: companyId,
metric: "billed_cents",
windowKind: "lifetime",
amount: 100,
},
{
id: hardPolicyId,
companyId,
scopeType: "agent",
scopeId: workerId,
metric: "billed_cents",
windowKind: "calendar_month_utc",
amount: 100,
},
]);
await db.insert(budgetIncidents).values([
{
companyId,
policyId: softPolicy85Id,
scopeType: "company",
scopeId: companyId,
metric: "billed_cents",
windowKind: "calendar_month_utc",
windowStart: new Date("2026-07-01T00:00:00.000Z"),
windowEnd: new Date("2026-08-01T00:00:00.000Z"),
thresholdType: "soft",
amountLimit: 100,
amountObserved: 85,
status: "open",
createdAt: new Date("2026-07-09T12:10:00.000Z"),
updatedAt: new Date("2026-07-09T12:10:00.000Z"),
},
{
companyId,
policyId: softPolicy84Id,
scopeType: "company",
scopeId: companyId,
metric: "billed_cents",
windowKind: "lifetime",
windowStart: new Date("1970-01-01T00:00:00.000Z"),
windowEnd: new Date("9999-01-01T00:00:00.000Z"),
thresholdType: "soft",
amountLimit: 100,
amountObserved: 84,
status: "open",
createdAt: new Date("2026-07-09T12:10:30.000Z"),
updatedAt: new Date("2026-07-09T12:10:30.000Z"),
},
{
companyId,
policyId: hardPolicyId,
scopeType: "agent",
scopeId: workerId,
metric: "billed_cents",
windowKind: "calendar_month_utc",
windowStart: new Date("2026-07-01T00:00:00.000Z"),
windowEnd: new Date("2026-08-01T00:00:00.000Z"),
thresholdType: "hard",
amountLimit: 100,
amountObserved: 100,
status: "open",
createdAt: new Date("2026-07-09T12:11:00.000Z"),
updatedAt: new Date("2026-07-09T12:11:00.000Z"),
},
]);
const feed = await attentionService(db).list(companyId, { userId: "board-user" });
expect(feed.totalCount).toBe(11);
expect(feed.countsBySourceKind).toMatchObject({
approval: 1,
issue_thread_interaction: 1,
join_request: 1,
recovery_action: 1,
productivity_review: 0,
blocker_attention: 1,
review: 2,
failed_run: 1,
budget_alert: 2,
agent_error_alert: 1,
});
expect(feed.items.map((item) => item.sourceKind)).toEqual(expect.arrayContaining([
"approval",
"issue_thread_interaction",
"join_request",
"recovery_action",
"blocker_attention",
"review",
"failed_run",
"budget_alert",
"agent_error_alert",
]));
for (const item of feed.items) {
expect(item.dedupKey).toBeTruthy();
expect(item.dismissalKey).toBe(`attention:${item.dedupKey}`);
expect(item.whyNow).toBeTruthy();
expect(item.entryRule).toBeTruthy();
expect(item.exitRule).toBeTruthy();
expect(item.decisionVerbs.length).toBeGreaterThan(0);
expect(item.rank).toBeGreaterThan(0);
}
expect(feed.items.some((item) => item.subject.title === "Revision requested")).toBe(false);
expect(feed.items.some((item) => item.sourceKind === "productivity_review")).toBe(false);
expect(feed.items.some((item) => item.subject.title === "Agent productivity review excluded")).toBe(false);
const legacyReviews = await db.select().from(issues).where(eq(issues.originKind, "issue_productivity_review"));
expect(legacyReviews).toHaveLength(2);
expect(legacyReviews).toEqual(expect.arrayContaining([
expect.objectContaining({ title: "Human productivity review", status: "todo", assigneeUserId: "board-user", parentId: productivitySourceIssueId }),
expect.objectContaining({ title: "Agent productivity review excluded", status: "todo", assigneeAgentId: workerId, parentId: agentProductivitySourceIssueId }),
]));
expect(feed.items.some((item) => item.subject.title === "Agent review excluded")).toBe(false);
expect(feed.items.some((item) =>
item.sourceKind === "failed_run" && item.subject.metadata?.errorCode === "provider_quota"
)).toBe(false);
expect(feed.items.find((item) => item.sourceKind === "approval")?.detail).toMatchObject({
kind: "approval",
approvalType: "hire_agent",
summaryExcerpt: "Hire Designer",
});
expect(feed.items.find((item) => item.sourceKind === "issue_thread_interaction")?.detail).toMatchObject({
kind: "questions",
questionCount: 0,
});
expect(feed.items.find((item) => item.sourceKind === "blocker_attention")?.detail).toMatchObject({
kind: "blocker",
blockingIssue: null,
blockedTaskCount: 1,
});
expect(feed.items.find((item) => item.sourceKind === "blocker_attention")?.subject.id).toBe(blockerLeafId);
expect(feed.items.find((item) =>
item.sourceKind === "review" && item.subject.title === "Stalled review blocker"
)).toMatchObject({
whyNow: expect.stringContaining("without a maintained"),
// A stalled review resolves in-row on the /decisions card (PAP-16080 §4.4).
inlineResolvable: true,
subject: expect.objectContaining({
metadata: expect.objectContaining({
reviewAttentionState: "stalled",
reviewPolicy: "human_only",
}),
}),
decisionVerbs: expect.arrayContaining([
expect.objectContaining({ id: "choose_review_path", label: "Choose review path" }),
]),
});
expect(feed.items.find((item) => item.sourceKind === "failed_run")?.detail).toMatchObject({
kind: "failed_run",
agentName: "Worker",
failureReasonExcerpt: "adapter failed",
});
expect(feed.items.find((item) =>
item.sourceKind === "budget_alert" && item.detail?.kind === "budget" && item.detail.observedPercent === 100
)).toBeTruthy();
expect(feed.items.find((item) => item.sourceKind === "agent_error_alert")?.detail).toMatchObject({
kind: "agent_error",
agentName: "Broken Agent",
failureReasonExcerpt: "adapter config missing",
});
});
it("returns addressed interactions to board attention after addressee pause or termination", async () => {
const { companyId, reviewerId } = await seedCompany("ATF");
const pausedReviewerId = randomUUID();
const terminatedReviewerId = randomUUID();
await db.insert(agents).values([
{
id: pausedReviewerId,
companyId,
name: "Paused Reviewer",
role: "qa",
status: "active",
adapterType: "codex_local",
adapterConfig: {},
runtimeConfig: {},
permissions: {},
},
{
id: terminatedReviewerId,
companyId,
name: "Terminated Reviewer",
role: "qa",
status: "active",
adapterType: "codex_local",
adapterConfig: {},
runtimeConfig: {},
permissions: {},
},
]);
const issueId = await insertIssue({
companyId,
identifier: "ATF-1",
title: "Needs a decision",
status: "in_progress",
});
await db.insert(issueThreadInteractions).values([
{
id: randomUUID(),
companyId,
issueId,
kind: "ask_user_questions",
status: "pending",
continuationPolicy: "wake_assignee",
title: "Board question",
payload: { version: 1, questions: [] },
},
{
id: randomUUID(),
companyId,
issueId,
kind: "ask_user_questions",
status: "pending",
continuationPolicy: "wake_assignee",
addresseeAgentId: reviewerId,
title: "Active reviewer question",
payload: { version: 1, questions: [] },
},
{
id: randomUUID(),
companyId,
issueId,
kind: "ask_user_questions",
status: "pending",
continuationPolicy: "wake_assignee",
addresseeAgentId: pausedReviewerId,
title: "Paused reviewer question",
payload: { version: 1, questions: [] },
},
{
id: randomUUID(),
companyId,
issueId,
kind: "ask_user_questions",
status: "pending",
continuationPolicy: "wake_assignee",
addresseeAgentId: terminatedReviewerId,
title: "Terminated reviewer question",
payload: { version: 1, questions: [] },
},
]);
await agentService(db).pause(pausedReviewerId);
await agentService(db).terminate(terminatedReviewerId);
const feed = await attentionService(db).list(companyId, { userId: "board-user" });
const interactionTitles = feed.items
.filter((item) => item.sourceKind === "issue_thread_interaction")
.map((item) => item.subject.title);
expect(interactionTitles).toEqual(expect.arrayContaining([
"Board question",
"Paused reviewer question",
"Terminated reviewer question",
]));
expect(interactionTitles).not.toContain("Active reviewer question");
});
// PAP-17287: a collapsed queue row offers Accept/Reject before anything fetches
// the interaction, so the audience the resolution routes will enforce has to
// ship with the feed item.
it("ships the effective resolver audience with each interaction row", async () => {
const { companyId, workerId, reviewerId } = await seedCompany("ARA");
const issueId = await insertIssue({
companyId,
identifier: "ARA-1",
title: "Needs a decision",
status: "in_progress",
});
await db.insert(issueThreadInteractions).values([
{
id: randomUUID(),
companyId,
issueId,
kind: "ask_user_questions",
status: "pending",
title: "Open question",
createdByAgentId: workerId,
payload: { version: 1, questions: [] },
},
{
id: randomUUID(),
companyId,
issueId,
kind: "ask_user_questions",
status: "pending",
title: "Creator-excluded question",
createdByAgentId: workerId,
requestedResolverPolicy: "not_creator",
effectiveResolverPolicy: "not_creator",
resolverPolicyProvenance: "explicit",
payload: { version: 1, questions: [] },
},
{
id: randomUUID(),
companyId,
issueId,
kind: "ask_user_questions",
status: "pending",
title: "Capped question",
createdByAgentId: workerId,
requestedResolverPolicy: "anyone",
effectiveResolverPolicy: "human_only",
effectiveResolverPolicySource: "company_cap",
payload: { version: 1, questions: [] },
},
]);
// An addressed card only reaches board attention once its addressee cannot
// act, which is exactly when naming the addressee matters most.
await db.insert(issueThreadInteractions).values({
id: randomUUID(),
companyId,
issueId,
kind: "ask_user_questions",
status: "pending",
title: "Addressed question",
createdByAgentId: workerId,
addresseeAgentId: reviewerId,
payload: { version: 1, questions: [] },
});
await db.insert(issueThreadInteractions).values({
id: randomUUID(),
companyId,
issueId,
kind: "ask_user_questions",
status: "pending",
title: "User-addressed question",
createdByAgentId: workerId,
addresseeUserId: "board-user",
requestedResolverPolicy: "human_only",
effectiveResolverPolicy: "human_only",
payload: { version: 1, questions: [] },
});
await agentService(db).pause(reviewerId);
const feed = await attentionService(db).list(companyId, { userId: "board-user" });
const otherUserFeed = await attentionService(db).list(companyId, { userId: "other-user" });
const audienceByTitle = new Map(feed.items
.filter((item) => item.sourceKind === "issue_thread_interaction")
.map((item) => [item.subject.title, item.resolverAudience]));
expect(audienceByTitle.get("Open question")).toMatchObject({
requestedResolverPolicy: "anyone",
effectiveResolverPolicy: "anyone",
effectiveResolverPolicySource: "requested",
addresseeAgentId: null,
addresseeName: null,
});
expect(audienceByTitle.get("Creator-excluded question")).toMatchObject({
effectiveResolverPolicy: "not_creator",
createdByAgentId: workerId,
createdByAgentName: "Worker",
});
expect(audienceByTitle.get("Capped question")).toMatchObject({
requestedResolverPolicy: "anyone",
effectiveResolverPolicy: "human_only",
effectiveResolverPolicySource: "company_cap",
});
expect(audienceByTitle.get("Addressed question")).toMatchObject({
addresseeAgentId: reviewerId,
addresseeName: "Reviewer",
});
expect(audienceByTitle.get("User-addressed question")).toMatchObject({
addresseeUserId: "board-user",
effectiveResolverPolicy: "human_only",
});
expect(otherUserFeed.items.some((item) => item.subject.title === "User-addressed question")).toBe(false);
// Non-interaction rows carry no resolver policy at all.
expect(feed.items.find((item) => item.sourceKind !== "issue_thread_interaction")?.resolverAudience)
.toBeNull();
});
it("suppresses failed-run attention after a newer run for the same issue", async () => {
const { companyId, workerId } = await seedCompany("ATN");
const issueId = await insertIssue({
companyId,
identifier: "ATN-1",
title: "Recoverable task",
status: "in_progress",
});
const failedRunId = randomUUID();
const failedAt = new Date("2026-07-09T12:00:00.000Z");
await db.insert(heartbeatRuns).values([
{
id: failedRunId,
companyId,
agentId: workerId,
invocationSource: "automation",
status: "failed",
error: "adapter failed",
contextSnapshot: { issueId },
createdAt: failedAt,
updatedAt: failedAt,
finishedAt: failedAt,
},
{
id: randomUUID(),
companyId,
agentId: workerId,
invocationSource: "automation",
status: "succeeded",
contextSnapshot: { issueId },
createdAt: new Date("2026-07-09T12:01:00.000Z"),
updatedAt: new Date("2026-07-09T12:01:00.000Z"),
finishedAt: new Date("2026-07-09T12:01:00.000Z"),
},
]);
await db.insert(heartbeatRunEvents).values({
companyId,
runId: failedRunId,
agentId: workerId,
seq: 1,
eventType: "lifecycle",
message: "Bounded retry exhausted after 4 scheduled attempts; no further automatic retry will be queued",
createdAt: new Date("2026-07-09T12:00:01.000Z"),
});
const feed = await attentionService(db).list(companyId, { userId: "board-user" });
expect(feed.items.filter((item) => item.sourceKind === "failed_run")).toEqual([]);
});
it("enriches interaction details with project, workspace, plan metadata, and images", async () => {
const { companyId, workerId } = await seedCompany("ATE");
const projectId = randomUUID();
const workspaceId = randomUUID();
const issueId = randomUUID();
const planDocumentId = randomUUID();
const planRevisionId = randomUUID();
const imageAssetIds = [randomUUID(), randomUUID(), randomUUID(), randomUUID()];
await db.insert(projects).values({
id: projectId,
companyId,
name: "Attention Project",
status: "in_progress",
color: "#0f766e",
icon: "rocket",
});
await db.insert(projectWorkspaces).values({
id: workspaceId,
companyId,
projectId,
name: "Preview workspace",
sourceType: "local_path",
isPrimary: true,
});
await insertIssue({
id: issueId,
companyId,
identifier: "ATE-1",
title: "Approve launch plan",
status: "in_progress",
assigneeAgentId: workerId,
projectId,
projectWorkspaceId: workspaceId,
updatedAt: new Date("2026-07-09T12:00:00.000Z"),
});
await db.insert(documents).values({
id: planDocumentId,
companyId,
title: "Launch Plan",
format: "markdown",
latestBody: "# Summary\n\nThis plan explains the launch checklist, rollout owner, QA gates, and risk controls for the homepage release.",
latestRevisionId: planRevisionId,
latestRevisionNumber: 2,
});
await db.insert(issueDocuments).values({
companyId,
issueId,
documentId: planDocumentId,
key: "plan",
});
await db.insert(assets).values([
{ id: imageAssetIds[0], companyId, provider: "local_disk", objectKey: "img-1", contentType: "image/png", byteSize: 10, sha256: "a".repeat(64), originalFilename: "one.png" },
{ id: imageAssetIds[1], companyId, provider: "local_disk", objectKey: "img-2", contentType: "image/jpeg", byteSize: 10, sha256: "b".repeat(64), originalFilename: "two.jpg" },
{ id: imageAssetIds[2], companyId, provider: "local_disk", objectKey: "img-3", contentType: "image/gif", byteSize: 10, sha256: "c".repeat(64), originalFilename: "three.gif" },
{ id: imageAssetIds[3], companyId, provider: "local_disk", objectKey: "img-4", contentType: "image/png", byteSize: 10, sha256: "d".repeat(64), originalFilename: "four.png" },
]);
await db.insert(issueAttachments).values(imageAssetIds.map((assetId, index) => ({
companyId,
issueId,
assetId,
createdAt: new Date(`2026-07-09T12:0${index}:30.000Z`),
updatedAt: new Date(`2026-07-09T12:0${index}:30.000Z`),
})));
const planInteractionId = randomUUID();
const questionsInteractionId = randomUUID();
const tasksInteractionId = randomUUID();
const checkboxInteractionId = randomUUID();
const verdictInteractionId = randomUUID();
await db.insert(issueThreadInteractions).values([
{
id: planInteractionId,
companyId,
issueId,
kind: "request_confirmation",
status: "pending",
continuationPolicy: "wake_assignee",
title: "Approve the plan",
payload: {
version: 1,
prompt: "Approve plan?",
acceptLabel: "Approve plan",
rejectLabel: "Request changes",
target: { type: "issue_document", issueId, key: "plan", revisionId: planRevisionId },
},
createdAt: new Date("2026-07-09T12:01:00.000Z"),
updatedAt: new Date("2026-07-09T12:01:00.000Z"),
},
{
id: questionsInteractionId,
companyId,
issueId,
kind: "ask_user_questions",
status: "pending",
continuationPolicy: "wake_assignee",
title: "Questions",
payload: {
version: 1,
questions: [
{ id: "q1", prompt: "Which auth provider should we use?", selectionMode: "single", options: [] },
{ id: "q2", prompt: "Should we add a fallback?", selectionMode: "single", options: [] },
],
},
createdAt: new Date("2026-07-09T12:02:00.000Z"),
updatedAt: new Date("2026-07-09T12:02:00.000Z"),
},
{
id: tasksInteractionId,
companyId,
issueId,
kind: "suggest_tasks",
status: "pending",
continuationPolicy: "wake_assignee",
title: "Tasks",
payload: { version: 1, tasks: [{ clientKey: "t1", title: "Build API" }, { clientKey: "t2", title: "Wire UI" }] },
createdAt: new Date("2026-07-09T12:03:00.000Z"),
updatedAt: new Date("2026-07-09T12:03:00.000Z"),
},
{
id: checkboxInteractionId,
companyId,
issueId,
kind: "request_checkbox_confirmation",
status: "pending",
continuationPolicy: "wake_assignee",
title: "Checkbox",
payload: { version: 1, prompt: "Select rollout regions", options: [{ id: "us", label: "US" }, { id: "eu", label: "EU" }] },
createdAt: new Date("2026-07-09T12:04:00.000Z"),
updatedAt: new Date("2026-07-09T12:04:00.000Z"),
},
{
id: verdictInteractionId,
companyId,
issueId,
kind: "request_item_verdicts",
status: "pending",
continuationPolicy: "wake_assignee",
title: "Verdicts",
payload: { version: 1, prompt: "Approve these screenshots", items: [{ id: "one", label: "One" }, { id: "two", label: "Two" }] },
createdAt: new Date("2026-07-09T12:05:00.000Z"),
updatedAt: new Date("2026-07-09T12:05:00.000Z"),
},
]);
const feed = await attentionService(db).list(companyId, { userId: "board-user" });
const interactionItems = feed.items.filter((item) => item.sourceKind === "issue_thread_interaction");
const detailsByKind = new Map(interactionItems.map((item) => [item.detail?.kind, item]));
const planItem = detailsByKind.get("plan_approval");
expect(planItem?.subject.title).toBe("Plan approval - Approve launch plan");
expect(planItem?.subject.metadata).toMatchObject({ isPlanTarget: true, targetDocumentKey: "plan" });
expect(planItem?.decisionVerbs).toEqual([
expect.objectContaining({ id: "accept", label: "Approve plan" }),
expect.objectContaining({ id: "reject", label: "Request changes" }),
]);
expect(planItem?.project).toMatchObject({
id: projectId,
name: "Attention Project",
color: "#0f766e",
icon: "rocket",
});
expect(planItem?.project?.urlKey).toEqual(expect.any(String));
expect(planItem?.workspace).toEqual({ id: workspaceId, name: "Preview workspace" });
expect(planItem?.detail).toMatchObject({
kind: "plan_approval",
issueTitle: "Approve launch plan",
planTitle: "Launch Plan",
summaryExcerpt: expect.stringContaining("launch checklist"),
images: imageAssetIds.slice(0, 3).map((assetId) => ({ assetId, alt: expect.any(String) })),
});
expect(detailsByKind.get("questions")?.detail).toMatchObject({
kind: "questions",
questionCount: 2,
firstQuestionText: "Which auth provider should we use?",
});
expect(detailsByKind.get("suggested_tasks")?.detail).toMatchObject({
kind: "suggested_tasks",
taskCount: 2,
firstTaskTitle: "Build API",
});
expect(detailsByKind.get("checkbox_confirmation")?.detail).toMatchObject({
kind: "checkbox_confirmation",
optionCount: 2,
promptExcerpt: "Select rollout regions",
});
expect(detailsByKind.get("item_verdicts")?.detail).toMatchObject({
kind: "item_verdicts",
itemCount: 2,
promptExcerpt: "Approve these screenshots",
});
});
it("shows only the newest pending confirmation per issue and kind", async () => {
const { companyId, workerId, reviewerId } = await seedCompany("ATC");
const issueId = await insertIssue({
companyId,
identifier: "ATC-1",
title: "Repeated sign-offs",
status: "in_review",
assigneeAgentId: workerId,
});
const olderConfirmationId = randomUUID();
const newerConfirmationId = randomUUID();
const checkboxId = randomUUID();
await db.insert(issueThreadInteractions).values([
{
id: olderConfirmationId,
companyId,
issueId,
kind: "request_confirmation",
status: "pending",
continuationPolicy: "wake_assignee",
createdByAgentId: workerId,
title: "Approve V1",
payload: { version: 1, prompt: "Approve V1?" },
createdAt: new Date("2026-07-09T12:00:00.000Z"),
updatedAt: new Date("2026-07-09T12:10:00.000Z"),
},
{
id: newerConfirmationId,
companyId,
issueId,
kind: "request_confirmation",
status: "pending",
continuationPolicy: "wake_assignee",
createdByAgentId: reviewerId,
title: "Approve V2",
payload: { version: 1, prompt: "Approve V2?" },
createdAt: new Date("2026-07-09T12:05:00.000Z"),
updatedAt: new Date("2026-07-09T12:05:00.000Z"),
},
{
id: checkboxId,
companyId,
issueId,
kind: "request_checkbox_confirmation",
status: "pending",
continuationPolicy: "wake_assignee",
createdByAgentId: workerId,
title: "Select rollout",
payload: { version: 1, prompt: "Select rollout", options: [{ id: "one", label: "One" }] },
createdAt: new Date("2026-07-09T12:01:00.000Z"),
updatedAt: new Date("2026-07-09T12:01:00.000Z"),
},
]);
const feed = await attentionService(db).list(companyId, { userId: "board-user" });
const interactionIds = feed.items
.filter((item) => item.sourceKind === "issue_thread_interaction")
.map((item) => item.subject.id);
expect(interactionIds).toContain(newerConfirmationId);
expect(interactionIds).toContain(checkboxId);
expect(interactionIds).not.toContain(olderConfirmationId);
});
it("uses inbox_dismissals with attention-prefixed dedup keys and resurfaces newer activity", async () => {
const { companyId } = await seedCompany("ATD");
const approvalId = randomUUID();
await db.insert(approvals).values({
id: approvalId,
companyId,
type: "hire_agent",
status: "pending",
payload: { title: "Hire Writer" },
createdAt: new Date("2026-07-09T12:00:00.000Z"),
updatedAt: new Date("2026-07-09T12:00:00.000Z"),
});
await db.insert(inboxDismissals).values({
companyId,
userId: "board-user",
itemKey: `attention:approval:${approvalId}`,
dismissedAt: new Date("2026-07-09T13:00:00.000Z"),
});
await expect(attentionService(db).list(companyId, { userId: "board-user" }))
.resolves.toMatchObject({ totalCount: 1 }); // agent_error_alert from seed
const includeDismissedFeed = await attentionService(db).list(companyId, { userId: "board-user", includeDismissed: true });
expect(includeDismissedFeed.totalCount).toBe(2);
expect(includeDismissedFeed.items.find((item) => item.dedupKey === `approval:${approvalId}`)?.dismissal)
.toMatchObject({ kind: "dismiss", isActive: true, snoozedUntil: null });
await db
.update(approvals)
.set({ updatedAt: new Date("2026-07-09T14:00:00.000Z") })
.where(eq(approvals.id, approvalId));
const feed = await attentionService(db).list(companyId, { userId: "board-user" });
expect(feed.items.some((item) => item.dedupKey === `approval:${approvalId}`)).toBe(true);
});
it("delivers a structured human unblock descriptor once per blocked transition", async () => {
const { companyId } = await seedCompany("ATU");
const transitionAt = new Date("2026-07-23T18:30:00.000Z");
const issueId = await insertIssue({
companyId,
identifier: "ATU-1",
title: "Needs board action",
status: "blocked",
unblockDescriptor: { owner: "board", action: "Approve the exception" },
blockedTransitionAt: transitionAt,
});
const feed = await attentionService(db).list(companyId, { userId: "board-user" });
const items = feed.items.filter((item) => item.dedupKey === `blocked-owner:${issueId}:${transitionAt.toISOString()}`);
expect(items).toHaveLength(1);
expect(items[0]).toMatchObject({ sourceKind: "blocker_attention", whyNow: "Approve the exception" });
});
it("keeps legacy blocker attention visible for pre-rollout blocked issues", async () => {
const { companyId } = await seedCompany("ATP");
const issueId = await insertIssue({
companyId,
identifier: "ATP-1",
title: "Blocked before rollout",
status: "blocked",
blockedTransitionAt: new Date(ROUTABLE_BLOCKED_ROLLOUT_AT.getTime() - 1),
});
const feed = await attentionService(db).list(companyId, { userId: "board-user" });
expect(feed.items.some((item) => item.dedupKey === `blocker:${issueId}`)).toBe(true);
});
// Regression: both blocker_attention call sites fell back to the blocked
// task's own identity when no `blocks` relation was loaded, so every such row
// claimed the task was blocked by itself ("PAP-23 — Blocked by PAP-23").
it("reports no blocking task rather than a self-reference when the blocker is unknown", async () => {
const { companyId } = await seedCompany("ATV");
const issueId = await insertIssue({
companyId,
identifier: "ATV-1",
title: "Blocked with no relation",
status: "blocked",
blockedTransitionAt: new Date(ROUTABLE_BLOCKED_ROLLOUT_AT.getTime() - 1),
});
const feed = await attentionService(db).list(companyId, { userId: "board-user" });
const row = feed.items.find((item) => item.dedupKey === `blocker:${issueId}`);
expect(row).toBeTruthy();
expect(row?.detail).toMatchObject({ kind: "blocker", blockingIssue: null });
expect(row?.dismissalKey).toBe(`attention:blocker:${issueId}`);
});
it("suppresses a blocked dependency row while its blocker is actively progressing", async () => {
const { companyId } = await seedCompany("ATW");
const blockedId = await insertIssue({
companyId,
identifier: "ATW-1",
title: "Blocked parent",
status: "blocked",
blockedTransitionAt: new Date(ROUTABLE_BLOCKED_ROLLOUT_AT.getTime() - 1),
});
const blockerId = await insertIssue({
companyId,
identifier: "ATW-2",
title: "The actual blocker",
status: "in_progress",
});
await db.insert(issueRelations).values({
companyId,
issueId: blockerId,
relatedIssueId: blockedId,
type: "blocks",
});
const feed = await attentionService(db).list(companyId, { userId: "board-user" });
expect(feed.items.some((item) => item.sourceKind === "blocker_attention")).toBe(false);
});
it("suppresses a mixed blocker tree when any blocker is live", async () => {
const { companyId } = await seedCompany("ATL");
const blockedId = await insertIssue({
companyId,
identifier: "ATL-1",
title: "Blocked rollout",
status: "blocked",
});
const blockerIds = await Promise.all([
insertIssue({ companyId, identifier: "ATL-2", title: "Live phase one", status: "in_progress" }),
insertIssue({ companyId, identifier: "ATL-3", title: "Live phase two", status: "in_progress" }),
insertIssue({ companyId, identifier: "ATL-4", title: "Live phase three", status: "in_progress" }),
insertIssue({ companyId, identifier: "ATL-5", title: "Stopped phase", status: "todo" }),
]);
await db.insert(issueRelations).values(blockerIds.map((issueId) => ({
companyId,
issueId,
relatedIssueId: blockedId,
type: "blocks" as const,
})));
const feed = await attentionService(db).list(companyId, { userId: "board-user" });
expect(feed.items.filter((item) => item.sourceKind === "blocker_attention")).toEqual([]);
});
it("emits one terminal-blocker row with a cycle-safe transitive blocked-work count", async () => {
const { companyId } = await seedCompany("ATC");
const terminalId = await insertIssue({
companyId,
identifier: "ATC-1",
title: "Choose migration owner",
status: "todo",
});
const blockedId = await insertIssue({
companyId,
identifier: "ATC-2",
title: "Blocked migration",
status: "blocked",
});
await insertIssue({
companyId,
identifier: "ATC-3",
title: "Open migration child",
status: "todo",
parentId: blockedId,
});
const transitiveId = await insertIssue({
companyId,
identifier: "ATC-4",
title: "Transitively blocked follow-up",
status: "todo",
});
await insertIssue({
companyId,
identifier: "ATC-5",
title: "Closed child",
status: "done",
parentId: blockedId,
});
await db.insert(issueRelations).values([
{ companyId, issueId: terminalId, relatedIssueId: blockedId, type: "blocks" },
{ companyId, issueId: blockedId, relatedIssueId: transitiveId, type: "blocks" },
// Corrupt legacy cycles must not inflate or hang the count.
{ companyId, issueId: transitiveId, relatedIssueId: blockedId, type: "blocks" },
]);
const feed = await attentionService(db).list(companyId, { userId: "board-user" });
const rows = feed.items.filter((item) => item.sourceKind === "blocker_attention");
expect(rows).toHaveLength(1);
expect(rows[0]).toMatchObject({
subject: { id: terminalId, identifier: "ATC-1", title: "Choose migration owner" },
relatedIssue: { id: blockedId },
whyNow: "Blocks 3 tasks and needs human attention.",
detail: { kind: "blocker", blockingIssue: null, blockedTaskCount: 3 },
});
expect(rows[0]?.subject.id).not.toBe(blockedId);
});
it("orders terminal blockers by blocked-work weight descending", async () => {
const { companyId } = await seedCompany("ATR");
const heavyTerminalId = await insertIssue({
companyId,
identifier: "ATR-1",
title: "Heavy blocker",
status: "todo",
updatedAt: new Date("2026-07-01T00:00:00.000Z"),
});
const lightTerminalId = await insertIssue({
companyId,
identifier: "ATR-2",
title: "Light blocker",
status: "todo",
updatedAt: new Date("2026-07-03T00:00:00.000Z"),
});
const heavyBlockedId = await insertIssue({
companyId,
identifier: "ATR-3",
title: "Heavy blocked root",
status: "blocked",
});
const lightBlockedId = await insertIssue({
companyId,
identifier: "ATR-4",
title: "Light blocked root",
status: "blocked",
});
await insertIssue({ companyId, identifier: "ATR-5", title: "Heavy child one", status: "todo", parentId: heavyBlockedId });
await insertIssue({ companyId, identifier: "ATR-6", title: "Heavy child two", status: "todo", parentId: heavyBlockedId });
await db.insert(issueRelations).values([
{ companyId, issueId: heavyTerminalId, relatedIssueId: heavyBlockedId, type: "blocks" },
{ companyId, issueId: lightTerminalId, relatedIssueId: lightBlockedId, type: "blocks" },
]);
const feed = await attentionService(db).list(companyId, { userId: "board-user" });
const rows = feed.items.filter((item) => item.sourceKind === "blocker_attention");
expect(rows.map((item) => item.subject.id)).toEqual([heavyTerminalId, lightTerminalId]);
expect(rows.map((item) => item.detail?.kind === "blocker" ? item.detail.blockedTaskCount : null)).toEqual([3, 1]);
});
it("does not name the blocked task as its own blocker on a human-owned unblock row", async () => {
const { companyId } = await seedCompany("ATX");
const transitionAt = new Date("2026-07-23T18:30:00.000Z");
const issueId = await insertIssue({
companyId,
identifier: "ATX-1",
title: "Needs board action",
status: "blocked",
unblockDescriptor: { owner: "board", action: "Approve the exception" },
blockedTransitionAt: transitionAt,
});
const feed = await attentionService(db).list(companyId, { userId: "board-user" });
const row = feed.items.find(
(item) => item.dedupKey === `blocked-owner:${issueId}:${transitionAt.toISOString()}`,
);
expect(row?.detail).toMatchObject({ kind: "blocker", blockingIssue: null });
});
it("does not route pre-rollout human unblock descriptors", async () => {
const { companyId } = await seedCompany("ATQ");
const transitionAt = new Date(ROUTABLE_BLOCKED_ROLLOUT_AT.getTime() - 1);
const issueId = await insertIssue({
companyId,
identifier: "ATQ-1",
title: "Human-owned before rollout",
status: "blocked",
unblockDescriptor: { owner: "board", action: "Review the issue" },
blockedTransitionAt: transitionAt,
});
const feed = await attentionService(db).list(companyId, { userId: "board-user" });
expect(feed.items.some((item) => item.dedupKey === `blocked-owner:${issueId}:${transitionAt.toISOString()}`)).toBe(false);
});
it("returns one pending approval row when the approval is linked to multiple tasks", async () => {
const { companyId } = await seedCompany("ATM");
const approvalId = randomUUID();
const firstIssueId = "00000000-0000-4000-8000-000000000001";
const secondIssueId = "00000000-0000-4000-8000-000000000002";
await insertIssue({ companyId, id: firstIssueId, identifier: "ATM-1", title: "First task", status: "in_progress" });
await insertIssue({ companyId, id: secondIssueId, identifier: "ATM-2", title: "Second task", status: "in_progress" });
await db.insert(approvals).values({
id: approvalId,
companyId,
type: "request_board_approval",
status: "pending",
payload: { title: "Approve rollout" },
});
await db.insert(issueApprovals).values([
{ companyId, issueId: secondIssueId, approvalId },
{ companyId, issueId: firstIssueId, approvalId },
]);
const feed = await attentionService(db).list(companyId, { userId: "board-user" });
const approvalItems = feed.items.filter((item) => item.dedupKey === `approval:${approvalId}`);
expect(approvalItems).toHaveLength(1);
expect(approvalItems[0]?.subject.metadata?.issueId).toBe(firstIssueId);
});
it("hides snoozed attention rows until snoozedUntil passes, then returns them unconditionally", async () => {
const { companyId } = await seedCompany("ATS");
const approvalId = randomUUID();
await db.insert(approvals).values({
id: approvalId,
companyId,
type: "hire_agent",
status: "pending",
payload: { title: "Hire Researcher" },
createdAt: new Date("2026-07-09T12:00:00.000Z"),
updatedAt: new Date("2026-07-09T12:00:00.000Z"),
});
await db.insert(inboxDismissals).values({
companyId,
userId: "board-user",
itemKey: `attention:approval:${approvalId}`,
kind: "snooze",
dismissedAt: new Date("2099-01-01T00:00:00.000Z"),
snoozedUntil: new Date("2099-01-02T00:00:00.000Z"),
});
await expect(attentionService(db).list(companyId, { userId: "board-user" }))
.resolves.toMatchObject({ totalCount: 1 }); // agent_error_alert from seed
const hiddenFeed = await attentionService(db).list(companyId, { userId: "board-user", includeDismissed: true });
expect(hiddenFeed.items.find((item) => item.dedupKey === `approval:${approvalId}`)?.dismissal)
.toMatchObject({ kind: "snooze", isActive: true, snoozedUntil: "2099-01-02T00:00:00.000Z" });
await db
.update(inboxDismissals)
.set({ snoozedUntil: new Date("2020-01-01T00:00:00.000Z") })
.where(eq(inboxDismissals.itemKey, `attention:approval:${approvalId}`));
const visibleFeed = await attentionService(db).list(companyId, { userId: "board-user" });
const visibleApproval = visibleFeed.items.find((item) => item.dedupKey === `approval:${approvalId}`);
expect(visibleApproval?.dismissal).toMatchObject({ kind: "snooze", isActive: false });
expect(visibleApproval).toBeTruthy();
});
it("enriches, filters, paginates, snoozes, and ranks the decide-now feed", async () => {
const { companyId, workerId, errorAgentId } = await seedCompany("ATP");
const now = Date.now();
const originIssueId = await insertIssue({
companyId,
identifier: "ATP-1",
title: "Decision origin",
status: "in_progress",
assigneeAgentId: workerId,
updatedAt: new Date(now - 10 * 60_000),
});
const runId = randomUUID();
await db.insert(heartbeatRuns).values({
id: runId,
companyId,
agentId: workerId,
status: "succeeded",
contextSnapshot: { issueId: originIssueId },
});
const expiringSoonId = randomUUID();
const expiringLaterId = randomUUID();
const wheneverId = randomUUID();
const snoozedId = randomUUID();
const decisionValues = [
{ id: expiringSoonId, ruleKey: "release.soon", title: "Expires sooner", updatedAt: new Date(now - 60_000), expiresAt: new Date(now + 60 * 60_000) },
{ id: expiringLaterId, ruleKey: "release.later", title: "Expires later", updatedAt: new Date(now - 2 * 60_000), expiresAt: new Date(now + 2 * 60 * 60_000) },
{ id: wheneverId, ruleKey: "release.whenever", title: "Can wait", updatedAt: new Date(now), expiresAt: new Date(now + 30 * 60_000) },
{ id: snoozedId, ruleKey: "release.snoozed", title: "Snoozed", updatedAt: new Date(now - 3 * 60_000), expiresAt: new Date(now + 15 * 60_000) },
];
await db.insert(decisions).values(decisionValues.map((value) => ({
...value,
companyId,
originAgentId: workerId,
originIssueId,
originRunId: runId,
body: value.title,
options: [],
status: "open",
signedSpec: "test",
targetSnapshots: {},
createdAt: value.updatedAt,
})));
const queueId = randomUUID();
await db.insert(decisionQueues).values({
id: queueId,
companyId,
key: "urgent-releases",
title: "Urgent releases",
createdByType: "user",
createdByUserId: "board-user",
});
await db.insert(decisionQueueItems).values([expiringSoonId, expiringLaterId].map((sourceId) => ({
companyId,
queueId,
sourceKind: "decision",
sourceId,
addedByType: "user",
addedByUserId: "board-user",
})));
await db.insert(decisionTriage).values([
expiringSoonId,
expiringLaterId,
snoozedId,
].map((sourceId) => ({
companyId,
sourceKind: "decision",
sourceId,
decideBy: "today",
setByType: "agent",
setByAgentId: workerId,
snoozedUntil: sourceId === snoozedId ? new Date(now + 60 * 60_000) : null,
})));
await db.insert(decisionTriage).values({
companyId,
sourceKind: "decision",
sourceId: wheneverId,
decideBy: "whenever",
setByType: "agent",
setByAgentId: workerId,
});
const approvalId = randomUUID();
await db.insert(approvals).values({
id: approvalId,
companyId,
type: "hire_agent",
status: "pending",
payload: { title: "Newer medium-severity approval" },
createdAt: new Date(now + 5 * 60_000),
updatedAt: new Date(now + 5 * 60_000),
});
await db.update(agents).set({ updatedAt: new Date(now - 5 * 60_000) }).where(eq(agents.id, errorAgentId));
const feed = await attentionService(db).list(companyId, {
userId: "board-user",
sort: "decide",
limit: 20,
});
// Desk badge = distinct items surfaced today OR with a due decide-by
// Everything here was seeded ~now, so every visible row
// counts; the whole page fits under limit:20 so items == rankedItems.
const startOfUtcDay = Date.UTC(
new Date(now).getUTCFullYear(),
new Date(now).getUTCMonth(),
new Date(now).getUTCDate(),
);
const expectedBadge = feed.items.filter(
(item) => new Date(item.createdAt).getTime() >= startOfUtcDay || item.decideBy === "today",
).length;
expect(expectedBadge).toBeGreaterThanOrEqual(2);
expect(feed.deskBadgeCount).toBe(expectedBadge);
expect(feed.items.some((item) => item.subject.id === snoozedId)).toBe(false);
expect(feed.items.slice(0, 3).map((item) => item.subject.id)).toEqual([
expiringSoonId,
expiringLaterId,
wheneverId,
]);
const erroredAgentIndex = feed.items.findIndex((item) => item.subject.id === errorAgentId);
const approvalIndex = feed.items.findIndex((item) => item.subject.id === approvalId);
expect(erroredAgentIndex).toBeGreaterThan(-1);
expect(erroredAgentIndex).toBeLessThan(approvalIndex);
const enriched = feed.items.find((item) => item.subject.id === expiringSoonId);
expect(enriched).toMatchObject({
expiresAt: decisionValues[0]!.expiresAt.toISOString(),
ruleKey: "release.soon",
originAgentName: "Worker",
queues: [{ key: "urgent-releases", title: "Urgent releases" }],
decideBy: "today",
decideByAttribution: {
type: "agent",
agentId: workerId,
agentName: "Worker",
},
snoozedUntil: null,
});
const firstPage = await attentionService(db).list(companyId, {
userId: "board-user",
queue: "urgent-releases",
sort: "decide",
limit: 1,
});
expect(firstPage).toMatchObject({ totalCount: 2, deskBadgeCount: 2 });
expect(firstPage.items.map((item) => item.subject.id)).toEqual([expiringSoonId]);
expect(firstPage.nextCursor).toBeTruthy();
const secondPage = await attentionService(db).list(companyId, {
userId: "board-user",
queue: "urgent-releases",
sort: "decide",
limit: 1,
cursor: firstPage.nextCursor!,
});
expect(secondPage.items.map((item) => item.subject.id)).toEqual([expiringLaterId]);
expect(secondPage.nextCursor).toBeNull();
const completeSnapshot = await attentionService(db).list(companyId, {
userId: "board-user",
queue: "urgent-releases",
sort: "decide",
all: true,
});
expect(completeSnapshot.items.map((item) => item.subject.id)).toEqual([expiringSoonId, expiringLaterId]);
expect(completeSnapshot.nextCursor).toBeNull();
const dateFiltered = await attentionService(db).list(companyId, {
userId: "board-user",
queue: "urgent-releases",
activitySince: new Date(now - 90_000).toISOString(),
activityUntil: new Date(now).toISOString(),
});
expect(dateFiltered.items.map((item) => item.subject.id)).toEqual([expiringSoonId]);
const withSnoozed = await attentionService(db).list(companyId, {
userId: "board-user",
includeDismissed: true,
limit: 20,
});
expect(withSnoozed.items.find((item) => item.subject.id === snoozedId)?.snoozedUntil)
.toBe(new Date(now + 60 * 60_000).toISOString());
});
it("returns a complete queue snapshot beyond the normal page limit", async () => {
const { companyId } = await seedCompany("ATS");
const queueId = randomUUID();
await db.insert(decisionQueues).values({
id: queueId,
companyId,
key: "bulk-review",
title: "Bulk review",
createdByType: "user",
createdByUserId: "board-user",
});
const approvalRows = Array.from({ length: 101 }, (_, index) => ({
id: randomUUID(),
companyId,
type: "hire_agent",
status: "pending",
payload: { title: `Review ${index + 1}` },
createdAt: new Date(Date.UTC(2026, 7, 2, 12, 0, index)),
updatedAt: new Date(Date.UTC(2026, 7, 2, 12, 0, index)),
}));
await db.insert(approvals).values(approvalRows);
await db.insert(decisionQueueItems).values(approvalRows.map((approval) => ({
companyId,
queueId,
sourceKind: "approval",
sourceId: approval.id,
addedByType: "user",
addedByUserId: "board-user",
})));
const firstPage = await attentionService(db).list(companyId, {
userId: "board-user",
queue: "bulk-review",
});
expect(firstPage.items).toHaveLength(50);
expect(firstPage.nextCursor).toBeTruthy();
const completeSnapshot = await attentionService(db).list(companyId, {
userId: "board-user",
queue: "bulk-review",
all: true,
});
expect(completeSnapshot.items).toHaveLength(101);
expect(new Set(completeSnapshot.items.map((item) => item.id)).size).toBe(101);
expect(completeSnapshot.nextCursor).toBeNull();
const internalSnapshot = await attentionService(db).list(companyId, {
userId: "board-user",
all: true,
allowUnscopedAll: true,
});
const internalSnapshotIds = new Set(internalSnapshot.items.map((item) => item.subject.id));
expect(approvalRows.every((approval) => internalSnapshotIds.has(approval.id))).toBe(true);
expect(internalSnapshot.items.length).toBeGreaterThan(100);
expect(internalSnapshot.nextCursor).toBeNull();
await expect(attentionService(db).list(companyId, { userId: "board-user", all: true }))
.rejects.toThrow("all requires a queue filter");
await expect(attentionService(db).list(companyId, {
userId: "board-user",
queue: "bulk-review",
all: true,
limit: 25,
})).rejects.toThrow("all cannot be combined with cursor or limit");
});
it("does not apply the open-decision safety limit to complete snapshots", async () => {
const { companyId, workerId } = await seedCompany("ATC");
const originIssueId = await insertIssue({
companyId,
identifier: "ATC-1",
title: "Decision origin",
status: "in_progress",
assigneeAgentId: workerId,
});
const runId = randomUUID();
await db.insert(heartbeatRuns).values({
id: runId,
companyId,
agentId: workerId,
status: "succeeded",
contextSnapshot: { issueId: originIssueId },
});
await db.insert(decisions).values(["First decision", "Second decision"].map((title) => ({
id: randomUUID(),
companyId,
originAgentId: workerId,
originIssueId,
originRunId: runId,
title,
body: title,
options: [],
status: "open" as const,
expiresAt: new Date("2026-08-10T00:00:00.000Z"),
signedSpec: "test",
targetSnapshots: {},
})));
const svc = attentionService(db, { openDecisionLimit: 1 });
const limited = await svc.list(companyId, { userId: "board-user" });
expect(limited.items.filter((item) => item.sourceKind === "decision")).toHaveLength(1);
const complete = await svc.list(companyId, {
userId: "board-user",
all: true,
allowUnscopedAll: true,
});
expect(complete.items.filter((item) => item.sourceKind === "decision")).toHaveLength(2);
expect(complete.nextCursor).toBeNull();
});
it("keeps this-week deadlines in the current UTC week", async () => {
const { companyId, workerId } = await seedCompany("ATW");
const now = Date.parse("2026-08-02T12:00:00.000Z"); // Sunday in an ISO Monday-Sunday week.
const originIssueId = await insertIssue({
companyId,
identifier: "ATW-1",
title: "Decision origin",
status: "in_progress",
assigneeAgentId: workerId,
updatedAt: new Date(now),
});
const runId = randomUUID();
await db.insert(heartbeatRuns).values({
id: runId,
companyId,
agentId: workerId,
status: "succeeded",
contextSnapshot: { issueId: originIssueId },
});
const thisWeekId = randomUUID();
const nextWeekId = randomUUID();
await db.insert(decisions).values([
{ id: thisWeekId, title: "This week", expiresAt: new Date("2026-08-09T12:00:00.000Z") },
{ id: nextWeekId, title: "Next week", expiresAt: new Date("2026-08-03T12:00:00.000Z") },
].map((value) => ({
...value,
companyId,
originAgentId: workerId,
originIssueId,
originRunId: runId,
body: value.title,
options: [],
status: "open" as const,
signedSpec: "test",
targetSnapshots: {},
createdAt: new Date(now),
updatedAt: new Date(now),
})));
await db.insert(decisionTriage).values([
{
companyId,
sourceKind: "decision",
sourceId: thisWeekId,
decideBy: "this_week",
setByType: "user",
setByUserId: "board-user",
},
{
companyId,
sourceKind: "decision",
sourceId: nextWeekId,
decideBy: "date",
decideByDate: "2026-08-03",
setByType: "user",
setByUserId: "board-user",
},
]);
const feed = await attentionService(db, { now: () => now }).list(companyId, {
userId: "board-user",
sort: "decide",
limit: 20,
});
const decisionItems = feed.items.filter((item) => item.sourceKind === "decision");
expect(decisionItems.map((item) => item.subject.id)).toEqual([thisWeekId, nextWeekId]);
expect(decisionItems[0]).toMatchObject({
decideBy: "this_week",
expiresAt: "2026-08-09T12:00:00.000Z",
});
expect(decisionItems[1]).toMatchObject({
decideBy: "2026-08-03",
expiresAt: "2026-08-03T12:00:00.000Z",
});
});
it("serves the route for board users and rejects agent callers", async () => {
const { companyId } = await seedCompany("ATR");
function app(actor: Record<string, unknown>) {
const testApp = express();
testApp.use(express.json());
testApp.use((req, _res, next) => {
(req as any).actor = actor;
next();
});
testApp.use("/api", attentionRoutes(db));
testApp.use(errorHandler);
return testApp;
}
const board = {
type: "board",
source: "local_implicit",
userId: "board-user",
companyIds: [companyId],
isInstanceAdmin: false,
};
const agent = {
type: "agent",
source: "agent_key",
companyId,
agentId: randomUUID(),
runId: null,
};
await request(app(board)).get(`/api/companies/${companyId}/attention`).expect(200);
const completeFeed = await request(app(board))
.get(`/api/companies/${companyId}/attention?includeDismissed=true&all=true`)
.expect(200);
expect(completeFeed.body.nextCursor).toBeNull();
await request(app(board))
.get(`/api/companies/${companyId}/attention?activitySince=yesterday`)
.expect(400, { error: "activitySince must be an ISO timestamp" });
await request(app(board))
.get(`/api/companies/${companyId}/attention?sort=oldest`)
.expect(400, { error: "sort must be 'activity' or 'decide'" });
await request(app(agent)).get(`/api/companies/${companyId}/attention`).expect(403);
});
it("computes the aging shelf uniformly across approval, interaction, and review sources", async () => {
const { companyId, workerId } = await seedCompany("AGE");
const now = Date.parse("2026-08-02T12:00:00.000Z");
const idleAt = new Date("2026-06-30T12:00:00.000Z");
const interactionIssueId = await insertIssue({
companyId,
identifier: "AGE-1",
title: "Old questions",
status: "in_progress",
assigneeAgentId: workerId,
createdAt: idleAt,
updatedAt: idleAt,
});
const reviewIssueId = await insertIssue({
companyId,
identifier: "AGE-2",
title: "Old review",
status: "in_review",
assigneeUserId: "board-user",
createdAt: idleAt,
updatedAt: idleAt,
});
const approvalId = randomUUID();
const queueApprovalId = randomUUID();
const interactionId = randomUUID();
const queueIdleAt = new Date("2026-07-18T12:00:00.000Z");
await db.insert(approvals).values([
{
id: approvalId,
companyId,
type: "request_board_approval",
requestedByAgentId: workerId,
status: "pending",
payload: { title: "Old approval" },
createdAt: idleAt,
updatedAt: idleAt,
},
{
id: queueApprovalId,
companyId,
type: "request_board_approval",
requestedByAgentId: workerId,
status: "pending",
payload: { title: "Queue-retained approval" },
createdAt: queueIdleAt,
updatedAt: queueIdleAt,
},
]);
await db.insert(issueThreadInteractions).values({
id: interactionId,
companyId,
issueId: interactionIssueId,
kind: "ask_user_questions",
status: "pending",
createdByAgentId: workerId,
payload: { version: 1, questions: [] },
createdAt: idleAt,
updatedAt: idleAt,
});
const [queue] = await db.insert(decisionQueues).values({
companyId,
key: "fast-aging",
title: "Fast aging",
retentionDays: 10,
createdByType: "system",
}).returning();
await db.insert(decisionQueueItems).values({
companyId,
queueId: queue!.id,
sourceKind: "approval",
sourceId: queueApprovalId,
addedByType: "system",
});
const feed = await attentionService(db, { now: () => now }).list(companyId, {
userId: "board-user",
limit: 100,
});
const byKey = new Map(feed.items.map((item) => [`${item.sourceKind}:${item.subject.id}`, item]));
for (const key of [
`approval:${approvalId}`,
`issue_thread_interaction:${interactionId}`,
`review:${reviewIssueId}`,
]) {
expect(byKey.get(key)).toMatchObject({ shelf: true, retentionDays: 30, archivedAt: null });
}
expect(byKey.get(`approval:${queueApprovalId}`)).toMatchObject({ shelf: true, retentionDays: 10 });
});
});