186 lines
6.5 KiB
Python
186 lines
6.5 KiB
Python
from __future__ import annotations
|
|
|
|
import importlib.util
|
|
import json
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import httpx
|
|
import pytest
|
|
|
|
|
|
PLUGIN = Path(__file__).resolve().parents[1] / "plugin" / "m2-solution-scout" / "__init__.py"
|
|
|
|
|
|
@pytest.fixture()
|
|
def scout(monkeypatch, tmp_path):
|
|
spec = importlib.util.spec_from_file_location("m2_solution_scout_test", PLUGIN)
|
|
module = importlib.util.module_from_spec(spec)
|
|
sys.modules[spec.name] = module
|
|
assert spec.loader is not None
|
|
spec.loader.exec_module(module)
|
|
monkeypatch.setattr(module, "STATE_PATH", tmp_path / "state.json")
|
|
monkeypatch.setattr(module, "STATE_DIR", tmp_path)
|
|
monkeypatch.setattr(module, "OUTBOX_DIR", tmp_path / "outbox")
|
|
return module
|
|
|
|
|
|
def policy(scout):
|
|
return {
|
|
"enabled": True,
|
|
"desktop_id": "chris-m2o",
|
|
"operator_id": "op_chris",
|
|
"tenant_id": "machine-machine",
|
|
"min_score": 0.78,
|
|
"min_coverage": 0.2,
|
|
"max_proposals_per_day": 5,
|
|
"max_proposals_per_session": 1,
|
|
"proposal_cooldown_minutes": 45,
|
|
"summary": {
|
|
"max_source_chars": 12000,
|
|
"max_summary_chars": 120,
|
|
"redact_client_identifiers": True,
|
|
},
|
|
"catalog": {
|
|
"memory_api_url": "https://memory.test",
|
|
"agent_id": "market:catalog",
|
|
"limit": 5,
|
|
"routing_strategy": "standard",
|
|
},
|
|
"store": {"deeplink_scheme": "m2store"},
|
|
}
|
|
|
|
|
|
def test_gate_disabled_opted_out_rate_capped_and_cooldown_noop(scout):
|
|
enabled = policy(scout)
|
|
assert scout.gate({"enabled": False}, {}, session_id="s1") == (False, "disabled")
|
|
assert scout.load_policy(Path("/no/such/file")) == {
|
|
"enabled": False,
|
|
"reason": "disabled_or_missing_tenant",
|
|
}
|
|
today = scout._utcnow().date().isoformat()
|
|
assert scout.gate(
|
|
enabled,
|
|
{"daily": {today: 5}, "sessions": {}, "last_proposal_at": None},
|
|
session_id="s1",
|
|
) == (False, "rate_capped")
|
|
assert scout.gate(
|
|
enabled,
|
|
{"daily": {}, "sessions": {"s1": 1}, "last_proposal_at": None},
|
|
session_id="s1",
|
|
) == (False, "session_capped")
|
|
assert scout.gate(
|
|
enabled,
|
|
{"daily": {}, "sessions": {}, "last_proposal_at": scout._iso()},
|
|
session_id="s1",
|
|
) == (False, "proposal_cooldown")
|
|
|
|
|
|
def test_policy_refuses_raw_sources(scout, tmp_path):
|
|
path = tmp_path / "pull-policy.toml"
|
|
path.write_text(
|
|
"""
|
|
[scout]
|
|
enabled = true
|
|
tenant_id = "machine-machine"
|
|
|
|
[scout.sources]
|
|
raw_keystrokes = true
|
|
raw_client_data = false
|
|
""",
|
|
encoding="utf-8",
|
|
)
|
|
with pytest.raises(scout.PolicyError):
|
|
scout.load_policy(path)
|
|
|
|
|
|
def test_redaction_and_summary_are_bounded(scout):
|
|
text = (
|
|
"Use sk-abcdefghijklmnopqrstuvwxyz and API_KEY=supersecretvalue for jane@example.com. "
|
|
"Build a competitor research report with sources and PDF output."
|
|
)
|
|
summary = scout.summarize_turn(text, "Prepare the report.", policy(scout))
|
|
assert "sk-" not in summary["summary"]
|
|
assert "supersecretvalue" not in summary["summary"]
|
|
assert "jane@example.com" not in summary["summary"]
|
|
assert "[REDACTED_SECRET]" in summary["summary"]
|
|
assert len(summary["summary"]) <= 120
|
|
assert summary["schema_version"] == "m2.scout.intent_summary.v1"
|
|
|
|
|
|
def test_proposal_outbox_record_shape_matches_schema(scout, tmp_path):
|
|
p = policy(scout)
|
|
summary = scout.summarize_turn("competitor research report pdf", "", p)
|
|
match = {
|
|
"listing_id": "lst_competitor-scan",
|
|
"solution_id": "sol_competitor-scan",
|
|
"score": 0.84,
|
|
"coverage": 0.72,
|
|
"listing": {
|
|
"name": "Competitor Scan",
|
|
"summary": "Research report package.",
|
|
"price": {"amount": 40, "currency": "m2cr", "model": "fixed"},
|
|
"stats": {"installs": 12, "rating": 4.6},
|
|
},
|
|
}
|
|
proposal = scout.build_proposal(match, summary, p)
|
|
event = scout.write_proposal_shown(proposal, tmp_path / "outbox")
|
|
assert event["event_type"] == "proposal_shown"
|
|
assert event["proposal"]["schema_version"] == "m2.scout.proposal.v1"
|
|
assert event["proposal"]["listing"]["listing_id"] == "lst_competitor-scan"
|
|
assert event["proposal"]["deeplink"].startswith("m2store://listing/lst_competitor-scan")
|
|
lines = list((tmp_path / "outbox").glob("*.jsonl"))[0].read_text(encoding="utf-8").splitlines()
|
|
assert json.loads(lines[0])["proposal_id"] == event["proposal_id"]
|
|
|
|
|
|
def test_mocked_catalog_search_filters_visibility_and_posts_live_shape(scout, monkeypatch):
|
|
requests = []
|
|
|
|
def handler(request: httpx.Request) -> httpx.Response:
|
|
requests.append(json.loads(request.content.decode("utf-8")))
|
|
return httpx.Response(
|
|
200,
|
|
json={
|
|
"results": [
|
|
{
|
|
"score": 0.91,
|
|
"metadata": {
|
|
"listing_id": "lst_competitor-scan",
|
|
"listing": {
|
|
"name": "Competitor Scan",
|
|
"summary": "Competitor research report with PDF output",
|
|
"keywords": ["competitor", "research", "report"],
|
|
"tenant_visibility": ["*"],
|
|
"status": "published",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
"score": 0.99,
|
|
"metadata": {
|
|
"listing_id": "lst_private",
|
|
"listing": {
|
|
"name": "Private",
|
|
"tenant_visibility": ["other-tenant"],
|
|
"status": "published",
|
|
},
|
|
},
|
|
},
|
|
],
|
|
"routing": {},
|
|
},
|
|
)
|
|
|
|
original_init = httpx.Client.__init__
|
|
|
|
def patched_init(self, *args, **kwargs):
|
|
kwargs["transport"] = httpx.MockTransport(handler)
|
|
original_init(self, *args, **kwargs)
|
|
|
|
monkeypatch.setattr(httpx.Client, "__init__", patched_init)
|
|
summary = scout.summarize_turn("competitor research report pdf", "", policy(scout))
|
|
results = scout.search_catalog(summary, policy(scout))
|
|
assert requests[0]["agent_id"] == "market:catalog"
|
|
assert requests[0]["query"] == summary["summary"]
|
|
assert "tenant_id" not in requests[0]
|
|
assert [item["listing_id"] for item in results] == ["lst_competitor-scan"]
|