"""演化分析测试:构造两季度(多窗口)数据验证 新增/增强/减弱/退出 事件。""" from __future__ import annotations from app.evolution.analyzer import analyze from app.graph.tkg import TemporalKG from app.models import GraphObservation, JobDefinition def _obs(job: str, skill: str, window: str, n: int) -> list[GraphObservation]: """生成 n 条同窗口观察(模拟窗口内 n 条 JD 提到该技能)。""" return [GraphObservation(job_id=job, skill=skill, importance="required", weight=1.0, window=window, source_ids=[f"{window}#{i}"]) for i in range(n)] def test_quarter_window_events(): kg = TemporalKG() kg.add_job(JobDefinition(job_id="job_A", title="岗位A")) observations: list[GraphObservation] = [] # 老技能 Legacy:2024Q1-Q3 稳定出现(3 窗口),Q4 与次年 Q1 连续消失 → 退出 for w in ["2024Q1", "2024Q2", "2024Q3"]: observations += _obs("job_A", "Legacy技能", w, 3) # 新技能 NewSkill:2024Q4 首次出现且支撑 >=2 → 新增 observations += _obs("job_A", "NewSkill", "2024Q4", 3) # 增强技能:3 → 9(>=1.5 倍且差 >=3) observations += _obs("job_A", "GrowSkill", "2024Q3", 3) observations += _obs("job_A", "GrowSkill", "2024Q4", 9) # 减弱技能:8 → 3(<=0.5 倍且差 >=3) observations += _obs("job_A", "ShrinkSkill", "2024Q3", 8) observations += _obs("job_A", "ShrinkSkill", "2024Q4", 3) for o in observations: # 与 pipeline 一致:观察先写入图谱,再分析演化 kg.observe(o) events = analyze(kg, observations=observations) by_type = {} for e in events: by_type.setdefault(e.change_type, []).append(e) # 新增:LegacySkill 首现 + NewSkill 首现 new_skills = {e.skill for e in by_type.get("新增", [])} assert "NewSkill" in new_skills and "Legacy技能" in new_skills # 退出:Legacy 技能在 2024Q4 判定(Q4、次年 Q1 连续为 0,且已稳定 3 窗口) exits = [e for e in by_type.get("退出", []) if e.skill == "Legacy技能"] assert exits and exits[0].window == "2024Q4" assert exits[0].prev_weight > 0 and exits[0].curr_weight == 0 assert "数据源" in exits[0].description # 中文说明注明数据源 # 增强 / 减弱 assert any(e.skill == "GrowSkill" and e.window == "2024Q4" for e in by_type.get("增强", [])) assert any(e.skill == "ShrinkSkill" and e.window == "2024Q4" for e in by_type.get("减弱", [])) # 退出事件关闭了图谱区间 assert kg.g["job_A"]["Legacy技能"]["intervals"][-1][1] == "2024Q4" # 事件保存前后权重、时间窗、来源 grow = next(e for e in by_type["增强"] if e.skill == "GrowSkill") assert grow.prev_weight == 3.0 and grow.curr_weight == 9.0 assert grow.source_ids def test_no_spurious_exit_for_new_skill(): """只出现 1-2 个窗口的技能消失不报“退出”(抑制稀疏噪声)。""" kg = TemporalKG() kg.add_job(JobDefinition(job_id="job_B", title="岗位B")) observations = _obs("job_B", "一次性技能", "2024Q1", 3) # 只出现 1 个窗口 observations += _obs("job_B", "常驻技能", "2024Q1", 2) observations += _obs("job_B", "常驻技能", "2024Q2", 2) events = analyze(kg, observations=observations) assert not [e for e in events if e.change_type == "退出" and e.skill == "一次性技能"]