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:
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.
|
||||
@@ -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(
|
||||
|
||||
@@ -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])
|
||||
|
||||
Reference in new issue
Block a user