Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
205e995f26 | ||
|
|
b492f8f4eb | ||
|
|
a074568cd9 | ||
|
|
e0048bb5b2 | ||
|
|
3b9f422acb | ||
|
|
53761f0a87 | ||
|
|
5f23fdda3c | ||
|
|
8008fb914e | ||
|
|
fa90b2e817 | ||
|
|
0a8e7bb499 |
No files matched your search
@@ -31,3 +31,6 @@ __pycache__/
|
||||
!/.claude/agents/
|
||||
|
||||
.skill-lock.json
|
||||
|
||||
# Кэш скиллов, синхронизируемых Claude Code с claude.ai (~/.claude/skills -> agents)
|
||||
agents/.agents/skills/synced/
|
||||
@@ -38,6 +38,7 @@ to-questionnaire/
|
||||
wait-what/
|
||||
wizard/
|
||||
writing-for-agents/
|
||||
retro/
|
||||
|
||||
# Skills, подключаемые в agents/ как внешние плагины (не версионируются здесь)
|
||||
claude-team-review
|
||||
|
||||
@@ -464,3 +464,30 @@ codex-cli 0.146.0 и была неверна. Развели две провер
|
||||
строк, вердикт ревью постановки) дали больше всего. Разведку просить
|
||||
индексом до 80 строк с путями; вердикты читать только строками
|
||||
находок через grep.
|
||||
|
||||
## 2026-09-11: накладные расходы конвейера против «сделать самому» (t2-ml-forecast, #36)
|
||||
|
||||
Тикет #36: условие с инерцией, семь файлов, все развилки закрыты
|
||||
ответами бизнеса и оркестратором до старта. Конвейер развёрнут по
|
||||
плейбуку: файл-задача, craftsman исполнителем, Astra ревьюером.
|
||||
|
||||
- **Замер контекста оркестратора (тысячи токенов, оценка).** Конвейер:
|
||||
чтение плейбука и справочников 22, сбор фактуры 8, файл-задача и
|
||||
промпты 5, приёмка ~10, итого ~45. Своими руками: чтение кода 15,
|
||||
правки и тесты 10, приёмка 5, итого ~30. Разница целиком в
|
||||
фиксированной плате за плейбук: вторая задача в той же сессии стоит
|
||||
конвейером ~20.
|
||||
- **Слова владельца.** У Astra как оркестратора накладные расходы на
|
||||
постановку, оркестрацию и приёмку во многих случаях превышают
|
||||
экономию от делегирования; её оркестрация тяжелее, чем у Fable, хотя и
|
||||
мощнее. Экономика субагентов полезна для ревью, где нужен чистый
|
||||
контекст, и редко для разработки.
|
||||
- **Следствие для вводных скилла (переписать позже):** одиночная задача
|
||||
с закрытыми развилками до десятка файлов делается оркестратором
|
||||
самостоятельно, конвейер сводится к слепому ревью. Исполнитель-субагент
|
||||
оправдан при нескольких задачах в сессии или при открытых решениях,
|
||||
когда контекст оркестратора нужен для них, а не для кода.
|
||||
- **Лишняя плата этого прогона:** часть фактуры ушла на ловушки Codex
|
||||
(snap `uv`, зависание `TestClient` в песочнице), хотя исполнителем в
|
||||
итоге выбран Claude. Исполнителя выбирать до чтения справочника по
|
||||
механике вызова.
|
||||
@@ -1,96 +1,37 @@
|
||||
---
|
||||
name: conventional-commits
|
||||
description: "ОБЯЗАТЕЛЬНО вызывай этот навык каждый раз, когда пользователь просит сделать коммит, написать сообщение для git, зафиксировать изменения или проанализировать git diff. Навык содержит критически важные правила формата Conventional Commits"
|
||||
description: "Формат сообщений Conventional Commits. Применяй при создании коммита, amend или подготовке сообщения коммита."
|
||||
---
|
||||
|
||||
# Твоя роль
|
||||
Ты — педантичный Release Manager и Senior Engineer. Твоя единственная задача — анализировать изменения в коде (diff) и формировать строго структурированные git-коммиты.
|
||||
# Формат сообщения
|
||||
|
||||
# Правила заголовка
|
||||
* Формат: `<type>(<scope>): <описание>`
|
||||
* Длина: не более 72 символов.
|
||||
* Типы (type): feat, fix, refactor, docs, test, chore, ci, perf.
|
||||
* Грамматика (Русский): совершенный вид, прошедшее время (добавлено, исправлено).
|
||||
* Грамматика (Английский): повелительное наклонение (add, fix, update). Без точки в конце.
|
||||
* Технические термины (SQL, DWH, PostgreSQL, Airflow) не переводи.
|
||||
## Заголовок
|
||||
|
||||
# Шаблон тела коммита (Markdown-список)
|
||||
- Зачем:
|
||||
- [Бизнес-причина или техническая необходимость]
|
||||
- Что:
|
||||
- [Ключевое изменение 1]
|
||||
- [Ключевое изменение 2, если есть]
|
||||
- Проверка:
|
||||
- [Как проверить: команда, тест или скрипт]
|
||||
- Формат: `<type>(<scope>): <описание>`. Указывай scope, когда он помогает обозначить область изменения.
|
||||
- Типы: feat, fix, refactor, docs, test, chore, ci, perf, build, revert.
|
||||
- До 72 символов, без точки в конце.
|
||||
- По умолчанию пиши по-русски, в совершенном виде и прошедшем времени: «добавлено», «исправлено». Если нужен английский — используй повелительное наклонение: add, fix, update.
|
||||
|
||||
Требования к телу:
|
||||
* Всегда используй именно список с маркерами `-`, как в шаблоне выше.
|
||||
* Обязательные секции: `Зачем`, `Что`, `Проверка`.
|
||||
* Внутри каждой секции минимум 1 подпункт.
|
||||
* В секции `Что` обычно 1–3 подпункта с фактами по diff.
|
||||
## Тело
|
||||
|
||||
# Правило переносов строк
|
||||
В тексте коммита должны быть реальные переносы строк. Не пиши буквальные `\n`, `\r`, `\t` как часть текста сообщения.
|
||||
Для простой правки достаточно заголовка. Когда нужны подробности, ориентируйся на короткий список «Зачем / Что / Проверка»:
|
||||
|
||||
# Специфические правила для Amend
|
||||
Если пользователь просит дополнить коммит (`amend`), твоя задача — обновить сообщение так, чтобы оно выглядело как единое целое.
|
||||
- Зачем: причина изменения из контекста задачи.
|
||||
- Что: существенные изменения, которые дополняют заголовок.
|
||||
- Проверка: что проверено и с каким результатом; предложенную проверку явно обозначай как ещё не выполненную.
|
||||
|
||||
1. **Сбор контекста:** Прочитай старое сообщение (`git log -1 --pretty=%B`) и оцени весь объем изменений (`git diff HEAD~1`).
|
||||
2. **Секция "Зачем":** НИКОГДА не меняй эту секцию из старого коммита.
|
||||
3. **Секция "Что":** Сгенерируй этот список ЗАНОВО для всего итогового кода.
|
||||
4. **Секция "Проверка":** Оставь старую, либо обнови, если тесты изменились.
|
||||
Сохраняй этот порядок и названия там, где они уместны. Включай только содержательные пункты; объём пояснений соразмеряй изменению. Если причина неизвестна, опирайся на подтверждённые факты.
|
||||
|
||||
# Примеры
|
||||
Пример:
|
||||
|
||||
Пример 1 (Русский, новая фича, PowerShell):
|
||||
@'
|
||||
feat(sql): добавлен скрипт валидации данных для DWH слоя
|
||||
|
||||
- Зачем:
|
||||
- необходима проверка целостности и уникальности перед загрузкой в хранилище.
|
||||
- Что:
|
||||
- создан файл 04_validation.sql.
|
||||
- добавлены проверки на NULL.
|
||||
- Проверка:
|
||||
- psql -f dwh-modeling/sql/04_validation.sql.
|
||||
'@ | git commit -F-
|
||||
|
||||
Пример 2 (Русский, багфикс):
|
||||
git commit -F- <<'EOF'
|
||||
```text
|
||||
fix(airflow): увеличен таймаут подключения ClickHouse
|
||||
|
||||
- Зачем:
|
||||
- DAG падал с ошибкой таймаута при ночной пакетной загрузке.
|
||||
- Что:
|
||||
- параметр conn_timeout изменен с 30 до 120 секунд.
|
||||
- Проверка:
|
||||
- тестовый запуск таски в Airflow UI.
|
||||
EOF
|
||||
- Зачем: ночная загрузка прерывалась по таймауту подключения.
|
||||
- Что: conn_timeout увеличен с 30 до 120 секунд.
|
||||
- Проверка: ещё не выполнена; нужен тестовый запуск DAG.
|
||||
```
|
||||
|
||||
Пример 3 (Английский, lang:en):
|
||||
git commit -F- <<'EOF'
|
||||
refactor(docker): optimize postgres-bookings image size
|
||||
## Amend
|
||||
|
||||
- Зачем:
|
||||
- reduce deployment time and registry storage costs.
|
||||
- Что:
|
||||
- switched base image to alpine.
|
||||
- removed intermediate build artifacts.
|
||||
- Проверка:
|
||||
- docker build and check image size.
|
||||
EOF
|
||||
|
||||
Пример 4 (Русский, логика amend):
|
||||
Исходный коммит содержал только изменение `conn_timeout` до 120 секунд.
|
||||
Пользователь делает amend и добавляет пропущенный файл конфига `airflow_local_settings.py`.
|
||||
|
||||
Сообщение (в `.git_commit_msg.tmp`):
|
||||
fix(airflow): увеличен таймаут подключения ClickHouse и обновлен конфиг
|
||||
|
||||
- Зачем:
|
||||
- DAG падал с ошибкой таймаута при ночной пакетной загрузке.
|
||||
- Что:
|
||||
- параметр conn_timeout изменен с 30 до 120 секунд.
|
||||
- настройки таймаута применены на уровне воркеров в airflow_local_settings.py.
|
||||
- Проверка:
|
||||
- тестовый запуск таски в Airflow UI.
|
||||
Сообщение описывает весь итоговый состав коммита. Перепиши его как единое целое; сохраняй исходную причину, пока она соответствует изменениям. Сведения о проверках должны относиться к итоговой версии.
|
||||
@@ -0,0 +1,37 @@
|
||||
---
|
||||
name: session-handoff
|
||||
description: Write a handoff for a fresh session when requested by the user or another skill.
|
||||
argument-hint: "What should the next session focus on?"
|
||||
disable-model-invocation: false
|
||||
---
|
||||
|
||||
# Session handoff
|
||||
|
||||
Напиши записку, по которой новый агент сможет продолжить работу.
|
||||
Если передан аргумент, используй его как фокус следующей сессии.
|
||||
|
||||
Сохрани записку по правилам проекта; если путь не задан —
|
||||
во временный каталог ОС. Верни путь к записке.
|
||||
|
||||
## Содержание
|
||||
|
||||
Обеспечь следующему агенту понимание:
|
||||
|
||||
- цели пользователя и действующих ограничений;
|
||||
- выполненной и оставшейся работы, препятствий;
|
||||
- принятых решений и открытых вопросов;
|
||||
- следующего действия.
|
||||
|
||||
Не дублируй содержание существующих артефактов: спецификаций,
|
||||
планов, ADR, задач, коммитов и diff. Ссылайся на них по пути
|
||||
или URL. В самой записке сохраняй то, что иначе останется
|
||||
только в разговоре: уточнения пользователя, причины решений,
|
||||
результаты проверок и незавершённые рассуждения.
|
||||
|
||||
Объём и структуру выбирай по задаче.
|
||||
|
||||
Если для продолжения нужны навыки, укажи их имена
|
||||
и когда их применять.
|
||||
|
||||
Исключи секретные значения и ненужные для продолжения персональные данные.
|
||||
При необходимости оставь имена переменных и пути к настройкам.
|
||||
@@ -1,16 +0,0 @@
|
||||
---
|
||||
name: session-state-handoff
|
||||
description: Keep current session state and possibility of resume after compaction or in fresh session.
|
||||
argument-hint: "How to keep current work after interruption?"
|
||||
disable-model-invocation: false
|
||||
---
|
||||
|
||||
Write a handoff document summarising the current conversation so a fresh agent can continue the work. Save to the temporary directory of the user's OS or where is prescribed in AGENTS.md or other project config.
|
||||
|
||||
Include a "suggested skills" section in the document, naming which skills the next agent should call the Skill tool for.
|
||||
|
||||
Do not duplicate content already captured in other artifacts (specs, plans, ADRs, issues, commits, diffs). Reference them by path or URL instead.
|
||||
|
||||
Redact any sensitive information, such as API keys, passwords, or personally identifiable information.
|
||||
|
||||
If the user passed arguments, treat them as a description of what the next session will focus on and tailor the doc accordingly.
|
||||
@@ -0,0 +1,31 @@
|
||||
---
|
||||
name: visual-pr
|
||||
description: Describe the current branch's pull request for a reviewer — one-sentence why, reviewer notes, and a show-me-style change outline.
|
||||
disable-model-invocation: true
|
||||
---
|
||||
|
||||
# Describe a Pull Request
|
||||
|
||||
Create or update the pull request (merge request on GitLab) for the current branch so a reviewer sees why the change exists and the shape of the implementation. Work through the hosting's CLI (`glab`, `tea`, `gh`); without one, hand the finished description to the user to paste.
|
||||
|
||||
Adapted from [humanlayer/skills visual-pr](https://github.com/humanlayer/skills/tree/main/plugins/visual-pr/skills/visual-pr).
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Identify or create the pull request:
|
||||
- Check the current branch for a PR.
|
||||
- If none exists, review `git status --short --branch` and the branch's commits; commit task-related changes when needed, push the branch with an upstream, and create the PR.
|
||||
- Ask the user to select a PR only when the current branch has no relevant work and there is no safe current-branch PR to create.
|
||||
|
||||
2. Gather only the context needed to explain the change:
|
||||
- Read the ticket and any relevant task artifacts.
|
||||
- Read the complete PR diff and enough surrounding code to understand behavior and ownership.
|
||||
- Read `../show-me/SKILL.md` for the visual vocabulary used in the change outline.
|
||||
|
||||
3. Write the description by filling `references/pr_description_template.md`; the PR body is the filled template. Write in the language of the repository's commit history.
|
||||
|
||||
4. Publish it to the PR and confirm the update succeeded.
|
||||
|
||||
5. Report the PR URL, a 2-3 sentence summary of the change, and the changed files.
|
||||
|
||||
Write as one human talking to another: simple, coherent, concise language.
|
||||
@@ -0,0 +1,53 @@
|
||||
[{RELEVANT LINK}]({RELEVANT LINK}) | [{RELEVANT LINK 2}]({RELEVANT LINK 2}) | ...
|
||||
|
||||
## Why the change
|
||||
|
||||
{Exactly one sentence explaining the problem this PR solves and what becomes possible after it ships.}
|
||||
|
||||
## Special things to note
|
||||
|
||||
- {List 1-3 reviewer-relevant warnings, migrations, constraints, deliberate omissions, or surprising decisions. Use "None." when there are no special considerations.}
|
||||
|
||||
## Change outline
|
||||
|
||||
{Use the smallest combination of the following `/show-me`-style views that explains the implementation. Prefer `diff` blocks for changes to existing shapes and complete blocks for mostly new shapes. Do not include headings for views that are not relevant.}
|
||||
|
||||
{...short-description...}
|
||||
|
||||
```diff|sql|json|etc
|
||||
{Show changed SQL tables, important columns and relationships, and endpoint request/response contracts.}
|
||||
```
|
||||
|
||||
{...short-description...}
|
||||
|
||||
```typescript|python|etc
|
||||
{show new data structures that are key to the implementation}
|
||||
```
|
||||
|
||||
{...short-description...}
|
||||
|
||||
```diff
|
||||
{Show concise pseudocode for the changed behavior.}
|
||||
```
|
||||
|
||||
|
||||
{...short-description...}
|
||||
|
||||
```diff
|
||||
{Show a shallow file tree with changed responsibilities.}
|
||||
```
|
||||
|
||||
|
||||
{...short-description...}
|
||||
|
||||
```diff
|
||||
{Show changed React component trees, important hooks or state, and package boundaries.}
|
||||
```
|
||||
|
||||
{...short-description...}
|
||||
|
||||
```diff
|
||||
{Show changed call trees, call stacks, control flow, or data flow.}
|
||||
```
|
||||
|
||||
{Tell the story in the order that makes it easiest to understand. It may make sense to show files first, or it may make sense to establish a data structure, SQL table, or API contract first. All views and subheadings are optional. Use only the views that help explain the pr, and name or order them based on the change rather than a fixed template. It should be written as one human would write to another. Use `diff` for a focused change to an existing shape. Show the complete target shape in a language-specific or `text` block when it is new, high-level, or clearer without diff notation.}
|
||||
@@ -1,6 +1,5 @@
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"plugin": ["superpowers@git+https://github.com/obra/superpowers.git"],
|
||||
"default_agent": "safe-plan",
|
||||
"permission": {
|
||||
"write": "ask",
|
||||
@@ -42,24 +41,5 @@
|
||||
"enabled": true
|
||||
}
|
||||
},
|
||||
"provider": {
|
||||
"zai": {
|
||||
"npm": "@ai-sdk/openai-compatible",
|
||||
"name": "GLM Coding Plan (custom)",
|
||||
"options": {
|
||||
"baseURL": "https://api.z.ai/api/coding/paas/v4",
|
||||
"apiKey": "{env:ZAI_API_KEY}"
|
||||
},
|
||||
"models": {
|
||||
"glm-5.1": {
|
||||
"name": "GLM-5.1",
|
||||
"limit": {
|
||||
"context": 200000,
|
||||
"output": 65536
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"share": "manual"
|
||||
}
|
||||
@@ -442,11 +442,12 @@ def parse_vless(url):
|
||||
parsed = urlsplit(url)
|
||||
except ValueError:
|
||||
raise ProxyError("неверная VLESS-ссылка")
|
||||
# Новые ссылки Hiddify пишут «/» перед «?»: ...@SERVER:PORT/?type=...
|
||||
if (
|
||||
parsed.scheme != "vless"
|
||||
or not parsed.netloc
|
||||
or parsed.password is not None
|
||||
or parsed.path
|
||||
or parsed.path not in ("", "/")
|
||||
):
|
||||
raise ProxyError("ожидалась одна ссылка vless://UUID@SERVER:PORT")
|
||||
try:
|
||||
@@ -464,8 +465,11 @@ def parse_vless(url):
|
||||
validate_address(server)
|
||||
query = parse_query(parsed.query)
|
||||
required(query, "type", "security", "encryption", "sni", "fp")
|
||||
if query["encryption"] != "none":
|
||||
raise ProxyError("поддерживается только encryption=none")
|
||||
# Кроме none, Xray принимает VLESS-шифрование вида
|
||||
# mlkem768x25519plus.РЕЖИМ.RTT.КЛЮЧ...; подробности проверит xray run -test.
|
||||
encrypted = query["encryption"] != "none"
|
||||
if encrypted and not query["encryption"].startswith("mlkem768x25519plus."):
|
||||
raise ProxyError("поддерживается только encryption=none или mlkem768x25519plus")
|
||||
if query.get("flow") not in (None, "xtls-rprx-vision"):
|
||||
raise ProxyError("поддерживается только flow=xtls-rprx-vision")
|
||||
if query.get("headerType", "none") != "none":
|
||||
@@ -485,7 +489,8 @@ def parse_vless(url):
|
||||
raise ProxyError("параметры профиля несовместимы с TCP/REALITY")
|
||||
elif query["type"] == "xhttp" and query["security"] == "tls":
|
||||
required(query, "host", "path")
|
||||
if "flow" in query or "pbk" in query or "sid" in query:
|
||||
# Vision поверх XHTTP работает только внутри VLESS-шифрования.
|
||||
if ("flow" in query and not encrypted) or "pbk" in query or "sid" in query:
|
||||
raise ProxyError("параметры профиля несовместимы с XHTTP/TLS")
|
||||
if "pcs" in query:
|
||||
hashes = list_value(query["pcs"])
|
||||
@@ -1188,6 +1193,8 @@ def safe_summary(profile, local_port):
|
||||
),
|
||||
flush=True,
|
||||
)
|
||||
if profile.query["encryption"] != "none":
|
||||
print("VLESS-шифрование: mlkem768x25519plus", flush=True)
|
||||
print("UUID и ключи: скрыты", flush=True)
|
||||
|
||||
|
||||
@@ -1310,6 +1317,10 @@ def check_port_available(address, port):
|
||||
family = socket.AF_INET6 if address.version == 6 else socket.AF_INET
|
||||
try:
|
||||
with socket.socket(family, socket.SOCK_STREAM) as listener:
|
||||
# Без флага bind отказывает, пока после остановки Xray на порту
|
||||
# доживают закрытые соединения (TIME_WAIT); живой слушатель
|
||||
# на Linux флаг не обходит, и занятый порт по-прежнему виден.
|
||||
listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
listener.bind((str(address), port))
|
||||
except OSError:
|
||||
raise ProxyError("локальный порт {} занят или недоступен".format(port))
|
||||
|
||||
@@ -345,6 +345,12 @@ source ~/.config/proxy-client/shell.bash
|
||||
повторные и несовместимые query-параметры отклоняются. XHTTP поддерживает ALPN
|
||||
`http/1.1`, `h2` и `h3`; `extra` должен быть JSON-объектом без повторных ключей.
|
||||
|
||||
Кроме `encryption=none`, принимается VLESS-шифрование
|
||||
`encryption=mlkem768x25519plus.…` из новых ссылок Hiddify; остальную часть
|
||||
значения проверяет сам Xray. Только с таким шифрованием XHTTP допускает
|
||||
`flow=xtls-rprx-vision`. Косая черта перед параметрами (`…:443/?type=…`)
|
||||
допустима, другой путь в ссылке — нет.
|
||||
|
||||
Xray 26.3.27 использует `pcs` и `vcn`, а устаревшие `allowInsecure` и
|
||||
`insecure` не переносит. Истинное legacy-значение даёт предупреждение, но
|
||||
обычная проверка сертификата остаётся включённой.
|
||||
|
||||
@@ -5,6 +5,7 @@ import json
|
||||
import hashlib
|
||||
import os
|
||||
import runpy
|
||||
import socket
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
@@ -1231,5 +1232,114 @@ class XrayInstallTest(unittest.TestCase):
|
||||
self.assertIn("уже установлен", result.stdout)
|
||||
|
||||
|
||||
class VlessProfileTest(unittest.TestCase):
|
||||
UUID = "11111111-2222-4333-8444-555555555555"
|
||||
ENCRYPTION = "mlkem768x25519plus.native.0rtt.SYNTHETIC_CLIENT_KEY"
|
||||
EXTRA = (
|
||||
"%7B%22downloadSettings%22%3A%7B%22address%22%3A%22cdn.example.invalid%22"
|
||||
"%2C%22port%22%3A443%2C%22network%22%3A%22xhttp%22%7D%7D"
|
||||
)
|
||||
OLD_REALITY = (
|
||||
"vless://{}@example.invalid:443?type=tcp&security=reality&encryption=none"
|
||||
"&flow=xtls-rprx-vision&sni=front.example.invalid&fp=chrome"
|
||||
"&pbk=SYNTHETIC_PUBLIC_KEY&sid=SYNTHETIC_SHORT_ID&headerType=none#old"
|
||||
).format(UUID)
|
||||
OLD_XHTTP = (
|
||||
"vless://{}@example.invalid:443?type=xhttp&security=tls&encryption=none"
|
||||
"&sni=cdn.example.invalid&fp=chrome&host=cdn.example.invalid&path=%2Fp"
|
||||
"&alpn=h2&mode=auto"
|
||||
).format(UUID)
|
||||
# Формат Hiddify с VLESS-шифрованием: «/» перед «?» и flow поверх XHTTP.
|
||||
NEW_XHTTP = (
|
||||
"vless://{}@192.0.2.1:443/?hiddify=1&fp=chrome&sni=cdn.example.invalid"
|
||||
"&security=tls&alpn=h3&type=xhttp&mode=auto&path=%2Fp"
|
||||
"&host=cdn.example.invalid&extra={}&flow=xtls-rprx-vision"
|
||||
"&encryption={}#xhttp%20vless%20QUIC"
|
||||
).format(UUID, EXTRA, ENCRYPTION)
|
||||
|
||||
def setUp(self):
|
||||
self.module = runpy.run_path(str(CLI))
|
||||
|
||||
def outbound(self, url):
|
||||
profile = self.module["parse_vless"](url)
|
||||
config = self.module["build_xray_config"](profile, "127.0.0.1", 12334)
|
||||
return config["outbounds"][0]
|
||||
|
||||
def test_old_formats_still_parse(self):
|
||||
reality = self.outbound(self.OLD_REALITY)
|
||||
self.assertEqual("none", reality["settings"]["encryption"])
|
||||
self.assertEqual("reality", reality["streamSettings"]["security"])
|
||||
|
||||
xhttp = self.outbound(self.OLD_XHTTP)
|
||||
self.assertEqual("none", xhttp["settings"]["encryption"])
|
||||
self.assertNotIn("flow", xhttp["settings"])
|
||||
self.assertEqual(["h2"], xhttp["streamSettings"]["tlsSettings"]["alpn"])
|
||||
|
||||
def test_new_hiddify_xhttp_with_vless_encryption_parses(self):
|
||||
outbound = self.outbound(self.NEW_XHTTP)
|
||||
settings = outbound["settings"]
|
||||
stream = outbound["streamSettings"]
|
||||
self.assertEqual(self.ENCRYPTION, settings["encryption"])
|
||||
self.assertEqual("xtls-rprx-vision", settings["flow"])
|
||||
self.assertEqual("192.0.2.1", settings["address"])
|
||||
self.assertEqual(["h3"], stream["tlsSettings"]["alpn"])
|
||||
self.assertEqual(
|
||||
"cdn.example.invalid",
|
||||
stream["xhttpSettings"]["extra"]["downloadSettings"]["address"],
|
||||
)
|
||||
|
||||
def test_incompatible_profiles_are_still_rejected(self):
|
||||
cases = {
|
||||
"flow over XHTTP without encryption": self.OLD_XHTTP
|
||||
+ "&flow=xtls-rprx-vision",
|
||||
"unknown encryption": self.NEW_XHTTP.replace(
|
||||
"mlkem768x25519plus.", "aes-128-gcm."
|
||||
),
|
||||
"non-root path": self.NEW_XHTTP.replace(":443/?", ":443/sub?"),
|
||||
}
|
||||
for label, url in cases.items():
|
||||
with self.subTest(label=label):
|
||||
with self.assertRaises(self.module["ProxyError"]):
|
||||
self.module["parse_vless"](url)
|
||||
|
||||
|
||||
class PortCheckTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.module = runpy.run_path(str(CLI))
|
||||
self.check = self.module["check_port_available"]
|
||||
self.address = self.module["ipaddress"].ip_address("127.0.0.1")
|
||||
|
||||
@staticmethod
|
||||
def listener():
|
||||
# Как у Xray: Go ставит SO_REUSEADDR на слушающий сокет.
|
||||
sock = socket.socket()
|
||||
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
sock.bind(("127.0.0.1", 0))
|
||||
sock.listen()
|
||||
return sock
|
||||
|
||||
def test_port_is_free_right_after_xray_closed_its_connections(self):
|
||||
listener = self.listener()
|
||||
port = listener.getsockname()[1]
|
||||
client = socket.create_connection(("127.0.0.1", port))
|
||||
accepted, _peer = listener.accept()
|
||||
# После Ctrl+C Xray первым закрывает соединения, и ядро держит их
|
||||
# «хвосты» на локальном порту ещё около минуты.
|
||||
accepted.close()
|
||||
listener.close()
|
||||
try:
|
||||
self.check(self.address, port)
|
||||
finally:
|
||||
client.close()
|
||||
|
||||
def test_live_listener_is_still_reported_as_busy(self):
|
||||
listener = self.listener()
|
||||
try:
|
||||
with self.assertRaisesRegex(self.module["ProxyError"], "занят"):
|
||||
self.check(self.address, listener.getsockname()[1])
|
||||
finally:
|
||||
listener.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user