mirror of
https://github.com/PerpetualSoftware/pad.git
synced 2026-10-26 06:19:49 +00:00
feat(events): derive SSE names from the taxonomy; retire item.updated_with_comment (TASK-2714)
SPEC-3 §"the choke point owns the canonical→surface name mapping". SSE's snake_case vocabulary and the webhook dot-form vocabulary drifted because nothing tied them together — each was hand-passed at its own call sites. This ties them. v1.5 pins what "derive" means: NAME derivation, not delivery path. SSE stays direct-published at the mutation site, because it carries request-scoped attribution (Actor / ActorName / Source) that a frozen outbox payload deliberately does not hold; only its NAME now comes from the taxonomy. Moving SSE behind the drain is TASK-2722. - eventSpec gains an `sse` field — ONE table, not a second map, for the reason round 11 of the last unit established: a separate map can disagree with the first and fails open exactly when it matters. Empty is a real value (attachment, member and pack events have no SSE surface) and SurfaceSSE reports false for it, so silence can't be mistaken for a name. - Several canonical events derive the SAME SSE name — status_changed and moved both surface as item_updated — because the SSE vocabulary is coarser than events/1 and the UI never distinguished them. The finer name is what the webhook wire and bindings get. - The 12 canonical SSE publish sites take their names from derived package vars, resolved AT INIT. Every call site is a compile-time constant, so a missing surface is a startup panic rather than a per-request decision between "log and drop" and "publish under an empty name". - handlers_item_links.go keeps the events.ItemUpdated literal, commented: link mutations are silent in events/1 (v1.5), so there is no canonical name to derive from. TASK-2723 carries link.created / link.removed. - item.updated_with_comment retired (v1.2, Dave's ruling). One producer deleted; the events.ItemUpdatedWithComment constant deleted with it — it had no producer and no web consumer (grepped .go/.ts/.svelte), so leaving it would leave a name a future publisher could reach for. The compat guard is what makes this a refactor rather than a wire change: TestDerivedSSENamesMatchTheLegacyWireVocabulary asserts each derived name equals the events.* constant clients are pinned to. A derivation producing "item.created" or "item_deleted" would break the live UI while every other Go test still passed. Mutation matrix, 3/3 caught: rename item.deleted's SSE surface to item_deleted (both the taxonomy test and the compat guard fail), split item.moved onto its own SSE name (same), make SurfaceSSE return (spec.sse, ok) so no-surface events fail open (the taxonomy test's silence leg names all four). Running total 9/9. go test ./internal/server ./internal/store ./internal/events: all green.
This commit is contained in:
@@ -42,8 +42,12 @@ const (
|
||||
ItemStarred = "item_starred"
|
||||
ItemUnstarred = "item_unstarred"
|
||||
|
||||
// Composite events
|
||||
ItemUpdatedWithComment = "item_updated_with_comment"
|
||||
// Composite events: NONE. item_updated_with_comment was declared here
|
||||
// and never published by anything — its only producer was a hand-called
|
||||
// webhook dispatch using the dot-form name, retired under SPEC-3 v1.2
|
||||
// (Dave's ruling, day-48) because it folds into item.updated +
|
||||
// comment.created. The constant went with it rather than being left as
|
||||
// a name a future publisher could reach for.
|
||||
|
||||
// Batch events. Emitted once for a whole bulk mutation (TASK-1668)
|
||||
// instead of one ItemUpdated/ItemArchived per row — the lane-header
|
||||
|
||||
@@ -147,6 +147,26 @@ const (
|
||||
type eventSpec struct {
|
||||
subject string
|
||||
payload string
|
||||
|
||||
// sse is the snake_case name this event publishes under on the SSE
|
||||
// surface, or "" when the event has no SSE surface at all.
|
||||
//
|
||||
// SPEC-3 §"The choke point owns the canonical→surface name mapping":
|
||||
// SSE (snake_case) and webhooks (dot-form) drifted because nothing tied
|
||||
// their vocabularies together. This field is the tie. SPEC-3 v1.5 pins
|
||||
// what "derive" means — NAME derivation, not delivery path: webhooks
|
||||
// deliver through the drain, SSE stays direct-published at the mutation
|
||||
// site (it carries request-scoped Actor/ActorName/Source that a frozen
|
||||
// payload deliberately does not hold) and takes only its NAME from here.
|
||||
//
|
||||
// The empty string is a real value, not a gap: attachment, member and
|
||||
// pack events have no SSE surface, and SurfaceSSE reports false for them
|
||||
// so a caller cannot mistake silence for a name. Several canonical events
|
||||
// map onto ONE SSE name — status_changed and moved both surface as
|
||||
// item_updated — because the SSE vocabulary is coarser than events/1 and
|
||||
// the UI has never distinguished them. That is a mapping, not a loss:
|
||||
// the fine-grained name is what the webhook wire and bindings receive.
|
||||
sse string
|
||||
}
|
||||
|
||||
// canonical is the closed events/1 set. Adding an entry here is the ONLY way
|
||||
@@ -186,22 +206,22 @@ const (
|
||||
)
|
||||
|
||||
var canonical = map[string]eventSpec{
|
||||
ItemCreated: {SubjectItem, PayloadItemSnapshot},
|
||||
ItemUpdated: {SubjectItem, PayloadItemSnapshot},
|
||||
ItemStatusChanged: {SubjectItem, PayloadItemSnapshot},
|
||||
ItemMoved: {SubjectItem, PayloadItemSnapshot},
|
||||
ItemDeleted: {SubjectItem, PayloadItemSnapshot},
|
||||
ItemRestored: {SubjectItem, PayloadItemSnapshot},
|
||||
ItemBulkUpdated: {SubjectItemBatch, PayloadItemBatch},
|
||||
CommentCreated: {SubjectComment, PayloadCommentSnapshot},
|
||||
CommentUpdated: {SubjectComment, PayloadCommentSnapshot},
|
||||
CommentDeleted: {SubjectComment, PayloadRefOnly},
|
||||
AttachmentAdded: {SubjectAttachment, PayloadAttachmentSnapshot},
|
||||
AttachmentRemoved: {SubjectAttachment, PayloadRefOnly},
|
||||
MemberJoined: {SubjectMember, PayloadMember},
|
||||
PackInstalled: {SubjectPack, PayloadPack},
|
||||
PackUpgraded: {SubjectPack, PayloadPack},
|
||||
PackDisabled: {SubjectPack, PayloadPack},
|
||||
ItemCreated: {SubjectItem, PayloadItemSnapshot, "item_created"},
|
||||
ItemUpdated: {SubjectItem, PayloadItemSnapshot, "item_updated"},
|
||||
ItemStatusChanged: {SubjectItem, PayloadItemSnapshot, "item_updated"},
|
||||
ItemMoved: {SubjectItem, PayloadItemSnapshot, "item_updated"},
|
||||
ItemDeleted: {SubjectItem, PayloadItemSnapshot, "item_archived"},
|
||||
ItemRestored: {SubjectItem, PayloadItemSnapshot, "item_restored"},
|
||||
ItemBulkUpdated: {SubjectItemBatch, PayloadItemBatch, "items_bulk_updated"},
|
||||
CommentCreated: {SubjectComment, PayloadCommentSnapshot, "comment_created"},
|
||||
CommentUpdated: {SubjectComment, PayloadCommentSnapshot, "comment_updated"},
|
||||
CommentDeleted: {SubjectComment, PayloadRefOnly, "comment_deleted"},
|
||||
AttachmentAdded: {SubjectAttachment, PayloadAttachmentSnapshot, ""},
|
||||
AttachmentRemoved: {SubjectAttachment, PayloadRefOnly, ""},
|
||||
MemberJoined: {SubjectMember, PayloadMember, ""},
|
||||
PackInstalled: {SubjectPack, PayloadPack, ""},
|
||||
PackUpgraded: {SubjectPack, PayloadPack, ""},
|
||||
PackDisabled: {SubjectPack, PayloadPack, ""},
|
||||
}
|
||||
|
||||
// PayloadFamily returns the payload shape a canonical event carries, and false
|
||||
@@ -247,3 +267,23 @@ func Canonical() []string {
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
// SurfaceSSE returns the SSE wire name a canonical event publishes under, and
|
||||
// false when it has none — either because the name is not canonical, or
|
||||
// because the event is deliberately absent from the SSE surface.
|
||||
//
|
||||
// ONE BOOL FOR BOTH CASES, deliberately. A caller has the same job either way:
|
||||
// do not publish. Splitting them would invite a caller to branch on
|
||||
// "canonical but unmapped" and invent a name, which is the drift this table
|
||||
// exists to end.
|
||||
//
|
||||
// The webhook wire name needs no accessor: it IS the canonical name. That
|
||||
// asymmetry is the point of SPEC-3's mapping sentence — the dot-form vocabulary
|
||||
// is the contract, and SSE's snake_case is a surface rendering of it.
|
||||
func SurfaceSSE(name string) (string, bool) {
|
||||
spec, ok := canonical[name]
|
||||
if !ok || spec.sse == "" {
|
||||
return "", false
|
||||
}
|
||||
return spec.sse, true
|
||||
}
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/PerpetualSoftware/pad/internal/kernelevents"
|
||||
)
|
||||
|
||||
// The SSE wire names, DERIVED from the events/1 taxonomy rather than written
|
||||
// out again here.
|
||||
//
|
||||
// SPEC-3 §"The choke point owns the canonical→surface name mapping" is the
|
||||
// reason this file exists: SSE's snake_case vocabulary and the webhook
|
||||
// dot-form vocabulary drifted because nothing tied them together, each being
|
||||
// hand-passed at its own call sites. v1.5 pins what "derive" means — NAME
|
||||
// derivation, not delivery path. Webhooks deliver through the outbox drain;
|
||||
// SSE stays direct-published at the mutation site, because it carries
|
||||
// request-scoped attribution (Actor / ActorName / Source) that a frozen outbox
|
||||
// payload deliberately does not hold. What changes is only where the NAME
|
||||
// comes from.
|
||||
//
|
||||
// DERIVED AT INIT, not per publish, and that is the whole safety argument. A
|
||||
// canonical event with no SSE surface is a programming error at these call
|
||||
// sites — every one of them is a compile-time constant — so the check belongs
|
||||
// where a failure is impossible to miss and impossible to reach a user:
|
||||
// process start. A per-request lookup would have to decide what to do with the
|
||||
// "no surface" answer in a void helper, and every available answer (log and
|
||||
// drop, publish under an empty name) is worse than not starting.
|
||||
//
|
||||
// Several canonical events derive the SAME name: status_changed and moved both
|
||||
// surface as item_updated, because the SSE vocabulary is coarser than events/1
|
||||
// and the UI has never distinguished them. The finer name is what the webhook
|
||||
// wire and bindings receive.
|
||||
var (
|
||||
sseItemCreated = mustSurfaceSSE(kernelevents.ItemCreated)
|
||||
sseItemUpdated = mustSurfaceSSE(kernelevents.ItemUpdated)
|
||||
sseItemMoved = mustSurfaceSSE(kernelevents.ItemMoved)
|
||||
sseItemArchived = mustSurfaceSSE(kernelevents.ItemDeleted)
|
||||
sseItemRestored = mustSurfaceSSE(kernelevents.ItemRestored)
|
||||
sseItemsBulk = mustSurfaceSSE(kernelevents.ItemBulkUpdated)
|
||||
sseCommentCreated = mustSurfaceSSE(kernelevents.CommentCreated)
|
||||
sseCommentUpdated = mustSurfaceSSE(kernelevents.CommentUpdated)
|
||||
)
|
||||
|
||||
// mustSurfaceSSE resolves a canonical event's SSE wire name or panics.
|
||||
//
|
||||
// The panic is deliberate and it fires at package init, before the server
|
||||
// listens: a canonical event that reaches here without an SSE surface means
|
||||
// the taxonomy and this file disagree, and the only honest outcomes are
|
||||
// "publish nothing" (silently breaking the live UI) or "refuse to start".
|
||||
// Refusing to start is the one a person notices.
|
||||
func mustSurfaceSSE(canonical string) string {
|
||||
name, ok := kernelevents.SurfaceSSE(canonical)
|
||||
if !ok {
|
||||
panic(fmt.Sprintf("kernelevents: %q has no SSE surface name", canonical))
|
||||
}
|
||||
return name
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/PerpetualSoftware/pad/internal/events"
|
||||
)
|
||||
|
||||
// TestDerivedSSENamesMatchTheLegacyWireVocabulary is the compat guard for the
|
||||
// rewire: deriving the SSE names from the events/1 taxonomy must reproduce the
|
||||
// strings the bus already published, byte for byte.
|
||||
//
|
||||
// This is what makes the rewire a REFACTOR rather than a wire change. Every
|
||||
// live client — the web app's SSE handler, delta-sync's cursor logic — matches
|
||||
// on these literals, so a derivation that quietly produced "item.created" or
|
||||
// "item_deleted" would break the UI while every Go test still passed.
|
||||
//
|
||||
// The assertion runs against events.* rather than against fresh literals on
|
||||
// purpose: those constants are what the CLIENTS are pinned to, so comparing
|
||||
// against them is comparing against the thing that actually has to hold.
|
||||
func TestDerivedSSENamesMatchTheLegacyWireVocabulary(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
derived string
|
||||
legacy string
|
||||
}{
|
||||
{"item created", sseItemCreated, events.ItemCreated},
|
||||
{"item updated", sseItemUpdated, events.ItemUpdated},
|
||||
{"item moved surfaces as updated", sseItemMoved, events.ItemUpdated},
|
||||
{"item deleted surfaces as archived", sseItemArchived, events.ItemArchived},
|
||||
{"item restored", sseItemRestored, events.ItemRestored},
|
||||
{"bulk", sseItemsBulk, events.ItemsBulkUpdated},
|
||||
{"comment created", sseCommentCreated, events.CommentCreated},
|
||||
{"comment updated", sseCommentUpdated, events.CommentUpdated},
|
||||
} {
|
||||
if tc.derived != tc.legacy {
|
||||
t.Errorf("%s: derived SSE name = %q, legacy wire name = %q — clients match the legacy string",
|
||||
tc.name, tc.derived, tc.legacy)
|
||||
}
|
||||
if tc.derived == "" {
|
||||
t.Errorf("%s: derived SSE name is empty", tc.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -136,7 +136,7 @@ func (s *Server) handleCreateComment(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// Publish SSE event
|
||||
s.publishCommentEvent(events.CommentCreated, workspaceID, item.ID, comment.ID, item.Title, item.CollectionSlug, actor, source)
|
||||
s.publishCommentEvent(sseCommentCreated, workspaceID, item.ID, comment.ID, item.Title, item.CollectionSlug, actor, source)
|
||||
s.dispatchWebhook(workspaceID, "comment.created", comment)
|
||||
|
||||
if s.watchEvents != nil {
|
||||
@@ -263,7 +263,7 @@ func (s *Server) handleUpdateComment(w http.ResponseWriter, r *http.Request) {
|
||||
title = item.Title
|
||||
collSlug = item.CollectionSlug
|
||||
}
|
||||
s.publishCommentEvent(events.CommentUpdated, workspaceID, updated.ItemID, updated.ID, title, collSlug, actor, source)
|
||||
s.publishCommentEvent(sseCommentUpdated, workspaceID, updated.ItemID, updated.ID, title, collSlug, actor, source)
|
||||
s.dispatchWebhook(workspaceID, "comment.updated", updated)
|
||||
|
||||
writeJSON(w, http.StatusOK, updated)
|
||||
@@ -365,7 +365,7 @@ func (s *Server) handleCreateReply(w http.ResponseWriter, r *http.Request) {
|
||||
replyItemRef = replyItem.Ref
|
||||
replyCollID = replyItem.CollectionID
|
||||
}
|
||||
s.publishCommentEvent(events.CommentCreated, workspaceID, parentComment.ItemID, comment.ID, parentComment.ItemTitle, replyCollSlug, actor, source)
|
||||
s.publishCommentEvent(sseCommentCreated, workspaceID, parentComment.ItemID, comment.ID, parentComment.ItemTitle, replyCollSlug, actor, source)
|
||||
|
||||
// TASK-2533 (codex round 1, finding 2): a reply is a SEPARATE code
|
||||
// path from handleCreateComment — it calls store.CreateComment
|
||||
|
||||
@@ -266,5 +266,13 @@ func (s *Server) publishStructuralLinkSourceEvent(r *http.Request, workspaceID,
|
||||
return
|
||||
}
|
||||
actor, source := actorFromRequest(r)
|
||||
// events.ItemUpdated LITERAL, not derived from the taxonomy, and that is
|
||||
// the decision rather than an oversight. Item-link mutations are SILENT in
|
||||
// events/1 (SPEC-3 v1.5): a link is plausibly its own subject kind, and
|
||||
// faking item.updated would ship a snapshot in which nothing the consumer
|
||||
// caches has changed. There is no canonical event to derive a name from
|
||||
// here, so this publish stays a pure SSE nudge for the live UI. TASK-2723
|
||||
// carries the link.created / link.removed shape and this call site with
|
||||
// it.
|
||||
s.publishItemEventWithName(events.ItemUpdated, workspaceID, item.ID, item.Title, item.CollectionSlug, actor, actorNameFromRequest(r), source, item.Seq)
|
||||
}
|
||||
|
||||
@@ -807,7 +807,7 @@ func (s *Server) createItemChecked(r *http.Request, workspaceID string, coll *mo
|
||||
actor, source := actorFromRequest(r)
|
||||
actorNameForCreate := actorNameFromRequest(r)
|
||||
s.logActivity(workspaceID, item.ID, "created", r)
|
||||
s.publishItemEventWithName(events.ItemCreated, workspaceID, item.ID, item.Title, coll.Slug, actor, actorNameForCreate, source, item.Seq)
|
||||
s.publishItemEventWithName(sseItemCreated, workspaceID, item.ID, item.Title, coll.Slug, actor, actorNameForCreate, source, item.Seq)
|
||||
s.dispatchWebhook(workspaceID, "item.created", item)
|
||||
|
||||
// Assignment-at-creation (TASK-2533): unlike an update, a freshly
|
||||
@@ -1694,7 +1694,7 @@ func (s *Server) handleUpdateItem(w http.ResponseWriter, r *http.Request) {
|
||||
actor, source := actorFromRequest(r)
|
||||
activityID, _ := s.logActivityWithMetaReturningID(workspaceID, updated.ID, "updated", r, meta)
|
||||
actorNameForUpdate := actorNameFromRequest(r)
|
||||
s.publishItemEventWithName(events.ItemUpdated, workspaceID, updated.ID, updated.Title, updated.CollectionSlug, actor, actorNameForUpdate, source, updated.Seq)
|
||||
s.publishItemEventWithName(sseItemUpdated, workspaceID, updated.ID, updated.Title, updated.CollectionSlug, actor, actorNameForUpdate, source, updated.Seq)
|
||||
s.dispatchWebhook(workspaceID, "item.updated", updated)
|
||||
s.publishWatchNotifications(workspaceID, updated, actor, actorNameForUpdate)
|
||||
|
||||
@@ -1715,12 +1715,15 @@ func (s *Server) handleUpdateItem(w http.ResponseWriter, r *http.Request) {
|
||||
slog.Warn("failed to create comment on item update", "item_id", updated.ID, "error", cerr)
|
||||
}
|
||||
if cerr == nil && comment != nil {
|
||||
s.publishCommentEvent(events.CommentCreated, workspaceID, updated.ID, comment.ID, updated.Title, updated.CollectionSlug, actor, source)
|
||||
s.dispatchWebhook(workspaceID, "item.updated_with_comment", map[string]interface{}{
|
||||
"item": updated,
|
||||
"comment": comment,
|
||||
"changes": meta,
|
||||
})
|
||||
s.publishCommentEvent(sseCommentCreated, workspaceID, updated.ID, comment.ID, updated.Title, updated.CollectionSlug, actor, source)
|
||||
// item.updated_with_comment is RETIRED (SPEC-3 v1.2, Dave's
|
||||
// ruling): a hand-rolled composite with no flood argument,
|
||||
// which folds into item.updated + comment.created — both of
|
||||
// which this path already emits, transactionally, from the
|
||||
// store. It is never emitted under events/1, with no
|
||||
// deprecation window, because webhooks are CLI-registered
|
||||
// only today (no UI surface) and there were no known
|
||||
// consumers at ruling time.
|
||||
// TASK-2533: this comment is created via a DIFFERENT code
|
||||
// path than handleCreateComment (store.CreateComment called
|
||||
// directly, not through POST .../comments) — a bypass Rider
|
||||
@@ -1791,7 +1794,7 @@ func (s *Server) handleDeleteItem(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
actor, source := actorFromRequest(r)
|
||||
s.logActivity(workspaceID, item.ID, "archived", r)
|
||||
s.publishItemEventWithName(events.ItemArchived, workspaceID, item.ID, item.Title, item.CollectionSlug, actor, actorNameFromRequest(r), source, deleteSeq)
|
||||
s.publishItemEventWithName(sseItemArchived, workspaceID, item.ID, item.Title, item.CollectionSlug, actor, actorNameFromRequest(r), source, deleteSeq)
|
||||
s.dispatchWebhook(workspaceID, "item.deleted", item)
|
||||
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
@@ -1844,7 +1847,7 @@ func (s *Server) handleRestoreItem(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
actor, source := actorFromRequest(r)
|
||||
s.logActivity(workspaceID, restored.ID, "restored", r)
|
||||
s.publishItemEventWithName(events.ItemRestored, workspaceID, restored.ID, restored.Title, restored.CollectionSlug, actor, actorNameFromRequest(r), source, restored.Seq)
|
||||
s.publishItemEventWithName(sseItemRestored, workspaceID, restored.ID, restored.Title, restored.CollectionSlug, actor, actorNameFromRequest(r), source, restored.Seq)
|
||||
|
||||
restoreVisIDs, _ := s.visibleCollectionIDs(r, workspaceID)
|
||||
if err := s.enrichItemForResponse(restored, restoreVisIDs); err != nil {
|
||||
@@ -2128,7 +2131,7 @@ func (s *Server) handleMoveItem(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// Publish events for both old and new collections
|
||||
actorNameForMove := actorNameFromRequest(r)
|
||||
s.publishItemEventWithName(events.ItemUpdated, workspaceID, moved.ID, moved.Title, targetColl.Slug, actor, actorNameForMove, source, moved.Seq)
|
||||
s.publishItemEventWithName(sseItemMoved, workspaceID, moved.ID, moved.Title, targetColl.Slug, actor, actorNameForMove, source, moved.Seq)
|
||||
s.dispatchWebhook(workspaceID, "item.moved", moved)
|
||||
s.publishWatchNotifications(workspaceID, moved, actor, actorNameForMove)
|
||||
|
||||
|
||||
@@ -697,7 +697,7 @@ func (s *Server) publishBulkItemsEvent(workspaceID, op, collection string, count
|
||||
return
|
||||
}
|
||||
s.events.Publish(events.Event{
|
||||
Type: events.ItemsBulkUpdated,
|
||||
Type: sseItemsBulk,
|
||||
WorkspaceID: workspaceID,
|
||||
Collection: collection,
|
||||
Op: op,
|
||||
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
"net/http"
|
||||
"runtime/debug"
|
||||
|
||||
"github.com/PerpetualSoftware/pad/internal/events"
|
||||
"github.com/PerpetualSoftware/pad/internal/models"
|
||||
"github.com/PerpetualSoftware/pad/internal/store"
|
||||
)
|
||||
@@ -662,7 +661,7 @@ func (s *Server) emitCopyFanout(r *http.Request, res *store.CrossWorkspaceCopyRe
|
||||
// --- Destination: always all three. ---
|
||||
targetWorkspaceID := res.Item.WorkspaceID
|
||||
s.logActivity(targetWorkspaceID, res.Item.ID, "created", r)
|
||||
s.publishItemEventWithName(events.ItemCreated, targetWorkspaceID, res.Item.ID, res.Item.Title,
|
||||
s.publishItemEventWithName(sseItemCreated, targetWorkspaceID, res.Item.ID, res.Item.Title,
|
||||
res.TargetCollection.Slug, actor, actorName, actorSource, res.Item.Seq)
|
||||
s.dispatchWebhook(targetWorkspaceID, "item.created", res.Item)
|
||||
|
||||
@@ -674,7 +673,7 @@ func (s *Server) emitCopyFanout(r *http.Request, res *store.CrossWorkspaceCopyRe
|
||||
return
|
||||
}
|
||||
s.logActivity(res.SourceWorkspaceID, res.Source.ID, "archived", r)
|
||||
s.publishItemEventWithName(events.ItemArchived, res.SourceWorkspaceID, res.Source.ID, res.Source.Title,
|
||||
s.publishItemEventWithName(sseItemArchived, res.SourceWorkspaceID, res.Source.ID, res.Source.Title,
|
||||
res.SourceCollection.Slug, actor, actorName, actorSource, *res.SourceSeq)
|
||||
s.dispatchWebhook(res.SourceWorkspaceID, "item.deleted", res.Source)
|
||||
}
|
||||
|
||||
@@ -1375,23 +1375,23 @@ func TestCanonicalEventsAreFullyDeclared(t *testing.T) {
|
||||
// The events/1 set at SPEC-3 v1.4. Adding, removing or re-homing an entry
|
||||
// here is a CONTRACT CHANGE: update the spec version and the taxonomy's
|
||||
// doc comment in the same commit.
|
||||
want := map[string]struct{ subject, family string }{
|
||||
"item.created": {kernelevents.SubjectItem, kernelevents.PayloadItemSnapshot},
|
||||
"item.updated": {kernelevents.SubjectItem, kernelevents.PayloadItemSnapshot},
|
||||
"item.status_changed": {kernelevents.SubjectItem, kernelevents.PayloadItemSnapshot},
|
||||
"item.moved": {kernelevents.SubjectItem, kernelevents.PayloadItemSnapshot},
|
||||
"item.deleted": {kernelevents.SubjectItem, kernelevents.PayloadItemSnapshot},
|
||||
"item.restored": {kernelevents.SubjectItem, kernelevents.PayloadItemSnapshot},
|
||||
"item.bulk_updated": {kernelevents.SubjectItemBatch, kernelevents.PayloadItemBatch},
|
||||
"comment.created": {kernelevents.SubjectComment, kernelevents.PayloadCommentSnapshot},
|
||||
"comment.updated": {kernelevents.SubjectComment, kernelevents.PayloadCommentSnapshot},
|
||||
"comment.deleted": {kernelevents.SubjectComment, kernelevents.PayloadRefOnly},
|
||||
"attachment.added": {kernelevents.SubjectAttachment, kernelevents.PayloadAttachmentSnapshot},
|
||||
"attachment.removed": {kernelevents.SubjectAttachment, kernelevents.PayloadRefOnly},
|
||||
"member.joined": {kernelevents.SubjectMember, kernelevents.PayloadMember},
|
||||
"pack.installed": {kernelevents.SubjectPack, kernelevents.PayloadPack},
|
||||
"pack.upgraded": {kernelevents.SubjectPack, kernelevents.PayloadPack},
|
||||
"pack.disabled": {kernelevents.SubjectPack, kernelevents.PayloadPack},
|
||||
want := map[string]struct{ subject, family, sse string }{
|
||||
"item.created": {kernelevents.SubjectItem, kernelevents.PayloadItemSnapshot, "item_created"},
|
||||
"item.updated": {kernelevents.SubjectItem, kernelevents.PayloadItemSnapshot, "item_updated"},
|
||||
"item.status_changed": {kernelevents.SubjectItem, kernelevents.PayloadItemSnapshot, "item_updated"},
|
||||
"item.moved": {kernelevents.SubjectItem, kernelevents.PayloadItemSnapshot, "item_updated"},
|
||||
"item.deleted": {kernelevents.SubjectItem, kernelevents.PayloadItemSnapshot, "item_archived"},
|
||||
"item.restored": {kernelevents.SubjectItem, kernelevents.PayloadItemSnapshot, "item_restored"},
|
||||
"item.bulk_updated": {kernelevents.SubjectItemBatch, kernelevents.PayloadItemBatch, "items_bulk_updated"},
|
||||
"comment.created": {kernelevents.SubjectComment, kernelevents.PayloadCommentSnapshot, "comment_created"},
|
||||
"comment.updated": {kernelevents.SubjectComment, kernelevents.PayloadCommentSnapshot, "comment_updated"},
|
||||
"comment.deleted": {kernelevents.SubjectComment, kernelevents.PayloadRefOnly, "comment_deleted"},
|
||||
"attachment.added": {kernelevents.SubjectAttachment, kernelevents.PayloadAttachmentSnapshot, ""},
|
||||
"attachment.removed": {kernelevents.SubjectAttachment, kernelevents.PayloadRefOnly, ""},
|
||||
"member.joined": {kernelevents.SubjectMember, kernelevents.PayloadMember, ""},
|
||||
"pack.installed": {kernelevents.SubjectPack, kernelevents.PayloadPack, ""},
|
||||
"pack.upgraded": {kernelevents.SubjectPack, kernelevents.PayloadPack, ""},
|
||||
"pack.disabled": {kernelevents.SubjectPack, kernelevents.PayloadPack, ""},
|
||||
}
|
||||
|
||||
// The name constants are pinned to their wire strings separately, because
|
||||
@@ -1444,6 +1444,17 @@ func TestCanonicalEventsAreFullyDeclared(t *testing.T) {
|
||||
if !kernelevents.IsCanonical(name) {
|
||||
t.Errorf("IsCanonical(%q) = false for an event Canonical() returned", name)
|
||||
}
|
||||
// The SSE surface name, including the events that deliberately have
|
||||
// none. An empty want.sse asserts SILENCE — SurfaceSSE must report
|
||||
// false rather than handing back a name a caller would publish under.
|
||||
sse, sseOK := kernelevents.SurfaceSSE(name)
|
||||
if expected.sse == "" {
|
||||
if sseOK || sse != "" {
|
||||
t.Errorf("SurfaceSSE(%q) = (%q, %v), want (\"\", false) — this event has no SSE surface", name, sse, sseOK)
|
||||
}
|
||||
} else if !sseOK || sse != expected.sse {
|
||||
t.Errorf("SurfaceSSE(%q) = (%q, %v), want (%q, true)", name, sse, sseOK, expected.sse)
|
||||
}
|
||||
}
|
||||
for name := range want {
|
||||
if !seen[name] {
|
||||
@@ -1464,6 +1475,9 @@ func TestCanonicalEventsAreFullyDeclared(t *testing.T) {
|
||||
if kernelevents.IsCanonical(name) {
|
||||
t.Errorf("IsCanonical(%q) = true", name)
|
||||
}
|
||||
if _, ok := kernelevents.SurfaceSSE(name); ok {
|
||||
t.Errorf("SurfaceSSE(%q) reported ok for a non-canonical name", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user