fix(api): personal-tasks endpoints must fire model_activity (Lark bot fan-out)#12
Merged
JOBYINC merged 1 commit intoMay 20, 2026
Conversation
…ot fan-out) PersonalTaskAPIEndpoint.post() and .patch() previously fired only issue_activity.delay, missing the model_activity.delay call that IssueListCreateAPIEndpoint / IssueDetailAPIEndpoint both make. That silently dropped webhook fan-out, so the Lark bot never received the issue.created / issue.updated event and never DM'd the assignee for tasks created via the system-token path. POST now fires model_activity with current_instance=None; PATCH snapshots the pre-update serialized issue so per-field updated events can be diffed. The 409 idempotent-reuse path stays silent — nothing was created. Adds 3 contract tests covering POST fan-out, PATCH fan-out, and the 409 no-refire guard, plus extends the autouse _no_celery fixture to patch the new task entry point.
4 tasks
JOBYINC
added a commit
that referenced
this pull request
May 20, 2026
…l-task create (#13) The actual Lark notification path is NOT model_activity / webhook fan-out (PR #12 was based on a wrong hypothesis). Lark DMs are dispatched inline from `issue_activity` itself: after IssueActivity rows are bulk_create'd, `dispatch_lark_for_activities` walks the rows and fires `notify_issue_assigned_task.delay` for any row with `field="assignees"` + `new_identifier=<user>`. That assignee row is only created when `create_issue_activity` calls `track_assignees`, and the gate at issue_activities_task.py:581 is: if requested_data.get("assignee_ids") is not None: ... The plane/api IssueSerializer uses `assignees` as its write key (vs. the plane/app variant which uses `assignee_ids`). personal_task.py forwards the request body as-is, so the gate never fires for the system-token create path → no assignee IssueActivity row → no Lark DM, even though `LARK_NOTIFICATIONS_ENABLED=1` and the worker correctly received the issue_activity task (verified in prod logs). Fix: mirror `payload["assignees"]` to `payload["assignee_ids"]` in the dict passed to `issue_activity.delay`. The serializer.save() above still consumes `assignees` (the serializer key it knows about); only the activity tracker payload is augmented. Adds a contract test that asserts the requested_data sent to issue_activity includes `assignee_ids` matching the resolved owner — the precise contract the gate at line 581 needs. PATCH path is unaffected: `update_issue_activity` ATTRIBUTE_MAPPER already maps both `"assignees"` and `"assignee_ids"` to track_assignees. Co-authored-by: Marcus Cheung <marcusm5@Marcuss-MacBook-Pro.local>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
PersonalTaskAPIEndpoint.post()and.patch()now firemodel_activity.delay(...)so webhook subscribers (Lark bot, et al.) receiveissue.created/issue.updatedevents for system-token-created tasks. Previously onlyissue_activity.delayfired, which is why Tom-created tasks landed in the target user's My Tasks bucket but never DM'd them on Lark.current_instance) somodel_activitycan diff old vs new and emit per-fieldwebhook_activityevents — matching the standardIssueDetailAPIEndpoint.patch()pattern (issue.py:785).Why
Regression vs. the standard
POST /api/v1/workspaces/{slug}/projects/{pid}/work-items/path: that endpoint fires bothissue_activityandmodel_activity(issue.py:473, :484). The new system-tokenpersonal-tasksendpoint only mirrored half the pattern.model_activityis the entry point forwebhook_activity.delay, which is what feeds the Lark-bot subscriber that DMs assignees.Files
apps/api/plane/api/views/personal_task.py— importmodel_activity, fire it inpost()(current_instance=None) andpatch()(snapshot pre-update viaIssueSerializer(issue).data).apps/api/plane/tests/contract/api/test_personal_tasks.py— 3 new tests inTestPersonalTasksWebhookFanOut: POST fan-out, PATCH fan-out, 409 no-refire guard. Autouse_no_celeryfixture also patches the new task entry point.Test plan
pytest plane/tests/contract/api/test_personal_tasks.py -v→ 14 passed (11 existing + 3 new).lark-stableimage rebuilt + redeployed: Tom creates a task on behalf of a target user viaPOST /personal-tasks/, assignee receives the Lark DM card.external_sourceupdates a personal task; assignee receives Lark "updated" notification.Related
feature/lark-oauth-providerso the merge triggers aBuild Lark Feature Images (GHCR)workflow → freshlark-stablefortask.vijimgroup.com.