224 lines
7.5 KiB
Python
224 lines
7.5 KiB
Python
"""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()
|