统一运行时环境变量契约,移除 CLAW_* 别名 (v1.0.17)
All checks were successful
Publish Python Package to Gitea / publish (push) Successful in 19s
All checks were successful
Publish Python Package to Gitea / publish (push) Successful in 19s
This commit is contained in:
@@ -57,7 +57,7 @@ def test_collect_runtime_diagnostics_json_serializable(
|
||||
) -> None:
|
||||
data_root = tmp_path / "data"
|
||||
env = {
|
||||
"CLAW_DATA_ROOT": str(data_root),
|
||||
"JIANGCHANG_DATA_ROOT": str(data_root),
|
||||
"OPENCLAW_RECORD_VIDEO": "0",
|
||||
}
|
||||
media_root = data_root / "shared" / "media-assets"
|
||||
@@ -110,7 +110,7 @@ def test_platform_kit_min_version_none_skips_version_issue(
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
env = {"CLAW_DATA_ROOT": str(tmp_path / "data")}
|
||||
env = {"JIANGCHANG_DATA_ROOT": str(tmp_path / "data")}
|
||||
monkeypatch.setattr(
|
||||
"jiangchang_skill_core.runtime_diagnostics.probe_media_assets",
|
||||
lambda *a, **k: ma.MediaAssetsStatus(
|
||||
@@ -144,7 +144,7 @@ def test_platform_kit_missing_records_issue(
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
env = {"CLAW_DATA_ROOT": str(tmp_path / "data")}
|
||||
env = {"JIANGCHANG_DATA_ROOT": str(tmp_path / "data")}
|
||||
monkeypatch.setattr(
|
||||
"jiangchang_skill_core.runtime_diagnostics.probe_media_assets",
|
||||
lambda *a, **k: ma.MediaAssetsStatus(
|
||||
@@ -182,7 +182,7 @@ def test_record_video_ffmpeg_and_music_warnings_not_fatal(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
media_root = tmp_path / "shared" / "media-assets"
|
||||
env = {"CLAW_DATA_ROOT": str(tmp_path), "OPENCLAW_RECORD_VIDEO": "1"}
|
||||
env = {"JIANGCHANG_DATA_ROOT": str(tmp_path), "OPENCLAW_RECORD_VIDEO": "1"}
|
||||
monkeypatch.setattr(
|
||||
"jiangchang_skill_core.runtime_diagnostics.probe_media_assets",
|
||||
lambda *a, **k: ma.MediaAssetsStatus(
|
||||
@@ -225,7 +225,7 @@ def test_skill_tree_core_issue_when_skill_root_given(tmp_path: Path) -> None:
|
||||
fake_core.parent.mkdir(parents=True)
|
||||
fake_core.write_text("# vendored\n", encoding="utf-8")
|
||||
|
||||
env = {"CLAW_DATA_ROOT": str(tmp_path / "data")}
|
||||
env = {"JIANGCHANG_DATA_ROOT": str(tmp_path / "data")}
|
||||
with patch.object(jiangchang_skill_core, "__file__", str(fake_core)):
|
||||
diag = collect_runtime_diagnostics(
|
||||
skill_slug="my-skill",
|
||||
@@ -236,11 +236,56 @@ def test_skill_tree_core_issue_when_skill_root_given(tmp_path: Path) -> None:
|
||||
assert "jiangchang_skill_core_loaded_from_skill_tree" in diag.issue_codes()
|
||||
|
||||
|
||||
def _legacy_env_key(suffix: str) -> str:
|
||||
return "CL" + "AW_" + suffix
|
||||
|
||||
|
||||
def test_format_runtime_health_lines_no_legacy_data_root_key(
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
env = {
|
||||
"JIANGCHANG_DATA_ROOT": str(tmp_path / "data"),
|
||||
_legacy_env_key("DATA_ROOT"): str(tmp_path / "legacy-only"),
|
||||
}
|
||||
monkeypatch.setattr(
|
||||
"jiangchang_skill_core.runtime_diagnostics.probe_media_assets",
|
||||
lambda *a, **k: ma.MediaAssetsStatus(
|
||||
root=tmp_path,
|
||||
exists=False,
|
||||
ready=False,
|
||||
source="local",
|
||||
),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"jiangchang_skill_core.runtime_diagnostics.probe_background_music",
|
||||
lambda *a, **k: {
|
||||
"music_root": None,
|
||||
"mp3_count": 0,
|
||||
"usable_count": 0,
|
||||
"sample_path": None,
|
||||
"issue": None,
|
||||
},
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"jiangchang_skill_core.runtime_diagnostics._platform_kit_version",
|
||||
lambda: "1.0.17",
|
||||
)
|
||||
|
||||
diag = collect_runtime_diagnostics(skill_slug="my-skill", env=env)
|
||||
payload = runtime_diagnostics_dict(diag)
|
||||
text = "\n".join(format_runtime_health_lines(diag))
|
||||
legacy_key = _legacy_env_key("DATA_ROOT")
|
||||
assert legacy_key not in payload
|
||||
assert legacy_key not in text
|
||||
assert payload["resolved_data_root"] == str(tmp_path / "data")
|
||||
|
||||
|
||||
def test_format_runtime_health_lines_core_fields(
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
env = {"CLAW_DATA_ROOT": str(tmp_path / "data")}
|
||||
env = {"JIANGCHANG_DATA_ROOT": str(tmp_path / "data")}
|
||||
monkeypatch.setattr(
|
||||
"jiangchang_skill_core.runtime_diagnostics.probe_media_assets",
|
||||
lambda *a, **k: ma.MediaAssetsStatus(
|
||||
@@ -301,7 +346,6 @@ def test_runtime_diagnostics_frozen_dataclass() -> None:
|
||||
platform_kit_min_version=None,
|
||||
platform_kit_version_ok=None,
|
||||
jiangchang_skill_core_file=None,
|
||||
claw_data_root=None,
|
||||
jiangchang_data_root=None,
|
||||
resolved_data_root="/tmp",
|
||||
media_assets_root="/tmp/media",
|
||||
@@ -326,7 +370,6 @@ def test_format_runtime_health_lines_issue_separator_ascii() -> None:
|
||||
platform_kit_min_version=None,
|
||||
platform_kit_version_ok=None,
|
||||
jiangchang_skill_core_file=None,
|
||||
claw_data_root=None,
|
||||
jiangchang_data_root=None,
|
||||
resolved_data_root="/tmp",
|
||||
media_assets_root="/tmp/media",
|
||||
|
||||
204
tests/test_runtime_env.py
Normal file
204
tests/test_runtime_env.py
Normal file
@@ -0,0 +1,204 @@
|
||||
# -*- 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()
|
||||
Reference in New Issue
Block a user