diff --git a/.gaai/core/scripts/tests/agent-activity-stale.test.sh b/.gaai/core/scripts/tests/agent-activity-stale.test.sh old mode 100644 new mode 100755 diff --git a/.gaai/core/scripts/tests/agent-hang-kill-target.test.sh b/.gaai/core/scripts/tests/agent-hang-kill-target.test.sh old mode 100644 new mode 100755 diff --git a/.gaai/core/scripts/tests/backlog-scheduler-phase.test.sh b/.gaai/core/scripts/tests/backlog-scheduler-phase.test.sh old mode 100644 new mode 100755 diff --git a/.gaai/core/scripts/tests/backlog-yaml.test.sh b/.gaai/core/scripts/tests/backlog-yaml.test.sh old mode 100644 new mode 100755 diff --git a/.gaai/core/scripts/tests/codex-executor.test.sh b/.gaai/core/scripts/tests/codex-executor.test.sh old mode 100644 new mode 100755 diff --git a/.gaai/core/scripts/tests/commit-pr-state.test.sh b/.gaai/core/scripts/tests/commit-pr-state.test.sh old mode 100644 new mode 100755 diff --git a/.gaai/core/scripts/tests/crash-recovery.test.sh b/.gaai/core/scripts/tests/crash-recovery.test.sh old mode 100644 new mode 100755 diff --git a/.gaai/core/scripts/tests/cross-cycle-qa-report.test.sh b/.gaai/core/scripts/tests/cross-cycle-qa-report.test.sh old mode 100644 new mode 100755 diff --git a/.gaai/core/scripts/tests/daemon-asset-home.test.sh b/.gaai/core/scripts/tests/daemon-asset-home.test.sh old mode 100644 new mode 100755 diff --git a/.gaai/core/scripts/tests/daemon-coordination-home.test.sh b/.gaai/core/scripts/tests/daemon-coordination-home.test.sh old mode 100644 new mode 100755 diff --git a/.gaai/core/scripts/tests/daemon-dispatch-auto-resolve.test.sh b/.gaai/core/scripts/tests/daemon-dispatch-auto-resolve.test.sh old mode 100644 new mode 100755 diff --git a/.gaai/core/scripts/tests/daemon-dispatch-mcp-config.test.sh b/.gaai/core/scripts/tests/daemon-dispatch-mcp-config.test.sh old mode 100644 new mode 100755 diff --git a/.gaai/core/scripts/tests/daemon-dispatch-reap-worktrees.test.sh b/.gaai/core/scripts/tests/daemon-dispatch-reap-worktrees.test.sh old mode 100644 new mode 100755 diff --git a/.gaai/core/scripts/tests/daemon-home-provision.test.sh b/.gaai/core/scripts/tests/daemon-home-provision.test.sh old mode 100644 new mode 100755 diff --git a/.gaai/core/scripts/tests/daemon-monitor.test.sh b/.gaai/core/scripts/tests/daemon-monitor.test.sh old mode 100644 new mode 100755 diff --git a/.gaai/core/scripts/tests/daemon-path-roots.test.sh b/.gaai/core/scripts/tests/daemon-path-roots.test.sh old mode 100644 new mode 100755 diff --git a/.gaai/core/scripts/tests/daemon-safe-increment.test.sh b/.gaai/core/scripts/tests/daemon-safe-increment.test.sh old mode 100644 new mode 100755 diff --git a/.gaai/core/scripts/tests/governance-files-excluded.test.sh b/.gaai/core/scripts/tests/governance-files-excluded.test.sh old mode 100644 new mode 100755 diff --git a/.gaai/core/scripts/tests/home-branch-guard.test.sh b/.gaai/core/scripts/tests/home-branch-guard.test.sh old mode 100644 new mode 100755 diff --git a/.gaai/core/scripts/tests/orphan-lock-cycle.test.sh b/.gaai/core/scripts/tests/orphan-lock-cycle.test.sh old mode 100644 new mode 100755 diff --git a/.gaai/core/scripts/tests/pnpm-guard.test.sh b/.gaai/core/scripts/tests/pnpm-guard.test.sh old mode 100644 new mode 100755 diff --git a/.gaai/core/scripts/tests/pr-watcher.test.sh b/.gaai/core/scripts/tests/pr-watcher.test.sh old mode 100644 new mode 100755 diff --git a/.gaai/core/scripts/tests/reconcile-own-write.test.sh b/.gaai/core/scripts/tests/reconcile-own-write.test.sh old mode 100644 new mode 100755 diff --git a/.gaai/core/scripts/tests/recovery-non-destructive.test.sh b/.gaai/core/scripts/tests/recovery-non-destructive.test.sh old mode 100644 new mode 100755 diff --git a/.gaai/core/scripts/tests/reset-purge.test.sh b/.gaai/core/scripts/tests/reset-purge.test.sh old mode 100644 new mode 100755 diff --git a/.gaai/core/scripts/tests/stale-race-mutex.test.sh b/.gaai/core/scripts/tests/stale-race-mutex.test.sh old mode 100644 new mode 100755 diff --git a/.gaai/core/scripts/tests/story-file-reconcile.test.sh b/.gaai/core/scripts/tests/story-file-reconcile.test.sh old mode 100644 new mode 100755 diff --git a/.gaai/core/scripts/tests/stuck-classifier.test.sh b/.gaai/core/scripts/tests/stuck-classifier.test.sh old mode 100644 new mode 100755 diff --git a/.gaai/core/scripts/tests/test-gate.test.sh b/.gaai/core/scripts/tests/test-gate.test.sh old mode 100644 new mode 100755 diff --git a/.gaai/core/scripts/tests/test-webhook-hmac.sh b/.gaai/core/scripts/tests/test-webhook-hmac.sh old mode 100644 new mode 100755 diff --git a/.gaai/core/scripts/tests/validate-public-release.test.sh b/.gaai/core/scripts/tests/validate-public-release.test.sh new file mode 100755 index 00000000..87f79ead --- /dev/null +++ b/.gaai/core/scripts/tests/validate-public-release.test.sh @@ -0,0 +1,441 @@ +#!/usr/bin/env bash +# ── validate-public-release.test.sh ───────────────────────────────────────── +# Hermetic contract test for validate-public-release.sh (E1095S01). +# +# Builds throwaway mktemp -d sandboxes containing a minimal-but-passing +# `.gaai/core` (satisfies health-check.sh) and `.gaai/project` (satisfies +# artefact-sync.sh), copies the real health-check.sh / artefact-sync.sh / +# validate-public-release.sh into the sandbox core, then plants fixture +# shell/Node tests and invokes the entrypoint as a real subprocess (never +# sourced — the ordering/fail-fast guarantees are process-level). +# +# Covers AC2 (discovery exactly-once, non-test files ignored, exit 0 only +# when everything passes), AC3 (failing test / non-executable / Node +# failure / traversal-failure diagnostics), AC6 (fail-fast determinism: +# first failure stops the run, reports next unstarted phase + next known +# test path, no later check executes). +# +# Run: bash .gaai/core/scripts/tests/validate-public-release.test.sh +# Exit 0 = all pass. + +set -uo pipefail +PASS_COUNT=0; FAIL_COUNT=0 +pass() { echo " PASS: $1"; PASS_COUNT=$(( PASS_COUNT + 1 )); } +fail() { echo " FAIL: $1"; FAIL_COUNT=$(( FAIL_COUNT + 1 )); } + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +REAL_CORE="$(cd "$SCRIPT_DIR/../.." && pwd)" # .../.gaai/core + +declare -a TMP_DIRS=() +cleanup_all() { + local d + for d in "${TMP_DIRS[@]:-}"; do + [[ -n "$d" && -d "$d" ]] && rm -rf "$d" + done +} +trap cleanup_all EXIT + +# Builds a sandbox .gaai/core that health-check.sh reports PASSED on, with +# the real gate scripts under test copied in (so validate-public-release.sh +# invokes the *shipped* health-check.sh / artefact-sync.sh, not a stub). +build_passing_core() { + local core="$1" + mkdir -p "$core"/agents "$core"/skills "$core"/contexts/rules "$core"/workflows \ + "$core"/scripts/tests "$core"/compat "$core"/adapters + : > "$core/GAAI.md" + : > "$core/README.md" + : > "$core/QUICK-REFERENCE.md" + echo "1.0.0" > "$core/VERSION" + : > "$core/agents/discovery.agent.md" + : > "$core/agents/bootstrap.agent.md" + for r in orchestration skills artefacts backlog memory context-discovery; do + : > "$core/contexts/rules/$r.rules.md" + done + cp "$REAL_CORE/scripts/health-check.sh" "$core/scripts/health-check.sh" + cp "$REAL_CORE/scripts/artefact-sync.sh" "$core/scripts/artefact-sync.sh" + cp "$REAL_CORE/scripts/validate-public-release.sh" "$core/scripts/validate-public-release.sh" + chmod +x "$core/scripts/health-check.sh" "$core/scripts/artefact-sync.sh" "$core/scripts/validate-public-release.sh" +} + +# Builds a sandbox .gaai/project that artefact-sync.sh reports PASSED on. +build_passing_project() { + local proj="$1" + mkdir -p "$proj"/contexts/memory "$proj"/contexts/backlog "$proj"/contexts/artefacts + for f in active blocked _template; do + echo "items: []" > "$proj/contexts/backlog/$f.backlog.yaml" + done +} + +new_sandbox() { + local tmp; tmp="$(mktemp -d)" + TMP_DIRS+=("$tmp") + echo "$tmp" +} + +# --------------------------------------------------------------------------- +# A/B — happy path: exactly-once discovery, non-matching entries ignored. +# --------------------------------------------------------------------------- +scenario_happy_path_and_discovery() { + local tmp core proj marker out rc lines + tmp="$(new_sandbox)"; core="$tmp/core"; proj="$tmp/project" + build_passing_core "$core"; build_passing_project "$proj" + + marker="$tmp/marker.txt"; : > "$marker" + export MARKER_FILE="$marker" + + cat > "$core/scripts/tests/aaa-fixture.sh" <<'EOF' +#!/usr/bin/env bash +echo "shell:aaa" >> "$MARKER_FILE" +exit 0 +EOF + chmod +x "$core/scripts/tests/aaa-fixture.sh" + + mkdir -p "$core/adapters/demo/__tests__" + cat > "$core/adapters/demo/__tests__/aaa.test.js" <<'EOF' +require("fs").appendFileSync(process.env.MARKER_FILE, "node:aaa\n"); +process.exit(0); +EOF + + # Poison / decoy entries — must never execute: + # subdir under scripts/tests/ (excluded by -maxdepth 1) + mkdir -p "$core/scripts/tests/helpers" + cat > "$core/scripts/tests/helpers/poison.sh" <<'EOF' +#!/usr/bin/env bash +echo "shell:poison-subdir" >> "$MARKER_FILE" +exit 0 +EOF + chmod +x "$core/scripts/tests/helpers/poison.sh" + # non-.sh file directly under scripts/tests/ + echo "not a test" > "$core/scripts/tests/notes.md" + # .js file under adapters/ but NOT inside __tests__/ (excluded by path pattern) + cat > "$core/adapters/demo/helper.fixture.js" <<'EOF' +require("fs").appendFileSync(process.env.MARKER_FILE, "node:poison-not-in-tests-dir\n"); +EOF + # non-.test.js file inside __tests__/ (excluded by name pattern) + echo "not a test" > "$core/adapters/demo/__tests__/readme.md" + + out="$("$core/scripts/validate-public-release.sh" --core-dir "$core" --project-dir "$proj" 2>&1)"; rc=$? + + [[ $rc -eq 0 ]] && pass "A: happy path exits 0" || fail "A: happy path expected exit 0, got $rc — $out" + if grep -qx "shell:aaa" "$marker" 2>/dev/null && grep -qx "node:aaa" "$marker" 2>/dev/null; then + pass "A: both real fixtures executed" + else + fail "A: expected fixtures missing from marker: $(cat "$marker" 2>/dev/null)" + fi + lines=$(wc -l < "$marker" | tr -d ' ') + [[ "$lines" == "2" ]] && pass "B: exactly 2 marker lines — decoys never ran, no double-count" \ + || fail "B: expected exactly 2 marker lines, got $lines: $(cat "$marker" 2>/dev/null)" + + unset MARKER_FILE +} + +# --------------------------------------------------------------------------- +# C — AC3: non-executable matched shell test. +# --------------------------------------------------------------------------- +scenario_non_executable() { + local tmp core proj marker out rc + tmp="$(new_sandbox)"; core="$tmp/core"; proj="$tmp/project" + build_passing_core "$core"; build_passing_project "$proj" + + marker="$tmp/marker.txt"; : > "$marker" + export MARKER_FILE="$marker" + + cat > "$core/scripts/tests/broken.sh" <<'EOF' +#!/usr/bin/env bash +echo "shell:broken-should-not-run" >> "$MARKER_FILE" +exit 0 +EOF + chmod -x "$core/scripts/tests/broken.sh" + + mkdir -p "$core/adapters/demo/__tests__" + cat > "$core/adapters/demo/__tests__/zzz.test.js" <<'EOF' +require("fs").appendFileSync(process.env.MARKER_FILE, "node:should-not-run\n"); +process.exit(0); +EOF + + out="$("$core/scripts/validate-public-release.sh" --core-dir "$core" --project-dir "$proj" 2>&1)"; rc=$? + + [[ $rc -ne 0 ]] && pass "C: non-executable fixture → non-zero exit" || fail "C: expected non-zero exit, got 0" + if echo "$out" | grep -qF "$core/scripts/tests/broken.sh" && echo "$out" | grep -q "not executable"; then + pass "C: exact path + 'not executable' reported" + else + fail "C: expected path + 'not executable' in output — got: $out" + fi + [[ ! -s "$marker" ]] && pass "C: no fixture executed (fail-fast before any test runs)" \ + || fail "C: marker unexpectedly non-empty: $(cat "$marker")" + + unset MARKER_FILE +} + +# --------------------------------------------------------------------------- +# D — AC3 + AC6: shell test failure mid-phase; next-path + next-phase report. +# --------------------------------------------------------------------------- +scenario_shell_failure_fail_fast() { + local tmp core proj marker out rc + tmp="$(new_sandbox)"; core="$tmp/core"; proj="$tmp/project" + build_passing_core "$core"; build_passing_project "$proj" + + marker="$tmp/marker.txt"; : > "$marker" + export MARKER_FILE="$marker" + + cat > "$core/scripts/tests/a-pass.sh" <<'EOF' +#!/usr/bin/env bash +echo "shell:a-pass" >> "$MARKER_FILE" +exit 0 +EOF + cat > "$core/scripts/tests/b-fail.sh" <<'EOF' +#!/usr/bin/env bash +exit 1 +EOF + cat > "$core/scripts/tests/c-should-not-run.sh" <<'EOF' +#!/usr/bin/env bash +echo "shell:c-should-not-run" >> "$MARKER_FILE" +exit 0 +EOF + chmod +x "$core/scripts/tests/a-pass.sh" "$core/scripts/tests/b-fail.sh" "$core/scripts/tests/c-should-not-run.sh" + + mkdir -p "$core/adapters/demo/__tests__" + cat > "$core/adapters/demo/__tests__/zzz.test.js" <<'EOF' +require("fs").appendFileSync(process.env.MARKER_FILE, "node:should-not-run\n"); +process.exit(0); +EOF + + out="$("$core/scripts/validate-public-release.sh" --core-dir "$core" --project-dir "$proj" 2>&1)"; rc=$? + + [[ $rc -eq 1 ]] && pass "D: b-fail.sh exit 1 propagates as entrypoint exit 1" || fail "D: expected exit 1, got $rc" + if echo "$out" | grep -qF "$core/scripts/tests/b-fail.sh" && echo "$out" | grep -q "exit 1"; then + pass "D: exact failing path + exit code reported" + else + fail "D: missing failing-path diagnostic — got: $out" + fi + echo "$out" | grep -qF "next test path (not run): $core/scripts/tests/c-should-not-run.sh" \ + && pass "D: next known test path (c-should-not-run.sh) reported" \ + || fail "D: next-test-path report missing/wrong — got: $out" + echo "$out" | grep -q "next unstarted phase: node-tests" \ + && pass "D: next unstarted phase (node-tests) reported" \ + || fail "D: next-phase report missing/wrong — got: $out" + if grep -qx "shell:a-pass" "$marker" 2>/dev/null && [[ "$(wc -l < "$marker" | tr -d ' ')" == "1" ]]; then + pass "D: a-pass ran, c-should-not-run and node fixture never ran" + else + fail "D: unexpected marker content: $(cat "$marker" 2>/dev/null)" + fi + + unset MARKER_FILE +} + +# --------------------------------------------------------------------------- +# E — AC3: shell test killed by signal → reported as signal, not exit code. +# --------------------------------------------------------------------------- +scenario_shell_signal() { + local tmp core proj out rc + tmp="$(new_sandbox)"; core="$tmp/core"; proj="$tmp/project" + build_passing_core "$core"; build_passing_project "$proj" + + cat > "$core/scripts/tests/self-kill.sh" <<'EOF' +#!/usr/bin/env bash +kill -9 $$ +EOF + chmod +x "$core/scripts/tests/self-kill.sh" + + out="$("$core/scripts/validate-public-release.sh" --core-dir "$core" --project-dir "$proj" 2>&1)"; rc=$? + + [[ $rc -eq 137 ]] && pass "E: signal-killed fixture propagates rc=137 (128+9)" || fail "E: expected rc=137, got $rc" + echo "$out" | grep -q "terminated by signal 9" \ + && pass "E: reported as 'terminated by signal 9', not a bogus exit code" \ + || fail "E: signal not reported correctly — got: $out" +} + +# --------------------------------------------------------------------------- +# F — AC3: scripts/tests/ cannot be traversed/listed (permission denied). +# --------------------------------------------------------------------------- +scenario_tests_dir_unreadable() { + local tmp core proj out rc + tmp="$(new_sandbox)"; core="$tmp/core"; proj="$tmp/project" + build_passing_core "$core"; build_passing_project "$proj" + + chmod 000 "$core/scripts/tests" + out="$("$core/scripts/validate-public-release.sh" --core-dir "$core" --project-dir "$proj" 2>&1)"; rc=$? + chmod 755 "$core/scripts/tests" # restore so sandbox cleanup can proceed + + [[ $rc -ne 0 ]] && pass "F: unreadable scripts/tests/ → non-zero exit" || fail "F: expected non-zero exit, got 0" + if echo "$out" | grep -q "cannot traverse/list" && echo "$out" | grep -qF "$core/scripts/tests"; then + pass "F: traversal-failure reason + directory path reported" + else + fail "F: expected traversal-failure diagnostic — got: $out" + fi +} + +# --------------------------------------------------------------------------- +# G — AC6: adapters/ missing entirely → node-tests traversal failure, final +# phase (no "next unstarted phase" to name). +# --------------------------------------------------------------------------- +scenario_adapters_dir_missing() { + local tmp core proj out rc + tmp="$(new_sandbox)"; core="$tmp/core"; proj="$tmp/project" + build_passing_core "$core"; build_passing_project "$proj" + rm -rf "$core/adapters" + + out="$("$core/scripts/validate-public-release.sh" --core-dir "$core" --project-dir "$proj" 2>&1)"; rc=$? + + [[ $rc -ne 0 ]] && pass "G: missing adapters/ → non-zero exit" || fail "G: expected non-zero exit, got 0" + if echo "$out" | grep -q "cannot traverse/list" && echo "$out" | grep -qF "$core/adapters"; then + pass "G: adapters traversal-failure reason + path reported" + else + fail "G: expected traversal-failure diagnostic — got: $out" + fi +} + +# --------------------------------------------------------------------------- +# H — AC6: phase 1 (health-check) failure stops everything before phase 3/4 +# even runs discovery; no phantom "next test path". +# --------------------------------------------------------------------------- +scenario_phase1_failure_stops_everything() { + local tmp core proj marker out rc + tmp="$(new_sandbox)"; core="$tmp/core"; proj="$tmp/project" + build_passing_core "$core"; build_passing_project "$proj" + rm -f "$core/VERSION" # breaks health-check.sh's VERSION-exists check + + marker="$tmp/marker.txt"; : > "$marker" + export MARKER_FILE="$marker" + cat > "$core/scripts/tests/should-not-run.sh" <<'EOF' +#!/usr/bin/env bash +echo "shell:should-not-run" >> "$MARKER_FILE" +exit 0 +EOF + chmod +x "$core/scripts/tests/should-not-run.sh" + + out="$("$core/scripts/validate-public-release.sh" --core-dir "$core" --project-dir "$proj" 2>&1)"; rc=$? + + [[ $rc -ne 0 ]] && pass "H: health-check failure → non-zero exit" || fail "H: expected non-zero exit, got 0" + echo "$out" | grep -q "next unstarted phase: artefact-sync" \ + && pass "H: reports next unstarted phase artefact-sync" \ + || fail "H: missing next-phase report — got: $out" + echo "$out" | grep -q "next test path" \ + && fail "H: unexpectedly reported a next test path (discovery never ran) — got: $out" \ + || pass "H: no phantom next-test-path (discovery correctly never ran)" + [[ ! -s "$marker" ]] && pass "H: no shell test executed (fail-fast before phase 3)" \ + || fail "H: marker unexpectedly written: $(cat "$marker")" + + unset MARKER_FILE +} + +# --------------------------------------------------------------------------- +# I — AC1: shell fixtures execute in lexical path order. +# --------------------------------------------------------------------------- +scenario_lexical_order() { + local tmp core proj marker n rc actual + tmp="$(new_sandbox)"; core="$tmp/core"; proj="$tmp/project" + build_passing_core "$core"; build_passing_project "$proj" + + marker="$tmp/marker.txt"; : > "$marker" + export MARKER_FILE="$marker" + for n in c b a; do + cat > "$core/scripts/tests/${n}-fixture.sh" <> "\$MARKER_FILE" +exit 0 +EOF + chmod +x "$core/scripts/tests/${n}-fixture.sh" + done + + "$core/scripts/validate-public-release.sh" --core-dir "$core" --project-dir "$proj" >/dev/null 2>&1 + rc=$? + [[ $rc -eq 0 ]] || fail "I: expected clean pass, got exit $rc" + actual="$(tr '\n' ',' < "$marker")" + [[ "$actual" == "shell:a,shell:b,shell:c," ]] \ + && pass "I: shell fixtures executed in lexical order (a,b,c)" \ + || fail "I: expected lexical order shell:a,shell:b,shell:c — got: $actual" + + unset MARKER_FILE +} + +# --------------------------------------------------------------------------- +# J — AC3 + AC6: Node invocation failure, exact exit code propagation, next +# known test path within the (final) node-tests phase. +# --------------------------------------------------------------------------- +scenario_node_failure_fail_fast() { + local tmp core proj marker out rc + tmp="$(new_sandbox)"; core="$tmp/core"; proj="$tmp/project" + build_passing_core "$core"; build_passing_project "$proj" + + marker="$tmp/marker.txt"; : > "$marker" + export MARKER_FILE="$marker" + mkdir -p "$core/adapters/demo/__tests__" + cat > "$core/adapters/demo/__tests__/a-pass.test.js" <<'EOF' +require("fs").appendFileSync(process.env.MARKER_FILE, "node:a-pass\n"); +process.exit(0); +EOF + cat > "$core/adapters/demo/__tests__/b-fail.test.js" <<'EOF' +process.exit(3); +EOF + cat > "$core/adapters/demo/__tests__/c-should-not-run.test.js" <<'EOF' +require("fs").appendFileSync(process.env.MARKER_FILE, "node:c-should-not-run\n"); +process.exit(0); +EOF + + out="$("$core/scripts/validate-public-release.sh" --core-dir "$core" --project-dir "$proj" 2>&1)"; rc=$? + + [[ $rc -eq 3 ]] && pass "J: node fixture exit 3 propagates as entrypoint exit 3" || fail "J: expected exit 3, got $rc" + if echo "$out" | grep -qF "$core/adapters/demo/__tests__/b-fail.test.js" && echo "$out" | grep -q "exit 3"; then + pass "J: exact failing Node path + exit code reported" + else + fail "J: missing failing-path diagnostic — got: $out" + fi + echo "$out" | grep -qF "next test path (not run): $core/adapters/demo/__tests__/c-should-not-run.test.js" \ + && pass "J: next known test path (c-should-not-run.test.js) reported" \ + || fail "J: next-test-path report missing/wrong — got: $out" + echo "$out" | grep -q "next unstarted phase: (none — final phase)" \ + && pass "J: final-phase 'no next phase' reported" \ + || fail "J: final-phase report missing/wrong — got: $out" + if grep -qx "node:a-pass" "$marker" 2>/dev/null && [[ "$(wc -l < "$marker" | tr -d ' ')" == "1" ]]; then + pass "J: a-pass ran, c-should-not-run never ran" + else + fail "J: unexpected marker content: $(cat "$marker" 2>/dev/null)" + fi + + unset MARKER_FILE +} + +# --------------------------------------------------------------------------- +echo "GAAI validate-public-release.sh — hermetic contract test" +echo "==========================================================" +echo "" +echo "=== A/B: happy path + exactly-once discovery (ignores non-matching entries) ===" +scenario_happy_path_and_discovery +echo "" +echo "=== C: non-executable matched shell test (AC3) ===" +scenario_non_executable +echo "" +echo "=== D: shell test failure — fail-fast + next-path/next-phase report (AC3+AC6) ===" +scenario_shell_failure_fail_fast +echo "" +echo "=== E: shell test killed by signal (AC3) ===" +scenario_shell_signal +echo "" +echo "=== F: scripts/tests/ unreadable — traversal failure (AC3) ===" +scenario_tests_dir_unreadable +echo "" +echo "=== G: adapters/ missing — traversal failure, final phase (AC3+AC6) ===" +scenario_adapters_dir_missing +echo "" +echo "=== H: phase 1 (health-check) failure stops everything (AC6) ===" +scenario_phase1_failure_stops_everything +echo "" +echo "=== I: lexical path ordering (AC1) ===" +scenario_lexical_order +echo "" +echo "=== J: Node invocation failure — fail-fast + next-path report (AC3+AC6) ===" +scenario_node_failure_fail_fast + +echo "" +echo "==========================================================" +echo "Results: $PASS_COUNT passed, $FAIL_COUNT failed" +echo "" + +if [[ $FAIL_COUNT -gt 0 ]]; then + echo "❌ validate-public-release.sh contract test FAILED" + exit 1 +else + echo "✅ validate-public-release.sh contract test PASSED" + exit 0 +fi diff --git a/.gaai/core/scripts/tests/worktree-integrity.test.sh b/.gaai/core/scripts/tests/worktree-integrity.test.sh old mode 100644 new mode 100755 diff --git a/.gaai/core/scripts/tests/worktree-reconcile.test.sh b/.gaai/core/scripts/tests/worktree-reconcile.test.sh old mode 100644 new mode 100755 diff --git a/.gaai/core/scripts/tests/wrapper-output-capture.test.sh b/.gaai/core/scripts/tests/wrapper-output-capture.test.sh old mode 100644 new mode 100755 diff --git a/.gaai/core/scripts/validate-public-release.sh b/.gaai/core/scripts/validate-public-release.sh new file mode 100755 index 00000000..f17c8b1d --- /dev/null +++ b/.gaai/core/scripts/validate-public-release.sh @@ -0,0 +1,190 @@ +#!/usr/bin/env bash +set -uo pipefail + +############################################################ +# Validate Public Release — GAAI +# +# Description: +# Single deterministic entrypoint for public-release validation. +# Runs, in fixed order: (1) health-check, (2) artefact-sync, +# (3) every hermetic shell test discovered under +# scripts/tests/*.sh, (4) every hermetic Node test discovered +# under adapters/**/__tests__/*.test.js. Discovery is filesystem- +# driven — no manifest, no expected count, no test framework. +# Fails fast: the first failing check stops the run, preserves +# its output and original exit code/signal, and reports the +# next unstarted phase (and, if already discovered, the next +# test path that would have run). +# +# Usage: +# ./scripts/validate-public-release.sh [--core-dir ] [--project-dir ] +# +# Exit codes: +# 0 — all phases passed +# non-zero — the failing phase's/test's original exit code +# (or 128+signal if a test was killed by a signal) +############################################################ + +CORE_DIR="" +PROJECT_DIR="" + +while [[ $# -gt 0 ]]; do + case "$1" in + --core-dir) CORE_DIR="$2"; shift 2 ;; + --project-dir) PROJECT_DIR="$2"; shift 2 ;; + *) >&2 echo "Unknown option: $1"; exit 1 ;; + esac +done + +# Auto-detect if not specified (same convention as health-check.sh) +if [[ -z "$CORE_DIR" ]]; then + SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + CORE_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" +fi +if [[ -z "$PROJECT_DIR" ]]; then + if [[ -d "$CORE_DIR/../project" ]]; then + PROJECT_DIR="$CORE_DIR/../project" + else + PROJECT_DIR="$CORE_DIR" # v1.x flat layout + fi +fi + +TESTS_DIR="$CORE_DIR/scripts/tests" +ADAPTERS_DIR="$CORE_DIR/adapters" + +echo "" +echo "GAAI Public Release Validation" +echo " core: $CORE_DIR" +echo " project: $PROJECT_DIR" +echo "================================" + +# Prints the fail-fast diagnostic block required by AC3/AC6: the exact +# path (if any) + reason, the next unstarted phase, and — only when +# discovery for the CURRENT phase already ran — the next test path that +# would have executed had the run continued. +fail_phase() { + local phase="$1" reason="$2" next_phase="$3" next_path="${4:-}" + echo "" + echo "❌ FAILED: phase '$phase' — $reason" + echo " next unstarted phase: $next_phase" + if [[ -n "$next_path" ]]; then + echo " next test path (not run): $next_path" + fi +} + +# Renders a raw exit status as either "exit N" or "terminated by signal N" +# (bash reports a signal-killed child as exit status 128+N). +describe_status() { + local rc="$1" + if (( rc > 128 )); then + echo "terminated by signal $((rc - 128))" + else + echo "exit $rc" + fi +} + +# --------------------------------------------------------------------------- +# Phase 1: health-check +# --------------------------------------------------------------------------- +echo "" +echo "[ Phase 1/4: health-check ]" +bash "$CORE_DIR/scripts/health-check.sh" --core-dir "$CORE_DIR" --project-dir "$PROJECT_DIR" +rc=$? +if [[ $rc -ne 0 ]]; then + fail_phase "health-check" "$(describe_status "$rc")" "artefact-sync" + exit "$rc" +fi + +# --------------------------------------------------------------------------- +# Phase 2: artefact-sync +# --------------------------------------------------------------------------- +echo "" +echo "[ Phase 2/4: artefact-sync ]" +bash "$CORE_DIR/scripts/artefact-sync.sh" --gaai-dir "$PROJECT_DIR" +rc=$? +if [[ $rc -ne 0 ]]; then + fail_phase "artefact-sync" "$(describe_status "$rc")" "shell-tests" + exit "$rc" +fi + +# --------------------------------------------------------------------------- +# Phase 3: shell tests — every regular *.sh file immediately under +# scripts/tests/, lexical path order, run as executables. +# --------------------------------------------------------------------------- +echo "" +echo "[ Phase 3/4: shell tests ]" +FIND_OUT="$(mktemp)" +FIND_ERR="$(mktemp)" +find "$TESTS_DIR" -maxdepth 1 -type f -name '*.sh' >"$FIND_OUT" 2>"$FIND_ERR" +find_rc=$? +if [[ $find_rc -ne 0 ]]; then + fail_phase "shell-tests" "cannot traverse/list $TESTS_DIR — $(cat "$FIND_ERR")" "node-tests" + rm -f "$FIND_OUT" "$FIND_ERR" + exit "$find_rc" +fi +sort "$FIND_OUT" -o "$FIND_OUT" +mapfile -t SHELL_TESTS < "$FIND_OUT" +rm -f "$FIND_OUT" "$FIND_ERR" + +shell_total=${#SHELL_TESTS[@]} +for ((i = 0; i < shell_total; i++)); do + path="${SHELL_TESTS[$i]}" + next_path="" + if (( i + 1 < shell_total )); then + next_path="${SHELL_TESTS[$((i + 1))]}" + fi + + if [[ ! -x "$path" ]]; then + fail_phase "shell-tests" "$path — not executable" "node-tests" "$next_path" + exit 1 + fi + + "$path" + rc=$? + if [[ $rc -ne 0 ]]; then + fail_phase "shell-tests" "$path — $(describe_status "$rc")" "node-tests" "$next_path" + exit "$rc" + fi + echo " ✅ $path" +done + +# --------------------------------------------------------------------------- +# Phase 4: Node tests — every *.test.js file under adapters/**/__tests__/, +# lexical path order, invoked directly via `node "$path"`. +# --------------------------------------------------------------------------- +echo "" +echo "[ Phase 4/4: node tests ]" +FIND_OUT="$(mktemp)" +FIND_ERR="$(mktemp)" +find "$ADAPTERS_DIR" -type f -path '*/__tests__/*.test.js' >"$FIND_OUT" 2>"$FIND_ERR" +find_rc=$? +if [[ $find_rc -ne 0 ]]; then + fail_phase "node-tests" "cannot traverse/list $ADAPTERS_DIR — $(cat "$FIND_ERR")" "(none — final phase)" + rm -f "$FIND_OUT" "$FIND_ERR" + exit "$find_rc" +fi +sort "$FIND_OUT" -o "$FIND_OUT" +mapfile -t NODE_TESTS < "$FIND_OUT" +rm -f "$FIND_OUT" "$FIND_ERR" + +node_total=${#NODE_TESTS[@]} +for ((i = 0; i < node_total; i++)); do + path="${NODE_TESTS[$i]}" + next_path="" + if (( i + 1 < node_total )); then + next_path="${NODE_TESTS[$((i + 1))]}" + fi + + node "$path" + rc=$? + if [[ $rc -ne 0 ]]; then + fail_phase "node-tests" "$path — $(describe_status "$rc")" "(none — final phase)" "$next_path" + exit "$rc" + fi + echo " ✅ $path" +done + +echo "" +echo "================================" +echo "✅ Public release validation PASSED (health-check, artefact-sync, $shell_total shell tests, $node_total node tests)" +exit 0