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.
- {s_skillkit-0.3.3 → s_skillkit-0.3.4}/PKG-INFO +2 -1
- {s_skillkit-0.3.3 → s_skillkit-0.3.4}/pyproject.toml +8 -1
- {s_skillkit-0.3.3 → s_skillkit-0.3.4}/skillkit/__init__.py +6 -0
- s_skillkit-0.3.4/skillkit/telemetry.py +118 -0
- s_skillkit-0.3.4/tests/test_telemetry_adapter.py +211 -0
- {s_skillkit-0.3.3 → s_skillkit-0.3.4}/uv.lock +12 -1
- {s_skillkit-0.3.3 → s_skillkit-0.3.4}/.gitignore +0 -0
- {s_skillkit-0.3.3 → s_skillkit-0.3.4}/.gitlab-ci.yml +0 -0
- {s_skillkit-0.3.3 → s_skillkit-0.3.4}/AGENTS.md +0 -0
- {s_skillkit-0.3.3 → s_skillkit-0.3.4}/CHANGELOG.md +0 -0
- {s_skillkit-0.3.3 → s_skillkit-0.3.4}/LICENSE +0 -0
- {s_skillkit-0.3.3 → s_skillkit-0.3.4}/README.md +0 -0
- {s_skillkit-0.3.3 → s_skillkit-0.3.4}/skillkit/binary_installer.py +0 -0
- {s_skillkit-0.3.3 → s_skillkit-0.3.4}/skillkit/bootstrap.py +0 -0
- {s_skillkit-0.3.3 → s_skillkit-0.3.4}/skillkit/collections.py +0 -0
- {s_skillkit-0.3.3 → s_skillkit-0.3.4}/skillkit/config.py +0 -0
- {s_skillkit-0.3.3 → s_skillkit-0.3.4}/skillkit/deps_installer.py +0 -0
- {s_skillkit-0.3.3 → s_skillkit-0.3.4}/skillkit/errors.py +0 -0
- {s_skillkit-0.3.3 → s_skillkit-0.3.4}/skillkit/filter.py +0 -0
- {s_skillkit-0.3.3 → s_skillkit-0.3.4}/skillkit/hook_register.py +0 -0
- {s_skillkit-0.3.3 → s_skillkit-0.3.4}/skillkit/installer/__init__.py +0 -0
- {s_skillkit-0.3.3 → s_skillkit-0.3.4}/skillkit/installer/differ.py +0 -0
- {s_skillkit-0.3.3 → s_skillkit-0.3.4}/skillkit/installer/fetchers.py +0 -0
- {s_skillkit-0.3.3 → s_skillkit-0.3.4}/skillkit/installer/fsops.py +0 -0
- {s_skillkit-0.3.3 → s_skillkit-0.3.4}/skillkit/installer/meta.py +0 -0
- {s_skillkit-0.3.3 → s_skillkit-0.3.4}/skillkit/installer/models.py +0 -0
- {s_skillkit-0.3.3 → s_skillkit-0.3.4}/skillkit/installer/ownership.py +0 -0
- {s_skillkit-0.3.3 → s_skillkit-0.3.4}/skillkit/linker.py +0 -0
- {s_skillkit-0.3.3 → s_skillkit-0.3.4}/skillkit/manifest.py +0 -0
- {s_skillkit-0.3.3 → s_skillkit-0.3.4}/skillkit/mcp_register.py +0 -0
- {s_skillkit-0.3.3 → s_skillkit-0.3.4}/skillkit/path_store.py +0 -0
- {s_skillkit-0.3.3 → s_skillkit-0.3.4}/skillkit/paths.py +0 -0
- {s_skillkit-0.3.3 → s_skillkit-0.3.4}/skillkit/pkg_manager.py +0 -0
- {s_skillkit-0.3.3 → s_skillkit-0.3.4}/skillkit/project.py +0 -0
- {s_skillkit-0.3.3 → s_skillkit-0.3.4}/skillkit/targets/__init__.py +0 -0
- {s_skillkit-0.3.3 → s_skillkit-0.3.4}/skillkit/targets/antigravity.py +0 -0
- {s_skillkit-0.3.3 → s_skillkit-0.3.4}/skillkit/targets/base.py +0 -0
- {s_skillkit-0.3.3 → s_skillkit-0.3.4}/skillkit/targets/claude_code.py +0 -0
- {s_skillkit-0.3.3 → s_skillkit-0.3.4}/skillkit/targets/codex.py +0 -0
- {s_skillkit-0.3.3 → s_skillkit-0.3.4}/skillkit/targets/detect.py +0 -0
- {s_skillkit-0.3.3 → s_skillkit-0.3.4}/skillkit/targets/opencode.py +0 -0
- {s_skillkit-0.3.3 → s_skillkit-0.3.4}/skillkit/tooling.py +0 -0
- {s_skillkit-0.3.3 → s_skillkit-0.3.4}/skillkit/uv_bootstrap.py +0 -0
- {s_skillkit-0.3.3 → s_skillkit-0.3.4}/tests/conftest.py +0 -0
- {s_skillkit-0.3.3 → s_skillkit-0.3.4}/tests/test_agent_skill_layouts_2026_07_21.py +0 -0
- {s_skillkit-0.3.3 → s_skillkit-0.3.4}/tests/test_binary_installer.py +0 -0
- {s_skillkit-0.3.3 → s_skillkit-0.3.4}/tests/test_bootstrap.py +0 -0
- {s_skillkit-0.3.3 → s_skillkit-0.3.4}/tests/test_collections.py +0 -0
- {s_skillkit-0.3.3 → s_skillkit-0.3.4}/tests/test_config.py +0 -0
- {s_skillkit-0.3.3 → s_skillkit-0.3.4}/tests/test_deps_binary.py +0 -0
- {s_skillkit-0.3.3 → s_skillkit-0.3.4}/tests/test_deps_installer.py +0 -0
- {s_skillkit-0.3.3 → s_skillkit-0.3.4}/tests/test_destructive_ownership_gate_2026_07_21.py +0 -0
- {s_skillkit-0.3.3 → s_skillkit-0.3.4}/tests/test_errors.py +0 -0
- {s_skillkit-0.3.3 → s_skillkit-0.3.4}/tests/test_filter.py +0 -0
- {s_skillkit-0.3.3 → s_skillkit-0.3.4}/tests/test_git_install.py +0 -0
- {s_skillkit-0.3.3 → s_skillkit-0.3.4}/tests/test_hooks.py +0 -0
- {s_skillkit-0.3.3 → s_skillkit-0.3.4}/tests/test_installer.py +0 -0
- {s_skillkit-0.3.3 → s_skillkit-0.3.4}/tests/test_legacy_paths_no_orphans_2026_07_21.py +0 -0
- {s_skillkit-0.3.3 → s_skillkit-0.3.4}/tests/test_manifest.py +0 -0
- {s_skillkit-0.3.3 → s_skillkit-0.3.4}/tests/test_mcp_register.py +0 -0
- {s_skillkit-0.3.3 → s_skillkit-0.3.4}/tests/test_no_raw_subprocess.py +0 -0
- {s_skillkit-0.3.3 → s_skillkit-0.3.4}/tests/test_packaging_lock.py +0 -0
- {s_skillkit-0.3.3 → s_skillkit-0.3.4}/tests/test_path_store.py +0 -0
- {s_skillkit-0.3.3 → s_skillkit-0.3.4}/tests/test_paths.py +0 -0
- {s_skillkit-0.3.3 → s_skillkit-0.3.4}/tests/test_pkg_manager.py +0 -0
- {s_skillkit-0.3.3 → s_skillkit-0.3.4}/tests/test_pkgsrc_not_in_agent_zone.py +0 -0
- {s_skillkit-0.3.3 → s_skillkit-0.3.4}/tests/test_project.py +0 -0
- {s_skillkit-0.3.3 → s_skillkit-0.3.4}/tests/test_remove_ownership_guard_2026_07_21.py +0 -0
- {s_skillkit-0.3.3 → s_skillkit-0.3.4}/tests/test_stub_guard.py +0 -0
- {s_skillkit-0.3.3 → s_skillkit-0.3.4}/tests/test_targets.py +0 -0
- {s_skillkit-0.3.3 → s_skillkit-0.3.4}/tests/test_tooling.py +0 -0
- {s_skillkit-0.3.3 → s_skillkit-0.3.4}/tests/test_transactional_install.py +0 -0
- {s_skillkit-0.3.3 → s_skillkit-0.3.4}/tests/test_traversal_guard.py +0 -0
- {s_skillkit-0.3.3 → s_skillkit-0.3.4}/tests/test_typed_fields.py +0 -0
- {s_skillkit-0.3.3 → s_skillkit-0.3.4}/tests/test_uv_bootstrap.py +0 -0
- {s_skillkit-0.3.3 → s_skillkit-0.3.4}/tests/test_w268_subdir_skill.py +0 -0
- {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
|
+
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
|
+
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.
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|