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

@@ -0,0 +1,66 @@
# Wait for split-shell startup before column probes
## Status
Proposed for review with the conformance regression fix.
## Context
The resize case opens a sibling pane and queries both shells for their native
column counts. Creating the pane returns before the new shell has initialized.
The existing boot case already waits for initial shell output before submitting
commands, but the resize fixture did not apply that precondition to its sibling.
## Evidence
[Windows job 106259683240](https://github.com/Kuddev/pebrel/actions/runs/35576551914/job/106259683240)
recorded the source pane's initial width as 38. The sibling produced a late
PowerShell prompt and only the leading `W` of its column probe. There was no
completed sibling measurement, so the case never reached `pane.resize`.
The focused regression simulates a sibling whose first output arrives after two
empty observations. Before the fix, its column command is submitted during
startup. A second regression requires a permanently silent sibling to fail and
be closed without performing the resize.
## Decision
Apply the existing boot fixture's bounded initial-output check before the new
sibling's first column command. Preserve the source pane's immediate initial
measurement and the existing startup timeout. This wait belongs to shell startup,
before the requested ratio change.
Both post-resize commands still run without additional polling for geometry.
Their output must satisfy the original growth, shrinkage and total-column bounds.
Initial output alone never counts as a successful column measurement.
## Rejected alternatives
- Retry the full resize case until it passes: that can hide a geometry defect.
- Delay or retry column measurements after changing the ratio: that weakens the
immediate resize contract rather than addressing the uninitialized fixture.
- Increase measurement timeouts or accept missing probe output: neither makes an
input command submitted during startup execute reliably.
## Consequences
Only the test fixture gains an initialization precondition. Product input rules,
the runtime API, geometry assertions and report fields retain their contracts.
The initial-output check follows the harness's managed shell setup; it is not a
general claim that every interactive profile is ready after its first output.
## Validation
Regression coverage includes delayed and silent startup, growth and shrinkage,
stale widths, total-column bounds, failure diagnostics and sibling cleanup. The
fixture rejects startup polling after the resize, preserving the timing boundary.
The real Windows conformance job must pass before the candidate is accepted.
## Supersedes
None.
## Revisit when
The runtime exposes an explicit shell-input readiness signal that the conformance
fixture can use in place of its existing initial-output precondition.
+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])