asm-protocol 0.5.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,54 @@
1
+ """Tests for the LangChain adapter (ASMToolSelectorTool). Skipped if langchain-core absent."""
2
+ from __future__ import annotations
3
+
4
+ import sys
5
+ from pathlib import Path
6
+
7
+ import pytest
8
+
9
+ pytest.importorskip("langchain_core")
10
+
11
+ ROOT = Path(__file__).resolve().parent.parent
12
+ if str(ROOT) not in sys.path:
13
+ sys.path.insert(0, str(ROOT))
14
+ sys.path.insert(0, str(ROOT / "integrations" / "langchain"))
15
+
16
+ from asm_tools import ASMToolSelectorTool # noqa: E402
17
+
18
+
19
+ def test_selector_tool_booking_flow():
20
+ tool = ASMToolSelectorTool()
21
+ out = tool._run(
22
+ task="find and book a refundable flight",
23
+ taxonomy="tool.booking.travel",
24
+ user_platform="windows",
25
+ required_functions="flight_search,flight_order_create",
26
+ )
27
+ assert "Amadeus" in out
28
+ assert "risk_class: critical" in out
29
+ assert "approval_required: True" in out
30
+
31
+
32
+ def test_selector_tool_setup_gate():
33
+ tool = ASMToolSelectorTool()
34
+ out = tool._run(
35
+ task="get property data",
36
+ taxonomy="tool.data.real_estate",
37
+ required_functions="real_estate_data",
38
+ require_agent_completable_setup=True,
39
+ )
40
+ assert "US Census Bureau Data API" in out
41
+ assert "setup not agent-completable" in out
42
+
43
+
44
+ def test_selector_tool_is_valid_langchain_tool():
45
+ tool = ASMToolSelectorTool()
46
+ assert tool.name == "asm_tool_selector"
47
+ schema = tool.args_schema.model_json_schema()
48
+ assert "task" in schema["properties"]
49
+ # invoke via the LangChain interface, not just _run
50
+ out = tool.invoke({"task": "store a study plan with daily reminders",
51
+ "taxonomy": "tool.productivity.task_management",
52
+ "user_platform": "windows",
53
+ "required_functions": "reminders,recurring_tasks"})
54
+ assert "Selected tool:" in out
@@ -0,0 +1,104 @@
1
+ """Tests for the shared tool selector (library_select) and the hosted select API."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ import sys
6
+ import threading
7
+ import urllib.request
8
+ from http.server import ThreadingHTTPServer
9
+ from pathlib import Path
10
+
11
+ ROOT = Path(__file__).resolve().parent.parent
12
+ if str(ROOT) not in sys.path:
13
+ sys.path.insert(0, str(ROOT))
14
+
15
+ from library_select import load_library, monthly_cost, select # noqa: E402
16
+
17
+
18
+ def test_library_loads_and_validates_shape():
19
+ lib = load_library()
20
+ assert len(lib) >= 30
21
+ assert all("service_id" in m and "taxonomy" in m for m in lib)
22
+
23
+
24
+ def test_local_device_tools_filtered_for_cloud_agent():
25
+ r = select("store a study plan", taxonomy="tool.productivity.task_management",
26
+ agent_reach="cloud", user_platform="windows",
27
+ required_functions=["reminders", "recurring_tasks"])
28
+ assert r["selected"] is not None
29
+ rejected_names = {x["service"] for x in r["rejected"]}
30
+ assert "Things 3" in rejected_names and "Apple Reminders" in rejected_names
31
+
32
+
33
+ def test_booking_surfaces_critical_risk_and_approval():
34
+ r = select("book a flight", taxonomy="tool.booking.travel",
35
+ agent_reach="cloud", user_platform="windows",
36
+ required_functions=["flight_search", "flight_order_create"],
37
+ require_approval_for=["financial_charge"])
38
+ assert r["selected"] is not None
39
+ assert r["risk_class"] == "critical"
40
+ assert r["approval_required"] is True
41
+ assert "financial_charge" in r["side_effects"]
42
+
43
+
44
+ def test_agent_completable_setup_gate():
45
+ r = select("property data", taxonomy="tool.data.real_estate",
46
+ agent_reach="cloud", user_platform="windows",
47
+ required_functions=["real_estate_data"],
48
+ require_agent_completable_setup=True)
49
+ assert r["selected"]["display_name"] == "US Census Bureau Data API"
50
+ reasons = " ".join(x["reason"] for x in r["rejected"])
51
+ assert "setup not agent-completable" in reasons
52
+
53
+
54
+ def test_monthly_cost_free_tier_is_zero():
55
+ lib = load_library()
56
+ todoist = next(m for m in lib if m["service_id"].startswith("todoist/"))
57
+ assert monthly_cost(todoist) == 0.0
58
+
59
+
60
+ def test_select_api_endpoints():
61
+ import asm_select_api as api
62
+
63
+ srv = ThreadingHTTPServer(("127.0.0.1", 0), api.Handler)
64
+ port = srv.server_address[1]
65
+ threading.Thread(target=srv.serve_forever, daemon=True).start()
66
+ try:
67
+ h = json.loads(urllib.request.urlopen(f"http://127.0.0.1:{port}/healthz").read())
68
+ assert h["ok"] and h["tools"] >= 30
69
+
70
+ tools = json.loads(urllib.request.urlopen(
71
+ f"http://127.0.0.1:{port}/tools?taxonomy=tool.data.real_estate").read())
72
+ assert len(tools) == 4
73
+
74
+ body = json.dumps({"task": "book a flight", "taxonomy": "tool.booking.travel",
75
+ "user_platform": "windows",
76
+ "required_functions": ["flight_search", "flight_order_create"]}).encode()
77
+ req = urllib.request.Request(f"http://127.0.0.1:{port}/select", data=body,
78
+ headers={"Content-Type": "application/json"})
79
+ r = json.loads(urllib.request.urlopen(req).read())
80
+ assert r["selected"] is not None and r["risk_class"] == "critical"
81
+
82
+ bad = urllib.request.Request(f"http://127.0.0.1:{port}/select", data=b"{}",
83
+ headers={"Content-Type": "application/json"})
84
+ try:
85
+ urllib.request.urlopen(bad)
86
+ assert False, "expected 400"
87
+ except urllib.error.HTTPError as e:
88
+ assert e.code == 400
89
+
90
+ # .well-known/asm catalog: one generated_at, per-manifest links
91
+ cat = json.loads(urllib.request.urlopen(
92
+ f"http://127.0.0.1:{port}/.well-known/asm").read())
93
+ assert cat["count"] >= 30 and cat["generated_at"]
94
+ assert all("service_id" in e and e["url"].startswith("/manifest/")
95
+ for e in cat["manifests"])
96
+
97
+ # follow a catalog link to a full manifest
98
+ first = cat["manifests"][0]
99
+ man = json.loads(urllib.request.urlopen(
100
+ f"http://127.0.0.1:{port}{first['url']}").read())
101
+ assert man["service_id"] == first["service_id"]
102
+ assert man.get("asm_version") == "0.3"
103
+ finally:
104
+ srv.shutdown()
@@ -0,0 +1,55 @@
1
+ """Schema validation: every manifest under manifests/ and library/ must validate against
2
+ schema/asm-v0.3.schema.json. This test catches the class of regression where
3
+ new manifests use enum values not in the schema (e.g. an invalid
4
+ verification_status string).
5
+
6
+ Run:
7
+ pip install jsonschema pytest
8
+ python -m pytest scorer/test_manifests_schema.py -v
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import json
14
+ from pathlib import Path
15
+
16
+ import pytest
17
+
18
+ try:
19
+ import jsonschema
20
+ except ImportError: # pragma: no cover
21
+ jsonschema = None
22
+
23
+ ROOT = Path(__file__).resolve().parent.parent
24
+ SCHEMA_PATH = ROOT / "schema" / "asm-v0.3.schema.json"
25
+ MANIFESTS_DIR = ROOT / "manifests"
26
+ LIBRARY_DIR = ROOT / "library"
27
+
28
+
29
+ @pytest.fixture(scope="module")
30
+ def schema():
31
+ if jsonschema is None:
32
+ pytest.skip("jsonschema not installed; pip install jsonschema")
33
+ return json.loads(SCHEMA_PATH.read_text(encoding="utf-8"))
34
+
35
+
36
+ def manifest_paths():
37
+ return sorted(MANIFESTS_DIR.glob("*.asm.json")) + sorted(LIBRARY_DIR.rglob("*.asm.json"))
38
+
39
+
40
+ def test_at_least_one_manifest():
41
+ paths = manifest_paths()
42
+ assert len(paths) > 0, "no manifests found under manifests/"
43
+
44
+
45
+ @pytest.mark.parametrize("manifest_path", manifest_paths(), ids=lambda p: p.name)
46
+ def test_manifest_validates(manifest_path: Path, schema):
47
+ document = json.loads(manifest_path.read_text(encoding="utf-8"))
48
+ jsonschema.validate(document, schema)
49
+
50
+
51
+ def test_no_extra_unknown_versions():
52
+ """Sanity: every manifest declares asm_version 0.3."""
53
+ for path in manifest_paths():
54
+ d = json.loads(path.read_text(encoding="utf-8"))
55
+ assert d.get("asm_version") == "0.3", f"{path.name} declares asm_version={d.get('asm_version')}"
@@ -0,0 +1,84 @@
1
+ #!/usr/bin/env python3
2
+ """Tests for ASM metadata embedded in MCP Registry server.json files."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import json
7
+ import sys
8
+ from pathlib import Path
9
+
10
+ ROOT = Path(__file__).resolve().parent.parent
11
+ if str(ROOT) not in sys.path:
12
+ sys.path.insert(0, str(ROOT))
13
+
14
+ from mcp_server_json_asm import PUBLISHER_META_KEY, inspect_server_json, validate_manifest
15
+
16
+
17
+ EXAMPLES = ROOT / "examples" / "mcp-server-json"
18
+
19
+
20
+ def test_valid_server_json_with_inline_asm_passes():
21
+ result = inspect_server_json(EXAMPLES / "remote-with-asm.server.json")
22
+
23
+ assert result.has_asm is True
24
+ assert result.valid_asm is True
25
+ assert result.can_convert is True
26
+ assert result.asm_service_id == "example/remote-search@1.0"
27
+ assert result.asm_taxonomy == "tool.data.search"
28
+ assert result.missing_recommended_fields == []
29
+
30
+
31
+ def test_missing_asm_block_is_warning_not_error(tmp_path):
32
+ server_json = tmp_path / "server.json"
33
+ server_json.write_text(
34
+ json.dumps({
35
+ "name": "io.asm.example/no-asm",
36
+ "description": "No ASM metadata yet",
37
+ "_meta": {
38
+ PUBLISHER_META_KEY: {
39
+ "maintainer": "example"
40
+ }
41
+ },
42
+ }),
43
+ encoding="utf-8",
44
+ )
45
+
46
+ result = inspect_server_json(server_json)
47
+
48
+ assert result.has_asm is False
49
+ assert result.valid_asm is False
50
+ assert result.errors == []
51
+ assert result.warnings
52
+
53
+
54
+ def test_malformed_asm_reports_schema_errors(tmp_path):
55
+ server_json = tmp_path / "server.json"
56
+ server_json.write_text(
57
+ json.dumps({
58
+ "name": "io.asm.example/bad-asm",
59
+ "_meta": {
60
+ PUBLISHER_META_KEY: {
61
+ "asm": {
62
+ "asm_version": "0.3",
63
+ "service_id": "example/bad@1.0"
64
+ }
65
+ }
66
+ },
67
+ }),
68
+ encoding="utf-8",
69
+ )
70
+
71
+ result = inspect_server_json(server_json)
72
+
73
+ assert result.has_asm is True
74
+ assert result.valid_asm is False
75
+ assert result.can_convert is False
76
+ assert any("taxonomy" in error for error in result.errors)
77
+
78
+
79
+ def test_extracted_manifest_validates_against_schema():
80
+ result = inspect_server_json(EXAMPLES / "package-with-asm.server.json")
81
+
82
+ assert result.asm_manifest is not None
83
+ assert validate_manifest(result.asm_manifest) == []
84
+ assert result.asm_manifest["service_id"] == "example/vector-db@1.0"
@@ -0,0 +1,238 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from pathlib import Path
5
+
6
+ import pytest
7
+ from jsonschema import Draft202012Validator
8
+
9
+ from asm_cli import main as asm_main
10
+ from openrouter_adapter import (
11
+ load_openrouter_manifests,
12
+ openrouter_models_to_manifests,
13
+ )
14
+ from scorer import parse_manifest
15
+ from scorer import Preferences, ServiceVector, score_topsis
16
+
17
+
18
+ SAMPLE_MODELS = {
19
+ "data": [
20
+ {
21
+ "id": "openai/gpt-test",
22
+ "name": "OpenAI: GPT Test",
23
+ "context_length": 128000,
24
+ "pricing": {"prompt": "0.00000015", "completion": "0.0000006"},
25
+ "top_provider": {"context_length": 128000, "max_completion_tokens": 16000},
26
+ "architecture": {
27
+ "input_modalities": ["text", "image"],
28
+ "output_modalities": ["text"],
29
+ },
30
+ },
31
+ {
32
+ "id": "cheap/free-model:free",
33
+ "name": "Cheap: Free Model",
34
+ "context_length": 8192,
35
+ "pricing": {"prompt": "0", "completion": "0"},
36
+ "architecture": {
37
+ "input_modalities": ["text"],
38
+ "output_modalities": ["text"],
39
+ },
40
+ },
41
+ {
42
+ "id": "router/special",
43
+ "name": "Router: Special",
44
+ "pricing": {"prompt": "-1", "completion": "-1"},
45
+ "architecture": {
46
+ "input_modalities": ["text"],
47
+ "output_modalities": ["text"],
48
+ },
49
+ },
50
+ ]
51
+ }
52
+
53
+
54
+ SAMPLE_RANKINGS = {
55
+ "generated_at": "2026-05-04T00:00:00Z",
56
+ "source": "fixture",
57
+ "n_models": 2,
58
+ "models": [
59
+ {
60
+ "permaslug": "openai/gpt-test-20260501",
61
+ "slug": "openai/gpt-test",
62
+ "author": "openai",
63
+ "prompt_tokens": 1000,
64
+ "completion_tokens": 500,
65
+ "total_tokens": 1500,
66
+ "count": 10,
67
+ "rank_by_prompt_tokens": 1,
68
+ },
69
+ {
70
+ "permaslug": "other/model-20260501",
71
+ "slug": "other/model",
72
+ "author": "other",
73
+ "prompt_tokens": 100,
74
+ "completion_tokens": 50,
75
+ "total_tokens": 150,
76
+ "count": 2,
77
+ "rank_by_prompt_tokens": 2,
78
+ },
79
+ ],
80
+ }
81
+
82
+
83
+ def test_openrouter_models_to_ephemeral_manifests():
84
+ manifests = openrouter_models_to_manifests(
85
+ SAMPLE_MODELS["data"],
86
+ models_source="https://openrouter.ai/api/v1/models",
87
+ retrieved_at="2026-05-10T00:00:00Z",
88
+ ranking_by_slug={"openai/gpt-test": SAMPLE_RANKINGS["models"][0]},
89
+ ranking_count=2,
90
+ ranking_generated_at=SAMPLE_RANKINGS["generated_at"],
91
+ )
92
+
93
+ assert len(manifests) == 2
94
+ first = manifests[0]
95
+ schema = json.loads((Path(__file__).resolve().parent.parent / "schema" / "asm-v0.3.schema.json").read_text(encoding="utf-8"))
96
+ validator = Draft202012Validator(schema)
97
+ for manifest in manifests:
98
+ assert list(validator.iter_errors(manifest)) == []
99
+
100
+ assert first["service_id"] == "openrouter/openai/gpt-test@current"
101
+ assert first["taxonomy"] == "ai.llm.chat"
102
+ assert first["ttl"] == 300
103
+ assert first["provenance"]["verification_status"] == "self_reported"
104
+ assert "usage signal" in first["provenance"]["notes"]
105
+ assert first["pricing"]["billing_dimensions"][0]["cost_per_unit"] == pytest.approx(0.15)
106
+ assert first["pricing"]["billing_dimensions"][1]["cost_per_unit"] == pytest.approx(0.6)
107
+ assert first["quality"]["metrics"][0]["name"] == "openrouter_usage_signal"
108
+ assert first["quality"]["metrics"][0]["score"] == pytest.approx(1.0)
109
+
110
+ vector = parse_manifest(first, io_ratio=0.5)
111
+ assert vector.cost_per_unit == pytest.approx(0.375 / 1_000_000)
112
+ assert vector.quality_score == pytest.approx(1.0)
113
+
114
+
115
+ def test_load_openrouter_manifests_from_cached_json(tmp_path: Path):
116
+ models_path = tmp_path / "models.json"
117
+ rankings_path = tmp_path / "rankings.json"
118
+ models_path.write_text(json.dumps(SAMPLE_MODELS), encoding="utf-8")
119
+ rankings_path.write_text(json.dumps(SAMPLE_RANKINGS), encoding="utf-8")
120
+
121
+ manifests, metadata = load_openrouter_manifests(
122
+ models_json=models_path,
123
+ rankings_json=rankings_path,
124
+ )
125
+
126
+ assert metadata["n_models"] == 3
127
+ assert metadata["n_manifests"] == 2
128
+ assert metadata["ranking_snapshot"] == "2026-05-04T00:00:00Z"
129
+ assert manifests[1]["quality"]["metrics"][0]["score"] == pytest.approx(0.5)
130
+
131
+
132
+ def test_cli_openrouter_source_with_cached_json(tmp_path: Path, capsys):
133
+ models_path = tmp_path / "models.json"
134
+ rankings_path = tmp_path / "rankings.json"
135
+ models_path.write_text(json.dumps(SAMPLE_MODELS), encoding="utf-8")
136
+ rankings_path.write_text(json.dumps(SAMPLE_RANKINGS), encoding="utf-8")
137
+
138
+ code = asm_main([
139
+ "score",
140
+ "--source",
141
+ "openrouter",
142
+ "--openrouter-models-json",
143
+ str(models_path),
144
+ "--openrouter-rankings-json",
145
+ str(rankings_path),
146
+ "cheap LLM under $1 per 1M tokens under 1s",
147
+ ])
148
+ output = capsys.readouterr().out
149
+
150
+ assert code == 0
151
+ assert "OpenRouter ephemeral manifests" in output
152
+ assert "ignored latency hard constraint" in output
153
+ assert "representative cost <= $1.0000/1M blended tokens" in output
154
+ assert "Selected:" in output
155
+
156
+
157
+ def test_cli_openrouter_subcommand_with_cached_json(tmp_path: Path, capsys):
158
+ models_path = tmp_path / "models.json"
159
+ rankings_path = tmp_path / "rankings.json"
160
+ models_path.write_text(json.dumps(SAMPLE_MODELS), encoding="utf-8")
161
+ rankings_path.write_text(json.dumps(SAMPLE_RANKINGS), encoding="utf-8")
162
+
163
+ code = asm_main([
164
+ "openrouter",
165
+ "--openrouter-models-json",
166
+ str(models_path),
167
+ "--openrouter-rankings-json",
168
+ str(rankings_path),
169
+ "cheap coding model under $1 per 1M tokens",
170
+ ])
171
+ output = capsys.readouterr().out
172
+
173
+ assert code == 0
174
+ assert "OpenRouter ephemeral manifests" in output
175
+ assert "Model: cheap/free-model:free" in output
176
+ assert "Selected:" in output
177
+
178
+
179
+ def test_cli_openrouter_json_format(tmp_path: Path, capsys):
180
+ models_path = tmp_path / "models.json"
181
+ rankings_path = tmp_path / "rankings.json"
182
+ models_path.write_text(json.dumps(SAMPLE_MODELS), encoding="utf-8")
183
+ rankings_path.write_text(json.dumps(SAMPLE_RANKINGS), encoding="utf-8")
184
+
185
+ code = asm_main([
186
+ "openrouter",
187
+ "--format",
188
+ "json",
189
+ "--openrouter-models-json",
190
+ str(models_path),
191
+ "--openrouter-rankings-json",
192
+ str(rankings_path),
193
+ "cheap coding model under $1 per 1M tokens",
194
+ ])
195
+ payload = json.loads(capsys.readouterr().out)
196
+
197
+ assert code == 0
198
+ assert payload["selected"]["model"] == "cheap/free-model:free"
199
+ assert payload["ranked"][0]["cost_per_1m_blended_tokens"] == 0
200
+ assert payload["source"]["n_manifests"] == 2
201
+
202
+
203
+ def test_cli_openrouter_route_litellm_format(tmp_path: Path, capsys):
204
+ models_path = tmp_path / "models.json"
205
+ rankings_path = tmp_path / "rankings.json"
206
+ models_path.write_text(json.dumps(SAMPLE_MODELS), encoding="utf-8")
207
+ rankings_path.write_text(json.dumps(SAMPLE_RANKINGS), encoding="utf-8")
208
+
209
+ code = asm_main([
210
+ "openrouter",
211
+ "route",
212
+ "--format",
213
+ "litellm",
214
+ "--openrouter-models-json",
215
+ str(models_path),
216
+ "--openrouter-rankings-json",
217
+ str(rankings_path),
218
+ "cheap coding model under $1 per 1M tokens",
219
+ ])
220
+ output = capsys.readouterr().out
221
+
222
+ assert code == 0
223
+ assert "model_list:" in output
224
+ assert "model: openrouter/cheap/free-model:free" in output
225
+ assert "asm_score:" in output
226
+
227
+
228
+ def test_topsis_handles_unknown_latency_without_nan():
229
+ services = [
230
+ ServiceVector("a", "A", "ai.llm.chat", 1.0, 0.5, float("inf"), 0.5),
231
+ ServiceVector("b", "B", "ai.llm.chat", 2.0, 0.8, float("inf"), 0.5),
232
+ ]
233
+ results = score_topsis(services, Preferences(cost=0.4, quality=0.3, speed=0.2, reliability=0.1))
234
+
235
+ assert len(results) == 2
236
+ for result in results:
237
+ assert result.total_score == result.total_score
238
+ assert result.breakdown["speed"] == 1.0