v3.6.2 同步发版:晚子时不再双重换日、港澳时区归位
- 晚子时(真太阳时 23:00–24:00,约 4% 的盘)修正:换日改由 tyme4py provider 原生实现(cast 新增 sect 参数,1=子初 / 2=子正),排盘层不再自行预滚日。3.6.1 及此前会滚两次,日柱多进一天、时柱随之错干——锚点 1973-08-23 23:32 男:旧 癸巳日壬子时 → 正 壬辰日庚子时。已排过的晚子时盘建议重排核对 - --day-boundary 子正 现真正生效(走 sect=2,晚子时日柱算当天),此前只改审计文案不改实际排盘 - data/cities.json 港澳 28 个条目由 Asia/Shanghai 改为 Asia/Hong_Kong(19)、Asia/Macau(9):大陆 1986–1991 行夏令时而港澳 1979 年后无,沿用上海区会把大陆夏令时错加到港澳命主身上 - 修副本环境 `pytest tests/` 收集期中断(test_sync_tool 模块级加载外层 tools/,skipif 挡不住导入),现按设计整模块 skip - .gitignore 新增 data/.license.json、data/.update_check.json,本机授权与更新缓存不再可能误入仓 - 测试 178 项全绿(副本内 sync 工具 4 项按设计 skip);manifest 1278 文件,ed25519 签名,公钥指纹 3089bc1cc9a453e5…
This commit is contained in:
@@ -17,6 +17,9 @@ SCRIPTS_DIR = SKILL_ROOT / "scripts"
|
||||
GOLDEN_DIR = Path(__file__).resolve().parent / "golden"
|
||||
if str(SCRIPTS_DIR) not in sys.path:
|
||||
sys.path.insert(0, str(SCRIPTS_DIR))
|
||||
_VENDOR = SKILL_ROOT / "vendor"
|
||||
if str(_VENDOR) not in sys.path:
|
||||
sys.path.insert(0, str(_VENDOR))
|
||||
|
||||
from astro.day_boundary import resolve_calendar_day # noqa: E402
|
||||
from astro.solar_time import to_true_solar_time # noqa: E402
|
||||
@@ -65,5 +68,39 @@ class LateZiGoldenTest(unittest.TestCase):
|
||||
self.assertIs(flag["is_late_zi"], s["expect"]["is_late_zi"])
|
||||
|
||||
|
||||
class LateZiSectPillarsTest(unittest.TestCase):
|
||||
"""晚子时双滚回归:换日由 tyme4py provider 口径实现(sect 参数),
|
||||
调用方不再预滚日。锚点:1973-08-23 23:32 真太阳时。
|
||||
子初 → 壬辰日庚子时(滚一日);子正 → 辛卯日庚子时(不滚,晚子时时柱仍次日起)。
|
||||
"""
|
||||
|
||||
def test_sect1_rolls_once(self):
|
||||
from chart.tyme_adapter import cast
|
||||
r = cast(datetime(1973, 8, 23, 23, 32), "男", sect=1)
|
||||
self.assertEqual(r["day"]["gan"] + r["day"]["zhi"], "壬辰")
|
||||
self.assertEqual(r["hour"]["gan"] + r["hour"]["zhi"], "庚子")
|
||||
|
||||
def test_sect2_no_roll(self):
|
||||
from chart.tyme_adapter import cast
|
||||
r = cast(datetime(1973, 8, 23, 23, 32), "男", sect=2)
|
||||
self.assertEqual(r["day"]["gan"] + r["day"]["zhi"], "辛卯")
|
||||
self.assertEqual(r["hour"]["gan"] + r["hour"]["zhi"], "庚子")
|
||||
|
||||
def test_provider_restored(self):
|
||||
from chart.tyme_adapter import cast
|
||||
from tyme4py.eightchar.provider.impl import DefaultEightCharProvider
|
||||
from tyme4py.lunar import LunarHour
|
||||
cast(datetime(1973, 8, 23, 23, 32), "男", sect=2)
|
||||
self.assertIsInstance(LunarHour.provider, DefaultEightCharProvider)
|
||||
|
||||
def test_non_late_zi_unaffected(self):
|
||||
from chart.tyme_adapter import cast
|
||||
for sect in (1, 2):
|
||||
with self.subTest(sect=sect):
|
||||
r = cast(datetime(1973, 8, 23, 12, 0), "男", sect=sect)
|
||||
self.assertEqual(r["day"]["gan"] + r["day"]["zhi"], "辛卯")
|
||||
self.assertEqual(r["hour"]["gan"] + r["hour"]["zhi"], "甲午")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -131,7 +131,7 @@ class TestUpdateChart:
|
||||
r1 = run(SAVE, tmp_path, *WANZI)
|
||||
assert r1.returncode == 0, f"stderr:\n{r1.stderr}"
|
||||
path = Path(r1.stdout.strip())
|
||||
assert "丙子庚子庚辰丙子" in path.name # 默认子初换日盘
|
||||
assert "丙子庚子己卯甲子" in path.name # 默认子初换日盘(晚子时归次日)
|
||||
|
||||
# 模拟核对后的回写 + 固定 created_at
|
||||
text = path.read_text(encoding="utf-8")
|
||||
@@ -140,28 +140,28 @@ class TestUpdateChart:
|
||||
text, count=1, flags=re.M)
|
||||
path.write_text(text, encoding="utf-8")
|
||||
|
||||
# 用户确认子正换日:重排替换盘面段,日柱/时柱变 → 改新主键名
|
||||
# 用户确认子正换日:重排替换盘面段,日柱变(时柱同为晚子时起法,不变)→ 改新主键名
|
||||
r2 = run(SAVE, tmp_path, "--update-chart", str(path),
|
||||
"--day-boundary", "子正")
|
||||
assert r2.returncode == 0, f"stderr:\n{r2.stderr}"
|
||||
assert "日柱 庚辰→己卯" in r2.stderr and "时柱 丙子→甲子" in r2.stderr
|
||||
assert "日柱 己卯→戊寅" in r2.stderr
|
||||
assert "待复核" in r2.stderr # 变动柱影响条目提示复核
|
||||
new_path = Path(r2.stdout.strip())
|
||||
assert new_path != path and not path.exists()
|
||||
assert "丙子庚子己卯甲子" in new_path.name
|
||||
assert "丙子庚子戊寅甲子" in new_path.name
|
||||
assert list((tmp_path / "archives").glob("*.md")) == [new_path]
|
||||
|
||||
text = new_path.read_text(encoding="utf-8")
|
||||
meta = read_meta(text)
|
||||
assert meta["pillars"] == '["丙子", "庚子", "己卯", "甲子"]'
|
||||
assert meta["day_master"] == "己"
|
||||
assert meta["pillars"] == '["丙子", "庚子", "戊寅", "甲子"]'
|
||||
assert meta["day_master"] == "戊"
|
||||
assert meta["created_at"] == "2000-01-01 00:00" # 保留
|
||||
# 盘面段只剩新盘:日界审计与晚子时警示均为子正口径,无旧盘残留
|
||||
assert "- 日界规则:子正换日(真太阳时 00:00)" in text
|
||||
assert "已按「子正换日」定盘" in text
|
||||
assert text.count("# 命盘") == 1
|
||||
chart_section = text.split("## 盘面")[1].split("## 主要关注方向")[0]
|
||||
assert "日柱:己卯" in chart_section and "日柱:庚辰" not in chart_section
|
||||
assert "日柱:戊寅" in chart_section and "日柱:己卯" not in chart_section
|
||||
# 其余小节保留
|
||||
assert "- 【已核实】2016 年离乡赴厦门读大学。" in text
|
||||
|
||||
|
||||
+12
-6
@@ -1,7 +1,9 @@
|
||||
"""外层 tools/sync_license_copy.py 的集成测试。
|
||||
|
||||
sync/build/sign 工具留在授权方工作区外层 tools/,不随本仓库分发;
|
||||
副本或 clone 环境下 tools/ 不存在,整个模块 skip。
|
||||
副本或 clone 环境下 tools/ 不存在,整个模块 skip。注意 skipif 只跳过测试
|
||||
**执行**、挡不住模块导入,故下面的模块级 _load_module 也带同一存在性守卫,
|
||||
否则副本里 `pytest tests/` 会在收集阶段就 FileNotFoundError 并中断全套测试。
|
||||
|
||||
测试用微型假母版(monkeypatch 替换模块 SRC)同步进临时授权副本,
|
||||
不碰真实母版与真实副本;签名用测试私钥,UPDATE_PUBLIC_KEY_HEX 同步造假。
|
||||
@@ -18,10 +20,12 @@ ROOT = Path(__file__).resolve().parents[1]
|
||||
TOOLS = ROOT.parent / "tools"
|
||||
SYNC_TOOL = TOOLS / "sync_license_copy.py"
|
||||
|
||||
_HAS_TOOLS = (SYNC_TOOL.is_file()
|
||||
and (TOOLS / "build_manifest.py").is_file()
|
||||
and (TOOLS / "sign_manifest.py").is_file())
|
||||
|
||||
pytestmark = pytest.mark.skipif(
|
||||
not (SYNC_TOOL.is_file()
|
||||
and (TOOLS / "build_manifest.py").is_file()
|
||||
and (TOOLS / "sign_manifest.py").is_file()),
|
||||
not _HAS_TOOLS,
|
||||
reason="外层 tools/ 不随仓库分发,本环境无同步工具",
|
||||
)
|
||||
|
||||
@@ -41,8 +45,10 @@ def _load_module(name: str, path: Path):
|
||||
return mod
|
||||
|
||||
|
||||
sync_tool = _load_module("sync_license_copy", SYNC_TOOL)
|
||||
build_manifest = _load_module("build_manifest", TOOLS / "build_manifest.py")
|
||||
# 无 tools/ 时置 None:本模块所有测试已被 skipif 跳过,不会触达这两个名字
|
||||
sync_tool = _load_module("sync_license_copy", SYNC_TOOL) if _HAS_TOOLS else None
|
||||
build_manifest = (_load_module("build_manifest", TOOLS / "build_manifest.py")
|
||||
if _HAS_TOOLS else None)
|
||||
|
||||
|
||||
def _make_master(src: Path, pk_hex: str) -> None:
|
||||
|
||||
@@ -50,5 +50,36 @@ class HistoryTzGoldenTest(unittest.TestCase):
|
||||
)
|
||||
|
||||
|
||||
class HkMoDstTest(unittest.TestCase):
|
||||
"""香港/澳门时区回归:1986–1991 中国大陆实行夏令时,港澳 1979 年后无;
|
||||
cities.json 港澳条目必须为 Asia/Hong_Kong / Asia/Macau,不得随大陆拨快。"""
|
||||
|
||||
def test_hongkong_1987_no_dst(self):
|
||||
loc = resolve_location("香港")
|
||||
_utc, audit = to_utc(datetime(1987, 7, 5, 5, 0), loc)
|
||||
self.assertEqual(loc.tz, "Asia/Hong_Kong")
|
||||
self.assertIs(audit["is_dst"], False)
|
||||
|
||||
def test_macau_1987_no_dst(self):
|
||||
loc = resolve_location("澳门")
|
||||
_utc, audit = to_utc(datetime(1987, 7, 5, 5, 0), loc)
|
||||
self.assertEqual(loc.tz, "Asia/Macau")
|
||||
self.assertIs(audit["is_dst"], False)
|
||||
|
||||
def test_mainland_1987_dst_kept(self):
|
||||
# 回归保护:大陆 1987 年夏令时仍应拨快
|
||||
loc = resolve_location("北京")
|
||||
_utc, audit = to_utc(datetime(1987, 7, 5, 5, 0), loc)
|
||||
self.assertIs(audit["is_dst"], True)
|
||||
|
||||
def test_hongkong_1970_historical_dst(self):
|
||||
# 香港 1949 后的历史夏令时(如 1970 年)应被 IANA 库正确处理
|
||||
# (1949 前走民国五时区历史表分支,不在此断言范围)
|
||||
loc = resolve_location("香港")
|
||||
_utc, audit = to_utc(datetime(1970, 7, 15, 12, 0), loc)
|
||||
self.assertEqual(audit["tz_used"], "Asia/Hong_Kong")
|
||||
self.assertIs(audit["is_dst"], True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user