- instance.brand (env AGNES_INSTANCE_BRAND, default "Agnes") + instance.workspace_dir replace hard-coded "Agnes" / "~/Agnes" across /home, /setup, /setup-advanced, /login, /install, /me/debug, and the Claude Code clipboard setup script. Terraform-friendly env override; defaults preserve existing Agnes branding. - Explicit "create workspace folder" step on /home (OS-tabbed mkdir+cd) + same step baked into the clipboard script as step 2. Drops the implicit assumption that `agnes init --workspace .` lands in a sensibly-cd'd shell. - Final "Restart Claude Code" step in the setup script (unconditional, between connectors and Confirm) so freshly-installed plugins, MCP servers, and SessionStart hooks load on the next Claude Code session. - Asana reverted from hosted Remote MCP back to PAT + raw REST against app.asana.com/api/1.0. MCP envelope shape consumed ~5x tokens per call; the PAT path lets the agent read flat REST fields. Existing MCP registration is detected and the user is asked whether to remove it (default Y, with benefits listed: token cost, no third-party hop, no OAuth refresh dance, deterministic envelope shape). - Atlassian connector instructs picking the longest API-token expiry (today "1 year") to cut re-mint friction. No public query-parameter hook exists on id.atlassian.com to pre-select expiry, so the prompt documents the manual click and acknowledges that limitation. - Uniform ✅ / ❌ per-connector marker contract (Asana, GWS, Atlassian) for the Confirm summary to grep. Each connector now ends with a Claude-driven end-to-end test that uses Claude Code's own bash to exercise the stored credential and prints "✅ <Connector> integration verified — ..." (or the failure variant).
129 lines
5.5 KiB
Python
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 "10) 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 "10) 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"]
|