diff --git a/.github/workflows/shell.yml b/.github/workflows/shell.yml index a234cde..b869223 100644 --- a/.github/workflows/shell.yml +++ b/.github/workflows/shell.yml @@ -288,6 +288,7 @@ jobs: test: - agent-state-ownership - agents-md-backup-retention + - agents-md-failure-does-not-abort-upgrade - carried-claude-code-plugin - ci-coverage - codex-runtime diff --git a/tests/agents-md-failure-does-not-abort-upgrade.sh b/tests/agents-md-failure-does-not-abort-upgrade.sh new file mode 100755 index 0000000..f03eb57 --- /dev/null +++ b/tests/agents-md-failure-does-not-abort-upgrade.sh @@ -0,0 +1,115 @@ +#!/bin/bash +# tests/agents-md-failure-does-not-abort-upgrade.sh +# +# A failed AGENTS.md composition must fail the run WITHOUT truncating it. +# +# upgrade.sh runs under `set -e`, and regenerate_agents_md is the only phase +# function in it with a non-zero return path. Called bare, one stale memory file +# ended the run at that line: eleven reconciliation phases and print_summary +# never executed, and the log stopped with nothing saying the rest had been +# skipped (#607). +# +# This runs the real execute block from upgrade.sh against stubbed phases rather +# than asserting on its source text, so it fails if the capture is dropped no +# matter how the call is written. +set -eu + +ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" +UPGRADE="$ROOT_DIR/upgrade.sh" +WORK="$(mktemp -d "${TMPDIR:-/tmp}/wpca-agents-md-abort.XXXXXX")" +trap 'rm -rf "$WORK"' EXIT + +# The execute block is everything from the first status accumulator to EOF. +sed -n '/^PLUGIN_ONLY_EXIT_STATUS=0$/,$p' "$UPGRADE" > "$WORK/execute.sh" +if [ ! -s "$WORK/execute.sh" ]; then + echo "FAIL: could not extract the execute block from upgrade.sh" >&2 + exit 1 +fi + +# Every phase reached after regenerate_agents_md. If the abort returns, these +# stop appearing in the transcript. +PHASES_AFTER=( + runtime_guidance_sync_managed_codex_projection + sync_claude_code_runtime + sync_runtime_signature + sync_runtime_instructions + opencode_project_subagents_optional + update_chat_bridge_systemd + update_chat_bridge_launchd + reconcile_wordpress_service + reconcile_datamachine_worker_service + refresh_opencode_runtime_signature_phase + installation_profile_write + print_summary +) + +{ + echo 'set -eu' + # Narrow-scope flags off; this is the full default upgrade path. + for flag in PLUGINS_ONLY SKILLS_ONLY KIMAKI_ONLY AGENTS_MD_ONLY RECONCILE_SERVICES_ONLY DRY_RUN; do + echo "${flag}=false" + done + echo 'SITE_PATH=/tmp/site' + echo 'SCRIPT_DIR=/tmp/wpca' + echo 'INSTALLATION_OPERATION_UPGRADE=upgrade' + echo '_run_filter_active() { return 0; }' + echo 'update_data_machine_plugins() { :; }' + echo 'convergence_run() { :; }' + echo 'reconciler_print_partial_evidence() { echo "RAN reconciler_print_partial_evidence"; }' + + # Phases before the failing one, plus the unconditional ones. + for phase in reconcile_provider_and_service_state sync_cli_transport_runtime \ + update_ai_gateway sync_chat_bridge_config systems_capabilities_apply \ + check_opencode_json_drift ai_gateway_configure_opencode sync_skills \ + "${PHASES_AFTER[@]}"; do + echo "${phase}() { echo \"RAN ${phase}\"; }" + done + + # The failure under test. + echo 'regenerate_agents_md() { echo "RAN regenerate_agents_md"; return 1; }' + + cat "$WORK/execute.sh" +} > "$WORK/harness.sh" + +set +e +transcript="$(bash "$WORK/harness.sh" 2>&1)" +status=$? +set -e + +failed=0 + +if [ "$status" -eq 0 ]; then + echo "FAIL: a failed AGENTS.md composition must still exit non-zero (got 0)" >&2 + failed=1 +fi + +for phase in "${PHASES_AFTER[@]}"; do + case "$transcript" in + *"RAN ${phase}"*) ;; + *) + echo "FAIL: ${phase} did not run after the AGENTS.md failure" >&2 + failed=1 + ;; + esac +done + +# Guard the inverse: the capture must not swallow the failure into a clean exit +# reported as success by the summary. +case "$transcript" in + *"RAN regenerate_agents_md"*) ;; + *) + echo "FAIL: harness never reached regenerate_agents_md" >&2 + failed=1 + ;; +esac + +if [ "$failed" -ne 0 ]; then + echo "--- transcript ---" >&2 + echo "$transcript" >&2 + echo "--- exit status: $status ---" >&2 + exit 1 +fi + +echo "ok remaining phases run after an AGENTS.md composition failure" +echo "ok the run still exits non-zero (${status})" +echo "OK (tests/agents-md-failure-does-not-abort-upgrade.sh)" diff --git a/upgrade.sh b/upgrade.sh index 13081a8..bd89b2e 100755 --- a/upgrade.sh +++ b/upgrade.sh @@ -1302,6 +1302,10 @@ print_summary() { warn "Plugin upgrade partially completed." elif [ "${CONVERGENCE_EXIT_STATUS:-0}" -ne 0 ]; then warn "Desired-state convergence partially completed." + elif [ "${AGENTS_MD_EXIT_STATUS:-0}" -ne 0 ]; then + # Named here rather than left to the reader: every other phase converged, + # so without this line a non-zero exit has no visible cause in the summary. + warn "Upgrade completed except AGENTS.md — composition failed; see above." else log "Upgrade complete." fi @@ -1457,6 +1461,7 @@ _print_plugins_only_verify_block() { PLUGIN_ONLY_EXIT_STATUS=0 CONVERGENCE_EXIT_STATUS=0 +AGENTS_MD_EXIT_STATUS=0 update_data_machine_plugins || PLUGIN_ONLY_EXIT_STATUS=$? if [ "$PLUGINS_ONLY" != true ]; then CONVERGENCE_ENTRYPOINT="$SCRIPT_DIR/upgrade.sh" @@ -1482,7 +1487,14 @@ if _run_filter_active reconciliation; then ai_gateway_configure_opencode fi sync_skills -regenerate_agents_md +# Captured, not aborted. This is the only phase function here with a non-zero +# return path, and under `set -e` a bare call made one stale memory file end the +# run: eleven reconciliation phases and print_summary never happened, and the +# log simply stopped mid-phase with nothing saying the rest had been skipped. +# A failed compose is still worth a non-zero exit at the end — see the +# AGENTS_MD_EXIT_STATUS check below — but not worth abandoning service +# reconciliation halfway through. +regenerate_agents_md || AGENTS_MD_EXIT_STATUS=$? if _run_filter_active agents-md; then runtime_guidance_sync_managed_codex_projection fi @@ -1508,3 +1520,6 @@ if [ "$CONVERGENCE_EXIT_STATUS" -ne 0 ]; then reconciler_print_partial_evidence exit "$CONVERGENCE_EXIT_STATUS" fi +if [ "$AGENTS_MD_EXIT_STATUS" -ne 0 ]; then + exit "$AGENTS_MD_EXIT_STATUS" +fi