Files
moxiaonian b20fd5c3c9 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…
2026-10-12 19:05:09 +08:00

218 lines
9.8 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""档案(memory/):save → lookup 往返、同主键更新保留正文小节、无匹配输出。
脚本以 --memory-root / ZIPING_MEMORY_ROOT 覆盖档案根目录,测试用 tmp_path 隔离。
"""
import os
import re
import subprocess
import sys
from pathlib import Path
SKILL_ROOT = Path(__file__).resolve().parents[1]
SCRIPTS_DIR = SKILL_ROOT / "scripts"
SAVE = SCRIPTS_DIR / "memory_save.py"
LOOKUP = SCRIPTS_DIR / "memory_lookup.py"
ZHANGSAN = ["--name", "张三", "--gender", "男",
"--birth", "1990-05-14 08:30", "--place", "四川省成都市"]
# 晚子时案例:真太阳时落于 23:00–24:00,子初/子正换日四柱不同
WANZI = ["--name", "李四", "--gender", "女",
"--birth", "1996-12-08 00:30", "--place", "四川省乐山市"]
def run(script, mem_root, *args, use_env=False):
env = dict(os.environ)
env["PYTHONPATH"] = str(SCRIPTS_DIR) + os.pathsep + env.get("PYTHONPATH", "")
cmd = [sys.executable, str(script), *args]
if use_env:
env["ZIPING_MEMORY_ROOT"] = str(mem_root)
else:
cmd += ["--memory-root", str(mem_root)]
return subprocess.run(cmd, cwd=str(SCRIPTS_DIR), env=env,
capture_output=True, text=True, timeout=120)
def read_meta(text: str) -> dict:
"""受限 frontmatter 解析:仅取测试断言所需的标量字段。"""
lines = text.splitlines()
assert lines[0] == "---"
meta = {}
for ln in lines[1:]:
if ln == "---":
break
k, _, v = ln.partition(":")
meta[k.strip()] = v.strip().strip('"')
return meta
class TestSaveLookupRoundtrip:
def test_save_then_lookup_by_name_and_pillars(self, tmp_path):
r = run(SAVE, tmp_path, *ZHANGSAN)
assert r.returncode == 0, f"stderr:\n{r.stderr}"
path = Path(r.stdout.strip())
assert path.is_file()
assert path.parent == tmp_path / "archives"
text = path.read_text(encoding="utf-8")
# schema:frontmatter + 固定小节 + 嵌入命盘
meta = read_meta(text)
assert meta["name"] == "张三" and meta["gender"] == "男"
assert meta["birth_clock"] == "1990-05-14 08:30"
assert meta["birthplace"] == "四川省成都市"
assert meta["created_at"] and meta["updated_at"]
for section in ("## 盘面", "## 主要关注方向", "## 归纳",
"## 流年大事", "## 反馈与澄清"):
assert section in text
assert "## 四柱" in text # 盘面嵌入命盘全文
assert "- 性格:未断" in text
# 按姓名检索命中(参数覆盖 memory 根目录)
r = run(LOOKUP, tmp_path, "--name", "张三")
assert r.returncode == 0, f"stderr:\n{r.stderr}"
assert str(path) in r.stdout and "张三" in r.stdout
# 按四柱检索命中(环境变量覆盖 memory 根目录)
pillars = meta["pillars"].strip("[]").replace('"', "").replace(",", "")
r = run(LOOKUP, tmp_path, "--pillars", pillars, use_env=True)
assert r.returncode == 0, f"stderr:\n{r.stderr}"
assert str(path) in r.stdout
# 出生信息组合检索,空白容错
r = run(LOOKUP, tmp_path, "--birth", "1990-05-14 08:30",
"--place", " 四川省成都市 ", "--gender", "男")
assert r.returncode == 0 and str(path) in r.stdout
class TestSameKeyUpdate:
def test_update_preserves_body_sections(self, tmp_path):
r1 = run(SAVE, tmp_path, *ZHANGSAN)
assert r1.returncode == 0, f"stderr:\n{r1.stderr}"
path = Path(r1.stdout.strip())
# 模拟解释完成后的回写:改归纳、加反馈、固定 created_at 以便断言
text = path.read_text(encoding="utf-8")
text = text.replace("- 性格:未断", "- 性格:沉稳内敛【已核实】")
text = text.replace("(无)", "- 【已证伪】断 2024 年升职;实际为调岗未升。")
text = re.sub(r'^created_at:.*$', 'created_at: "2000-01-01 00:00"',
text, count=1, flags=re.M)
path.write_text(text, encoding="utf-8")
# 同主键再次建档:同一文件、盘面刷新、其余小节保留、created_at 不动
r2 = run(SAVE, tmp_path, *ZHANGSAN)
assert r2.returncode == 0, f"stderr:\n{r2.stderr}"
assert Path(r2.stdout.strip()) == path
assert list((tmp_path / "archives").glob("*.md")) == [path]
text = path.read_text(encoding="utf-8")
assert "- 性格:沉稳内敛【已核实】" in text
assert "【已证伪】断 2024 年升职;实际为调岗未升。" in text
assert "## 四柱" in text # 盘面仍为完整命盘
assert text.count("# 命盘") == 1 # 旧盘面整体替换,无残留重复
meta = read_meta(text)
assert meta["created_at"] == "2000-01-01 00:00"
assert meta["updated_at"] != meta["created_at"]
class TestLookupNoMatch:
def test_no_match_exit_0(self, tmp_path):
r = run(LOOKUP, tmp_path, "--name", "不存在的人")
assert r.returncode == 0, f"stderr:\n{r.stderr}"
assert "无档案" in r.stdout
def test_no_condition_exit_2(self, tmp_path):
r = run(LOOKUP, tmp_path)
assert r.returncode == 2
class TestUpdateChart:
"""换盘(--update-chart):工作盘唯一——盘面段整体替换为新盘,frontmatter
与文件名同步,其余小节保留;旧盘信息由使用方记入反馈与澄清。"""
def test_zizheng_replaces_chart_and_renames(self, tmp_path):
r1 = run(SAVE, tmp_path, *WANZI)
assert r1.returncode == 0, f"stderr:\n{r1.stderr}"
path = Path(r1.stdout.strip())
assert "丙子庚子己卯甲子" in path.name # 默认子初换日盘(晚子时归次日)
# 模拟核对后的回写 + 固定 created_at
text = path.read_text(encoding="utf-8")
text = text.replace("(无)", "- 【已核实】2016 年离乡赴厦门读大学。")
text = re.sub(r'^created_at:.*$', 'created_at: "2000-01-01 00:00"',
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
assert "待复核" in r2.stderr # 变动柱影响条目提示复核
new_path = Path(r2.stdout.strip())
assert new_path != path and not path.exists()
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["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 "- 【已核实】2016 年离乡赴厦门读大学。" in text
def test_birth_correction_updates_meta(self, tmp_path):
r1 = run(SAVE, tmp_path, *ZHANGSAN)
assert r1.returncode == 0, f"stderr:\n{r1.stderr}"
path = Path(r1.stdout.strip())
r2 = run(SAVE, tmp_path, "--update-chart", str(path),
"--birth", "1990-05-14 10:30")
assert r2.returncode == 0, f"stderr:\n{r2.stderr}"
assert "时柱" in r2.stderr and "待复核" in r2.stderr
new_path = Path(r2.stdout.strip())
assert new_path != path and not path.exists() # hash6 随 birth 变
meta = read_meta(new_path.read_text(encoding="utf-8"))
assert meta["birth_clock"] == "1990-05-14 10:30"
def test_same_chart_keeps_filename(self, tmp_path):
r1 = run(SAVE, tmp_path, *ZHANGSAN)
assert r1.returncode == 0, f"stderr:\n{r1.stderr}"
path = Path(r1.stdout.strip())
r2 = run(SAVE, tmp_path, "--update-chart", str(path))
assert r2.returncode == 0, f"stderr:\n{r2.stderr}"
assert Path(r2.stdout.strip()) == path # 四柱未变,文件名不动
assert "待复核" not in r2.stderr
def test_conflict_refuses_overwrite(self, tmp_path):
run(SAVE, tmp_path, *WANZI)
olds = list((tmp_path / "archives").glob("*.md"))
assert len(olds) == 1
old = olds[0]
r = run(SAVE, tmp_path, "--update-chart", str(old),
"--day-boundary", "子正")
assert r.returncode == 0, f"stderr:\n{r.stderr}"
# 重新建子初盘档案,再换子正 → 目标已存在,拒绝覆盖
run(SAVE, tmp_path, *WANZI)
r = run(SAVE, tmp_path, "--update-chart", str(old),
"--day-boundary", "子正")
assert r.returncode == 2
assert "已存在" in r.stderr
assert len(list((tmp_path / "archives").glob("*.md"))) == 2
def test_missing_archive_exit_2(self, tmp_path):
r = run(SAVE, tmp_path, "--update-chart",
str(tmp_path / "archives" / "nope.md"))
assert r.returncode == 2
def test_rejects_name_gender_overrides(self, tmp_path):
r1 = run(SAVE, tmp_path, *ZHANGSAN)
assert r1.returncode == 0, f"stderr:\n{r1.stderr}"
path = r1.stdout.strip()
r = run(SAVE, tmp_path, "--update-chart", path, "--name", "王五")
assert r.returncode == 2