s-skillkit 0.3.3__tar.gz → 0.3.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. {s_skillkit-0.3.3 → s_skillkit-0.3.4}/PKG-INFO +2 -1
  2. {s_skillkit-0.3.3 → s_skillkit-0.3.4}/pyproject.toml +8 -1
  3. {s_skillkit-0.3.3 → s_skillkit-0.3.4}/skillkit/__init__.py +6 -0
  4. s_skillkit-0.3.4/skillkit/telemetry.py +118 -0
  5. s_skillkit-0.3.4/tests/test_telemetry_adapter.py +211 -0
  6. {s_skillkit-0.3.3 → s_skillkit-0.3.4}/uv.lock +12 -1
  7. {s_skillkit-0.3.3 → s_skillkit-0.3.4}/.gitignore +0 -0
  8. {s_skillkit-0.3.3 → s_skillkit-0.3.4}/.gitlab-ci.yml +0 -0
  9. {s_skillkit-0.3.3 → s_skillkit-0.3.4}/AGENTS.md +0 -0
  10. {s_skillkit-0.3.3 → s_skillkit-0.3.4}/CHANGELOG.md +0 -0
  11. {s_skillkit-0.3.3 → s_skillkit-0.3.4}/LICENSE +0 -0
  12. {s_skillkit-0.3.3 → s_skillkit-0.3.4}/README.md +0 -0
  13. {s_skillkit-0.3.3 → s_skillkit-0.3.4}/skillkit/binary_installer.py +0 -0
  14. {s_skillkit-0.3.3 → s_skillkit-0.3.4}/skillkit/bootstrap.py +0 -0
  15. {s_skillkit-0.3.3 → s_skillkit-0.3.4}/skillkit/collections.py +0 -0
  16. {s_skillkit-0.3.3 → s_skillkit-0.3.4}/skillkit/config.py +0 -0
  17. {s_skillkit-0.3.3 → s_skillkit-0.3.4}/skillkit/deps_installer.py +0 -0
  18. {s_skillkit-0.3.3 → s_skillkit-0.3.4}/skillkit/errors.py +0 -0
  19. {s_skillkit-0.3.3 → s_skillkit-0.3.4}/skillkit/filter.py +0 -0
  20. {s_skillkit-0.3.3 → s_skillkit-0.3.4}/skillkit/hook_register.py +0 -0
  21. {s_skillkit-0.3.3 → s_skillkit-0.3.4}/skillkit/installer/__init__.py +0 -0
  22. {s_skillkit-0.3.3 → s_skillkit-0.3.4}/skillkit/installer/differ.py +0 -0
  23. {s_skillkit-0.3.3 → s_skillkit-0.3.4}/skillkit/installer/fetchers.py +0 -0
  24. {s_skillkit-0.3.3 → s_skillkit-0.3.4}/skillkit/installer/fsops.py +0 -0
  25. {s_skillkit-0.3.3 → s_skillkit-0.3.4}/skillkit/installer/meta.py +0 -0
  26. {s_skillkit-0.3.3 → s_skillkit-0.3.4}/skillkit/installer/models.py +0 -0
  27. {s_skillkit-0.3.3 → s_skillkit-0.3.4}/skillkit/installer/ownership.py +0 -0
  28. {s_skillkit-0.3.3 → s_skillkit-0.3.4}/skillkit/linker.py +0 -0
  29. {s_skillkit-0.3.3 → s_skillkit-0.3.4}/skillkit/manifest.py +0 -0
  30. {s_skillkit-0.3.3 → s_skillkit-0.3.4}/skillkit/mcp_register.py +0 -0
  31. {s_skillkit-0.3.3 → s_skillkit-0.3.4}/skillkit/path_store.py +0 -0
  32. {s_skillkit-0.3.3 → s_skillkit-0.3.4}/skillkit/paths.py +0 -0
  33. {s_skillkit-0.3.3 → s_skillkit-0.3.4}/skillkit/pkg_manager.py +0 -0
  34. {s_skillkit-0.3.3 → s_skillkit-0.3.4}/skillkit/project.py +0 -0
  35. {s_skillkit-0.3.3 → s_skillkit-0.3.4}/skillkit/targets/__init__.py +0 -0
  36. {s_skillkit-0.3.3 → s_skillkit-0.3.4}/skillkit/targets/antigravity.py +0 -0
  37. {s_skillkit-0.3.3 → s_skillkit-0.3.4}/skillkit/targets/base.py +0 -0
  38. {s_skillkit-0.3.3 → s_skillkit-0.3.4}/skillkit/targets/claude_code.py +0 -0
  39. {s_skillkit-0.3.3 → s_skillkit-0.3.4}/skillkit/targets/codex.py +0 -0
  40. {s_skillkit-0.3.3 → s_skillkit-0.3.4}/skillkit/targets/detect.py +0 -0
  41. {s_skillkit-0.3.3 → s_skillkit-0.3.4}/skillkit/targets/opencode.py +0 -0
  42. {s_skillkit-0.3.3 → s_skillkit-0.3.4}/skillkit/tooling.py +0 -0
  43. {s_skillkit-0.3.3 → s_skillkit-0.3.4}/skillkit/uv_bootstrap.py +0 -0
  44. {s_skillkit-0.3.3 → s_skillkit-0.3.4}/tests/conftest.py +0 -0
  45. {s_skillkit-0.3.3 → s_skillkit-0.3.4}/tests/test_agent_skill_layouts_2026_07_21.py +0 -0
  46. {s_skillkit-0.3.3 → s_skillkit-0.3.4}/tests/test_binary_installer.py +0 -0
  47. {s_skillkit-0.3.3 → s_skillkit-0.3.4}/tests/test_bootstrap.py +0 -0
  48. {s_skillkit-0.3.3 → s_skillkit-0.3.4}/tests/test_collections.py +0 -0
  49. {s_skillkit-0.3.3 → s_skillkit-0.3.4}/tests/test_config.py +0 -0
  50. {s_skillkit-0.3.3 → s_skillkit-0.3.4}/tests/test_deps_binary.py +0 -0
  51. {s_skillkit-0.3.3 → s_skillkit-0.3.4}/tests/test_deps_installer.py +0 -0
  52. {s_skillkit-0.3.3 → s_skillkit-0.3.4}/tests/test_destructive_ownership_gate_2026_07_21.py +0 -0
  53. {s_skillkit-0.3.3 → s_skillkit-0.3.4}/tests/test_errors.py +0 -0
  54. {s_skillkit-0.3.3 → s_skillkit-0.3.4}/tests/test_filter.py +0 -0
  55. {s_skillkit-0.3.3 → s_skillkit-0.3.4}/tests/test_git_install.py +0 -0
  56. {s_skillkit-0.3.3 → s_skillkit-0.3.4}/tests/test_hooks.py +0 -0
  57. {s_skillkit-0.3.3 → s_skillkit-0.3.4}/tests/test_installer.py +0 -0
  58. {s_skillkit-0.3.3 → s_skillkit-0.3.4}/tests/test_legacy_paths_no_orphans_2026_07_21.py +0 -0
  59. {s_skillkit-0.3.3 → s_skillkit-0.3.4}/tests/test_manifest.py +0 -0
  60. {s_skillkit-0.3.3 → s_skillkit-0.3.4}/tests/test_mcp_register.py +0 -0
  61. {s_skillkit-0.3.3 → s_skillkit-0.3.4}/tests/test_no_raw_subprocess.py +0 -0
  62. {s_skillkit-0.3.3 → s_skillkit-0.3.4}/tests/test_packaging_lock.py +0 -0
  63. {s_skillkit-0.3.3 → s_skillkit-0.3.4}/tests/test_path_store.py +0 -0
  64. {s_skillkit-0.3.3 → s_skillkit-0.3.4}/tests/test_paths.py +0 -0
  65. {s_skillkit-0.3.3 → s_skillkit-0.3.4}/tests/test_pkg_manager.py +0 -0
  66. {s_skillkit-0.3.3 → s_skillkit-0.3.4}/tests/test_pkgsrc_not_in_agent_zone.py +0 -0
  67. {s_skillkit-0.3.3 → s_skillkit-0.3.4}/tests/test_project.py +0 -0
  68. {s_skillkit-0.3.3 → s_skillkit-0.3.4}/tests/test_remove_ownership_guard_2026_07_21.py +0 -0
  69. {s_skillkit-0.3.3 → s_skillkit-0.3.4}/tests/test_stub_guard.py +0 -0
  70. {s_skillkit-0.3.3 → s_skillkit-0.3.4}/tests/test_targets.py +0 -0
  71. {s_skillkit-0.3.3 → s_skillkit-0.3.4}/tests/test_tooling.py +0 -0
  72. {s_skillkit-0.3.3 → s_skillkit-0.3.4}/tests/test_transactional_install.py +0 -0
  73. {s_skillkit-0.3.3 → s_skillkit-0.3.4}/tests/test_traversal_guard.py +0 -0
  74. {s_skillkit-0.3.3 → s_skillkit-0.3.4}/tests/test_typed_fields.py +0 -0
  75. {s_skillkit-0.3.3 → s_skillkit-0.3.4}/tests/test_uv_bootstrap.py +0 -0
  76. {s_skillkit-0.3.3 → s_skillkit-0.3.4}/tests/test_w268_subdir_skill.py +0 -0
  77. {s_skillkit-0.3.3 → s_skillkit-0.3.4}/tests/test_w272_empty_manifest_guard.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: s-skillkit
