agnes-the-ai-analyst/tests/test_web_home_page.py
Vojtech d63f1473ab
feat(home): drop /home connectors block — onboarding covers it (#305)
The dedicated `<details data-section="connectors">` section on /home
duplicated content that the install hero's Step 4 clipboard payload
already inlines. Both surfaces sourced the same prompt strings from
`app/web/connector_prompts.py` (home tiles via `<code id="*-prompt">`,
setup script via `app/web/setup_instructions.py::_connectors_block`),
so users walking the install script visited each connector inline and
then had no reason to scroll back up.

Removed the full block (3 tiles + summary + section-label). Lead
paragraph in the install hero now mentions the connector families
briefly so the benefit is visible before kick-off:

  "... your team's curated data, plugins, third-party tools (Asana,
   Google Workspace, Atlassian), and shared knowledge ... the install
   script also connects your tools for you, so there's no extra page
   to visit."

The "Email admin" mailto CTA, previously gated inside the GWS tile
when admin_email was set + GWS unconfigured, moves implicitly to the
install script's GWS step (Claude prompts the user when the OAuth
gating wall lands). Tests updated:

- test_connectors_section_removed_from_home (renamed from
  test_connectors_render_flat_when_onboarded_by_default) — asserts
  `class="connector-tiles"` and `data-section="connectors"` are absent
  in BOTH onboarded states, and that the lead paragraph still mentions
  the three connector families so the benefit isn't lost.
- test_home_renders_connector_prompts_from_shared_module — DROPPED.
  Was a parity check between the home tiles and the setup script's
  connector_prompts.py source. One surface now → no drift risk → test
  redundant. Replaced with an inline comment pointing future readers
  at where the strings flow (setup_instructions.py::_connectors_block).
- test_home_no_longer_shows_email_admin_button (renamed from
  test_home_shows_email_admin_button_when_admin_email_set_and_gws_unconfigured)
  — asserts the mailto CTA is gone from /home regardless of
  admin_email / GWS-configured state; documents the path-move.

CSS for `.connector-tile*` left in place as dead bytes — small
footprint, no behavior, easy follow-up if/when someone audits.
2026-05-14 18:31:24 +02:00

