agnes-the-ai-analyst/tests/test_setup_page_unified.py
Vojtech 41829e8a45
Setup-prompt + bootstrap fixes from 2026-05-10 init report (#240)
* Setup-prompt + bootstrap fixes from David's 2026-05-10 init report

Three issues from clean-machine bootstrap evidence:

1. `agnes refresh-marketplace --bootstrap` failed to recover when the
   local clone existed but Claude Code's marketplace registry had lost
   the `agnes` entry. Bootstrap path now parses
   `claude plugin marketplace list`, re-runs
   `claude plugin marketplace add ~/.agnes/marketplace` when missing,
   and treats `add` failures as fatal (was warn-and-continue, root cause
   of the cascade into "Marketplace 'agnes' not found" plugin install
   errors).

2. Setup prompt now always emits the marketplace-registration block,
   even when the operator has zero plugin grants. Pre-wires the
   SessionStart hook so future admin grants land automatically without
   re-running setup. Block copy adapts: empty list shows
   "no plugins granted yet", populated list shows "install plugins".

3. Setup prompt registers the Atlassian Remote MCP server unattended
   (`claude mcp add --transport sse atlassian
   https://mcp.atlassian.com/v1/sse`). Hosted Remote MCP, OAuth handled
   automatically by Claude Code on first use. Asana / GWS stay on the
   /home connector cards (PAT/keychain flows don't fit unattended
   bootstrap).

Confirm step nudges the user toward the /home connector cards for the
PAT-flow services. CLAUDE.md template renames the marketplace section
to "Agnes Marketplace" and documents that all plugins are addressed as
`<plugin>@agnes` regardless of upstream slug.

Layout: Confirm shifts from step 6/8 to step 9 across all variants
(preflight, marketplace, MCP all unconditional). Tests updated.

* Link Claude license options from /home install pane

Step-1 Claude install on /home pointed users to  OAuth without
explaining what to do if they don't have a Pro/Max subscription. Add
a one-line follow-up link to the plan-tier section on /setup-advanced
(new `#claude-plan` anchor) so first-time users discover the
subscription tiers rather than bouncing on the OAuth screen.

* Add idempotent + no-TLS-bypass guardrails to /home connector prompts

The Asana / Google Workspace / Atlassian connector prompts on /home
already shipped a precheck step that short-circuits when the service
is already wired, but they didn't carry the same idempotency +
surface-errors-verbatim + don't-disable-TLS-verification guardrails
the bash bootstrap prompt has. Add a one-paragraph 'Ground rules'
block at the top of each prompt so a connector failure doesn't
tempt the model into bypass workarounds, matching the same posture
David's 2026-05-10 init report flagged for the bash flow.

* skip Source: lines in marketplace registry detector

`claude plugin marketplace list` prints a `Source: <local path>` line
under each registered marketplace; the local clone almost always lives
under a path containing the marketplace name itself
(`~/.agnes/marketplace`). A naive \\bagnes\\b match over the full
stdout therefore false-positives whenever ANY unrelated marketplace
sits under `~/.agnes-…/` or similar. Filter Source: lines out before
matching so the recovery path actually re-adds when needed instead of
silently falling through to a broken `marketplace update agnes`.
Adds regression test covering the substring-only case.

* drop customer-specific tokens from CHANGELOG entries

Per CLAUDE.md vendor-agnostic OSS rule ("nothing customer-specific
... in changelogs"):
- "agnes-vrysanek.groupondev.com" -> "a private-CA Agnes deployment"
- "Groupon Marketplace / groupon-marketplace" -> "<Org> Marketplace /
  <org>-marketplace" (placeholder example)
- Removed "David flagged" attribution language; init-report context
  stays intact, just stripped of the named host + brand

---------

Co-authored-by: ZdenekSrotyr <zdenek.srotyr@keboola.com>
2026-05-10 20:24:00 +02:00

127 lines
5.3 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 block all unconditional):
# 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 plugin grants in `resource_grants`, the
unified flow inserts the marketplace + plugins block (step 5) and
Confirm shifts to step 8.
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 switch, 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`.
assert "Register the Agnes Claude Code marketplace" in text
assert "agnes refresh-marketplace --bootstrap" in text
# Layout shift: Confirm is now step 9 (preflight + marketplace + MCP all
# always-on per Fix B + Fix C in 2026-05-10 init-report response).
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"]