3
- Version: 0.3.3
3
+ Version: 0.3.4
4
4
  Summary: Локальное ядро управления навыками: install из git/локальной папки, junction/symlink-линковка в scope агента, .skillignore-фильтр, build manifest, project-манифест. 0 завязок на сеть/auth — stdlib + tomli-w + pathspec + platformdirs.
5
5
  Author: Dmitry
6
6
  License: MIT
@@ -9,6 +9,7 @@ Requires-Python: >=3.11
9
9
  Requires-Dist: pathspec>=0.12.0
10
10
  Requires-Dist: platformdirs>=4.0
11
11
  Requires-Dist: s-librarykit>=0.7.1
12
+ Requires-Dist: s-telemetrykit>=0.2.0
12
13
  Requires-Dist: tomli-w>=1.0
13
14
  Provides-Extra: dev
14
15
  Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "s-skillkit"
3
- version = "0.3.3"
3
+ version = "0.3.4"
4
4
  description = "Локальное ядро управления навыками: install из git/локальной папки, junction/symlink-линковка в scope агента, .skillignore-фильтр, build manifest, project-манифест. 0 завязок на сеть/auth — stdlib + tomli-w + pathspec + platformdirs."
5
5
  readme = "README.md"
6
6
  license = { text = "MIT" }
@@ -18,11 +18,18 @@ authors = [{ name = "Dmitry" }]
18
18
  # ВИДИМОЕ окно поверх браузера; ``capture_output`` его не подавляет. Своей
