File size: 29,718 Bytes
2a87ede | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 | """
Tests for mcp_server/server.py -- the dense_evolution_mcp MCP adapter.
Routes every request through httpx.ASGITransport straight into the real,
in-process local_site.app.server.app (client._TEST_TRANSPORT, swapped via
monkeypatch.setattr in the autouse fixture below -- not a raw manual
global mutation, for automatic restoration even if a test fails partway
through) instead of a live subprocess kernel bound to a real port. That
means every test below still exercises the real DenseSVSimulator / real PennyLane
Hartree-Fock Hamiltonians -- no mocked physics, only the network hop is
swapped for an in-process one, for the same reason test_local_site_server.py
uses FastAPI's TestClient rather than a live server.
"""
import asyncio
import base64
import json
import os
import pytest
# fastapi/uvicorn/pydantic (composer) and mcp/httpx (mcp) are both optional
# extras -- skip this whole module cleanly wherever they aren't installed,
# same pattern test_local_site_server.py uses for the composer extra alone.
pytest.importorskip("fastapi")
pytest.importorskip("mcp")
import httpx # noqa: E402
from local_site.app import server as kernel # noqa: E402
from mcp_server import server as mcp_adapter # noqa: E402
from mcp_server import client as mcp_client # noqa: E402
from mcp_server.utils import images as mcp_images # noqa: E402
from mcp_server import models as mcp_models # noqa: E402
from mcp_server import molecules as mcp_molecules # noqa: E402
H2 = "H2 (Idrogeno) - R = 0.7414 A [equilibrio reale]"
EXACT_H2_ENERGY_HARTREE = -1.1372701748786913
BELL_QASM = """OPENQASM 2.0;
include "qelib1.inc";
qreg q[2];
creg c[2];
h q[0];
cx q[0],q[1];
measure q[0] -> c[0];
measure q[1] -> c[1];
"""
def run(coro):
return asyncio.run(coro)
@pytest.fixture(autouse=True)
def route_through_real_kernel_in_process(monkeypatch):
monkeypatch.setattr(mcp_client, "_TEST_TRANSPORT", httpx.ASGITransport(app=kernel.app))
# A fresh molecule-alias cache per test: several tests below rely on
# the real /api/hamiltonians catalog being (re)fetched, not whatever a
# previous test happened to populate.
mcp_molecules._molecule_catalog_cache.invalidate()
yield
mcp_molecules._molecule_catalog_cache.invalidate()
def test_health_reports_real_kernel_info():
data = json.loads(run(mcp_adapter.dense_evolution_health()))
assert data["status"] == "ok"
assert data["dense_evolution_version"] == kernel.dense_evolution.__version__
assert data["total_ram_gb"] > 0
def test_system_limits_reports_positive_qubit_ceiling():
data = json.loads(run(mcp_adapter.dense_evolution_system_limits()))
assert data["max_qubits_dense"] > 0
def test_list_presets_includes_a_real_bell_state():
data = json.loads(run(mcp_adapter.dense_evolution_list_presets()))
assert any("Bell" in name for name in data)
def test_list_gates_is_non_empty():
data = json.loads(run(mcp_adapter.dense_evolution_list_gates()))
assert len(data) > 0
def test_list_noise_models_includes_depolarizing():
data = json.loads(run(mcp_adapter.dense_evolution_list_noise_models()))
assert "depolarizing" in data
def test_build_circuit_from_ops_round_trips_to_valid_qasm():
ops = [{"gate": "h", "qubits": [0]}, {"gate": "cx", "qubits": [0, 1]}]
result = run(mcp_adapter.dense_evolution_build_circuit(mcp_models.BuildCircuitInput(n_qubits=2, ops=ops)))
data = json.loads(result)
assert "OPENQASM" in data["qasm"]
assert "h q[0]" in data["qasm"]
def test_custom_molecule_energy_matches_catalog_h2():
result = json.loads(run(mcp_adapter.dense_evolution_custom_molecule_energy(mcp_models.CustomMoleculeInput(
symbols=["H", "H"], geometry=[[0, 0, 0], [0, 0, 0.7414]],
))))
assert result["ground_state_energy_hartree"] == pytest.approx(EXACT_H2_ENERGY_HARTREE, abs=1e-6)
def test_qmmm_forces_returns_real_force_vectors():
result = json.loads(run(mcp_adapter.dense_evolution_qmmm_forces(mcp_models.QmmmForcesInput(name="H2"))))
assert "forces" in result or "force_norm" in result or "energy_hartree" in result
def test_md_trajectory_runs_a_few_fixed_electronic_state_steps():
result = json.loads(run(mcp_adapter.dense_evolution_md_trajectory(mcp_models.MdTrajectoryInput(
name="H2", n_steps=2, recompute_electronic_state=False,
))))
assert "error" not in result
def test_mitigate_zne_richardson_extrapolates_toward_ideal():
# Single-seed, single-comparison assertions here are genuinely
# statistical, not deterministic: NoiseModel.apply_to_sv's depolarizing
# channel is a real stochastic single-qubit-per-shot Kraus draw (fixed
# 2026-08-11, see registry.py's changelog entry -- the channel used to
# decide fire/no-fire independently per computational-basis amplitude
# pair instead of once per qubit per shot, which understated true
# per-shot variance on entangled states). At the default n_trials=200
# per noise scale, a single fixed seed can land on the unlucky side of
# the distribution (verified directly: 42's own default seed passes
# only ~70% of nearby seeds at n_trials=200). Average the comparison
# over several seeds instead of trusting one arbitrary seed's outcome
# -- this is the statistically honest form of the same physics check.
ideal_ok = True
zne_gaps = []
noisiest_gaps = []
for seed in range(5):
result = json.loads(run(mcp_adapter.dense_evolution_mitigate_zne(mcp_models.MitigateZneInput(
qasm=BELL_QASM, pauli_string="ZZ", noise_model="depolarizing", noise_p=0.05, seed=seed,
))))
ideal_ok = ideal_ok and (result["ideal_expectation"] == pytest.approx(1.0, abs=1e-6))
zne_gaps.append(abs(result["zne_extrapolated"] - 1.0))
noisiest_gaps.append(abs(result["noisy_expectations"][-1] - 1.0))
assert ideal_ok
# Real physics check, not just "did it return something": averaged
# over several seeds, ZNE should land closer to ideal than the
# noisiest single measurement.
assert sum(zne_gaps) / len(zne_gaps) < sum(noisiest_gaps) / len(noisiest_gaps)
def test_mitigate_density_matrix_reports_fidelity_improvement():
result = json.loads(run(mcp_adapter.dense_evolution_mitigate_density_matrix(
mcp_models.MitigateDensityMatrixInput(qasm=BELL_QASM, noise_model="depolarizing", noise_p=0.05)
)))
assert 0.0 <= result["fidelity_raw"] <= 1.0
assert 0.0 <= result["fidelity_corrected"] <= 1.0
def test_vector_healing_replaces_an_outlier_with_the_local_median():
vectors = [[1.0, 2.0], [1.1, 2.1], [1.05, 2.05], [50.0, -30.0], [1.08, 2.08], [1.02, 2.02]]
result = json.loads(run(mcp_adapter.dense_evolution_vector_healing(
mcp_models.VectorHealingInput(vectors=vectors)
)))
assert len(result["healed_vectors"]) == 6
assert result["healed_vectors"][3] != vectors[3]
assert result["reconstruction_error"] > 0.0
def test_list_molecules_includes_short_ids():
data = json.loads(run(mcp_adapter.dense_evolution_list_molecules(mcp_models.ListMoleculesInput())))
by_id = {m["id"]: m for m in data}
assert "H2" in by_id
assert by_id["H2"]["full_name"] == H2
assert by_id["H2"]["n_qubits"] == 4
def test_molecule_energy_accepts_short_id():
result = run(mcp_adapter.dense_evolution_molecule_energy(mcp_models.MoleculeEnergyInput(name="H2")))
data = json.loads(result)
assert data["ground_state_energy_hartree"] == pytest.approx(EXACT_H2_ENERGY_HARTREE, abs=1e-6)
def test_molecule_energy_accepts_full_catalog_name():
result = run(mcp_adapter.dense_evolution_molecule_energy(mcp_models.MoleculeEnergyInput(name=H2)))
data = json.loads(result)
assert data["ground_state_energy_hartree"] == pytest.approx(EXACT_H2_ENERGY_HARTREE, abs=1e-6)
def test_molecule_energy_unknown_name_is_a_clear_error_not_a_crash():
result = run(mcp_adapter.dense_evolution_molecule_energy(mcp_models.MoleculeEnergyInput(name="Unobtainium")))
assert result.startswith("Error:")
assert "Unobtainium" in result
def test_mix_molecules_accepts_short_ids():
result = run(mcp_adapter.dense_evolution_mix_molecules(
mcp_models.MixMoleculesInput(name_a="H2", name_b="HeH+", weight_a=0.5, weight_b=0.5)
))
# H2 (4 qubits) and HeH+ (4 qubits) share a qubit count -- a real,
# meaningful mix; the tool should resolve both short ids and succeed.
data = json.loads(result)
assert "energy_mixed" in data
def test_run_circuit_bell_state_gives_real_50_50_split():
result = run(mcp_adapter.dense_evolution_run_circuit(
mcp_models.RunCircuitInput(qasm=BELL_QASM, shots=500, seed=42)
))
data = json.loads(result)
assert data["n_qubits"] == 2
assert set(data["counts"]) == {"00", "11"}
assert data["counts"]["00"] + data["counts"]["11"] == 500
# Both truncation summaries should be present and self-consistent.
assert data["statevector"]["total_nonzero_amplitudes"] == 2
assert data["probabilities"]["total_basis_states"] == 4
def test_run_circuit_top_k_is_configurable():
# BUG FIX: top_k used to be hardcoded at 25 inside the truncation
# helpers with no caller-facing way to change it. A 3-qubit GHZ-ish
# circuit only has 2 nonzero amplitudes/basis states, so top_k=1 is
# the meaningful case to check truncation actually engages.
ghz_qasm = """OPENQASM 2.0;
include "qelib1.inc";
qreg q[3];
creg c[3];
h q[0];
cx q[0],q[1];
cx q[1],q[2];
"""
result = run(mcp_adapter.dense_evolution_run_circuit(
mcp_models.RunCircuitInput(qasm=ghz_qasm, shots=50, top_k=1)
))
data = json.loads(result)
assert data["statevector"]["total_nonzero_amplitudes"] == 2
assert data["statevector"]["shown"] == 1
assert len(data["statevector"]["top_amplitudes_by_magnitude"]) == 1
assert data["probabilities"]["shown"] == 1
assert len(data["probabilities"]["top_states_by_probability"]) == 1
def test_run_circuit_visualizations_are_saved_to_disk_not_inlined(tmp_path):
original_dir = mcp_images.IMAGE_OUTPUT_DIR
mcp_images.IMAGE_OUTPUT_DIR = tmp_path
try:
result = run(mcp_adapter.dense_evolution_run_circuit(
mcp_models.RunCircuitInput(qasm=BELL_QASM, shots=50, include_visualizations=True)
))
data = json.loads(result)
for key in ("circuit_png_path", "histogram_png_path", "qsphere_png_path", "bloch_png_path"):
assert key in data
path = data[key]
assert os.path.exists(path)
assert os.path.getsize(path) > 0
assert str(tmp_path) in path
# The point of saving to disk: no raw base64 anywhere in the response
# -- checked against the parsed dict's own keys, since "circuit_png"
# is (correctly) a substring of "circuit_png_path" in the raw text.
for raw_key in ("circuit_png", "histogram_png", "qsphere_png", "bloch_png"):
assert raw_key not in data
finally:
mcp_images.IMAGE_OUTPUT_DIR = original_dir
def test_run_circuit_image_metadata_sidecar_is_written(tmp_path):
# BUG FIX: a saved image's filename (circuit_<timestamp>.png) used to
# carry no way to trace it back to the circuit/tool call that produced
# it. Every PNG dense_evolution_run_circuit saves should now have a
# same-stem .json sidecar identifying the source qasm/seed/etc.
original_dir = mcp_images.IMAGE_OUTPUT_DIR
mcp_images.IMAGE_OUTPUT_DIR = tmp_path
try:
result = run(mcp_adapter.dense_evolution_run_circuit(
mcp_models.RunCircuitInput(qasm=BELL_QASM, shots=50, seed=7, include_visualizations=True)
))
data = json.loads(result)
import pathlib
png_path = pathlib.Path(data["circuit_png_path"])
json_path = png_path.with_suffix(".json")
assert json_path.exists()
meta = json.loads(json_path.read_text())
assert meta["tool"] == "dense_evolution_run_circuit"
assert meta["qasm"] == BELL_QASM
assert meta["seed"] == 7
finally:
mcp_images.IMAGE_OUTPUT_DIR = original_dir
def test_prune_old_images_keeps_only_the_most_recent_max_files(tmp_path):
# BUG FIX: nothing ever cleaned up IMAGE_OUTPUT_DIR -- a long-running
# MCP session grows it without bound. Uses synthetic files with
# explicit mtimes (not _save_png's real-clock filenames) so the
# "oldest" ordering is deterministic instead of racing real time.
original_dir = mcp_images.IMAGE_OUTPUT_DIR
original_max = mcp_images.IMAGE_MAX_FILES
mcp_images.IMAGE_OUTPUT_DIR = tmp_path
mcp_images.IMAGE_MAX_FILES = 3
try:
for i in range(5):
p = tmp_path / f"img{i}.png"
p.write_bytes(b"x")
os.utime(p, (i, i))
mcp_images._prune_old_images()
remaining = {p.name for p in tmp_path.glob("*.png")}
assert remaining == {"img2.png", "img3.png", "img4.png"}
finally:
mcp_images.IMAGE_OUTPUT_DIR = original_dir
mcp_images.IMAGE_MAX_FILES = original_max
def test_prune_old_images_disabled_when_max_files_non_positive(tmp_path):
original_dir = mcp_images.IMAGE_OUTPUT_DIR
original_max = mcp_images.IMAGE_MAX_FILES
mcp_images.IMAGE_OUTPUT_DIR = tmp_path
mcp_images.IMAGE_MAX_FILES = 0
try:
for i in range(5):
(tmp_path / f"img{i}.png").write_bytes(b"x")
mcp_images._prune_old_images()
assert len(list(tmp_path.glob("*.png"))) == 5
finally:
mcp_images.IMAGE_OUTPUT_DIR = original_dir
mcp_images.IMAGE_MAX_FILES = original_max
def test_save_png_calls_prune_so_the_directory_stays_bounded(tmp_path):
# End-to-end wiring check: _save_png itself must trigger pruning, not
# just the helper in isolation. Real-clock filenames make exact
# oldest-file identity unreliable at test speed, so this only checks
# the invariant that actually matters: the directory never exceeds
# the configured cap.
original_dir = mcp_images.IMAGE_OUTPUT_DIR
original_max = mcp_images.IMAGE_MAX_FILES
mcp_images.IMAGE_OUTPUT_DIR = tmp_path
mcp_images.IMAGE_MAX_FILES = 3
try:
tiny_png_b64 = base64.b64encode(b"not a real png but bytes are bytes").decode()
for i in range(6):
mcp_images._save_png(tiny_png_b64, f"shot{i}")
assert len(list(tmp_path.glob("*.png"))) <= 3
finally:
mcp_images.IMAGE_OUTPUT_DIR = original_dir
mcp_images.IMAGE_MAX_FILES = original_max
def test_molecule_catalog_second_call_hits_the_cache_not_the_network():
# BUG FIX target: the pre-Phase-2 code never cached across calls in
# production (only test code ever reset the old plain-dict cache) --
# this checks the actual cache-HIT code path fires: a second call for
# the same mapping must NOT reach _request again.
first = run(mcp_adapter.dense_evolution_list_molecules(mcp_models.ListMoleculesInput()))
async def _fail_if_called(*args, **kwargs):
raise AssertionError("second call should have hit the cache, not _request")
original_request = mcp_molecules._request
mcp_molecules._request = _fail_if_called
try:
second = run(mcp_adapter.dense_evolution_list_molecules(mcp_models.ListMoleculesInput()))
finally:
mcp_molecules._request = original_request
assert first == second
def test_molecule_catalog_fetch_failure_is_cached_and_reraised(monkeypatch):
mcp_molecules._molecule_catalog_cache.invalidate()
class _BrokenClient:
async def request(self, method, path, **kwargs):
raise httpx.ConnectError("simulated kernel down")
monkeypatch.setattr(mcp_client, "_get_client", lambda: _BrokenClient())
result = run(mcp_adapter.dense_evolution_list_molecules(mcp_models.ListMoleculesInput()))
assert result.startswith("Error:")
# Second call within the failure TTL must reuse the cached failure
# (not attempt a fresh connection) -- proven by NOT patching _get_client
# back yet and getting the same error shape again, fast.
result_again = run(mcp_adapter.dense_evolution_list_molecules(mcp_models.ListMoleculesInput()))
assert result_again.startswith("Error:")
mcp_molecules._molecule_catalog_cache.invalidate()
def test_run_circuit_large_scale_response_includes_image_metadata(monkeypatch, tmp_path):
# Mocks the kernel's own large_scale response shape (same technique
# test_kernel_timeout_gives_actionable_error_not_a_traceback uses)
# rather than driving a real >24-qubit MPS run, which would be slow
# and depend on exact MPS internals unrelated to what this test checks:
# that the large_scale branch's saved image also gets a metadata
# sidecar, same as the normal branch already does.
original_dir = mcp_images.IMAGE_OUTPUT_DIR
mcp_images.IMAGE_OUTPUT_DIR = tmp_path
tiny_png_b64 = base64.b64encode(b"not a real png but bytes are bytes").decode()
class _FakeResponse:
status_code = 200
def json(self):
return {
"large_scale": True, "n_qubits": 30, "k_requested": 32,
"top_k_states": [], "circuit_png": tiny_png_b64,
}
class _FakeClient:
async def request(self, method, path, **kwargs):
return _FakeResponse()
monkeypatch.setattr(mcp_client, "_get_client", lambda: _FakeClient())
try:
result = run(mcp_adapter.dense_evolution_run_circuit(
mcp_models.RunCircuitInput(qasm=BELL_QASM, include_visualizations=True)
))
data = json.loads(result)
assert data["large_scale"] is True
assert "circuit_png" not in data
import pathlib
png_path = pathlib.Path(data["circuit_png_path"])
assert png_path.with_suffix(".json").exists()
finally:
mcp_images.IMAGE_OUTPUT_DIR = original_dir
def test_save_png_returns_none_for_falsy_input():
assert mcp_images._save_png(None, "whatever") is None
assert mcp_images._save_png("", "whatever") is None
def test_energy_scan_matches_individual_molecule_energy_call():
single = json.loads(run(mcp_adapter.dense_evolution_molecule_energy(mcp_models.MoleculeEnergyInput(name="H2"))))
scan_result = json.loads(run(mcp_adapter.dense_evolution_energy_scan(mcp_models.EnergyScanInput(
symbols=["H", "H"],
geometries=[[[0, 0, 0], [0, 0, 0.7414]]],
labels=["equilibrium"],
))))
assert scan_result["n_points"] == 1
assert scan_result["results"][0]["ground_state_energy_hartree"] == pytest.approx(
single["ground_state_energy_hartree"], abs=1e-9
)
assert scan_result["minimum"]["label"] == "equilibrium"
def test_energy_scan_finds_h2_minimum_near_known_equilibrium():
result = json.loads(run(mcp_adapter.dense_evolution_energy_scan(mcp_models.EnergyScanInput(
symbols=["H", "H"],
geometries=[[[0, 0, 0], [0, 0, r]] for r in (0.5, 0.7414, 1.2)],
labels=[0.5, 0.7414, 1.2],
))))
assert result["minimum"]["label"] == 0.7414
def test_energy_scan_isolates_a_failing_point_from_the_rest():
# A 13-atom linear hydrogen chain needs far more than 12 qubits --
# dense_evolution_custom_molecule_energy's own real, documented cap --
# so this point must fail while the small, valid point still succeeds.
big_symbols = ["H"] * 13
big_geometry = [[0, 0, float(i)] for i in range(13)]
result = json.loads(run(mcp_adapter.dense_evolution_energy_scan(mcp_models.EnergyScanInput(
symbols=["H", "H"],
geometries=[[[0, 0, 0], [0, 0, 0.7414]]],
labels=["ok"],
))))
assert "error" not in result["results"][0]
# Mismatched symbols/geometry length within one scan point is caught
# before ever calling the kernel, and reported per-point.
mismatched = json.loads(run(mcp_adapter.dense_evolution_energy_scan(mcp_models.EnergyScanInput(
symbols=["H", "H", "H"],
geometries=[[[0, 0, 0], [0, 0, 0.7414]]], # only 2 rows for 3 symbols
labels=["bad"],
))))
assert mismatched["results"][0]["error"]
assert mismatched["minimum"] is None
def test_energy_scan_isolates_a_real_kernel_error_from_the_rest():
# Distinct from test_energy_scan_isolates_a_failing_point_from_the_rest,
# which only exercises the LOCAL symbols/geometry length check (never
# reaches the kernel at all). This one needs a point where that local
# check passes but the kernel itself rejects the request (13 H atoms
# needs far more than the real, documented 12-qubit exact-diagonalization
# cap) -- the try/except around the real _request call inside _one_point.
big_symbols = ["H"] * 13
big_geometry = [[0, 0, float(i)] for i in range(13)]
result = json.loads(run(mcp_adapter.dense_evolution_energy_scan(mcp_models.EnergyScanInput(
symbols=big_symbols,
geometries=[big_geometry],
labels=["too_big"],
))))
assert "error" in result["results"][0]
assert result["minimum"] is None
def test_energy_scan_rejects_mismatched_labels_length():
result = run(mcp_adapter.dense_evolution_energy_scan(mcp_models.EnergyScanInput(
symbols=["H", "H"],
geometries=[[[0, 0, 0], [0, 0, 0.7]], [[0, 0, 0], [0, 0, 0.8]]],
labels=["only-one-label"],
)))
assert result.startswith("Error:")
def test_run_vqe_hardware_efficient_converges_close_to_exact_for_h2():
result = json.loads(run(mcp_adapter.dense_evolution_run_vqe(mcp_models.RunVqeInput(
name="H2", ansatz_type="hardware_efficient", n_layers=4, maxiter=150, seed=0,
))))
assert abs(result["vqe_energy_hartree"] - result["exact_energy_hartree"]) < 0.01
def test_kernel_unreachable_gives_actionable_error_not_a_traceback(monkeypatch):
monkeypatch.setattr(mcp_client, "_TEST_TRANSPORT", None) # force a real (failing) TCP attempt
monkeypatch.setattr(mcp_client, "KERNEL_URL", "http://127.0.0.1:1") # nothing listens here
result = run(mcp_adapter.dense_evolution_health())
assert result.startswith("Error:")
assert "dense-evolution serve" in result
def test_kernel_timeout_gives_actionable_error_not_a_traceback(monkeypatch):
class _TimeoutClient:
async def request(self, method, path, **kwargs):
raise httpx.TimeoutException("simulated timeout")
monkeypatch.setattr(mcp_client, "_get_client", lambda: _TimeoutClient())
result = run(mcp_adapter.dense_evolution_health())
assert result.startswith("Error:")
assert "timed out" in result
def test_kernel_error_response_with_non_json_body_falls_back_to_raw_text(monkeypatch):
# _request's status_code>=400 branch tries resp.json().get("detail", ...)
# first and falls back to resp.text if the body isn't valid JSON (e.g. a
# raw HTML 502 from a proxy in front of the kernel, not the kernel's own
# structured FastAPI error response).
class _FakeResponse:
status_code = 502
text = "<html>Bad Gateway</html>"
def json(self):
raise ValueError("not valid json")
class _ErrorClient:
async def request(self, method, path, **kwargs):
return _FakeResponse()
monkeypatch.setattr(mcp_client, "_get_client", lambda: _ErrorClient())
result = run(mcp_adapter.dense_evolution_health())
assert result.startswith("Error:")
assert "<html>Bad Gateway</html>" in result
def test_wormhole_select_instance_finds_the_known_seed_61():
data = json.loads(run(mcp_adapter.dense_evolution_wormhole_select_instance(
mcp_models.WormholeSelectInstanceInput(
n_majorana=8, k_terms=10, J=2 ** 0.5, n_candidates=200, target_commuting=34,
)
)))
assert data["seed"] == 61
assert data["commuting"] == 34
assert data["anticommuting"] == 11
def test_wormhole_teleportation_exact_backend_matches_known_reference_values():
"""seed=61, t0=0.3, t1=0.60 known peak: I(mu=+12)=0.01326, I(mu=-12)=0.01793
(exact backend) -- same values pinned in tests/test_local_site_server.py,
checked again here through the MCP adapter's own request path."""
pos = json.loads(run(mcp_adapter.dense_evolution_wormhole_teleportation(
mcp_models.WormholeTeleportationInput(mu=12.0, t0=0.3, t1=0.6, seed=61, backend="exact")
)))
neg = json.loads(run(mcp_adapter.dense_evolution_wormhole_teleportation(
mcp_models.WormholeTeleportationInput(mu=-12.0, t0=0.3, t1=0.6, seed=61, backend="exact")
)))
assert pos["mutual_information_pt"] == pytest.approx(0.01326, abs=1e-5)
assert neg["mutual_information_pt"] == pytest.approx(0.01793, abs=1e-5)
def test_wormhole_teleportation_trotter_backend_matches_known_reference_values():
pos = json.loads(run(mcp_adapter.dense_evolution_wormhole_teleportation(
mcp_models.WormholeTeleportationInput(mu=12.0, t0=0.3, t1=0.6, seed=61, backend="trotter")
)))
neg = json.loads(run(mcp_adapter.dense_evolution_wormhole_teleportation(
mcp_models.WormholeTeleportationInput(mu=-12.0, t0=0.3, t1=0.6, seed=61, backend="trotter")
)))
assert pos["mutual_information_pt"] == pytest.approx(0.01301, abs=1e-5)
assert neg["mutual_information_pt"] == pytest.approx(0.01821, abs=1e-5)
def test_wormhole_teleportation_invalid_backend_gives_error_not_a_traceback():
result = run(mcp_adapter.dense_evolution_wormhole_teleportation(
mcp_models.WormholeTeleportationInput(backend="not-a-backend")
))
assert result.startswith("Error:")
def test_wormhole_scan_sweeps_t1_and_finds_the_known_peak():
"""One batched call replacing the two-call-per-point pattern above --
the peak (largest mu<0 minus mu>0 delta) for this exact configuration
is known to land at t1=0.60 (delta=+0.00468, exact backend) from the
original research reproduction's 11-point sweep."""
result = json.loads(run(mcp_adapter.dense_evolution_wormhole_scan(
mcp_models.WormholeScanInput(
mu_magnitude=12.0, t0=0.3, t1_values=[0.10, 0.30, 0.60, 0.85, 1.20],
seed=61, backend="exact",
)
)))
assert result["n_points"] == 5
assert all("delta" in r for r in result["results"])
assert result["peak"]["t1"] == pytest.approx(0.60)
assert result["peak"]["delta"] == pytest.approx(0.00468, abs=1e-4)
def test_wormhole_scan_reports_per_point_errors_without_aborting():
result = json.loads(run(mcp_adapter.dense_evolution_wormhole_scan(
mcp_models.WormholeScanInput(
n_majorana=200, t0=0.3, t1_values=[0.5], seed=61, backend="exact",
)
)))
assert result["n_points"] == 1
assert "error" in result["results"][0]
assert result["peak"] is None
def test_request_reuses_the_same_client_across_calls():
# BUG FIX (perf): _request used to open a fresh httpx.AsyncClient
# (TCP handshake) per call -- _get_client now caches and reuses one
# as long as (_TEST_TRANSPORT, KERNEL_URL) haven't changed, which
# they don't within a single test (the autouse fixture only swaps
# _TEST_TRANSPORT between tests, not within one).
run(mcp_adapter.dense_evolution_health())
client_after_first_call = mcp_client._shared_client
run(mcp_adapter.dense_evolution_health())
assert mcp_client._shared_client is client_after_first_call
def test_request_rebuilds_client_when_transport_or_url_changes(monkeypatch):
run(mcp_adapter.dense_evolution_health())
stale_client = mcp_client._shared_client
monkeypatch.setattr(mcp_client, "_TEST_TRANSPORT", None)
monkeypatch.setattr(mcp_client, "KERNEL_URL", "http://127.0.0.1:1")
result = run(mcp_adapter.dense_evolution_health())
assert result.startswith("Error:") # confirms it actually hit the new (broken) target
assert mcp_client._shared_client is not stale_client
def test_per_tool_timeouts_reach_the_real_http_call(monkeypatch):
"""Each tool used to share one flat 180s timeout regardless of real
cost -- a fast health check waited as long as a slow VQE run would
need, and a genuinely long VQE/MD run could get cut off by the same
180s ceiling. Verifies the actual timeout= kwarg httpx.AsyncClient.request
receives differs per tool and matches each endpoint's real expected
cost, not that the call merely succeeds (which wouldn't catch a timeout
value silently being ignored or defaulted)."""
seen_timeouts = {}
real_request = httpx.AsyncClient.request
async def _spy_request(self, method, url, *args, **kwargs):
seen_timeouts[url] = kwargs.get("timeout")
return await real_request(self, method, url, *args, **kwargs)
monkeypatch.setattr(httpx.AsyncClient, "request", _spy_request)
run(mcp_adapter.dense_evolution_health())
run(mcp_adapter.dense_evolution_run_vqe(mcp_models.RunVqeInput(
name="H2", ansatz_type="hardware_efficient", n_layers=1, maxiter=1,
)))
assert seen_timeouts["/api/health"] == 5.0
assert seen_timeouts["/api/vqe"] == 600.0
assert seen_timeouts["/api/health"] != seen_timeouts["/api/vqe"]
|