fix: customer-based board visibility, chat widget UX, atomized Subdivide, static cache-busting
- Board visibility (§4.4): developers now see tasks for any customer with a consultant who pooled them, not only tasks whose consultantId is in their pool. Fixes a published task being invisible to its own author who is both consultant and developer. Adds TestBoardVisibilityIsCustomerBased. - Chat widget: working ✕ close, distinct (non-ghost) header buttons, reliable conversation switching, ← Back, title ellipsis. - Atomization: atomized/imported subtasks now expose a Subdivide button. - Static assets: serve with ETag + Cache-Control: no-cache so JS/CSS fixes reach clients immediately instead of being masked by stale max-age caching. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -295,6 +295,56 @@ func TestDeclineWithdrawUnassignAbandon(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestBoardVisibilityIsCustomerBased: a developer pooled by consultant A
|
||||
// sees tasks published by consultant B on the same customer (§4.4 — all
|
||||
// consultants of a customer manage its tasks).
|
||||
func TestBoardVisibilityIsCustomerBased(t *testing.T) {
|
||||
l := newLifecycleStack(t, nil)
|
||||
|
||||
// second consultant on the same customer publishes their own task
|
||||
bc := newClient(t)
|
||||
consB := registerUser(t, l.ts.URL, bc, "lc-cons-b@example.com", "Cons B")
|
||||
promote(t, l.st, consB.ID, map[string]bool{"consultant": true})
|
||||
if _, err := l.st.DB.Collection("customers").UpdateOne(t.Context(),
|
||||
bson.M{"_id": l.customerID},
|
||||
bson.M{"$push": bson.M{"consultantIds": consB.ID}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
taskB := &domain.Task{
|
||||
CustomerID: l.customerID, ConsultantID: consB.ID,
|
||||
Origin: domain.OriginSubdivided, Status: domain.StatusAtomized,
|
||||
Title: "Task owned by consultant B", EffortCoefficient: 0.5, Budget: 1000, Bounty: 500,
|
||||
}
|
||||
if err := l.st.InsertTask(t.Context(), taskB); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
bCSRF := csrfFrom(t, bc, l.ts.URL)
|
||||
mustPost(t, bc, l.ts.URL+"/api/v1/tasks/"+taskB.ID+"/publish", map[string]any{}, bCSRF, http.StatusOK).Body.Close()
|
||||
|
||||
// the developer is pooled ONLY by consultant A, yet sees B's task
|
||||
resp, err := l.developer.Get(l.ts.URL + "/api/v1/board")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var board struct {
|
||||
Tasks []domain.Task `json:"tasks"`
|
||||
}
|
||||
bodyJSON(t, resp, &board)
|
||||
found := false
|
||||
for _, tk := range board.Tasks {
|
||||
if tk.ID == taskB.ID {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Fatalf("developer pooled by consultant A cannot see consultant B's task on the same customer: %+v", board.Tasks)
|
||||
}
|
||||
|
||||
// and can claim it
|
||||
mustPost(t, l.developer, l.ts.URL+"/api/v1/tasks/"+taskB.ID+"/claim",
|
||||
map[string]string{"note": "cross-consultant"}, l.devCSRF, http.StatusNoContent).Body.Close()
|
||||
}
|
||||
|
||||
func TestDeveloperOutsidePoolSeesNothing(t *testing.T) {
|
||||
l := newLifecycleStack(t, nil)
|
||||
base := l.ts.URL + "/api/v1/tasks/" + l.task.ID
|
||||
|
||||
Reference in New Issue
Block a user