diff --git a/backend/app/api/simulation.py b/backend/app/api/simulation.py index aae76788..04f0ce00 100644 --- a/backend/app/api/simulation.py +++ b/backend/app/api/simulation.py @@ -273,7 +273,7 @@ def _check_simulation_prepared(simulation_id: str) -> tuple: 检查条件: 1. state.json 存在且 status 为 "ready" - 2. 必要文件存在:reddit_profiles.json, twitter_profiles.csv, simulation_config.json + 2. simulation_config.json 和已启用平台对应的 Profile 文件存在 注意:运行脚本(run_*.py)保留在 backend/scripts/ 目录,不再复制到模拟目录 @@ -295,10 +295,26 @@ def _check_simulation_prepared(simulation_id: str) -> tuple: # 必要文件列表(不包括脚本,脚本位于 backend/scripts/) required_files = [ "state.json", - "simulation_config.json", - "reddit_profiles.json", - "twitter_profiles.csv" + "simulation_config.json" ] + + # Profile文件仅在对应平台启用时生成。旧状态文件没有平台字段, + # 此时继续按双平台处理以保持向后兼容。 + state_file = os.path.join(simulation_dir, "state.json") + platform_state = {} + if os.path.exists(state_file): + try: + import json + with open(state_file, 'r', encoding='utf-8') as f: + platform_state = json.load(f) + except (OSError, json.JSONDecodeError): + # 保留后续原有的缺失文件和状态文件错误处理。 + pass + + if platform_state.get("enable_reddit", True): + required_files.append("reddit_profiles.json") + if platform_state.get("enable_twitter", True): + required_files.append("twitter_profiles.csv") # 检查文件是否存在 existing_files = [] @@ -318,7 +334,6 @@ def _check_simulation_prepared(simulation_id: str) -> tuple: } # 检查state.json中的状态 - state_file = os.path.join(simulation_dir, "state.json") try: import json with open(state_file, 'r', encoding='utf-8') as f: @@ -341,14 +356,19 @@ def _check_simulation_prepared(simulation_id: str) -> tuple: prepared_statuses = ["ready", "preparing", "running", "completed", "stopped", "failed"] if status in prepared_statuses and config_generated: # 获取文件统计信息 - profiles_file = os.path.join(simulation_dir, "reddit_profiles.json") config_file = os.path.join(simulation_dir, "simulation_config.json") - profiles_count = 0 - if os.path.exists(profiles_file): + profiles_count = state_data.get("profiles_count", 0) + if state_data.get("enable_reddit", True): + profiles_file = os.path.join(simulation_dir, "reddit_profiles.json") with open(profiles_file, 'r', encoding='utf-8') as f: profiles_data = json.load(f) profiles_count = len(profiles_data) if isinstance(profiles_data, list) else 0 + elif state_data.get("enable_twitter", True): + import csv + profiles_file = os.path.join(simulation_dir, "twitter_profiles.csv") + with open(profiles_file, 'r', encoding='utf-8', newline='') as f: + profiles_count = sum(1 for _ in csv.DictReader(f)) # 如果状态是preparing但文件已完成,自动更新状态为ready if status == "preparing": diff --git a/backend/tests/test_simulation_prepared_platforms.py b/backend/tests/test_simulation_prepared_platforms.py new file mode 100644 index 00000000..83d75a60 --- /dev/null +++ b/backend/tests/test_simulation_prepared_platforms.py @@ -0,0 +1,117 @@ +import json + +import pytest + +from app.api.simulation import _check_simulation_prepared +from app.config import Config + + +def _write_prepared_simulation( + root, + *, + enable_twitter=None, + enable_reddit=None, + profile_files=(), +): + simulation_id = "sim_prepared" + simulation_dir = root / simulation_id + simulation_dir.mkdir(parents=True) + + state = { + "status": "ready", + "config_generated": True, + "profiles_count": 1, + } + if enable_twitter is not None: + state["enable_twitter"] = enable_twitter + if enable_reddit is not None: + state["enable_reddit"] = enable_reddit + + (simulation_dir / "state.json").write_text( + json.dumps(state), + encoding="utf-8", + ) + (simulation_dir / "simulation_config.json").write_text( + "{}", + encoding="utf-8", + ) + + for filename in profile_files: + content = "[{}]" if filename.endswith(".json") else "user_id,user_name\n0,alice\n" + (simulation_dir / filename).write_text(content, encoding="utf-8") + + return simulation_id + + +def test_twitter_only_simulation_does_not_require_reddit_profiles(tmp_path, monkeypatch): + simulation_id = _write_prepared_simulation( + tmp_path, + enable_twitter=True, + enable_reddit=False, + profile_files=("twitter_profiles.csv",), + ) + monkeypatch.setattr(Config, "OASIS_SIMULATION_DATA_DIR", str(tmp_path)) + + is_prepared, info = _check_simulation_prepared(simulation_id) + + assert is_prepared is True + assert info["profiles_count"] == 1 + assert "twitter_profiles.csv" in info["existing_files"] + assert "reddit_profiles.json" not in info["existing_files"] + + +def test_reddit_only_simulation_does_not_require_twitter_profiles(tmp_path, monkeypatch): + simulation_id = _write_prepared_simulation( + tmp_path, + enable_twitter=False, + enable_reddit=True, + profile_files=("reddit_profiles.json",), + ) + monkeypatch.setattr(Config, "OASIS_SIMULATION_DATA_DIR", str(tmp_path)) + + is_prepared, info = _check_simulation_prepared(simulation_id) + + assert is_prepared is True + assert info["profiles_count"] == 1 + assert "reddit_profiles.json" in info["existing_files"] + assert "twitter_profiles.csv" not in info["existing_files"] + + +@pytest.mark.parametrize( + ("enable_twitter", "enable_reddit", "missing_profile"), + [ + (True, False, "twitter_profiles.csv"), + (False, True, "reddit_profiles.json"), + ], +) +def test_enabled_platform_profile_is_still_required( + tmp_path, + monkeypatch, + enable_twitter, + enable_reddit, + missing_profile, +): + simulation_id = _write_prepared_simulation( + tmp_path, + enable_twitter=enable_twitter, + enable_reddit=enable_reddit, + ) + monkeypatch.setattr(Config, "OASIS_SIMULATION_DATA_DIR", str(tmp_path)) + + is_prepared, info = _check_simulation_prepared(simulation_id) + + assert is_prepared is False + assert info["missing_files"] == [missing_profile] + + +def test_legacy_state_without_platform_flags_requires_both_profiles(tmp_path, monkeypatch): + simulation_id = _write_prepared_simulation( + tmp_path, + profile_files=("reddit_profiles.json",), + ) + monkeypatch.setattr(Config, "OASIS_SIMULATION_DATA_DIR", str(tmp_path)) + + is_prepared, info = _check_simulation_prepared(simulation_id) + + assert is_prepared is False + assert info["missing_files"] == ["twitter_profiles.csv"]