package main // Every call a hook prints for a session to copy must be accepted by claude_code_wait's own argument check // (issue 1757): the Stop hook's suggested watched and said calls, and the SessionStart hook's suggested // adopt call. import ( "encoding/json" "strings" "testing" "time" ) // suggestedCall extracts the one JSON object embedded in a hook's message, right after "claude_code_wait " // — what a session is expected to copy verbatim — and parses it into the arguments a tool call takes. func suggestedCall(t *testing.T, msg string) map[string]any { t.Helper() i := strings.Index(msg, "claude_code_wait ") if i < 0 { t.Fatalf("no suggested call in: %s", msg) } rest := msg[i+len("claude_code_wait "):] start := strings.Index(rest, "{") if start < 0 { t.Fatalf("no JSON object in: %s", msg) } depth, end := 0, -1 for j, c := range rest[start:] { switch c { case '{': depth++ case '}': depth-- if depth == 0 { end = start + j + 1 } } if end >= 0 { break } } if end < 0 { t.Fatalf("unbalanced JSON object in: %s", msg) } var args map[string]any if err := json.Unmarshal([]byte(rest[start:end]), &args); err != nil { t.Fatalf("the suggested call is not valid JSON: %v (%s)", err, rest[start:end]) } return args } // Issue 1757: the Stop hook's suggested watched call carries `by`, and the tool accepts it as printed. func TestTheStopHooksSuggestedWatchedCallIsAcceptedByTheTool(t *testing.T) { p, _ := node(t, "g14") store := newMemWaits() waiting = &Waiting{Store: store, Now: func() time.Time { return t0 }} t.Cleanup(func() { waiting = nil }) s := serve(t, p, &bus{kept: map[string]map[string]any{}}, memConfig{}) text, isErr := s.call(t, "claude_code_wait", map[string]any{"op": "add", "session": "verify-0489-test", "by": "fix-1757@g14", "what": "the operator's Approve", "read": map[string]any{"address": "mesh-delivery.recheck"}, "bound_minutes": 10}) if isErr { t.Fatalf("add: %s", text) } f, err := readWaitsFile(waitsFilePath(p, "verify-0489-test")) if err != nil || len(f.Waits) != 1 { t.Fatalf("%+v %v", f, err) } _, reason := waitBlocksStop("verify-0489-test", f.Waits[0], t0.Add(time.Minute)) if !strings.Contains(reason, "\"by\":\"fix-1757\"") { t.Fatalf("the suggested call does not carry by: %s", reason) } args := suggestedCall(t, reason) text, isErr = s.call(t, "claude_code_wait", args) if isErr { t.Fatalf("the suggested watched call, as printed, was refused: %s", text) } } // Issue 1757: the Stop hook's suggested said call (past its bound) carries `by` too. func TestTheStopHooksSuggestedSaidCallIsAcceptedByTheTool(t *testing.T) { p, _ := node(t, "g14") store := newMemWaits() clock := t0 waiting = &Waiting{Store: store, Now: func() time.Time { return clock }} t.Cleanup(func() { waiting = nil }) s := serve(t, p, &bus{kept: map[string]map[string]any{}}, memConfig{}) text, isErr := s.call(t, "claude_code_wait", map[string]any{"op": "add", "session": "verify-0489-test", "by": "fix-1757@g14", "what": "the operator's Approve", "read": map[string]any{"address": "mesh-delivery.recheck"}, "bound_minutes": 1}) if isErr { t.Fatalf("add: %s", text) } clock = t0.Add(2 * time.Minute) // past its 1-minute bound, for the live tool call too f, err := readWaitsFile(waitsFilePath(p, "verify-0489-test")) if err != nil || len(f.Waits) != 1 { t.Fatalf("%+v %v", f, err) } _, reason := waitBlocksStop("verify-0489-test", f.Waits[0], clock) if !strings.Contains(reason, "\"by\":\"fix-1757\"") { t.Fatalf("the suggested call does not carry by: %s", reason) } args := suggestedCall(t, reason) text, isErr = s.call(t, "claude_code_wait", args) if isErr { t.Fatalf("the suggested said call, as printed, was refused: %s", text) } } // Issue 1757: the SessionStart hook's suggested adopt call already carries `by` — checked, as asked, so a // regression here is caught the same way as the Stop hook's. func TestTheSessionStartHooksSuggestedAdoptCallIsAcceptedByTheTool(t *testing.T) { p, _ := node(t, "g14") store := newMemWaits() waiting = &Waiting{Store: store, Now: func() time.Time { return t0 }} t.Cleanup(func() { waiting = nil }) s := serve(t, p, &bus{kept: map[string]map[string]any{}}, memConfig{}) text, isErr := s.call(t, "claude_code_wait", map[string]any{"op": "add", "session": "sess-old", "by": "fix-1757@g14", "what": "the operator's Approve", "read": map[string]any{"address": "mesh-delivery.recheck"}, "bound_minutes": 10}) if isErr { t.Fatalf("add: %s", text) } msg := SessionStartMessage("sess-new", WaitsDirPath(p), t0.Add(time.Minute)) if !strings.Contains(msg, "\"by\":\"fix-1757@g14\"") { t.Fatalf("the suggested adopt call does not carry by: %s", msg) } args := suggestedCall(t, msg) text, isErr = s.call(t, "claude_code_wait", args) if isErr { t.Fatalf("the suggested adopt call, as printed, was refused: %s", text) } }