493 lines
21 KiB
Bash
Executable File
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"
|