From 7027812c0c86efff6b532c3174b429767675eb27 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 16 Oct 2026 12:42:49 +0000 Subject: [PATCH 1/3] fix(ci): pass required output root to the core RPC docker check The protocol workflow still called test-core-rpc-docker.py without --output-root after that flag became required. Point it at a fresh directory under RUNNER_TEMP and keep that directory with the evidence artifact. Fixes #923 Co-authored-by: LIghtJUNction --- .github/workflows/core-protocol.yml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/.github/workflows/core-protocol.yml b/.github/workflows/core-protocol.yml index 477afcbda..f1927fddb 100644 --- a/.github/workflows/core-protocol.yml +++ b/.github/workflows/core-protocol.yml @@ -169,7 +169,14 @@ jobs: MK06_GO_FIXTURE="$RUNNER_TEMP/mk06-go-fixture" python3 contracts/proto/tests/process_recovery.py 2>&1 | tee "$RUNNER_TEMP/events-process.log" - name: Native protobuf round trips and failure recovery if: ${{ !cancelled() }} - run: python3 -B scripts/test-core-rpc-docker.py 2>&1 | tee "$RUNNER_TEMP/core-rpc-docker.log" + run: | + # Absolute, symlink-free, and absent: the fixture creates a private empty 0700 root. + evidence="$(realpath -m -- "$RUNNER_TEMP/core-rpc-docker-evidence")" + if [[ -e "$evidence" || -L "$evidence" ]]; then + echo "RPC evidence root must be absent before the fixture creates it" >&2 + exit 1 + fi + python3 -B scripts/test-core-rpc-docker.py --output-root "$evidence" 2>&1 | tee "$RUNNER_TEMP/core-rpc-docker.log" - name: Service boundary contracts if: ${{ !cancelled() }} run: python3 -B scripts/test-core-boundaries.py @@ -184,6 +191,7 @@ jobs: ${{ runner.temp }}/core-tests.log ${{ runner.temp }}/extension-tests.jsonl ${{ runner.temp }}/core-rpc-docker.log + ${{ runner.temp }}/core-rpc-docker-evidence ${{ runner.temp }}/store-postgres.jsonl ${{ runner.temp }}/events-process.log if-no-files-found: warn From 498a4f97c321dc4af6dcee57bdb8d42a3c4ef51a Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 16 Oct 2026 14:45:43 +0000 Subject: [PATCH 2/3] fix(test): wait for HTTP 200 in the core RPC docker check A live extension proxy returns 503 JSON while core restarts. The fixture treated that body as readiness, so the reconnect check failed immediately. Stage only runner-readable evidence so artifact upload is not rejected by container-owned secret files. Co-authored-by: LIghtJUNction --- .github/workflows/core-protocol.yml | 24 +++++++++- scripts/test-core-rpc-docker.py | 27 +++++++---- scripts/test_core_rpc_fixture.py | 74 +++++++++++++++++++++++++++++ scripts/workflow-topology.test.mjs | 3 ++ 4 files changed, 117 insertions(+), 11 deletions(-) diff --git a/.github/workflows/core-protocol.yml b/.github/workflows/core-protocol.yml index f1927fddb..0fa26b217 100644 --- a/.github/workflows/core-protocol.yml +++ b/.github/workflows/core-protocol.yml @@ -177,6 +177,28 @@ jobs: exit 1 fi python3 -B scripts/test-core-rpc-docker.py --output-root "$evidence" 2>&1 | tee "$RUNNER_TEMP/core-rpc-docker.log" + - name: Stage readable core RPC evidence + if: ${{ always() }} + run: | + # Secret files are chowned to container uid 65532 and mode 0400. + # upload-artifact fails the whole zip if any selected file is unreadable. + src="$RUNNER_TEMP/core-rpc-docker-evidence" + dest="$RUNNER_TEMP/core-rpc-docker-evidence-readable" + rm -rf -- "$dest" + if [[ ! -d "$src" ]]; then + echo "No core RPC evidence directory to stage" + exit 0 + fi + mkdir -m 0700 -- "$dest" + while IFS= read -r -d '' dir; do + rel="${dir#"$src"/}" + mkdir -m 0700 -p -- "$dest/$rel" + done < <(find "$src" -mindepth 1 -type d -print0) + while IFS= read -r -d '' file; do + rel="${file#"$src"/}" + mkdir -m 0700 -p -- "$dest/$(dirname -- "$rel")" + cp -p -- "$file" "$dest/$rel" + done < <(find "$src" -type f -readable -print0) - name: Service boundary contracts if: ${{ !cancelled() }} run: python3 -B scripts/test-core-boundaries.py @@ -191,7 +213,7 @@ jobs: ${{ runner.temp }}/core-tests.log ${{ runner.temp }}/extension-tests.jsonl ${{ runner.temp }}/core-rpc-docker.log - ${{ runner.temp }}/core-rpc-docker-evidence + ${{ runner.temp }}/core-rpc-docker-evidence-readable ${{ runner.temp }}/store-postgres.jsonl ${{ runner.temp }}/events-process.log if-no-files-found: warn diff --git a/scripts/test-core-rpc-docker.py b/scripts/test-core-rpc-docker.py index f549457f0..ac7719ea9 100644 --- a/scripts/test-core-rpc-docker.py +++ b/scripts/test-core-rpc-docker.py @@ -216,6 +216,20 @@ def request(url, token=None, method="GET", body=None, user=None): return error.code, error.read() +def wait_for_http(call, on_ready, *, attempts=100, pause=0.3): + """Retry until HTTP 200. A live proxy's 502/503 JSON is not readiness.""" + for _ in range(attempts): + try: + status, body = call() + if status == 200: + on_ready() + return json.loads(body) + except (OSError, TimeoutError): + pass + time.sleep(pause) + raise AssertionError("test endpoint did not become available") + + class FixtureError(RuntimeError): """Safe public description; raw Docker/SQL output stays in private files.""" @@ -1085,16 +1099,9 @@ def rpc(path, user=None, token=ext_token): ) def wait(call): - for _ in range(100): - try: - status, body = call() - if status == 200: - context.capture_owned("ready", runtime=True) - return json.loads(body) - except (OSError, TimeoutError): - pass - time.sleep(0.3) - raise AssertionError("test endpoint did not become available") + return wait_for_http( + call, lambda: context.capture_owned("ready", runtime=True) + ) def snapshot(service): data = json.loads( diff --git a/scripts/test_core_rpc_fixture.py b/scripts/test_core_rpc_fixture.py index 9ff7edecb..6f45796c9 100644 --- a/scripts/test_core_rpc_fixture.py +++ b/scripts/test_core_rpc_fixture.py @@ -338,6 +338,80 @@ def test_failed_or_interrupted_rotation_restores_read_only_mode(self): self.assertEqual(stat.S_IMODE(path.stat().st_mode), 0o400) +class HttpReadinessTests(unittest.TestCase): + def test_execute_wait_delegates_to_http_200_helper(self): + source = ast.parse(SOURCE.read_text()) + waits = [ + node + for node in ast.walk(source) + if isinstance(node, ast.FunctionDef) and node.name == "wait" + ] + self.assertEqual(len(waits), 1) + calls = [ + node + for node in ast.walk(waits[0]) + if isinstance(node, ast.Call) + and isinstance(node.func, ast.Name) + and node.func.id == "wait_for_http" + ] + self.assertEqual(len(calls), 1) + + def test_proxy_error_json_is_not_readiness(self): + calls = [] + + def call(): + calls.append(1) + if len(calls) < 3: + return 503, b'{"error":"core_unavailable"}' + return 200, b'{"user_id":"1"}' + + ready = [] + with mock.patch.object(FIXTURE.time, "sleep") as sleep: + body = FIXTURE.wait_for_http( + call, lambda: ready.append(True), pause=0 + ) + self.assertEqual(body, {"user_id": "1"}) + self.assertEqual(ready, [True]) + self.assertEqual(len(calls), 3) + self.assertGreaterEqual(sleep.call_count, 2) + + def test_connection_errors_retry_until_http_200(self): + state = {"n": 0} + + def call(): + state["n"] += 1 + if state["n"] < 2: + raise TimeoutError("not listening") + return 200, b'{"ok":true}' + + with mock.patch.object(FIXTURE.time, "sleep"): + body = FIXTURE.wait_for_http(call, lambda: None, pause=0) + self.assertEqual(body, {"ok": True}) + self.assertEqual(state["n"], 2) + + def test_persistent_gateway_error_is_not_success(self): + def call(): + return 502, b'{"error":"core_unavailable"}' + + with mock.patch.object(FIXTURE.time, "sleep"): + with self.assertRaises(AssertionError): + FIXTURE.wait_for_http( + call, lambda: self.fail("ready"), attempts=3, pause=0 + ) + + def test_readiness_observation_failure_is_not_retried(self): + def call(): + return 200, b'{"ok":true}' + + def on_ready(): + raise FIXTURE.FixtureError("product executable changed across restart") + + with mock.patch.object(FIXTURE.time, "sleep") as sleep: + with self.assertRaises(FIXTURE.FixtureError): + FIXTURE.wait_for_http(call, on_ready, pause=0) + sleep.assert_not_called() + + # @mark deployment.database-authority role=test class DurableOutputTests(unittest.TestCase): def test_cli_requires_explicit_output_before_any_docker_operation(self): diff --git a/scripts/workflow-topology.test.mjs b/scripts/workflow-topology.test.mjs index 81fc0f450..7ab5a3375 100644 --- a/scripts/workflow-topology.test.mjs +++ b/scripts/workflow-topology.test.mjs @@ -142,6 +142,9 @@ test('manual checks reuse the exact core and extension verification pipeline', ( 'contracts/proto/tests/process_recovery.py', 'LMM_RELAY_EXTENSION_BIN']) { assert.ok(source.includes(command), command); } + assert.match(source, /python3 -B scripts\/test-core-rpc-docker\.py --output-root "\$evidence"/); + assert.match(source, /core-rpc-docker-evidence-readable/); + assert.doesNotMatch(source, /runner\.temp \}\}\/core-rpc-docker-evidence\n/); assert.match(source, /services:\n postgres:/); assert.match(source, /DATABASE_URL: postgres:\/\/postgres:postgres@127\.0\.0\.1:5432\/postgres/); assert.match(source, /working-directory: services\/extensions/); From f452cdb4d05f503f14ebf6e8d8855624249aee7d Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 16 Oct 2026 15:06:50 +0000 Subject: [PATCH 3/3] fix(ci): chmod staged RPC evidence after mkdir shellcheck SC2174 rejects mkdir -m together with -p, and actionlint failed the protocol workflow on that warning. Co-authored-by: LIghtJUNction --- .github/workflows/core-protocol.yml | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/.github/workflows/core-protocol.yml b/.github/workflows/core-protocol.yml index 0fa26b217..6d7f4cca1 100644 --- a/.github/workflows/core-protocol.yml +++ b/.github/workflows/core-protocol.yml @@ -189,14 +189,18 @@ jobs: echo "No core RPC evidence directory to stage" exit 0 fi - mkdir -m 0700 -- "$dest" + mkdir -- "$dest" + chmod 0700 -- "$dest" while IFS= read -r -d '' dir; do rel="${dir#"$src"/}" - mkdir -m 0700 -p -- "$dest/$rel" + mkdir -p -- "$dest/$rel" + chmod 0700 -- "$dest/$rel" done < <(find "$src" -mindepth 1 -type d -print0) while IFS= read -r -d '' file; do rel="${file#"$src"/}" - mkdir -m 0700 -p -- "$dest/$(dirname -- "$rel")" + parent="$dest/$(dirname -- "$rel")" + mkdir -p -- "$parent" + chmod 0700 -- "$parent" cp -p -- "$file" "$dest/$rel" done < <(find "$src" -type f -readable -print0) - name: Service boundary contracts