diff --git a/AGENTS.md b/AGENTS.md index 0cb4d20..a7d95a2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -34,6 +34,7 @@ Canonical implementation checks: ```sh zig build zig build test +zig build v1-smoke zig build run -- --help ``` diff --git a/build.zig b/build.zig index e3af08b..8bc6245 100644 --- a/build.zig +++ b/build.zig @@ -58,7 +58,7 @@ pub fn build(b: *std.Build) void { }, }; - const main_mod = profileModule(b, target, optimize, profiles[0]); + const main_mod = profileModule(b, target, optimize, profiles[0], "src/main.zig"); const exe = b.addExecutable(.{ .name = profiles[0].exe_name, .root_module = main_mod, @@ -68,7 +68,7 @@ pub fn build(b: *std.Build) void { inline for (profiles[1..]) |profile| { const profile_exe = b.addExecutable(.{ .name = profile.exe_name, - .root_module = profileModule(b, target, optimize, profile), + .root_module = profileModule(b, target, optimize, profile, "src/main.zig"), }); b.installArtifact(profile_exe); addRunProfileStep(b, profile, profile_exe); @@ -84,11 +84,18 @@ pub fn build(b: *std.Build) void { run_step.dependOn(&run_cmd.step); const tests = b.addTest(.{ - .root_module = profileModule(b, target, optimize, profiles[0]), + .root_module = profileModule(b, target, optimize, profiles[0], "src/main.zig"), }); const test_cmd = b.addRunArtifact(tests); const test_step = b.step("test", "Run unit tests"); test_step.dependOn(&test_cmd.step); + + const smoke_tests = b.addTest(.{ + .root_module = profileModule(b, target, optimize, profiles[0], "src/v1_smoke.zig"), + }); + const smoke_cmd = b.addRunArtifact(smoke_tests); + const smoke_step = b.step("v1-smoke", "Run cheap v1 end-to-end fixture suite"); + smoke_step.dependOn(&smoke_cmd.step); } fn addRunProfileStep(b: *std.Build, profile: Profile, exe: *std.Build.Step.Compile) void { @@ -116,7 +123,7 @@ fn addServerInstallSteps(b: *std.Build, profile: Profile, exe: *std.Build.Step.C verify_step.dependOn(&verify_cmd.step); } -fn profileModule(b: *std.Build, target: std.Build.ResolvedTarget, optimize: std.builtin.OptimizeMode, profile: Profile) *std.Build.Module { +fn profileModule(b: *std.Build, target: std.Build.ResolvedTarget, optimize: std.builtin.OptimizeMode, profile: Profile, root_source_file: []const u8) *std.Build.Module { const options = b.addOptions(); options.addOption([]const u8, "active_profile", profile.name); options.addOption(bool, "local_socket", profile.local_socket); @@ -127,7 +134,7 @@ fn profileModule(b: *std.Build, target: std.Build.ResolvedTarget, optimize: std. options.addOption([]const u8, "tool_mutation", profile.tool_mutation); const mod = b.createModule(.{ - .root_source_file = b.path("src/main.zig"), + .root_source_file = b.path(root_source_file), .target = target, .optimize = optimize, }); diff --git a/src/v1_smoke.zig b/src/v1_smoke.zig new file mode 100644 index 0000000..a092c22 --- /dev/null +++ b/src/v1_smoke.zig @@ -0,0 +1,113 @@ +const std = @import("std"); + +const lsp = @import("lsp.zig"); +const profile = @import("profile.zig"); +const repo = @import("repo.zig"); +const session_mod = @import("session.zig"); +const socket = @import("socket.zig"); +const syntax = @import("syntax.zig"); +const tui = @import("tui.zig"); + +const allocator = std.testing.allocator; + +test "regular: v1 smoke opens edits saves and reports local socket context" { + var client = try tui.Client.init(allocator, .{ .width = 44, .height = 10 }); + defer client.deinit(); + + try client.handleTraceLine("open pub fn main() void {}"); + try client.handleTraceLine("right"); + try client.handleTraceLine("insert // mobile fix"); + try client.handleTraceLine("save"); + try std.testing.expect(std.mem.indexOf(u8, try client.saved(), "// mobile fix") != null); + + const frame = try client.render(allocator); + defer allocator.free(frame); + try std.testing.expect(std.mem.indexOf(u8, frame, "saved") != null); + + var session = session_mod.Session.init(allocator); + defer session.deinit(); + try session.openFixture("pub const answer = 42;\n"); + try session.openListPanel("files", &.{ "src/main.zig", "src/lib.zig" }); + + const path = "/tmp/mim-v1-smoke.sock"; + var server = try socket.Server.listen(allocator, path, &session); + defer server.deinit(); + + const thread = try std.Thread.spawn(.{}, socket.Server.acceptOnce, .{&server}); + const response = try socket.request(allocator, path, "context iphone_fix inspect"); + defer allocator.free(response); + thread.join(); + + try std.testing.expect(std.mem.indexOf(u8, response, "context:v1\n") != null); + try std.testing.expect(std.mem.indexOf(u8, response, "active_panel=files") != null); + try std.testing.expect(std.mem.indexOf(u8, response, "task=iphone_fix") != null); +} + +test "regular: v1 smoke covers repo panels, syntax, and LSP fixture rows" { + var client = try tui.Client.init(allocator, .{ .width = 52, .height = 12 }); + defer client.deinit(); + + try client.handleTraceLine("repo_gitignore zig-out/"); + try client.handleTraceLine("repo_file src/main.zig=pub fn main() void { add(1, 2); }"); + try client.handleTraceLine("repo_file zig-out/log.txt=ignored build output"); + try client.handleTraceLine("file_picker"); + { + const frame = try client.render(allocator); + defer allocator.free(frame); + try std.testing.expect(std.mem.indexOf(u8, frame, "src/main.zig") != null); + try std.testing.expect(std.mem.indexOf(u8, frame, "zig-out/log.txt") == null); + } + + try client.handleTraceLine("search_text add"); + { + const frame = try client.render(allocator); + defer allocator.free(frame); + try std.testing.expect(std.mem.indexOf(u8, frame, "src/main.zig:1:") != null); + } + + const syntax_rows = try syntax.rowsAlloc(allocator, "zig", "pub const answer = 42; // ok\n"); + defer freeRows(syntax_rows); + try std.testing.expect(syntax_rows.len >= 2); + try std.testing.expect(std.mem.indexOf(u8, syntax_rows[0], "syntax:zig:keyword") != null); + + const hover_rows = try lsp.hoverRowsAlloc(allocator, + \\{"jsonrpc":"2.0","id":8,"result":{"contents":{"kind":"markdown","value":"pub fn add(lhs: i32, rhs: i32) i32"}}} + , 28); + defer freeRows(hover_rows); + try std.testing.expect(hover_rows.len >= 2); + try std.testing.expect(std.mem.indexOf(u8, hover_rows[0], "lsp:hover") != null); + + const param = try lsp.parameterMoveRowsAlloc(allocator, "add(1, 2)", 4, .next); + defer freeRows(param.rows); + try std.testing.expect(param.cursor > 4); + try std.testing.expect(std.mem.indexOf(u8, param.rows[0], "lsp:param:active_2") != null); +} + +test "adversarial: v1 smoke catches realistic profile and boundary failures" { + try std.testing.expectError(error.NonLocalPath, socket.validateLocalSocketPath("relative.sock")); + try std.testing.expectError(error.NonLocalPath, socket.validateLocalSocketPath("/home/user/mim.sock")); + + var index = repo.Index.init(allocator); + defer index.deinit(); + try index.addIgnorePattern("secret/"); + try index.addFile("src/main.zig", "pub fn main() void {}\n"); + try index.addFile("secret/token.txt", "do-not-show\n"); + + const rows = try index.pickerItemsAlloc(allocator, false); + defer allocator.free(rows); + try std.testing.expectEqual(@as(usize, 1), rows.len); + try std.testing.expectEqualStrings("src/main.zig", rows[0]); + + try std.testing.expectError(lsp.Error.InvalidNavigationRow, lsp.navigationLocationFromRow("not a navigation row")); + + const status = try profile.statusAlloc(allocator); + defer allocator.free(status); + try std.testing.expect(std.mem.indexOf(u8, status, "profile:v1") != null); + try std.testing.expect(std.mem.indexOf(u8, status, "remote_transport=not_built") != null); + try std.testing.expect(std.mem.indexOf(u8, status, "plugin_host=not_built") != null); +} + +fn freeRows(rows: [][]const u8) void { + for (rows) |row| allocator.free(row); + allocator.free(rows); +}