Files

224 lines
7.5 KiB
Python
Raw Permalink Normal View History

2026-06-24 12:44:34 -07:00
"""Tests for the _snapshot_on_write decorator — final.json written after every write tool call."""
import json
import pytest
@pytest.fixture
def shopify_data(tmp_path):
"""Seed minimal Shopify state."""
data_file = tmp_path / "shopify_data.json"
data_file.write_text(
json.dumps(
{
"products": {
"product-1": {
"id": "product-1",
"title": "Test Product",
"variants": [
{
"id": "variant-1",
"title": "Default",
"price": {"amount": "10.00", "currencyCode": "USD"},
"availableForSale": True,
}
],
}
},
"carts": {},
"shipping_methods": {
"standard": {
"id": "standard",
"title": "Standard",
"price": {"amount": "5.99", "currencyCode": "USD"},
"active": True,
},
},
"policies": [],
"faqs": [],
}
)
)
return data_file
@pytest.fixture
def outputdir(tmp_path):
out = tmp_path / "output" / "shopify"
out.mkdir(parents=True)
return out
@pytest.fixture(autouse=True)
def _patch_globals(shopify_data, outputdir, monkeypatch):
"""Patch Shopify state globals for isolated testing."""
from shopify import state as shopify_state
# Reset state and load from our test data
monkeypatch.setattr(shopify_state, "_STATE_FILE", shopify_data)
shopify_state._current_state = None
shopify_state._stores.clear()
shopify_state._active_store_id = "default"
shopify_state.set_snapshot_paths(final_path=None, bundle_state_path=None)
shopify_state.load_state()
shopify_state.set_snapshot_paths(final_path=outputdir / "final.json")
yield
shopify_state.set_snapshot_paths(final_path=None, bundle_state_path=None)
@pytest.mark.asyncio
async def test_update_cart_writes_final_json(outputdir):
from shopify.server import update_cart
final = outputdir / "final.json"
assert not final.exists()
result = await update_cart(
add_items=[{"merchandiseId": "variant-1", "quantity": 1}],
)
assert "id" in result # cart was created
assert final.exists(), "final.json must be written after update_cart"
snapshot = json.loads(final.read_text())
assert len(snapshot.get("carts", {})) > 0
@pytest.mark.asyncio
async def test_search_does_not_write_final_json(outputdir):
from shopify.server import search_shop_catalog
final = outputdir / "final.json"
await search_shop_catalog(query="test", context="browsing")
assert not final.exists(), "final.json must NOT be written after a read-only tool"
@pytest.mark.asyncio
async def test_create_order_writes_final_json(outputdir):
from shopify.server import create_order, update_cart
final = outputdir / "final.json"
# First create a cart with items
cart = await update_cart(add_items=[{"merchandiseId": "variant-1", "quantity": 1}])
# Clear final from cart creation
if final.exists():
final.unlink()
result = await create_order(
cart_id=cart["id"],
payment_method={"type": "credit_card", "card_number": "4111111111111111", "cvv": "123", "expiry": "12/26"},
shipping_address={"address1": "123 Main St", "city": "Test", "countryCode": "US"},
billing_address={"address1": "123 Main St", "city": "Test", "countryCode": "US"},
shipping_method_id="standard",
)
assert result.get("order") is not None
assert final.exists(), "final.json must be written after create_order"
@pytest.mark.asyncio
async def test_get_order_does_not_write_final_json(outputdir):
from shopify.server import get_order
final = outputdir / "final.json"
await get_order(order_id="nonexistent")
assert not final.exists(), "final.json must NOT be written after a read-only tool"
@pytest.mark.asyncio
async def test_no_final_path_skips_snapshot(outputdir):
from shopify import state as shopify_state
from shopify.server import update_cart
shopify_state.set_snapshot_paths(final_path=None)
result = await update_cart(
add_items=[{"merchandiseId": "variant-1", "quantity": 1}],
)
assert "id" in result
final = outputdir / "final.json"
assert not final.exists()
def test_init_state_writes_initial_and_bundle_not_final(shopify_data, tmp_path, monkeypatch):
from shopify import state as shopify_state
outputdir = tmp_path / "output"
bundledir = tmp_path / "bundle"
monkeypatch.setenv("OUTPUTDIR", str(outputdir))
monkeypatch.setenv("BUNDLE_OUTPUT_DIR", str(bundledir))
shopify_state._stores.clear()
shopify_state._current_state = None
shopify_state._active_store_id = "default"
monkeypatch.setattr(shopify_state, "_STATE_FILE", shopify_data)
shopify_state.set_snapshot_paths(final_path=None, bundle_state_path=None)
shopify_state.init_state()
assert (outputdir / "initial.json").exists()
assert not (outputdir / "final.json").exists()
assert (bundledir / "state.json").exists()
@pytest.mark.asyncio
async def test_snapshot_on_write_restores_state_when_tool_raises(shopify_data, outputdir):
from shopify import server as srv
from shopify import state as shopify_state
@srv._snapshot_on_write
def mutates_then_raises():
shopify_state.get_state().products["product-1"].title = "Mutated"
raise RuntimeError("boom")
with pytest.raises(RuntimeError, match="boom"):
await mutates_then_raises()
assert shopify_state.get_state().products["product-1"].title == "Test Product"
persisted = json.loads(shopify_data.read_text())
assert persisted["products"]["product-1"]["title"] == "Test Product"
assert not (outputdir / "final.json").exists()
@pytest.mark.asyncio
async def test_snapshot_on_write_restores_state_on_hard_error_result(shopify_data, outputdir):
from shopify import server as srv
from shopify import state as shopify_state
@srv._snapshot_on_write
def mutates_then_returns_hard_error():
shopify_state.get_state().products["product-1"].title = "Mutated"
return {"product": None, "userErrors": [{"field": "title", "message": "invalid"}]}
result = await mutates_then_returns_hard_error()
assert result["product"] is None
assert shopify_state.get_state().products["product-1"].title == "Test Product"
persisted = json.loads(shopify_data.read_text())
assert persisted["products"]["product-1"]["title"] == "Test Product"
assert not (outputdir / "final.json").exists()
@pytest.mark.asyncio
async def test_snapshot_on_write_keeps_partial_success_result(outputdir):
from shopify import server as srv
from shopify import state as shopify_state
@srv._snapshot_on_write
def mutates_then_returns_partial_success():
product = shopify_state.get_state().products["product-1"]
product.title = "Partially Mutated"
return {
"product": product,
"userErrors": [{"field": "optional_field", "message": "ignored"}],
}
result = await mutates_then_returns_partial_success()
assert result["product"].title == "Partially Mutated"
assert shopify_state.get_state().products["product-1"].title == "Partially Mutated"
assert (outputdir / "final.json").exists()