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 +[![CI](https://github.com/Modsofthenation/PR-Reviewer/actions/workflows/ci.yml/badge.svg)](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`). +![Review screen with impact graph](docs/screenshots/review.png) + +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. + +![Empty review onboarding](docs/screenshots/review-empty.png) ### 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). -![Review](docs/screenshots/review.png) +![Review with brief and impact graph](docs/screenshots/review.png) + +### 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. + +![Inspector on InvoiceSerializer](docs/screenshots/review-inspector.png) ### 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. -![Architecture](docs/screenshots/architecture.png) +![Architecture graph and findings](docs/screenshots/architecture.png) ### 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. + +![Impact graph, this review](docs/screenshots/graph.png) -![Impact graph](docs/screenshots/graph.png) +![Indexed architecture in the graph tab](docs/screenshots/graph-architecture.png) ### 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. + +![Pull requests list](docs/screenshots/pull-requests.png) + +### Repo explorer -![Pull requests](docs/screenshots/pull-requests.png) +Browse the filesystem, pick a project root. Loadpath remembers recent workspaces. + +![Repo explorer](docs/screenshots/explorer.png) ### 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. + +![Settings with theme grid](docs/screenshots/settings.png) + +### 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`. -![Settings](docs/screenshots/settings.png) +![MCP consent](docs/screenshots/mcp-consent.png) + +## Themes + +Default is **Obsidian**. Settings lists every palette; the shots below are the same Review screen under nine of them. + + + + + + + + + + + + + + + + + +
Obsidian
Obsidian theme
Nord
Nord theme
Neon noir
Neon noir theme
Synthwave
Synthwave theme
Phosphor
Phosphor theme
Paper
Paper theme
Sakura
Sakura theme
Citrus
Citrus theme
High contrast
High contrast theme
+ +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