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"]