PranitInitial commit439f83c3 months agoHistory
430 lines · 16.8 KB · Zig
RawDownload
const std = @import("std");const builtin = @import("builtin");const debug_trace = @import("../../core/shared/debug_trace.zig");const io_mod = @import("../../core/shared/io.zig");const pathing = @import("../../core/workspace/pathing.zig");const tool_dispatch = @import("../../core/tooling/tool_dispatch.zig"); const Allocator = std.mem.Allocator; pub const Input = struct {    path: []u8,     pub fn deinit(self: *Input, alloc: Allocator) void {        alloc.free(self.path);        self.* = .{ .path = &.{} };    }}; pub fn decode(ctx: tool_dispatch.DispatchContext, args_json: []const u8) tool_dispatch.DispatchError!tool_dispatch.DecodeResult {    var parsed = std.json.parseFromSlice(std.json.Value, ctx.allocator, args_json, .{}) catch {        return .{ .failure = try ctx.allocator.dupe(u8, "open_file arguments must be valid JSON") };    };    defer parsed.deinit();     if (parsed.value != .object) {        return .{ .failure = try ctx.allocator.dupe(u8, "open_file arguments must be an object") };    }     const path_value = parsed.value.object.get("path") orelse {        return .{ .failure = try ctx.allocator.dupe(u8, "open_file field \"path\" is required") };    };    if (path_value != .string) {        return .{ .failure = try ctx.allocator.dupe(u8, "open_file field \"path\" must be a string") };    }     const owned_path = try ctx.allocator.dupe(u8, path_value.string);    errdefer ctx.allocator.free(owned_path);     const input = try ctx.allocator.create(Input);    errdefer ctx.allocator.destroy(input);    input.* = .{ .path = owned_path };     return .{ .input = .{ .ptr = input, .deinit_fn = inputDeinit } };} fn inputDeinit(ptr: *anyopaque, alloc: Allocator) void {    const input: *Input = @ptrCast(@alignCast(ptr));    input.deinit(alloc);    alloc.destroy(input);} pub fn validate(_: tool_dispatch.DispatchContext, _: tool_dispatch.ToolInput) tool_dispatch.DispatchError!?[]u8 {    return null;} pub fn call(ctx: tool_dispatch.DispatchContext, erased: tool_dispatch.ToolInput) tool_dispatch.DispatchError!tool_dispatch.ToolResult {    return callWithLauncher(ctx, erased, builtin.os.tag, .{});} const LaunchResult = struct {    term: std.process.Child.Term,}; const LaunchFn = *const fn (*anyopaque, Allocator, []const []const u8) anyerror!LaunchResult; const Launcher = struct {    ctx: *anyopaque = undefined,    launch: LaunchFn = launchActual,}; fn launchActual(_: *anyopaque, arena: Allocator, argv: []const []const u8) anyerror!LaunchResult {    const result = try std.process.run(arena, io_mod.getIo(), .{ .argv = argv });    defer arena.free(result.stdout);    defer arena.free(result.stderr);    return .{ .term = result.term };} fn callWithLauncher(ctx: tool_dispatch.DispatchContext, erased: tool_dispatch.ToolInput, os_tag: std.Target.Os.Tag, launcher: Launcher) tool_dispatch.DispatchError!tool_dispatch.ToolResult {    const input = erased.as(Input);     var arena_state = std.heap.ArenaAllocator.init(ctx.allocator);    defer arena_state.deinit();    const arena = arena_state.allocator();     const target = pathing.resolveWorkspaceOrExternalPath(arena, ctx.workspace_root, input.path) catch |err| return mapPathError(err);    const display_path = try displayPath(arena, ctx.workspace_root, target);    return launch_file(ctx.allocator, display_path, target, os_tag, launcher);} fn mapPathError(err: anyerror) tool_dispatch.DispatchError {    return switch (err) {        error.OutOfMemory => error.OutOfMemory,        error.InvalidPath => error.InvalidPath,        error.FileNotFound => error.FileNotFound,        error.HomeNotSet => error.HomeNotSet,        error.PathOutsideWorkspace => error.PathOutsideWorkspace,        error.WorkspaceUnavailable => error.WorkspaceUnavailable,        else => error.InvalidPath,    };} test "open_file preserves missing home path errors" {    try std.testing.expectEqual(error.HomeNotSet, mapPathError(error.HomeNotSet));} fn displayPath(arena: Allocator, workspace_root: []const u8, absolute_path: []const u8) ![]const u8 {    const rel = try pathing.workspaceRelativePath(arena, workspace_root, absolute_path);    return if (rel.len == 0) "." else rel;} fn launch_file(alloc: Allocator, display_path: []const u8, target: []const u8, os_tag: std.Target.Os.Tag, launcher: Launcher) tool_dispatch.DispatchError!tool_dispatch.ToolResult {    var argv: [2][]const u8 = undefined;    switch (os_tag) {        .macos => argv = .{ "open", target },        .linux => argv = .{ "xdg-open", target },        else => return .{ .failure = try alloc.dupe(u8, "open_file not supported on this OS") },    }     const result = launcher.launch(launcher.ctx, alloc, &argv) catch |err| {        debug_trace.logf("core", "open_file launcher failed err={s} path={s} target={s}", .{ @errorName(err), display_path, target });        return .{ .failure = try std.fmt.allocPrint(alloc, "failed to open {s}", .{target}) };    };    switch (result.term) {        .exited => |code| if (code == 0) {            return .{ .success = try std.fmt.allocPrint(alloc, "opened {s}", .{display_path}) };        },        else => {},    }     logUnsuccessfulTerm("open_file", display_path, target, result.term);    return .{ .failure = try std.fmt.allocPrint(alloc, "failed to open {s}", .{target}) };} fn logUnsuccessfulTerm(tool_name: []const u8, display_path: []const u8, target: []const u8, term: std.process.Child.Term) void {    switch (term) {        .exited => |code| debug_trace.logf("core", "{s} launcher unsuccessful exit_code={d} path={s} target={s}", .{ tool_name, code, display_path, target }),        .signal => |sig| debug_trace.logf("core", "{s} launcher unsuccessful term=signal signal={d} path={s} target={s}", .{ tool_name, @intFromEnum(sig), display_path, target }),        .stopped => |sig| debug_trace.logf("core", "{s} launcher unsuccessful term=stopped signal={d} path={s} target={s}", .{ tool_name, @intFromEnum(sig), display_path, target }),        .unknown => |code| debug_trace.logf("core", "{s} launcher unsuccessful term=unknown status={d} path={s} target={s}", .{ tool_name, code, display_path, target }),    }} pub fn readsOnly(_: tool_dispatch.ToolInput) bool {    return false;} pub fn isIrreversible(_: tool_dispatch.ToolInput) bool {    return false;} fn noopInputDeinit(_: *anyopaque, _: Allocator) void {} fn stackInput(input: *Input) tool_dispatch.ToolInput {    return .{ .ptr = input, .deinit_fn = noopInputDeinit };} fn validateStackInput(alloc: Allocator, input: *Input) !?[]u8 {    return validate(.{ .allocator = alloc }, stackInput(input));} fn expectDecodeFailure(args_json: []const u8, reason: []const u8) !void {    const alloc = std.testing.allocator;    const decoded = try decode(.{ .allocator = alloc }, args_json);    switch (decoded) {        .failure => |body| {            defer alloc.free(body);            try std.testing.expectEqualStrings(reason, body);        },        .input => |input| {            defer input.deinit(alloc);            try std.testing.expect(false);        },    }} fn expectDecodeInput(args_json: []const u8, path: []const u8) !void {    const alloc = std.testing.allocator;    const decoded = try decode(.{ .allocator = alloc }, args_json);    switch (decoded) {        .failure => |body| {            defer alloc.free(body);            try std.testing.expect(false);        },        .input => |erased| {            defer erased.deinit(alloc);            const input = erased.as(Input);            try std.testing.expectEqualStrings(path, input.path);        },    }} fn expectSuccessBody(result: tool_dispatch.ToolResult, expected: []const u8) !void {    switch (result) {        .success => |body| try std.testing.expectEqualStrings(expected, body),        .failure => try std.testing.expect(false),    }} fn expectFailureBody(result: tool_dispatch.ToolResult, expected: []const u8) !void {    switch (result) {        .success => try std.testing.expect(false),        .failure => |body| try std.testing.expectEqualStrings(expected, body),    }} const MockLauncher = struct {    calls: usize = 0,    term: std.process.Child.Term = .{ .exited = 0 },    err: ?anyerror = null,    argv: std.ArrayList([]u8) = .empty,     fn deinit(self: *@This(), alloc: Allocator) void {        for (self.argv.items) |arg| alloc.free(arg);        self.argv.deinit(alloc);    }     fn launch(ctx: *anyopaque, alloc: Allocator, argv: []const []const u8) anyerror!LaunchResult {        const self: *@This() = @ptrCast(@alignCast(ctx));        self.calls += 1;        for (argv) |arg| {            const owned = try alloc.dupe(u8, arg);            errdefer alloc.free(owned);            try self.argv.append(alloc, owned);        }        if (self.err) |err| return err;        return .{ .term = self.term };    }}; fn callPathWithMock(alloc: Allocator, workspace_root: []const u8, path: []const u8, os_tag: std.Target.Os.Tag, launcher: *MockLauncher) !tool_dispatch.ToolResult {    var input = Input{ .path = try alloc.dupe(u8, path) };    defer input.deinit(alloc);    return callWithLauncher(.{ .allocator = alloc, .workspace_root = workspace_root }, stackInput(&input), os_tag, .{        .ctx = @ptrCast(launcher),        .launch = MockLauncher.launch,    });} fn writeTempFile(alloc: Allocator, tmp: *std.testing.TmpDir, sub_path: []const u8, content: []const u8) ![]u8 {    var file = try tmp.dir.createFile(io_mod.getIo(), sub_path, .{});    defer file.close(io_mod.getIo());    try file.writeStreamingAll(io_mod.getIo(), content);    return io_mod.dirRealpathAlloc(alloc, tmp.dir, sub_path);} test "open_file rejects invalid JSON" {    try expectDecodeFailure("{", "open_file arguments must be valid JSON");} test "open_file rejects non-object JSON" {    try expectDecodeFailure("[]", "open_file arguments must be an object");} test "open_file rejects missing path" {    try expectDecodeFailure("{}", "open_file field \"path\" is required");} test "open_file rejects non-string path" {    try expectDecodeFailure("{\"path\":1}", "open_file field \"path\" must be a string");} test "open_file decodes owned path input" {    try expectDecodeInput("{\"path\":\"docs/readme.md\"}", "docs/readme.md");} test "open_file validation preserves path bytes" {    const alloc = std.testing.allocator;    var input = Input{ .path = try alloc.dupe(u8, " \t docs/readme.md \n") };    defer input.deinit(alloc);     try std.testing.expect((try validateStackInput(alloc, &input)) == null);    try std.testing.expectEqualStrings(" \t docs/readme.md \n", input.path);} test "open_file validation does not reject empty path before active resolver" {    const alloc = std.testing.allocator;    var input = Input{ .path = try alloc.dupe(u8, "") };    defer input.deinit(alloc);     try std.testing.expect((try validateStackInput(alloc, &input)) == null);    try std.testing.expectEqualStrings("", input.path);} test "open_file resolution failure propagates active resolver error" {    const alloc = std.testing.allocator;    var tmp = std.testing.tmpDir(.{});    defer tmp.cleanup();    const workspace = try io_mod.dirRealpathAlloc(alloc, tmp.dir, ".");    defer alloc.free(workspace);    var launcher = MockLauncher{};    defer launcher.deinit(alloc);     try std.testing.expectError(error.FileNotFound, callPathWithMock(alloc, workspace, "missing.txt", .macos, &launcher));    try std.testing.expectEqual(@as(usize, 0), launcher.calls);} test "open_file displays workspace-relative path on success" {    const alloc = std.testing.allocator;    var tmp = std.testing.tmpDir(.{});    defer tmp.cleanup();    const workspace = try io_mod.dirRealpathAlloc(alloc, tmp.dir, ".");    defer alloc.free(workspace);    const target = try writeTempFile(alloc, &tmp, "notes.txt", "hello\n");    defer alloc.free(target);    var launcher = MockLauncher{};    defer launcher.deinit(alloc);     var result = try callPathWithMock(alloc, workspace, "notes.txt", .macos, &launcher);    defer result.deinit(alloc);     try expectSuccessBody(result, "opened notes.txt");    try std.testing.expectEqual(@as(usize, 1), launcher.calls);    try std.testing.expectEqual(@as(usize, 2), launcher.argv.items.len);    try std.testing.expectEqualStrings("open", launcher.argv.items[0]);    try std.testing.expectEqualStrings(target, launcher.argv.items[1]);} test "open_file accepts external absolute paths through active workspace resolver" {    const alloc = std.testing.allocator;    var workspace_tmp = std.testing.tmpDir(.{});    defer workspace_tmp.cleanup();    const workspace = try io_mod.dirRealpathAlloc(alloc, workspace_tmp.dir, ".");    defer alloc.free(workspace);    var external_tmp = std.testing.tmpDir(.{});    defer external_tmp.cleanup();    const target = try writeTempFile(alloc, &external_tmp, "outside.txt", "hello\n");    defer alloc.free(target);    var launcher = MockLauncher{};    defer launcher.deinit(alloc);     var result = try callPathWithMock(alloc, workspace, target, .linux, &launcher);    defer result.deinit(alloc);     const expected = try std.fmt.allocPrint(alloc, "opened {s}", .{target});    defer alloc.free(expected);    try expectSuccessBody(result, expected);    try std.testing.expectEqual(@as(usize, 1), launcher.calls);    try std.testing.expectEqual(@as(usize, 2), launcher.argv.items.len);    try std.testing.expectEqualStrings("xdg-open", launcher.argv.items[0]);    try std.testing.expectEqualStrings(target, launcher.argv.items[1]);} test "open_file unsupported os does not launch" {    const alloc = std.testing.allocator;    var tmp = std.testing.tmpDir(.{});    defer tmp.cleanup();    const workspace = try io_mod.dirRealpathAlloc(alloc, tmp.dir, ".");    defer alloc.free(workspace);    const target = try writeTempFile(alloc, &tmp, "notes.txt", "hello\n");    defer alloc.free(target);    var launcher = MockLauncher{};    defer launcher.deinit(alloc);     var result = try callPathWithMock(alloc, workspace, "notes.txt", .windows, &launcher);    defer result.deinit(alloc);     try expectFailureBody(result, "open_file not supported on this OS");    try std.testing.expectEqual(@as(usize, 0), launcher.calls);    try std.testing.expectEqual(@as(usize, 0), launcher.argv.items.len);} test "open_file returns failure body for nonzero exit" {    const alloc = std.testing.allocator;    var tmp = std.testing.tmpDir(.{});    defer tmp.cleanup();    const workspace = try io_mod.dirRealpathAlloc(alloc, tmp.dir, ".");    defer alloc.free(workspace);    const target = try writeTempFile(alloc, &tmp, "notes.txt", "hello\n");    defer alloc.free(target);    var launcher = MockLauncher{ .term = .{ .exited = 1 } };    defer launcher.deinit(alloc);     var result = try callPathWithMock(alloc, workspace, "notes.txt", .macos, &launcher);    defer result.deinit(alloc);     const expected = try std.fmt.allocPrint(alloc, "failed to open {s}", .{target});    defer alloc.free(expected);    try expectFailureBody(result, expected);    try std.testing.expectEqual(@as(usize, 1), launcher.calls);} test "open_file returns failure body for launcher error" {    const alloc = std.testing.allocator;    var tmp = std.testing.tmpDir(.{});    defer tmp.cleanup();    const workspace = try io_mod.dirRealpathAlloc(alloc, tmp.dir, ".");    defer alloc.free(workspace);    const target = try writeTempFile(alloc, &tmp, "notes.txt", "hello\n");    defer alloc.free(target);    var launcher = MockLauncher{ .err = error.AccessDenied };    defer launcher.deinit(alloc);     var result = try callPathWithMock(alloc, workspace, "notes.txt", .macos, &launcher);    defer result.deinit(alloc);     const expected = try std.fmt.allocPrint(alloc, "failed to open {s}", .{target});    defer alloc.free(expected);    try expectFailureBody(result, expected);    try std.testing.expectEqual(@as(usize, 1), launcher.calls);} test "open_file returns failure body for non-exited term" {    const alloc = std.testing.allocator;    var tmp = std.testing.tmpDir(.{});    defer tmp.cleanup();    const workspace = try io_mod.dirRealpathAlloc(alloc, tmp.dir, ".");    defer alloc.free(workspace);    const target = try writeTempFile(alloc, &tmp, "notes.txt", "hello\n");    defer alloc.free(target);    var launcher = MockLauncher{ .term = .{ .unknown = 9 } };    defer launcher.deinit(alloc);     var result = try callPathWithMock(alloc, workspace, "notes.txt", .linux, &launcher);    defer result.deinit(alloc);     const expected = try std.fmt.allocPrint(alloc, "failed to open {s}", .{target});    defer alloc.free(expected);    try expectFailureBody(result, expected);    try std.testing.expectEqual(@as(usize, 1), launcher.calls);} test "open_file classifiers are side-effecting and reversible" {    const alloc = std.testing.allocator;    var input = Input{ .path = try alloc.dupe(u8, "notes.txt") };    defer input.deinit(alloc);    const erased = stackInput(&input);     try std.testing.expect(!readsOnly(erased));    try std.testing.expect(!isIrreversible(erased));}