#!/usr/bin/env bash # Тест sync-statements.sh на синтетических репозиториях (bare origin + клон), без tea и сети. # Разделы: помощник statements_check.py; дальше — сценарии самого скрипта (дописываются по задачам плана). # Запуск: make test-sync-statements (или напрямую из корня docs; интерпретатор — STATEMENTS_PY, по умолчанию python3). set -euo pipefail HERE="$(cd "$(dirname "$0")" && pwd)" PY=${STATEMENTS_PY:-python3} WORK=$(mktemp -d) trap 'rm -rf "$WORK"' EXIT export GIT_AUTHOR_NAME=t GIT_AUTHOR_EMAIL=t@t GIT_COMMITTER_NAME=t GIT_COMMITTER_EMAIL=t@t if ! "$PY" -c 'import tomllib, yaml' 2>/dev/null; then echo "нужен Python >= 3.11 с PyYAML: выполните make venv (или задайте STATEMENTS_PY)" >&2 exit 1 fi fail=0 check() { if [ "$1" = 0 ]; then echo "ok: $2"; else echo "FAIL: $2" >&2; fail=1; fi; } put() { mkdir -p "$(dirname "$1")"; printf '%s\n' "${2:-x}" > "$1"; } # shellcheck disable=SC2317 commit_push() { git -C "$1" add -A; git -C "$1" commit -qm "$2"; git -C "$1" push -q origin "HEAD:$3" 2>/dev/null; git -C "$1" fetch -q origin; } # --- помощник statements_check.py --------------------------------------------------------------------------------- H="$WORK/h"; mkdir -p "$H" CHK="$HERE/statements_check.py" # run <описание> <аргументы…>: вывод — в $OUT run() { local want=$1 desc=$2 rc=0; shift 2 OUT=$("$PY" "$CHK" "$@" 2>&1) || rc=$? check "$([ "$rc" = "$want" ] && echo 0 || echo 1)" "$desc (rc=$rc, ожидался $want)" } has() { grep -q -- "$1" <<<"$OUT"; } put "$H/pp.toml" $'[project]\ndependencies = ["PyYAML==6.0.3"]' put "$H/r1.txt" $'aiohttp==3.14.3\nPyYAML==6.0.3' run 0 "deps: есть" deps "$H/pp.toml" "$H/r1.txt" check "$([ -z "$OUT" ] && echo 0 || echo 1)" "deps: есть — вывод пустой" put "$H/r2.txt" $'aiohttp==3.14.3\npyyaml==6.0.3' run 0 "deps: регистр" deps "$H/pp.toml" "$H/r2.txt" put "$H/pp_u.toml" $'[project]\ndependencies = ["Py_YAML==6.0.3"]' put "$H/r2u.txt" $'py-yaml==6.0.3' run 0 "deps: подчёркивание и дефис равны" deps "$H/pp_u.toml" "$H/r2u.txt" put "$H/r3.txt" $'aiohttp==3.14.3\n# PyYAML==6.0.3\n-r other.txt' run 20 "deps: нет" deps "$H/pp.toml" "$H/r3.txt" check "$(has PyYAML && echo 0 || echo 1)" "deps: нет — в выводе PyYAML" put "$H/r4.txt" $'PyYAML==6.0.4' run 0 "deps: другая версия" deps "$H/pp.toml" "$H/r4.txt" check "$(has 'примечание:' && has 6.0.4 && echo 0 || echo 1)" "deps: другая версия — примечание с 6.0.4" put "$H/pp_none.toml" $'[tool.ruff]\nline-length = 120' run 0 "deps: без [project]" deps "$H/pp_none.toml" "$H/r1.txt" put "$H/pp_jwt.toml" $'[project]\ndependencies = ["PyJWT==2.8.0"]' put "$H/r5.txt" $'PyJWT[crypto]==2.8.0' run 0 "deps: экстры хоста" deps "$H/pp_jwt.toml" "$H/r5.txt" put "$H/orders.yaml" 'version: 1 repeats: {} editions: 66n: BALANCE: {full: ["1110", "1100"], simplified: ["1100"]} INCOME: {full: ["2110"], simplified: ["2110"]} fsbu4: BALANCE: {full: ["1110", "1100"], simplified: ["1100"]} INCOME: {full: ["2110"], simplified: ["2110"]}' put "$H/lines.yaml" 'editions: - id: 66n forms: BALANCE: - {code: "1110", name: "Основные средства"} - {code: "1100", name: "Итого"} - id: fsbu4 forms: BALANCE: - {code: "1110", name: "Основные средства"}' run 0 "lines: все коды есть" lines "$H/lines.yaml" "$H/orders.yaml" put "$H/lines_x.yaml" 'editions: - id: 66n forms: BALANCE: - {code: "1110", name: "а"} - id: fsbu4 forms: BALANCE: - {code: "1110", name: "а"} - {code: "1120", name: "б"}' run 20 "lines: код вне перечня" lines "$H/lines_x.yaml" "$H/orders.yaml" check "$(has 1120 && has fsbu4 && echo 0 || echo 1)" "lines: код вне перечня — в выводе 1120 и fsbu4" put "$H/orders_s.yaml" 'version: 1 editions: fsbu4: BALANCE: {full: ["1110"], simplified: ["1100"]}' put "$H/lines_s.yaml" 'editions: - id: fsbu4 forms: BALANCE: - {code: "1100", name: "а"}' run 0 "lines: код только в simplified" lines "$H/lines_s.yaml" "$H/orders_s.yaml" put "$H/bad.yaml" 'editions: [' run 1 "битый YAML" lines "$H/bad.yaml" "$H/orders.yaml" run 2 "аргументы" # --- sync-statements.sh --check ----------------------------------------------------------------------------------- SYNC="$HERE/sync-statements.sh" export STATEMENTS_PY="$PY" SRC="$WORK/arm-statements" git init -q --bare "$WORK/arm-statements.git" git clone -q "$WORK/arm-statements.git" "$SRC" 2>/dev/null git -C "$SRC" checkout -q -b main put "$SRC/pyproject.toml" $'[project]\nname = "arm-statements"\ndependencies = ["PyYAML==6.0.3"]' put "$SRC/README.md" "пакет" put "$SRC/arm_statements/__init__.py" "# init" put "$SRC/arm_statements/steps.py" "def step(): return 1" mkdir -p "$SRC/arm_statements/rules"; cp "$H/orders.yaml" "$SRC/arm_statements/rules/statement_code_orders.yaml" put "$SRC/arm_statements/tests/__init__.py" "# t" put "$SRC/arm_statements/tests/test_steps.py" "def test_step(): pass" commit_push "$SRC" "S1" main S1=$(git -C "$SRC" rev-parse HEAD) mkdir -p "$WORK/docs/contracts" "$WORK/docs-empty" "$WORK/wt" cp "$H/orders.yaml" "$WORK/docs/contracts/statement-code-orders.yaml" # make_host <каталог> <имя origin>: bare + клон с копией S1 в api/arm_statements, requirements и (для arm-core) словарём make_host() { local dir=$1 name=$2 git init -q --bare "$WORK/$name.git" git clone -q "$WORK/$name.git" "$dir" 2>/dev/null git -C "$dir" checkout -q -b main put "$dir/api/requirements.txt" $'aiohttp==3.14.3\nPyYAML==6.0.3' mkdir -p "$dir/api" git -C "$SRC" archive "$S1" arm_statements | tar -x -C "$dir/api" printf '%s\n' "$S1" > "$dir/api/arm_statements/VERSION" } make_host "$WORK/arm-core" arm-core mkdir -p "$WORK/arm-core/api/rules"; cp "$H/lines.yaml" "$WORK/arm-core/api/rules/statement_lines.yaml" commit_push "$WORK/arm-core" "copy S1" main make_host "$WORK/tabular-parser" arm-tabular-parser commit_push "$WORK/tabular-parser" "copy S1" main make_host "$WORK/scan-gateway" arm-scan-gateway mkdir -p "$WORK/scan-gateway/api/rules" put "$WORK/scan-gateway/api/rules/statement_lines.yaml" $'editions:\n - id: fsbu4\n forms:\n BALANCE:\n - {code: "9999", name: "х"}' commit_push "$WORK/scan-gateway" "copy S1" main COMMON=(--source "$WORK/arm-statements" --docs "$WORK/docs" --wt-root "$WORK/wt" --core-dir "$WORK/arm-core") AC="$WORK/arm-core/api" reset_hosts() { local h; for h in arm-core tabular-parser scan-gateway; do git -C "$WORK/$h" checkout -q -- . && git -C "$WORK/$h" clean -fdq; done; } # sync <описание> <аргументы…>: вывод — в $OUT sync() { local want=$1 desc=$2 rc=0; shift 2 OUT=$("$SYNC" "${COMMON[@]}" "$@" 2>&1) || rc=$? check "$([ "$rc" = "$want" ] && echo 0 || echo 1)" "$desc (rc=$rc, ожидался $want)" } sync 0 "check: синхронно" --check --host-dir "$WORK/arm-core" check "$(has 'statements-check arm-core: 0' && echo 0 || echo 1)" "check: синхронно — итоговая строка" sync 0 "check: имя по origin, словарь не у scan-gateway" --check --host-dir "$WORK/scan-gateway" check "$(has 'scan-gateway' && ! has 9999 && echo 0 || echo 1)" "check: scan-gateway — в выводе имя, нет 9999" OUT=$(cd "$WORK/tabular-parser" && "$SYNC" --check --host-dir . "${COMMON[@]}" 2>&1) && rc=0 || rc=$? check "$([ "$rc" = 0 ] && has 'statements-check tabular-parser: 0' && echo 0 || echo 1)" "check: --host-dir . из каталога хоста (rc=$rc)" sync 0 "check: --host вместо origin" --check --host scan-gateway --host-dir "$WORK/scan-gateway" sync 2 "check: неизвестное имя --host" --check --host foo --host-dir "$WORK/arm-core" TP="$WORK/tabular-parser/api/arm_statements" mkdir -p "$TP/__pycache__" "$TP/tests/.pytest_cache" "$TP/.mypy_cache" : > "$TP/__pycache__/steps.cpython-311.pyc"; : > "$TP/tests/.pytest_cache/x"; : > "$TP/.mypy_cache/x"; : > "$TP/.DS_Store" sync 0 "check: кэши в копии игнорируются" --check --host-dir "$WORK/tabular-parser" reset_hosts echo "# правка" >> "$AC/arm_statements/steps.py" sync 20 "check: изменён файл" --check --host-dir "$WORK/arm-core" check "$(has steps.py && has 'прямая правка' && echo 0 || echo 1)" "check: изменён файл — steps.py и «прямая правка»" reset_hosts put "$AC/arm_statements/extra.py" "x = 1" sync 20 "check: добавлен файл" --check --host-dir "$WORK/arm-core" check "$(has extra.py && echo 0 || echo 1)" "check: добавлен файл — extra.py" reset_hosts rm "$AC/arm_statements/tests/test_steps.py" sync 20 "check: удалён файл" --check --host-dir "$WORK/arm-core" check "$(has test_steps.py && echo 0 || echo 1)" "check: удалён файл — test_steps.py" reset_hosts rm "$AC/arm_statements/VERSION" sync 20 "check: нет VERSION" --check --host-dir "$WORK/arm-core" reset_hosts printf '%s\n' 0000000000000000000000000000000000000000 > "$AC/arm_statements/VERSION" sync 20 "check: неизвестный VERSION" --check --host-dir "$WORK/arm-core" reset_hosts rm -rf "$AC/arm_statements" sync 20 "check: нет копии" --check --host-dir "$WORK/arm-core" check "$(has 'копии нет' && echo 0 || echo 1)" "check: нет копии — сообщение" reset_hosts git -C "$SRC" checkout -q -b feat/x echo "# F" >> "$SRC/arm_statements/steps.py" commit_push "$SRC" "F" feat/x F=$(git -C "$SRC" rev-parse HEAD) git -C "$SRC" checkout -q main git -C "$SRC" archive "$F" arm_statements | tar -x -C "$AC" printf '%s\n' "$F" > "$AC/arm_statements/VERSION" sync 20 "check: чужая версия" --check --host-dir "$WORK/arm-core" check "$(has 'чужая версия' && echo 0 || echo 1)" "check: чужая версия — сообщение" reset_hosts put "$AC/requirements.txt" "aiohttp==3.14.3" sync 20 "check: нет зависимости" --check --host-dir "$WORK/arm-core" check "$(has PyYAML && echo 0 || echo 1)" "check: нет зависимости — PyYAML" reset_hosts cp "$H/lines_x.yaml" "$AC/rules/statement_lines.yaml" sync 20 "check: код словаря вне перечня" --check --host-dir "$WORK/arm-core" check "$(has 1120 && has fsbu4 && echo 0 || echo 1)" "check: код вне перечня — 1120 и fsbu4" reset_hosts echo "# новый код" >> "$WORK/docs/contracts/statement-code-orders.yaml" sync 10 "check: перечень в docs новее" --check --host-dir "$WORK/arm-core" check "$(has contracts/statement-code-orders.yaml && has 'statements-check arm-core: 10' && echo 0 || echo 1)" "check: перечень в docs — в выводе файл и итог 10" cp "$H/orders.yaml" "$WORK/docs/contracts/statement-code-orders.yaml" sync 10 "check: перечня нет в docs" --check --host-dir "$WORK/arm-core" --docs "$WORK/docs-empty" sync 2 "check: нет источника" --check --host-dir "$WORK/arm-core" --source "$WORK/nope" sync 2 "check: нет хоста" --check --host-dir "$WORK/nope" sync 2 "check: неизвестный аргумент" --check --bogus sync 2 "check: нет --host/--host-dir" --check sync 2 "check: --ref не разрешается" --check --host-dir "$WORK/arm-core" --ref origin/nope sync 2 "--in-place вместе с --dry-run" --in-place --dry-run --host-dir "$WORK/arm-core" sync 0 "check важнее --dry-run и --in-place" --in-place --dry-run --check --host-dir "$WORK/arm-core" # проверки с S2 — последними put "$SRC/README.md" "пакет, правка" commit_push "$SRC" "S2 правка README" main sync 10 "check: отстала" --check --host-dir "$WORK/arm-core" check "$(has 'копия отстала' && has 'S2 правка README' && echo 0 || echo 1)" "check: отстала — сообщение и тема S2" echo "# правка" >> "$AC/arm_statements/steps.py" sync 20 "check: правка + отставание = 20" --check --host-dir "$WORK/arm-core" reset_hosts # коммит без arm_statements/ (T2.1): код 20 с итоговой строкой, а не 128 NOARCH=$(git -C "$SRC" commit-tree "$(git -C "$SRC" hash-object -t tree /dev/null)" -m "без пакета") printf '%s\n' "$NOARCH" > "$AC/arm_statements/VERSION" sync 20 "check: в коммите VERSION нет arm_statements/" --check --host-dir "$WORK/arm-core" check "$(has 'нет arm_statements/' && has 'statements-check arm-core: 20' && echo 0 || echo 1)" "check: нет arm_statements/ — сообщение и итог" reset_hosts # --- sync-statements.sh --in-place -------------------------------------------------------------------------------- S2=$(git -C "$SRC" rev-parse origin/main) FR="$WORK/fresh/tabular-parser" mkdir -p "$WORK/fresh" git init -q --bare "$WORK/fresh/arm-tabular-parser.git" git clone -q "$WORK/fresh/arm-tabular-parser.git" "$FR" 2>/dev/null git -C "$FR" checkout -q -b main put "$FR/api/requirements.txt" $'PyYAML==6.0.3' commit_push "$FR" "fresh" main sync 0 "in-place: первичная раскладка" --in-place --host-dir "$FR" --ref origin/main check "$([ "$(tr -d '\n' < "$FR/api/arm_statements/VERSION")" = "$S2" ] && echo 0 || echo 1)" "in-place: VERSION = полный хэш S2" rm -rf "$WORK/arc"; mkdir -p "$WORK/arc"; git -C "$SRC" archive "$S2" arm_statements | tar -x -C "$WORK/arc" check "$(diff -r -x VERSION "$WORK/arc/arm_statements" "$FR/api/arm_statements" >/dev/null && echo 0 || echo 1)" "in-place: копия равна git archive S2" check "$(git -C "$FR" status --porcelain | grep -qv '^?? api/arm_statements' && echo 1 || echo 0)" "in-place: хост менялся только в api/arm_statements" check "$(has закоммитьте && echo 0 || echo 1)" "in-place: подсказка про коммит" sync 0 "in-place: повтор" --in-place --host-dir "$FR" --ref origin/main check "$(has 'раскладывать нечего' && echo 0 || echo 1)" "in-place: повтор — раскладывать нечего" sync 0 "in-place: обновление arm-core S1 -> S2" --in-place --host-dir "$WORK/arm-core" check "$([ "$(tr -d '\n' < "$AC/arm_statements/VERSION")" = "$S2" ] && echo 0 || echo 1)" "in-place: arm-core VERSION = S2" echo "# правка" >> "$AC/arm_statements/steps.py" sync 20 "in-place: отказ при правке" --in-place --host-dir "$WORK/arm-core" --ref origin/main check "$(grep -q '^# правка' "$AC/arm_statements/steps.py" && [ "$(tr -d '\n' < "$AC/arm_statements/VERSION")" = "$S2" ] && echo 0 || echo 1)" "in-place: правка на месте, VERSION прежний" reset_hosts TPR="$WORK/tabular-parser/api" put "$TPR/requirements.txt" "aiohttp==3.14.3" sync 20 "in-place: отказ без зависимости (обновление)" --in-place --host-dir "$WORK/tabular-parser" check "$([ "$(tr -d '\n' < "$TPR/arm_statements/VERSION")" = "$S1" ] && has PyYAML && echo 0 || echo 1)" "in-place: копия не тронута, в выводе PyYAML" reset_hosts cp "$FR/api/requirements.txt" "$WORK/req.bak" put "$FR/api/requirements.txt" "aiohttp==3.14.3" sync 20 "in-place: отказ без зависимости (fresh)" --in-place --host-dir "$FR" --ref origin/main check "$([ "$(tr -d '\n' < "$FR/api/arm_statements/VERSION")" = "$S2" ] && diff -r -x VERSION "$WORK/arc/arm_statements" "$FR/api/arm_statements" >/dev/null && echo 0 || echo 1)" "in-place: fresh — api/arm_statements не тронут" cp "$WORK/req.bak" "$FR/api/requirements.txt" cp "$H/lines_x.yaml" "$AC/rules/statement_lines.yaml" sync 20 "in-place: код словаря вне перечня" --in-place --host-dir "$WORK/arm-core" --ref origin/main check "$([ "$(tr -d '\n' < "$AC/arm_statements/VERSION")" = "$S1" ] && has 1120 && echo 0 || echo 1)" "in-place: словарь — копия не тронута, 1120 в выводе" reset_hosts # защиты: коммиты S3/S4 на ветке test-guards, чтобы origin/main (S2) остался прежним git -C "$SRC" checkout -q -b test-guards put "$SRC/arm_statements/tests/golden/case.json" '{}' commit_push "$SRC" "S3 json" test-guards put "$FR/.gitignore" '*.json' sync 1 "in-place: .gitignore прячет файл" --in-place --host-dir "$FR" --ref origin/test-guards check "$(has case.json && has .gitignore && echo 0 || echo 1)" "in-place: в выводе case.json и .gitignore" sync 1 "in-place: повтор после защиты .gitignore остаётся 1" --in-place --host-dir "$FR" --ref origin/test-guards check "$(has case.json && has .gitignore && echo 0 || echo 1)" "in-place: повтор — снова case.json и .gitignore" rm "$FR/.gitignore" echo "# /Users/someone" >> "$SRC/arm_statements/steps.py" commit_push "$SRC" "S4 путь" test-guards sync 1 "in-place: содержимое для контура" --in-place --host-dir "$FR" --ref origin/test-guards check "$(has '/Users/' && echo 0 || echo 1)" "in-place: в выводе /Users/" sync 1 "in-place: повтор после защиты путей остаётся 1" --in-place --host-dir "$FR" --ref origin/test-guards check "$(has '/Users/' && echo 0 || echo 1)" "in-place: повтор — снова /Users/" git -C "$SRC" checkout -q main # --- sync-statements.sh --dry-run (обычный режим без issue и PR) -------------------------------------------------- S27=${S2:0:7} git -C "$SRC" checkout -q main rm_wt() { # rm_wt <хост-каталог> <имя хоста>: убрать worktree и ветку синхронизации git -C "$1" worktree remove --force "$WORK/wt/statements-sync-$S27/$2" 2>/dev/null || true git -C "$1" branch -D "tmp/statements-sync-$S27" >/dev/null 2>&1 || true } WTC="$WORK/wt/statements-sync-$S27/arm-core" sync 0 "dry-run: arm-core" --dry-run --host-dir "$WORK/arm-core" check "$([ -d "$WTC" ] && git -C "$WORK/arm-core" rev-parse -q --verify "tmp/statements-sync-$S27" >/dev/null && [ -f "$WTC/.bounded" ] && echo 0 || echo 1)" "dry-run: worktree, ветка и .bounded" check "$([ "$(tr -d '\n' < "$WTC/api/arm_statements/VERSION")" = "$S2" ] && echo 0 || echo 1)" "dry-run: VERSION = S2" rm -rf "$WORK/arc"; mkdir -p "$WORK/arc"; git -C "$SRC" archive "$S2" arm_statements | tar -x -C "$WORK/arc" check "$(diff -r -x VERSION "$WORK/arc/arm_statements" "$WTC/api/arm_statements" >/dev/null && echo 0 || echo 1)" "dry-run: копия равна git archive S2" check "$(has 'dry-run: issue, проверки, коммит и PR не выполнялись' && echo 0 || echo 1)" "dry-run: сообщение и команды удаления" check "$([ -z "$(git -C "$WORK/arm-core" status --porcelain)" ] && [ "$(tr -d '\n' < "$AC/arm_statements/VERSION")" = "$S1" ] && echo 0 || echo 1)" "dry-run: хост не тронут" sync 2 "dry-run: повтор" --dry-run --host-dir "$WORK/arm-core" check "$(has 'уже существует' && echo 0 || echo 1)" "dry-run: повтор — уже существует" rm_wt "$WORK/arm-core" arm-core check "$([ ! -d "$WTC" ] && ! git -C "$WORK/arm-core" rev-parse -q --verify "tmp/statements-sync-$S27" >/dev/null && echo 0 || echo 1)" "dry-run: worktree и ветка удалены" sync 0 "dry-run: сначала ядро" --dry-run --host-dir "$WORK/tabular-parser" check "$(has 'сначала ядро' && echo 0 || echo 1)" "dry-run: предупреждение «сначала ядро»" rm_wt "$WORK/tabular-parser" tabular-parser # arm-core на S2 закоммичена и запушена -> раскладывать нечего "$SYNC" "${COMMON[@]}" --in-place --host-dir "$WORK/arm-core" >/dev/null commit_push "$WORK/arm-core" "copy S2" main sync 0 "dry-run: уже синхронно" --dry-run --host-dir "$WORK/arm-core" check "$(has 'раскладывать нечего' && [ ! -e "$WTC" ] && echo 0 || echo 1)" "dry-run: уже синхронно — worktree не создан" # прямая правка на origin/main хоста -> отказ 20 echo "# прямая правка" >> "$WORK/tabular-parser/api/arm_statements/steps.py" commit_push "$WORK/tabular-parser" "прямая правка" main sync 20 "dry-run: прямая правка на origin/main" --dry-run --host-dir "$WORK/tabular-parser" check "$(has steps.py && [ ! -e "$WORK/wt/statements-sync-$S27/tabular-parser" ] && echo 0 || echo 1)" "dry-run: отказ — steps.py, worktree не создан" # хост без копии sync 2 "dry-run: хост без копии" --dry-run --host-dir "$FR" check "$(has -- '--in-place' && echo 0 || echo 1)" "dry-run: хост без копии — подсказка --in-place" # tea: флаги только после подкоманды check "$(grep -nE '(^|[^[:alnum:]_-])tea[[:space:]]+-' "$SYNC" >/dev/null && echo 1 || echo 0)" "tea: флаги после подкоманды" # недостижимые прежде ветки: заглушки tea и make в PATH (обычный режим, scan-gateway: копия S1, ядро на S2) STUB="$WORK/stub"; mkdir -p "$STUB" printf '#!/bin/sh\nexit 0\n' > "$STUB/make" printf '#!/bin/sh\nexit 1\n' > "$STUB/tea" chmod +x "$STUB/make" "$STUB/tea" OUT=$(PATH="$STUB:$PATH" "$SYNC" "${COMMON[@]}" --host-dir "$WORK/scan-gateway" 2>&1) && rc=0 || rc=$? check "$([ "$rc" = 1 ] && has 'не удалось создать issue' && echo 0 || echo 1)" "tea упал на issue: сообщение и код 1 (rc=$rc)" rm_wt "$WORK/scan-gateway" scan-gateway printf '#!/bin/sh\nif [ "$1" = issues ]; then echo https://git.example/RSHB/arm-scan-gateway/issues/77; exit 0; fi\nexit 1\n' > "$STUB/tea" OUT=$(PATH="$STUB:$PATH" "$SYNC" "${COMMON[@]}" --host-dir "$WORK/scan-gateway" 2>&1) && rc=0 || rc=$? check "$([ "$rc" = 1 ] && has 'PR не создан' && has 'tea pulls create' && has 'feat/77-statements-sync' && echo 0 || echo 1)" "tea упал на PR: итог и ручная команда, код 1 (rc=$rc)" rm_wt "$WORK/scan-gateway" scan-gateway git -C "$WORK/scan-gateway" branch -D "feat/77-statements-sync-$S27" >/dev/null 2>&1 || true [ "$fail" = 0 ] || exit 1 echo "ok: sync-statements.sh — все сценарии пройдены"