mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-24 01:41:42 +02:00
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work > - The server records actions that change issues and their queued comments > - The queued-comment edit, reorder, and discard routes changed queue state without activity rows > - Operators could not inspect these queue mutations in the activity feed > - This pull request adds one identifier-only activity row for each successful queue mutation > - The benefit is a durable audit trail with no comment text in the activity log ## Linked Issues or Issue Description **What existing behavior does this improve?** The queued-comment edit, reorder, and discard routes now record their successful mutations in the activity feed. **Subsystem affected** Cross-cutting (server and ui). **Current behavior** The three queue mutation routes change queued comments but do not write an activity row. The activity feed has no label for these actions. **Proposed behavior** Each successful route writes one activity row with the actor fields, entity fields, queue identifiers, and queue revision. The discard row also includes the cancelled run identifier. The activity feed shows a label for each action. **Reason and benefit** Operators need a durable record of queue changes. Identifier-only details support audit and troubleshooting without storing comment text. **Breaking changes** None. The routes keep their existing response and authorization behavior. **Additional context** Each mutation writes its activity row on the same locked transaction that applies the mutation, so the two commit or roll back together. The route publishes the live activity event only after that transaction commits. The separate comment-cancel route opts out of this write and keeps its existing single activity row. ## What Changed - Add activity rows for queued-comment edit, reorder, and discard mutations. - Include queue identifiers, revisions, ordered comment identifiers, and cancelled run identifiers as applicable. - Add activity-feed labels for the three new actions. - Add route and activity-format tests for the new behavior. - Write each activity row on the same transaction as the mutation it records, through a new port method that the adapter implements. - Keep the comment-delete route opted out of that write, so a cancellation does not log two rows. ## Verification - [x] `npx vitest run server/src/__tests__/issue-queued-comments-routes.test.ts` passes. - [x] `npx vitest run ui/src/lib/activity-format.test.ts` passes. - [x] `pnpm --filter @paperclipai/server typecheck` exits 0. - [x] `pnpm --filter @paperclipai/ui typecheck` exits 0. - [x] `node scripts/check-module-boundaries.mjs` passes. - [x] The full CI suite is green. ## Risks Low risk. The change adds activity rows after successful mutations and does not change route responses, authorization, or stored comment text. ## Model Used OpenAI Codex, GPT-5 Codex. The model used repository inspection, Git operations, and command execution. The context window and reasoning mode are not exposed by this runtime. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes: #` / `Refs: #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change (e.g. `docs/...`, `fix/...`) and contains no internal Paperclip ticket id or instance-derived details - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] All Paperclip CI gates are green - [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge --------- Co-authored-by: Paperclip <noreply@paperclip.ing>
153 lines
8.2 KiB
TypeScript
153 lines
8.2 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("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",
|
|
);
|
|
});
|
|
});
|