fix(conformance): wait for split shell startup before column probes

The resize fixture submitted the new pane's command before its shell produced initial output. Reuse the bounded boot precondition before the first sibling probe while preserving immediate post-resize measurements and strict geometry assertions. Cover delayed and silent startup with cleanup.
This commit is contained in:
kuddev committed 2026-09-21 17:05:43 +08:00
1 parent e0e180c2ac
commit c8c951bcda
3 files changed
+118 -2

No files matched your search

+9
View File
@@ -75,6 +75,15 @@ def run(ctx: ConformanceContext) -> dict[str, object]:
try:
source_before = ctx.measure_columns(source)
measurements["source_columns_before"] = source_before
# Creating a pane precedes shell startup. Use the same initial-output
# precondition as boot before sending the sibling's first command.
# Measurements after pane.resize remain immediate and strict.
ctx.poll(
lambda: ctx.read(sibling),
lambda value: bool(value.get("text", "").strip()),
"split shell did not render initial output",
timeout=ctx.startup_timeout,
)
sibling_before = ctx.measure_columns(sibling)
measurements["sibling_columns_before"] = sibling_before
resized = ctx.api(
+43 -2
View File
@@ -14,7 +14,7 @@ if str(SCRIPTS_DIR) not in sys.path:
sys.path.insert(0, str(SCRIPTS_DIR))
from conformance.cases import resize
from conformance.harness import ConformanceError
from conformance.harness import ConformanceContext, ConformanceError
class ResizeContext:
@@ -23,15 +23,23 @@ class ResizeContext:
shell = "bash"
platform_family = "macos"
def __init__(self, artifact_dir: Path, columns: list[int]) -> None:
def __init__(self, artifact_dir: Path, columns: list[int], outputs=None) -> None:
self.artifact_dir = artifact_dir
self.columns = iter(columns)
self.cleanup: list[str] = []
self.startup_timeout = 3.0
self.outputs = list(outputs) if outputs is not None else ["fixture prompt"]
self.sibling_ready = outputs is None
self.read_count = 0
self.measured_panes: list[int] = []
self.resized = False
self.poll = ConformanceContext.poll.__get__(self)
def api(self, method: str, params=None, **kwargs):
if method == "pane.split":
return {"action": {"pane_id": 11}}
if method == "pane.resize":
self.resized = True
return {"action": {"ratio": params["ratio"]}}
if method == "runtime.snapshot":
return {"windows": [{"id": self.window_id}]}
@@ -40,13 +48,46 @@ class ResizeContext:
raise AssertionError(method)
def measure_columns(self, pane_id: int) -> int:
if pane_id == 11 and not self.sibling_ready:
raise AssertionError("columns probe sent before sibling startup")
self.measured_panes.append(pane_id)
return next(self.columns)
def read(self, pane_id: int):
if self.resized:
raise AssertionError("startup polling must not delay post-resize measurements")
self.read_count += 1
text = self.outputs[0]
if len(self.outputs) > 1:
self.outputs.pop(0)
self.sibling_ready = bool(text.strip())
return {"text": text}
def best_effort_api(self, method: str, params) -> None:
self.cleanup.append(method)
class ResizeDiagnosticsTests(unittest.TestCase):
def test_cold_sibling_renders_output_before_its_first_columns_command(self) -> None:
with tempfile.TemporaryDirectory() as directory:
ctx = ResizeContext(Path(directory), [57, 56, 80, 33], ["", " \n", "PS C:\\fixture> "])
with patch("time.sleep"), patch("time.monotonic", side_effect=[0, 0.1, 0.2, 0.3]):
result = resize.run(ctx)
self.assertTrue(result["pty_columns_changed"])
self.assertEqual(ctx.read_count, 3)
self.assertEqual(ctx.measured_panes, [10, 11, 10, 11])
self.assertEqual(ctx.cleanup, ["pane.close", "window.focus"])
def test_silent_sibling_fails_before_resize_and_is_closed(self) -> None:
with tempfile.TemporaryDirectory() as directory:
ctx = ResizeContext(Path(directory), [57], [""])
with patch("time.sleep"), patch("time.monotonic", side_effect=range(20)):
with self.assertRaisesRegex(ConformanceError, "split shell did not render initial output"):
resize.run(ctx)
self.assertFalse(ctx.resized)
self.assertEqual(ctx.measured_panes, [10])
self.assertEqual(ctx.cleanup, ["pane.close", "window.focus"])
def test_growing_and_shrinking_panes_pass_without_diagnostic_sampling(self) -> None:
with tempfile.TemporaryDirectory() as directory:
ctx = ResizeContext(Path(directory), [57, 56, 80, 33])