Files
agent-pet-companion/script/validate_real_app_server.sh

493 lines
21 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
TMP_DIR="$(mktemp -d "${TMPDIR:-/tmp}/apc-real-app-server.XXXXXX")"
REAL_USER_HOME="${HOME:-}"
. "$ROOT_DIR/script/validation_helpers.sh"
apc_use_isolated_home "$TMP_DIR"
JOB_ID=""
LIFECYCLE_JOB_ID=""
PETCORE_PID=""
ARTIFACTS_PRESERVED=0
GENERATION_POLL_SECONDS="${APC_REAL_APP_SERVER_POLL_SECONDS:-0.5}"
GENERATION_POLL_COUNT="${APC_REAL_APP_SERVER_POLL_COUNT:-45000}"
ARTIFACT_CHECKPOINT_EVERY="${APC_REAL_APP_SERVER_ARTIFACT_CHECKPOINT_EVERY:-600}"
INPUT_WAIT_SECONDS="${APC_REAL_APP_SERVER_INPUT_WAIT_SECONDS:-0}"
checkpoint_artifacts() {
if [[ -z "${APC_REAL_APP_SERVER_ARTIFACT_DIR:-}" ]]; then
return 0
fi
local artifact_dir="$APC_REAL_APP_SERVER_ARTIFACT_DIR"
mkdir -p "$artifact_dir"
if [[ -n "$JOB_ID" && -d "$TMP_DIR/home/generation-jobs/$JOB_ID" ]]; then
ditto "$TMP_DIR/home/generation-jobs/$JOB_ID" "$artifact_dir/job"
APC_HOME="$TMP_DIR/home" \
"$ROOT_DIR/target/debug/petcore-cli" generation status \
--job-id "$JOB_ID" --include-messages \
>"$artifact_dir/in-progress-status.json" 2>/dev/null || true
fi
if [[ -n "$LIFECYCLE_JOB_ID" && -d "$TMP_DIR/home/generation-jobs/$LIFECYCLE_JOB_ID" ]]; then
ditto "$TMP_DIR/home/generation-jobs/$LIFECYCLE_JOB_ID" "$artifact_dir/lifecycle-job"
APC_HOME="$TMP_DIR/home" \
"$ROOT_DIR/target/debug/petcore-cli" generation status \
--job-id "$LIFECYCLE_JOB_ID" --include-messages \
>"$artifact_dir/lifecycle-in-progress-status.json" 2>/dev/null || true
fi
if [[ -s "$TMP_DIR/probe.err" ]]; then
cp "$TMP_DIR/probe.err" "$artifact_dir/probe.err"
fi
}
preserve_artifacts() {
if [[ -z "${APC_REAL_APP_SERVER_ARTIFACT_DIR:-}" ]] \
|| [[ "$ARTIFACTS_PRESERVED" == "1" ]]; then
return 0
fi
checkpoint_artifacts
ARTIFACTS_PRESERVED=1
printf 'Preserved real App Server validation artifacts: %s\n' \
"$APC_REAL_APP_SERVER_ARTIFACT_DIR"
}
cleanup() {
local status="$?"
if ((status != 0)); then
preserve_artifacts || true
fi
if [[ -n "$PETCORE_PID" ]]; then
kill "$PETCORE_PID" >/dev/null 2>&1 || true
wait "$PETCORE_PID" >/dev/null 2>&1 || true
fi
rm -rf "$TMP_DIR"
return "$status"
}
trap cleanup EXIT
truthy() {
case "${1:-}" in
1|true|TRUE|yes|YES) return 0 ;;
*) return 1 ;;
esac
}
disabled() {
case "${1:-}" in
0|false|FALSE|no|NO) return 0 ;;
*) return 1 ;;
esac
}
skip() {
printf 'Skipped real Codex App Server validation: %s\n' "$1"
exit 0
}
fail() {
printf 'Real Codex App Server validation failed: %s\n' "$1" >&2
if [[ -n "${PROBE:-}" ]]; then
printf '\n--- codex app-server probe ---\n%s\n' "$PROBE" >&2
fi
if [[ -s "$TMP_DIR/probe.err" ]]; then
printf '\n--- probe stderr ---\n' >&2
cat "$TMP_DIR/probe.err" >&2
fi
if [[ -n "$JOB_ID" ]]; then
printf '\n--- generation status ---\n' >&2
APC_HOME="$TMP_DIR/home" "$ROOT_DIR/target/debug/petcore-cli" generation status \
--job-id "$JOB_ID" --include-messages >&2 || true
fi
printf '\nAction: set CODEX_APP_SERVER_CMD to a real stdio Codex App Server command, or install a codex CLI that supports `codex app-server --stdio`. This validation never reads auth/token/cookie/API key files directly.\n' >&2
exit 1
}
assert_json() {
local json="$1"
local expr="$2"
JSON="$json" python3 - "$expr" <<'PY'
import json
import os
import sys
data = json.loads(os.environ["JSON"])
expr = sys.argv[1]
allowed = {"__builtins__": {}, "data": data, "any": any, "all": all, "len": len, "set": set}
if not eval(expr, allowed, {}):
raise SystemExit(f"assertion failed: {expr}\n{json.dumps(data, ensure_ascii=False, indent=2)}")
PY
}
app_server_skip_reason() {
local setting="${APC_VALIDATE_REAL_APP_SERVER:-0}"
if disabled "$setting"; then
printf 'APC_VALIDATE_REAL_APP_SERVER=%s explicitly disables real App Server validation' "$setting"
return 0
fi
if ! truthy "$setting"; then
printf 'APC_VALIDATE_REAL_APP_SERVER=%s is not an explicit opt-in; use 1 to run the real App Server validation' "$setting"
return 0
fi
if [[ -n "${CODEX_APP_SERVER_CMD:-}" ]]; then
return 1
fi
if command -v codex >/dev/null 2>&1 && codex app-server --help >/dev/null 2>&1; then
return 1
fi
if truthy "$setting"; then
return 1
fi
if ! command -v codex >/dev/null 2>&1; then
printf 'CODEX_APP_SERVER_CMD is unset and codex CLI was not found'
else
printf 'CODEX_APP_SERVER_CMD is unset and codex CLI does not expose a working app-server command'
fi
return 0
}
if reason="$(app_server_skip_reason)"; then
skip "$reason"
fi
python3 - "$GENERATION_POLL_SECONDS" "$GENERATION_POLL_COUNT" \
"$ARTIFACT_CHECKPOINT_EVERY" "$INPUT_WAIT_SECONDS" <<'PY'
import sys
try:
interval = float(sys.argv[1])
count = int(sys.argv[2])
checkpoint_every = int(sys.argv[3])
input_wait = float(sys.argv[4])
except ValueError as error:
raise SystemExit(
"real App Server validation poll settings must be numeric"
) from error
if interval <= 0 or count <= 0 or checkpoint_every <= 0 or input_wait < 0:
raise SystemExit(
"real App Server validation poll settings must all be positive"
)
if interval * count < 22_500:
raise SystemExit(
"real App Server validation must allow at least 375 minutes so its "
"acceptance window covers a task lasting more than six hours, "
"including final import and validation"
)
PY
if truthy "${APC_VALIDATE_REAL_APP_SERVER:-0}" && [[ -z "${CODEX_APP_SERVER_CMD:-}" ]]; then
if ! command -v codex >/dev/null 2>&1; then
fail "APC_VALIDATE_REAL_APP_SERVER is forced but CODEX_APP_SERVER_CMD is unset and codex CLI was not found"
fi
if ! codex app-server --help >/dev/null 2>&1; then
fail "APC_VALIDATE_REAL_APP_SERVER is forced but CODEX_APP_SERVER_CMD is unset and codex app-server is unavailable"
fi
fi
cd "$ROOT_DIR"
cargo build --workspace >/dev/null
start_petcore() {
rm -f "$TMP_DIR/ready"
(
# PetCore storage and connector paths remain isolated. HOME is restored only
# for the explicitly opted-in Codex subprocess so it can use its own normal
# login context; this validator never reads authentication files itself.
HOME="$REAL_USER_HOME" \
APC_HOME="$TMP_DIR/home" \
APC_AGENT_CONFIG_HOME="$TMP_DIR/agent-home" \
APC_CONNECTOR_CLI_PATH="$ROOT_DIR/target/debug/petcore-cli" \
APC_ALLOW_LOCAL_PET_STUDIO_FALLBACK=0 \
APC_REQUIRE_SKILL_FULL_SOURCE="${APC_REQUIRE_SKILL_FULL_SOURCE:-1}" \
APC_REQUIRE_EXTERNAL_SKILL_SOURCE="${APC_REQUIRE_EXTERNAL_SKILL_SOURCE:-1}" \
"$ROOT_DIR/target/debug/petcore" serve --ready-file "$TMP_DIR/ready"
) &
PETCORE_PID="$!"
for _ in {1..100}; do
[[ -f "$TMP_DIR/ready" ]] && return 0
sleep 0.05
done
fail "PetCore did not report ready"
}
stop_petcore() {
if [[ -z "$PETCORE_PID" ]]; then
return 0
fi
kill "$PETCORE_PID" >/dev/null 2>&1 || true
wait "$PETCORE_PID" >/dev/null 2>&1 || true
PETCORE_PID=""
}
start_petcore
if ! PROBE="$(APC_HOME="$TMP_DIR/home" "$ROOT_DIR/target/debug/petcore-cli" codex 2>"$TMP_DIR/probe.err")"; then
fail "petcore-cli codex probe command failed"
fi
assert_json "$PROBE" 'data["initialized"] is True and data["transport"] == "stdio"' \
|| fail "Codex App Server probe did not initialize"
FORM='{"description":"真实 Codex App Server 验收用的小型半写实桌宠,透明背景,动作简洁。主体是一只蓝白云朵猫,圆眼、轻盈尾巴。开始制作前必须通过 App Server 原生 requestUserInput 询问一次尾巴发光颜色,并在收到回复后继续;随后返回完整九动作 V3 设计 brief。不要读取秘密或无关项目文件。","style":"半写实","quality":"standard","reference_images":[]}'
JOB_JSON="$(APC_HOME="$TMP_DIR/home" "$ROOT_DIR/target/debug/petcore-cli" generation start --form-json "$FORM")"
JOB_ID="$(JSON="$JOB_JSON" python3 - <<'PY'
import json
import os
print(json.loads(os.environ["JSON"])["job_id"])
PY
)"
REPLIED_TO_INPUT_REQUEST=0
for ((attempt = 0; attempt < GENERATION_POLL_COUNT; attempt += 1)); do
STATUS="$(APC_HOME="$TMP_DIR/home" "$ROOT_DIR/target/debug/petcore-cli" generation status --job-id "$JOB_ID" --include-messages)"
if ((attempt % ARTIFACT_CHECKPOINT_EVERY == 0)); then
checkpoint_artifacts
fi
JOB_STATUS="$(JSON="$STATUS" python3 - <<'PY'
import json
import os
print(json.loads(os.environ["JSON"]).get("status", ""))
PY
)"
case "$JOB_STATUS" in
completed) break ;;
failed) fail "generation job entered failed status" ;;
esac
if [[ "$REPLIED_TO_INPUT_REQUEST" == "0" ]] && grep -Eq '"kind"[[:space:]]*:[[:space:]]*"input_request"' <<<"$STATUS"; then
if [[ "$INPUT_WAIT_SECONDS" != "0" && "$INPUT_WAIT_SECONDS" != "0.0" ]]; then
printf 'Holding the real task in waiting-for-user for %s seconds.\n' "$INPUT_WAIT_SECONDS"
sleep "$INPUT_WAIT_SECONDS"
fi
APC_HOME="$TMP_DIR/home" "$ROOT_DIR/target/debug/petcore-cli" generation reply \
--job-id "$JOB_ID" \
--content "尾巴使用柔和青蓝色发光;待机时呼吸漂浮,工具执行时短暂增强。" \
--request-id "real-input-$JOB_ID" >/dev/null
REPLIED_TO_INPUT_REQUEST=1
fi
sleep "$GENERATION_POLL_SECONDS"
done
FINAL_STATUS="$(APC_HOME="$TMP_DIR/home" "$ROOT_DIR/target/debug/petcore-cli" generation status --job-id "$JOB_ID" --include-messages)"
assert_json "$FINAL_STATUS" 'data["status"] == "completed"' \
|| fail "generation job did not complete"
[[ "$REPLIED_TO_INPUT_REQUEST" == "1" ]] \
|| fail "real App Server did not produce and resume from a native input request"
if [[ "$INPUT_WAIT_SECONDS" != "0" && "$INPUT_WAIT_SECONDS" != "0.0" ]]; then
INPUT_WAIT_SECONDS="$INPUT_WAIT_SECONDS" JSON="$FINAL_STATUS" python3 - <<'PY' \
|| fail "real task duration did not include the waiting-for-user soak interval"
import datetime
import json
import os
data = json.loads(os.environ["JSON"])["lifecycle"]
start = datetime.datetime.fromisoformat(data["started_at"].replace("Z", "+00:00"))
end = datetime.datetime.fromisoformat(data["ended_at"].replace("Z", "+00:00"))
assert (end - start).total_seconds() >= float(os.environ["INPUT_WAIT_SECONDS"])
PY
fi
assert_json "$FINAL_STATUS" 'data["app_server"]["initialized"] is True and data["app_server"]["started"] is True and data["app_server"]["turn_started"] is True' \
|| fail "App Server session did not initialize/start a Pet Studio turn"
assert_json "$FINAL_STATUS" 'data["app_server"]["thread_id"] is not None and data["app_server"]["turn_id"] is not None' \
|| fail "App Server session is missing thread_id or turn_id"
assert_json "$FINAL_STATUS" 'data["app_server"]["completed"] is True or data["artifacts"]["petpack_source"]["manifest_exists"] is True' \
|| fail "App Server turn did not complete and no Skill full-source artifact was written"
assert_json "$FINAL_STATUS" 'data["app_server"]["ai_brief"]["states_count"] == 9 or data["artifacts"]["petpack_source"]["states_count"] == 9' \
|| fail "real App Server output did not prove nine fixed actions"
assert_json "$FINAL_STATUS" 'data["artifacts"]["petpack_source"]["manifest_exists"] is True and data["artifacts"]["petpack_source"]["validation_ok"] is True and len(data["artifacts"]["petpack_files"]) >= 1' \
|| fail "generation artifacts are incomplete"
assert_json "$FINAL_STATUS" 'data["artifacts"]["petpack_source"]["source_metadata"]["generator"] in ["codex-app-server-skill", "codex-app-server-brief-petpack-v3"] and data["artifacts"]["petpack_source"]["source_metadata"]["provenance"] in ["skill-full-source", "codex_app_server_brief"] and data["artifacts"]["petpack_source"]["skill_session"]["exists"] is True' \
|| fail "real App Server generation did not preserve App Server provenance"
if truthy "${APC_REQUIRE_SKILL_FULL_SOURCE:-1}"; then
assert_json "$FINAL_STATUS" 'data["artifacts"]["petpack_source"]["generation_mode"] == "skill_full_source" and data["artifacts"]["petpack_source"]["real_skill_source"] is True and data["artifacts"]["petpack_source"]["fallback_used"] is False and data["artifacts"]["petpack_source"]["sample_output"] is False and data["artifacts"]["petpack_source"]["repaired_validation"] is False and data["artifacts"]["petpack_source"]["materialized_by_petcore"] is False' \
|| fail "strict full-source mode used fallback/sample/repaired artifacts"
assert_json "$FINAL_STATUS" 'data["artifacts"]["petpack_source"].get("materialized_by_cli") is False' \
|| fail "strict full-source mode accepted a CLI-materialized petpack-source"
assert_json "$FINAL_STATUS" 'data["artifacts"]["petpack_source"]["source_metadata"]["generator"] == "codex-app-server-skill" and data["artifacts"]["petpack_source"]["source_metadata"]["provenance"] == "skill-full-source"' \
|| fail "strict full-source mode did not produce trusted Skill provenance"
fi
if truthy "${APC_REQUIRE_EXTERNAL_SKILL_SOURCE:-1}"; then
assert_json "$FINAL_STATUS" 'data["artifacts"]["petpack_source"]["generation_mode"] == "skill_full_source" and data["artifacts"]["petpack_source"]["real_skill_source"] is True and data["artifacts"]["petpack_source"].get("materializer") is None and (data["artifacts"]["petpack_source"].get("skill_helper") is None or not data["artifacts"]["petpack_source"]["skill_helper"].startswith("agent-pet-studio-preview-helper"))' \
|| fail "external full-source mode accepted preview or internally materialized output"
fi
SOURCE_DIR="$TMP_DIR/home/generation-jobs/$JOB_ID/petpack-source"
if truthy "${APC_REQUIRE_EXTERNAL_SKILL_SOURCE:-1}"; then
SOURCE_DIR="$SOURCE_DIR" python3 - <<'PY' || fail "external full-source visual semantics were not proven"
import hashlib
import json
import os
from pathlib import Path
root = Path(os.environ["SOURCE_DIR"])
metadata = json.loads((root / "source/source.json").read_text(encoding="utf-8"))
manifest = json.loads((root / "manifest.json").read_text(encoding="utf-8"))
motion_report = json.loads(
(root.parent / ".agent-pet-maker/motion-qa/report.json").read_text(encoding="utf-8")
)
assert metadata.get("generator") == "codex-app-server-skill"
assert metadata.get("provenance") == "skill-full-source"
assert metadata.get("visual_source") in {"image-generation", "user-reference-derived"}
assert metadata.get("preview_only") is False
assert manifest.get("schema_version") == "apc.petpack.v3"
timings = manifest["states"]
assert {state["name"] for state in timings} == {
"idle", "thinking", "tool", "waiting", "done", "failed",
"acknowledge", "drag_left", "drag_right"
}
expected_counts = {
state["name"]: len(state["frame_durations_ms"])
for state in timings
}
assert all(2 <= count <= 40 for count in expected_counts.values())
assert all(
50 <= duration <= 2000
for state in timings
for duration in state["frame_durations_ms"]
)
assert all(sum(state["frame_durations_ms"]) <= 5000 for state in timings)
assert metadata.get("states") == timings
assert metadata.get("state_frame_counts") == expected_counts
encoded_timing = json.dumps(
timings, ensure_ascii=False, separators=(",", ":")
).encode("utf-8")
assert motion_report.get("timing_digest") == hashlib.sha256(encoded_timing).hexdigest()
assert all(
motion_report["states"][state]["previews"].get("authored_timing")
for state in expected_counts
)
first_frames = set()
for state in [
"idle", "thinking", "tool", "waiting", "done", "failed",
"acknowledge", "drag_left", "drag_right",
]:
frames = sorted((root / "assets/frames" / state).glob("*.png"))
assert len(frames) == expected_counts[state]
digests = {hashlib.sha256(path.read_bytes()).hexdigest() for path in frames}
assert len(digests) >= 2
first_frames.add(hashlib.sha256(frames[0].read_bytes()).hexdigest())
assert len(first_frames) >= 4
PY
fi
SOURCE_VALIDATION="$("$ROOT_DIR/target/debug/petcore-cli" petpack validate "$SOURCE_DIR")"
assert_json "$SOURCE_VALIDATION" 'data["ok"] is True and len(data["manifest"]["states"]) == 9 and data["frame_count"] == 50' \
|| fail "built petpack-source validation failed"
SNAPSHOT="$(APC_HOME="$TMP_DIR/home" "$ROOT_DIR/target/debug/petcore-cli" snapshot)"
assert_json "$SNAPSHOT" 'len(data["pets"]) == 1 and data["pets"][0]["active"] is True and data["pets"][0]["quality"] == "standard"' \
|| fail "completed pet was not imported and activated"
assert_json "$SNAPSHOT" 'data["pets"][0]["generator"] in ["codex-app-server-skill", "codex-app-server-brief-petpack-v3"] and data["pets"][0]["provenance"] in ["skill-full-source", "codex_app_server_brief"]' \
|| fail "imported pet does not preserve real App Server provenance"
if truthy "${APC_REQUIRE_SKILL_FULL_SOURCE:-1}"; then
assert_json "$SNAPSHOT" 'data["pets"][0]["generator"] == "codex-app-server-skill" and data["pets"][0]["provenance"] == "skill-full-source"' \
|| fail "strict full-source imported pet does not preserve Skill provenance"
fi
# Exercise the resumable task lifecycle separately from the completed artifact
# acceptance above. This intentionally terminates only the validator-owned
# PetCore, waits beyond the stale-owner lease, then resumes and cancels the
# exact task through the public CLI/RPC surface.
LIFECYCLE_FORM='{"description":"真实 App Server 生命周期验收任务:制作一只低分辨率青绿色圆形机械宠物。立即开始工作,不要请求额外输入;持续保存检查点,直到任务完成或收到取消。","style":"像素","quality":"low","reference_images":[]}'
LIFECYCLE_JSON="$(APC_HOME="$TMP_DIR/home" "$ROOT_DIR/target/debug/petcore-cli" generation start --form-json "$LIFECYCLE_FORM")"
LIFECYCLE_JOB_ID="$(JSON="$LIFECYCLE_JSON" python3 - <<'PY'
import json
import os
print(json.loads(os.environ["JSON"])["job_id"])
PY
)"
LIFECYCLE_THREAD_ID=""
for _ in {1..240}; do
LIFECYCLE_STATUS="$(APC_HOME="$TMP_DIR/home" "$ROOT_DIR/target/debug/petcore-cli" generation status --job-id "$LIFECYCLE_JOB_ID")"
if JSON="$LIFECYCLE_STATUS" python3 - <<'PY'
import json
import os
data = json.loads(os.environ["JSON"])
lifecycle = data.get("lifecycle", {})
raise SystemExit(0 if data.get("status") == "running" and lifecycle.get("active_turn_id") and lifecycle.get("thread_id") else 1)
PY
then
LIFECYCLE_THREAD_ID="$(JSON="$LIFECYCLE_STATUS" python3 - <<'PY'
import json
import os
print(json.loads(os.environ["JSON"])["lifecycle"]["thread_id"])
PY
)"
break
fi
sleep 0.5
done
[[ -n "$LIFECYCLE_THREAD_ID" && "$LIFECYCLE_THREAD_ID" != "None" ]] \
|| fail "lifecycle task did not reach a real active App Server turn"
stop_petcore
sleep "${APC_REAL_APP_SERVER_SLEEP_GAP_SECONDS:-32}"
start_petcore
RECOVERABLE_DETAIL=""
for _ in {1..120}; do
RECOVERABLE_DETAIL="$(APC_HOME="$TMP_DIR/home" "$ROOT_DIR/target/debug/petcore-cli" generation history detail --job-id "$LIFECYCLE_JOB_ID")"
if JSON="$RECOVERABLE_DETAIL" python3 - <<'PY'
import json
import os
data = json.loads(os.environ["JSON"])
raise SystemExit(0 if data.get("status") == "failed" and data.get("recoverable") is True else 1)
PY
then
break
fi
sleep 0.5
done
assert_json "$RECOVERABLE_DETAIL" 'data["status"] == "failed" and data["recoverable"] is True and data["capabilities"]["can_resume"] is True' \
|| fail "PetCore restart did not preserve the task as recoverable"
APC_HOME="$TMP_DIR/home" "$ROOT_DIR/target/debug/petcore-cli" generation resume \
--job-id "$LIFECYCLE_JOB_ID" \
--instruction "从最后一个已验证检查点继续,只处理尚未完成的阶段。" \
--request-id "real-resume-$LIFECYCLE_JOB_ID" >/dev/null
RESUMED_STATUS=""
for _ in {1..240}; do
RESUMED_STATUS="$(APC_HOME="$TMP_DIR/home" "$ROOT_DIR/target/debug/petcore-cli" generation status --job-id "$LIFECYCLE_JOB_ID")"
if JSON="$RESUMED_STATUS" python3 - <<'PY'
import json
import os
data = json.loads(os.environ["JSON"])
raise SystemExit(0 if data.get("status") == "running" and data.get("lifecycle", {}).get("active_turn_id") else 1)
PY
then
break
fi
sleep 0.5
done
assert_json "$RESUMED_STATUS" 'data["status"] == "running" and data["lifecycle"]["active_turn_id"] is not None and data["lifecycle"]["thread_id"] is not None' \
|| fail "real thread/resume did not start a continuation turn"
RESUMED_THREAD_ID="$(JSON="$RESUMED_STATUS" python3 - <<'PY'
import json
import os
print(json.loads(os.environ["JSON"])["lifecycle"]["thread_id"])
PY
)"
[[ "$RESUMED_THREAD_ID" == "$LIFECYCLE_THREAD_ID" ]] \
|| fail "real thread/resume replaced the original durable thread identity"
APC_HOME="$TMP_DIR/home" "$ROOT_DIR/target/debug/petcore-cli" generation cancel \
--job-id "$LIFECYCLE_JOB_ID" >/dev/null
LIFECYCLE_FINAL="$(HOME="$REAL_USER_HOME" APC_HOME="$TMP_DIR/home" "$ROOT_DIR/target/debug/petcore-cli" generation status --job-id "$LIFECYCLE_JOB_ID" --inspect-thread)"
assert_json "$LIFECYCLE_FINAL" 'data["status"] == "canceled" and data["lifecycle"]["active_turn_id"] is None and data["lifecycle"]["execution_stopped_at"] is not None and data["lifecycle"]["thread_archived_at"] is not None' \
|| fail "real cancellation did not stop the exact active turn and worker"
assert_json "$LIFECYCLE_FINAL" 'data["thread_inspection"]["availability"] in ["archived", "missing"] and data["thread_inspection"]["can_open"] is False' \
|| fail "canceled real Studio thread is still present in the ordinary thread list"
if [[ -n "${APC_REAL_APP_SERVER_ARTIFACT_DIR:-}" ]]; then
preserve_artifacts
printf '%s\n' "$FINAL_STATUS" \
>"$APC_REAL_APP_SERVER_ARTIFACT_DIR/final-status.json"
printf '%s\n' "$SNAPSHOT" \
>"$APC_REAL_APP_SERVER_ARTIFACT_DIR/snapshot.json"
printf '%s\n' "$LIFECYCLE_FINAL" \
>"$APC_REAL_APP_SERVER_ARTIFACT_DIR/lifecycle-final-status.json"
fi
echo "Real Codex App Server validation ok"