mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-14 17:40:37 +02:00
Settle published, successful Slack turns as Idle; resume the same conversation on an admitted message. Preserve unfinished work, delivery errors, and explicit dispositions. Verified through focused lifecycle/API/UI tests, full CI, and a real staging Slack conversation in the embedded browser. Co-Authored-By: Paperclip <noreply@paperclip.ing>
165 lines
8.9 KiB
TypeScript
165 lines
8.9 KiB
TypeScript
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<string, Agent>([
|
|
["agent-reviewer", { id: "agent-reviewer", name: "Reviewer Bot" } as Agent],
|
|
["agent-approver", { id: "agent-approver", name: "Approver Bot" } as Agent],
|
|
]);
|
|
|
|
it("labels a Slack settlement as idle while retaining actual review wording", () => {
|
|
const details = { status: "in_review", externalConversationState: "waiting" };
|
|
expect(formatActivityVerb("issue.updated", details)).toBe("changed status to idle on");
|
|
expect(formatIssueActivityAction("issue.updated", details)).toBe("changed the status to idle");
|
|
expect(formatActivityVerb("issue.updated", { status: "in_review" })).toBe("changed status to in review on");
|
|
});
|
|
|
|
it("uses readable verbs for task read-state changes", () => {
|
|
expect(formatActivityVerb("issue.read_marked")).toBe("read");
|
|
expect(formatActivityVerb("issue.read_unmarked")).toBe("marked unread");
|
|
});
|
|
|
|
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");
|
|
});
|
|
|
|
it("labels each queued-comment queue mutation", () => {
|
|
expect(formatActivityVerb("issue.queued_comment_edited")).toBe("edited a queued comment on");
|
|
expect(formatActivityVerb("issue.queued_comments_reordered")).toBe("reordered queued comments on");
|
|
expect(formatActivityVerb("issue.queued_comment_discarded")).toBe("discarded a queued comment on");
|
|
expect(formatIssueActivityAction("issue.queued_comment_edited")).toBe("edited a queued comment");
|
|
expect(formatIssueActivityAction("issue.queued_comments_reordered")).toBe("reordered queued comments");
|
|
expect(formatIssueActivityAction("issue.queued_comment_discarded")).toBe("discarded a queued comment");
|
|
});
|
|
|
|
// 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",
|
|
);
|
|
});
|
|
});
|