diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..1659c75
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,54 @@
+# Contributing
+
+## Setup
+
+Python 3.12+ and Node 22+.
+
+```bash
+pip install -e ".[dev]"
+python -m playwright install chromium # optional; needed for UI screenshot tests
+cd ui && npm install && npm run build && cd ..
+loadpath --help
+```
+
+## Tests
+
+```bash
+pytest
+cd ui && npm test
+node --test desktop/*.test.mjs
+```
+
+CI installs Chromium and runs the full suite. The demo fixture is `fixtures/demo_monorepo`.
+
+## Screenshots
+
+README images live in `docs/screenshots/` and are produced by Playwright:
+
+```bash
+LOADPATH_SCREENSHOT_DIR=docs/screenshots python -m pytest tests/e2e/test_ui_screenshots.py
+```
+
+Do not commit a 3D screenshot from headless Chromium unless `[data-testid=graph-3d-canvas]` is present. This environment often falls back to “WebGL is unavailable.”
+
+## UI
+
+The React app in `ui/` is built into `src/loadpath/static/` and served by `loadpath serve`.
+
+```bash
+cd ui
+npm run build # writes into src/loadpath/static/
+npm run dev # Vite, proxies /api to the Django-shaped FastAPI server
+```
+
+If you change `ui/` and only run `loadpath serve`, you are looking at a stale bundle until you `npm run build`.
+
+## Desktop
+
+```bash
+pip install -e .
+cd ui && npm install && npm run build && cd ..
+cd desktop && npm install && npm start
+```
+
+Requires Python 3.12+ on `PATH` (`python` on Windows, `python3` elsewhere), or set `LOADPATH_PYTHON`.
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..3e616ab
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2026 Loadpath contributors
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/README.md b/README.md
index 83e86f4..78cef14 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,7 @@
# Loadpath
+[](https://github.com/Modsofthenation/PR-Reviewer/actions/workflows/ci.yml)
+
Review as **load-path inspection** on a Django + React architecture graph. Not another hunk-comment bot.
A change is a force. Loadpath traces where that force travels until it hits a sink — HTTP response, UI, Celery/Dramatiq job, migration, permission — then scores whether you have enough evidence to merge.
@@ -19,39 +21,93 @@ On the demo monorepo that path is:
plus the jobs the view enqueues (`send_invoice_email.delay`, `rebuild_ledger.send`).
+
+
+It is a local CLI, a desktop UI, and an MCP server you can point Cursor at. Not a SaaS. Tokens stay on the machine in `~/.loadpath/settings.json`.
+
## App
-`loadpath serve --port 7345` opens a local desktop-style UI: icon rail, labeled toolbar, merge-box confidence, and an inspectable impact graph. The same process hosts MCP at `/mcp` (OAuth). Tokens stay on the machine in `~/.loadpath/settings.json`. AI is used **only** for residual uncertainty the graph cannot close. Two dozen themes (Obsidian, Nord, Neon Noir, Synthwave, Phosphor, Sakura, Citrus, Paper, high-contrast, …) live in Settings and `localStorage`. Last repo, git range, and SCM slug are remembered the same way. Copy the markdown brief, or post **one** PR comment (updated in place) from the Review tab. Keyboard: `1`–`5` switches tabs. Outside Settings and Pull requests, `⌘`/`Ctrl`+`Enter` runs a review.
+`loadpath serve --port 7345` opens a local desktop-style UI: icon rail, labeled toolbar, merge-box confidence, and an inspectable impact graph. The same process hosts MCP at `/mcp` (OAuth). AI is used **only** for residual uncertainty the graph cannot close. Twenty-four themes live in Settings and `localStorage`. Last repo, git range, and SCM slug are remembered the same way. Copy the markdown brief, or post **one** PR comment (updated in place) from the Review tab. Keyboard: `1`–`5` switches tabs. Outside Settings and Pull requests, `⌘`/`Ctrl`+`Enter` runs a review.
+
+### Empty review
+
+Until a repo is indexed and a range is walked, Review is an onboarding card — not a blank graph.
+
+
### Review
Confidence brief, read-order, clusters, architecture findings on the impact path, residual list, and the subgraph for the git range. Review walks the **indexed** graph (incremental refresh by default).
-
+
+
+### Node inspector
+
+Click a node. The inspector answers *what is this, what feeds it, what does it call, what would break* — not a dump of the indexer row.
+
+
### Architecture
Index a repo first. The architecture tab is the full typed graph plus `loadpath.yml` contexts and rules — not a PR diff. Findings here are repo-wide; review then scopes them to the change.
-
+
### Impact graph
-Toggle **This review** (impact subgraph) vs **Indexed architecture** (the repo map). Dashed edges are inferred (URL/Zod overlap); solid edges are extracted or generated-client stitches.
+Toggle **This review** (impact subgraph) vs **Indexed architecture** (the repo map). Dashed edges are inferred (URL/Zod overlap); solid edges are extracted or generated-client stitches. 2D is the default; 3D is available when WebGL is.
+
+
-
+
### Pull requests
-GitHub and Bitbucket via API tokens from Settings. Pick a PR and jump to a branch-range review.
+GitHub and Bitbucket via API tokens from Settings. Pick a PR and jump to a branch-range review. The screenshot uses a fixture PR so the tab is not empty.
+
+
+
+### Repo explorer
-
+Browse the filesystem, pick a project root. Loadpath remembers recent workspaces.
+
+
### Settings
-GitHub / Bitbucket tokens; AI providers (Anthropic, OpenAI, Grok/xAI, DeepSeek, Cursor-compatible, Ollama). Residual analysis only — Loadpath does not comment every hunk.
+Appearance (all 24 themes), GitHub / Bitbucket tokens, and AI providers (Anthropic, OpenAI, Grok/xAI, DeepSeek, Cursor-compatible, Ollama). Residual analysis only — Loadpath does not comment every hunk.
+
+
+
+### MCP consent
+
+When Cursor (or another client) connects over HTTP MCP, Loadpath does not silently grant access. You get a local consent page: client name, Loadpath issuer URL, optional PIN. Approve or deny. Tokens stay in `~/.loadpath/oauth.json`.
-
+
+
+## Themes
+
+Default is **Obsidian**. Settings lists every palette; the shots below are the same Review screen under nine of them.
+
+
+
+Obsidian
 |
+Nord
 |
+Neon noir
 |
+
+
+Synthwave
 |
+Phosphor
 |
+Paper
 |
+
+
+Sakura
 |
+Citrus
 |
+High contrast
 |
+
+
+
+Also shipping: Solarized Dark/Light, Forest, Rose Pine, Midnight Amber, Volcano, Lavender, Aurora, Biolume, Carbon, Seafoam, Peach Fuzz, Cotton Candy, Clear Sky, Coral Reef. High contrast is a first-class theme, not an afterthought.
## Install
@@ -64,6 +120,8 @@ cd ui && npm install && npm run build && cd ..
loadpath --help
```
+See [CONTRIBUTING.md](CONTRIBUTING.md) for tests, screenshot regeneration, and the Electron app.
+
## Desktop app (Windows, macOS, Linux)
Electron wraps the same local app: it starts the Loadpath backend and opens it in a native window. Tokens still live in `~/.loadpath/settings.json`.
@@ -115,6 +173,29 @@ loadpath mcp
**Flow:** `index` builds the architecture graph → `architecture` shows contexts and rule hits on the whole repo → `review` walks that same graph for a git range. The app mirrors this: Index registers a workspace, Architecture inspects it, Review traces a change through it.
+The bundled demo checkout is [`fixtures/demo_monorepo`](fixtures/demo_monorepo) (Django billing API + React invoice UI). Copy it and make **two** commits so `HEAD~1` is a real range — the fixture itself is not a git repo, and a single commit leaves the default range empty.
+
+```bash
+cp -R fixtures/demo_monorepo /tmp/acme-billing
+cd /tmp/acme-billing
+git init -b main
+git add -A && git commit -m "baseline"
+# Same contract tweak the screenshots use:
+python3 - <<'PY'
+from pathlib import Path
+p = Path("backend/billing/serializers.py")
+p.write_text(p.read_text().replace(
+ 'fields = ["id", "customer_id", "total", "status"]',
+ 'fields = ["id", "customer_id", "total", "status"]\n extra_kwargs = {"total": {"required": True}}',
+))
+PY
+git add -A && git commit -m "tighten Invoice.total contract"
+loadpath index /tmp/acme-billing
+loadpath serve --open
+```
+
+Then point the UI at `/tmp/acme-billing`, or pick it from the repo explorer. `loadpath serve` always boots the app; it does not take a repo path. Default range is `HEAD~1`…`HEAD`.
+
## MCP (Cursor, Claude, ChatGPT, Gemini)
`loadpath serve` exposes Streamable HTTP MCP at `/mcp`, protected with OAuth 2.1 (PKCE, dynamic client registration, Client ID Metadata Documents). Cloud hosts need HTTPS; set `--public-url` to the public origin when tunneling. `--oauth-pin` adds a PIN on the consent page.
@@ -138,7 +219,7 @@ MCP URL: `https://your-tunnel.example/mcp` (or `http://127.0.0.1:7345/mcp` on th
}
```
-**Cursor / Claude / ChatGPT / Gemini (HTTP + OAuth)** — add that MCP URL in the host’s connectors. The first connect opens a consent page on the Loadpath machine. Tokens stay in `~/.loadpath/oauth.json`.
+**Cursor / Claude / ChatGPT / Gemini (HTTP + OAuth)** — add that MCP URL in the host’s connectors. The first connect opens a consent page on the Loadpath machine.
Tools: `list_workspaces`, `init_repo`, `index_repo`, `architecture`, `review`, `detect_repo`, `list_pull_requests`, `post_review_comment`. `review` returns the load-path brief (confidence, sinks, reviewers) — not hunk comments.
@@ -193,6 +274,8 @@ AST is enough for review. If you need live `_meta` (db_table, resolved relations
**Stitch (the moat):** OpenAPI from Spectacular/schema files first; generated clients (`generated/`, orval, openapi-typescript) as high-confidence `consumed_by_client`; fallback URL-template matching and serializer/Zod field overlap marked **inferred**.
+Frontend roots prefer `frontend/src`, `frontend`, `web/src`, `client/src`, `ui/src`, `src-ui/src` — not a Python package `src/` and not `docs` / docs-site trees. `app/` is a Django root candidate.
+
## Architecture rules (`loadpath.yml`)
| Rule | Meaning |
@@ -247,10 +330,14 @@ node --test desktop/*.test.mjs
| `tests/e2e/test_mcp_oauth.py` | OAuth metadata/DCR/PKCE, consent, CIMD, MCP `review` stays on the billing load path |
| `tests/e2e/test_index_architecture_flow.py` | index snapshot, review without index, review walking an existing graph |
| `tests/e2e/test_brokers_and_django.py` | Celery + Dramatiq sinks, actor-only PR, non-idempotent Dramatiq warning, destructive migration, cross-context blocker, boot overlay, management commands, beat/canvas |
-| `tests/e2e/test_ui_screenshots.py` | Playwright: Architecture, Review, Impact graph, Pull requests, Settings → `docs/screenshots/` |
+| `tests/e2e/test_ui_screenshots.py` | Playwright screenshots (tmpdir by default; set `LOADPATH_SCREENSHOT_DIR=docs/screenshots` to regenerate README assets) |
| `desktop/*.test.mjs` | Electron sidecar command, health-wait, and external-URL allowlist |
-CI installs Chromium and runs the full suite.
+To regenerate the README screenshots:
+
+```bash
+LOADPATH_SCREENSHOT_DIR=docs/screenshots python -m pytest tests/e2e/test_ui_screenshots.py
+```
## Demo fixture
@@ -259,3 +346,7 @@ CI installs Chromium and runs the full suite.
## What this is not
Not CodeRabbit (comments without a closed impact set). Not a CodeScene clone (we do not replace its hotspot maps; we only score churn/coupling on the load path). Not django-orm-lens (we do not boot an ER explorer; we reuse its N+1 / cascade / blast-radius heuristics inside the typed graph). Not a generic SCIP call graph. The product is review as load-path inspection.
+
+## License
+
+MIT. See [LICENSE](LICENSE). Vulnerability reports: [SECURITY.md](SECURITY.md).
diff --git a/SECURITY.md b/SECURITY.md
new file mode 100644
index 0000000..96ac4e2
--- /dev/null
+++ b/SECURITY.md
@@ -0,0 +1,7 @@
+# Security
+
+Please report vulnerabilities privately to the repository owner (`Modsofthenation` on GitHub). Do not open a public issue for unreleased security problems.
+
+Once this repository is public, enable [GitHub private vulnerability reporting](https://docs.github.com/code-security/security-advisories/working-with-repository-security-advisories/configuring-private-vulnerability-reporting-for-a-repository) so researchers can use Security Advisories. The `/security/advisories/new` form 404s until that setting is on.
+
+Tokens and OAuth state live on the machine that runs Loadpath (`~/.loadpath/`); treat that host as trusted.
diff --git a/docs/screenshots/architecture.png b/docs/screenshots/architecture.png
index dc07d57..cf41053 100644
Binary files a/docs/screenshots/architecture.png and b/docs/screenshots/architecture.png differ
diff --git a/docs/screenshots/explorer.png b/docs/screenshots/explorer.png
new file mode 100644
index 0000000..4951757
Binary files /dev/null and b/docs/screenshots/explorer.png differ
diff --git a/docs/screenshots/graph-architecture.png b/docs/screenshots/graph-architecture.png
new file mode 100644
index 0000000..6517f4f
Binary files /dev/null and b/docs/screenshots/graph-architecture.png differ
diff --git a/docs/screenshots/graph.png b/docs/screenshots/graph.png
index cf776d0..6f3d1de 100644
Binary files a/docs/screenshots/graph.png and b/docs/screenshots/graph.png differ
diff --git a/docs/screenshots/mcp-consent.png b/docs/screenshots/mcp-consent.png
new file mode 100644
index 0000000..202aa54
Binary files /dev/null and b/docs/screenshots/mcp-consent.png differ
diff --git a/docs/screenshots/pull-requests.png b/docs/screenshots/pull-requests.png
index d3df1d4..79349bd 100644
Binary files a/docs/screenshots/pull-requests.png and b/docs/screenshots/pull-requests.png differ
diff --git a/docs/screenshots/review-empty.png b/docs/screenshots/review-empty.png
new file mode 100644
index 0000000..c7f31fc
Binary files /dev/null and b/docs/screenshots/review-empty.png differ
diff --git a/docs/screenshots/review-inspector.png b/docs/screenshots/review-inspector.png
new file mode 100644
index 0000000..dfb65a5
Binary files /dev/null and b/docs/screenshots/review-inspector.png differ
diff --git a/docs/screenshots/review.png b/docs/screenshots/review.png
index 8aec122..326e7bb 100644
Binary files a/docs/screenshots/review.png and b/docs/screenshots/review.png differ
diff --git a/docs/screenshots/settings.png b/docs/screenshots/settings.png
index 310ceb2..54756ec 100644
Binary files a/docs/screenshots/settings.png and b/docs/screenshots/settings.png differ
diff --git a/docs/screenshots/theme-citrus.png b/docs/screenshots/theme-citrus.png
new file mode 100644
index 0000000..99b50ec
Binary files /dev/null and b/docs/screenshots/theme-citrus.png differ
diff --git a/docs/screenshots/theme-high-contrast.png b/docs/screenshots/theme-high-contrast.png
new file mode 100644
index 0000000..3a82332
Binary files /dev/null and b/docs/screenshots/theme-high-contrast.png differ
diff --git a/docs/screenshots/theme-neon-noir.png b/docs/screenshots/theme-neon-noir.png
new file mode 100644
index 0000000..6a4e3cd
Binary files /dev/null and b/docs/screenshots/theme-neon-noir.png differ
diff --git a/docs/screenshots/theme-nord.png b/docs/screenshots/theme-nord.png
new file mode 100644
index 0000000..a5e4779
Binary files /dev/null and b/docs/screenshots/theme-nord.png differ
diff --git a/docs/screenshots/theme-obsidian.png b/docs/screenshots/theme-obsidian.png
new file mode 100644
index 0000000..73c2c01
Binary files /dev/null and b/docs/screenshots/theme-obsidian.png differ
diff --git a/docs/screenshots/theme-paper.png b/docs/screenshots/theme-paper.png
new file mode 100644
index 0000000..5483077
Binary files /dev/null and b/docs/screenshots/theme-paper.png differ
diff --git a/docs/screenshots/theme-phosphor.png b/docs/screenshots/theme-phosphor.png
new file mode 100644
index 0000000..da1a2cd
Binary files /dev/null and b/docs/screenshots/theme-phosphor.png differ
diff --git a/docs/screenshots/theme-sakura.png b/docs/screenshots/theme-sakura.png
new file mode 100644
index 0000000..1f9949b
Binary files /dev/null and b/docs/screenshots/theme-sakura.png differ
diff --git a/docs/screenshots/theme-synthwave.png b/docs/screenshots/theme-synthwave.png
new file mode 100644
index 0000000..49e0da7
Binary files /dev/null and b/docs/screenshots/theme-synthwave.png differ
diff --git a/pyproject.toml b/pyproject.toml
index f696cba..98afc15 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -8,7 +8,14 @@ version = "0.1.0"
description = "Architecture-typed impact graphs for Django + React pull requests."
readme = "README.md"
requires-python = ">=3.12"
+license = { text = "MIT" }
authors = [{ name = "Loadpath" }]
+keywords = ["django", "react", "code-review", "architecture", "mcp"]
+classifiers = [
+ "License :: OSI Approved :: MIT License",
+ "Programming Language :: Python :: 3.12",
+ "Topic :: Software Development :: Quality Assurance",
+]
dependencies = [
"fastapi>=0.115.0",
"uvicorn[standard]>=0.32.0",
@@ -31,6 +38,11 @@ dev = [
"playwright>=1.49.0",
]
+[project.urls]
+Homepage = "https://github.com/Modsofthenation/PR-Reviewer"
+Repository = "https://github.com/Modsofthenation/PR-Reviewer"
+Issues = "https://github.com/Modsofthenation/PR-Reviewer/issues"
+
[project.scripts]
loadpath = "loadpath.cli:app"
diff --git a/tests/e2e/test_ui_screenshots.py b/tests/e2e/test_ui_screenshots.py
index 55cb392..a6bd1bb 100644
--- a/tests/e2e/test_ui_screenshots.py
+++ b/tests/e2e/test_ui_screenshots.py
@@ -1,10 +1,51 @@
from __future__ import annotations
import os
+import shutil
from pathlib import Path
import pytest
+THEME_SHOTS = (
+ "obsidian",
+ "nord",
+ "neon-noir",
+ "synthwave",
+ "phosphor",
+ "paper",
+ "sakura",
+ "citrus",
+ "high-contrast",
+)
+
+EXPECTED = (
+ "review-empty.png",
+ "review.png",
+ "review-inspector.png",
+ "architecture.png",
+ "graph.png",
+ "graph-architecture.png",
+ "pull-requests.png",
+ "settings.png",
+ "explorer.png",
+ "mcp-consent.png",
+ *(f"theme-{theme}.png" for theme in THEME_SHOTS),
+)
+
+
+def _wait_fonts(page) -> None:
+ page.wait_for_function("document.fonts ? document.fonts.status === 'loaded' : true", timeout=5_000)
+
+
+def _shot(page, dest: Path, name: str) -> None:
+ page.wait_for_timeout(400)
+ page.screenshot(path=str(dest / name), full_page=False)
+
+
+def _wait_graph(page) -> None:
+ page.locator(".react-flow__node").first.wait_for(timeout=15_000)
+ page.locator(".react-flow__edge").first.wait_for(timeout=15_000)
+
@pytest.mark.playwright
def test_ui_review_graph_prs_settings(live_app, tmp_path: Path, browser_page):
@@ -19,17 +60,33 @@ def test_ui_review_graph_prs_settings(live_app, tmp_path: Path, browser_page):
),
)
page.goto(base_url, wait_until="networkidle")
- try:
- page.wait_for_function("document.fonts ? document.fonts.status === 'loaded' : true", timeout=5_000)
- except Exception:
- pass
+ _wait_fonts(page)
assert page.get_by_test_id("rail").is_visible()
+ page.get_by_test_id("review-empty").wait_for()
+
+ dest = Path(os.environ.get("LOADPATH_SCREENSHOT_DIR", str(tmp_path / "shots")))
+ dest.mkdir(parents=True, exist_ok=True)
+
+ # Pretty path only when regenerating README assets. CI must not rmtree a
+ # shared /tmp directory that the demo instructions also use.
+ if os.environ.get("LOADPATH_SCREENSHOT_DIR"):
+ pretty = Path("/tmp/acme-billing")
+ if pretty.exists():
+ shutil.rmtree(pretty)
+ shutil.copytree(repo, pretty)
+ repo = pretty
+
+ _shot(page, dest, "review-empty.png")
page.get_by_test_id("repo-path").fill(str(repo))
page.get_by_test_id("base-ref").fill("HEAD~1")
page.get_by_test_id("head-ref").fill("HEAD")
- dest = Path(os.environ.get("LOADPATH_SCREENSHOT_DIR", str(tmp_path / "shots")))
- dest.mkdir(parents=True, exist_ok=True)
+ page.get_by_test_id("btn-browse-repo").click()
+ page.get_by_test_id("repo-explorer").wait_for()
+ page.get_by_test_id("explorer-entry").first.wait_for(timeout=10_000)
+ _shot(page, dest, "explorer.png")
+ page.get_by_test_id("explorer-cancel").click()
+ page.get_by_test_id("repo-explorer").wait_for(state="hidden")
with page.expect_response(
lambda r: "/api/index" in r.url and r.request.method == "POST",
@@ -40,10 +97,9 @@ def test_ui_review_graph_prs_settings(live_app, tmp_path: Path, browser_page):
pytest.fail(f"index API {pending_index.value.status}: {pending_index.value.text()}")
page.get_by_test_id("architecture-brief").wait_for(timeout=15_000)
page.get_by_test_id("architecture-brief").locator(".level").wait_for(timeout=15_000)
- page.locator(".react-flow__node").first.wait_for(timeout=15_000)
- page.locator(".react-flow__edge").first.wait_for(timeout=15_000)
+ _wait_graph(page)
page.wait_for_timeout(800)
- page.screenshot(path=str(dest / "architecture.png"), full_page=False)
+ _shot(page, dest, "architecture.png")
with page.expect_response(
lambda r: "/api/review" in r.url and r.request.method == "POST",
@@ -59,48 +115,80 @@ def test_ui_review_graph_prs_settings(live_app, tmp_path: Path, browser_page):
page.locator(".level").wait_for(timeout=15_000)
brief = page.get_by_test_id("brief").inner_text()
assert "MEDIUM" in brief or "LOW" in brief or "HIGH" in brief
- page.locator(".react-flow__node").first.wait_for(timeout=15_000)
- page.locator(".react-flow__edge").first.wait_for(timeout=15_000)
+ _wait_graph(page)
page.wait_for_timeout(800)
- page.screenshot(path=str(dest / "review.png"), full_page=False)
+ _shot(page, dest, "review.png")
+
+ page.locator(".react-flow__node").filter(has_text="InvoiceSerializer").first.click()
+ inspector = page.get_by_test_id("graph-inspector")
+ inspector.wait_for(timeout=5_000)
+ assert page.get_by_test_id("graph-inspector-purpose").inner_text().strip()
+ _shot(page, dest, "review-inspector.png")
+ page.get_by_test_id("graph-inspector-close").click()
+ inspector.wait_for(state="hidden")
page.get_by_test_id("tab-graph").click()
page.get_by_test_id("graph-full").wait_for()
- page.locator(".react-flow__node").first.wait_for(timeout=15_000)
- page.locator(".react-flow__edge").first.wait_for(timeout=15_000)
+ _wait_graph(page)
page.locator(".react-flow__minimap-node").first.wait_for(timeout=10_000)
assert page.locator(".react-flow__minimap-node").count() > 3
assert page.locator(".react-flow__edge").count() > 0
page.wait_for_timeout(600)
- page.screenshot(path=str(dest / "graph.png"), full_page=False)
+ _shot(page, dest, "graph.png")
+
+ page.get_by_test_id("graph-mode-architecture").click()
+ _wait_graph(page)
+ page.wait_for_timeout(600)
+ _shot(page, dest, "graph-architecture.png")
+ page.get_by_test_id("graph-mode-review").click()
+ _wait_graph(page)
page.get_by_test_id("graph-view-3d").click()
assert page.get_by_test_id("graph-view-3d").get_attribute("aria-pressed") == "true"
page.get_by_test_id("graph-3d").wait_for(timeout=15_000)
page.locator("[data-testid='graph-3d-canvas'], [data-testid='graph-3d-fallback']").first.wait_for(timeout=20_000)
- page.wait_for_timeout(800)
- page.screenshot(path=str(dest / "graph_3d.png"), full_page=False)
page.get_by_test_id("graph-view-2d").click()
- page.locator(".react-flow__node").first.wait_for(timeout=15_000)
+ _wait_graph(page)
page.get_by_test_id("tab-prs").click()
page.get_by_test_id("pr-repo").fill("acme/demo")
page.get_by_test_id("btn-list-prs").click()
page.get_by_text("#42").wait_for(timeout=10_000)
- page.wait_for_timeout(300)
- page.screenshot(path=str(dest / "pull-requests.png"), full_page=False)
+ _shot(page, dest, "pull-requests.png")
page.get_by_test_id("tab-settings").click()
page.get_by_test_id("settings-form").wait_for()
page.locator('select[name="ai_provider"]').select_option("grok")
page.get_by_test_id("theme-grid").wait_for()
- page.get_by_test_id("theme-paper").click()
- page.wait_for_function("document.documentElement.dataset.theme === 'paper'")
- page.wait_for_timeout(200)
- page.screenshot(path=str(dest / "settings.png"), full_page=False)
+ _shot(page, dest, "settings.png")
- page.get_by_test_id("theme-select").select_option("nord")
- page.wait_for_function("document.documentElement.dataset.theme === 'nord'")
+ page.get_by_test_id("tab-review").click()
+ page.get_by_test_id("brief").wait_for()
+ _wait_graph(page)
+ for theme in THEME_SHOTS:
+ page.get_by_test_id("theme-select").select_option(theme)
+ page.wait_for_function(
+ "id => document.documentElement.dataset.theme === id",
+ arg=theme,
+ )
+ page.wait_for_timeout(250)
+ _shot(page, dest, f"theme-{theme}.png")
+
+ from loadpath.mcp.oauth import LoadpathOAuthProvider
+
+ oauth = LoadpathOAuthProvider(
+ issuer="http://127.0.0.1:7345",
+ resource="http://127.0.0.1:7345/mcp",
+ pin="123456",
+ )
+ consent = page.context.new_page()
+ consent.set_content(oauth._consent_html("demo", "Cursor"))
+ consent.set_viewport_size({"width": 1440, "height": 900})
+ consent.wait_for_timeout(200)
+ consent.screenshot(path=str(dest / "mcp-consent.png"), full_page=False)
+ consent.close()
- for name in ("architecture.png", "review.png", "graph.png", "pull-requests.png", "settings.png"):
- assert (dest / name).stat().st_size > 1000
+ for name in EXPECTED:
+ path = dest / name
+ assert path.is_file(), name
+ assert path.stat().st_size > 1000, name