Skip to content

Commit 97f01fc

Browse files
julianknutsenclaude
andcommitted
Add wl reject command and restrict accept/reject to poster-only
Accept and reject now require the caller to be the original poster. Accept retains the existing self-accept guard (completer != acceptor). Reject reverts in_review → claimed by deleting the completion record, allowing the worker to re-submit via wl done. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 109c013 commit 97f01fc

File tree

11 files changed

+371
-18
lines changed

11 files changed

+371
-18
lines changed

cmd/wl/cmd_accept.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,10 @@ func acceptCompletion(store commons.WLCommonsStore, wantedID, rigHandle string,
133133
return nil, fmt.Errorf("wanted item %s is not in_review (status: %s)", wantedID, item.Status)
134134
}
135135

136+
if item.PostedBy != rigHandle {
137+
return nil, fmt.Errorf("only the poster can accept (posted by %q)", item.PostedBy)
138+
}
139+
136140
completion, err := store.QueryCompletion(wantedID)
137141
if err != nil {
138142
return nil, fmt.Errorf("querying completion: %w", err)

cmd/wl/cmd_accept_test.go

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ func TestValidateAcceptInputs(t *testing.T) {
6767
func TestAcceptCompletion_Success(t *testing.T) {
6868
t.Parallel()
6969
store := newFakeWLCommonsStore()
70-
_ = store.InsertWanted(&commons.WantedItem{ID: "w-abc", Title: "Fix bug"})
70+
_ = store.InsertWanted(&commons.WantedItem{ID: "w-abc", Title: "Fix bug", PostedBy: "reviewer-rig"})
7171
_ = store.ClaimWanted("w-abc", "worker-rig")
7272
_ = store.SubmitCompletion("c-test123", "w-abc", "worker-rig", "https://github.com/pr/1")
7373

@@ -131,7 +131,7 @@ func TestAcceptCompletion_NotFound(t *testing.T) {
131131
func TestAcceptCompletion_SelfAccept(t *testing.T) {
132132
t.Parallel()
133133
store := newFakeWLCommonsStore()
134-
_ = store.InsertWanted(&commons.WantedItem{ID: "w-abc", Title: "Fix bug"})
134+
_ = store.InsertWanted(&commons.WantedItem{ID: "w-abc", Title: "Fix bug", PostedBy: "my-rig"})
135135
_ = store.ClaimWanted("w-abc", "my-rig")
136136
_ = store.SubmitCompletion("c-test123", "w-abc", "my-rig", "evidence")
137137

@@ -144,10 +144,26 @@ func TestAcceptCompletion_SelfAccept(t *testing.T) {
144144
}
145145
}
146146

147+
func TestAcceptCompletion_NotPoster(t *testing.T) {
148+
t.Parallel()
149+
store := newFakeWLCommonsStore()
150+
_ = store.InsertWanted(&commons.WantedItem{ID: "w-abc", Title: "Fix bug", PostedBy: "poster-rig"})
151+
_ = store.ClaimWanted("w-abc", "worker-rig")
152+
_ = store.SubmitCompletion("c-test123", "w-abc", "worker-rig", "evidence")
153+
154+
_, err := acceptCompletion(store, "w-abc", "other-rig", 4, 3, "leaf", nil, "")
155+
if err == nil {
156+
t.Fatal("acceptCompletion() expected error for non-poster")
157+
}
158+
if !strings.Contains(err.Error(), "only the poster can accept") {
159+
t.Errorf("error = %q, want to contain 'only the poster can accept'", err.Error())
160+
}
161+
}
162+
147163
func TestAcceptCompletion_QueryCompletionError(t *testing.T) {
148164
t.Parallel()
149165
store := newFakeWLCommonsStore()
150-
_ = store.InsertWanted(&commons.WantedItem{ID: "w-abc", Title: "Fix bug", Status: "in_review"})
166+
_ = store.InsertWanted(&commons.WantedItem{ID: "w-abc", Title: "Fix bug", Status: "in_review", PostedBy: "reviewer-rig"})
151167
store.QueryCompletionErr = fmt.Errorf("completion query error")
152168

153169
_, err := acceptCompletion(store, "w-abc", "reviewer-rig", 4, 3, "leaf", nil, "")
@@ -162,7 +178,7 @@ func TestAcceptCompletion_QueryCompletionError(t *testing.T) {
162178
func TestAcceptCompletion_AcceptCompletionError(t *testing.T) {
163179
t.Parallel()
164180
store := newFakeWLCommonsStore()
165-
_ = store.InsertWanted(&commons.WantedItem{ID: "w-abc", Title: "Fix bug"})
181+
_ = store.InsertWanted(&commons.WantedItem{ID: "w-abc", Title: "Fix bug", PostedBy: "reviewer-rig"})
166182
_ = store.ClaimWanted("w-abc", "worker-rig")
167183
_ = store.SubmitCompletion("c-test123", "w-abc", "worker-rig", "evidence")
168184
store.AcceptCompletionErr = fmt.Errorf("accept store error")

cmd/wl/cmd_fake_test.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ type fakeWLCommonsStore struct {
2323
QueryCompletionErr error
2424
QueryStampErr error
2525
AcceptCompletionErr error
26+
RejectCompletionErr error
2627
UpdateWantedErr error
2728
DeleteWantedErr error
2829
}
@@ -192,6 +193,26 @@ func (f *fakeWLCommonsStore) AcceptCompletion(wantedID, _, rigHandle string, sta
192193
return nil
193194
}
194195

196+
func (f *fakeWLCommonsStore) RejectCompletion(wantedID, _, _ string) error {
197+
if f.RejectCompletionErr != nil {
198+
return f.RejectCompletionErr
199+
}
200+
201+
f.mu.Lock()
202+
defer f.mu.Unlock()
203+
204+
item, ok := f.items[wantedID]
205+
if !ok {
206+
return fmt.Errorf("wanted item %q not found", wantedID)
207+
}
208+
if item.Status != "in_review" {
209+
return fmt.Errorf("wanted item %q is not in_review (status: %s)", wantedID, item.Status)
210+
}
211+
item.Status = "claimed"
212+
delete(f.completions, wantedID)
213+
return nil
214+
}
215+
195216
func (f *fakeWLCommonsStore) UpdateWanted(wantedID string, fields *commons.WantedUpdate) error {
196217
if f.UpdateWantedErr != nil {
197218
return f.UpdateWantedErr

cmd/wl/cmd_reject.go

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"io"
6+
7+
"github.com/spf13/cobra"
8+
"github.com/steveyegge/wasteland/internal/commons"
9+
"github.com/steveyegge/wasteland/internal/style"
10+
)
11+
12+
func newRejectCmd(stdout, stderr io.Writer) *cobra.Command {
13+
var (
14+
reason string
15+
noPush bool
16+
)
17+
18+
cmd := &cobra.Command{
19+
Use: "reject <wanted-id>",
20+
Short: "Reject a completed wanted item back to claimed",
21+
Long: `Reject a completed wanted item, reverting it from 'in_review' to 'claimed'.
22+
23+
The item must be in 'in_review' status. Only the poster can reject.
24+
The completion record is deleted so the claimer can re-submit.
25+
26+
In wild-west mode the commit is auto-pushed to upstream and origin.
27+
Use --no-push to skip pushing (offline work).
28+
29+
Examples:
30+
wl reject w-abc123
31+
wl reject w-abc123 --reason "tests failing"`,
32+
Args: cobra.ExactArgs(1),
33+
RunE: func(cmd *cobra.Command, args []string) error {
34+
return runReject(cmd, stdout, stderr, args[0], reason, noPush)
35+
},
36+
}
37+
38+
cmd.Flags().StringVar(&reason, "reason", "", "Reason for rejection (included in commit message)")
39+
cmd.Flags().BoolVar(&noPush, "no-push", false, "Skip pushing to remotes (offline work)")
40+
41+
return cmd
42+
}
43+
44+
func runReject(cmd *cobra.Command, stdout, _ io.Writer, wantedID, reason string, noPush bool) error {
45+
wlCfg, err := resolveWasteland(cmd)
46+
if err != nil {
47+
return fmt.Errorf("loading wasteland config: %w", err)
48+
}
49+
rigHandle := wlCfg.RigHandle
50+
51+
store := commons.NewWLCommons(wlCfg.LocalDir)
52+
53+
if err := rejectCompletion(store, wantedID, rigHandle, reason); err != nil {
54+
return err
55+
}
56+
57+
fmt.Fprintf(stdout, "%s Rejected %s\n", style.Bold.Render("✓"), wantedID)
58+
if reason != "" {
59+
fmt.Fprintf(stdout, " Reason: %s\n", reason)
60+
}
61+
fmt.Fprintf(stdout, " Status: claimed\n")
62+
63+
if !noPush {
64+
_ = commons.PushWithSync(wlCfg.LocalDir, stdout)
65+
}
66+
67+
return nil
68+
}
69+
70+
// rejectCompletion contains the testable business logic for rejecting a completion.
71+
func rejectCompletion(store commons.WLCommonsStore, wantedID, rigHandle, reason string) error {
72+
item, err := store.QueryWanted(wantedID)
73+
if err != nil {
74+
return fmt.Errorf("querying wanted item: %w", err)
75+
}
76+
77+
if item.Status != "in_review" {
78+
return fmt.Errorf("wanted item %s is not in_review (status: %s)", wantedID, item.Status)
79+
}
80+
81+
if item.PostedBy != rigHandle {
82+
return fmt.Errorf("only the poster can reject (posted by %q)", item.PostedBy)
83+
}
84+
85+
if err := store.RejectCompletion(wantedID, rigHandle, reason); err != nil {
86+
return fmt.Errorf("rejecting completion: %w", err)
87+
}
88+
89+
return nil
90+
}

cmd/wl/cmd_reject_test.go

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"strings"
6+
"testing"
7+
8+
"github.com/steveyegge/wasteland/internal/commons"
9+
)
10+
11+
func TestRejectCompletion_Success(t *testing.T) {
12+
t.Parallel()
13+
store := newFakeWLCommonsStore()
14+
_ = store.InsertWanted(&commons.WantedItem{ID: "w-abc", Title: "Fix bug", PostedBy: "poster-rig"})
15+
_ = store.ClaimWanted("w-abc", "worker-rig")
16+
_ = store.SubmitCompletion("c-test123", "w-abc", "worker-rig", "https://github.com/pr/1")
17+
18+
err := rejectCompletion(store, "w-abc", "poster-rig", "tests failing")
19+
if err != nil {
20+
t.Fatalf("rejectCompletion() error: %v", err)
21+
}
22+
23+
item, _ := store.QueryWanted("w-abc")
24+
if item.Status != "claimed" {
25+
t.Errorf("Status = %q, want %q", item.Status, "claimed")
26+
}
27+
28+
// Completion should be deleted.
29+
_, err = store.QueryCompletion("w-abc")
30+
if err == nil {
31+
t.Error("expected completion to be deleted after reject")
32+
}
33+
}
34+
35+
func TestRejectCompletion_NotInReview(t *testing.T) {
36+
t.Parallel()
37+
store := newFakeWLCommonsStore()
38+
_ = store.InsertWanted(&commons.WantedItem{ID: "w-abc", Title: "Fix bug", PostedBy: "poster-rig"})
39+
40+
err := rejectCompletion(store, "w-abc", "poster-rig", "")
41+
if err == nil {
42+
t.Fatal("rejectCompletion() expected error for non-in_review item")
43+
}
44+
if !strings.Contains(err.Error(), "not in_review") {
45+
t.Errorf("error = %q, want to contain 'not in_review'", err.Error())
46+
}
47+
}
48+
49+
func TestRejectCompletion_NotFound(t *testing.T) {
50+
t.Parallel()
51+
store := newFakeWLCommonsStore()
52+
53+
err := rejectCompletion(store, "w-nonexistent", "poster-rig", "")
54+
if err == nil {
55+
t.Fatal("rejectCompletion() expected error for missing item")
56+
}
57+
}
58+
59+
func TestRejectCompletion_NotPoster(t *testing.T) {
60+
t.Parallel()
61+
store := newFakeWLCommonsStore()
62+
_ = store.InsertWanted(&commons.WantedItem{ID: "w-abc", Title: "Fix bug", PostedBy: "poster-rig"})
63+
_ = store.ClaimWanted("w-abc", "worker-rig")
64+
_ = store.SubmitCompletion("c-test123", "w-abc", "worker-rig", "evidence")
65+
66+
err := rejectCompletion(store, "w-abc", "other-rig", "bad work")
67+
if err == nil {
68+
t.Fatal("rejectCompletion() expected error for non-poster")
69+
}
70+
if !strings.Contains(err.Error(), "only the poster can reject") {
71+
t.Errorf("error = %q, want to contain 'only the poster can reject'", err.Error())
72+
}
73+
}
74+
75+
func TestRejectCompletion_RejectError(t *testing.T) {
76+
t.Parallel()
77+
store := newFakeWLCommonsStore()
78+
_ = store.InsertWanted(&commons.WantedItem{ID: "w-abc", Title: "Fix bug", PostedBy: "poster-rig"})
79+
_ = store.ClaimWanted("w-abc", "worker-rig")
80+
_ = store.SubmitCompletion("c-test123", "w-abc", "worker-rig", "evidence")
81+
store.RejectCompletionErr = fmt.Errorf("reject store error")
82+
83+
err := rejectCompletion(store, "w-abc", "poster-rig", "reason")
84+
if err == nil {
85+
t.Fatal("rejectCompletion() expected error when RejectCompletion fails")
86+
}
87+
if !strings.Contains(err.Error(), "reject store error") {
88+
t.Errorf("error = %q, want to contain 'reject store error'", err.Error())
89+
}
90+
}

cmd/wl/main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ func newRootCmd(stdout, stderr io.Writer) *cobra.Command {
6868
newClaimCmd(stdout, stderr),
6969
newDoneCmd(stdout, stderr),
7070
newAcceptCmd(stdout, stderr),
71+
newRejectCmd(stdout, stderr),
7172
newUpdateCmd(stdout, stderr),
7273
newDeleteCmd(stdout, stderr),
7374
newBrowseCmd(stdout, stderr),

cmd/wl/main_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ func TestSubcommandRegistration(t *testing.T) {
2828
var stdout, stderr bytes.Buffer
2929
root := newRootCmd(&stdout, &stderr)
3030

31-
expected := []string{"join", "post", "claim", "done", "accept", "update", "delete", "browse", "status", "sync", "leave", "list", "version"}
31+
expected := []string{"join", "post", "claim", "done", "accept", "reject", "update", "delete", "browse", "status", "sync", "leave", "list", "version"}
3232
for _, name := range expected {
3333
found := false
3434
for _, c := range root.Commands() {

cmd/wl/testdata/errors.txtar

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,14 @@ stderr 'invalid severity'
8282
! exec wl accept w-abc --quality 3
8383
stderr 'not joined'
8484

85+
# reject with no args.
86+
! exec wl reject
87+
stderr 'accepts 1 arg'
88+
89+
# reject not joined.
90+
! exec wl reject w-abc
91+
stderr 'not joined'
92+
8593
# update with no args.
8694
! exec wl update
8795
stderr 'accepts 1 arg'

internal/commons/commons.go

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ type WLCommonsStore interface {
2525
QueryCompletion(wantedID string) (*CompletionRecord, error)
2626
QueryStamp(stampID string) (*Stamp, error)
2727
AcceptCompletion(wantedID, completionID, rigHandle string, stamp *Stamp) error
28+
RejectCompletion(wantedID, rigHandle, reason string) error
2829
UpdateWanted(wantedID string, fields *WantedUpdate) error
2930
DeleteWanted(wantedID string) error
3031
}
@@ -81,6 +82,11 @@ func (w *WLCommons) UpdateWanted(wantedID string, fields *WantedUpdate) error {
8182
return UpdateWanted(w.dbDir, wantedID, fields)
8283
}
8384

85+
// RejectCompletion reverts a wanted item from in_review to claimed.
86+
func (w *WLCommons) RejectCompletion(wantedID, rigHandle, reason string) error {
87+
return RejectCompletion(w.dbDir, wantedID, rigHandle, reason)
88+
}
89+
8490
// DeleteWanted soft-deletes a wanted item by setting status=withdrawn.
8591
func (w *WLCommons) DeleteWanted(wantedID string) error {
8692
return DeleteWanted(w.dbDir, wantedID)
@@ -277,7 +283,7 @@ CALL DOLT_COMMIT('-m', 'wl done: %s');
277283
// QueryWanted fetches a wanted item by ID. Returns an error if not found.
278284
// dbDir is the actual database directory.
279285
func QueryWanted(dbDir, wantedID string) (*WantedItem, error) {
280-
query := fmt.Sprintf(`SELECT id, title, status, COALESCE(claimed_by, '') as claimed_by FROM wanted WHERE id='%s';`,
286+
query := fmt.Sprintf(`SELECT id, title, status, COALESCE(claimed_by, '') as claimed_by, COALESCE(posted_by, '') as posted_by FROM wanted WHERE id='%s';`,
281287
EscapeSQL(wantedID))
282288

283289
output, err := doltSQLQuery(dbDir, query)
@@ -296,6 +302,7 @@ func QueryWanted(dbDir, wantedID string) (*WantedItem, error) {
296302
Title: row["title"],
297303
Status: row["status"],
298304
ClaimedBy: row["claimed_by"],
305+
PostedBy: row["posted_by"],
299306
}
300307
return item, nil
301308
}
@@ -583,3 +590,28 @@ CALL DOLT_COMMIT('-m', 'wl delete: %s');
583590
}
584591
return fmt.Errorf("delete failed: %w", err)
585592
}
593+
594+
// RejectCompletion reverts a wanted item from in_review to claimed and deletes
595+
// the completion record. The reason is embedded in the dolt commit message.
596+
// dbDir is the actual database directory.
597+
func RejectCompletion(dbDir, wantedID, _, reason string) error {
598+
commitMsg := fmt.Sprintf("wl reject: %s", EscapeSQL(wantedID))
599+
if reason != "" {
600+
commitMsg += " — " + EscapeSQL(reason)
601+
}
602+
603+
script := fmt.Sprintf(`DELETE FROM completions WHERE wanted_id='%s';
604+
UPDATE wanted SET status='claimed', updated_at=NOW() WHERE id='%s' AND status='in_review';
605+
CALL DOLT_ADD('-A');
606+
CALL DOLT_COMMIT('-m', '%s');
607+
`, EscapeSQL(wantedID), EscapeSQL(wantedID), commitMsg)
608+
609+
err := doltSQLScript(dbDir, script)
610+
if err == nil {
611+
return nil
612+
}
613+
if isNothingToCommit(err) {
614+
return fmt.Errorf("wanted item %q is not in_review or does not exist", wantedID)
615+
}
616+
return fmt.Errorf("reject failed: %w", err)
617+
}

internal/commons/commons_fake_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,10 @@ func (f *fakeWLCommonsStore) AcceptCompletion(_, _, _ string, _ *Stamp) error {
116116
return fmt.Errorf("not implemented in commons fake")
117117
}
118118

119+
func (f *fakeWLCommonsStore) RejectCompletion(_, _, _ string) error {
120+
return fmt.Errorf("not implemented in commons fake")
121+
}
122+
119123
func (f *fakeWLCommonsStore) UpdateWanted(_ string, _ *WantedUpdate) error {
120124
return fmt.Errorf("not implemented in commons fake")
121125
}

0 commit comments

Comments
 (0)