* fix(web): /home Step 2 recommends --dangerously-skip-permissions for setup The Step 4 paste runs ~20 shell commands (CLI install, workspace bootstrap, marketplace clone, MCP register, connector logins). Previous Step 2 recommended auto-accept-edits via Shift + Tab, which covers file edits but not Bash — users still clicked ~20 Yes prompts during setup. Step 2 now leads with `claude --dangerously-skip-permissions` as the recommended session flag (Bash + edits both skip). Session-scoped, drops on next plain `claude` — safe here because the pasted script is generated by this server and ends after a fixed sequence; the flag does not weaken future Claude sessions. Auto-accept-edits via Shift + Tab kept as the strict-review fallback; persistent YOLO allowlist link to /setup-advanced#yolo unchanged. * fix(web): swap /home Steps 2↔3, claude --yolo as copy-button command Folder creation moves to Step 2; Step 3 launches Claude from that directory with `claude --dangerously-skip-permissions`. The YOLO flag is rendered through the standard .install-cmd + copy-button affordance (matching Step 1 + Step 2), not inline prose. Step 4 paste runs ~20 shell commands that auto-accept-edits would not cover (Bash still prompts), so the YOLO flag is the default recommendation; session- scoped, drops on next plain `claude`. Setup script's pwd-check warning copy refreshed to reference "/home Step 2" (the new folder-creation step number). # Conflicts: # CHANGELOG.md * fix(web): open YOLO setup-advanced link in new tab Step 3 install-hero's persistent-YOLO link now opens /setup-advanced#yolo in a new window so users don't lose their /home install context mid- setup. target="_blank" + rel="noopener" (no reverse-tabnabbing). * fix(web): merge /home Step 3 fallback prose into prior paragraph Drop the <br><br> between the 'Session-scoped' line and the 'Prefer reviewing each command' line so the strict-review fallback flows on the same paragraph — less vertical space in the install-hero block. * docs(web): add "What leaves your machine" privacy callout on /home Install-hero lead now includes a short privacy paragraph: explains that session telemetry (prompts / tool-calls / tool-responses) flows back to the central catalog for failure-pattern analysis while raw data rows the user queries locally stay on their machine. Points at /agnes-private as the per-session opt-out. Also collapses leftover cherry-pick conflict markers in CHANGELOG.md into one clean [Unreleased] section. * fix(init): harden agnes init UX — 5 issues from David's report 1. chmod +x hooks. agnes init + agnes refresh-marketplace --bootstrap now set the execute bit on every .sh they land on disk (`<workspace>/.claude/hooks/*.sh` after init; every `.sh` under the `~/.agnes/marketplace` clone after a bootstrap/pull). Git checkout doesn't always preserve filemode (filemode=false repos, ZIP extractions), so hooks were firing with "Permission denied" — silent SessionStart / PreToolUse breakage. Best-effort, no-op on Windows. 2. --token-file + AGNES_TOKEN. agnes init now accepts `--token-file <path>` and an `AGNES_TOKEN` env fallback alongside `--token`. Precedence: --token > --token-file > AGNES_TOKEN. The file / env-var paths dodge Claude Code's auto-classifier, which sometimes flags a long bearer token in `--token "eyJ..."` command line as a credential- exfil pattern. The pasted setup script now uses `--token-file ~/.agnes/token` (token written via single-quoted heredoc, umask 077) for the same reason. 3. Bash(agnes *) in allow. Default `.claude/settings.json` permissions. allow seeded by agnes init now includes `Bash(agnes *)` alongside the bare `Bash` entry, so Claude Code's classifier sees an explicit allow for subsequent `agnes <verb>` calls inside the workspace it just bootstrapped. 4. .zshrc PATH dedup. Setup-script step 1's PATH-persist snippet (no-CA install path) replaced with a `grep -qF + ||` idiom so a re-run doesn't append a duplicate `export PATH=...` line. Fixed- string match (not regex) per the dedup-bug report. 5. `!` prefix doc note. Setup-script step 3 now explicitly tells the user: if Claude Code blocks an `agnes` command, prefix it with `!` (e.g. `! agnes init …`) to run the command directly in the shell, bypassing the auto-classifier. * release: 0.55.1 — /home onboarding install-hero rework + agnes init UX hardening --------- Co-authored-by: ZdenekSrotyr <zdenek.srotyr@keboola.com>
413 lines
17 KiB
Python
413 lines
17 KiB
Python
"""``get_home_route`` and the ``/`` redirect chain.
|
|
|
|
Resolution order is env > yaml > default ``/dashboard``. The env path is
|
|
the Terraform-overrideable knob — operators set ``AGNES_HOME_ROUTE`` on
|
|
the VM without forking instance.yaml. Bad values fall through to the
|
|
default rather than producing an external-host redirect.
|
|
"""
|
|
|
|
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!!")
|
|
# Ensure the env-var override is unset between tests.
|
|
monkeypatch.delenv("AGNES_HOME_ROUTE", raising=False)
|
|
yield tmp
|
|
|
|
|
|
def _make_user_and_session(conn, email="u@example.com"):
|
|
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])
|
|
return uid, create_access_token(user_id=uid, email=email)
|
|
|
|
|
|
def _client():
|
|
from fastapi.testclient import TestClient
|
|
from app.main import app
|
|
|
|
return TestClient(app, follow_redirects=False)
|
|
|
|
|
|
def test_default_home_route_is_dashboard(fresh_db, monkeypatch):
|
|
monkeypatch.delenv("AGNES_HOME_ROUTE", raising=False)
|
|
from app.instance_config import get_home_route
|
|
assert get_home_route() == "/dashboard"
|
|
|
|
|
|
def test_env_overrides_default(fresh_db, monkeypatch):
|
|
monkeypatch.setenv("AGNES_HOME_ROUTE", "/home")
|
|
from app.instance_config import get_home_route
|
|
assert get_home_route() == "/home"
|
|
|
|
|
|
def test_env_rejects_external_redirect(fresh_db, monkeypatch):
|
|
"""An attacker controlling the env var (or a typo) must not pivot
|
|
the root redirect to ``//evil.com`` or ``https://evil.com``."""
|
|
monkeypatch.setenv("AGNES_HOME_ROUTE", "//evil.com/path")
|
|
from app.instance_config import get_home_route
|
|
assert get_home_route() == "/dashboard"
|
|
|
|
monkeypatch.setenv("AGNES_HOME_ROUTE", "https://evil.com")
|
|
assert get_home_route() == "/dashboard"
|
|
|
|
|
|
def test_root_redirect_authed_user_uses_home_route(fresh_db, monkeypatch):
|
|
"""``GET /`` for an authenticated user redirects to the configured
|
|
home route, not the hard-coded ``/dashboard``."""
|
|
monkeypatch.setenv("AGNES_HOME_ROUTE", "/home")
|
|
|
|
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()
|
|
|
|
c = _client()
|
|
resp = c.get("/", cookies={"access_token": sess})
|
|
assert resp.status_code == 302
|
|
assert resp.headers["location"] == "/home"
|
|
|
|
|
|
def test_root_redirect_unauthed_goes_to_login(fresh_db):
|
|
c = _client()
|
|
resp = c.get("/")
|
|
assert resp.status_code == 302
|
|
assert resp.headers["location"] == "/login"
|
|
|
|
|
|
def test_instance_admin_email_default_empty(fresh_db, monkeypatch):
|
|
"""Unset env + unset YAML → empty string. Template branches on
|
|
truthiness so empty hides the GWS Email-admin button cleanly."""
|
|
monkeypatch.delenv("AGNES_INSTANCE_ADMIN_EMAIL", raising=False)
|
|
from app.instance_config import get_instance_admin_email
|
|
assert get_instance_admin_email() == ""
|
|
|
|
|
|
def test_instance_admin_email_env_overrides(fresh_db, monkeypatch):
|
|
"""env var takes precedence over YAML / default."""
|
|
monkeypatch.setenv("AGNES_INSTANCE_ADMIN_EMAIL", "ops@example.com")
|
|
from app.instance_config import get_instance_admin_email
|
|
assert get_instance_admin_email() == "ops@example.com"
|
|
|
|
|
|
def test_instance_admin_email_strips_whitespace(fresh_db, monkeypatch):
|
|
"""Operator quoting habits ("` ops@example.com `") shouldn't break the
|
|
mailto link — strip surrounding whitespace at the resolver."""
|
|
monkeypatch.setenv("AGNES_INSTANCE_ADMIN_EMAIL", " ops@example.com ")
|
|
from app.instance_config import get_instance_admin_email
|
|
assert get_instance_admin_email() == "ops@example.com"
|
|
|
|
|
|
def test_instance_admin_email_empty_env_treated_as_unset(fresh_db, monkeypatch):
|
|
"""Empty-string env var is intentional opt-out, not garbage."""
|
|
monkeypatch.setenv("AGNES_INSTANCE_ADMIN_EMAIL", "")
|
|
from app.instance_config import get_instance_admin_email
|
|
assert get_instance_admin_email() == ""
|
|
|
|
|
|
def test_gws_oauth_default_unset(fresh_db, monkeypatch):
|
|
monkeypatch.delenv("AGNES_GWS_CLIENT_ID", raising=False)
|
|
monkeypatch.delenv("AGNES_GWS_CLIENT_SECRET", raising=False)
|
|
from app.instance_config import get_gws_oauth_credentials
|
|
creds = get_gws_oauth_credentials()
|
|
assert creds["configured"] is False
|
|
assert creds["client_id"] == ""
|
|
assert creds["client_secret"] == ""
|
|
# OAUTHLIB_INSECURE_TRANSPORT defaults to "1" (gws CLI uses HTTP loopback)
|
|
assert creds["oauthlib_insecure_transport"] == "1"
|
|
|
|
|
|
def test_gws_oauth_env_overrides(fresh_db, monkeypatch):
|
|
monkeypatch.setenv("AGNES_GWS_CLIENT_ID", "abc.apps.googleusercontent.com")
|
|
monkeypatch.setenv("AGNES_GWS_CLIENT_SECRET", "GOCSPX-secret")
|
|
from app.instance_config import get_gws_oauth_credentials
|
|
creds = get_gws_oauth_credentials()
|
|
assert creds["configured"] is True
|
|
assert creds["client_id"] == "abc.apps.googleusercontent.com"
|
|
assert creds["client_secret"] == "GOCSPX-secret"
|
|
|
|
|
|
def test_gws_oauth_project_id_derived_from_client_id(fresh_db, monkeypatch):
|
|
"""Numeric project_id is the prefix of the client_id before the first '-'.
|
|
Required by the gws CLI's client_secret.json schema (non-Option in Rust)."""
|
|
monkeypatch.setenv(
|
|
"AGNES_GWS_CLIENT_ID", "123456789012-abcd5678efgh.apps.googleusercontent.com"
|
|
)
|
|
monkeypatch.setenv("AGNES_GWS_CLIENT_SECRET", "GOCSPX-x")
|
|
monkeypatch.delenv("AGNES_GWS_PROJECT_ID", raising=False)
|
|
from app.instance_config import get_gws_oauth_credentials
|
|
assert get_gws_oauth_credentials()["project_id"] == "123456789012"
|
|
|
|
|
|
def test_gws_oauth_project_id_explicit_override(fresh_db, monkeypatch):
|
|
"""Explicit AGNES_GWS_PROJECT_ID wins over the derived value — covers
|
|
edge cases where the client_id doesn't contain a numeric prefix."""
|
|
monkeypatch.setenv(
|
|
"AGNES_GWS_CLIENT_ID", "abc-x.apps.googleusercontent.com"
|
|
)
|
|
monkeypatch.setenv("AGNES_GWS_CLIENT_SECRET", "GOCSPX-x")
|
|
monkeypatch.setenv("AGNES_GWS_PROJECT_ID", "explicit-id")
|
|
from app.instance_config import get_gws_oauth_credentials
|
|
assert get_gws_oauth_credentials()["project_id"] == "explicit-id"
|
|
|
|
|
|
def test_gws_oauth_half_configured_falls_back(fresh_db, monkeypatch):
|
|
"""Only client_id, no secret → not configured. Half-configuration must
|
|
not engage the shortcut branch."""
|
|
monkeypatch.setenv("AGNES_GWS_CLIENT_ID", "abc.apps.googleusercontent.com")
|
|
monkeypatch.delenv("AGNES_GWS_CLIENT_SECRET", raising=False)
|
|
from app.instance_config import get_gws_oauth_credentials
|
|
assert get_gws_oauth_credentials()["configured"] is False
|
|
|
|
|
|
def test_home_renders_configured_gws_branch(fresh_db, monkeypatch):
|
|
"""Configured branch writes ~/.config/gws/client_secret.json directly
|
|
instead of exporting env vars. Claude Code's security layer redacts
|
|
env vars whose name contains 'SECRET', so the file-write path is the
|
|
only reliable way to seed the OAuth app credentials.
|
|
|
|
The gws prompt body now flows through Jinja's autoescape (the template
|
|
moved from inline `<code>` text to a `{{ connector_prompts.gws }}`
|
|
expression after the connector-prompts extraction). That means `"`
|
|
characters render as `"` in the served HTML — the browser
|
|
un-escapes them on read, but the raw response body has the entity-
|
|
encoded form. So the test un-escapes before substring-matching."""
|
|
import html as _html
|
|
|
|
monkeypatch.setenv(
|
|
"AGNES_GWS_CLIENT_ID", "123456789012-abcd5678efgh.apps.googleusercontent.com"
|
|
)
|
|
monkeypatch.setenv("AGNES_GWS_CLIENT_SECRET", "GOCSPX-secret-xyz")
|
|
|
|
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()
|
|
|
|
c = _client()
|
|
resp = c.get("/home", cookies={"access_token": sess})
|
|
assert resp.status_code == 200
|
|
body = _html.unescape(resp.text)
|
|
# Configured branch — JSON file path
|
|
assert "~/.config/gws/client_secret.json" in body
|
|
assert '"client_id": "123456789012-abcd5678efgh.apps.googleusercontent.com"' in body
|
|
assert '"client_secret": "GOCSPX-secret-xyz"' in body
|
|
# Project ID derived from client_id prefix
|
|
assert '"project_id": "123456789012"' in body
|
|
# Full read+write scopes — no --readonly flag (Agnes needs Drive/Gmail
|
|
# write so the agent can create, edit, and send on the user's behalf).
|
|
assert "gws auth login --readonly" not in body
|
|
assert "OAUTHLIB_INSECURE_TRANSPORT=1 gws auth login" in body
|
|
# Manual-setup walkthrough should NOT appear in the configured branch
|
|
assert "Run `gws auth setup` for me" not in body
|
|
# Old env-var approach should not leak back in
|
|
assert "export GOOGLE_WORKSPACE_CLI_CLIENT_SECRET=" not in body
|
|
|
|
|
|
def test_home_renders_manual_gws_branch_when_unset(fresh_db, monkeypatch):
|
|
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()
|
|
|
|
c = _client()
|
|
resp = c.get("/home", cookies={"access_token": sess})
|
|
assert resp.status_code == 200
|
|
body = resp.text
|
|
# Manual setup walkthrough renders
|
|
assert "Run `gws auth setup` for me" in body
|
|
# No leaked client_id placeholder
|
|
assert "GOOGLE_WORKSPACE_CLI_CLIENT_ID=" not in body
|
|
|
|
|
|
def test_home_automode_default_show(fresh_db, monkeypatch):
|
|
monkeypatch.delenv("AGNES_HOME_SHOW_AUTOMODE", raising=False)
|
|
from app.instance_config import get_home_automode_visibility
|
|
assert get_home_automode_visibility() is True
|
|
|
|
|
|
def test_home_automode_env_can_hide(fresh_db, monkeypatch):
|
|
monkeypatch.setenv("AGNES_HOME_SHOW_AUTOMODE", "0")
|
|
from app.instance_config import get_home_automode_visibility
|
|
assert get_home_automode_visibility() is False
|
|
|
|
|
|
def test_home_renders_automode_block_by_default(fresh_db, monkeypatch):
|
|
"""The permission-mode step renders by default for the not-onboarded
|
|
/home view. The block is Step 3 (folder creation moved up to Step 2
|
|
so the user mkdir+cd's first, then this step launches Claude in that
|
|
directory with the right flag for Step 4's ~20 shell commands).
|
|
Label primarily recommends `claude --dangerously-skip-permissions`
|
|
via the standard `.install-cmd` + copy-button affordance; auto-
|
|
accept-edits via Shift + Tab kept as the strict fallback for users
|
|
who want to review each command."""
|
|
monkeypatch.delenv("AGNES_HOME_SHOW_AUTOMODE", 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()
|
|
|
|
c = _client()
|
|
body = c.get("/home", cookies={"access_token": sess}).text
|
|
assert "Step 3 — start Claude Code with permission-skip" in body
|
|
# Recommended path: `claude --dangerously-skip-permissions`.
|
|
assert "claude --dangerously-skip-permissions" in body
|
|
# Strict fallback: Shift + Tab → auto-accept-edits.
|
|
assert "Shift + Tab" in body
|
|
|
|
|
|
def test_home_hides_automode_block_when_env_off(fresh_db, monkeypatch):
|
|
monkeypatch.setenv("AGNES_HOME_SHOW_AUTOMODE", "0")
|
|
|
|
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()
|
|
|
|
c = _client()
|
|
body = c.get("/home", cookies={"access_token": sess}).text
|
|
assert "Step 3 — start Claude Code with permission-skip" not in body
|
|
|
|
|
|
def test_navbar_home_link_uses_home_route(fresh_db, monkeypatch):
|
|
"""The shared navbar's primary "Home" link respects
|
|
``AGNES_HOME_ROUTE`` so a single env flip routes it to /home or
|
|
/dashboard. Tested by rendering an authed page and grepping the
|
|
rendered HTML — keeps the assertion close to what users see."""
|
|
monkeypatch.setenv("AGNES_HOME_ROUTE", "/home")
|
|
|
|
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()
|
|
|
|
c = _client()
|
|
# /home page itself renders the shared header.
|
|
resp = c.get("/home", cookies={"access_token": sess})
|
|
assert resp.status_code == 200
|
|
# Navbar link href reflects the resolved home_route, not hard-coded /dashboard.
|
|
# Label is "Home" (was "Dashboard" before the nav reorg).
|
|
assert 'href="/home">Home' in resp.text
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Atlassian base URL — operator-provisioned site root, Terraform-overrideable
|
|
# via AGNES_ATLASSIAN_BASE_URL.
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def test_atlassian_base_url_default_empty(fresh_db, monkeypatch):
|
|
"""Unset env + unset YAML → empty string. Connector prompt falls
|
|
back to asking the user for the site URL (the existing flow)."""
|
|
monkeypatch.delenv("AGNES_ATLASSIAN_BASE_URL", raising=False)
|
|
from app.instance_config import get_atlassian_base_url
|
|
assert get_atlassian_base_url() == ""
|
|
|
|
|
|
def test_atlassian_base_url_env_overrides(fresh_db, monkeypatch):
|
|
"""Env var takes precedence over YAML / default."""
|
|
monkeypatch.setenv("AGNES_ATLASSIAN_BASE_URL", "https://acme.atlassian.net")
|
|
from app.instance_config import get_atlassian_base_url
|
|
assert get_atlassian_base_url() == "https://acme.atlassian.net"
|
|
|
|
|
|
def test_atlassian_base_url_strips_trailing_slash(fresh_db, monkeypatch):
|
|
"""`https://acme.atlassian.net/` → `https://acme.atlassian.net`.
|
|
Matches the per-user helper script's normalization at storage time
|
|
(atlassian_prompt step 4 guard 2). Without this, $BASE_URL/rest/...
|
|
becomes $BASE_URL//rest/... which some CDN paths reject."""
|
|
monkeypatch.setenv("AGNES_ATLASSIAN_BASE_URL", "https://acme.atlassian.net/")
|
|
from app.instance_config import get_atlassian_base_url
|
|
assert get_atlassian_base_url() == "https://acme.atlassian.net"
|
|
|
|
|
|
def test_atlassian_base_url_strips_trailing_wiki(fresh_db, monkeypatch):
|
|
"""`https://acme.atlassian.net/wiki` (the Confluence path) →
|
|
`https://acme.atlassian.net` (bare site root). The connector
|
|
prompt's verify step probes both Jira (root) and Confluence
|
|
(root + /wiki), so the canonical stored value is the root."""
|
|
monkeypatch.setenv("AGNES_ATLASSIAN_BASE_URL", "https://acme.atlassian.net/wiki")
|
|
from app.instance_config import get_atlassian_base_url
|
|
assert get_atlassian_base_url() == "https://acme.atlassian.net"
|
|
|
|
monkeypatch.setenv("AGNES_ATLASSIAN_BASE_URL", "https://acme.atlassian.net/wiki/")
|
|
assert get_atlassian_base_url() == "https://acme.atlassian.net"
|
|
|
|
|
|
def test_atlassian_prompt_uses_base_url_when_set():
|
|
"""The atlassian connector prompt bakes the operator's base URL into
|
|
the helper script instead of asking the user. Saves a chat round-
|
|
trip and avoids the "guess your org's Atlassian URL" footgun."""
|
|
from app.web.connector_prompts import atlassian_prompt
|
|
|
|
p = atlassian_prompt(base_url="https://acme.atlassian.net")
|
|
# The literal URL is baked into the prompt body.
|
|
assert "https://acme.atlassian.net" in p
|
|
# The "ask me for the site URL" step disappears.
|
|
assert "Ask me for my Atlassian Cloud site URL" not in p
|
|
# The placeholder in step 4's helper-script body is replaced with the literal.
|
|
assert "<the site URL I gave you>" not in p
|
|
# The new "operator baked it in" wording appears in step 1.
|
|
assert "already provisioned by the Agnes operator" in p
|
|
|
|
|
|
def test_atlassian_prompt_asks_user_when_base_url_empty():
|
|
"""When no operator override is set, prompt falls back to the
|
|
existing "ask me for the site URL" flow — no regression for OSS
|
|
instances that don't set the env var."""
|
|
from app.web.connector_prompts import atlassian_prompt
|
|
|
|
p = atlassian_prompt(base_url="")
|
|
assert "Ask me for my Atlassian Cloud site URL" in p
|
|
assert "<the site URL I gave you>" in p
|
|
assert "already provisioned by the Agnes operator" not in p
|
|
|
|
|
|
def test_all_connector_prompts_threads_atlassian_base_url():
|
|
"""all_connector_prompts() must forward the atlassian_base_url
|
|
kwarg to atlassian_prompt — otherwise the operator's Terraform
|
|
override never reaches the rendered text."""
|
|
from app.web.connector_prompts import all_connector_prompts
|
|
|
|
out = all_connector_prompts(atlassian_base_url="https://acme.atlassian.net")
|
|
assert "https://acme.atlassian.net" in out["atlassian"]
|
|
assert "Ask me for my Atlassian Cloud site URL" not in out["atlassian"]
|