# -*- coding: utf-8 -*- """runtime_env:JIANGCHANG_* 路径契约与兄弟技能根解析。""" from __future__ import annotations import os import sys import tempfile import unittest from pathlib import Path from unittest.mock import patch from jiangchang_skill_core import config, unified_logging as ul from jiangchang_skill_core.runtime_diagnostics import ( collect_runtime_diagnostics, format_runtime_health_lines, runtime_diagnostics_dict, ) from jiangchang_skill_core.runtime_env import ( apply_cli_local_defaults, get_data_root, get_sibling_skills_root, get_skills_root, get_user_id, platform_default_data_root, ) def _legacy_env_key(suffix: str) -> str: return "CL" + "AW_" + suffix class _EnvIsolation(unittest.TestCase): _KEYS = ( "JIANGCHANG_DATA_ROOT", "JIANGCHANG_USER_ID", "JIANGCHANG_SKILLS_ROOT", "JIANGCHANG_APP_ROOT", _legacy_env_key("DATA_ROOT"), _legacy_env_key("USER_ID"), _legacy_env_key("SKILLS_ROOT"), "JIANGCHANG_CLI_LOCAL_DEV", ) def setUp(self) -> None: self._saved = {k: os.environ.get(k) for k in self._KEYS} for k in self._KEYS: os.environ.pop(k, None) config.reset_cache() def tearDown(self) -> None: for k, v in self._saved.items(): if v is None: os.environ.pop(k, None) else: os.environ[k] = v config.reset_cache() class TestGetDataRoot(_EnvIsolation): def test_jiangchang_data_root_only(self) -> None: with tempfile.TemporaryDirectory() as tmp: os.environ["JIANGCHANG_DATA_ROOT"] = tmp self.assertEqual(get_data_root(), tmp) def test_platform_default_when_unset(self) -> None: self.assertEqual(get_data_root(), platform_default_data_root()) def test_legacy_claw_data_root_ignored(self) -> None: with tempfile.TemporaryDirectory() as claw_tmp, tempfile.TemporaryDirectory() as jc_tmp: os.environ[_legacy_env_key("DATA_ROOT")] = claw_tmp os.environ["JIANGCHANG_DATA_ROOT"] = jc_tmp self.assertEqual(get_data_root(), jc_tmp) def test_legacy_claw_data_root_alone_ignored(self) -> None: with tempfile.TemporaryDirectory() as claw_tmp: os.environ[_legacy_env_key("DATA_ROOT")] = claw_tmp self.assertEqual(get_data_root(), platform_default_data_root()) class TestGetUserId(_EnvIsolation): def test_jiangchang_user_id_only(self) -> None: os.environ["JIANGCHANG_USER_ID"] = "user-42" self.assertEqual(get_user_id(), "user-42") def test_anon_when_unset(self) -> None: self.assertEqual(get_user_id(), "_anon") def test_legacy_claw_user_id_ignored(self) -> None: os.environ[_legacy_env_key("USER_ID")] = "claw-only" os.environ["JIANGCHANG_USER_ID"] = "jc-user" self.assertEqual(get_user_id(), "jc-user") def test_legacy_claw_user_id_alone_ignored(self) -> None: os.environ[_legacy_env_key("USER_ID")] = "claw-only" self.assertEqual(get_user_id(), "_anon") class TestApplyCliLocalDefaults(_EnvIsolation): def test_sets_jiangchang_only(self) -> None: with patch("jiangchang_skill_core.runtime_env.CLI_LOCAL_DEV_ENABLED", True): apply_cli_local_defaults() self.assertTrue(os.environ.get("JIANGCHANG_DATA_ROOT")) self.assertTrue(os.environ.get("JIANGCHANG_USER_ID")) self.assertNotIn(_legacy_env_key("DATA_ROOT"), os.environ) self.assertNotIn(_legacy_env_key("USER_ID"), os.environ) def test_does_not_override_existing(self) -> None: with tempfile.TemporaryDirectory() as tmp: os.environ["JIANGCHANG_DATA_ROOT"] = tmp os.environ["JIANGCHANG_USER_ID"] = "existing" with patch("jiangchang_skill_core.runtime_env.CLI_LOCAL_DEV_ENABLED", True): apply_cli_local_defaults() self.assertEqual(os.environ["JIANGCHANG_DATA_ROOT"], tmp) self.assertEqual(os.environ["JIANGCHANG_USER_ID"], "existing") class TestGetSkillsRoot(_EnvIsolation): def test_jiangchang_skills_root_override(self) -> None: with tempfile.TemporaryDirectory() as tmp: os.environ["JIANGCHANG_SKILLS_ROOT"] = tmp self.assertEqual(get_skills_root(), os.path.normpath(tmp)) def test_legacy_claw_skills_root_ignored(self) -> None: with tempfile.TemporaryDirectory() as claw_tmp, tempfile.TemporaryDirectory() as jc_tmp: os.environ[_legacy_env_key("SKILLS_ROOT")] = claw_tmp os.environ["JIANGCHANG_SKILLS_ROOT"] = jc_tmp self.assertEqual(get_skills_root(), os.path.normpath(jc_tmp)) def test_legacy_claw_skills_root_alone_ignored(self) -> None: with tempfile.TemporaryDirectory() as claw_tmp: os.environ[_legacy_env_key("SKILLS_ROOT")] = claw_tmp root = get_skills_root() self.assertNotEqual(root, os.path.normpath(claw_tmp)) class TestGetSiblingSkillsRoot(_EnvIsolation): def _make_skills_tree(self, base: Path) -> Path: skills_root = base / "skills" (skills_root / "account-manager").mkdir(parents=True) (skills_root / "receive-order" / "scripts").mkdir(parents=True) return skills_root def test_inference_preferred_over_env(self) -> None: with tempfile.TemporaryDirectory() as tree_tmp, tempfile.TemporaryDirectory() as env_tmp: skills_root = self._make_skills_tree(Path(tree_tmp)) scripts_dir = str(skills_root / "receive-order" / "scripts") os.environ["JIANGCHANG_SKILLS_ROOT"] = env_tmp result = get_sibling_skills_root(scripts_dir) self.assertEqual(result, os.path.normpath(str(skills_root))) def test_env_override_when_inference_fails(self) -> None: with tempfile.TemporaryDirectory() as env_tmp: os.environ["JIANGCHANG_SKILLS_ROOT"] = env_tmp result = get_sibling_skills_root("/nonexistent/skill/scripts") self.assertEqual(result, os.path.normpath(env_tmp)) def test_legacy_claw_skills_root_ignored_for_sibling(self) -> None: with tempfile.TemporaryDirectory() as claw_tmp, tempfile.TemporaryDirectory() as jc_tmp: os.environ[_legacy_env_key("SKILLS_ROOT")] = claw_tmp os.environ["JIANGCHANG_SKILLS_ROOT"] = jc_tmp result = get_sibling_skills_root("/nonexistent/skill/scripts") self.assertEqual(result, os.path.normpath(jc_tmp)) class TestConfigAndLoggingPaths(_EnvIsolation): def test_config_env_file_under_user_isolated_dir(self) -> None: with tempfile.TemporaryDirectory() as tmp: os.environ["JIANGCHANG_DATA_ROOT"] = tmp os.environ["JIANGCHANG_USER_ID"] = "u-test" example = os.path.join(tmp, ".env.example") with open(example, "w", encoding="utf-8") as f: f.write("FOO=bar\n") dest = config.ensure_env_file("my-skill", example) expected_prefix = os.path.join(tmp, "u-test", "my-skill") self.assertTrue(os.path.normpath(dest).startswith(os.path.normpath(expected_prefix))) def test_unified_logging_path_under_user_dir(self) -> None: with tempfile.TemporaryDirectory() as tmp: os.environ["JIANGCHANG_DATA_ROOT"] = tmp os.environ["JIANGCHANG_USER_ID"] = "log-user" log_dir = ul.get_unified_logs_dir() self.assertTrue( os.path.normpath(log_dir).startswith( os.path.normpath(os.path.join(tmp, "log-user", "logs")) ) ) class TestDiagnosticsNoClawFields(_EnvIsolation): def test_diagnostics_json_and_text_exclude_claw(self) -> None: with tempfile.TemporaryDirectory() as tmp: os.environ["JIANGCHANG_DATA_ROOT"] = tmp diag = collect_runtime_diagnostics(skill_slug="x", env=dict(os.environ)) payload = runtime_diagnostics_dict(diag) text = "\n".join(format_runtime_health_lines(diag)) legacy_data_root_key = _legacy_env_key("DATA_ROOT") self.assertNotIn(legacy_data_root_key, payload) self.assertNotIn(legacy_data_root_key, text) self.assertIn("JIANGCHANG_DATA_ROOT", payload) self.assertEqual(payload["resolved_data_root"], tmp) if __name__ == "__main__": unittest.main()