19
19
  # второй реализации «как правильно на Windows» в ките быть не должно —
20
20
  # поэтому прежний статус «sibling, ни от чего не зависим» осознанно снят.
21
+ # s-telemetrykit — продуктово-нейтральный SDK учёта запусков. Кит нейтрален
22
+ # намеренно (он переиспользуем в любом продукте), поэтому skillery-специфику
23
+ # телеметрии — каталог ~/.skillery, префикс SKILLERY_*, канон _skill_meta.* и
24
+ # kind="skill_run" — держит адаптер `skillkit.telemetry`: канон меты навыка это
25
+ # ответственность ИМЕННО этого кита. Зависимость лёгкая: telemetrykit
26
+ # stdlib-only и своих зависимостей не имеет.
21
27
  dependencies = [
22
28
  "tomli-w>=1.0",
23
29
  "pathspec>=0.12.0",
24
30
  "platformdirs>=4.0",
25
31
  "s-librarykit>=0.7.1",
32
+ "s-telemetrykit>=0.2.0",
26
33
  ]
27
34
 
28
35
  [project.optional-dependencies]
@@ -27,6 +27,12 @@ platformdirs. Отвечает за ФС-механику навыка:
27
27
  бинари с GitHub Releases), ``bootstrap.bootstrap_skill`` (ensure_uv → deps →
28
28
  бинари → gate → self_check) + ``bootstrap.run_self_check``.
