ig-v1 / tests /test_deployment_modes.py
harshith99's picture
Fix: KML invalid-XML chars rejected by Google My Maps
7178492
Raw
History Blame Contribute Delete
7.64 kB
"""
Guardrail: the two deployment options must not break each other.
This project ships in exactly two deployment shapes, differentiated by
environment configuration over the SAME codebase:
1) Hosted free web (HF Spaces / Render / …)
OLLAMA_ENABLED=false, HOSTED=1, NOMINATIM_URL=<self-hosted/3rd-party>
- Ollama is never used (no local model server on a shared host) β€”
requests fall back to Claude (BYOK) / the free chatbot path.
- Bulk geocoding against PUBLIC OSM is refused (would get the IP banned).
2) Local Docker (localhost)
OLLAMA_ENABLED=true, HOSTED unset, public OSM Nominatim
- The Ollama provider is honored.
- Bulk geocoding against public OSM is allowed (single user, ~1 req/s).
These two columns are the contract. A code change that breaks either column
fails this test β€” that's the guardrail. The behaviour is encoded in two pure
helpers so it stays testable without a server or the network:
web.app.resolve_provider(...) β€” provider/model selection
pipeline.geocode.public_bulk_blocked β€” geocoder hosted guard
Run: python3 tests/test_deployment_modes.py
"""
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent))
import xml.etree.ElementTree as ET
from pipeline.chatbot import get_prompt
from pipeline.export import build_kml
from pipeline.extract import DEFAULT_MODEL, DEFAULT_OLLAMA_MODEL
from pipeline.geocode import public_bulk_blocked
from web.app import resolve_provider, safe_err, valid_latlng
PUBLIC = "https://nominatim.openstreetmap.org"
SELF_HOSTED = "http://nominatim:8080"
failures: list[str] = []
def check(label: str, got, expected) -> None:
if got != expected:
failures.append(f"{label}: expected {expected!r}, got {got!r}")
# ── Deployment 1: Hosted free web (OLLAMA_ENABLED=false, HOSTED=1) ─────────────
# Ollama must NEVER be selected on a hosted deploy β€” fall back to Claude.
check("hosted: ollama request falls back to Claude",
resolve_provider("ollama", DEFAULT_MODEL, "qwen2.5", ollama_enabled=False),
("anthropic", DEFAULT_MODEL))
check("hosted: explicit Claude model honored",
resolve_provider("anthropic", "claude-haiku-4-5", DEFAULT_OLLAMA_MODEL, ollama_enabled=False),
("anthropic", "claude-haiku-4-5"))
check("hosted: unknown Claude model defaults",
resolve_provider("anthropic", "made-up-model", DEFAULT_OLLAMA_MODEL, ollama_enabled=False),
("anthropic", DEFAULT_MODEL))
# Bulk geocoding against public OSM must be refused; a self-hosted endpoint is OK.
check("hosted: public OSM bulk geocode blocked",
public_bulk_blocked("1", PUBLIC), True)
check("hosted: 'true'/'yes' variants also block",
public_bulk_blocked("true", PUBLIC) and public_bulk_blocked("yes", PUBLIC), True)
check("hosted: self-hosted endpoint is allowed even when HOSTED=1",
public_bulk_blocked("1", SELF_HOSTED), False)
# ── Deployment 2: Local Docker (OLLAMA_ENABLED=true, HOSTED unset) ─────────────
# The Ollama provider is honored locally.
check("local: ollama request honored",
resolve_provider("ollama", DEFAULT_MODEL, "qwen2.5", ollama_enabled=True),
("ollama", "qwen2.5"))
check("local: unknown ollama model defaults",
resolve_provider("ollama", DEFAULT_MODEL, "made-up-model", ollama_enabled=True),
("ollama", DEFAULT_OLLAMA_MODEL))
check("local: anthropic still selectable",
resolve_provider("anthropic", "claude-opus-4-8", DEFAULT_OLLAMA_MODEL, ollama_enabled=True),
("anthropic", "claude-opus-4-8"))
# Geocoding against public OSM is allowed when HOSTED is unset.
check("local: public OSM geocode allowed (HOSTED unset)",
public_bulk_blocked(None, PUBLIC), False)
check("local: public OSM geocode allowed (HOSTED empty string)",
public_bulk_blocked("", PUBLIC), False)
# ── Client-side geocoding contract (hosted mode, option B) ────────────────────
# Hosted mode geocodes in the browser: the server is "geocode-blocked", which is
# exactly what flips web.app.CLIENT_GEOCODE on. Same predicate, asserted as the
# client-geocode trigger.
check("client-geocode ON when server bulk-blocked (hosted)",
public_bulk_blocked("1", PUBLIC), True)
check("client-geocode OFF for local Docker (server geocodes)",
public_bulk_blocked(None, PUBLIC), False)
# /set-coords must only accept well-formed, in-range coordinates from the browser.
check("set-coords accepts valid lat/lng",
valid_latlng("37.7749", "-122.4194"), (37.7749, -122.4194))
check("set-coords rejects non-numeric", valid_latlng("abc", "1"), None)
check("set-coords rejects out-of-range lat", valid_latlng("91", "0"), None)
check("set-coords rejects out-of-range lng", valid_latlng("0", "181"), None)
# Errors surfaced to the client must not leak an API key (BYOK).
check("safe_err redacts an Anthropic key",
"sk-ant-" in safe_err(Exception("boom sk-ant-abc123DEF_key tail")) and
"abc123DEF_key" not in safe_err(Exception("boom sk-ant-abc123DEF_key tail")),
True)
# ── Free chatbot path (primary no-key hosted provider) ────────────────────────
# get_prompt must not crash on the literal JSON braces in the template
# (regression: it used str.format() and KeyError'd) and must NOT hardcode a post
# count (regression: the prefiltered total mismatched chunked files).
try:
_prompt = get_prompt(217)
check("chatbot get_prompt renders a string", isinstance(_prompt, str) and len(_prompt) > 100, True)
check("chatbot get_prompt keeps JSON example", '"post_number"' in _prompt, True)
check("chatbot get_prompt has no leftover {post_count} token", "{post_count}" not in _prompt, True)
check("chatbot get_prompt embeds no fixed count", "217" not in _prompt, True)
check("chatbot get_prompt refers to the attached file", "attached file" in _prompt.lower(), True)
except Exception as exc: # noqa: BLE001
failures.append(f"chatbot get_prompt raised: {exc!r}")
# ── KML export must stay valid XML (Google My Maps rejects invalid XML) ────────
# Caption/name data can carry control chars that escape() doesn't strip; the KML
# must still parse. (Regression: "invalid or unsupported data" on My Maps import.)
try:
_kml = build_kml([{
"name": "Bad\x07CafΓ©", "city": "Lisbon", "country": "Portugal",
"category": "Cafe", "lat": "38.71", "lng": "-9.14",
"instagram_url": "https://instagram.com/reel/AB?x=1&y=2",
}])
ET.fromstring(_kml) # raises if not well-formed
check("KML is valid XML even with control chars", "\x07" not in _kml, True)
except Exception as exc: # noqa: BLE001
failures.append(f"build_kml produced invalid XML: {exc!r}")
# ── Report ────────────────────────────────────────────────────────────────────
print("Deployment-mode guardrail\n")
if failures:
print(f"FAIL β€” {len(failures)} contract violation(s):")
for f in failures:
print(f" βœ— {f}")
print("\nA change broke one deployment mode's contract. See the docstring "
"and DEPLOY.md for the two-column invariant.")
sys.exit(1)
print("PASS β€” both deployment modes honor their contract:")
print(" βœ“ Hosted free web: Ollama hidden β†’ Claude; public-OSM bulk geocode refused")
print(" βœ“ Local Docker: Ollama honored; public-OSM geocode allowed")
sys.exit(0)