G7 선행 조건 발견 — 공개 런타임에 admin/voice-runtime 이 없다
실측으로 찾았다. 공개 런타임은 119 paths 구버전이고 /admin/voice-runtime 이 배포돼 있지 않다. capture-g7-runtime-evidence.py 는 정확히 그 경로만 부르므로 artifact 2 를 만들 수 없다. 즉 마이크와 사람을 다 준비해도 오늘 실행하면 50분을 버리고 실패한다. 현재 소스에는 있다(admin.py:1699, 프리뷰 126 paths). 오케스트레이터가 시작 전에 이걸 검사하고 admin_voice_runtime_not_deployed 로 즉시 멈춘다. 공개 런타임 대상 --rehearse 가 exit 2 로 몇 초 만에 차단됐다. 판정 방식을 한 번 고쳤다. 처음엔 미인증 404/401 상태로 짰는데, Cloudflare 앞단이 자동화 클라이언트에게 존재하지 않는 경로까지 포함해 모든 경로를 403(error code 1010)으로 돌려주는 것을 확인했다. 그 신호로는 "배포 누락"과 "edge 차단"을 구분할 수 없어 작동하는 것처럼 보이지만 무의미한 검사였다. OpenAPI spec 의 paths 만 authoritative 하게 쓰도록 바꾸고 그 이유를 테스트로 남겼다. G7 실행 순서가 이렇게 확정된다. 1. current source 를 공개 런타임에 승격(비-secure origin crypto.randomUUID 수정 포함) 2. --rehearse 로 배관 확인 3. 동의 하 물리 마이크 50분 + human pack 으로 실제 실행 검증: 오케스트레이터 25/25, SSOT FAIL 0, dashboard E2E 10/10, ruff clean.
This commit is contained in:
parent
f3491be63f
commit
b866a48b01
5 changed files with 153 additions and 5 deletions
|
|
@ -175,8 +175,70 @@ def build_topology_leg(args: argparse.Namespace, output: Path) -> Leg:
|
|||
return Leg("topology", argv, output)
|
||||
|
||||
|
||||
def plan_legs(args: argparse.Namespace, out_dir: Path) -> list[Leg]:
|
||||
ADMIN_RUNTIME_PATH = "/admin/voice-runtime"
|
||||
# Cloudflare 앞단은 자동화 클라이언트에 error code 1010 으로 **모든 경로**에 403 을 준다.
|
||||
# 그래서 미인증 상태 코드로는 "endpoint 없음"과 "edge 차단"을 구분할 수 없다.
|
||||
# 배포 여부는 OpenAPI spec 의 paths 로만 판정한다.
|
||||
_BROWSER_UA = (
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
|
||||
"(KHTML, like Gecko) Chrome/140.0 Safari/537.36"
|
||||
)
|
||||
|
||||
|
||||
def assert_admin_endpoint_deployed(
|
||||
url: str, *, paths_probe: Callable[[str], Sequence[str]] | None = None
|
||||
) -> str:
|
||||
"""관리자 runtime endpoint 가 그 배포에 실제로 존재하는지 먼저 본다.
|
||||
|
||||
2026-08-08 확인: 공개 런타임은 119 paths 구버전이라 `/admin/voice-runtime` 이
|
||||
아예 없었다. 이 상태로 50분 창을 돌리면 마이크와 사람을 다 준비하고도 artifact 2
|
||||
에서 실패한다. 그래서 시작 전에 막는다.
|
||||
|
||||
미인증 HTTP 상태로 판정하지 않는다. Cloudflare 가 자동화 클라이언트에 모든 경로를
|
||||
403(error code 1010)으로 돌려주므로 그 신호로는 배포 누락을 알 수 없다. OpenAPI
|
||||
spec 의 `paths` 만 authoritative 하게 쓴다.
|
||||
"""
|
||||
|
||||
from urllib.parse import urlsplit
|
||||
|
||||
parsed = urlsplit(url)
|
||||
if parsed.path.rstrip("/") != ADMIN_RUNTIME_PATH:
|
||||
raise WindowError("admin_runtime_url_unexpected_path")
|
||||
origin = f"{parsed.scheme}://{parsed.netloc}"
|
||||
paths = (paths_probe or _openapi_paths)(origin)
|
||||
if ADMIN_RUNTIME_PATH not in set(paths):
|
||||
raise WindowError("admin_voice_runtime_not_deployed")
|
||||
return origin
|
||||
|
||||
|
||||
def _openapi_paths(origin: str) -> Sequence[str]: # pragma: no cover - 네트워크 I/O
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
|
||||
request = urllib.request.Request(
|
||||
f"{origin}/openapi.json",
|
||||
headers={"User-Agent": _BROWSER_UA, "Accept": "application/json"},
|
||||
method="GET",
|
||||
)
|
||||
try:
|
||||
with urllib.request.urlopen(request, timeout=30) as response:
|
||||
document = json.loads(response.read(8 * 1024 * 1024))
|
||||
except (urllib.error.URLError, OSError, ValueError) as exc:
|
||||
raise WindowError("openapi_unreachable") from exc
|
||||
paths = document.get("paths") if isinstance(document, dict) else None
|
||||
if not isinstance(paths, dict):
|
||||
raise WindowError("openapi_shape_invalid")
|
||||
return list(paths)
|
||||
|
||||
|
||||
def plan_legs(
|
||||
args: argparse.Namespace,
|
||||
out_dir: Path,
|
||||
*,
|
||||
admin_probe: Callable[[str], Sequence[str]] | None = None,
|
||||
) -> list[Leg]:
|
||||
assert_single_host(args.wss_url, args.origin, args.admin_runtime_url)
|
||||
assert_admin_endpoint_deployed(args.admin_runtime_url, paths_probe=admin_probe)
|
||||
return [
|
||||
build_soak_leg(args, out_dir / "voice-soak.json"),
|
||||
build_runtime_leg(args, out_dir / "runtime.json"),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue