Add LSP-assisted terminal E2E coverage
This commit is contained in:
+24
-4
@@ -127,7 +127,18 @@ pub fn main(init: std.process.Init) !u8 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const command = if (first_arg) |arg|
|
var e2e_prelude_path: ?[]const u8 = null;
|
||||||
|
var open_arg = first_arg;
|
||||||
|
if (first_arg) |arg| {
|
||||||
|
if (std.mem.eql(u8, arg, "--e2e-prelude-file")) {
|
||||||
|
e2e_prelude_path = args.next() orelse {
|
||||||
|
try stderr.writeStreamingAll(init.io, "mim: --e2e-prelude-file requires a path\n");
|
||||||
|
return 64;
|
||||||
|
};
|
||||||
|
open_arg = args.next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const command = if (open_arg) |arg|
|
||||||
parseArgs(&.{ "mim", arg })
|
parseArgs(&.{ "mim", arg })
|
||||||
else
|
else
|
||||||
parseArgs(&.{"mim"});
|
parseArgs(&.{"mim"});
|
||||||
@@ -135,7 +146,7 @@ pub fn main(init: std.process.Init) !u8 {
|
|||||||
switch (command) {
|
switch (command) {
|
||||||
.help => try stdout.writeStreamingAll(init.io, help_text),
|
.help => try stdout.writeStreamingAll(init.io, help_text),
|
||||||
.version => try stdout.writeStreamingAll(init.io, versionText()),
|
.version => try stdout.writeStreamingAll(init.io, versionText()),
|
||||||
.open => |path| try openLocalEditor(allocator, init.io, stdout, stderr, path),
|
.open => |path| try openLocalEditor(allocator, init.io, stdout, stderr, path, e2e_prelude_path),
|
||||||
}
|
}
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
@@ -147,6 +158,7 @@ fn openLocalEditor(
|
|||||||
stdout: std.Io.File,
|
stdout: std.Io.File,
|
||||||
stderr: std.Io.File,
|
stderr: std.Io.File,
|
||||||
path: []const u8,
|
path: []const u8,
|
||||||
|
e2e_prelude_path: ?[]const u8,
|
||||||
) !void {
|
) !void {
|
||||||
var client = try tui.Client.init(allocator, .{ .width = 80, .height = 22 });
|
var client = try tui.Client.init(allocator, .{ .width = 80, .height = 22 });
|
||||||
defer client.deinit();
|
defer client.deinit();
|
||||||
@@ -167,7 +179,7 @@ fn openLocalEditor(
|
|||||||
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");
|
||||||
return runLocalEditor(allocator, io, stdout, stderr, &client, path, false);
|
return runLocalEditor(allocator, io, stdout, stderr, &client, path, false, e2e_prelude_path);
|
||||||
},
|
},
|
||||||
else => return err,
|
else => return err,
|
||||||
};
|
};
|
||||||
@@ -182,7 +194,7 @@ fn openLocalEditor(
|
|||||||
try client.handleTraceLine("open ");
|
try client.handleTraceLine("open ");
|
||||||
}
|
}
|
||||||
|
|
||||||
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, e2e_prelude_path);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn seedDirectoryRepo(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 {
|
||||||
@@ -227,6 +239,7 @@ fn runLocalEditor(
|
|||||||
client: *tui.Client,
|
client: *tui.Client,
|
||||||
path: []const u8,
|
path: []const u8,
|
||||||
is_dir: bool,
|
is_dir: bool,
|
||||||
|
e2e_prelude_path: ?[]const u8,
|
||||||
) !void {
|
) !void {
|
||||||
const stdin_file = std.Io.File.stdin();
|
const stdin_file = std.Io.File.stdin();
|
||||||
const interactive = stdin_file.isTty(io) catch false;
|
const interactive = stdin_file.isTty(io) catch false;
|
||||||
@@ -236,6 +249,13 @@ fn runLocalEditor(
|
|||||||
var last_client_saved: ?[]u8 = null;
|
var last_client_saved: ?[]u8 = null;
|
||||||
defer if (last_client_saved) |bytes| allocator.free(bytes);
|
defer if (last_client_saved) |bytes| allocator.free(bytes);
|
||||||
|
|
||||||
|
if (e2e_prelude_path) |prelude_path| {
|
||||||
|
const prelude = try std.Io.Dir.cwd().readFileAlloc(io, prelude_path, allocator, .limited(64 * 1024));
|
||||||
|
defer allocator.free(prelude);
|
||||||
|
var lines = std.mem.splitScalar(u8, prelude, '\n');
|
||||||
|
while (lines.next()) |line| if (line.len != 0) try client.handleTraceLine(line);
|
||||||
|
}
|
||||||
|
|
||||||
if (!interactive) {
|
if (!interactive) {
|
||||||
try renderLocalFrame(allocator, io, stdout, client, path, is_dir, false);
|
try renderLocalFrame(allocator, io, stdout, client, path, is_dir, false);
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -31,6 +31,9 @@ Artifact roles:
|
|||||||
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
|
- `saved-files.tsv`: per-file hashes/byte counts for scenarios that assert more
|
||||||
than the launched file, such as multi-file coding workflows.
|
than the launched file, such as multi-file coding workflows.
|
||||||
|
- `e2e-prelude.trace` (scenario temp dir only when needed): harness-only trace
|
||||||
|
lines consumed before raw-mode input to seed deterministic provider fixtures;
|
||||||
|
this is not a runtime config surface.
|
||||||
- `raw.bin`: exact PTY bytes for low-level debugging.
|
- `raw.bin`: exact PTY bytes for low-level debugging.
|
||||||
|
|
||||||
Golden policy:
|
Golden policy:
|
||||||
|
|||||||
+21
-1
@@ -45,6 +45,7 @@ class Scenario:
|
|||||||
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], ...] = ()
|
expect_files: tuple[tuple[str, str], ...] = ()
|
||||||
|
prelude: str = ""
|
||||||
|
|
||||||
|
|
||||||
def visible_controls(data: bytes) -> str:
|
def visible_controls(data: bytes) -> str:
|
||||||
@@ -456,10 +457,23 @@ def setup_multifile(tmp: Path) -> tuple[Path, str | None]:
|
|||||||
return beta, "beta\n"
|
return beta, "beta\n"
|
||||||
|
|
||||||
|
|
||||||
|
def setup_lsp_assist(tmp: Path) -> tuple[Path, str | None]:
|
||||||
|
target = tmp / "lsp.zig"
|
||||||
|
target.write_text("abc\n", encoding="utf-8")
|
||||||
|
return target, "abc\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")
|
||||||
PANEL_QUIT = (b" ", b"q")
|
PANEL_QUIT = (b" ", b"q")
|
||||||
|
LSP_ASSIST_PRELUDE = "\n".join((
|
||||||
|
"lsp_hover_fixture zls|add(lhs, rhs)|Very long documentation that should stay viewport safe.",
|
||||||
|
"lsp_signature_fixture zls|add(lhs: i32, rhs: i32) active=rhs",
|
||||||
|
"diagnostic_fixture zls|1|lsp.zig|0|3|warning|demo",
|
||||||
|
"language_default_format zls",
|
||||||
|
"language_edit zls|format|1|0|3|ZLS",
|
||||||
|
))
|
||||||
|
|
||||||
IOS_DEFAULT_KEYS = set(b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ ")
|
IOS_DEFAULT_KEYS = set(b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ ")
|
||||||
|
|
||||||
@@ -478,6 +492,7 @@ SCENARIOS = [
|
|||||||
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("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("lsp-assisted-coding", 60, 12, "lsp-fixture", "attached-keyboard", "lsp", "truecolor", "file", setup_lsp_assist, (b" ", b"l", b"h", b" ", b"l", b"s", b" ", b"d", b"q", b" ", b"d", b"n", b" ", b"l", b"f", b" ", b"Q"), "abc\n", ("[zls] add(lhs, rhs)", "[zls] add(lhs: i32, rhs: i32) active=rhs", "diag:fresh:zls", "format:zls:applied", "ZLS", "mode:normal"), max_key_events=17, prelude=LSP_ASSIST_PRELUDE),
|
||||||
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=()),
|
||||||
]
|
]
|
||||||
@@ -503,7 +518,12 @@ def run_one(mim: Path, root_out: Path, scenario: Scenario) -> tuple[str, Path]:
|
|||||||
target, expected_initial = scenario.setup(tmp)
|
target, expected_initial = scenario.setup(tmp)
|
||||||
_ = expected_initial
|
_ = expected_initial
|
||||||
launch_arg = target.relative_to(tmp).as_posix()
|
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)
|
argv = [str(mim), launch_arg]
|
||||||
|
if scenario.prelude:
|
||||||
|
prelude_path = tmp / "e2e-prelude.trace"
|
||||||
|
prelude_path.write_text(scenario.prelude + "\n", encoding="utf-8")
|
||||||
|
argv = [str(mim), "--e2e-prelude-file", prelude_path.name, launch_arg]
|
||||||
|
pid, fd = spawn_under_pty(argv, 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:
|
||||||
|
|||||||
Reference in New Issue
Block a user