"""时序图谱测试:时间点切片、演化追加不覆盖、持久化往返。""" from __future__ import annotations from app.graph.tkg import TemporalKG, quarter_of, to_window from app.models import ChangeEvent, GraphObservation, JobDefinition def _kg() -> TemporalKG: kg = TemporalKG() kg.add_job(JobDefinition(job_id="job_T", title="测试工程师")) return kg def test_quarter_and_window_conversion(): from datetime import date assert quarter_of(date(2022, 2, 15)) == "2022Q1" assert quarter_of(date(2022, 11, 1)) == "2022Q4" assert to_window("2022-05-01") == "2022Q2" assert to_window("2022Q3") == "2022Q3" def test_snapshot_time_slicing(): """任意时间点切片:只看到该时刻已生效且未退出的技能。""" kg = _kg() kg.observe(GraphObservation(job_id="job_T", skill="Java", importance="required", weight=1.0, window="2020Q1", source_ids=["s1"])) kg.observe(GraphObservation(job_id="job_T", skill="Spring Cloud", importance="required", weight=1.0, window="2023Q1", source_ids=["s2"])) snap_2020 = kg.snapshot("job_T", "2020-06-01") assert {s["skill"] for s in snap_2020["skills"]} == {"Java"} snap_2024 = kg.snapshot("job_T", "2024-01-01") assert {s["skill"] for s in snap_2024["skills"]} == {"Java", "Spring Cloud"} def test_close_interval_removes_from_snapshot(): kg = _kg() kg.observe(GraphObservation(job_id="job_T", skill="Struts", importance="preferred", weight=1.0, window="2019Q1", source_ids=["s1"])) kg.close_interval("job_T", "Struts", "2022Q1") assert {s["skill"] for s in kg.snapshot("job_T", "2021-01-01")["skills"]} == {"Struts"} assert kg.snapshot("job_T", "2023-01-01")["skills"] == [] def test_evolution_appends_not_overwrites(): """同一技能退出后重新出现:新区间追加,旧区间保留(历史不覆盖)。""" kg = _kg() obs = GraphObservation(job_id="job_T", skill="Kafka", importance="required", weight=1.0, window="2020Q1", source_ids=["s1"]) kg.observe(obs) kg.close_interval("job_T", "Kafka", "2021Q1") kg.observe(GraphObservation(job_id="job_T", skill="Kafka", importance="required", weight=0.8, window="2023Q1", source_ids=["s2"])) intervals = kg.g["job_T"]["Kafka"]["intervals"] assert intervals == [["2020Q1", "2021Q1"], ["2023Q1", None]] assert kg.snapshot("job_T", "2020-06-01")["skills"] # 旧区间仍可切片 assert kg.snapshot("job_T", "2022-01-01")["skills"] == [] # 退出期间为空 assert kg.snapshot("job_T", "2024-01-01")["skills"] # 重现后恢复 # 事件流只追加 kg.add_event(ChangeEvent(event_id="e1", job_id="job_T", skill="Kafka", change_type="退出", window="2021Q1", description="x")) kg.add_event(ChangeEvent(event_id="e2", job_id="job_T", skill="Kafka", change_type="新增", window="2023Q1", description="y")) assert [e.event_id for e in kg.events_of("job_T")] == ["e1", "e2"] def test_save_load_roundtrip(tmp_path): kg = _kg() kg.observe(GraphObservation(job_id="job_T", skill="Java", importance="required", weight=1.0, window="2020Q1", source_ids=["s1"])) kg.add_event(ChangeEvent(event_id="e1", job_id="job_T", skill="Java", change_type="新增", window="2020Q1", description="d")) path = tmp_path / "g.json" kg.save(path) kg2 = TemporalKG.load(path) assert kg2.stats()["edges"] == 1 assert kg2.jobs["job_T"].title == "测试工程师" assert kg2.snapshot("job_T", "2020-06-01")["skills"][0]["skill"] == "Java" assert kg2.events_of("job_T")[0].event_id == "e1"