Files
boshu2__agentops/scripts/test-codex-hookless-lifecycle.sh
T
2026-03-24 10:09:27 -04:00

350 lines
12 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
# test-codex-hookless-lifecycle.sh
# Verifies the non-release Codex hookless lifecycle path against a locally built ao binary.
#
# What it checks:
# 1) builds ao from the current worktree unless --ao-bin is supplied
# 2) seeds a temp Codex home with session_index + history fallback data
# 3) seeds a temp repo with repo-local learnings
# 4) verifies ao codex ensure-start writes startup context/state and skips duplicate startup
# 5) verifies ao lookup and ao search --local --cite record citations
# 6) verifies ao codex ensure-stop uses history fallback, writes lifecycle state, and skips duplicate closeout
# 7) verifies ao codex status reports coherent hookless health and citation counts
# 8) verifies ao rpi phased can complete in tracker-degraded tasklist mode with a deterministic fake runtime
# 9) verifies ao rpi status surfaces tracker_mode=tasklist for that run
# 10) verifies the Codex RPI contract validator still passes
#
# Usage:
# bash scripts/test-codex-hookless-lifecycle.sh
# bash scripts/test-codex-hookless-lifecycle.sh --keep-temp
# bash scripts/test-codex-hookless-lifecycle.sh --ao-bin /path/to/ao
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
cd "$REPO_ROOT"
AO_BIN=""
KEEP_TEMP="false"
TMP_ROOT=""
usage() {
cat <<'EOF'
test-codex-hookless-lifecycle.sh
Options:
--ao-bin <path> Use an existing ao binary instead of building ./cli/cmd/ao
--keep-temp Preserve the temp Codex home + repo for inspection
--help Show this help
Examples:
bash scripts/test-codex-hookless-lifecycle.sh
bash scripts/test-codex-hookless-lifecycle.sh --keep-temp
bash scripts/test-codex-hookless-lifecycle.sh --ao-bin ./cli/bin/ao
EOF
}
while [[ $# -gt 0 ]]; do
case "$1" in
--ao-bin)
AO_BIN="${2:-}"
shift 2
;;
--keep-temp)
KEEP_TEMP="true"
shift 1
;;
--help|-h)
usage
exit 0
;;
*)
echo "Unknown arg: $1" >&2
usage >&2
exit 2
;;
esac
done
info() {
echo "INFO: $*"
}
fail() {
echo "FAIL: $*" >&2
exit 1
}
require_cmd() {
command -v "$1" >/dev/null 2>&1 || fail "Required command not found: $1"
}
cleanup() {
if [[ "$KEEP_TEMP" != "true" && -n "${TMP_ROOT:-}" && -d "$TMP_ROOT" ]]; then
rm -rf "$TMP_ROOT"
fi
}
trap cleanup EXIT
require_cmd bash
require_cmd git
require_cmd jq
require_cmd rg
TMP_ROOT="$(mktemp -d "${TMPDIR:-/tmp}/codex-hookless-lifecycle-test.XXXXXX")"
HOME_ROOT="$TMP_ROOT/home"
REPO_DIR="$TMP_ROOT/repo"
RPI_REPO_DIR="$TMP_ROOT/rpi-repo"
CODEX_HOME="$HOME_ROOT/.codex"
SESSION_ID="019d1bf7-58ea-79e1-9f5d-02109d930081"
QUERY="explicit codex lifecycle"
RPI_GOAL="Codex no-beads smoke"
mkdir -p "$CODEX_HOME" "$REPO_DIR/.agents/learnings" "$RPI_REPO_DIR"
if [[ -z "$AO_BIN" ]]; then
require_cmd go
AO_BIN="$TMP_ROOT/ao"
info "Building ao from current worktree"
(
cd "$REPO_ROOT/cli"
go build -o "$AO_BIN" ./cmd/ao
)
fi
[[ -x "$AO_BIN" ]] || fail "ao binary is not executable: $AO_BIN"
cat > "$CODEX_HOME/session_index.jsonl" <<EOF
{"id":"$SESSION_ID","thread_name":"Hookless fallback smoke","updated_at":"2026-03-23T12:00:00Z"}
EOF
cat > "$CODEX_HOME/history.jsonl" <<EOF
{"session_id":"$SESSION_ID","ts":1766945655,"text":"Review prior learnings for codex fallback"}
{"session_id":"$SESSION_ID","ts":1766945658,"text":"Implement explicit ao codex stop closeout"}
EOF
cat > "$REPO_DIR/.agents/learnings/codex-lifecycle.md" <<'EOF'
---
id: codex-lifecycle
type: learning
date: 2026-03-23
source: codex-test
maturity: provisional
utility: 0.9
---
# Explicit Codex lifecycle
Use ao codex start and ao codex stop when runtime hooks are unavailable.
EOF
cat > "$REPO_DIR/.agents/learnings/codex-lifecycle.jsonl" <<'EOF'
{"id":"codex-lifecycle-jsonl","summary":"Explicit Codex lifecycle uses ao codex start and ao codex stop when runtime hooks are unavailable.","maturity":"provisional","utility":0.9}
EOF
git init -q "$REPO_DIR"
git init -q "$RPI_REPO_DIR"
run_ao() {
(
cd "$REPO_DIR"
HOME="$HOME_ROOT" \
CODEX_THREAD_ID="$SESSION_ID" \
CODEX_INTERNAL_ORIGINATOR_OVERRIDE="Codex Desktop" \
"$AO_BIN" "$@"
)
}
assert_json() {
local json="$1"
local expr="$2"
local message="$3"
printf '%s' "$json" | jq -e "$expr" >/dev/null || fail "$message"
}
info "Running ao codex ensure-start"
start_json="$(run_ao codex ensure-start --json --query "$QUERY")"
assert_json "$start_json" '.runtime.mode == "codex-hookless-fallback"' "codex ensure-start did not detect hookless Codex mode"
assert_json "$start_json" '.performed == true' "first codex ensure-start should perform startup"
START_CONTEXT_PATH="$(printf '%s' "$start_json" | jq -r '.startup_context_path')"
STATE_PATH="$(printf '%s' "$start_json" | jq -r '.state_path')"
MEMORY_PATH="$(printf '%s' "$start_json" | jq -r '.memory_path')"
[[ -f "$START_CONTEXT_PATH" ]] || fail "startup context not written: $START_CONTEXT_PATH"
[[ -f "$STATE_PATH" ]] || fail "lifecycle state not written on start: $STATE_PATH"
[[ -n "$MEMORY_PATH" && "$MEMORY_PATH" != "null" ]] || fail "codex start did not report a memory path"
CITATIONS_PATH="$REPO_DIR/.agents/ao/citations.jsonl"
[[ -f "$CITATIONS_PATH" ]] || fail "citations ledger missing after codex start"
rg -q '"citation_type":"retrieved"' "$CITATIONS_PATH" || fail "codex start did not record retrieved citations"
info "Re-running ao codex ensure-start to verify idempotency"
start_repeat_json="$(run_ao codex ensure-start --json --query "$QUERY")"
assert_json "$start_repeat_json" '.performed == false' "second codex ensure-start should be a no-op"
info "Running ao lookup for curated retrieval"
lookup_json="$(run_ao lookup --query "$QUERY" --limit 1 --json)"
assert_json "$lookup_json" '.learnings | length >= 1' "lookup did not return the seeded learning"
info "Running ao search --local with assisted citation"
search_json="$(run_ao search "$QUERY" --local --json --cite reference)"
assert_json "$search_json" 'length >= 1' "local search did not return the seeded learning"
rg -q '"citation_type":"reference"' "$CITATIONS_PATH" || fail "search --cite reference did not record a reference citation"
info "Running ao codex ensure-stop"
stop_json="$(run_ao codex ensure-stop --json)"
assert_json "$stop_json" '.runtime.mode == "codex-hookless-fallback"' "codex ensure-stop did not report hookless Codex mode"
assert_json "$stop_json" '.performed == true' "first codex ensure-stop should perform closeout"
assert_json "$stop_json" '.transcript_source == "history-fallback"' "codex ensure-stop did not use history fallback in the temp Codex home"
assert_json "$stop_json" '.synthetic_transcript == true' "codex ensure-stop did not mark the history transcript as synthetic"
TRANSCRIPT_PATH="$(printf '%s' "$stop_json" | jq -r '.transcript_path')"
HANDOFF_PATH="$(printf '%s' "$stop_json" | jq -r '.handoff_path')"
[[ -f "$TRANSCRIPT_PATH" ]] || fail "history fallback transcript missing: $TRANSCRIPT_PATH"
[[ -f "$HANDOFF_PATH" ]] || fail "closeout handoff artifact missing: $HANDOFF_PATH"
info "Re-running ao codex ensure-stop to verify idempotency"
stop_repeat_json="$(run_ao codex ensure-stop --json)"
assert_json "$stop_repeat_json" '.performed == false' "second codex ensure-stop should be a no-op"
info "Running ao codex status"
status_json="$(run_ao codex status --json)"
assert_json "$status_json" '.runtime.mode == "codex-hookless-fallback"' "codex status did not report hookless Codex mode"
printf '%s' "$status_json" | jq -e --arg path "$START_CONTEXT_PATH" '.state.last_start.startup_context_path == $path' >/dev/null \
|| fail "codex status did not preserve the startup context path"
assert_json "$status_json" '.state.last_stop.transcript_source == "history-fallback"' "codex status did not preserve the stop transcript source"
assert_json "$status_json" '.retrieval.learnings >= 1' "codex status did not report retrievable learnings"
assert_json "$status_json" '.capture.sessions_indexed >= 1' "codex status did not report indexed sessions"
assert_json "$status_json" '.citations.retrieved >= 2' "codex status retrieved citation count is too low"
assert_json "$status_json" '.citations.reference >= 1' "codex status reference citation count is too low"
assert_json "$status_json" '.citations.total >= 3' "codex status total citation count is too low"
BROKEN_BD_BIN="$TMP_ROOT/bd-broken.sh"
cat > "$BROKEN_BD_BIN" <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
printf 'column "crystallizes" could not be found in any table in scope\n' >&2
exit 1
EOF
chmod +x "$BROKEN_BD_BIN"
FAKE_RUNTIME_BIN="$TMP_ROOT/fake-runtime.sh"
cat > "$FAKE_RUNTIME_BIN" <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
PROMPT=""
while [[ $# -gt 0 ]]; do
case "$1" in
-p)
PROMPT="${2:-}"
shift 2
;;
*)
shift
;;
esac
done
mkdir -p .agents/council .agents/plans .agents/rpi
if grep -q "phase 1 of 3" <<<"$PROMPT"; then
cat > .agents/plans/2026-03-24-codex-no-beads-plan.md <<'PLAN'
# Codex No-Beads Plan
- Goal: prove tasklist fallback end to end
- Handoff: execution packet
PLAN
cat > .agents/council/2026-03-24-pre-mortem-codex-no-beads.md <<'REPORT'
# Pre-Mortem
## Council Verdict: PASS
Tasklist fallback is acceptable for this bounded smoke test.
REPORT
printf '<promise>DONE</promise>\n'
exit 0
fi
if grep -q "phase 2 of 3" <<<"$PROMPT"; then
cat > .agents/rpi/phase-2-summary-2026-03-24-crank.md <<'SUMMARY'
# Phase 2 Summary: Crank
- Execution packet mode completed.
SUMMARY
printf '<promise>DONE</promise>\n'
exit 0
fi
if grep -q "phase 3 of 3" <<<"$PROMPT"; then
cat > .agents/council/2026-03-24-vibe-codex-no-beads.md <<'REPORT'
# Vibe
## Council Verdict: PASS
Validation passed in tasklist mode.
REPORT
cat > .agents/council/2026-03-24-post-mortem-codex-no-beads.md <<'REPORT'
# Post-Mortem
## Council Verdict: PASS
Closeout passed in tasklist mode.
REPORT
printf '<promise>DONE</promise>\n'
exit 0
fi
printf 'unrecognized prompt shape\n' >&2
exit 1
EOF
chmod +x "$FAKE_RUNTIME_BIN"
run_ao_rpi() {
(
cd "$RPI_REPO_DIR"
HOME="$HOME_ROOT" \
CODEX_THREAD_ID="$SESSION_ID" \
CODEX_INTERNAL_ORIGINATOR_OVERRIDE="Codex Desktop" \
AGENTOPS_RPI_BD_COMMAND="$BROKEN_BD_BIN" \
"$AO_BIN" "$@"
)
}
info "Running ao rpi phased in tracker-degraded tasklist mode"
run_ao_rpi rpi phased "$RPI_GOAL" \
--runtime direct \
--runtime-cmd "$FAKE_RUNTIME_BIN" \
--no-worktree \
--no-budget >/dev/null
RPI_PACKET_PATH="$RPI_REPO_DIR/.agents/rpi/execution-packet.json"
RPI_STATE_PATH="$RPI_REPO_DIR/.agents/rpi/phased-state.json"
[[ -f "$RPI_PACKET_PATH" ]] || fail "RPI execution packet missing after tasklist fallback run"
[[ -f "$RPI_STATE_PATH" ]] || fail "RPI phased state missing after tasklist fallback run"
jq -e '.tracker_mode == "tasklist"' "$RPI_PACKET_PATH" >/dev/null || fail "execution packet did not record tracker_mode=tasklist"
jq -e '.plan_path | length > 0' "$RPI_PACKET_PATH" >/dev/null || fail "execution packet missing tasklist plan_path"
jq -e '.tracker_health.mode == "tasklist" and .tracker_health.healthy == false' "$RPI_PACKET_PATH" >/dev/null || fail "execution packet missing degraded tracker health"
jq -e '.tracker_mode == "tasklist"' "$RPI_STATE_PATH" >/dev/null || fail "phased state did not persist tracker_mode=tasklist"
info "Running ao rpi status to verify tasklist visibility"
rpi_status_json="$(run_ao_rpi rpi status --json)"
assert_json "$rpi_status_json" '[.runs[] | select(.goal == "'"$RPI_GOAL"'" and .tracker_mode == "tasklist" and .status == "completed")] | length >= 1' "ao rpi status did not surface the completed tasklist-mode run"
echo ""
info "Running Codex RPI contract validation"
bash "$REPO_ROOT/scripts/validate-codex-rpi-contract.sh"
info "Running Codex lifecycle guard validation"
bash "$REPO_ROOT/scripts/validate-codex-lifecycle-guards.sh"
echo "PASS: Codex hookless lifecycle smoke verified"
echo " ao binary: $AO_BIN"
echo " temp root: $TMP_ROOT"
echo " repo dir: $REPO_DIR"
if [[ "$KEEP_TEMP" != "true" ]]; then
echo " temp files: cleaned on exit"
fi