#!/usr/bin/env bash
# PR #25136 (fix(web): keep earlier PR selections and findings in the composer), a draft.
# Claim: already-fixed. #24990 (merged, closed the same issue #24960) made every pull request handoff
# append to the composer draft through the stateless appendHandoff.
# Behavioral proof: a focused test that walks each behavior #25136 claims (selections and findings pile
# up, cross-repo context, restored drafts, refreshed findings, single requests) passes on current main.
# Needs deps installed (`vp i`) and `gh` authenticated. Run from the root of a clean t3code checkout.
set -euo pipefail

REPO=pingdotgg/t3code
ok=1
pass() { echo "PASS  $*"; }
fail() { echo "FAIL  $*"; ok=0; }

if [ -n "$(git status --porcelain)" ]; then
  echo "refusing to run on a dirty tree"; exit 2
fi
ORIG=$(git symbolic-ref -q --short HEAD || git rev-parse HEAD)
WORK=$(mktemp -d)
TEST=apps/web/src/components/pullRequest/prHandoffs25136.logic.test.ts
cleanup() {
  rm -f "$TEST"
  git checkout -q -f -- . 2>/dev/null || true
  git checkout -q -f "$ORIG"
  rm -rf "$WORK"
}
trap 'rc=$?; cleanup; [ "${PROOF_DONE:-0}" = 1 ] && exit 0; [ "$rc" -ne 0 ] && exit "$rc"; exit 1' EXIT

git fetch -q origin main

MAIN=$(git rev-parse FETCH_HEAD)
git fetch -q origin pull/25136/head
PR_HEAD=$(git rev-parse FETCH_HEAD)
MB=$(git merge-base "$MAIN" "$PR_HEAD")
echo "main=$MAIN pr_head=$PR_HEAD merge_base=$MB"

# 1. The superseding PR is on main and closed the linked issue.
sha=$(git log "$MAIN" --format=%h --grep="(#24990)" | head -1)
if [ -n "$sha" ]; then pass "#24990 is on main as $sha ($(git log -1 --format=%s "$sha"))"
else fail "#24990 not found on main"; fi
closer=$(gh api graphql -f query='query{repository(owner:"pingdotgg",name:"t3code"){issue(number:24960){state stateReason timelineItems(itemTypes:[CLOSED_EVENT],last:1){nodes{... on ClosedEvent{closer{... on PullRequest{number merged}}}}}}}}' \
  --jq '.data.repository.issue | "\(.state) \(.stateReason) \(.timelineItems.nodes[0].closer.number) \(.timelineItems.nodes[0].closer.merged)"')
if [ "$closer" = "CLOSED COMPLETED 24990 true" ]; then
  pass "linked issue #24960 was closed as completed by merged PR #24990"
else
  fail "issue #24960 state/closer is '$closer'"
fi
if git merge-base --is-ancestor "$sha" "$PR_HEAD"; then
  fail "#25136 already contains #24990 (unexpected)"
else
  pass "#25136 was branched before #24990 and does not contain it"
fi

# 2. Each behavior #25136 claims holds on main, through main's handoff API.
git checkout -q --detach "$MAIN"
cat > "$TEST" <<'TS'
// Proof for PR #25136: each behavior it claims, exercised through main's handoff API.
import type { PullRequestReviewThread } from "@t3tools/contracts";
import { describe, expect, it } from "vite-plus/test";
import type { ReviewCommentContext } from "~/reviewCommentContext";

import {
  appendHandoff,
  buildAddSelectionToAgentHandoff,
  buildExplainPullRequestHandoff,
  buildFixFindingHandoff,
} from "./pullRequestDetail.logic";

const pr = {
  number: 42,
  title: "Add the pull requests page",
  url: "https://github.com/pingdotgg/t3code/pull/42",
  headBranch: "feat/page",
  baseBranch: "main",
  state: "open" as const,
  isDraft: false,
};
const empty = { prompt: "", reviewComments: [] as ReviewCommentContext[] };
const selection = (id: string, start: number): ReviewCommentContext => ({
  id,
  sectionId: "pull-request:42",
  sectionTitle: "PR #42 review",
  filePath: "apps/web/src/page.tsx",
  startIndex: start,
  endIndex: start + 2,
  rangeLabel: `L${start + 1}-L${start + 3}`,
  text: "",
  diff: "+const answer = 42;",
});
const thread = (id: string, bodies: string[]): PullRequestReviewThread => ({
  id,
  path: "apps/web/src/page.tsx",
  line: 12,
  side: "right",
  isResolved: false,
  isOutdated: false,
  comments: bodies.map((body, index) => ({
    id: `${id}-c${index}`,
    author: { login: "reviewer", name: null, avatarUrl: null },
    body,
    createdAt: "2026-07-03T00:00:00Z",
    url: null,
  })),
});
const addSelection = (comment: ReviewCommentContext, request: string, url = pr.url) =>
  buildAddSelectionToAgentHandoff({ ...pr, url, comment, request });
const fix = (t: PullRequestReviewThread) =>
  buildFixFindingHandoff({ ...pr, finding: { kind: "thread", thread: t } });
const ids = (draft: { reviewComments: ReadonlyArray<ReviewCommentContext> }) =>
  draft.reviewComments.map((comment) => comment.id);

