import type { Agent } from "@paperclipai/shared"; import { describe, expect, it } from "vitest"; import { formatActivityVerb, formatIssueActivityAction } from "./activity-format"; describe("activity formatting", () => { const agentMap = new Map([ ["agent-reviewer", { id: "agent-reviewer", name: "Reviewer Bot" } as Agent], ["agent-approver", { id: "agent-approver", name: "Approver Bot" } as Agent], ]); it("formats blocker activity using linked issue identifiers", () => { const details = { addedBlockedByIssues: [ { id: "issue-2", identifier: "PAP-22", title: "Blocked task" }, ], removedBlockedByIssues: [], }; expect(formatActivityVerb("issue.blockers_updated", details)).toBe("added blocker PAP-22 to"); expect(formatIssueActivityAction("issue.blockers_updated", details)).toBe("added blocker PAP-22"); }); it("formats reviewer activity using agent names", () => { const details = { addedParticipants: [ { type: "agent", agentId: "agent-reviewer", userId: null }, ], removedParticipants: [], }; expect(formatActivityVerb("issue.reviewers_updated", details, { agentMap })).toBe("added reviewer Reviewer Bot to"); expect(formatIssueActivityAction("issue.reviewers_updated", details, { agentMap })).toBe("added reviewer Reviewer Bot"); }); it("formats approver removals using user-aware labels", () => { const details = { addedParticipants: [], removedParticipants: [ { type: "user", agentId: null, userId: "local-board" }, ], }; expect(formatActivityVerb("issue.approvers_updated", details)).toBe("removed approver Board from"); expect(formatIssueActivityAction("issue.approvers_updated", details)).toBe("removed approver Board"); }); it("falls back to updated wording when reviewers are both added and removed", () => { const details = { addedParticipants: [ { type: "agent", agentId: "agent-reviewer", userId: null }, ], removedParticipants: [ { type: "agent", agentId: "agent-approver", userId: null }, ], }; expect(formatActivityVerb("issue.reviewers_updated", details, { agentMap })).toBe("updated reviewers on"); expect(formatIssueActivityAction("issue.reviewers_updated", details, { agentMap })).toBe("updated reviewers"); }); it("formats monitor activity with direct verbs", () => { expect(formatActivityVerb("issue.monitor_scheduled")).toBe("scheduled monitor on"); expect(formatActivityVerb("issue.monitor_exhausted")).toBe("exhausted monitor on"); expect(formatIssueActivityAction("issue.monitor_triggered")).toBe("triggered a monitor"); expect(formatIssueActivityAction("issue.monitor_cleared")).toBe("cleared a monitor"); expect(formatIssueActivityAction("issue.monitor_recovery_issue_created")).toBe("created a monitor recovery issue"); }); // PAP-16506 P4: agents can now resolve an interaction, including a review of // their own work, so an outcome has to read as an outcome in the timeline // instead of leaking the raw action id. it("reads an interaction outcome as an outcome, whoever gave it", () => { expect(formatIssueActivityAction("issue.thread_interaction_accepted")).toBe("accepted the request"); expect(formatIssueActivityAction("issue.thread_interaction_rejected")).toBe("rejected the request"); expect(formatIssueActivityAction("issue.thread_interaction_withdrawn")).toBe("withdrew the request"); expect(formatIssueActivityAction("issue.thread_interaction_expired")).toBe("expired the request"); expect(formatActivityVerb("issue.thread_interaction_accepted")).toBe("accepted the request on"); expect(formatActivityVerb("issue.thread_interaction_rejected")).toBe("rejected the request on"); }); // The accepted/rejected actions fire for every interaction kind, so only a // confirmation may read as an approval. it("says 'approved' only for a confirmation, never for a suggestion or a question", () => { expect(formatIssueActivityAction("issue.thread_interaction_accepted", { interactionKind: "request_confirmation" })) .toBe("approved the request"); expect(formatIssueActivityAction("issue.thread_interaction_accepted", { interactionKind: "request_checkbox_confirmation" })) .toBe("approved the request"); expect(formatIssueActivityAction("issue.thread_interaction_accepted", { interactionKind: "suggest_tasks" })) .toBe("accepted the task suggestions"); expect(formatIssueActivityAction("issue.thread_interaction_accepted", { interactionKind: "ask_user_questions" })) .toBe("accepted the answers"); expect(formatIssueActivityAction("issue.thread_interaction_rejected", { interactionKind: "request_confirmation" })) .toBe("rejected the request"); expect(formatIssueActivityAction("issue.thread_interaction_rejected", { interactionKind: "suggest_tasks" })) .toBe("declined the task suggestions"); expect(formatActivityVerb("issue.thread_interaction_accepted", { interactionKind: "suggest_tasks" })) .toBe("accepted the task suggestions on"); }); it("keeps the neutral wording for a kind it does not know", () => { expect(formatIssueActivityAction("issue.thread_interaction_accepted", { interactionKind: "request_item_verdicts" })) .toBe("accepted the request"); expect(formatIssueActivityAction("issue.thread_interaction_accepted", { interactionKind: "future_kind" })) .toBe("accepted the request"); expect(formatIssueActivityAction("issue.thread_interaction_accepted", { interactionKind: 7 })) .toBe("accepted the request"); }); it("names the verb an actor chose on a stalled review", () => { expect(formatIssueActivityAction("issue.stalled_review_decided", { action: "approve" })) .toBe("approved the review"); expect(formatIssueActivityAction("issue.stalled_review_decided", { action: "request_changes" })) .toBe("requested changes on the review"); expect(formatIssueActivityAction("issue.stalled_review_decided", { action: "send_back" })) .toBe("sent the review back to work"); expect(formatActivityVerb("issue.stalled_review_decided", { action: "approve" })) .toBe("approved the review on"); }); it("falls back to a generic verdict line when the decision verb is missing", () => { expect(formatIssueActivityAction("issue.stalled_review_decided")).toBe("recorded a review verdict"); expect(formatIssueActivityAction("issue.stalled_review_decided", { action: "shrug" })) .toBe("recorded a review verdict"); }); it("describes a review-policy change without calling the default 'none'", () => { expect(formatIssueActivityAction("issue.updated", { reviewPolicy: null })) .toBe("changed who can approve to anyone"); expect(formatIssueActivityAction("issue.updated", { reviewPolicy: "human_only" })) .toBe("changed who can approve to human only"); expect(formatIssueActivityAction("issue.updated", { reviewPolicy: "not_creator" })) .toBe("changed who can approve to anyone else"); }); it("uses plain next-step copy for successful-run handoff activity", () => { expect(formatActivityVerb("issue.successful_run_handoff_required")).toBe("flagged missing next step on"); expect(formatIssueActivityAction("issue.successful_run_handoff_required")).toBe("Run finished without a clear next step"); expect(formatIssueActivityAction("issue.successful_run_handoff_resolved")).toBe("Next step chosen"); expect(formatIssueActivityAction("issue.successful_run_handoff_escalated")).toBe( "Run finished without a next step - recovery escalated", ); }); });