agnes-the-ai-analyst/tests/test_setup_page_unified.py
Vojtech a46b9dc928
/home install-hero polish: license link contrast, auto-mode reorder, Shift+Tab guidance (#243)
* Make /home install-hero links readable against blue background

The Claude license-options link added in the previous commit inherited
the default `<a>` style (`var(--hp-primary)` blue), which renders as
blue-on-blue and is unreadable inside the blue install-hero. Add a
scoped `.install-hero a` rule that uses white with an underline
(matching the existing lead-paragraph contrast pattern) so any link
nested in the hero stays legible.

* Reorder /home install flow: auto-mode is now Step 2, Agnes install becomes Step 3

Step 3 (was Step 2) pastes a ~20-command bash bootstrap into a fresh
Claude Code session. Without auto-mode enabled first, each Bash/edit
command needs a manual approve click — bad UX for first-time users.

Move auto-mode from the outside-hero `<details>` reference block into
the install-hero as a real Step 2, between "install Claude Code" and
"install Agnes". Content is the persistent `acceptEdits` snippet
(write to ~/.claude/settings.json) plus a one-liner pointing at
Shift+Tab for users who are already inside a running Claude Code
session. YOLO mode for full Bash auto-approve stays on
/setup-advanced behind the existing link.

The outside-hero `setup-collapsible[data-section="step3"]` block is
dropped — auto-mode is no longer reference content, it's a real
install step, and duplicating it would just diverge over time.
Onboarded users no longer see the auto-mode block at all (consistent
with Steps 1 + 3 also hiding post-onboarding).

Completion banner copy updated: "Step 1, 2 & 3 done — Claude Code
installed, auto-mode set, Agnes ready". Dashboard CTA partial and
other templates don't reference step numbers for this flow, so no
adaptation needed there.

* Simplify /home Step 2 to Shift+Tab only — drop the JSON snippet

Operator pointed out two issues with the prior Step 2:

1. The settings.json snippet is redundant. Claude Code's first
   Shift+Tab cycle to auto-accept mode already prompts the user
   whether to persist it as default — Claude writes the config
   itself, no manual file edit needed.

2. The snippet only showed the POSIX path `~/.claude/settings.json`,
   which doesn't translate to native Windows.

Replace the snippet + copy button with a plain Shift+Tab instruction,
explicitly call out the first-time "make this the default?" prompt,
and note that Claude handles the config write itself — same flow on
macOS / Linux / WSL / Windows. Adds a fallback line for users who
already closed the post-OAuth session.

* Tighten /home Step 2 install-note to two paragraphs

Operator: drop the 'Claude writes the setting itself, so this works
the same on macOS / Linux / WSL / Windows...' line plus the
'auto-approves file edits going forward; Bash commands stay gated
— that's the safe default' line. Both were filler — the make-default
prompt already implies persistence, and gated Bash is the obvious
default users won't be surprised by.

Result: paragraph 1 carries Shift+Tab + first-time make-default
say-yes + closed-session fallback in one breath; paragraph 2 keeps
the verbatim YOLO link. Same affordances, less vertical space.
2026-05-11 16:46:58 +00:00

129 lines
5.5 KiB
Python

"""Tests for the unified `/setup` route.
The previous `?role=analyst|admin` query parameter is gone. The route
renders a single layout for everyone — admin-vs-analyst is no longer a
branch. The marketplace + plugins block is gated by per-user
`resource_grants` resolved inside `compute_default_agent_prompt`.
"""
import pytest
from fastapi.testclient import TestClient
@pytest.fixture
def client(tmp_path, monkeypatch):
"""TestClient against a freshly-built FastAPI app rooted at tmp_path.
Mirrors the `web_client` fixture in tests/test_web_ui.py — we re-create
the app so the DuckDB singleton picks up the per-test DATA_DIR rather
than leaking state across tests on the same xdist worker.
"""
monkeypatch.setenv("DATA_DIR", str(tmp_path))
monkeypatch.setenv("TESTING", "1")
monkeypatch.setenv("JWT_SECRET_KEY", "test-secret-key-min-32-characters!!")
(tmp_path / "state").mkdir()
(tmp_path / "analytics").mkdir()
(tmp_path / "extracts").mkdir()
from src.db import close_system_db
close_system_db()
from app.main import create_app
app = create_app()
yield TestClient(app)
close_system_db()
def test_setup_page_renders_unified_layout(client):
"""Bare `/setup` (no query param) renders the unified flow:
- `agnes init` is mandatory (subsumes the old admin-only
`agnes auth import-token` + `agnes auth whoami` pair).
- Marketplace block is always emitted (Fix B in 2026-05-10
init-report response): anonymous visitors with no plugin grants
still get the marketplace registration step so the SessionStart
hook is pre-wired. Confirm = step 8.
"""
resp = client.get("/setup", follow_redirects=True)
assert resp.status_code == 200
text = resp.text
# Unified flow markers.
assert "agnes init" in text
# Legacy admin-only login verbs are gone from the rendered prompt.
assert "agnes auth import-token" not in text
# Always-on layout (preflight + marketplace + MCP + connectors block all
# unconditional; skills step deleted in #242): Confirm = step 9.
assert "9) Confirm:" in text
def test_setup_page_ignores_role_query_param(client):
"""`?role=...` is no longer accepted by the route signature. FastAPI
ignores unknown query params silently — `/setup?role=admin` still
serves the unified layout. No 422, no redirect, no behavior delta
vs. bare `/setup`."""
bare = client.get("/setup", follow_redirects=True)
with_role = client.get("/setup?role=admin", follow_redirects=True)
assert bare.status_code == 200
assert with_role.status_code == 200
# Both responses contain the unified-flow marker.
assert "agnes init" in bare.text
assert "agnes init" in with_role.text
# Legacy admin-only login verbs are gone from both.
assert "agnes auth import-token" not in bare.text
assert "agnes auth import-token" not in with_role.text
def test_setup_page_renders_marketplace_for_user_with_grants(client, monkeypatch):
"""When the caller has a non-empty served stack, the marketplace block
renders the "install your current stack" copy variant. Confirm stays
at step 8 in the post-skills-removal layout (preflight + marketplace
+ MCP all always-on regardless of stack contents).
Stub `marketplace_filter.resolve_user_marketplace` to return a
plugin so we don't have to seed the full marketplace plumbing in
this test — we're verifying the layout, not the RBAC resolver
itself (covered by `test_marketplace_filter`).
Post-Model B (v28+): the setup page reads from
`resolve_user_marketplace` (which gates on explicit subscriptions)
rather than `resolve_allowed_plugins` (RBAC-only)."""
from app.web.router import get_optional_user
from fastapi import Request
from src import marketplace_filter
async def _admin_user(request: Request): # type: ignore[no-redef]
return {"id": "admin-1", "email": "admin@example.com",
"is_admin": True, "name": "Admin", "groups": ["Admin"]}
monkeypatch.setattr(
marketplace_filter,
"resolve_user_marketplace",
lambda conn, user: [{"manifest_name": "demo-plugin"}],
)
client.app.dependency_overrides[get_optional_user] = _admin_user
try:
resp = client.get("/setup", follow_redirects=True)
finally:
client.app.dependency_overrides.pop(get_optional_user, None)
assert resp.status_code == 200
text = resp.text
# Marketplace block marker. The per-plugin install lines moved inside
# `agnes refresh-marketplace --bootstrap`, so we check the section
# header + the one-liner instead of `claude plugin install <name>@agnes`.
# Non-empty stack → "install plugins" header variant.
assert "Register the Agnes Claude Code marketplace and install plugins" in text
assert "agnes refresh-marketplace --bootstrap" in text
# Layout shift: Confirm is now step 9 (preflight + marketplace + MCP +
# connectors all always-on; skills step deleted in #242).
assert "9) Confirm:" in text
# Pre-flight is in the rendered prompt at step 4.
assert "Make sure git and claude are installed" in text
# Atlassian MCP registration is at step 6.
assert "claude mcp add --transport sse atlassian" in text
def test_install_legacy_path_redirects_to_setup(client):
"""`/install` legacy path keeps redirecting to `/setup` (302/307)."""
resp = client.get("/install", follow_redirects=False)
assert resp.status_code in (302, 307)
assert "/setup" in resp.headers["location"]