fix(core): stabilize editor lifecycle, transactional versions, and runtime config

This commit is contained in:
greebo
2026-03-20 12:38:10 +03:00
parent 0f9c2a1cbd
commit 239b32a246
17 changed files with 1224 additions and 457 deletions

View File

@@ -0,0 +1,274 @@
#!/usr/bin/env bash
set -Eeuo pipefail
API_URL="${API_URL:-http://127.0.0.1:9020}"
API_KEY="${API_KEY:-admin-local-dev-key}"
SCHEME_ID="${SCHEME_ID:-82086336d385427f9d56244f9e1dd772}"
TMP_DIR="$(mktemp -d)"
trap 'rm -rf "${TMP_DIR}"' EXIT
log() {
echo
echo "===== $* ====="
}
fail() {
echo
echo "[FAIL] $*" >&2
exit 1
}
request() {
local name="$1"
local method="$2"
local url="$3"
local body="${4:-}"
local expected="${5:-200}"
local body_file="${TMP_DIR}/${name}.body"
local code_file="${TMP_DIR}/${name}.code"
if [[ -n "${body}" ]]; then
curl -sS \
-X "${method}" \
-H "X-API-Key: ${API_KEY}" \
-H "Content-Type: application/json" \
-o "${body_file}" \
-w "%{http_code}" \
--data "${body}" \
"${url}" > "${code_file}"
else
curl -sS \
-X "${method}" \
-H "X-API-Key: ${API_KEY}" \
-o "${body_file}" \
-w "%{http_code}" \
"${url}" > "${code_file}"
fi
local code
code="$(cat "${code_file}")"
echo "[${method}] ${url} -> ${code}"
cat "${body_file}"
echo
if [[ "${code}" != "${expected}" ]]; then
fail "Unexpected HTTP status for ${name}: expected ${expected}, got ${code}"
fi
}
json_get() {
local file="$1"
local expr="$2"
python3 - <<PY
import json
from pathlib import Path
data = json.loads(Path("${file}").read_text())
expr = "${expr}"
value = data
for part in expr.split("."):
if not part:
continue
if part.startswith("[") and part.endswith("]"):
cond = part[1:-1]
try:
if cond.endswith("!=null"):
k = cond[:-6]
value = next(item for item in value if item.get(k) is not None)
elif cond.endswith("==null"):
k = cond[:-6]
value = next(item for item in value if item.get(k) is None)
elif cond == "LAST":
value = value[-1]
else:
value = value[0]
except StopIteration:
value = None
elif part.isdigit():
value = value[int(part)]
else:
value = value[part] if value else None
if value is None:
print("")
elif isinstance(value, bool):
print("true" if value else "false")
else:
print(value)
PY
}
assert_json_eq() {
local file="$1"
local expr="$2"
local expected="$3"
local actual
actual="$(json_get "${file}" "${expr}")"
if [[ "${actual}" != "${expected}" ]]; then
fail "Assertion failed: ${expr} expected '${expected}', got '${actual}'"
fi
echo "[OK] ${expr}=${actual}"
}
extract_current() {
request "current" "GET" "${API_URL}/api/v1/schemes/${SCHEME_ID}/current" "" "200"
CURRENT_VERSION_ID="$(json_get "${TMP_DIR}/current.body" "scheme_version_id")"
CURRENT_STATUS="$(json_get "${TMP_DIR}/current.body" "status")"
echo "CURRENT_VERSION_ID=${CURRENT_VERSION_ID}"
echo "CURRENT_STATUS=${CURRENT_STATUS}"
}
ensure_draft() {
request "ensure_draft" "POST" "${API_URL}/api/v1/schemes/${SCHEME_ID}/draft/ensure" "" "200"
DRAFT_VERSION_ID="$(json_get "${TMP_DIR}/ensure_draft.body" "scheme_version_id")"
DRAFT_CREATED="$(json_get "${TMP_DIR}/ensure_draft.body" "created")"
echo "DRAFT_VERSION_ID=${DRAFT_VERSION_ID}"
echo "DRAFT_CREATED=${DRAFT_CREATED}"
}
read_structure() {
request "draft_structure" "GET" \
"${API_URL}/api/v1/schemes/${SCHEME_ID}/draft/structure?expected_scheme_version_id=${DRAFT_VERSION_ID}" \
"" "200"
SEAT_RECORD_ID="$(json_get "${TMP_DIR}/draft_structure.body" "seats.[seat_id!=null].seat_record_id")"
SEAT_ID="$(json_get "${TMP_DIR}/draft_structure.body" "seats.[seat_id!=null].seat_id")"
ORIG_SEAT_NUMBER="$(json_get "${TMP_DIR}/draft_structure.body" "seats.[seat_id!=null].seat_number")"
echo "SEAT_RECORD_ID=${SEAT_RECORD_ID}"
echo "SEAT_ID=${SEAT_ID}"
echo "ORIG_SEAT_NUMBER=${ORIG_SEAT_NUMBER}"
}
check_read_models() {
request "draft_summary" "GET" \
"${API_URL}/api/v1/schemes/${SCHEME_ID}/draft/summary?expected_scheme_version_id=${DRAFT_VERSION_ID}" \
"" "200"
request "draft_validation" "GET" \
"${API_URL}/api/v1/schemes/${SCHEME_ID}/draft/validation?expected_scheme_version_id=${DRAFT_VERSION_ID}" \
"" "200"
request "draft_compare_preview" "GET" \
"${API_URL}/api/v1/schemes/${SCHEME_ID}/draft/compare-preview?expected_scheme_version_id=${DRAFT_VERSION_ID}" \
"" "200"
assert_json_eq "${TMP_DIR}/draft_summary.body" "scheme_version_id" "${DRAFT_VERSION_ID}"
assert_json_eq "${TMP_DIR}/draft_validation.body" "scheme_version_id" "${DRAFT_VERSION_ID}"
assert_json_eq "${TMP_DIR}/draft_compare_preview.body" "draft_scheme_version_id" "${DRAFT_VERSION_ID}"
}
log "health"
curl -fsS "${API_URL}/healthz" >/dev/null || fail "healthz failed"
echo "[OK] healthz"
log "current + ensure draft"
extract_current
ensure_draft
read_structure
check_read_models
STAMP="$(date +%s)"
TEST_SECTOR_ID="reg-sector-${STAMP}"
TEST_GROUP_ID="reg-group-${STAMP}"
TEST_SECTOR_ELEMENT_ID="reg-sector-element-${STAMP}"
TEST_GROUP_ELEMENT_ID="reg-group-element-${STAMP}"
log "create sector"
request "create_sector" "POST" \
"${API_URL}/api/v1/schemes/${SCHEME_ID}/draft/sectors?expected_scheme_version_id=${DRAFT_VERSION_ID}" \
"{\"element_id\":\"${TEST_SECTOR_ELEMENT_ID}\",\"sector_id\":\"${TEST_SECTOR_ID}\",\"name\":\"${TEST_SECTOR_ID}\"}" \
"200"
CREATE_SECTOR_RECORD_ID="$(json_get "${TMP_DIR}/create_sector.body" "sector_record_id")"
echo "CREATE_SECTOR_RECORD_ID=${CREATE_SECTOR_RECORD_ID}"
log "create group"
request "create_group" "POST" \
"${API_URL}/api/v1/schemes/${SCHEME_ID}/draft/groups?expected_scheme_version_id=${DRAFT_VERSION_ID}" \
"{\"element_id\":\"${TEST_GROUP_ELEMENT_ID}\",\"group_id\":\"${TEST_GROUP_ID}\",\"name\":\"${TEST_GROUP_ID}\"}" \
"200"
CREATE_GROUP_RECORD_ID="$(json_get "${TMP_DIR}/create_group.body" "group_record_id")"
echo "CREATE_GROUP_RECORD_ID=${CREATE_GROUP_RECORD_ID}"
log "patch seat -> bind to new group"
request "patch_seat_group" "PATCH" \
"${API_URL}/api/v1/schemes/${SCHEME_ID}/draft/seats/records/${SEAT_RECORD_ID}?expected_scheme_version_id=${DRAFT_VERSION_ID}" \
"{\"group_id\":\"${TEST_GROUP_ID}\"}" \
"200"
log "verify seat after patch"
request "seat_after_patch" "GET" \
"${API_URL}/api/v1/schemes/${SCHEME_ID}/draft/seats/records/${SEAT_RECORD_ID}?expected_scheme_version_id=${DRAFT_VERSION_ID}" \
"" "200"
assert_json_eq "${TMP_DIR}/seat_after_patch.body" "group_id" "${TEST_GROUP_ID}"
assert_json_eq "${TMP_DIR}/seat_after_patch.body" "seat_number" "${ORIG_SEAT_NUMBER}"
log "patch group name"
request "patch_group" "PATCH" \
"${API_URL}/api/v1/schemes/${SCHEME_ID}/draft/groups/records/${CREATE_GROUP_RECORD_ID}?expected_scheme_version_id=${DRAFT_VERSION_ID}" \
"{\"name\":\"${TEST_GROUP_ID}-updated\"}" \
"200"
log "patch sector name"
request "patch_sector" "PATCH" \
"${API_URL}/api/v1/schemes/${SCHEME_ID}/draft/sectors/records/${CREATE_SECTOR_RECORD_ID}?expected_scheme_version_id=${DRAFT_VERSION_ID}" \
"{\"name\":\"${TEST_SECTOR_ID}-updated\"}" \
"200"
log "verify sector after patch"
request "sector_after_patch" "GET" \
"${API_URL}/api/v1/schemes/${SCHEME_ID}/draft/sectors/records/${CREATE_SECTOR_RECORD_ID}?expected_scheme_version_id=${DRAFT_VERSION_ID}" \
"" "200"
assert_json_eq "${TMP_DIR}/sector_after_patch.body" "name" "${TEST_SECTOR_ID}-updated"
assert_json_eq "${TMP_DIR}/sector_after_patch.body" "sector_id" "${TEST_SECTOR_ID}"
log "bulk seat update validation path"
request "bulk_seats" "POST" \
"${API_URL}/api/v1/schemes/${SCHEME_ID}/draft/seats/bulk?expected_scheme_version_id=${DRAFT_VERSION_ID}" \
"{\"items\":[{\"seat_record_id\":\"${SEAT_RECORD_ID}\",\"row_label\":\"ZZ\",\"seat_number\":\"999\"}]}" \
"200"
log "verify seat after bulk patch"
request "seat_after_bulk" "GET" \
"${API_URL}/api/v1/schemes/${SCHEME_ID}/draft/seats/records/${SEAT_RECORD_ID}?expected_scheme_version_id=${DRAFT_VERSION_ID}" \
"" "200"
assert_json_eq "${TMP_DIR}/seat_after_bulk.body" "row_label" "ZZ"
assert_json_eq "${TMP_DIR}/seat_after_bulk.body" "seat_number" "999"
log "typed error: duplicate sector id"
request "duplicate_sector" "POST" \
"${API_URL}/api/v1/schemes/${SCHEME_ID}/draft/sectors?expected_scheme_version_id=${DRAFT_VERSION_ID}" \
"{\"element_id\":\"dup-${TEST_SECTOR_ELEMENT_ID}\",\"sector_id\":\"${TEST_SECTOR_ID}\",\"name\":\"dup\"}" \
"422"
log "typed error: duplicate group id"
request "duplicate_group" "POST" \
"${API_URL}/api/v1/schemes/${SCHEME_ID}/draft/groups?expected_scheme_version_id=${DRAFT_VERSION_ID}" \
"{\"element_id\":\"dup-${TEST_GROUP_ELEMENT_ID}\",\"group_id\":\"${TEST_GROUP_ID}\",\"name\":\"dup\"}" \
"422"
log "typed error: stale draft version"
request "stale_patch" "PATCH" \
"${API_URL}/api/v1/schemes/${SCHEME_ID}/draft/seats/records/${SEAT_RECORD_ID}?expected_scheme_version_id=deadbeefdeadbeefdeadbeefdeadbeef" \
"{\"row_label\":\"STALE\"}" \
"409"
log "typed error: remap preview without filters"
request "remap_preview_invalid" "POST" \
"${API_URL}/api/v1/schemes/${SCHEME_ID}/draft/remap/preview?expected_scheme_version_id=${DRAFT_VERSION_ID}" \
"{}" \
"422"
log "repair references"
request "repair_refs" "POST" \
"${API_URL}/api/v1/schemes/${SCHEME_ID}/draft/repair-references?expected_scheme_version_id=${DRAFT_VERSION_ID}" \
"{}" \
"200"
log "post-mutation read models"
check_read_models
log "done"
echo "[OK] editor mutation regression completed successfully"