144 lines
5.8 KiB
Python
144 lines
5.8 KiB
Python
import json
|
|
import pytest
|
|
from pathlib import Path
|
|
|
|
|
|
@pytest.fixture
|
|
def app_client(tmp_path, monkeypatch):
|
|
from backend.app.services.simulation_manager import SimulationManager
|
|
monkeypatch.setattr(SimulationManager, 'SIMULATION_DATA_DIR', str(tmp_path))
|
|
from backend.app import create_app
|
|
app = create_app({'TESTING': True})
|
|
with app.test_client() as c:
|
|
yield c, tmp_path
|
|
|
|
|
|
@pytest.fixture
|
|
def completed_sim(app_client):
|
|
client, tmp_path = app_client
|
|
sim_id = "sim_src001"
|
|
sim_dir = tmp_path / sim_id
|
|
sim_dir.mkdir()
|
|
state = {
|
|
"simulation_id": sim_id, "project_id": "proj_src", "graph_id": "g1",
|
|
"status": "completed", "entities_count": 2, "profiles_count": 2,
|
|
"entity_types": [], "config_generated": True,
|
|
"parent_simulation_id": None, "graph_id_simulation": None,
|
|
}
|
|
(sim_dir / "state.json").write_text(json.dumps(state))
|
|
profiles = [
|
|
{"user_id": 0, "name": "Alice", "bio": "Bio A", "manually_edited": False},
|
|
{"user_id": 1, "name": "Bob", "bio": "Bio B", "manually_edited": True},
|
|
]
|
|
(sim_dir / "reddit_profiles.json").write_text(json.dumps(profiles))
|
|
(sim_dir / "twitter_profiles.csv").write_text("user_id,name\n0,Alice\n1,Bob\n")
|
|
(sim_dir / "agent_profiles.json").write_text(json.dumps(profiles))
|
|
(sim_dir / "simulation_config.json").write_text(json.dumps({"time_config": {}}))
|
|
return client, tmp_path, sim_id
|
|
|
|
|
|
def test_clone_creates_new_simulation(completed_sim):
|
|
"""POST /api/simulation/<src_id>/clone returns 200, success=True, with new_simulation_id and parent_simulation_id."""
|
|
client, tmp_path, src_id = completed_sim
|
|
resp = client.post(
|
|
f'/api/simulation/{src_id}/clone',
|
|
json={"project_id": "proj_clone"},
|
|
content_type='application/json',
|
|
)
|
|
assert resp.status_code == 200
|
|
data = resp.get_json()
|
|
assert data["success"] is True
|
|
assert "new_simulation_id" in data["data"]
|
|
assert data["data"]["parent_simulation_id"] == src_id
|
|
assert data["data"]["new_simulation_id"] != src_id
|
|
|
|
|
|
def test_clone_copies_profiles_not_config(completed_sim):
|
|
"""Cloned sim dir has reddit_profiles.json + twitter_profiles.csv, but NOT simulation_config.json."""
|
|
client, tmp_path, src_id = completed_sim
|
|
resp = client.post(
|
|
f'/api/simulation/{src_id}/clone',
|
|
json={"project_id": "proj_clone"},
|
|
content_type='application/json',
|
|
)
|
|
assert resp.status_code == 200
|
|
data = resp.get_json()
|
|
new_id = data["data"]["new_simulation_id"]
|
|
|
|
new_dir = tmp_path / new_id
|
|
assert (new_dir / "reddit_profiles.json").exists(), "reddit_profiles.json should be copied"
|
|
assert (new_dir / "twitter_profiles.csv").exists(), "twitter_profiles.csv should be copied"
|
|
assert not (new_dir / "simulation_config.json").exists(), "simulation_config.json must NOT be copied"
|
|
|
|
|
|
def test_clone_status_is_profiles_ready(completed_sim):
|
|
"""Cloned sim state.json has status='profiles_ready' and correct parent_simulation_id."""
|
|
client, tmp_path, src_id = completed_sim
|
|
resp = client.post(
|
|
f'/api/simulation/{src_id}/clone',
|
|
json={"project_id": "proj_clone"},
|
|
content_type='application/json',
|
|
)
|
|
assert resp.status_code == 200
|
|
data = resp.get_json()
|
|
new_id = data["data"]["new_simulation_id"]
|
|
|
|
state_file = tmp_path / new_id / "state.json"
|
|
assert state_file.exists(), "state.json must be created for cloned simulation"
|
|
state = json.loads(state_file.read_text())
|
|
assert state["status"] == "profiles_ready"
|
|
assert state["parent_simulation_id"] == src_id
|
|
|
|
|
|
def test_clone_source_not_found_returns_404(app_client):
|
|
client, tmp_path = app_client
|
|
resp = client.post("/api/simulation/nonexistent_sim/clone", json={"project_id": "proj_x"})
|
|
assert resp.status_code == 404
|
|
|
|
|
|
def test_start_simulation_clones_graph_when_enabled(app_client, monkeypatch):
|
|
"""When enable_graph_memory_update=true and graph backend supports clone_graph, it should be called."""
|
|
client, tmp_path = app_client
|
|
|
|
# Create a sim in 'ready' status with all required files
|
|
import json
|
|
sim_id = "sim_ready001"
|
|
sim_dir = tmp_path / sim_id
|
|
sim_dir.mkdir()
|
|
state = {
|
|
"simulation_id": sim_id, "project_id": "proj_r1", "graph_id": "g_original",
|
|
"status": "ready", "entities_count": 2, "profiles_count": 2,
|
|
"entity_types": [], "config_generated": True,
|
|
"parent_simulation_id": None, "graph_id_simulation": None,
|
|
"enable_twitter": False, "enable_reddit": True,
|
|
}
|
|
(sim_dir / "state.json").write_text(json.dumps(state))
|
|
config = {"time_config": {"total_hours": 24}, "reddit_config": {}}
|
|
(sim_dir / "simulation_config.json").write_text(json.dumps(config))
|
|
profiles = [{"user_id": 0, "name": "Alice"}]
|
|
(sim_dir / "reddit_profiles.json").write_text(json.dumps(profiles))
|
|
(sim_dir / "twitter_profiles.csv").write_text("user_id,name\n0,Alice\n")
|
|
|
|
# Mock graph backend
|
|
from unittest.mock import MagicMock, AsyncMock
|
|
mock_backend = MagicMock()
|
|
mock_backend.clone_graph = AsyncMock(return_value=None)
|
|
|
|
import backend.app.graph as graph_module
|
|
monkeypatch.setattr(graph_module, "get_graph_backend", lambda: mock_backend)
|
|
|
|
# Mock SimulationRunner.start_simulation
|
|
import backend.app.services.simulation_runner as runner_module
|
|
fake_run_state = MagicMock()
|
|
fake_run_state.to_dict.return_value = {"runner_status": "running"}
|
|
monkeypatch.setattr(runner_module.SimulationRunner, "start_simulation",
|
|
staticmethod(lambda **kw: fake_run_state))
|
|
|
|
resp = client.post("/api/simulation/start", json={
|
|
"simulation_id": sim_id,
|
|
"enable_graph_memory_update": True,
|
|
})
|
|
# Test that clone_graph was called (or at minimum the request didn't 500)
|
|
assert resp.status_code == 200
|
|
assert resp.get_json()["success"] is True
|