Files
ironclaw/scripts/test-pre-commit-safety.sh
Illia Polosukhin b6da0272a8 refactor(tier-b): delete v1 legacy monolith (src/) and cut deploy over to Reborn (#6375)
* refactor(tier-b): delete v1 legacy monolith (src/) and cut deploy over to Reborn

Removes the v1 `src/` monolith (package `ironclaw_legacy`, binary
`ironclaw-legacy`) and its two legacy-only crates (`ironclaw_gateway`,
`ironclaw_tui`), then repoints the production deploy pipeline at the
already-production-grade Reborn image. This is a real production cutover,
not just a code cleanup — see issue #6369 for the tracked capability gaps
(sandboxed job execution, TUI/Signal channels, heartbeat, self-repair)
that this retirement leaves behind and that were accepted before proceeding.

Deletion:
- `src/` (v1 monolith), `crates/ironclaw_gateway`, `crates/ironclaw_tui`,
  root `build.rs` (built the deleted legacy binary's embedded assets).
- Root `Cargo.toml` repurposed: the `[package]` is now the test-only host
  `ironclaw_reborn_integration_tests` for the Reborn integration `[[test]]`
  suite — it has no lib/bin of its own.
- ~89 legacy-coupled `tests/*.rs` files + the legacy `tests/support/`
  harness; the genuinely Reborn-only tests and shared support submodules
  are kept.

Migration tool decoupling (completes the Tier B follow-up deferred by
PR #6368): `ironclaw_reborn_migration` no longer has any `ironclaw_legacy`
edge — `tests/migration_roundtrip.rs` now seeds its v1 fixture with raw SQL
against a frozen schema snapshot (`tests/fixtures/legacy_v1_schema.sql`),
re-encrypting its seeded secret with the same AES-256-GCM + HKDF-SHA256
scheme the frozen decrypt path reads back.

Deploy / CI repointed at Reborn:
- Root `Dockerfile` is now the Reborn multi-stage build (was
  `Dockerfile.reborn`); deleted `Dockerfile.worker`, `Dockerfile.test`,
  `docker/sandbox.Dockerfile`, and `railway.reborn.toml`.
- `railway.toml`, `deploy/ironclaw.service`, `deploy/env.example` repointed
  at the Reborn entrypoint contract.
- Deleted orphaned legacy `test.yml`/`e2e.yml` workflows; removed the
  legacy Docker-worker build, `runtime-staging` target, gateway JS/boundary
  jobs, and legacy `ironclaw_legacy` build steps from the remaining
  workflows; repointed the WIT instantiation check at
  `ironclaw_wasm --test wit_tool_runtime_contract` and the Windows matrix
  at workspace-wide features.
- `classify-test-scope.sh` + self-test and the change-scope regexes drop
  dead `src/` paths; `check_gateway_boundaries.py` (legacy web gateway)
  deleted.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(tier-b): repoint scripts + e2e + docs off the deleted v1 src/

Follow-on cleanup to the src/ deletion. No production code change.

Scripts / hooks:
- Delete `build-all.sh` (built the deleted `ironclaw-legacy` binary) and
  `check-i18n-parity.sh` (validated the deleted gateway `.js` language packs;
  Reborn WebUI i18n is `.ts`, checked by the frontend's `pnpm lint`/`pnpm test`).
- `check-version-bumps.sh`: repoint the WIT_TOOL_VERSION cross-check at
  `crates/ironclaw_wasm/src/config.rs`; drop the channel-WIT constant check
  (no Reborn host constant exists).
- `commit-msg-regression.sh` + `.githooks/pre-commit`: drop the gateway i18n
  block and repoint the static-asset exemption at
  `crates/ironclaw_webui/frontend/public/`.
- `pre-commit-safety.sh` + its self-test: remove the v1 gateway-specific checks
  (DISPATCH / CREDNAME / SSE-projection / multi-tenant-broadcast, i18n,
  gateway-JS) keyed on the deleted `src/channels/web/`; keep the general +
  Reborn-applicable checks (UTF-8, panics, ARCH-SPRAWL, composition budget).
- `composition-budget.toml`: drop the stale `src/slack`/`src/extension_host`
  exclusion note.

E2E:
- Delete the 8 legacy-gateway browser scenarios that drove the deleted binary.
- Rewrite `tests/e2e/CLAUDE.md` to make Reborn WebChat v2 the sole live surface
  and flag the remaining legacy `conftest.py` fixtures / `test_v2_*` coupling as
  a tracked follow-up (issue #6369). CI-gating Reborn scenarios use the separate
  `reborn_webui_harness` fixture and are unaffected.

Docs:
- `CLAUDE.md`: replace the `src/` Project Structure tree, "Where to Build",
  Extension/Auth ownership, Module Specs rows, Database/Channel/Tools/Workspace
  pointers, and Debugging commands with their Reborn equivalents.
- `.claude/rules/*`: drop dead `src/**` frontmatter globs and prose references.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(tier-b): address CI + review findings on the src/ deletion PR

- Formatting: `cargo fmt` the migration roundtrip test (raw-SQL rewrite) and
  smoke.rs (fixes the Formatting / Code Style CI failures).
- `tests/dockerfile_runtime_home.rs` (auto-discovered root test, missed in the
  first pass): repoint its `Dockerfile.reborn` reads at the canonical
  `Dockerfile`, update the runtime-home assertions to the Reborn image's
  `/workspace` + `/data/ironclaw-reborn` layout, and update the CI-coverage
  check to the `docker build --target runtime .` command. This was failing
  `cargo test --workspace` at runtime.
- Railway preview deploy failure: the entrypoint now binds `0.0.0.0` when a
  Railway runtime is detected (RAILWAY_* markers) and no explicit
  IRONCLAW_REBORN_SERVE_HOST is set, so the platform health check / ingress can
  reach the container; the conservative loopback default is preserved
  off-Railway. Added regression tests for both arms.
- `docker.yml`: extract the release version from
  `crates/ironclaw_reborn_cli/Cargo.toml` (the shipped `ironclaw` package,
  1.0.0-rc.1), not the root test-only package (0.1.0), so release image tags
  are correct.
- Dockerfile: pin the `debian:bookworm-slim` runtime base by digest (matches
  the digest-pinned chef/node stages; the runtime image is the security
  boundary).
- migration_roundtrip.rs: drop the no-op `#[allow(clippy::too_many_arguments)]`
  on `insert_routine` (6 params, below the threshold).
- Cargo.toml: restore the `replay` feature's descriptive comment; stale
  `-p ironclaw_legacy` run instruction fixed in the live GitHub PAT contract test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(tier-b): keep the test-host package out of the reborn crate-bucket matrix

The `Tests (Reborn)` failure (`Test Reborn crate bucket (adapters-misc)`)
came from the root package rename, not a code change.

`reborn-tests.yml`'s crate-bucket discovery allowlists any package whose name
`startswith("ironclaw_reborn")`. Renaming the root workspace package to
`ironclaw_reborn_integration_tests` (Tier B) made it match, so the matrix ran
`cargo test -p ironclaw_reborn_integration_tests --all-targets` in a crate
bucket — redundantly re-running every reborn `[[test]]` binary. That surfaced
`reborn_integration_telegram_journey`'s slack-gate deny-arm scenario, which
fails deterministically under the plain `-p <pkg> --all-targets` invocation
(it also fails that way on `main` locally; it is only ever green in the
`--workspace` integration-coverage lane, and the flat-file int-tier / group
discovery never selected it — so it never ran on `main`'s CI at all).

Pre-Tier-B this was implicit: the host package was `ironclaw_legacy`, which
never matched the allowlist and was not in the `ironclaw` CLI dependency
closure, so its `[[test]]` targets only ran via the dedicated root /
integration-coverage / group jobs. Exclude the test-host package by name to
restore exactly that behavior. The integration `[[test]]` suites continue to
run in their owning lanes; no coverage is lost relative to `main`.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 17:16:18 -07:00

340 lines
14 KiB
Bash
Executable File

#!/usr/bin/env bash
# Regression tests for the grep pipelines in `pre-commit-safety.sh`.
#
# The PANIC / ARCH-SPRAWL checks pipe through
# `grep -nE '^\+' | grep -E <positive> | grep -vE <exclusions>`.
# A previous version of the exclusion regex used `^\+\+\+` to filter
# diff header lines (`+++ b/file.rs`), which silently never fired —
# `grep -n` prepends a `N:` line-number prefix, so `^` no longer
# anchors against the `+++` bytes. This test locks in the corrected
# `:\+\+\+ ` shape.
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$ROOT_DIR"
PASS=0
FAIL=0
assert_filtered() {
local label="$1" input="$2" positive="$3" exclusions="$4"
# Emulate the production pipeline: `grep -n '^+'` adds the line-number
# prefix, then positive/negative filters run against that shape.
local result
if result=$(printf '%s\n' "$input" \
| grep -nE '^\+' \
| grep -E "$positive" \
| grep -vE "$exclusions" \
| head -5 || true); then :; fi
if [ -z "${result:-}" ]; then
echo "OK: $label (correctly filtered)"
PASS=$((PASS + 1))
else
echo "FAIL: $label — line leaked past exclusions:"
echo "$result" | sed 's/^/ /'
FAIL=$((FAIL + 1))
fi
}
assert_flagged() {
local label="$1" input="$2" positive="$3" exclusions="$4"
local result
if result=$(printf '%s\n' "$input" \
| grep -nE '^\+' \
| grep -E "$positive" \
| grep -vE "$exclusions" \
| head -5 || true); then :; fi
if [ -n "${result:-}" ]; then
echo "OK: $label (correctly flagged)"
PASS=$((PASS + 1))
else
echo "FAIL: $label — line not flagged by positive pattern"
FAIL=$((FAIL + 1))
fi
}
assert_precommit_blocks() {
local label="$1" path="$2" base_content="$3" changed_content="$4" expected="$5"
local tmp output status
tmp=$(mktemp -d "${TMPDIR:-/tmp}/precommit-safety.XXXXXX")
set +e
(
cd "$tmp"
git init -q
git config user.email test@example.com
git config user.name "Test User"
mkdir -p "$(dirname "$path")"
printf '%b' "$base_content" > "$path"
git add "$path"
git commit -qm base
printf '%b' "$changed_content" > "$path"
git add "$path"
set +e
output=$("$ROOT_DIR/scripts/pre-commit-safety.sh" 2>&1)
status=$?
set -e
if [ "$status" -ne 0 ] && printf '%s\n' "$output" | grep -Fq "$expected"; then
echo "OK: $label (pre-commit blocked)"
exit 0
fi
echo "FAIL: $label — expected block containing '$expected', got status $status"
printf '%s\n' "$output" | sed 's/^/ /'
exit 1
)
status=$?
set -e
rm -rf "$tmp"
if [ "$status" -eq 0 ]; then
PASS=$((PASS + 1))
else
FAIL=$((FAIL + 1))
fi
}
assert_precommit_allows() {
local label="$1" path="$2" base_content="$3" changed_content="$4"
local tmp output status
tmp=$(mktemp -d "${TMPDIR:-/tmp}/precommit-safety.XXXXXX")
set +e
(
cd "$tmp"
git init -q
git config user.email test@example.com
git config user.name "Test User"
mkdir -p "$(dirname "$path")"
printf '%b' "$base_content" > "$path"
git add "$path"
git commit -qm base
printf '%b' "$changed_content" > "$path"
git add "$path"
set +e
output=$("$ROOT_DIR/scripts/pre-commit-safety.sh" 2>&1)
status=$?
set -e
if [ "$status" -eq 0 ]; then
echo "OK: $label (pre-commit allowed)"
exit 0
fi
echo "FAIL: $label — expected allow, got status $status"
printf '%s\n' "$output" | sed 's/^/ /'
exit 1
)
status=$?
set -e
rm -rf "$tmp"
if [ "$status" -eq 0 ]; then
PASS=$((PASS + 1))
else
FAIL=$((FAIL + 1))
fi
}
assert_precommit_allows_with_unstaged() {
local label="$1" path="$2" base_content="$3" staged_content="$4" unstaged_content="$5"
local tmp output status
tmp=$(mktemp -d "${TMPDIR:-/tmp}/precommit-safety.XXXXXX")
set +e
(
cd "$tmp"
git init -q
git config user.email test@example.com
git config user.name "Test User"
mkdir -p "$(dirname "$path")"
printf '%b' "$base_content" > "$path"
git add "$path"
git commit -qm base
printf '%b' "$staged_content" > "$path"
git add "$path"
printf '%b' "$unstaged_content" > "$path"
set +e
output=$("$ROOT_DIR/scripts/pre-commit-safety.sh" 2>&1)
status=$?
set -e
if [ "$status" -eq 0 ]; then
echo "OK: $label (pre-commit allowed)"
exit 0
fi
echo "FAIL: $label — expected allow, got status $status"
printf '%s\n' "$output" | sed 's/^/ /'
exit 1
)
status=$?
set -e
rm -rf "$tmp"
if [ "$status" -eq 0 ]; then
PASS=$((PASS + 1))
else
FAIL=$((FAIL + 1))
fi
}
# Rust commonly keeps large unit-test modules in a sibling `*_tests.rs` file
# that is included by a `#[cfg(test)] mod ...;` declaration. Those files are
# test-only even though they do not live under a `tests/` directory.
assert_precommit_allows "PANIC: sibling *_tests.rs files are test-only" \
"crates/demo/src/auth_tests.rs" \
"fn existing_test() {}\n" \
"fn existing_test() {}\n#[test]\nfn new_test() { Result::<(), &str>::Ok(()).expect(\"fixture\"); }\n"
assert_precommit_allows "PANIC: sibling test_*.rs files are test-only" \
"crates/demo/src/test_auth.rs" \
"fn existing_test() {}\n" \
"fn existing_test() {}\n#[test]\nfn new_test() { Result::<(), &str>::Ok(()).expect(\"fixture\"); }\n"
assert_precommit_allows "PANIC: sibling *_test.rs files are test-only" \
"crates/demo/src/auth_test.rs" \
"fn existing_test() {}\n" \
"fn existing_test() {}\n#[test]\nfn new_test() { Result::<(), &str>::Ok(()).expect(\"fixture\"); }\n"
assert_precommit_allows_unstaged_diff() {
local label="$1" path="$2" base_content="$3" changed_content="$4"
local tmp output status
tmp=$(mktemp -d "${TMPDIR:-/tmp}/precommit-safety.XXXXXX")
set +e
(
cd "$tmp"
git init -q
git config user.email test@example.com
git config user.name "Test User"
mkdir -p "$(dirname "$path")"
printf '%b' "$base_content" > "$path"
git add "$path"
git commit -qm base
git branch -M main
printf '%b' "$changed_content" > "$path"
set +e
output=$("$ROOT_DIR/scripts/pre-commit-safety.sh" 2>&1)
status=$?
set -e
if [ "$status" -eq 0 ]; then
echo "OK: $label (standalone diff allowed)"
exit 0
fi
echo "FAIL: $label — expected allow, got status $status"
printf '%s\n' "$output" | sed 's/^/ /'
exit 1
)
status=$?
set -e
rm -rf "$tmp"
if [ "$status" -eq 0 ]; then
PASS=$((PASS + 1))
else
FAIL=$((FAIL + 1))
fi
}
# ── ARCH-SPRAWL integration checks ────────────────────────────
assert_precommit_blocks "ARCH-SPRAWL: too_many_arguments allow needs arch-exempt plan" \
"crates/demo/src/runtime.rs" \
"fn existing() {}\n" \
"#[allow(clippy::too_many_arguments)]\nfn execute(a: u8, b: u8, c: u8, d: u8, e: u8, f: u8, g: u8, h: u8) {}\n" \
"ARCH-SPRAWL"
assert_precommit_allows "ARCH-SPRAWL: annotated too_many_arguments allow is accepted" \
"crates/demo/src/runtime.rs" \
"fn existing() {}\n" \
"// arch-exempt: too_many_args, needs RuntimeInputs bundle, plan #2800\n#[allow(clippy::too_many_arguments)]\nfn execute(a: u8, b: u8, c: u8, d: u8, e: u8, f: u8, g: u8, h: u8) {}\n"
assert_precommit_allows "ARCH-SPRAWL: exemption reason may contain commas" \
"crates/demo/src/runtime.rs" \
"fn existing() {}\n" \
"// arch-exempt: too_many_args, split struct, add builder, plan #2800\n#[allow(clippy::too_many_arguments)]\nfn execute(a: u8, b: u8, c: u8, d: u8, e: u8, f: u8, g: u8, h: u8) {}\n"
assert_precommit_blocks "ARCH-SPRAWL: optional Arc plus with-builder needs arch-exempt plan" \
"crates/demo/src/runtime.rs" \
"fn existing() {}\n" \
"use std::sync::Arc;\ntrait Baz {}\nstruct Runtime {\n baz: Option<Arc<dyn Baz>>,\n}\nimpl Runtime {\n fn with_baz(mut self, baz: Arc<dyn Baz>) -> Self {\n self.baz = Some(baz);\n self\n }\n}\n" \
"ARCH-SPRAWL"
assert_precommit_allows "ARCH-SPRAWL: annotated optional Arc plus with-builder is accepted" \
"crates/demo/src/runtime.rs" \
"fn existing() {}\n" \
"use std::sync::Arc;\ntrait Baz {}\n// arch-exempt: optional_arc, feature-gated runtime adapter, plan #2800\nstruct Runtime {\n baz: Option<Arc<dyn Baz>>,\n}\nimpl Runtime {\n fn with_baz(mut self, baz: Arc<dyn Baz>) -> Self {\n self.baz = Some(baz);\n self\n }\n}\n"
assert_precommit_allows "ARCH-SPRAWL: optional Arc and unrelated with-builder in separate hunks are allowed" \
"crates/demo/src/runtime.rs" \
"use std::sync::Arc;\ntrait Baz {}\nstruct Runtime {\n}\n\n\n\n\n\n\n\n\nimpl Runtime {\n}\n" \
"use std::sync::Arc;\ntrait Baz {}\nstruct Runtime {\n baz: Option<Arc<dyn Baz>>,\n}\n\n\n\n\n\n\n\n\nimpl Runtime {\n fn with_cache(mut self, enabled: bool) -> Self {\n self\n }\n}\n"
assert_precommit_blocks "ARCH-SPRAWL: same-name optional Arc and with-builder in separate hunks still block" \
"crates/demo/src/runtime.rs" \
"use std::sync::Arc;\ntrait Baz {}\nstruct Runtime {\n}\n\n\n\n\n\n\n\n\nimpl Runtime {\n}\n" \
"use std::sync::Arc;\ntrait Baz {}\nstruct Runtime {\n baz: Option<Arc<dyn Baz>>,\n}\n\n\n\n\n\n\n\n\nimpl Runtime {\n fn with_baz(mut self, baz: Arc<dyn Baz>) -> Self {\n self.baz = Some(baz);\n self\n }\n}\n" \
"ARCH-SPRAWL"
assert_precommit_allows "ARCH-SPRAWL: annotated optional Arc and with-builder in separate hunks are accepted" \
"crates/demo/src/runtime.rs" \
"use std::sync::Arc;\ntrait Baz {}\nstruct Runtime {\n}\n\n\n\n\n\n\n\n\nimpl Runtime {\n}\n" \
"use std::sync::Arc;\ntrait Baz {}\n// arch-exempt: optional_arc, feature-gated runtime adapter, plan #2800\nstruct Runtime {\n baz: Option<Arc<dyn Baz>>,\n}\n\n\n\n\n\n\n\n\nimpl Runtime {\n fn with_baz(mut self, baz: Arc<dyn Baz>) -> Self {\n self.baz = Some(baz);\n self\n }\n}\n"
assert_precommit_allows "ARCH-SPRAWL: optional Arc and unrelated with-builder in same hunk are allowed" \
"crates/demo/src/runtime.rs" \
"use std::sync::Arc;\ntrait Baz {}\nstruct Runtime {\n}\nimpl Runtime {\n}\n" \
"use std::sync::Arc;\ntrait Baz {}\nstruct Runtime {\n baz: Option<Arc<dyn Baz>>,\n}\nimpl Runtime {\n fn with_cache(mut self, enabled: bool) -> Self {\n self\n }\n}\n"
assert_precommit_allows_unstaged_diff "ARCH-SPRAWL: existing optional Arc pair near unrelated edit is allowed" \
"crates/demo/src/runtime.rs" \
"use std::sync::Arc;\ntrait Baz {}\nstruct Runtime { baz: Option<Arc<dyn Baz>> }\nimpl Runtime { fn with_baz(mut self, baz: Arc<dyn Baz>) -> Self { self } }\n" \
"use std::sync::Arc;\ntrait Baz {}\nstruct Runtime { baz: Option<Arc<dyn Baz>> }\nimpl Runtime { fn with_baz(mut self, baz: Arc<dyn Baz>) -> Self { self } }\nfn unrelated() {}\n"
large_base=""
for i in $(seq 1 1501); do
large_base="${large_base}// existing ${i}
"
done
assert_precommit_blocks "ARCH-SPRAWL: large file additions need arch-exempt plan" \
"crates/demo/src/large.rs" \
"$large_base" \
"${large_base}// added line
" \
"ARCH-SPRAWL"
large_base_with_exempt="// arch-exempt: large_file, tracked decomposition, plan #2800
${large_base}"
assert_precommit_allows "ARCH-SPRAWL: existing large-file exemption covers later edits" \
"crates/demo/src/large.rs" \
"$large_base_with_exempt" \
"${large_base_with_exempt}// added line
"
assert_precommit_allows_with_unstaged "ARCH-SPRAWL: large-file check uses staged content, not unstaged working tree" \
"crates/demo/src/large.rs" \
"$large_base_with_exempt" \
"${large_base_with_exempt}// staged added line
" \
"${large_base}// unstaged edit removed exemption
"
assert_precommit_allows "ARCH-SPRAWL: single dispatcher call is allowed" \
"crates/demo/src/runtime.rs" \
"fn existing() {}\n" \
"fn execute(dispatcher: &Dispatcher, request: Request) {\n dispatcher.dispatch(request);\n}\n"
assert_precommit_blocks "ARCH-SPRAWL: repeated dispatcher calls need arch-exempt plan" \
"crates/demo/src/runtime.rs" \
"fn existing() {}\n" \
"fn execute_one(dispatcher: &Dispatcher, request: Request) {\n dispatcher.dispatch(request);\n}\nfn execute_two(dispatcher: &Dispatcher, request: Request) {\n dispatcher.dispatch(request);\n}\n" \
"ARCH-SPRAWL"
assert_precommit_blocks "ARCH-SPRAWL: repeated dispatcher calls in separate hunks need arch-exempt plan" \
"crates/demo/src/runtime.rs" \
"fn existing() {}\n\n\n\n\n\n\n\n\n" \
"fn existing() {}\nfn execute_one(dispatcher: &Dispatcher, request: Request) {\n dispatcher.dispatch(request);\n}\n\n\n\n\n\n\n\n\nfn execute_two(dispatcher: &Dispatcher, request: Request) {\n dispatcher.dispatch(request);\n}\n" \
"ARCH-SPRAWL"
assert_precommit_allows "ARCH-SPRAWL: annotated repeated dispatcher calls are accepted" \
"crates/demo/src/runtime.rs" \
"fn existing() {}\n" \
"// arch-exempt: parallel_dispatch, temporary split while gateway lands, plan #2800\nfn execute_one(dispatcher: &Dispatcher, request: Request) {\n dispatcher.dispatch(request);\n}\nfn execute_two(dispatcher: &Dispatcher, request: Request) {\n dispatcher.dispatch(request);\n}\n"
assert_precommit_allows "ARCH-SPRAWL: annotated repeated dispatcher calls in separate hunks are accepted" \
"crates/demo/src/runtime.rs" \
"fn existing() {}\n\n\n\n\n\n\n\n\n" \
"fn existing() {}\n// arch-exempt: parallel_dispatch, temporary split while gateway lands, plan #2800\nfn execute_one(dispatcher: &Dispatcher, request: Request) {\n dispatcher.dispatch(request);\n}\n\n\n\n\n\n\n\n\nfn execute_two(dispatcher: &Dispatcher, request: Request) {\n dispatcher.dispatch(request);\n}\n"
echo ""
echo "Passed: $PASS, Failed: $FAIL"
[ "$FAIL" -eq 0 ]