Prove multi-file terminal coding loop
This commit is contained in:
+19
-3
@@ -163,6 +163,7 @@ fn openLocalEditor(
|
|||||||
switch (metadata.kind) {
|
switch (metadata.kind) {
|
||||||
.directory => try openDirectoryPreview(allocator, io, &client, path),
|
.directory => try openDirectoryPreview(allocator, io, &client, path),
|
||||||
.file => {
|
.file => {
|
||||||
|
if (std.fs.path.dirname(path)) |parent| seedDirectoryRepo(allocator, io, &client, parent) catch {};
|
||||||
const bytes = std.Io.Dir.cwd().readFileAlloc(io, path, allocator, .limited(diagnostics.max_file_bytes)) catch |err| switch (err) {
|
const bytes = std.Io.Dir.cwd().readFileAlloc(io, path, allocator, .limited(diagnostics.max_file_bytes)) catch |err| switch (err) {
|
||||||
error.StreamTooLong => {
|
error.StreamTooLong => {
|
||||||
try client.handleTraceLine("open diagnostic:file_too_large");
|
try client.handleTraceLine("open diagnostic:file_too_large");
|
||||||
@@ -184,7 +185,7 @@ fn openLocalEditor(
|
|||||||
try runLocalEditor(allocator, io, stdout, stderr, &client, path, stat != null and stat.?.kind == .directory);
|
try runLocalEditor(allocator, io, stdout, stderr, &client, path, stat != null and stat.?.kind == .directory);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn openDirectoryPreview(allocator: std.mem.Allocator, io: std.Io, client: *tui.Client, path: []const u8) !void {
|
fn seedDirectoryRepo(allocator: std.mem.Allocator, io: std.Io, client: *tui.Client, path: []const u8) !void {
|
||||||
var dir = try std.Io.Dir.cwd().openDir(io, path, .{ .iterate = true });
|
var dir = try std.Io.Dir.cwd().openDir(io, path, .{ .iterate = true });
|
||||||
defer dir.close(io);
|
defer dir.close(io);
|
||||||
|
|
||||||
@@ -211,6 +212,10 @@ fn openDirectoryPreview(allocator: std.mem.Allocator, io: std.Io, client: *tui.C
|
|||||||
}
|
}
|
||||||
count += 1;
|
count += 1;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn openDirectoryPreview(allocator: std.mem.Allocator, io: std.Io, client: *tui.Client, path: []const u8) !void {
|
||||||
|
try seedDirectoryRepo(allocator, io, client, path);
|
||||||
try client.handleTraceLine("file_picker");
|
try client.handleTraceLine("file_picker");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -236,6 +241,8 @@ fn runLocalEditor(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (is_dir) try client.openFilePicker();
|
||||||
|
|
||||||
var raw_terminal = try RawTerminal.enable(stdin_file.handle);
|
var raw_terminal = try RawTerminal.enable(stdin_file.handle);
|
||||||
defer raw_terminal.restore();
|
defer raw_terminal.restore();
|
||||||
|
|
||||||
@@ -259,10 +266,12 @@ fn runLocalEditor(
|
|||||||
if (client.saved()) |saved_bytes| {
|
if (client.saved()) |saved_bytes| {
|
||||||
const new_save_request = last_client_saved == null or !std.mem.eql(u8, saved_bytes, last_client_saved.?);
|
const new_save_request = last_client_saved == null or !std.mem.eql(u8, saved_bytes, last_client_saved.?);
|
||||||
if (new_save_request) {
|
if (new_save_request) {
|
||||||
if (is_dir) {
|
if (is_dir and client.currentPath() == null) {
|
||||||
client.setStatusMessage("directory browser has nothing to save");
|
client.setStatusMessage("directory browser has nothing to save");
|
||||||
} else {
|
} else {
|
||||||
try persistLocalSave(allocator, io, path, saved_bytes, &last_saved, &last_client_saved);
|
const save_path = try resolveLocalSavePath(allocator, path, client.currentPath());
|
||||||
|
defer allocator.free(save_path);
|
||||||
|
try persistLocalSave(allocator, io, save_path, saved_bytes, &last_saved, &last_client_saved);
|
||||||
dirty = false;
|
dirty = false;
|
||||||
client.setStatusMessage("saved");
|
client.setStatusMessage("saved");
|
||||||
}
|
}
|
||||||
@@ -280,6 +289,13 @@ fn runLocalEditor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn resolveLocalSavePath(allocator: std.mem.Allocator, launch_path: []const u8, current_path: ?[]const u8) ![]u8 {
|
||||||
|
const selected = current_path orelse launch_path;
|
||||||
|
if (std.fs.path.isAbsolute(selected) or std.mem.indexOfScalar(u8, selected, '/') != null) return allocator.dupe(u8, selected);
|
||||||
|
const base = std.fs.path.dirname(launch_path) orelse ".";
|
||||||
|
return std.fs.path.join(allocator, &.{ base, selected });
|
||||||
|
}
|
||||||
|
|
||||||
fn persistLocalSave(
|
fn persistLocalSave(
|
||||||
allocator: std.mem.Allocator,
|
allocator: std.mem.Allocator,
|
||||||
io: std.Io,
|
io: std.Io,
|
||||||
|
|||||||
+20
-1
@@ -474,6 +474,10 @@ pub const Client = struct {
|
|||||||
return self.saved_bytes orelse Error.NothingSaved;
|
return self.saved_bytes orelse Error.NothingSaved;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn currentPath(self: *const Client) ?[]const u8 {
|
||||||
|
return self.current_path;
|
||||||
|
}
|
||||||
|
|
||||||
pub fn snapshotBytesAlloc(self: *const Client, allocator: std.mem.Allocator) ![]u8 {
|
pub fn snapshotBytesAlloc(self: *const Client, allocator: std.mem.Allocator) ![]u8 {
|
||||||
const snap = try self.session.snapshot();
|
const snap = try self.session.snapshot();
|
||||||
return allocator.dupe(u8, snap.bytes);
|
return allocator.dupe(u8, snap.bytes);
|
||||||
@@ -487,6 +491,10 @@ pub const Client = struct {
|
|||||||
return self.discard_on_quit;
|
return self.discard_on_quit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn openFilePicker(self: *Client) !void {
|
||||||
|
try self.openFilePickerPanel();
|
||||||
|
}
|
||||||
|
|
||||||
pub fn clearQuit(self: *Client) void {
|
pub fn clearQuit(self: *Client) void {
|
||||||
self.quit = false;
|
self.quit = false;
|
||||||
self.discard_on_quit = false;
|
self.discard_on_quit = false;
|
||||||
@@ -669,11 +677,18 @@ pub const Client = struct {
|
|||||||
const path = repo_mod.gitPathFromStatusRow(selected) catch return Error.ProtocolRejected;
|
const path = repo_mod.gitPathFromStatusRow(selected) catch return Error.ProtocolRejected;
|
||||||
const content = repo_mod.readRepoFileAlloc(self.allocator, io, cwd, path) catch return Error.ProtocolRejected;
|
const content = repo_mod.readRepoFileAlloc(self.allocator, io, cwd, path) catch return Error.ProtocolRejected;
|
||||||
defer self.allocator.free(content);
|
defer self.allocator.free(content);
|
||||||
try self.session.openFixture(content);
|
try self.openRepoContentAtPath(path, content, 0);
|
||||||
try self.session.closePanel();
|
try self.session.closePanel();
|
||||||
self.message = null;
|
self.message = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn openRepoContentAtPath(self: *Client, path: []const u8, content: []const u8, cursor: usize) !void {
|
||||||
|
if (self.current_path) |old| self.allocator.free(old);
|
||||||
|
self.current_path = try self.allocator.dupe(u8, path);
|
||||||
|
try self.session.openFixtureAt(content, cursor);
|
||||||
|
self.message = null;
|
||||||
|
}
|
||||||
|
|
||||||
fn openJobRun(self: *Client, cwd_and_command: []const u8) !void {
|
fn openJobRun(self: *Client, cwd_and_command: []const u8) !void {
|
||||||
const io = self.io orelse return Error.ProtocolRejected;
|
const io = self.io orelse return Error.ProtocolRejected;
|
||||||
const split = splitCwdAndCommand(cwd_and_command) orelse return Error.ProtocolRejected;
|
const split = splitCwdAndCommand(cwd_and_command) orelse return Error.ProtocolRejected;
|
||||||
@@ -2239,9 +2254,13 @@ pub const Client = struct {
|
|||||||
self.message = "quit:discard";
|
self.message = "quit:discard";
|
||||||
},
|
},
|
||||||
.open => |path| {
|
.open => |path| {
|
||||||
|
if (self.repo.content(path)) |content| {
|
||||||
|
try self.openRepoContentAtPath(path, content, 0);
|
||||||
|
} else |_| {
|
||||||
const line = try std.fmt.allocPrint(self.allocator, "open {s}", .{path});
|
const line = try std.fmt.allocPrint(self.allocator, "open {s}", .{path});
|
||||||
defer self.allocator.free(line);
|
defer self.allocator.free(line);
|
||||||
try self.applyProtocol(line);
|
try self.applyProtocol(line);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
.symbol => |symbol| try self.applyProtocol(symbol_mod.protocolCommand(symbol)),
|
.symbol => |symbol| try self.applyProtocol(symbol_mod.protocolCommand(symbol)),
|
||||||
.file_picker => try self.openFilePickerPanel(),
|
.file_picker => try self.openFilePickerPanel(),
|
||||||
|
|||||||
@@ -29,14 +29,16 @@ Artifact roles:
|
|||||||
- `keystrokes.tsv`: logical user key-event count and raw byte count for scenario
|
- `keystrokes.tsv`: logical user key-event count and raw byte count for scenario
|
||||||
payloads. A CSI sequence such as PageDown counts as one attached-key event
|
payloads. A CSI sequence such as PageDown counts as one attached-key event
|
||||||
while still recording all raw bytes sent.
|
while still recording all raw bytes sent.
|
||||||
|
- `saved-files.tsv`: per-file hashes/byte counts for scenarios that assert more
|
||||||
|
than the launched file, such as multi-file coding workflows.
|
||||||
- `raw.bin`: exact PTY bytes for low-level debugging.
|
- `raw.bin`: exact PTY bytes for low-level debugging.
|
||||||
|
|
||||||
Golden policy:
|
Golden policy:
|
||||||
|
|
||||||
- Investigate before updating any expected artifact or assertion.
|
- Investigate before updating any expected artifact or assertion.
|
||||||
- Prefer semantic artifacts (`manifest.tsv`, `visible-controls.normalized.txt`,
|
- Prefer semantic artifacts (`manifest.tsv`, `visible-controls.normalized.txt`,
|
||||||
`transcript.txt`, `widths.tsv`, `keystrokes.tsv`) for assertions; visual
|
`transcript.txt`, `widths.tsv`, `keystrokes.tsv`, `saved-files.tsv`) for
|
||||||
artifacts explain what a user would see.
|
assertions; visual artifacts explain what a user would see.
|
||||||
- Updating artifacts is safe only after the change is intentional and the receipt
|
- Updating artifacts is safe only after the change is intentional and the receipt
|
||||||
names the changed UI/control-byte behavior.
|
names the changed UI/control-byte behavior.
|
||||||
- Adding a mobile scenario must preserve the `ios-default-qwertz-space-path`
|
- Adding a mobile scenario must preserve the `ios-default-qwertz-space-path`
|
||||||
|
|||||||
+32
-4
@@ -44,6 +44,7 @@ class Scenario:
|
|||||||
forbidden_raw: tuple[str, ...] = ("^[",)
|
forbidden_raw: tuple[str, ...] = ("^[",)
|
||||||
required_attrs: tuple[str, ...] = ("cursor", "status", "current-line")
|
required_attrs: tuple[str, ...] = ("cursor", "status", "current-line")
|
||||||
max_key_events: int | None = None
|
max_key_events: int | None = None
|
||||||
|
expect_files: tuple[tuple[str, str], ...] = ()
|
||||||
|
|
||||||
|
|
||||||
def visible_controls(data: bytes) -> 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
|
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)
|
visible = visible_controls(transcript)
|
||||||
normalized_visible = normalize_visible_controls(visible)
|
normalized_visible = normalize_visible_controls(visible)
|
||||||
raw_seen_attrs = set(seen_attrs)
|
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}")
|
width_report.append(f"{idx} {len(line)} {line}")
|
||||||
(out_dir / "widths.tsv").write_text("\n".join(width_report) + "\n", encoding="utf-8")
|
(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")
|
(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 = [
|
snapshot = [
|
||||||
f"scenario\t{scenario.id}",
|
f"scenario\t{scenario.id}",
|
||||||
f"viewport\t{scenario.width}x{scenario.height}",
|
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"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"max_key_events\t{scenario.max_key_events if scenario.max_key_events is not None else ''}",
|
||||||
f"key_budget\t{budget_status}",
|
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")
|
(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"
|
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_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")
|
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")
|
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("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("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("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("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=()),
|
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)
|
tmp = Path(tmp_name)
|
||||||
target, expected_initial = scenario.setup(tmp)
|
target, expected_initial = scenario.setup(tmp)
|
||||||
_ = expected_initial
|
_ = 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()
|
transcript = bytearray()
|
||||||
grid = TerminalGrid(scenario.width, scenario.height)
|
grid = TerminalGrid(scenario.width, scenario.height)
|
||||||
try:
|
try:
|
||||||
@@ -518,9 +537,14 @@ def run_one(mim: Path, root_out: Path, scenario: Scenario) -> tuple[str, Path]:
|
|||||||
saved: str | None = None
|
saved: str | None = None
|
||||||
if scenario.target_kind == "file" and target.exists():
|
if scenario.target_kind == "file" and target.exists():
|
||||||
saved = target.read_text(encoding="utf-8")
|
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()
|
lines = grid.lines()
|
||||||
raw = bytes(transcript)
|
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] = []
|
failures: list[str] = []
|
||||||
raw_text = raw.decode("utf-8", errors="ignore")
|
raw_text = raw.decode("utf-8", errors="ignore")
|
||||||
_, key_events, _ = keystroke_rows(scenario)
|
_, 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}")
|
failures.append(f"key budget exceeded: {key_events} > {scenario.max_key_events}")
|
||||||
if scenario.expect_saved is not None and saved != scenario.expect_saved:
|
if scenario.expect_saved is not None and saved != scenario.expect_saved:
|
||||||
failures.append(f"saved file mismatch: {saved!r} != {scenario.expect_saved!r}")
|
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):
|
if has_bare_lf(raw):
|
||||||
failures.append("raw terminal output contains bare LF; expected CRLF in raw mode")
|
failures.append("raw terminal output contains bare LF; expected CRLF in raw mode")
|
||||||
plain_text = strip_csi(raw_text)
|
plain_text = strip_csi(raw_text)
|
||||||
|
|||||||
Reference in New Issue
Block a user