f512d94c74
git-subtree-dir: clients/ds-app git-subtree-mainline:a24e3e6f22git-subtree-split:5206cf3b0c
81 lines
3.8 KiB
Bash
Executable File
81 lines
3.8 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# contract-check.sh — S2-Ff: 라이브 엔드포인트 ↔ 동결 fixture *모양* 드리프트 감지 (비차단 runbook).
|
|
#
|
|
# 라이브 재호출 → 키/타입 diff(LLM 값은 무시) → 드리프트면 비0 exit + fixture-update 안내.
|
|
# PR 게이트 아님 — 수동/Tailscale-CI 트리거([[feedback_pr_gate_vs_runbook_separation]]).
|
|
# fixture 는 동결·불변이며, 이 도구가 감지한 드리프트가 **재캡처 PR 의 유일 합법 트리거**(S2-0d).
|
|
#
|
|
# exit: 0 = 드리프트 없음(체크된 것 중) · 1 = breaking 드리프트 · 2 = 아무것도 체크 못함(전부 도달불가)
|
|
# env override: AIFABRIC_LOCALMLX_URL, AIFABRIC_DS_URL
|
|
#
|
|
# 스킵은 항상 *가시적*으로 출력(silent green 금지, [[feedback_silent_skip_accumulation]]).
|
|
set -uo pipefail # NOT -e: 개별 체크 실패를 모아 보고
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
FIX="$SCRIPT_DIR/fixtures"
|
|
PY="$(command -v python3 || echo /usr/bin/python3)"
|
|
LLM_URL="${AIFABRIC_LOCALMLX_URL:-http://100.76.254.116:8890}"
|
|
DS_URL="${AIFABRIC_DS_URL:-https://document.hyungi.net/api}"
|
|
TMP="$(mktemp -d)"; trap 'rm -rf "$TMP"' EXIT
|
|
|
|
breaking=0
|
|
checked=0
|
|
|
|
echo "== S2-Ff contract drift check =="
|
|
echo " llm-router: $LLM_URL"
|
|
echo " DS: $DS_URL"
|
|
|
|
# --- 1) llm-router /v1/chat/completions (S2 fixture) ---
|
|
echo
|
|
echo "[1] llm-router /v1/chat/completions ↔ llm-router-chat.response.json"
|
|
# 동결 request fixture 에서 _meta 떼고 그대로 wire body 로 POST(call-shape 도 함께 검증).
|
|
"$PY" -c 'import json,sys; o=json.load(open(sys.argv[1],encoding="utf-8")); o.pop("_meta",None); json.dump(o,open(sys.argv[2],"w"))' \
|
|
"$FIX/llm-router-chat.request.json" "$TMP/req.json" 2>"$TMP/strip_err" || { echo " SKIP — request fixture 처리 실패: $(head -c160 "$TMP/strip_err")"; }
|
|
if [ -s "$TMP/req.json" ]; then
|
|
code="$(curl -sS -m 60 -o "$TMP/llm_live.json" -w '%{http_code}' \
|
|
-H 'Content-Type: application/json' --data @"$TMP/req.json" \
|
|
"$LLM_URL/v1/chat/completions" 2>"$TMP/llm_err" || true)"
|
|
if [ "$code" = "200" ]; then
|
|
checked=$((checked + 1))
|
|
if "$PY" "$SCRIPT_DIR/shape_diff.py" "$FIX/llm-router-chat.response.json" "$TMP/llm_live.json"; then
|
|
echo " PASS — shape 일치"
|
|
else
|
|
echo " >> 드리프트: llm-router-chat.{request,response}.json 재캡처 PR 필요"
|
|
breaking=1
|
|
fi
|
|
else
|
|
echo " SKIP — 도달 불가/비200 (http=${code:-curlfail}; 맥미니 offline?) $(head -c120 "$TMP/llm_err" 2>/dev/null)"
|
|
fi
|
|
fi
|
|
|
|
# --- 2) DS /search/ask (S1 fixture, best-effort: 인증 필요할 수 있음) ---
|
|
echo
|
|
echo "[2] DS /search/ask ↔ ask.json (best-effort)"
|
|
code="$(curl -sS -m 30 -G -o "$TMP/ds_live.json" -w '%{http_code}' \
|
|
--data-urlencode 'q=충격시험은 언제 면제되나' --data-urlencode 'backend=mac-mini-default' \
|
|
"$DS_URL/search/ask" 2>"$TMP/ds_err" || true)"
|
|
case "$code" in
|
|
200)
|
|
checked=$((checked + 1))
|
|
if "$PY" "$SCRIPT_DIR/shape_diff.py" "$FIX/ask.json" "$TMP/ds_live.json"; then
|
|
echo " PASS — shape 일치"
|
|
else
|
|
echo " >> 드리프트: ask.json(S1) 재캡처 검토"
|
|
breaking=1
|
|
fi ;;
|
|
401|403) echo " SKIP — 인증 필요(JWT). DS ask 체크는 토큰 주입 시에만(S1 도메인)." ;;
|
|
*) echo " SKIP — 도달 불가/비200 (http=${code:-curlfail})." ;;
|
|
esac
|
|
|
|
echo
|
|
if [ "$breaking" -ne 0 ]; then
|
|
echo "RESULT: DRIFT 발견 — 위 fixture 재캡처 PR 필요(S2-0d 의 유일 합법 fixture 변경 경로)."
|
|
exit 1
|
|
elif [ "$checked" -eq 0 ]; then
|
|
echo "RESULT: 체크한 엔드포인트 0건(전부 도달 불가) — 드리프트 판정 불가. (green 아님 → exit 2)"
|
|
exit 2
|
|
else
|
|
echo "RESULT: OK — 체크 ${checked}건 모두 shape 일치, 드리프트 없음."
|
|
exit 0
|
|
fi
|