A Button configured with nodeAction = RESET whose child is a
Project.TaskList: clicking it ran ServerTaskListProcessor.processReset
(tasks were marked complete in the DB), but the UI kept showing the old
incomplete tasks and the past-due “confounded face” / escalated chip until
the app was fully restarted. Intermediate states of the fix also exposed an
infinite reset loop.
Three stacked issues, peeled one at a time:
update() settled with state = NONE.
SseStateForwarding.filtered deliberately drops NONE from /sse, so the
reset meta was persisted but never broadcast.state = RESET instead is forwarded — but
ServerTaskListProcessor.post() re-dispatches RESET into processReset,
so re-emitting under RESET is an infinite loop.EventClient (the client’s only SSE consumer) reads
/events, not /sse. Its STATE_CHANGE handler copies only the new
state onto the client’s stale local node, discarding server-side
meta. No /events type carried a generic node-meta mutation, so a
TaskList reset was invisible until a full REST reload. This was
pre-existing and general (server-side expiredExecuted flips had the same
blind spot); RESET merely surfaced it.processReset now writes the reset meta under NodeState.PROCESSING
(SSE-forwarded and falls through post()’s else, so no loop), then
setStateToNone to settle. To actually reach live clients it additionally
posts an EventType.CREATED event carrying the full reset node — CREATED
is the one /events type whose EventClient handler replaces the whole node
(nodeManager.update(payload.node)). IconManager then re-derives
computeTaskListState over the completed/future-dated tasks → NONE, so
visuals clear without a restart. (CREATED-for-an-existing-node is a
deliberate stopgap; the clean form is a dedicated NODE_UPDATED event.)
Regression test asserts the propagation, not just the DB row:
ServerTaskListProcessorResetTest verifies a CREATED event is emitted whose
payload node carries the reset meta and state == NONE, and that
executeChildren is never called. TaskListResetTest (pure
resetAllTasks) locks the no-immediate-refire invariant and that
computeTaskListState derives to NONE post-reset. General rule: when a
server-side change must reach clients, the test must observe it via the
event/SSE path a client actually consumes — asserting persistence alone hides
this entire class of bug.