diff --git a/backend/app/storage/plan_session_storage.py b/backend/app/storage/plan_session_storage.py index c6c0f3da..6b99cde2 100644 --- a/backend/app/storage/plan_session_storage.py +++ b/backend/app/storage/plan_session_storage.py @@ -84,7 +84,7 @@ def _deserialize(self, data: Dict[str, Any]) -> PlanSession: def create(self, session: PlanSession) -> PlanSession: path = self._path(session.id) - with open(path, 'w') as f: + with open(path, 'w', encoding='utf-8') as f: json.dump(self._serialize(session), f, indent=2, default=str) return session @@ -92,7 +92,7 @@ def get(self, session_id: str) -> Optional[PlanSession]: path = self._path(session_id) if not path.exists(): return None - with open(path, 'r') as f: + with open(path, 'r', encoding='utf-8') as f: return self._deserialize(json.load(f)) def update(self, session: PlanSession) -> PlanSession: @@ -102,7 +102,7 @@ def list_all(self, status: Optional[PlanSessionStatus] = None) -> List[PlanSessi sessions = [] for p in sorted(self.base_path.glob("psess_*.json"), reverse=True): try: - with open(p, 'r') as f: + with open(p, 'r', encoding='utf-8') as f: s = self._deserialize(json.load(f)) if status is None or s.status == status: sessions.append(s) @@ -136,7 +136,7 @@ def _deserialize(self, data: Dict[str, Any]) -> CandidatePlan: def create(self, candidate: CandidatePlan) -> CandidatePlan: path = self._path(candidate.id) - with open(path, 'w') as f: + with open(path, 'w', encoding='utf-8') as f: json.dump(self._serialize(candidate), f, indent=2, default=str) return candidate @@ -144,14 +144,14 @@ def get(self, candidate_id: str) -> Optional[CandidatePlan]: path = self._path(candidate_id) if not path.exists(): return None - with open(path, 'r') as f: + with open(path, 'r', encoding='utf-8') as f: return self._deserialize(json.load(f)) def list_by_session(self, session_id: str) -> List[CandidatePlan]: candidates = [] for p in sorted(self.base_path.glob("cplan_*.json")): try: - with open(p, 'r') as f: + with open(p, 'r', encoding='utf-8') as f: c = self._deserialize(json.load(f)) if c.sessionId == session_id: candidates.append(c) diff --git a/backend/tests/test_pr_04_plan_session_encoding.py b/backend/tests/test_pr_04_plan_session_encoding.py new file mode 100644 index 00000000..ed76b52c --- /dev/null +++ b/backend/tests/test_pr_04_plan_session_encoding.py @@ -0,0 +1,81 @@ +"""Test that plan_session_storage.py handles non-ASCII content via encoding=utf-8.""" + +from datetime import datetime + +import pytest + +from app.models.plan_session import ( + PlanSession, + PlanSessionConfig, + PlanSessionStatus, + CandidatePlan, +) +from app.storage.plan_session_storage import PlanSessionStorage, CandidatePlanStorage + + +@pytest.fixture +def tmp_data_dir(tmp_path): + return str(tmp_path) + + +def test_session_roundtrip_non_ascii(tmp_data_dir): + """Create a PlanSession with non-ASCII config and verify round-trip.""" + storage = PlanSessionStorage(tmp_data_dir) + config = PlanSessionConfig( + userNotes="优化模型性能并提高代码覆盖率", + paperType="algorithmic_method", + ) + session = PlanSession( + id="psess_test001", + config=config, + status=PlanSessionStatus.PENDING, + createdAt=datetime.now(), + ) + + storage.create(session) + fetched = storage.get("psess_test001") + + assert fetched is not None + assert fetched.config.userNotes == "优化模型性能并提高代码覆盖率" + + +def test_list_all_non_ascii(tmp_data_dir): + """Verify list_all returns sessions with non-ASCII content correctly.""" + storage = PlanSessionStorage(tmp_data_dir) + + for i, note in enumerate(["First note", "Deuxième note", "第三笔记"]): + config = PlanSessionConfig(userNotes=note, paperType="algorithmic_method") + session = PlanSession( + id=f"psess_test{i:03d}", + config=config, + status=PlanSessionStatus.PENDING, + createdAt=datetime.now(), + ) + storage.create(session) + + sessions = storage.list_all() + assert len(sessions) == 3 + notes = {s.config.userNotes for s in sessions} + assert "First note" in notes + assert "Deuxième note" in notes + assert "第三笔记" in notes + + +def test_candidate_roundtrip_non_ascii(tmp_data_dir): + """Create a CandidatePlan with non-ASCII content and verify round-trip.""" + storage = CandidatePlanStorage(tmp_data_dir) + candidate = CandidatePlan( + id="cplan_test001", + sessionId="psess_001", + indexNumber=1, + title="使用遗传算法优化超参数配置", + planAbstract="本文提出了一种新的方法", + createdAt=datetime.now(), + ) + + storage.create(candidate) + fetched = storage.get("cplan_test001") + + assert fetched is not None + assert fetched.title == "使用遗传算法优化超参数配置" + assert fetched.planAbstract == "本文提出了一种新的方法"