mirror of
https://github.com/PerpetualSoftware/pad.git
synced 2026-09-22 18:43:45 +00:00
7943234dd7
* fix(store): treat empty assignment IDs as clear-to-NULL (BUG-2566)
PATCH with {"assigned_user_id": ""} 500'd with a raw FK constraint
error and left the item assigned. An explicit empty string is what a
JSON client sends when a user blanks the field (JSON null on a *string
decodes to nil = "don't change", so it can't express clearing), and
validateAssignmentScope already skips "" as nothing-to-validate — the
SQL builder just never learned the same convention and bound it
verbatim into the FK column.
Store-level fix so every surface (HTTP, bulk, MCP, CLI) inherits it:
"" now clears assigned_user_id / agent_role_id exactly like
ClearAssignedUser / ClearAgentRole on update, and binds NULL on
create. The mutation signal keeps the ClearAssignedUser shape (tested)
so watch notifications are unaffected. parent_id deliberately NOT
given the same coercion: parent relations also live in item_links, and
clearing the column alone would desync them.
Web UI is unaffected either way — it already sends
clear_assigned_user: true; only API clients hit this.
Tests reproduce the exact FK failure pre-fix (verified by stash-run)
and pass on both SQLite and PostgreSQL post-fix.
Claude-Session: https://claude.ai/code/session_01BhQoeaWXxJbvw86ezzK8dt
* fix(store): relocate nullIfEmptyID out of createItemTx's doc comment
Codex round 2 P3: the helper was inserted between createItemTx's doc
block and the function, orphaning the doc comment.
Claude-Session: https://claude.ai/code/session_01BhQoeaWXxJbvw86ezzK8dt
93 lines
3.0 KiB
Go
93 lines
3.0 KiB
Go
package store
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/PerpetualSoftware/pad/internal/models"
|
|
)
|
|
|
|
// BUG-2566: an explicit empty string for assigned_user_id / agent_role_id
|
|
// is what a JSON client sends when a user blanks the field. It must clear
|
|
// the assignment (write NULL) exactly like ClearAssignedUser /
|
|
// ClearAgentRole — before the fix it was bound verbatim and failed the FK
|
|
// with a driver-specific 500.
|
|
|
|
func TestUpdateItem_EmptyAssignedUserIDClearsAssignment(t *testing.T) {
|
|
t.Parallel()
|
|
s := testStore(t)
|
|
wsID, colID := newTransitionTestWorkspace(t, s)
|
|
item := createTestItem(t, s, wsID, colID, "Assign then blank", "")
|
|
|
|
assignee := createTestUser(t, s, "blankme@example.com", "BlankMe", "pw")
|
|
if err := s.AddWorkspaceMember(wsID, assignee.ID, "editor"); err != nil {
|
|
t.Fatalf("add member: %v", err)
|
|
}
|
|
if _, err := s.UpdateItem(item.ID, models.ItemUpdate{AssignedUserID: &assignee.ID}); err != nil {
|
|
t.Fatalf("assign item: %v", err)
|
|
}
|
|
|
|
empty := ""
|
|
cleared, err := s.UpdateItem(item.ID, models.ItemUpdate{AssignedUserID: &empty})
|
|
if err != nil {
|
|
t.Fatalf("update with empty assigned_user_id: %v", err)
|
|
}
|
|
if cleared.AssignedUserID != nil {
|
|
t.Fatalf("expected assignment cleared, got %q", *cleared.AssignedUserID)
|
|
}
|
|
// The mutation signal must match the ClearAssignedUser shape — the
|
|
// notification pipeline keys off it.
|
|
if cleared.LastMutation == nil || !cleared.LastMutation.AssignmentChanged {
|
|
t.Fatalf("expected AssignmentChanged=true, got %+v", cleared.LastMutation)
|
|
}
|
|
if cleared.LastMutation.FromAssignedUserID != assignee.ID || cleared.LastMutation.ToAssignedUserID != "" {
|
|
t.Fatalf("expected %q -> '', got %q -> %q", assignee.ID,
|
|
cleared.LastMutation.FromAssignedUserID, cleared.LastMutation.ToAssignedUserID)
|
|
}
|
|
}
|
|
|
|
func TestUpdateItem_EmptyAgentRoleIDClearsRole(t *testing.T) {
|
|
t.Parallel()
|
|
s := testStore(t)
|
|
wsID, colID := newTransitionTestWorkspace(t, s)
|
|
item := createTestItem(t, s, wsID, colID, "Role then blank", "")
|
|
|
|
role, err := s.CreateAgentRole(wsID, models.AgentRoleCreate{Name: "Blanker"})
|
|
if err != nil {
|
|
t.Fatalf("CreateAgentRole: %v", err)
|
|
}
|
|
if _, err := s.UpdateItem(item.ID, models.ItemUpdate{AgentRoleID: &role.ID}); err != nil {
|
|
t.Fatalf("set role: %v", err)
|
|
}
|
|
|
|
empty := ""
|
|
cleared, err := s.UpdateItem(item.ID, models.ItemUpdate{AgentRoleID: &empty})
|
|
if err != nil {
|
|
t.Fatalf("update with empty agent_role_id: %v", err)
|
|
}
|
|
if cleared.AgentRoleID != nil {
|
|
t.Fatalf("expected role cleared, got %q", *cleared.AgentRoleID)
|
|
}
|
|
}
|
|
|
|
func TestCreateItem_EmptyAssignmentIDsAreNull(t *testing.T) {
|
|
t.Parallel()
|
|
s := testStore(t)
|
|
wsID, colID := newTransitionTestWorkspace(t, s)
|
|
|
|
empty := ""
|
|
item, err := s.CreateItem(wsID, colID, models.ItemCreate{
|
|
Title: "Born unassigned",
|
|
AssignedUserID: &empty,
|
|
AgentRoleID: &empty,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create with empty assignment ids: %v", err)
|
|
}
|
|
if item.AssignedUserID != nil {
|
|
t.Fatalf("expected nil assigned_user_id, got %q", *item.AssignedUserID)
|
|
}
|
|
if item.AgentRoleID != nil {
|
|
t.Fatalf("expected nil agent_role_id, got %q", *item.AgentRoleID)
|
|
}
|
|
}
|