Prove multi-file terminal coding loop

This commit is contained in:
slhx agent
2026-06-21 18:59:45 +02:00
parent e486856f79
commit d6ebf1cc06
4 changed files with 78 additions and 13 deletions
+32 -4
View File
@@ -44,6 +44,7 @@ class Scenario:
forbidden_raw: tuple[str, ...] = ("^[",)
required_attrs: tuple[str, ...] = ("cursor", "status", "current-line")
max_key_events: int | None = None
expect_files: tuple[tuple[str, str], ...] = ()
def visible_controls(data: bytes) -> str:
@@ -334,7 +335,7 @@ def keystroke_rows(scenario: Scenario) -> tuple[list[str], int, int]:
return rows, len(scenario.payloads), raw_bytes
def write_artifacts(out_dir: Path, scenario: Scenario, transcript: bytes, lines: list[str], rows: list[list[Cell]], seen_attrs: set[str], saved: str | None) -> None:
def write_artifacts(out_dir: Path, scenario: Scenario, transcript: bytes, lines: list[str], rows: list[list[Cell]], seen_attrs: set[str], saved: str | None, saved_files: dict[str, str] | None = None) -> None:
visible = visible_controls(transcript)
normalized_visible = normalize_visible_controls(visible)
raw_seen_attrs = set(seen_attrs)
@@ -366,6 +367,12 @@ def write_artifacts(out_dir: Path, scenario: Scenario, transcript: bytes, lines:
width_report.append(f"{idx} {len(line)} {line}")
(out_dir / "widths.tsv").write_text("\n".join(width_report) + "\n", encoding="utf-8")
(out_dir / "keystrokes.tsv").write_text("\n".join(key_rows) + "\n", encoding="utf-8")
saved_files = saved_files or {}
if saved_files:
saved_rows = ["path\tsha256\tbytes"]
for rel_path, contents in sorted(saved_files.items()):
saved_rows.append(f"{rel_path}\t{hashlib.sha256(contents.encode('utf-8')).hexdigest()}\t{len(contents.encode('utf-8'))}")
(out_dir / "saved-files.tsv").write_text("\n".join(saved_rows) + "\n", encoding="utf-8")
snapshot = [
f"scenario\t{scenario.id}",
f"viewport\t{scenario.width}x{scenario.height}",
@@ -381,7 +388,8 @@ def write_artifacts(out_dir: Path, scenario: Scenario, transcript: bytes, lines:
f"raw_input_bytes\t{raw_input_bytes}",
f"max_key_events\t{scenario.max_key_events if scenario.max_key_events is not None else ''}",
f"key_budget\t{budget_status}",
"artifacts\traw.bin visible-controls.txt visible-controls.normalized.txt transcript.txt screenshot.svg terminal.html widths.tsv keystrokes.tsv manifest.tsv",
f"saved_files\t{','.join(sorted(saved_files.keys()))}",
"artifacts\traw.bin visible-controls.txt visible-controls.normalized.txt transcript.txt screenshot.svg terminal.html widths.tsv keystrokes.tsv saved-files.tsv manifest.tsv",
]
(out_dir / "manifest.tsv").write_text("key\tvalue\n" + "\n".join(snapshot) + "\n", encoding="utf-8")
@@ -439,6 +447,15 @@ def setup_long(tmp: Path) -> tuple[Path, str | None]:
return target, "l1\nl2\nl3\nl4\nl5\nl6\nl7\nl8\n"
def setup_multifile(tmp: Path) -> tuple[Path, str | None]:
root = tmp / "repo"
root.mkdir()
(root / "alpha.zig").write_text("alpha\n", encoding="utf-8")
beta = root / "beta.zig"
beta.write_text("beta\n", encoding="utf-8")
return beta, "beta\n"
MOBILE_SYMBOL_SAVE_QUIT = tuple(bytes([b]) for b in b" ps w q")
MOBILE_REPLACE_SAVE_QUIT = tuple(bytes([b]) for b in b"rx w q")
ATTACHED_SAVE_QUIT = (b"i", b"\x1b[F", b"!", b"\x1b", b" ", b"w", b" ", b"q")
@@ -460,6 +477,7 @@ SCENARIOS = [
Scenario("counted-move-inserts-at-count", 56, 12, "short", "attached-keyboard", "counts", "truecolor", "file", setup_short, (b"3", b"l", b"i", b"X", b"\x1b", b" ", b"w", b" ", b"q"), "abcXdef", ("1│", "")),
Scenario("percent-match-jump", 56, 12, "brackets", "attached-keyboard", "match-jump", "truecolor", "file", setup_brackets, (b"%", b"i", b"X", b"\x1b", b" ", b"w", b" ", b"q"), "(abX)", ("1│", "")),
Scenario("select-mode-colors", 56, 12, "short", "attached-keyboard", "select", "truecolor", "file", setup_short, (b"s", b"w", b"n", b" ", b"q"), "abcdef\n", ("attr:selection", "", "mode:normal")),
Scenario("multi-file-coding-loop", 72, 16, "repo-multifile", "attached-keyboard", "multi-file", "truecolor", "file", setup_multifile, (b"A", b"?", b"\x1b", b" ", b"w", b" ", b"o", b"r", b"e", b"p", b"o", b"/", b"a", b"l", b"p", b"h", b"a", b".", b"z", b"i", b"g", b"\r", b"A", b"!", b"\x1b", b" ", b"w", b" ", b"q"), None, ("alpha.zig", "beta", "", "mode:normal"), max_key_events=30, expect_files=(("alpha.zig", "alpha!\n"), ("beta.zig", "beta?"))),
Scenario("dirty-discard-shift-q", 52, 12, "empty", "ios-default-qwertz-space-path", "dirty-discard", "truecolor", "file", setup_empty, tuple(bytes([b]) for b in b" ps Q"), "", ("1│", "")),
Scenario("directory-panel-narrow", 52, 12, "directory", "ios-default-qwertz-space-path", "panel", "mono", "directory", setup_directory, PANEL_QUIT, None, ("file", "one.zig"), required_attrs=()),
]
@@ -484,7 +502,8 @@ def run_one(mim: Path, root_out: Path, scenario: Scenario) -> tuple[str, Path]:
tmp = Path(tmp_name)
target, expected_initial = scenario.setup(tmp)
_ = expected_initial
pid, fd = spawn_under_pty([str(mim), target.name], scenario.width, scenario.height, scenario.color_mode, tmp)
launch_arg = target.relative_to(tmp).as_posix()
pid, fd = spawn_under_pty([str(mim), launch_arg], scenario.width, scenario.height, scenario.color_mode, tmp)
transcript = bytearray()
grid = TerminalGrid(scenario.width, scenario.height)
try:
@@ -518,9 +537,14 @@ def run_one(mim: Path, root_out: Path, scenario: Scenario) -> tuple[str, Path]:
saved: str | None = None
if scenario.target_kind == "file" and target.exists():
saved = target.read_text(encoding="utf-8")
saved_files: dict[str, str] = {}
for rel_path, _expected in scenario.expect_files:
file_path = (target / rel_path) if scenario.target_kind == "directory" else (target.parent / rel_path)
if file_path.exists():
saved_files[rel_path] = file_path.read_text(encoding="utf-8")
lines = grid.lines()
raw = bytes(transcript)
write_artifacts(out_dir, scenario, raw, lines, grid.colored_rows(), grid.attrs_seen(), saved)
write_artifacts(out_dir, scenario, raw, lines, grid.colored_rows(), grid.attrs_seen(), saved, saved_files)
failures: list[str] = []
raw_text = raw.decode("utf-8", errors="ignore")
_, key_events, _ = keystroke_rows(scenario)
@@ -528,6 +552,10 @@ def run_one(mim: Path, root_out: Path, scenario: Scenario) -> tuple[str, Path]:
failures.append(f"key budget exceeded: {key_events} > {scenario.max_key_events}")
if scenario.expect_saved is not None and saved != scenario.expect_saved:
failures.append(f"saved file mismatch: {saved!r} != {scenario.expect_saved!r}")
for rel_path, expected in scenario.expect_files:
actual = saved_files.get(rel_path)
if actual != expected:
failures.append(f"saved file mismatch for {rel_path}: {actual!r} != {expected!r}")
if has_bare_lf(raw):
failures.append("raw terminal output contains bare LF; expected CRLF in raw mode")
plain_text = strip_csi(raw_text)