Files
hermes-agent/tests/tools/test_web_tools_perplexity.py
ethernet c13ea774e6 refactor: make install-stamp.json the single runtime version identity
Runtime identity resolved through hermes_cli.__version__ (a static 0.0.0
on source installs, rewritten by release stamping) leaked v0.0.0 into
About, /api/health, User-Agents, and plugin compat, and source updates
showed "couldn't reach update server" because identity and channel
authority disagreed with the checkout.

Now: get_version_info() resolves install stamp -> live git -> unknown,
never pyproject metadata, never a package constant. Source checkouts
derive identity from their reachable release tag; the completion tail of
every successful install/update/historical takeover atomically rewrites
install-stamp.json with that identity; a stale source stamp whose commit
no longer matches HEAD defers to live git. ACP/TUI use derived_version
for display and base_version for protocol fields; all ~44 runtime
__version__ consumers migrated; hermes_cli.__version__ and generated
_version.py are gone; release stamping only touches the native manifests
external builders consume (nix/tauri/cargo) and passes release identity
straight into write_install_stamp.py; pyproject.toml stays inert 0.0.0.
Desktop no longer synthesizes a competing install-stamp.json: the
checkout owns its stamp, and desktop-bootstrap classification keys on
the bootstrap-complete marker. verify-bootstrap-version-stamp.py now
cross-checks the checkout's stamp (baseVersion + commit == HEAD).

Validation: 31-file focused suite green (version identity, stamping,
adoption, providers, gateway, acp/tui runtime identity, api server via
extras env, release graph); desktop tsc + 25 vitest green; real-repo
probe: base=unknown derived=git.0635606.dirty source=git on this
checkout; clean-env imports resolve entirely from this tree; windows
footgun + compat-pointer scans clean.
2026-09-23 11:41:01 -04:00

100 lines
4.3 KiB
Python

"""Perplexity web backend — search + snippets dispatch through the real tools."""
import asyncio
import json
import os
from unittest.mock import MagicMock, patch
from tests.tools.conftest import register_all_web_providers
def _ok(payload):
resp = MagicMock()
resp.status_code = 200
resp.json.return_value = payload
resp.text = json.dumps(payload)
return resp
def _assert_hermes_identity_headers(headers):
"""Both Perplexity endpoints carry the Hermes identity headers (same set as Kimi/OpenCode)
plus Perplexity's integration header."""
from hermes_cli.version_info import get_version_info
assert headers["HTTP-Referer"] == "https://hermes-agent.nousresearch.com"
assert headers["X-Title"] == "Hermes Agent"
assert headers["User-Agent"] == f"HermesAgent/{get_version_info().base_version}"
assert headers["X-Pplx-Integration"] == "hermes-agent"
def test_search_dispatch_maps_search_api_shape():
"""web_search on backend=perplexity hits /search with Bearer auth and maps snippet→description."""
import tools.web_tools as wt
register_all_web_providers()
payload = {
"results": [
{"title": "Bloom filter", "url": "https://en.wikipedia.org/wiki/Bloom_filter",
"snippet": "space-efficient probabilistic structure", "date": "2004-04-17"},
],
"id": "abc",
}
with patch.dict(os.environ, {"PERPLEXITY_API_KEY": "pplx-test"}), \
patch.object(wt, "_get_search_backend", return_value="perplexity"), \
patch("plugins.web.perplexity.provider.httpx.post", return_value=_ok(payload)) as post:
out = json.loads(wt.web_search_tool("bloom filter", limit=3))
assert post.call_args.args[0] == "https://api.perplexity.ai/search"
assert post.call_args.kwargs["headers"]["Authorization"] == "Bearer pplx-test"
_assert_hermes_identity_headers(post.call_args.kwargs["headers"])
body = post.call_args.kwargs["json"]
assert body["query"] == "bloom filter"
assert 1 <= body["max_results"] <= 20 # dispatcher bucket-rounds the fetch limit
assert body["search_context_size"] == "low"
assert out["success"] is True
assert out["data"]["web"][0] == {
"title": "Bloom filter",
"url": "https://en.wikipedia.org/wiki/Bloom_filter",
"description": "space-efficient probabilistic structure",
"position": 1,
}
def test_extract_dispatch_snippets_per_url_and_missing_key():
"""web_extract on backend=perplexity posts every URL to /sdk/content/snippets;
a URL the backend failed carries ``error`` instead of content; no key → error, no HTTP."""
import tools.web_tools as wt
register_all_web_providers()
urls = ["https://tokio.rs/tokio/tutorial", "https://docs.rs/smol"]
payload = {"results": [
{"url": urls[0], "text": "Tokio is an asynchronous runtime … for Rust.", "tokens_count": 12},
{"url": urls[1], "error": "Page not found or unavailable."},
]}
with patch.dict(os.environ, {"PERPLEXITY_API_KEY": "pplx-test"}), \
patch.object(wt, "_get_extract_backend", return_value="perplexity"), \
patch("plugins.web.perplexity.provider.httpx.post", return_value=_ok(payload)) as post:
out = json.loads(asyncio.run(wt.web_extract_tool(urls)))
assert post.call_args.args[0] == "https://api.perplexity.ai/sdk/content/snippets"
_assert_hermes_identity_headers(post.call_args.kwargs["headers"])
body = post.call_args.kwargs["json"]
assert body["urls"] == urls
assert body["query"] == "tokio tutorial smol"
assert body["max_tokens_per_page"] <= body["max_tokens"]
by_url = {r["url"]: r for r in out["results"]}
assert "Tokio is an asynchronous runtime" in by_url[urls[0]]["content"]
assert by_url[urls[1]]["error"] == "Page not found or unavailable."
with patch.dict(os.environ, {}, clear=False), \
patch("plugins.web.perplexity.provider.httpx.post") as post:
os.environ.pop("PERPLEXITY_API_KEY", None)
from plugins.web.perplexity.provider import PerplexityWebSearchProvider
p = PerplexityWebSearchProvider()
assert p.is_available() is False
res = p.search("x")
assert res["success"] is False and "PERPLEXITY_API_KEY" in res["error"]
docs = p.extract(["https://example.com"])
assert "PERPLEXITY_API_KEY" in docs[0]["error"]
post.assert_not_called()