29
29
 
30
+ - ``telemetry`` — skillery-адаптер продуктово-нейтрального ``telemetrykit``:
31
+ ``~/.skillery`` + префикс ``SKILLERY_*`` + канон ``_skill_meta.*``/``SKILL.md``
32
+ + ``kind="skill_run"``. Импортируется ОТДЕЛЬНО (``from skillkit.telemetry
33
+ import track_skill``) и НЕ входит в eager-импорты пакета: применение
34
+ конфигурации — побочный эффект, он не должен случаться от ``import skillkit``.
35
+
30
36
  Хаб-специфика (track_skill_event, install-by-slug bundle, /me/installs) живёт в
31
37
  CLI, НЕ здесь.
32
38
  """
@@ -0,0 +1,118 @@
1
+ """Skillery-адаптер нейтрального ``telemetrykit`` — вся skillery-специфика здесь.
2
+
3
+ Зачем отдельный модуль. ``s-telemetrykit`` — кит ОБЩЕГО назначения: он должен
4
+ одинаково годиться любому продукту и поэтому не вправе знать ни про
5
+ ``~/.skillery``, ни про ``SKILLERY_*``, ни про ``_skill_meta.json``, ни про слово
6
+ «навык». А ``skillkit`` — кит ИМЕННО ЭТОГО продукта: канон стора навыков и его
7
+ меты — его прямая ответственность. Значит и место склейки — здесь.
8
+
9
+ Подключение в навыке — одна строка, skillery-имя сохранено::
10
+
11
+ from skillkit.telemetry import track_skill
12
+
13
+ @track_skill()
14
+ def main() -> int:
15
+ ...
16
+
17
+ Импорт модуля САМ применяет конфигурацию (``apply()`` внизу файла): навыку не
18
+ нужно помнить о втором вызове, а забытый вызов означал бы события в чужом
19
+ каталоге. Идемпотентно — повторный импорт ничего не ломает.
20
+
21
+ **Кто читает outbox — обязан импортировать этот же модуль.** Писатели (навыки)
22
+ и читатель (воркер доставки) должны сойтись в одном каталоге; без общей
23
+ конфигурации воркер будет смотреть в нейтральный ``~/.telemetrykit``, а события
24
+ лежать в ``~/.skillery``.
25
+ """
26
+ from __future__ import annotations
27
+
28
+ from pathlib import Path
29
+
30
+ from telemetrykit import TelemetryConfig, configure, outbox, paths, track_run
31
+
32
+ #: Каталог-дом семейства инструментов Skillery. Кит НЕ заводит своего: outbox
33
+ #: лежит рядом с конфигом CLI, потому что читать его будет именно CLI-воркер, а
34
+ #: писатели и читатель обязаны сходиться в одной точке без договорённости в
35
+ #: рантайме.
36
+ HOME_DIR_NAME = ".skillery"
37
+
38
+ #: Префикс env-переменных продукта: ``SKILLERY_HOME``, ``SKILLERY_DISABLED``, …
39
+ ENV_PREFIX = "SKILLERY"
40
+
41
+ #: Канон меты навыка. ``_skill_meta.json`` пишет установщик (см.
42
+ #: ``skillkit.installer.meta.write_meta``), ``_skill_meta.toml`` — манифест в
43
+ #: исходниках, ``SKILL.md`` — frontmatter, источник правды формы навыка у агентов.
44
+ META_FILES = ("_skill_meta.json", "_skill_meta.toml", "SKILL.md")
45
+
46
+ #: Ключи идентичности внутри меты. ``skill_id`` — числовой id slug-less навыка
47
+ #: (PK-миграция): если slug не задан, идентичностью каталога становится он.
48
+ META_ID_KEYS = ("slug", "skill_id", "name")
49
+ META_VERSION_KEYS = ("version",)
50
+
51
+ #: Тип конверта в outbox'е для запусков навыков.
52
+ KIND_SKILL_RUN = "skill_run"
53
+
54
+ #: Исторические имена env-переменных Skillery. Проверяются ПОСЛЕ префиксных
55
+ #: (``SKILLERY_DISABLED`` сильнее ``SKILLERY_TELEMETRY_DISABLED``), но ломать их
56
+ #: нельзя: они уже разошлись по машинам пользователей и по документации.
57
+ #: ``SKILLERY_CONFIG_DIR`` в списке домов намеренно — если пользователь увёл
58
+ #: конфиг CLI, туда же должен уехать outbox, иначе воркер не найдёт файл.
59
+ LEGACY_ENV_NAMES = {
60
+ paths.ENV_HOME: ("SKILLERY_CONFIG_DIR",),
61
+ paths.ENV_DISABLED: ("SKILLERY_TELEMETRY_DISABLED",),
62
+ paths.ENV_COMPONENT_ID: ("SKILLERY_SKILL_ID",),
63
+ paths.ENV_COMPONENT_VERSION: ("SKILLERY_SKILL_VERSION",),
64
+ paths.ENV_OUTBOX_DIR: ("SKILLERY_TELEMETRY_DIR",),
65
+ }
66
+
67
+
68
+ def telemetry_config() -> TelemetryConfig:
69
+ """Конфигурация ``telemetrykit`` под канон Skillery.
70
+
71
+ ``home`` задан ЛЕНИВЫМ провайдером, а не готовым путём: ``Path.home()`` на
72
+ момент импорта модуля и на момент записи события — не обязательно одно и то
73
+ же (тесты, sandbox, смена профиля), и вычислять его заранее значит зашить
74
+ первое попавшееся значение.
75
+ """
76
+ return TelemetryConfig(
77
+ home_provider=lambda: Path.home() / HOME_DIR_NAME,
78
+ env_prefix=ENV_PREFIX,
79
+ extra_env_names=LEGACY_ENV_NAMES,
80
+ meta_files=META_FILES,
81
+ meta_id_keys=META_ID_KEYS,
82
+ meta_version_keys=META_VERSION_KEYS,
83
+ default_kind=KIND_SKILL_RUN,
84
+ )
85
+
86
+
87
+ def apply() -> None:
88
+ """Применить skillery-конфигурацию к ``telemetrykit`` (идемпотентно)."""
89
+ configure(telemetry_config())
90
+
91
+
92
+ #: Skillery-имя канонического ``telemetrykit.track_run``.
93
+ #:
94
+ #: ПРИСВОЕНИЕ, а не функция-обёртка: каскад определения навыка идёт вверх по
95
+ #: кадрам стека и пропускает только кадры самого кита (``telemetrykit.*``).
96
+ #: Обёртка в ``skillkit`` добавила бы кадр, который каскад не пропускает, и
97
+ #: вызывающим оказался бы ЭТОТ файл — идентичность резолвилась бы в мету
98
+ #: skillkit вместо меты навыка.
99
+ track_skill = track_run
100
+
101
+ __all__ = [
102
+ "ENV_PREFIX",
103
+ "HOME_DIR_NAME",
104
+ "KIND_SKILL_RUN",
105
+ "LEGACY_ENV_NAMES",
106
+ "META_FILES",
107
+ "META_ID_KEYS",
108
+ "META_VERSION_KEYS",
109
+ "apply",
110
+ "outbox",
111
+ "telemetry_config",
112
+ "track_run",
113
+ "track_skill",
114
+ ]
115
+
116
+ # Конфигурация применяется на ИМПОРТЕ: навыку достаточно
117
+ # ``from skillkit.telemetry import track_skill``.
118
+ apply()
@@ -0,0 +1,211 @@
1
+ """Адаптер ``skillkit.telemetry``: skillery-специфика поверх нейтрального кита.
2
+
3
+ ``s-telemetrykit`` продуктово-нейтрален и сам по себе пишет в ``~/.telemetrykit``
4
+ с префиксом ``TELEMETRYKIT_`` и ``kind="run"``. Skillery-канон (``~/.skillery``,
5
+ ``SKILLERY_*``, ``_skill_meta.*``, ``kind="skill_run"``) задаётся ИМЕННО здесь —
6
+ и проверяется здесь же, иначе «настроили» и «работает как настроили» разъедутся.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ import os
12
+ import subprocess
13
+ import sys
14
+ from collections.abc import Iterator
15
+ from pathlib import Path
16
+
17
+ import pytest
18
+ import telemetrykit
19
+ from telemetrykit import outbox, paths
20
+
21
+ from skillkit import telemetry
22
+
23
+ REPO_ROOT = Path(__file__).resolve().parent.parent
24
+
25
+
26
+ @pytest.fixture(autouse=True)
27
+ def _skillery_config(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> Iterator[Path]:
28
+ """Skillery-конфигурация + изолированный дом; после теста — нейтральный сброс."""
29
+ for name in (
30
+ "SKILLERY_HOME",
31
+ "SKILLERY_CONFIG_DIR",
32
+ "SKILLERY_DISABLED",
33
+ "SKILLERY_TELEMETRY_DISABLED",
34
+ "SKILLERY_OUTBOX_PATH",
35
+ "SKILLERY_OUTBOX_DIR",
36
+ "SKILLERY_TELEMETRY_DIR",
37
+ "SKILLERY_OUTBOX_DISABLED",
38
+ "SKILLERY_SKILL_ID",
39
+ "SKILLERY_SKILL_VERSION",
40
+ "SKILLERY_COMPONENT_ID",
41
+ "SKILLERY_COMPONENT_VERSION",
42
+ ):
43
+ monkeypatch.delenv(name, raising=False)
44
+ telemetry.apply()
45
+ home = tmp_path / "skillery-home"
46
+ monkeypatch.setenv("SKILLERY_HOME", str(home))
47
+ telemetrykit.identity.reset_cache()
48
+ telemetrykit.detect.reset_cache()
49
+ yield home
50
+ telemetrykit.reset_to_defaults()
51
+
52
+
53
+ # --- адаптер реально переключает кит -----------------------------------------
54
+
55
+
56
+ def test_home_is_skillery_dir(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
57
+ """Дом — ``~/.skillery``, а не нейтральный ``~/.telemetrykit``."""
58
+ monkeypatch.delenv("SKILLERY_HOME", raising=False)
59
+ monkeypatch.setattr(Path, "home", classmethod(lambda cls: tmp_path))
60
+ assert paths.home_base() == tmp_path / ".skillery"
61
+ assert outbox.path() == tmp_path / ".skillery" / "outbox.jsonl"
62
+
63
+
64
+ def test_home_is_lazy_not_frozen_at_import(
65
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path
66
+ ) -> None:
67
+ """``Path.home()`` берётся в момент записи, а не в момент импорта модуля."""
68
+ monkeypatch.delenv("SKILLERY_HOME", raising=False)
69
+ monkeypatch.setattr(Path, "home", classmethod(lambda cls: tmp_path / "later"))
70
+ assert paths.home_base() == tmp_path / "later" / ".skillery"
71
+
72
+
73
+ def test_env_prefix_is_skillery(monkeypatch: pytest.MonkeyPatch) -> None:
74
+ assert paths.env_names(paths.ENV_DISABLED)[0] == "SKILLERY_DISABLED"
75
+ monkeypatch.setenv("SKILLERY_OUTBOX_DISABLED", "1")
76
+ assert outbox.is_disabled() is True
77
+
78
+
79
+ def test_legacy_env_names_still_work(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
80
+ """Исторические имена переменных ломать нельзя — они уже у пользователей."""
81
+ assert paths.env_names(paths.ENV_DISABLED) == (
82
+ "SKILLERY_DISABLED",
83
+ "SKILLERY_TELEMETRY_DISABLED",
84
+ )
85
+ monkeypatch.setenv("SKILLERY_TELEMETRY_DISABLED", "1")
86
+ assert telemetrykit.telemetry_disabled() is True
87
+
88
+ monkeypatch.delenv("SKILLERY_HOME", raising=False)
89
+ monkeypatch.setenv("SKILLERY_CONFIG_DIR", str(tmp_path / "cfg"))
90
+ assert paths.home_base() == tmp_path / "cfg"
91
+
92
+
93
+ def test_legacy_skill_id_env_override(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
94
+ monkeypatch.setenv("SKILLERY_SKILL_ID", "vk")
95
+ monkeypatch.setenv("SKILLERY_SKILL_VERSION", "1.2.3")
96
+ telemetrykit.detect.reset_cache()
97
+ found = telemetrykit.detect.resolve(caller_file=tmp_path / "x.py", module_name="x")
98
+ assert (found.component_id, found.component_version) == ("vk", "1.2.3")
99
+
100
+
101
+ def test_skill_meta_json_is_read(tmp_path: Path) -> None:
102
+ """Канон стора: ``_skill_meta.json`` со slug и version."""
103
+ (tmp_path / "_skill_meta.json").write_text(
104
+ json.dumps({"slug": "vk", "version": "3.1.4"}), encoding="utf-8"
105
+ )
106
+ found = telemetrykit.detect.resolve(caller_file=tmp_path / "cli.py", module_name="absent_pkg")
107
+ assert (found.component_id, found.component_version, found.source) == (
108
+ "vk",
109
+ "3.1.4",
110
+ "meta_file",
111
+ )
112
+
113
+
114
+ def test_slugless_skill_identified_by_numeric_skill_id(tmp_path: Path) -> None:
115
+ """PK-миграция: без slug идентичностью становится числовой ``skill_id``."""
116
+ (tmp_path / "_skill_meta.json").write_text(
117
+ json.dumps({"slug": None, "skill_id": 42, "version": "0.1.0"}), encoding="utf-8"
118
+ )
119
+ found = telemetrykit.detect.resolve(caller_file=tmp_path / "cli.py", module_name="absent_pkg")
120
+ assert found.component_id == "42"
121
+
122
+
123
+ def test_skill_md_frontmatter_is_read(tmp_path: Path) -> None:
124
+ (tmp_path / "SKILL.md").write_text(
125
+ "---\nname: max-cli\nversion: 0.4.2\n---\n\n# Навык\n", encoding="utf-8"
126
+ )
127
+ found = telemetrykit.detect.resolve(caller_file=tmp_path / "cli.py", module_name="absent_pkg")
128
+ assert (found.component_id, found.component_version) == ("max-cli", "0.4.2")
129
+
130
+
131
+ def test_default_kind_is_skill_run(_skillery_config: Path) -> None:
132
+ with telemetry.track_skill("vk", "1.0.0"):
133
+ pass
134
+ assert [e["kind"] for e in outbox.read_batch(10)] == ["skill_run"]
135
+
136
+
137
+ def test_track_skill_is_the_neutral_canon(_skillery_config: Path) -> None:
138
+ """Алиас — ПРИСВОЕНИЕ: лишний кадр обёртки сбил бы определение навыка."""
139
+ assert telemetry.track_skill is telemetrykit.track_run
140
+
141
+
142
+ # --- навык, использующий адаптер (сквозной прогон в отдельном процессе) ------
143
+
144
+
145
+ _SKILL_MODULE = """
146
+ from skillkit.telemetry import track_skill
147
+
148
+
149
+ @track_skill()
150
+ def main() -> int:
151
+ return 0
152
+
153
+
154
+ main()
155
+ """
156
+
157
+
158
+ def test_skill_using_adapter_writes_skillery_outbox(tmp_path: Path) -> None:
159
+ """Сквозь: навык с адаптером пишет в ~/.skillery/outbox.jsonl, kind=skill_run.
160
+
161
+ Отдельный процесс, а не in-process вызов: только так проверяется, что
162
+ конфигурация применяется САМИМ импортом адаптера, без чужой подготовки.
163
+ """
164
+ skill_dir = tmp_path / "skills" / "vk"
165
+ (skill_dir / "vk_skill").mkdir(parents=True)
166
+ (skill_dir / "_skill_meta.json").write_text(
167
+ json.dumps({"slug": "vk", "version": "3.1.4"}), encoding="utf-8"
168
+ )
169
+ (skill_dir / "vk_skill" / "__init__.py").write_text("", encoding="utf-8")
170
+ (skill_dir / "vk_skill" / "cli.py").write_text(_SKILL_MODULE, encoding="utf-8")
171
+ fake_home = tmp_path / "fake-home"
172
+ fake_home.mkdir()
173
+
174
+ env = dict(os.environ)
175
+ env["PYTHONPATH"] = str(REPO_ROOT) + os.pathsep + str(skill_dir)
176
+ # Дом НЕ задаём переменной: проверяем именно дефолт адаптера (~/.skillery).
177
+ for junk in (
178
+ "SKILLERY_HOME",
179
+ "SKILLERY_CONFIG_DIR",
180
+ "SKILLERY_OUTBOX_PATH",
181
+ "SKILLERY_OUTBOX_DIR",
182
+ "SKILLERY_TELEMETRY_DIR",
183
+ "SKILLERY_TELEMETRY_DISABLED",
184
+ "SKILLERY_DISABLED",
185
+ "SKILLERY_SKILL_ID",
186
+ "SKILLERY_SKILL_VERSION",
187
+ ):
188
+ env.pop(junk, None)
189
+ env["USERPROFILE"] = str(fake_home) # Windows
190
+ env["HOME"] = str(fake_home) # POSIX
191
+
192
+ res = subprocess.run(
193
+ [sys.executable, "-c", "import vk_skill.cli"],
194
+ cwd=str(tmp_path),
195
+ env=env,
196
+ capture_output=True,
197
+ text=True,
198
+ timeout=180,
199
+ )
200
+ assert res.returncode == 0, res.stderr
201
+ assert res.stdout == "" and res.stderr == "", "кит обязан молчать в консоль"
202
+
203
+ outbox_file = fake_home / ".skillery" / "outbox.jsonl"
204
+ assert outbox_file.is_file(), "события должны лечь в ~/.skillery, а не ~/.telemetrykit"
205
+ assert not (fake_home / ".telemetrykit").exists()
206
+
207
+ envelopes = [json.loads(line) for line in outbox_file.read_text(encoding="utf-8").splitlines()]
208
+ assert [e["kind"] for e in envelopes] == ["skill_run"]
209
+ payload = envelopes[0]["payload"]
210
+ assert (payload["component_id"], payload["component_version"]) == ("vk", "3.1.4")
211
+ assert payload["status"] == "OK"
@@ -481,12 +481,13 @@ wheels = [
481
481
 
482
482
  [[package]]
483
483
  name = "s-skillkit"
484
- version = "0.3.3"
484
+ version = "0.3.4"
485
485
  source = { editable = "." }
486
486
  dependencies = [
487
487
  { name = "pathspec" },
488
488
  { name = "platformdirs" },
489
489
  { name = "s-librarykit" },
490
+ { name = "s-telemetrykit" },
490
491
  { name = "tomli-w" },
491
492
  ]
492
493
 
@@ -503,10 +504,20 @@ requires-dist = [
503
504
  { name = "pytest", marker = "extra == 'dev'", specifier = ">=8" },
504
505
  { name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=0.24" },
505
506
  { name = "s-librarykit", specifier = ">=0.7.1" },
507
+ { name = "s-telemetrykit", specifier = ">=0.2.0" },
506
508
  { name = "tomli-w", specifier = ">=1.0" },
507
509
  ]
508
510
  provides-extras = ["dev"]
509
511
 
512
+ [[package]]
513
+ name = "s-telemetrykit"
514
+ version = "0.2.0"
515
+ source = { registry = "https://pypi.org/simple" }
516
+ sdist = { url = "https://files.pythonhosted.org/packages/ef/bb/b94167ec1bb53ecddf06638085d29504b0894d7c493c793d5d1cd3676484/s_telemetrykit-0.2.0.tar.gz", hash = "sha256:1eecc0cca068d1e07b38fe0f265bb29f09c0887cb2991a88bbb06ec215092062", size = 55381, upload-time = "2026-07-28T13:24:03.369Z" }
517
+ wheels = [
518
+ { url = "https://files.pythonhosted.org/packages/28/86/7d3bb8f0ca17d9470d6ba8c0aa814478b32ea43bc2ea50dee04f8fb72259/s_telemetrykit-0.2.0-py3-none-any.whl", hash = "sha256:447a8446837d4454d7ee253e32a43638b9d64a078405cab3cab30fbe40575f53", size = 45525, upload-time = "2026-07-28T13:24:02.405Z" },
519
+ ]
520
+
510
521
  [[package]]
511
522
  name = "secretstorage"
512
523
  version = "3.5.0"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes