fix(#838): bound broker reply deadlines + fail-close empty-read; de-flake acceptance harness (#839)
All checks were successful
ci/woodpecker/push/publish Pipeline was successful
ci/woodpecker/push/ci Pipeline was successful

Bound broker reply deadlines (separate read/lock/send budgets, BROKER_BUSY-before-mutation, fresh post-handle send budget → closes drop-after-commit window); fail-close empty/truncated read → GATE_UNAVAILABLE deny. De-flakes the 1917 acceptance surface. terra CODE APPROVE (pi) + Opus SECREV APPROVED (claude) — RoR comment 18143. Promote-lease-lost-ACK residual = fail-safe two-generals observability-gap, routed to WI-3 D2-v5 as named-disclosed-bounded-residual (route i). Gate-16 3-principal author=gpt-sol.

closes #838
This commit was merged in pull request #839.
This commit is contained in:
2026-07-18 07:15:50 +00:00
parent abd2791f59
commit 8dfcf1903e
10 changed files with 595 additions and 51 deletions

View File

@@ -1,5 +1,4 @@
import { chmod, mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
import { createConnection } from 'node:net';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { spawn, spawnSync, type ChildProcess } from 'node:child_process';
@@ -7,6 +6,7 @@ import { spawn, spawnSync, type ChildProcess } from 'node:child_process';
import { afterEach, describe, expect, test } from 'vitest';
import { launchClaudex, type ClaudexHarnessAdapter } from '../commands/claudex.js';
import { requestBrokerReply } from '../lease-broker/broker-test-client.js';
interface BrokerReply {
ok: boolean;
@@ -43,17 +43,7 @@ const binding = (compaction_epoch = 1) => ({
});
async function request(socketPath: string, requestValue: object): Promise<BrokerReply> {
return await new Promise<BrokerReply>((resolve, reject) => {
const socket = createConnection(socketPath);
let response = '';
socket.setEncoding('utf8');
socket.once('error', reject);
socket.on('data', (chunk: string) => {
response += chunk;
});
socket.once('end', () => resolve(JSON.parse(response) as BrokerReply));
socket.once('connect', () => socket.end(`${JSON.stringify(requestValue)}\n`));
});
return await requestBrokerReply<BrokerReply>(socketPath, requestValue);
}
async function startBroker(): Promise<BrokerPaths> {

View File

@@ -9,7 +9,10 @@ import json
import os
import runpy
import socket
import subprocess
import sys
import tempfile
import threading
import unittest
from contextlib import redirect_stderr
from pathlib import Path
@@ -225,6 +228,53 @@ class LaunchRuntimeTest(unittest.TestCase):
class ExecutableEntrypointTest(unittest.TestCase):
def test_real_claude_and_pi_gates_fail_closed_on_empty_or_truncated_reply(self) -> None:
for runtime in ("claude", "pi"):
for wire_reply in (b"", b'{"ok":true'):
with self.subTest(runtime=runtime, wire_reply=wire_reply), tempfile.TemporaryDirectory() as root:
socket_path = Path(root) / "broker.sock"
server = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
server.bind(str(socket_path))
server.listen(1)
def serve_reply() -> None:
with server:
connection, _ = server.accept()
with connection:
while connection.recv(4096):
pass
if wire_reply:
connection.sendall(wire_reply)
thread = threading.Thread(target=serve_reply, daemon=True)
thread.start()
environment = {
**os.environ,
"MOSAIC_LEASE_BROKER_SOCKET": str(socket_path),
"MOSAIC_LEASE_SESSION_ID": "d" * 64,
"MOSAIC_RUNTIME_GENERATION": "1",
}
try:
result = subprocess.run(
[sys.executable, str(TOOLS_DIR / "mutator-gate.py"), "--runtime", runtime],
input=b'{"tool_name":"Read"}\n',
capture_output=True,
env=environment,
check=False,
timeout=5,
)
except subprocess.TimeoutExpired as exc:
server.close()
thread.join(timeout=2)
self.fail(
f"{runtime} gate hung on wire reply {wire_reply!r}: {exc}"
)
thread.join(timeout=2)
self.assertFalse(thread.is_alive())
self.assertEqual(result.returncode, 2)
self.assertIn(b"GATE_UNAVAILABLE", result.stderr)
def test_launcher_entrypoint_returns_usage_without_a_command(self) -> None:
with patch.object(
sys,