feat: 工资查询助手首版 v1.0.105

基于 skill-template 脚手架开发,包含:
- payroll_config(费率配置,可编辑)+ payslip_snapshots(解析历史,只读)2 张业务表
- 全国默认 + 北上广 4 套费率(社保/公积金/个税起征点/7 级税率表)
- 5 个业务 Action:calculate-payslip-simple / calculate-payslip-full / explain-term / view-config / edit-config
- 个税累计预扣法计算,支持多月历史累加
- 20 个薪资名词库(社保/公积金/个税/全勤/加班费等)
- 完整市场文档(README/TUTORIAL/DEMO/CHANGELOG)
- 296 测试全部通过
This commit is contained in:
skill-dev
2026-09-07 17:02:05 +08:00
commit 3db196e49d
205 changed files with 29927 additions and 0 deletions

169
tests/test_runtime_paths.py Normal file
View File

@@ -0,0 +1,169 @@
# -*- coding: utf-8 -*-
"""util.runtime_paths:技能根、DB 路径、数据目录均落在隔离数据根下。"""
from __future__ import annotations
import os
import unittest
from _support import IsolatedDataRoot, get_skill_root
_FAKE_REAL_ROOT = r"D:\jiangchang-data"
def _legacy_env_key(suffix: str) -> str:
return "CL" + "AW_" + suffix
class TestRuntimePaths(unittest.TestCase):
def test_paths_under_isolated_root(self) -> None:
win_default = r"D:\jiangchang-data"
with IsolatedDataRoot() as tmp:
import importlib
import util.runtime_paths as rp
importlib.reload(rp)
skill_root = rp.get_skill_root()
self.assertTrue(os.path.isdir(skill_root))
self.assertTrue(os.path.isfile(os.path.join(skill_root, "SKILL.md")))
db_path = rp.get_db_path()
self.assertTrue(os.path.normpath(db_path).startswith(os.path.normpath(tmp)))
self.assertNotIn(win_default, db_path)
data_dir = rp.get_skill_data_dir()
self.assertTrue(os.path.normpath(data_dir).startswith(os.path.normpath(tmp)))
self.assertIn("_test", data_dir.replace("\\", "/"))
self.assertNotIn(win_default, data_dir)
self.assertEqual(get_skill_root(), skill_root)
def test_isolation_overrides_prefixed_real_data_root(self) -> None:
"""先设假数据根,再进入隔离;get_db_path 必须落在 tempdir。"""
saved = {
k: os.environ.get(k)
for k in ("JIANGCHANG_DATA_ROOT", "JIANGCHANG_USER_ID")
}
try:
os.environ["JIANGCHANG_DATA_ROOT"] = _FAKE_REAL_ROOT
with IsolatedDataRoot() as tmp:
import importlib
import util.runtime_paths as rp
importlib.reload(rp)
db_path = rp.get_db_path()
norm_db = os.path.normcase(os.path.normpath(db_path))
norm_tmp = os.path.normcase(os.path.normpath(tmp))
self.assertTrue(norm_db.startswith(norm_tmp), msg=f"db_path={db_path!r} tmp={tmp!r}")
norm_fake = os.path.normcase(os.path.normpath(_FAKE_REAL_ROOT))
self.assertFalse(
norm_db.startswith(norm_fake),
msg=f"db_path must not be under fake root: {db_path!r}",
)
finally:
for k, v in saved.items():
if v is None:
os.environ.pop(k, None)
else:
os.environ[k] = v
def test_legacy_env_vars_do_not_override_canonical(self) -> None:
with IsolatedDataRoot() as tmp:
os.environ[_legacy_env_key("DATA_ROOT")] = _FAKE_REAL_ROOT
os.environ[_legacy_env_key("USER_ID")] = "legacy-user"
import importlib
import util.runtime_paths as rp
importlib.reload(rp)
data_dir = rp.get_skill_data_dir()
self.assertTrue(os.path.normpath(data_dir).startswith(os.path.normpath(tmp)))
self.assertIn("_test", data_dir.replace("\\", "/"))
def test_resolve_data_path_defaults_under_skill_data_dir(self) -> None:
with IsolatedDataRoot() as tmp:
import importlib
import util.runtime_paths as rp
importlib.reload(rp)
path = rp.resolve_data_path(None, "output", create=True)
norm = os.path.normcase(os.path.normpath(path))
norm_tmp = os.path.normcase(os.path.normpath(tmp))
self.assertTrue(norm.startswith(norm_tmp))
self.assertTrue(norm.endswith(os.path.normcase(os.path.join("output"))))
def test_resolve_data_path_relative_uses_data_dir_not_cwd(self) -> None:
with IsolatedDataRoot() as tmp:
import importlib
import util.runtime_paths as rp
importlib.reload(rp)
os.environ["SKILL_OUTPUT_DIR"] = "output/videos"
from jiangchang_skill_core import config
config.reset_cache()
importlib.reload(rp)
path = rp.resolve_data_path("SKILL_OUTPUT_DIR", "output", create=False)
norm = os.path.normcase(os.path.normpath(path))
self.assertIn(os.path.normcase("output"), norm)
self.assertIn(os.path.normcase("videos"), norm)
self.assertTrue(norm.startswith(os.path.normcase(os.path.normpath(tmp))))
os.environ.pop("SKILL_OUTPUT_DIR", None)
config.reset_cache()
def test_resolve_input_path_relative_under_input(self) -> None:
with IsolatedDataRoot() as tmp:
import importlib
import util.runtime_paths as rp
importlib.reload(rp)
resolved = rp.resolve_input_path("urls.txt", create_parent=True)
norm = os.path.normcase(os.path.normpath(resolved))
self.assertIn(os.path.normcase("input"), norm)
self.assertTrue(norm.startswith(os.path.normcase(os.path.normpath(tmp))))
def test_get_input_and_output_dirs(self) -> None:
with IsolatedDataRoot() as tmp:
import importlib
import util.runtime_paths as rp
importlib.reload(rp)
input_dir = rp.get_input_dir(create=True)
output_dir = rp.get_output_dir(create=True)
html_dir = rp.get_html_dir(create=True)
norm_tmp = os.path.normcase(os.path.normpath(tmp))
self.assertTrue(os.path.normcase(os.path.normpath(input_dir)).startswith(norm_tmp))
self.assertTrue(os.path.normcase(os.path.normpath(output_dir)).startswith(norm_tmp))
self.assertTrue(os.path.normcase(os.path.normpath(html_dir)).startswith(norm_tmp))
self.assertTrue(input_dir.replace("\\", "/").endswith("/input"))
self.assertTrue(output_dir.replace("\\", "/").endswith("/output"))
self.assertTrue(html_dir.replace("\\", "/").endswith("/html"))
def test_list_resolved_data_paths_keys(self) -> None:
with IsolatedDataRoot():
import importlib
import util.runtime_paths as rp
importlib.reload(rp)
paths = rp.list_resolved_data_paths()
for key in (
"skill_data_dir",
"input_dir",
"output_dir",
"html_dir",
):
self.assertIn(key, paths)
self.assertTrue(paths[key])
self.assertNotIn("downloads_dir", paths)
self.assertNotIn("imports_dir", paths)
self.assertNotIn("exports_dir", paths)
if __name__ == "__main__":
unittest.main()