Slice 2: the §8 active-RFC view in full
Per the §19.1 brief: the three-column shape (§8.1) opens on main
in discuss mode (§8.2), supports the §8.3 discuss-vs-contribute
flip on non-main branches, hosts §8.4's per-branch chat with AI
participation (§18's <change> protocol → §8.14 changes rows), the
§8.8 change-card panel with §8.9 accept/decline/edit-before-accept,
the §8.10 tracked-change markup + DiffView toggle, the §8.11
manual-edit flushes with the stale-change mechanic, the §8.12
range and paragraph sub-threads, the §8.13 flag affordance, and
the §8.14 discuss-mode buffer.
Backend: bot.py grew per-RFC-repo write ops (cut_branch_from_main,
commit_accepted_change with the structured original/proposed/reason
body and Change-Id + Source-Message-Id + On-behalf-of trailers,
commit_manual_flush, ensure_rfc_repo_seed). cache.py grew
refresh_rfc_repo and the webhook dispatches on repository.full_name.
providers.py and chat.py port the §18 carryovers — multi-provider
LLM abstraction and SSE-streaming chat against the §5 threads /
thread_messages / changes schema. api_branches.py mounts the §17
branches/<branch>/* and threads/<thread_id>/* routes with the §6
/ §11 permission checks inline.
Frontend: RFCView.jsx rebuilt as the §8 surface; Editor.jsx,
ChatPanel.jsx, ChangePanel.jsx, PromptBar.jsx, SelectionTooltip.jsx,
DiffView.jsx, ModelPicker.jsx, modelStyles.js lifted from the
prototype and adapted to the canonical schema.
Covered by `backend/tests/test_rfc_view_vertical.py` — eleven new
integration tests against an extended FakeGitea (PUT contents,
POST orgs/{org}/repos, seed_rfc_repo): main-view read,
promote-to-branch, accept (with and without edit-before-accept),
decline, manual flush + system message, flag creation, visibility
flip, anonymous read-but-no-contribute, stale-change refusal, and
the chat-streaming path with a fake provider injected. The 5
Slice 1 tests continue to pass alongside.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -34,22 +34,38 @@ import pytest
|
||||
|
||||
|
||||
class FakeGitea:
|
||||
"""A narrow in-memory simulation of the Gitea API the slice uses."""
|
||||
"""A narrow in-memory simulation of the Gitea API the slices exercise.
|
||||
|
||||
Slice 2 extends the seam to cover per-RFC repos: PUT contents
|
||||
(update file), POST orgs/{org}/repos (create repo), and branch
|
||||
listing with commit timestamps. The simulator is intentionally
|
||||
minimal — only the routes the production paths actually call.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
# files: (owner, repo, branch, path) -> {"content": str, "sha": str}
|
||||
self.files: dict[tuple[str, str, str, str], dict] = {}
|
||||
# branches: (owner, repo) -> {branch_name -> {"sha": str}}
|
||||
# branches: (owner, repo) -> {branch_name -> {"sha": str, "ts": str}}
|
||||
self.branches: dict[tuple[str, str], dict[str, dict]] = {}
|
||||
# pulls: (owner, repo) -> list[pull-dict]
|
||||
self.pulls: dict[tuple[str, str], list[dict]] = {}
|
||||
# repos: set of (owner, repo)
|
||||
self.repos: set[tuple[str, str]] = set()
|
||||
self._pr_counter = 0
|
||||
self._commit_counter = 0
|
||||
self._seed_repo("wiggleverse", "meta")
|
||||
|
||||
def _seed_repo(self, owner, repo):
|
||||
self.branches[(owner, repo)] = {"main": {"sha": "initial"}}
|
||||
self.branches[(owner, repo)] = {"main": {"sha": "initial", "ts": "2026-05-23T00:00:00Z"}}
|
||||
self.pulls[(owner, repo)] = []
|
||||
self.repos.add((owner, repo))
|
||||
|
||||
def seed_rfc_repo(self, owner, repo, *, rfc_md_body):
|
||||
"""Convenience: seed a per-RFC repo with an RFC.md on main."""
|
||||
self._seed_repo(owner, repo)
|
||||
sha = self._next_sha()
|
||||
self.files[(owner, repo, "main", "RFC.md")] = {"content": rfc_md_body, "sha": sha}
|
||||
self.branches[(owner, repo)]["main"] = {"sha": sha, "ts": "2026-05-23T00:00:00Z"}
|
||||
|
||||
def _next_sha(self):
|
||||
self._commit_counter += 1
|
||||
@@ -62,8 +78,29 @@ class FakeGitea:
|
||||
payload = json.loads(body) if body else {}
|
||||
|
||||
# GET /repos/{owner}/{repo}
|
||||
if method == "GET" and re.fullmatch(r"/repos/[^/]+/[^/]+", path):
|
||||
return httpx.Response(200, json={"name": path.split("/")[-1]})
|
||||
m_repo = re.fullmatch(r"/repos/([^/]+)/([^/]+)", path)
|
||||
if method == "GET" and m_repo:
|
||||
owner, repo = m_repo.groups()
|
||||
if (owner, repo) in self.repos:
|
||||
return httpx.Response(200, json={"name": repo, "full_name": f"{owner}/{repo}"})
|
||||
return httpx.Response(404, json={"message": "not found"})
|
||||
|
||||
# POST /orgs/{org}/repos
|
||||
m = re.fullmatch(r"/orgs/([^/]+)/repos", path)
|
||||
if method == "POST" and m:
|
||||
org = m.group(1)
|
||||
name = payload["name"]
|
||||
self._seed_repo(org, name)
|
||||
return httpx.Response(201, json={"name": name, "full_name": f"{org}/{name}"})
|
||||
|
||||
# GET /repos/{owner}/{repo}/branches (list)
|
||||
m = re.fullmatch(r"/repos/([^/]+)/([^/]+)/branches", path)
|
||||
if method == "GET" and m:
|
||||
owner, repo = m.groups()
|
||||
items = []
|
||||
for name, b in self.branches.get((owner, repo), {}).items():
|
||||
items.append({"name": name, "commit": {"id": b["sha"], "timestamp": b.get("ts")}})
|
||||
return httpx.Response(200, json=items)
|
||||
|
||||
# GET /repos/{owner}/{repo}/branches/{branch}
|
||||
m = re.fullmatch(r"/repos/([^/]+)/([^/]+)/branches/([^/]+)", path)
|
||||
@@ -126,9 +163,20 @@ class FakeGitea:
|
||||
content = base64.b64decode(payload["content"]).decode()
|
||||
sha = self._next_sha()
|
||||
self.files[(owner, repo, branch, fpath)] = {"content": content, "sha": sha}
|
||||
self.branches[(owner, repo)][branch]["sha"] = sha
|
||||
self.branches[(owner, repo)][branch] = {"sha": sha, "ts": "2026-05-23T00:00:00Z"}
|
||||
return httpx.Response(201, json={"commit": {"sha": sha}})
|
||||
|
||||
# PUT /repos/{owner}/{repo}/contents/{path} — update_file
|
||||
m = re.fullmatch(r"/repos/([^/]+)/([^/]+)/contents/(.+)", path)
|
||||
if method == "PUT" and m:
|
||||
owner, repo, fpath = m.groups()
|
||||
branch = payload["branch"]
|
||||
content = base64.b64decode(payload["content"]).decode()
|
||||
sha = self._next_sha()
|
||||
self.files[(owner, repo, branch, fpath)] = {"content": content, "sha": sha}
|
||||
self.branches[(owner, repo)][branch] = {"sha": sha, "ts": "2026-05-23T00:00:00Z"}
|
||||
return httpx.Response(200, json={"commit": {"sha": sha}, "content": {"sha": sha}})
|
||||
|
||||
# GET /repos/{owner}/{repo}/pulls?state=...
|
||||
m = re.fullmatch(r"/repos/([^/]+)/([^/]+)/pulls", path)
|
||||
if method == "GET" and m:
|
||||
|
||||
Reference in New Issue
Block a user