describe("PR #25136 claims on main", () => {
  it("keeps earlier selections when more selections are added to the agent", () => {
    const first = appendHandoff(empty, addSelection(selection("pull-request-selection:a:1:3", 0), "why this?"));
    const second = appendHandoff(first, addSelection(selection("pull-request-selection:a:8:9", 7), "and this?"));
    expect(second.prompt).toContain("why this?");
    expect(second.prompt).toContain("and this?");
    expect(ids(second)).toEqual([
      `pull-request-context:${pr.url}`,
      "pull-request-selection:a:1:3",
      "pull-request-selection:a:8:9",
    ]);
  });

  it("piles up selections and findings in the order they were added", () => {
    let draft = appendHandoff(empty, addSelection(selection("pull-request-selection:a:1:3", 0), "why this?"));
    draft = appendHandoff(draft, fix(thread("t1", ["rename the helper"])));
    draft = appendHandoff(draft, fix(thread("t2", ["add a test"])));
    expect(ids(draft)).toEqual([
      `pull-request-context:${pr.url}`,
      "pull-request-selection:a:1:3",
      "pull-request-finding:t1",
      "pull-request-finding:t2",
    ]);
    expect(draft.prompt).toContain("why this?");
    expect(draft.prompt).toContain("Fix the review finding attached to this message.");
  });

  it("keeps the same pull request number from another repository", () => {
    const otherUrl = "https://github.com/acme/other/pull/42";
    const draft = appendHandoff(
      appendHandoff(empty, addSelection(selection("pull-request-selection:a:1:3", 0), "why?")),
      addSelection(selection("pull-request-selection:b:1:3", 0), "and here?", otherUrl),
    );
    expect(ids(draft)).toContain(`pull-request-context:${pr.url}`);
    expect(ids(draft)).toContain(`pull-request-context:${otherUrl}`);
  });

  it("keeps a selection added before a reload when another is added after it", () => {
    const before = appendHandoff(empty, addSelection(selection("pull-request-selection:a:1:3", 0), "why this?"));
    // A reload restores only the persisted draft; main keeps no in-memory handoff state.
    const restored = JSON.parse(JSON.stringify(before)) as typeof before;
    const after = appendHandoff(restored, addSelection(selection("pull-request-selection:a:8:9", 7), "and this?"));
    expect(ids(after)).toContain("pull-request-selection:a:1:3");
    expect(ids(after)).toContain("pull-request-selection:a:8:9");
    expect(after.prompt).toContain("why this?");
  });

  it("hands a finding's instructions once and sends its latest comments when it is fixed again", () => {
    const once = appendHandoff(empty, fix(thread("t1", ["rename the helper"])));
    const twice = appendHandoff(once, fix(thread("t1", ["rename the helper", "also the export"])));
    expect(twice.prompt.split("Fix the review finding attached to this message.")).toHaveLength(2);
    const chip = twice.reviewComments.find((comment) => comment.id === "pull-request-finding:t1");
    expect(chip?.text).toContain("also the export");
    expect(ids(twice).filter((id) => id === "pull-request-finding:t1")).toHaveLength(1);
  });

  it("adds a new request about the same selection without duplicating its chip", () => {
    const sel = selection("pull-request-selection:a:1:3", 0);
    const draft = appendHandoff(appendHandoff(empty, addSelection(sel, "why this?")), addSelection(sel, "is it tested?"));
    expect(draft.prompt).toContain("why this?");
    expect(draft.prompt).toContain("is it tested?");
    expect(ids(draft)).toEqual([`pull-request-context:${pr.url}`, "pull-request-selection:a:1:3"]);
  });

  it("drops an explanation's do-not-change-code instructions when a finding is fixed next", () => {
    const explained = appendHandoff(empty, buildExplainPullRequestHandoff(pr));
    expect(explained.reviewComments.some((comment) => comment.text.includes("Do not change any code"))).toBe(true);
    const fixed = appendHandoff(explained, fix(thread("t1", ["rename the helper"])));
    expect(fixed.reviewComments.some((comment) => comment.text.includes("Do not change any code"))).toBe(false);
    expect(fixed.prompt).not.toContain("Do not change any code");
  });

  it("keeps what the reader typed around the handoffs", () => {
    const typed = { prompt: "My own note.", reviewComments: [] as ReviewCommentContext[] };
    const draft = appendHandoff(typed, addSelection(selection("pull-request-selection:a:1:3", 0), "why this?"));
    expect(draft.prompt.startsWith("My own note.")).toBe(true);
    expect(draft.prompt).toContain("why this?");
  });
});
TS
vp test run --reporter=verbose "$TEST" > "$WORK/main.log" 2>&1 || true
grep -E "^ +(✓|×)|Tests +[0-9]" "$WORK/main.log" || true
if grep -qE "Tests +8 passed" "$WORK/main.log" && ! grep -qE "Tests +.*[0-9]+ failed" "$WORK/main.log"; then
  pass "all 8 claimed behaviors pass on main ($MAIN) without the PR's source changes"
else
  fail "a claimed behavior fails on main"
fi

if [ "$ok" = 1 ]; then
  echo "VERDICT: CLOSE (already-fixed) - #24990 already keeps earlier PR selections and findings in the composer"
  PROOF_DONE=1; exit 0
fi
echo "VERDICT: claim not supported"
exit 1
