From 39bead136277b0d93aff5867c8195e8a32402f27 Mon Sep 17 00:00:00 2001 From: sebi Date: Sat, 19 Sep 2026 00:48:02 -0500 Subject: [PATCH 1/2] fix(check): probe the Redis at REDIS_URL, not a pinned 127.0.0.1:6379 The bin/check guard refused unless a Redis answered on 127.0.0.1:6379, even when REDIS_URL (which test_helper and RedisPool read) pointed somewhere else. developerz.ai is moving its box sidecar Redis off 6379, so a repo's own store can bind that port. The gate has to find the server the suite will actually use. The guard now parses REDIS_URL, falling back to redis://127.0.0.1:6379/0, so an unset REDIS_URL behaves exactly as before. bin/test-ecosystem uses DB 15 of the same server. Co-Authored-By: Claude Opus 5 (1M context) --- CHANGELOG.md | 4 ++ CONTRIBUTING.md | 14 +++---- bin/check | 19 ++++++--- bin/test-ecosystem | 6 ++- test/unit/bin_check_redis_url_test.rb | 56 +++++++++++++++++++++++++++ 5 files changed, 85 insertions(+), 14 deletions(-) create mode 100644 test/unit/bin_check_redis_url_test.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index e1038c9e..c60029cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ All notable changes to Wurk are recorded here. Format: [Keep a Changelog](https: ## [Unreleased] +### Changed + +- **`bin/check` probes the Redis at `REDIS_URL`, not a pinned `127.0.0.1:6379`.** The guard now checks the server the suite actually connects to (`REDIS_URL`, which `test_helper` and `RedisPool` already read), and falls back to `redis://127.0.0.1:6379/0` when it is unset, so nothing changes for a local run. `bin/test-ecosystem` uses DB 15 of the same server. A developerz.ai box can now run this gate against a Redis on a private port, which leaves 6379 free for another repo's own store on the same box. + ## [1.7.6] - 2026-09-07 One runtime fix that matters to anyone using encrypted jobs — on json 3.0.0, every one of them failed — plus the repo hygiene that had accumulated since 1.7.5: the gate a contributor runs before opening a PR now checks what CI checks, the docs stopped claiming things the tree does not do, and three timing-sensitive tests stopped failing for reasons that were never the code. No Redis key, command sequence, job-JSON field, or public API changed: a 1.7.5 worker and a 1.7.6 worker can drain the same queue. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9e0122f4..e02a625e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -69,13 +69,13 @@ treating it as unknown: | Exit | Trigger | Source | |---|---|---| -| `0` | Every stage passed, OR `-h` / `--help` / `help` printed the help block without running any stage. | `bin/check:201-204`, `bin/check:23-26` | -| `1` | At least one stage reported failure. | `bin/check:206-207` | +| `0` | Every stage passed, OR `-h` / `--help` / `help` printed the help block without running any stage. | `bin/check:210-213`, `bin/check:23-26` | +| `1` | At least one stage reported failure. | `bin/check:215-216` | | `64` | Unrecognised mode argument — anything other than `''`, `pr`, `fast`, `full`, `-h`, `--help`, or `help`. | `bin/check:27-30` | | `75` | No bundler on `PATH` — the environment cannot run the Ruby gate at all. | `bin/check:62-66` | | `75` | Bundler is on `PATH` but the gems are not installed — `bin/setup` has never run here. | `bin/check:80-84` | -| `75` | No Redis on `127.0.0.1:6379`. | `bin/check:92-96` | -| `75` | No `bun` on `PATH` — frontend gate cannot execute. | `bin/check:104-109` | +| `75` | No Redis at `REDIS_URL` (default `redis://127.0.0.1:6379/0`). | `bin/check:101-105` | +| `75` | No `bun` on `PATH` — frontend gate cannot execute. | `bin/check:113-118` | `75` is the platform's "preconditions unmet" code, and all four triggers share it on purpose: none of them says anything about the diff. A gate that cannot reach its Redis, @@ -86,9 +86,9 @@ nobody wrote. ### Env knobs -- `SKIP_LINT=1` — drop the rubocop stage (`bin/check:125`). -- `SKIP_PARITY=1` — drop the parity oracles stage (`bin/check:153`). -- `SKIP_FRONTEND=1` — drop the frontend stage (typecheck + oxlint + vitest in `frontend/`; `bin/check:144-145`). +- `SKIP_LINT=1` — drop the rubocop stage (`bin/check:134`). +- `SKIP_PARITY=1` — drop the parity oracles stage (`bin/check:162`). +- `SKIP_FRONTEND=1` — drop the frontend stage (typecheck + oxlint + vitest in `frontend/`; `bin/check:153-154`). - `NCPU=` — see [Worker count](#worker-count) below for the trade-off (ceiling 14). The individual tasks, when you want one: diff --git a/bin/check b/bin/check index b1fb856f..f1f2eb97 100755 --- a/bin/check +++ b/bin/check @@ -86,11 +86,20 @@ fi # Redis is not optional — integration and parity tests use a real server, never # a mock (CLAUDE.md). Refuse with the platform's preconditions-unmet contract # code (apps/runner/src/modes/setup/base.ts, CHECK_EXIT_PRECONDITIONS_UNMET = -# 75). Local developers who hit this need a Redis on 127.0.0.1:6379 — the two -# lines on stderr above tell them how. `docker run --rm -p 6379:6379 redis:7.4` -# is the supported path. -if ! (exec 3<>/dev/tcp/127.0.0.1/6379) 2>/dev/null; then - echo "bin/check: no Redis on 127.0.0.1:6379." >&2 +# 75). The guard probes the server the suite will actually use: REDIS_URL, which +# test_helper and RedisPool read, else their default. A developerz.ai box hands +# the gate a REDIS_URL on a private port, because 6379 belongs to whatever repo +# stack the box also runs. Locally, `docker run --rm -p 6379:6379 redis:7.4` is +# the supported path. +redis_addr="${REDIS_URL:-redis://127.0.0.1:6379/0}" +redis_addr="${redis_addr#*://}" +redis_addr="${redis_addr##*@}" +redis_addr="${redis_addr%%/*}" +redis_host="${redis_addr%:*}" +redis_port="${redis_addr##*:}" +[ "$redis_host" != "$redis_addr" ] || redis_port=6379 +if ! (exec 3<>"/dev/tcp/${redis_host}/${redis_port}") 2>/dev/null; then + echo "bin/check: no Redis on ${redis_host}:${redis_port} (REDIS_URL, default redis://127.0.0.1:6379/0)." >&2 echo " start one with: docker run --rm -p 6379:6379 redis:7.4" >&2 exit 75 fi diff --git a/bin/test-ecosystem b/bin/test-ecosystem index 528568e4..2d6c17f0 100755 --- a/bin/test-ecosystem +++ b/bin/test-ecosystem @@ -10,7 +10,7 @@ set -euo pipefail # suite installs NO real sidekiq and runs entirely on wurk. # # Checkouts are cloned (cached) into test/ecosystem/.checkouts/ (gitignored). -# The gem's suite hits real Redis; default DB 15 — override with +# The gem's suite hits real Redis; DB 15 of REDIS_URL's server — override with # ECOSYSTEM_REDIS_URL. The DB is the suite's to trash; don't point it at # data you care about. @@ -19,7 +19,9 @@ ROOT=$(pwd) ECOSYSTEM_DIR="test/ecosystem" CHECKOUTS="$ROOT/$ECOSYSTEM_DIR/.checkouts" -REDIS_URL="${ECOSYSTEM_REDIS_URL:-redis://127.0.0.1:6379/15}" +# DB 15 on the same server the rest of the gate uses (REDIS_URL), not a pinned port. +redis_base="${REDIS_URL:-redis://127.0.0.1:6379/0}" +REDIS_URL="${ECOSYSTEM_REDIS_URL:-$(printf '%s' "$redis_base" | sed -E 's#/[0-9]*$##')/15}" ran=0 failures=0 diff --git a/test/unit/bin_check_redis_url_test.rb b/test/unit/bin_check_redis_url_test.rb new file mode 100644 index 00000000..f6ea9240 --- /dev/null +++ b/test/unit/bin_check_redis_url_test.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +require 'minitest/autorun' +require 'open3' +require 'socket' +require 'tmpdir' + +# bin/check's Redis guard must look where the suite will connect: REDIS_URL, +# which lib/wurk and test_helper already read, falling back to the default. +# A box whose Redis is not on 127.0.0.1:6379 (the developerz.ai fleet moves its +# sidecar off the well-known port so a repo's own store can bind it) must not +# be refused for a server the suite was never going to use. +# +# Drives the real script in `fast` mode with a stub `bundle` on PATH, so every +# stage past the guard is a no-op and the exit code is the guard's verdict. +class BinCheckRedisUrlTest < Minitest::Test + parallelize_me! + + CHECK = File.expand_path('../../bin/check', __dir__) + + def run_check(redis_url) + Dir.mktmpdir do |dir| + stub = File.join(dir, 'bundle') + File.write(stub, "#!/usr/bin/env bash\nexit 0\n") + File.chmod(0o755, stub) + env = { 'PATH' => "#{dir}:#{ENV.fetch('PATH')}", 'REDIS_URL' => redis_url } + Open3.capture3(env, CHECK, 'fast') + end + end + + def closed_port + server = TCPServer.new('127.0.0.1', 0) + port = server.addr[1] + server.close + port + end + + def test_refuses_when_the_redis_url_server_is_down_and_names_it + port = closed_port + _out, err, status = run_check("redis://127.0.0.1:#{port}/0") + + assert_equal 75, status.exitstatus, err + assert_includes err, "127.0.0.1:#{port}" + end + + def test_runs_the_gate_when_the_redis_url_server_answers + server = TCPServer.new('127.0.0.1', 0) + begin + _out, err, status = run_check("redis://127.0.0.1:#{server.addr[1]}/3") + + assert_equal 0, status.exitstatus, err + ensure + server.close + end + end +end From 95a16810f9538efebbd2f951f36b151de30715c5 Mon Sep 17 00:00:00 2001 From: sebi Date: Sat, 19 Sep 2026 01:04:33 -0500 Subject: [PATCH 2/2] fix(check): fall back to the suite's own default, redis://localhost:6379/0 RedisPool::DEFAULT_URL and test_helper fall back to redis://localhost:6379/0, but bin/check and bin/test-ecosystem fell back to 127.0.0.1. If the two names resolve to different listeners, the guard can refuse a usable Redis, or run the ecosystem suite against a different server. A new test pins both scripts' fallback to RedisPool's literal. Co-Authored-By: Claude Opus 5 (1M context) --- CHANGELOG.md | 2 +- CONTRIBUTING.md | 2 +- bin/check | 4 ++-- bin/test-ecosystem | 2 +- test/unit/bin_check_redis_url_test.rb | 17 +++++++++++++++++ 5 files changed, 22 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c60029cc..34d68ecc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ All notable changes to Wurk are recorded here. Format: [Keep a Changelog](https: ### Changed -- **`bin/check` probes the Redis at `REDIS_URL`, not a pinned `127.0.0.1:6379`.** The guard now checks the server the suite actually connects to (`REDIS_URL`, which `test_helper` and `RedisPool` already read), and falls back to `redis://127.0.0.1:6379/0` when it is unset, so nothing changes for a local run. `bin/test-ecosystem` uses DB 15 of the same server. A developerz.ai box can now run this gate against a Redis on a private port, which leaves 6379 free for another repo's own store on the same box. +- **`bin/check` probes the Redis at `REDIS_URL`, not a pinned `127.0.0.1:6379`.** The guard now checks the server the suite actually connects to (`REDIS_URL`, which `test_helper` and `RedisPool` already read), and falls back to the suite's own default, `redis://localhost:6379/0`, when it is unset, so nothing changes for a local run. `bin/test-ecosystem` uses DB 15 of the same server. A developerz.ai box can now run this gate against a Redis on a private port, which leaves 6379 free for another repo's own store on the same box. ## [1.7.6] - 2026-09-07 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e02a625e..eb910a54 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -74,7 +74,7 @@ treating it as unknown: | `64` | Unrecognised mode argument — anything other than `''`, `pr`, `fast`, `full`, `-h`, `--help`, or `help`. | `bin/check:27-30` | | `75` | No bundler on `PATH` — the environment cannot run the Ruby gate at all. | `bin/check:62-66` | | `75` | Bundler is on `PATH` but the gems are not installed — `bin/setup` has never run here. | `bin/check:80-84` | -| `75` | No Redis at `REDIS_URL` (default `redis://127.0.0.1:6379/0`). | `bin/check:101-105` | +| `75` | No Redis at `REDIS_URL` (default `redis://localhost:6379/0`). | `bin/check:101-105` | | `75` | No `bun` on `PATH` — frontend gate cannot execute. | `bin/check:113-118` | `75` is the platform's "preconditions unmet" code, and all four triggers share it diff --git a/bin/check b/bin/check index f1f2eb97..cd903f3f 100755 --- a/bin/check +++ b/bin/check @@ -91,7 +91,7 @@ fi # the gate a REDIS_URL on a private port, because 6379 belongs to whatever repo # stack the box also runs. Locally, `docker run --rm -p 6379:6379 redis:7.4` is # the supported path. -redis_addr="${REDIS_URL:-redis://127.0.0.1:6379/0}" +redis_addr="${REDIS_URL:-redis://localhost:6379/0}" redis_addr="${redis_addr#*://}" redis_addr="${redis_addr##*@}" redis_addr="${redis_addr%%/*}" @@ -99,7 +99,7 @@ redis_host="${redis_addr%:*}" redis_port="${redis_addr##*:}" [ "$redis_host" != "$redis_addr" ] || redis_port=6379 if ! (exec 3<>"/dev/tcp/${redis_host}/${redis_port}") 2>/dev/null; then - echo "bin/check: no Redis on ${redis_host}:${redis_port} (REDIS_URL, default redis://127.0.0.1:6379/0)." >&2 + echo "bin/check: no Redis on ${redis_host}:${redis_port} (REDIS_URL, default redis://localhost:6379/0)." >&2 echo " start one with: docker run --rm -p 6379:6379 redis:7.4" >&2 exit 75 fi diff --git a/bin/test-ecosystem b/bin/test-ecosystem index 2d6c17f0..164aad50 100755 --- a/bin/test-ecosystem +++ b/bin/test-ecosystem @@ -20,7 +20,7 @@ ROOT=$(pwd) ECOSYSTEM_DIR="test/ecosystem" CHECKOUTS="$ROOT/$ECOSYSTEM_DIR/.checkouts" # DB 15 on the same server the rest of the gate uses (REDIS_URL), not a pinned port. -redis_base="${REDIS_URL:-redis://127.0.0.1:6379/0}" +redis_base="${REDIS_URL:-redis://localhost:6379/0}" REDIS_URL="${ECOSYSTEM_REDIS_URL:-$(printf '%s' "$redis_base" | sed -E 's#/[0-9]*$##')/15}" ran=0 diff --git a/test/unit/bin_check_redis_url_test.rb b/test/unit/bin_check_redis_url_test.rb index f6ea9240..ff09efd1 100644 --- a/test/unit/bin_check_redis_url_test.rb +++ b/test/unit/bin_check_redis_url_test.rb @@ -43,6 +43,23 @@ def test_refuses_when_the_redis_url_server_is_down_and_names_it assert_includes err, "127.0.0.1:#{port}" end + # The unset-REDIS_URL fallback must be the suite's own default, byte for byte: a + # guard that probes `127.0.0.1` while RedisPool connects to `localhost` can pass + # or refuse over a server the suite never uses. Read, not run — the real 6379 + # on the box would otherwise decide the answer. + def test_the_unset_fallback_is_the_suites_own_default + suite_default = File.read(File.expand_path('../../lib/wurk/redis_pool.rb', __dir__))[ + /DEFAULT_URL\s*=\s*ENV\.fetch\('REDIS_URL', '([^']+)'\)/, 1 + ] + + refute_nil suite_default, 'RedisPool::DEFAULT_URL must still read REDIS_URL with a literal fallback' + %w[bin/check bin/test-ecosystem].each do |script| + fallback = File.read(File.expand_path("../../#{script}", __dir__))[/\$\{REDIS_URL:-([^}]+)\}/, 1] + + assert_equal suite_default, fallback, "#{script} must fall back to the suite's own default" + end + end + def test_runs_the_gate_when_the_redis_url_server_answers server = TCPServer.new('127.0.0.1', 0) begin