294 lines
9.6 KiB
Python
294 lines
9.6 KiB
Python
from __future__ import annotations
|
|
|
|
import io
|
|
import json
|
|
from email.message import Message
|
|
from urllib.error import HTTPError
|
|
|
|
from src.services import agent_market_watch
|
|
from src.services.agent_market_watch import (
|
|
FetchedSource,
|
|
fetch_url,
|
|
run_agent_market_watch,
|
|
)
|
|
|
|
|
|
def test_market_watch_detects_version_change_without_approving_replacement():
|
|
registry = {
|
|
"schema_version": "agent_market_watch_sources_v1",
|
|
"updated_at": "2026-06-02",
|
|
"cadence": {
|
|
"weekly_market_watch": "weekly",
|
|
"monthly_integration_review": "monthly",
|
|
"trigger_on_major_version": True,
|
|
},
|
|
"policy": {
|
|
"replacement_decision_allowed": False,
|
|
"integration_requires_replay": True,
|
|
"paid_provider_requires_approval": True,
|
|
"new_dependency_requires_approval": True,
|
|
},
|
|
"candidates": [
|
|
{
|
|
"candidate_id": "langgraph_incident_kernel",
|
|
"display_name": "LangGraph",
|
|
"evaluation_priority": "must_test",
|
|
"recommended_role": "workflow kernel",
|
|
"requires_cost_approval": False,
|
|
"requires_dependency_approval": True,
|
|
"sources": [
|
|
{
|
|
"source_id": "langgraph_pypi",
|
|
"type": "pypi",
|
|
"url": "https://pypi.org/pypi/langgraph/json",
|
|
"reference_version": "1.0.0",
|
|
}
|
|
],
|
|
}
|
|
],
|
|
}
|
|
|
|
def fetcher(_url: str, _timeout: int) -> FetchedSource:
|
|
payload = {
|
|
"info": {"version": "1.1.0"},
|
|
"releases": {
|
|
"1.1.0": [{"upload_time_iso_8601": "2026-06-02T01:02:03Z"}]
|
|
},
|
|
}
|
|
return FetchedSource(status="ok", http_status=200, body=json.dumps(payload).encode())
|
|
|
|
report = run_agent_market_watch(
|
|
registry,
|
|
registry_path="registry.json",
|
|
mode="live",
|
|
fetcher=fetcher,
|
|
generated_at="2026-06-02T00:00:00+00:00",
|
|
)
|
|
|
|
assert report["summary"]["changed_candidates"] == 1
|
|
assert report["summary"]["integration_queue_count"] == 1
|
|
assert report["policy"]["replacement_decision_allowed"] is False
|
|
candidate = report["candidates"][0]
|
|
assert candidate["changed"] is True
|
|
assert candidate["decision"] == "changed_requires_replay_readiness_review"
|
|
assert "run_offline_replay_before_shadow" in candidate["recommended_actions"]
|
|
assert report["integration_queue"][0]["required_next_gate"] == (
|
|
"refresh_market_scorecard_then_offline_replay"
|
|
)
|
|
assert report["integration_queue"][0]["requires_dependency_approval"] is True
|
|
|
|
|
|
def test_market_watch_offline_mode_skips_network():
|
|
registry = {
|
|
"schema_version": "agent_market_watch_sources_v1",
|
|
"cadence": {
|
|
"weekly_market_watch": "weekly",
|
|
"monthly_integration_review": "monthly",
|
|
"trigger_on_major_version": True,
|
|
},
|
|
"policy": {
|
|
"replacement_decision_allowed": False,
|
|
"integration_requires_replay": True,
|
|
"paid_provider_requires_approval": True,
|
|
"new_dependency_requires_approval": True,
|
|
},
|
|
"candidates": [
|
|
{
|
|
"candidate_id": "openai_agents_sdk_coordinator",
|
|
"display_name": "OpenAI",
|
|
"evaluation_priority": "must_test",
|
|
"recommended_role": "coordinator",
|
|
"sources": [
|
|
{
|
|
"source_id": "openai_docs",
|
|
"type": "docs",
|
|
"url": "https://example.invalid",
|
|
}
|
|
],
|
|
}
|
|
],
|
|
}
|
|
|
|
def fetcher(_url: str, _timeout: int) -> FetchedSource:
|
|
raise AssertionError("offline mode must not fetch")
|
|
|
|
report = run_agent_market_watch(
|
|
registry,
|
|
registry_path="registry.json",
|
|
mode="offline",
|
|
fetcher=fetcher,
|
|
generated_at="2026-06-02T00:00:00+00:00",
|
|
)
|
|
|
|
assert report["summary"]["changed_candidates"] == 0
|
|
assert report["summary"]["integration_queue_count"] == 0
|
|
assert report["candidates"][0]["sources"][0]["status"] == "skipped_offline"
|
|
|
|
|
|
def test_fetch_url_follows_permanent_redirect(monkeypatch):
|
|
class Response:
|
|
status = 200
|
|
|
|
def __enter__(self):
|
|
return self
|
|
|
|
def __exit__(self, *_args):
|
|
return False
|
|
|
|
def read(self):
|
|
return b'{"ok": true}'
|
|
|
|
calls: list[str] = []
|
|
|
|
def fake_urlopen(request, timeout: int):
|
|
calls.append(request.full_url)
|
|
if request.full_url == "https://example.com/start":
|
|
headers = Message()
|
|
headers["Location"] = "/final"
|
|
raise HTTPError(
|
|
request.full_url,
|
|
308,
|
|
"Permanent Redirect",
|
|
headers,
|
|
io.BytesIO(b"redirect"),
|
|
)
|
|
assert timeout == 12
|
|
return Response()
|
|
|
|
monkeypatch.setattr(agent_market_watch, "urlopen", fake_urlopen)
|
|
|
|
fetched = fetch_url("https://example.com/start", 12)
|
|
|
|
assert fetched.status == "ok"
|
|
assert fetched.http_status == 200
|
|
assert fetched.body == b'{"ok": true}'
|
|
assert calls == ["https://example.com/start", "https://example.com/final"]
|
|
|
|
|
|
def test_docs_hash_ignores_dynamic_script_noise():
|
|
registry = {
|
|
"schema_version": "agent_market_watch_sources_v1",
|
|
"cadence": {
|
|
"weekly_market_watch": "weekly",
|
|
"monthly_integration_review": "monthly",
|
|
"trigger_on_major_version": True,
|
|
},
|
|
"policy": {
|
|
"replacement_decision_allowed": False,
|
|
"integration_requires_replay": True,
|
|
"paid_provider_requires_approval": True,
|
|
"new_dependency_requires_approval": True,
|
|
},
|
|
"candidates": [
|
|
{
|
|
"candidate_id": "docs_candidate",
|
|
"display_name": "Docs Candidate",
|
|
"sources": [
|
|
{
|
|
"source_id": "docs",
|
|
"type": "docs",
|
|
"url": "https://example.com/docs",
|
|
}
|
|
],
|
|
}
|
|
],
|
|
}
|
|
bodies = [
|
|
b"<html><title>Agent Docs</title><script>nonce='one'</script><main>Stable contract text</main></html>",
|
|
b"<html><title>Agent Docs</title><script>nonce='two'</script><main>Stable contract text</main></html>",
|
|
]
|
|
|
|
def first_fetcher(_url: str, _timeout: int) -> FetchedSource:
|
|
return FetchedSource(status="ok", http_status=200, body=bodies[0])
|
|
|
|
first_report = run_agent_market_watch(
|
|
registry,
|
|
registry_path="registry.json",
|
|
mode="live",
|
|
fetcher=first_fetcher,
|
|
generated_at="2026-06-02T00:00:00+00:00",
|
|
)
|
|
|
|
def second_fetcher(_url: str, _timeout: int) -> FetchedSource:
|
|
return FetchedSource(status="ok", http_status=200, body=bodies[1])
|
|
|
|
second_report = run_agent_market_watch(
|
|
registry,
|
|
registry_path="registry.json",
|
|
mode="live",
|
|
previous_report=first_report,
|
|
fetcher=second_fetcher,
|
|
generated_at="2026-06-02T00:00:00+00:00",
|
|
)
|
|
|
|
assert second_report["summary"]["changed_candidates"] == 0
|
|
assert second_report["candidates"][0]["sources"][0]["changed_since_reference"] is False
|
|
|
|
|
|
def test_versioned_source_ignores_metadata_hash_noise_when_version_is_unchanged():
|
|
registry = {
|
|
"schema_version": "agent_market_watch_sources_v1",
|
|
"cadence": {
|
|
"weekly_market_watch": "weekly",
|
|
"monthly_integration_review": "monthly",
|
|
"trigger_on_major_version": True,
|
|
},
|
|
"policy": {
|
|
"replacement_decision_allowed": False,
|
|
"integration_requires_replay": True,
|
|
"paid_provider_requires_approval": True,
|
|
"new_dependency_requires_approval": True,
|
|
},
|
|
"candidates": [
|
|
{
|
|
"candidate_id": "versioned_candidate",
|
|
"display_name": "Versioned Candidate",
|
|
"sources": [
|
|
{
|
|
"source_id": "pypi",
|
|
"type": "pypi",
|
|
"url": "https://example.com/pypi.json",
|
|
}
|
|
],
|
|
}
|
|
],
|
|
}
|
|
previous_report = {
|
|
"candidates": [
|
|
{
|
|
"candidate_id": "versioned_candidate",
|
|
"sources": [
|
|
{
|
|
"source_id": "pypi",
|
|
"version": "1.2.3",
|
|
"content_hash": "old-hash",
|
|
}
|
|
],
|
|
}
|
|
]
|
|
}
|
|
|
|
def fetcher(_url: str, _timeout: int) -> FetchedSource:
|
|
payload = {
|
|
"info": {"version": "1.2.3"},
|
|
"releases": {
|
|
"1.2.3": [{"upload_time_iso_8601": "2026-06-02T01:02:03Z"}],
|
|
"0.0.1": [{"upload_time_iso_8601": "2025-01-01T00:00:00Z"}],
|
|
},
|
|
"volatile_metadata": "changed package json body",
|
|
}
|
|
return FetchedSource(status="ok", http_status=200, body=json.dumps(payload).encode())
|
|
|
|
report = run_agent_market_watch(
|
|
registry,
|
|
registry_path="registry.json",
|
|
mode="live",
|
|
previous_report=previous_report,
|
|
fetcher=fetcher,
|
|
generated_at="2026-06-04T00:00:00+00:00",
|
|
)
|
|
|
|
assert report["summary"]["changed_candidates"] == 0
|
|
assert report["candidates"][0]["sources"][0]["version"] == "1.2.3"
|
|
assert report["candidates"][0]["sources"][0]["changed_since_reference"] is False
|