399 lines
17 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""GET /home — state-aware landing page.
The boolean ``users.onboarded`` drives template selection. No
auto-transition: the not-onboarded view stays put until the user reloads
(the brainstorm called this out explicitly — quiet UI is preferable to a
surprise redirect mid-setup).
See origin: docs/brainstorms/home-page-requirements.md.
"""
from __future__ import annotations
import tempfile
import uuid
import pytest
@pytest.fixture
def fresh_db(monkeypatch):
with tempfile.TemporaryDirectory() as tmp:
monkeypatch.setenv("DATA_DIR", tmp)
monkeypatch.setenv("TESTING", "1")
monkeypatch.setenv("JWT_SECRET_KEY", "test-jwt-secret-key-minimum-32-chars!!")
yield tmp
def _make_user_and_session(conn, email="u@example.com", onboarded=False):
from src.repositories.users import UserRepository
from app.auth.jwt import create_access_token
uid = str(uuid.uuid4())
UserRepository(conn).create(id=uid, email=email, name=email.split("@")[0])
if onboarded:
conn.execute("UPDATE users SET onboarded = TRUE WHERE id = ?", [uid])
return uid, create_access_token(user_id=uid, email=email)
def _client(follow_redirects: bool = True):
from fastapi.testclient import TestClient
from app.main import app
return TestClient(app, follow_redirects=follow_redirects)
def test_home_unauth_redirects_to_login(fresh_db):
"""Non-API HTML routes redirect 401→/login per app.main's
StarletteHTTPException handler. /home follows that contract."""
c = _client(follow_redirects=False)
resp = c.get("/home")
assert resp.status_code == 302
assert resp.headers["location"].startswith("/login")
def test_home_not_onboarded_user_sees_setup_view(fresh_db):
"""A FALSE-onboarded user gets the install/setup template, identifiable
by its 'Install Claude Code' heading and the self-mark button."""
from src.db import get_system_db, close_system_db
conn = get_system_db()
try:
_, sess = _make_user_and_session(conn, onboarded=False)
finally:
conn.close()
close_system_db()
c = _client()
resp = c.get("/home", cookies={"access_token": sess})
assert resp.status_code == 200
body = resp.text
assert "install Claude Code" in body # step 1 label
assert "install Agnes" in body # step 2 label
assert "self-mark-btn" in body # self-acknowledged escape hatch
assert "setupClaudeBtn" in body # primary one-click CTA from shared partial
def test_home_onboarded_user_sees_nav_hub(fresh_db):
"""A TRUE-onboarded user gets the post-onboarding view: the blue
install-hero is gone entirely (no welcome banner, no completion
badge, no inline step commands), the offboard escape strip is the
only setup-flow remnant rendered, and the rest of /home (connector
tiles, news, etc.) stays. PR #289 collapsed the dual-state hero
into a single not-onboarded-only render — pre-PR the onboarded
branch reused the same `.install-hero` shell with welcome copy
and a "Steps 14 done" badge."""
from src.db import get_system_db, close_system_db
conn = get_system_db()
try:
_, sess = _make_user_and_session(conn, onboarded=True)
finally:
conn.close()
close_system_db()
c = _client()
resp = c.get("/home", cookies={"access_token": sess})
assert resp.status_code == 200
body = resp.text
# Install hero entirely absent for onboarded users.
assert '<div class="install-hero">' not in body
# Offboard escape strip + its button replace the in-hero self-mark control.
assert '<div class="offboard-strip">' in body
assert "Mark me as offboarded" in body
# All four inline install-blocks are hidden post-onboarding — the
# labels rendered inside the install-block divs go away.
assert "Step 1 — install Claude Code" not in body
assert "Step 2 — turn on auto-mode" not in body
assert "Step 3 — create your workspace folder" not in body
assert "Step 4 — install" not in body
def test_connectors_section_removed_from_home(fresh_db):
"""The dedicated `<details data-section="connectors">` block was
dropped from `/home` — the install-hero's Step 4 clipboard payload
(rendered via `_claude_setup_instructions.jinja` inside the manual
fallback) already inlines the same Asana / GWS / Atlassian prompts
from `app/web/connector_prompts.py` via
`app/web/setup_instructions.py::_connectors_block`. Showing them
twice on the same page was duplicate UX. The lead paragraph in the
install-hero now mentions the connectors briefly so users still see
the benefit before they hit the install.
Co-asserts the auto-mode block removal that this test originally
pinned — onboarded users still see neither the connectors block
nor the legacy auto-mode peer section."""
from src.db import get_system_db, close_system_db
conn = get_system_db()
try:
_, sess = _make_user_and_session(conn, onboarded=True)
finally:
conn.close()
close_system_db()
c = _client()
resp = c.get("/home", cookies={"access_token": sess})
assert resp.status_code == 200
body = resp.text
# Auto-mode peer section still gone (legacy guard, not regressed).
assert 'class="automode-card"' not in body
assert 'data-section="step3"' not in body
assert "Step 2 — turn on auto-mode" not in body
# Dedicated connectors block is gone from /home in BOTH states.
assert 'class="connector-tiles"' not in body
assert 'data-section="connectors"' not in body
# Server-rendered HTML never carries the data-setup-minimized
# attribute on the .home-mock root — that's a client-side
# localStorage decision applied via JS on load.
assert '<div class="home-mock" data-setup-minimized' not in body
assert 'class="home-mock"\n' in body or '<div class="home-mock">' in body
# Not-onboarded path: same — the section disappears regardless of
# state. Lead-paragraph still surfaces the connector names so users
# know the benefit exists before they kick off the install.
conn = get_system_db()
try:
_, sess2 = _make_user_and_session(
conn, email="not-onboarded@example.com", onboarded=False
)
finally:
conn.close()
close_system_db()
body2 = _client().get("/home", cookies={"access_token": sess2}).text
assert 'class="connector-tiles"' not in body2
assert 'data-section="connectors"' not in body2
# Lead-paragraph mentions the three connector families so the
# benefit isn't lost when the dedicated section disappears.
assert "Asana, Google Workspace, Atlassian" in body2
def test_minimize_toggle_no_longer_rendered(fresh_db):
"""The "Minimize setup view" toggle used to live inside the
onboarded-branch of the install-hero. PR #289 hides the hero
entirely once `users.onboarded=true`, so the minimize toggle
has no rendering site anymore — verify the markup is absent
from both states. (The localStorage `agnes_home_setup_minimized`
flag and its applyMinimize() JS handler stay in the page so a
stale flag from a pre-PR session no-ops cleanly.)"""
from src.db import get_system_db, close_system_db
for onboarded in (False, True):
conn = get_system_db()
try:
_, sess = _make_user_and_session(
conn, email=f"user-{onboarded}@example.com", onboarded=onboarded
)
finally:
conn.close()
close_system_db()
c = _client()
resp = c.get("/home", cookies={"access_token": sess})
assert resp.status_code == 200
assert '<button id="setupMinimizeToggle"' not in resp.text
assert 'class="setup-minimize"' not in resp.text
def test_home_no_auto_transition_after_post_until_reload(fresh_db):
"""POST /api/me/onboarded flips the flag in the DB but the in-flight
/home response from before the POST keeps showing the setup view —
the next GET /home picks up the new state. Verifies the manual-reload
contract called out in the brainstorm."""
from src.db import get_system_db, close_system_db
conn = get_system_db()
try:
_, sess = _make_user_and_session(conn, onboarded=False)
finally:
conn.close()
close_system_db()
c = _client()
pre = c.get("/home", cookies={"access_token": sess})
# `class="install-block"` is the not-onboarded-only structural element
# holding the inline Step-1 install pane. Use it as the discriminator
# instead of a free-form string like "install Claude Code", which now
# also appears in the always-on SETUP_INSTRUCTIONS_TEMPLATE clipboard
# payload's preflight comment after the 2026-05-10 init-report fix.
assert 'class="install-block"' in pre.text # setup view
flip = c.post("/api/me/onboarded", cookies={"access_token": sess})
assert flip.status_code == 200
post = c.get("/home", cookies={"access_token": sess})
# PR #289: hero disappears entirely; offboard strip is the
# only setup-flow remnant. Use either as the nav-hub view marker.
assert '<div class="offboard-strip">' in post.text
assert 'class="install-block"' not in post.text
# ── GWS Email-admin button render tests (admin_email knob coverage) ────────
def test_home_hides_email_admin_button_when_admin_email_unset(fresh_db, monkeypatch):
"""When ``instance.admin_email`` is unset, the GWS connector tile
must NOT render the mailto link (template guards on truthiness;
empty resolver value cleanly hides). Defends against a `mailto:?`
link sneaking out as a render-time artifact."""
monkeypatch.delenv("AGNES_INSTANCE_ADMIN_EMAIL", raising=False)
monkeypatch.delenv("AGNES_GWS_CLIENT_ID", raising=False)
monkeypatch.delenv("AGNES_GWS_CLIENT_SECRET", raising=False)
from src.db import get_system_db, close_system_db
conn = get_system_db()
try:
_, sess = _make_user_and_session(conn)
finally:
conn.close()
close_system_db()
body = _client().get("/home", cookies={"access_token": sess}).text
# No "Email admin" CTA, no mailto: link in the body.
assert "Email admin" not in body
assert "mailto:?" not in body # specifically, no broken empty mailto
def test_home_no_longer_shows_email_admin_button(fresh_db, monkeypatch):
"""The Email-admin mailto CTA used to live inside the /home GWS
connector tile. With the dedicated `<details data-section="connectors">`
block removed (see test_connectors_section_removed_from_home above),
the button has no rendering site even when admin_email is set + GWS
is unconfigured. The escalation path lives inside the install
script's GWS step now — Claude prompts the user with the admin
email when the connector setup hits an OAuth gating wall, so the
affordance moves to the surface where it's actually useful."""
monkeypatch.setenv("AGNES_INSTANCE_ADMIN_EMAIL", "ops@example.com")
monkeypatch.delenv("AGNES_GWS_CLIENT_ID", raising=False)
monkeypatch.delenv("AGNES_GWS_CLIENT_SECRET", raising=False)
from src.db import get_system_db, close_system_db
conn = get_system_db()
try:
_, sess = _make_user_and_session(conn)
finally:
conn.close()
close_system_db()
body = _client().get("/home", cookies={"access_token": sess}).text
assert "Email admin" not in body
assert 'mailto:ops@example.com' not in body
def test_home_hides_email_admin_button_when_gws_configured(fresh_db, monkeypatch):
"""Even with admin_email set, when GWS OAuth is operator-provisioned
(gws_oauth.configured = True), the Email-admin CTA is redundant —
user can just connect. Template gates on `not gws_oauth.configured`."""
monkeypatch.setenv("AGNES_INSTANCE_ADMIN_EMAIL", "ops@example.com")
monkeypatch.setenv("AGNES_GWS_CLIENT_ID", "abc.apps.googleusercontent.com")
monkeypatch.setenv("AGNES_GWS_CLIENT_SECRET", "GOCSPX-secret")
from src.db import get_system_db, close_system_db
conn = get_system_db()
try:
_, sess = _make_user_and_session(conn)
finally:
conn.close()
close_system_db()
body = _client().get("/home", cookies={"access_token": sess}).text
assert "Email admin" not in body
# `test_home_renders_connector_prompts_from_shared_module` was dropped here
# alongside the removal of the /home `<details data-section="connectors">`
# block. The test pinned source-of-truth parity between the home tile
# `<code id="*-prompt">` blocks and `app/web/connector_prompts.py`. With the
# tiles gone, the only surface left for those strings is the install-hero's
# Step 4 clipboard payload (rendered via `_claude_setup_instructions.jinja`
# from `setup_instructions_lines`, which is built in
# `app/web/setup_instructions.py::_connectors_block` calling the same
# `connector_prompts.py` functions). One surface, no drift risk → the
# parity test is redundant. If a second surface ever re-renders these
# prompts, restore a parity test scoped to that new consumer.
# ── Getting Started + Overview + Usage modes (PR #289 home additions) ────
def test_getting_started_card_renders_on_home(fresh_db):
"""The dismissible Getting Started card now renders BEFORE the
install-hero (chronologically first in the not-onboarded flow) as
a <details> element — collapsed by default so the install hero
stays visible on first paint. Disappears when the user is
onboarded (no `<details class="home-getting-started">`) so the
in-page #install-hero anchor on the first row never points at
nothing. First row links to #install-hero (same-page jump to the
blue setup hero); second row still leaves the page for
/setup-advanced."""
from src.db import get_system_db, close_system_db
# Not-onboarded: GS is rendered + install-hero anchor target exists.
conn = get_system_db()
try:
_, sess = _make_user_and_session(
conn, email="gs-not-onboarded@example.com", onboarded=False
)
finally:
conn.close()
close_system_db()
body = _client().get("/home", cookies={"access_token": sess}).text
assert '<details class="home-getting-started"' in body
assert 'data-dismiss-key="agnes_home_gs_dismissed"' in body
assert 'class="home-gs-item" href="#install-hero"' in body
assert 'class="home-gs-item" href="/setup-advanced"' in body
# Install-hero must carry the matching id so the first-row anchor
# resolves. Co-asserted with the GS markup so a refactor that drops
# one but not the other breaks here, not in the browser.
assert '<div class="install-hero" id="install-hero">' in body
# Onboarded: install-hero is gone, GS rides alongside it — neither
# renders. Prevents a dangling #install-hero anchor.
conn = get_system_db()
try:
_, sess2 = _make_user_and_session(
conn, email="gs-onboarded@example.com", onboarded=True
)
finally:
conn.close()
close_system_db()
body2 = _client().get("/home", cookies={"access_token": sess2}).text
assert '<details class="home-getting-started"' not in body2
assert '<div class="install-hero"' not in body2
def test_overview_section_renders_when_yaml_set(fresh_db, monkeypatch):
"""Setting `AGNES_INSTANCE_OVERVIEW` env (mirrors
instance.overview yaml) injects raw HTML into the Overview section
via the same `| safe` filter as news_intro. The marker text must
appear inside the rendered section wrapper. Overview deliberately
has NO dismiss button — it's operator-owned reference content
(privacy posture, telemetry policy, product framing), and a
per-device hide would leave returning users unable to re-read
it without clearing localStorage."""
monkeypatch.setenv("AGNES_INSTANCE_OVERVIEW", "<p>OVERVIEW_TEST_MARKER</p>")
from src.db import get_system_db, close_system_db
conn = get_system_db()
try:
_, sess = _make_user_and_session(conn)
finally:
conn.close()
close_system_db()
body = _client().get("/home", cookies={"access_token": sess}).text
assert '<section class="home-overview">' in body
assert "OVERVIEW_TEST_MARKER" in body
# Overview must NOT carry a dismiss key — content stays
# reachable on every visit so users can re-read it.
assert 'data-dismiss-key="agnes_home_overview_dismissed"' not in body
def test_overview_section_hidden_when_yaml_empty(fresh_db, monkeypatch):
"""Default empty `instance.overview` (no env override) hides the
section entirely so the OSS ships without a stray empty
Overview placeholder."""
monkeypatch.delenv("AGNES_INSTANCE_OVERVIEW", raising=False)
from src.db import get_system_db, close_system_db
conn = get_system_db()
try:
_, sess = _make_user_and_session(conn)
finally:
conn.close()
close_system_db()
body = _client().get("/home", cookies={"access_token": sess}).text
assert '<section class="home-overview">' not in body