233 lines
14 KiB
Bash
Executable File
233 lines
14 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
# Тест prepare-contour-snapshot.sh на синтетическом репозитории arm-core: артефакты агента вырезаются на любой глубине
|
||
# (в т.ч. во вложенном web/), спецификации и код остаются. Запуск: make test-snapshot (или напрямую из корня docs).
|
||
set -euo pipefail
|
||
|
||
SCRIPT="$(cd "$(dirname "$0")" && pwd)/prepare-contour-snapshot.sh"
|
||
WORK=$(mktemp -d)
|
||
trap 'rm -rf "$WORK"' EXIT
|
||
|
||
git init -q --bare "$WORK/origin.git"
|
||
git clone -q "$WORK/origin.git" "$WORK/src/arm-core" 2>/dev/null
|
||
R="$WORK/src/arm-core"
|
||
|
||
put() { mkdir -p "$(dirname "$R/$1")"; printf '%s\n' "${2:-x}" > "$R/$1"; }
|
||
|
||
# платформенные файлы, без которых проверка снимка падает
|
||
printf 'venv:\n\tuv venv api/.venv\n\n# Фронт: biome, типы, сборка\nweb-check:\n\tcd web && npm install\n\tcd web && npx biome check src\n\tcd web && npx tsc --noEmit\n\tcd web && npm run build\n\ncheck-full: check web-check\n' > "$R/Makefile"
|
||
for p in raisa_config.yml setup/setup.sh setup/pip.conf api/run_server.py api/requirements.txt api/.env.test \
|
||
api/contracts/schema.json; do put "$p"; done
|
||
# корень: артефакты агента и локальная сборка
|
||
for p in .claude/skills/a/SKILL.md CLAUDE.md openspec/config.yaml docs/superpowers/plans/p.md Dockerfile .dockerignore; do
|
||
put "$p" "/Users/someone/projects"
|
||
done
|
||
put openspec/specs/osv/spec.md
|
||
# вложенный фронт (как web/ из arm-web): те же артефакты на второй глубине
|
||
for p in web/.claude/commands/opsx/apply.md web/.codegraph/.gitignore web/CLAUDE.md web/openspec/config.yaml \
|
||
web/docs/superpowers/plans/2026-10-08-demo.md; do
|
||
put "$p" "/Users/someone/projects"
|
||
done
|
||
put web/openspec/specs/osv-ui/spec.md
|
||
put web/openspec/changes/archive/2026-10-08-demo/design.md
|
||
put web/src/App.tsx
|
||
cat > "$R/web/package.json" <<'JSON'
|
||
{
|
||
"name": "frontend",
|
||
"private": true,
|
||
"scripts": {
|
||
"build": "rsbuild build",
|
||
"test": "vitest run",
|
||
"format": "eslint --fix src && prettier --write src",
|
||
"lint": "eslint src && prettier --check src",
|
||
"start": "rsbuild dev --open",
|
||
"preview": "rsbuild preview"
|
||
},
|
||
"dependencies": {"antd": "^6.5.0", "react": "^18.3.1"},
|
||
"devDependencies": {
|
||
"@biomejs/biome": "1.9.4", "@rsbuild/core": "1.6.7", "@rsbuild/plugin-react": "^1.1.0", "@types/react": "^18.3.1",
|
||
"daisyui": "^4.12.10", "eslint": "9.17.0", "prettier": "3.3.3", "tailwindcss": "^3.4.13", "typescript": "^5.7.3",
|
||
"vitest": "3.1.4"
|
||
},
|
||
"optionalDependencies": {"@biomejs/cli-linux-x64-musl": "^1.9.4", "raisa-ui": "^1.0.0"}
|
||
}
|
||
JSON
|
||
put web/docs/notes.md # прочее содержимое вложенного docs/ остаётся
|
||
|
||
git -C "$R" add -A
|
||
git -C "$R" -c user.name=t -c user.email=t@t commit -qm init
|
||
git -C "$R" push -q origin HEAD:main 2>/dev/null
|
||
git -C "$R" fetch -q origin
|
||
|
||
# проверка сборки web/ в тесте отключена (нет сети); её включённый режим проверяется ниже заглушкой npm
|
||
SNAPSHOT_SKIP_WEB_BUILD=1 "$SCRIPT" "$R" "$WORK/out" origin/main >/dev/null
|
||
D="$WORK/out/arm-core"
|
||
|
||
fail=0
|
||
for p in .claude CLAUDE.md openspec/config.yaml docs Dockerfile .dockerignore \
|
||
web/.claude web/.codegraph web/CLAUDE.md web/openspec/config.yaml web/docs/superpowers; do
|
||
if [ -e "$D/$p" ]; then echo "FAIL: в снимке остался $p" >&2; fail=1; fi
|
||
done
|
||
for p in openspec/specs/osv/spec.md web/openspec/specs/osv-ui/spec.md web/openspec/changes/archive/2026-10-08-demo/design.md \
|
||
web/src/App.tsx web/docs/notes.md SNAPSHOT.md; do
|
||
if [ ! -e "$D/$p" ]; then echo "FAIL: из снимка пропал $p" >&2; fail=1; fi
|
||
done
|
||
[ "$fail" = 0 ] || exit 1
|
||
echo "ok: prepare-contour-snapshot.sh вырезает артефакты агента на любой глубине"
|
||
|
||
# --- web/package.json урезан до зависимостей сборки -------------------------------------------------------------------
|
||
python3 -I - "$D/web/package.json" <<'PY' || fail=1
|
||
import json, sys
|
||
pk = json.load(open(sys.argv[1], encoding="utf-8"))
|
||
errs = []
|
||
if sorted(pk["devDependencies"]) != ["@rsbuild/core", "@rsbuild/plugin-react", "daisyui", "tailwindcss"]:
|
||
errs.append(f"devDependencies: {sorted(pk['devDependencies'])}")
|
||
if pk["devDependencies"]["@rsbuild/core"] != "1.6.7":
|
||
errs.append("версии devDependencies должны сохраниться")
|
||
if pk["dependencies"] != {"antd": "^6.5.0", "react": "^18.3.1"}:
|
||
errs.append("dependencies изменились")
|
||
if pk["optionalDependencies"] != {"raisa-ui": "^1.0.0"}:
|
||
errs.append(f"optionalDependencies: {pk['optionalDependencies']}")
|
||
if sorted(pk["scripts"]) != ["build", "preview", "start"]:
|
||
errs.append(f"scripts: {sorted(pk['scripts'])}")
|
||
if errs:
|
||
print("FAIL:", *errs, sep="\n ", file=sys.stderr)
|
||
sys.exit(1)
|
||
PY
|
||
# Makefile: web-check без biome/tsc, check-full на месте
|
||
if grep -Eq "biome|tsc" "$D/Makefile"; then echo "FAIL: в Makefile снимка остались biome/tsc" >&2; fail=1; fi
|
||
grep -q "cd web && npm install" "$D/Makefile" && grep -q "cd web && npm run build" "$D/Makefile" \
|
||
|| { echo "FAIL: web-check потерял npm install/build" >&2; fail=1; }
|
||
grep -q "^check-full:.*web-check" "$D/Makefile" || { echo "FAIL: check-full не ссылается на web-check" >&2; fail=1; }
|
||
# SNAPSHOT.md перечисляет убранное
|
||
for w in "@biomejs/biome" "eslint" "prettier" "typescript" "vitest" "@types/react" "@biomejs/cli-linux-x64-musl" \
|
||
"test" "lint" "format"; do
|
||
grep -q "$w" "$D/SNAPSHOT.md" || { echo "FAIL: SNAPSHOT.md не упоминает убранное: $w" >&2; fail=1; }
|
||
done
|
||
[ "$fail" = 0 ] || exit 1
|
||
echo "ok: web/package.json, Makefile и SNAPSHOT.md урезаны под сборку"
|
||
|
||
# --- проверка сборки web/ (включена по умолчанию): заглушка npm в PATH -----------------------------------------------
|
||
STUB="$WORK/stub"; mkdir -p "$STUB"
|
||
cat > "$STUB/npm" <<'SH'
|
||
#!/usr/bin/env bash
|
||
# заглушка: install создаёт node_modules, build — dist; NPM_STUB_FAIL=build роняет сборку
|
||
echo "$*" >> "$NPM_STUB_LOG"
|
||
case "$1" in
|
||
install) mkdir -p node_modules; echo "$PWD" >> "$NPM_STUB_LOG.cwd" ;;
|
||
run) if [ "${NPM_STUB_FAIL:-}" = build ]; then echo "Module not found: eslint" >&2; exit 1; fi; mkdir -p dist ;;
|
||
esac
|
||
SH
|
||
chmod +x "$STUB/npm"
|
||
export NPM_STUB_LOG="$WORK/npm.log"
|
||
|
||
# сборка проходит: снимок выдан, node_modules/dist в него не попали, исходный каталог вывода чист
|
||
PATH="$STUB:$PATH" "$SCRIPT" "$R" "$WORK/out-ok" origin/main >/dev/null
|
||
O="$WORK/out-ok/arm-core"
|
||
grep -q "^install" "$NPM_STUB_LOG" && grep -q "^run build" "$NPM_STUB_LOG" \
|
||
|| { echo "FAIL: проверка сборки не вызвала npm install и npm run build" >&2; fail=1; }
|
||
for p in web/node_modules web/dist web/package-lock.json; do
|
||
[ ! -e "$O/$p" ] || { echo "FAIL: в снимке остался $p" >&2; fail=1; }
|
||
done
|
||
[ -e "$O/web/package.json" ] || { echo "FAIL: после проверки сборки пропал web/package.json" >&2; fail=1; }
|
||
|
||
# сборка падает: код 1, понятное сообщение, снимка нет
|
||
rc=0
|
||
PATH="$STUB:$PATH" NPM_STUB_FAIL=build "$SCRIPT" "$R" "$WORK/out-bad" origin/main >"$WORK/bad.out" 2>&1 || rc=$?
|
||
[ "$rc" = 1 ] || { echo "FAIL: при падении сборки ожидался код 1, получен $rc" >&2; fail=1; }
|
||
grep -q "не в разрешённом списке" "$WORK/bad.out" || { echo "FAIL: нет сообщения про разрешённый список" >&2; fail=1; }
|
||
[ ! -e "$WORK/out-bad/arm-core" ] || { echo "FAIL: при падении сборки снимок выдан" >&2; fail=1; }
|
||
|
||
# tabular-parser и scan-gateway фронта не имеют: npm не вызывается
|
||
: > "$NPM_STUB_LOG"
|
||
git init -q --bare "$WORK/tp-origin.git"
|
||
git clone -q "$WORK/tp-origin.git" "$WORK/src/tabular-parser" 2>/dev/null
|
||
T="$WORK/src/tabular-parser"
|
||
printf 'venv:\n\tuv venv api/.venv\n' > "$T/Makefile"
|
||
for p in raisa_config.yml setup/setup.sh setup/pip.conf api/run_server.py api/requirements.txt api/.env.test api/contracts/s.json; do
|
||
mkdir -p "$(dirname "$T/$p")"; echo x > "$T/$p"
|
||
done
|
||
git -C "$T" add -A; git -C "$T" -c user.name=t -c user.email=t@t commit -qm init
|
||
git -C "$T" push -q origin HEAD:main 2>/dev/null; git -C "$T" fetch -q origin
|
||
PATH="$STUB:$PATH" "$SCRIPT" "$T" "$WORK/out-tp" origin/main >/dev/null
|
||
[ ! -s "$NPM_STUB_LOG" ] || { echo "FAIL: для tabular-parser вызван npm" >&2; fail=1; }
|
||
[ "$fail" = 0 ] || exit 1
|
||
echo "ok: проверка сборки web/ — выдаёт снимок без node_modules/dist, при падении снимка нет, у сервисов без фронта не вызывается"
|
||
|
||
# --- копия arm_statements в сервисе: sync-statements.sh --check (Р-42) -------------------------------------------------
|
||
# Синтетический arm-statements и хосты; флаги sync-statements.sh — через SYNC_STATEMENTS_ARGS (реальные репозитории не нужны).
|
||
PY=${STATEMENTS_PY:-python3}
|
||
if ! "$PY" -c 'import tomllib, yaml' 2>/dev/null; then
|
||
echo "нужен Python >= 3.11 с PyYAML: выполните make venv (или задайте STATEMENTS_PY)" >&2
|
||
exit 1
|
||
fi
|
||
export STATEMENTS_PY="$PY"
|
||
export GIT_AUTHOR_NAME=t GIT_AUTHOR_EMAIL=t@t GIT_COMMITTER_NAME=t GIT_COMMITTER_EMAIL=t@t
|
||
SS="$WORK/arm-statements"
|
||
git init -q --bare "$WORK/arm-statements.git"
|
||
git clone -q "$WORK/arm-statements.git" "$SS" 2>/dev/null
|
||
git -C "$SS" checkout -q -b main
|
||
printf '[project]\nname = "arm-statements"\ndependencies = ["PyYAML==6.0.3"]\n' > "$SS/pyproject.toml"
|
||
mkdir -p "$SS/arm_statements"
|
||
echo "# init" > "$SS/arm_statements/__init__.py"
|
||
echo "пакет" > "$SS/README.md"
|
||
mkdir -p "$SS/arm_statements/rules"
|
||
printf 'version: 1\nrepeats: {}\neditions:\n 66n:\n BALANCE: {full: ["1110", "1100"], simplified: ["1100"]}\n' \
|
||
> "$SS/arm_statements/rules/statement_code_orders.yaml"
|
||
git -C "$SS" add -A; git -C "$SS" commit -qm S1; git -C "$SS" push -q origin HEAD:main 2>/dev/null; git -C "$SS" fetch -q origin
|
||
S1=$(git -C "$SS" rev-parse HEAD)
|
||
mkdir -p "$WORK/docs-x/contracts"
|
||
cp "$SS/arm_statements/rules/statement_code_orders.yaml" "$WORK/docs-x/contracts/statement-code-orders.yaml"
|
||
export SYNC_STATEMENTS_ARGS="--source $SS --docs $WORK/docs-x"
|
||
|
||
# mk_service <имя>: сервис с копией пакета на S1 в api/arm_statements (после коммита — origin/main)
|
||
mk_service() {
|
||
local name=$1 d="$WORK/src/$1"
|
||
git init -q --bare "$WORK/$name-o.git"
|
||
git clone -q "$WORK/$name-o.git" "$d" 2>/dev/null
|
||
printf 'venv:\n\tuv venv api/.venv\n' > "$d/Makefile"
|
||
for p in raisa_config.yml setup/setup.sh setup/pip.conf api/run_server.py api/.env.test api/contracts/s.json; do
|
||
mkdir -p "$(dirname "$d/$p")"; echo x > "$d/$p"
|
||
done
|
||
printf 'aiohttp==3.14.3\nPyYAML==6.0.3\n' > "$d/api/requirements.txt"
|
||
git -C "$SS" archive "$S1" arm_statements | tar -x -C "$d/api"
|
||
echo "$S1" > "$d/api/arm_statements/VERSION"
|
||
git -C "$d" add -A; git -C "$d" commit -qm init; git -C "$d" push -q origin HEAD:main 2>/dev/null; git -C "$d" fetch -q origin
|
||
}
|
||
|
||
# хост без api/arm_statements (tabular-parser выше): проверка пропущена молча
|
||
: > "$WORK/tp.err"
|
||
"$SCRIPT" "$T" "$WORK/out-tp2" origin/main >/dev/null 2>"$WORK/tp.err"
|
||
[ ! -s "$WORK/tp.err" ] && ! grep -q arm_statements "$WORK/out-tp2/tabular-parser/SNAPSHOT.md" \
|
||
|| { echo "FAIL: хост без arm_statements: проверка не пропущена молча" >&2; fail=1; }
|
||
|
||
# копия на актуальной версии: тихо, абзаца нет
|
||
mk_service scan-gateway
|
||
G="$WORK/src/scan-gateway"
|
||
"$SCRIPT" "$G" "$WORK/out-g1" origin/main >/dev/null 2>"$WORK/g1.err"
|
||
[ ! -s "$WORK/g1.err" ] && ! grep -q arm_statements "$WORK/out-g1/scan-gateway/SNAPSHOT.md" \
|
||
|| { echo "FAIL: актуальная копия: ожидалась тишина и SNAPSHOT.md без абзаца" >&2; cat "$WORK/g1.err" >&2; fail=1; }
|
||
|
||
# нет источника (код 2): предупреждение, снимок делается
|
||
rc=0
|
||
SYNC_STATEMENTS_ARGS="--source $WORK/nope --docs $WORK/docs-x" "$SCRIPT" "$G" "$WORK/out-g2" origin/main >/dev/null 2>"$WORK/g2.err" || rc=$?
|
||
[ "$rc" = 0 ] && [ -e "$WORK/out-g2/scan-gateway/SNAPSHOT.md" ] && grep -q "arm_statements" "$WORK/g2.err" \
|
||
|| { echo "FAIL: нет источника: ожидалось предупреждение и снимок (rc=$rc)" >&2; fail=1; }
|
||
|
||
# копия отстала (код 10): предупреждение в stderr и абзац в SNAPSHOT.md, снимок делается
|
||
echo "пакет, правка" > "$SS/README.md"
|
||
git -C "$SS" add -A; git -C "$SS" commit -qm S2; git -C "$SS" push -q origin HEAD:main 2>/dev/null; git -C "$SS" fetch -q origin
|
||
rc=0
|
||
"$SCRIPT" "$G" "$WORK/out-g3" origin/main >/dev/null 2>"$WORK/g3.err" || rc=$?
|
||
[ "$rc" = 0 ] && grep -q "arm_statements" "$WORK/g3.err" && grep -q "Копия пакета arm_statements" "$WORK/out-g3/scan-gateway/SNAPSHOT.md" \
|
||
|| { echo "FAIL: копия отстала: ожидались предупреждение и абзац в SNAPSHOT.md (rc=$rc)" >&2; cat "$WORK/g3.err" >&2; fail=1; }
|
||
|
||
# прямая правка копии (код 20): код 1, снимка нет
|
||
echo "# правка" >> "$G/api/arm_statements/__init__.py"
|
||
git -C "$G" add -A; git -C "$G" commit -qm "direct edit"; git -C "$G" push -q origin HEAD:main 2>/dev/null; git -C "$G" fetch -q origin
|
||
rc=0
|
||
"$SCRIPT" "$G" "$WORK/out-g4" origin/main >/dev/null 2>"$WORK/g4.err" || rc=$?
|
||
[ "$rc" = 1 ] && [ ! -e "$WORK/out-g4/scan-gateway" ] && grep -q "arm_statements" "$WORK/g4.err" \
|
||
|| { echo "FAIL: прямая правка копии: ожидался код 1 без снимка (rc=$rc)" >&2; cat "$WORK/g4.err" >&2; fail=1; }
|
||
[ "$fail" = 0 ] || exit 1
|
||
echo "ok: копия arm_statements — пропуск без пакета, тишина при актуальной, предупреждение при отставании, стоп при правке"
|