Merge aa72015a2f into c9e3bb7ca4
This commit is contained in:
commit
c7334433ee
|
|
@ -924,6 +924,10 @@ Minimum event set:
|
|||
- `agent.run.cancelled`
|
||||
- `approval.created`
|
||||
- `approval.decided`
|
||||
- `decision.created`
|
||||
- `decision.expired`
|
||||
- `decision.dismissed`
|
||||
- `decision.cancelled`
|
||||
- `budget.incident.opened`
|
||||
- `budget.incident.resolved`
|
||||
- `cost_event.created`
|
||||
|
|
|
|||
|
|
@ -1708,6 +1708,14 @@ export const PLUGIN_EVENT_TYPES = [
|
|||
"goal.updated",
|
||||
"approval.created",
|
||||
"approval.decided",
|
||||
// Decisions Desk lifecycle. A decision is the platform's "a human must choose,
|
||||
// by this date" object, so excluding it left notifications unable to cover the
|
||||
// surface operators most need to be woken for. The queue, triage, training and
|
||||
// retention actions stay private: they are desk plumbing, not decisions.
|
||||
"decision.created",
|
||||
"decision.expired",
|
||||
"decision.dismissed",
|
||||
"decision.cancelled",
|
||||
"budget.incident.opened",
|
||||
"budget.incident.resolved",
|
||||
"cost_event.created",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,74 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import { PLUGIN_EVENT_TYPES } from "@paperclipai/shared";
|
||||
import { eventTypeForActivityAction } from "../services/activity-log.js";
|
||||
|
||||
/**
|
||||
* The activity-action → plugin-event mapping is a plugin-facing contract that
|
||||
* fails silently: an unmapped action produces no event, so a subscriber simply
|
||||
* never hears about it and nothing errors. The Decisions Desk was in exactly that
|
||||
* state — `decision.created` was logged with the payload a notifier needs
|
||||
* (`originIssueId`, `originAgentId`, `originResponsibleUserId`) but mapped to
|
||||
* nothing.
|
||||
*/
|
||||
describe("decision lifecycle plugin events", () => {
|
||||
const decisionActions = [
|
||||
"decision.created",
|
||||
"decision.expired",
|
||||
"decision.dismissed",
|
||||
"decision.cancelled",
|
||||
] as const;
|
||||
|
||||
it("declares every decision lifecycle action as a subscribable event type", () => {
|
||||
for (const action of decisionActions) {
|
||||
expect(PLUGIN_EVENT_TYPES).toContain(action);
|
||||
}
|
||||
});
|
||||
|
||||
it("maps each decision action onto its own event type", () => {
|
||||
for (const action of decisionActions) {
|
||||
expect(eventTypeForActivityAction(action)).toBe(action);
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps desk plumbing out of the plugin event surface", () => {
|
||||
// Queue, triage, training and retention actions are internal desk mechanics.
|
||||
// Exposing them would invite plugins to depend on queue internals, and each
|
||||
// one would become a compatibility obligation.
|
||||
for (const action of [
|
||||
"decision_queue_item.added",
|
||||
"decision_queue.created",
|
||||
"decision_queue_item.removed",
|
||||
"decision_triage.updated",
|
||||
"decision_training.created",
|
||||
"decision_retention.archived",
|
||||
]) {
|
||||
expect(eventTypeForActivityAction(action)).toBeNull();
|
||||
}
|
||||
});
|
||||
|
||||
it("does not treat a decision action as a prefix match", () => {
|
||||
// The pass-through is an exact set lookup, not a prefix rule: a future
|
||||
// action must be declared deliberately rather than inheriting an event.
|
||||
expect(eventTypeForActivityAction("decision.created.v2")).toBeNull();
|
||||
expect(eventTypeForActivityAction("decision")).toBeNull();
|
||||
});
|
||||
|
||||
it("still maps the legacy underscore actions it already covered", () => {
|
||||
expect(eventTypeForActivityAction("approval_approved")).toBe("approval.decided");
|
||||
expect(eventTypeForActivityAction("approval_rejected")).toBe("approval.decided");
|
||||
expect(eventTypeForActivityAction("issue_comment_added")).toBe("issue.comment.created");
|
||||
expect(eventTypeForActivityAction("budget_soft_threshold_crossed")).toBe(
|
||||
"budget.incident.opened",
|
||||
);
|
||||
});
|
||||
|
||||
it("passes through the event types that were already exact names", () => {
|
||||
expect(eventTypeForActivityAction("issue.created")).toBe("issue.created");
|
||||
expect(eventTypeForActivityAction("approval.created")).toBe("approval.created");
|
||||
});
|
||||
|
||||
it("leaves unrelated activity unmapped", () => {
|
||||
expect(eventTypeForActivityAction("company.created.note")).toBeNull();
|
||||
expect(eventTypeForActivityAction("some.unmapped.action")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
|
@ -37,7 +37,14 @@ export function setPluginEventBus(bus: PluginEventBus): void {
|
|||
_pluginEventBus = bus;
|
||||
}
|
||||
|
||||
function eventTypeForActivityAction(action: string): PluginEventType | null {
|
||||
/**
|
||||
* Resolve the plugin event type for a logged activity action.
|
||||
*
|
||||
* Exported for direct testing, following `resolveResponsibleUserIdForActivity`:
|
||||
* the mapping is the contract plugins subscribe against, and a silent regression
|
||||
* here is invisible at runtime (the event is simply never delivered).
|
||||
*/
|
||||
export function eventTypeForActivityAction(action: string): PluginEventType | null {
|
||||
if (PLUGIN_EVENT_SET.has(action)) return action as PluginEventType;
|
||||
return ACTIVITY_ACTION_TO_PLUGIN_EVENT[action.replaceAll(".", "_")] ?? null;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue