PranitInitial commit439f83c3 months agoHistory
252 lines · 10.0 KB · Zig
RawDownload
const std = @import("std");const io_mod = @import("../../core/shared/io.zig");const profile_paths = @import("../../core/shared/profile_paths.zig");const tool_args = @import("../../core/tooling/tool_args.zig");const tool_dispatch = @import("../../core/tooling/tool_dispatch.zig"); const Allocator = std.mem.Allocator; pub const Input = struct {    action: []u8,    fact: ?[]u8,     pub fn deinit(self: *Input, alloc: Allocator) void {        alloc.free(self.action);        if (self.fact) |fact| alloc.free(fact);        self.* = .{ .action = &.{}, .fact = null };    }}; 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, "memory arguments must be valid JSON") };    };    defer parsed.deinit();     if (parsed.value != .object) {        return .{ .failure = try ctx.allocator.dupe(u8, "memory arguments must be an object") };    }     const action_value = parsed.value.object.get("action") orelse {        return .{ .failure = try ctx.allocator.dupe(u8, "memory field \"action\" is required") };    };    if (action_value != .string) {        return .{ .failure = try ctx.allocator.dupe(u8, "memory field \"action\" must be a string") };    }     const fact_value = parsed.value.object.get("fact");    const fact: ?[]u8 = if (fact_value) |value|        if (value == .string) try ctx.allocator.dupe(u8, value.string) else null    else        null;    errdefer if (fact) |owned| ctx.allocator.free(owned);     const action = try ctx.allocator.dupe(u8, action_value.string);    errdefer ctx.allocator.free(action);     const input = try ctx.allocator.create(Input);    errdefer ctx.allocator.destroy(input);    input.* = .{ .action = action, .fact = fact };     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 {    const input = erased.as(Input);    const output = runMemory(ctx.allocator, input.action, input.fact) catch |err| switch (err) {        error.OutOfMemory => return error.OutOfMemory,        else => return .{ .failure = try std.fmt.allocPrint(ctx.allocator, "memory failed: {s}", .{@errorName(err)}) },    };    return .{ .success = output };} pub fn execute(arena: Allocator, args_json: []const u8) ![]u8 {    const args = try tool_args.parseToolArgsObject(arena, args_json);    const action = try tool_args.requiredStringArg(args, "action");    const fact = tool_args.optionalStringArg(args, "fact");    return runMemory(arena, action, fact);} fn runMemory(alloc: Allocator, action: []const u8, fact: ?[]const u8) ![]u8 {    const home = io_mod.getenv("HOME") orelse return std.fmt.allocPrint(alloc, "memory unavailable: HOME not set", .{});    const memories_path = try profile_paths.memoriesPath(alloc, home);    defer alloc.free(memories_path);     if (std.mem.eql(u8, action, "save")) {        const fact_value = fact orelse return std.fmt.allocPrint(alloc, "no fact provided", .{});        var existing = loadMemories(alloc, memories_path);        defer freeMemories(alloc, &existing);         for (existing.items) |memory| {            if (std.mem.eql(u8, memory, fact_value)) return std.fmt.allocPrint(alloc, "remembered", .{});        }         try existing.append(alloc, try alloc.dupe(u8, fact_value));        try saveMemories(alloc, memories_path, existing.items);        return std.fmt.allocPrint(alloc, "remembered", .{});    }     if (std.mem.eql(u8, action, "list")) {        var existing = loadMemories(alloc, memories_path);        defer freeMemories(alloc, &existing);         if (existing.items.len == 0) return std.fmt.allocPrint(alloc, "No saved memories", .{});         var out: std.Io.Writer.Allocating = .init(alloc);        defer out.deinit();        for (existing.items) |memory| {            try out.writer.print("- {s}\n", .{memory});        }        return try out.toOwnedSlice();    }     if (std.mem.eql(u8, action, "clear")) {        std.Io.Dir.deleteFileAbsolute(io_mod.getIo(), memories_path) catch {};        return std.fmt.allocPrint(alloc, "memories cleared", .{});    }     return std.fmt.allocPrint(alloc, "unknown memory action: {s}", .{action});} fn loadMemories(alloc: Allocator, path: []const u8) std.ArrayList([]u8) {    var list: std.ArrayList([]u8) = .empty;    var file = std.Io.Dir.openFileAbsolute(io_mod.getIo(), path, .{}) catch return list;    defer file.close(io_mod.getIo());    const content = io_mod.readFileToEnd(alloc, &file, 1024 * 1024) catch return list;    defer alloc.free(content);     const parsed = std.json.parseFromSlice(std.json.Value, alloc, content, .{}) catch return list;    defer parsed.deinit();     if (parsed.value != .array) return list;    for (parsed.value.array.items) |item| {        if (item != .string) continue;        list.append(alloc, alloc.dupe(u8, item.string) catch continue) catch continue;    }    return list;} fn freeMemories(alloc: Allocator, list: *std.ArrayList([]u8)) void {    for (list.items) |memory| alloc.free(memory);    list.deinit(alloc);} fn saveMemories(alloc: Allocator, path: []const u8, memories: []const []u8) !void {    const dir_path = std.fs.path.dirname(path) orelse return error.InvalidPath;    std.Io.Dir.createDirAbsolute(io_mod.getIo(), dir_path, .default_dir) catch |err| {        if (err != error.PathAlreadyExists) return err;    };     var out: std.Io.Writer.Allocating = .init(alloc);    defer out.deinit();    try out.writer.writeByte('[');    for (memories, 0..) |memory, index| {        if (index > 0) try out.writer.writeByte(',');        try out.writer.writeByte('\n');        try out.writer.writeAll("  ");        try std.json.Stringify.value(memory, .{}, &out.writer);    }    if (memories.len > 0) try out.writer.writeByte('\n');    try out.writer.writeAll("]\n");    const json = try out.toOwnedSlice();    defer alloc.free(json);     io_mod.writeFileAtomic(alloc, path, json) catch {        var file = try std.Io.Dir.createFileAbsolute(io_mod.getIo(), path, .{ .truncate = true });        defer file.close(io_mod.getIo());        try file.writeStreamingAll(io_mod.getIo(), json);    };} pub fn readsOnly(erased: tool_dispatch.ToolInput) bool {    return std.mem.eql(u8, erased.as(Input).action, "list");} pub fn isIrreversible(erased: tool_dispatch.ToolInput) bool {    return std.mem.eql(u8, erased.as(Input).action, "clear");} fn expectDecodeFailure(args_json: []const u8, expected: []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(expected, body);        },        .input => |input| {            defer input.deinit(alloc);            try std.testing.expect(false);        },    }} fn expectMemoryOutput(args_json: []const u8, expected: []const u8) !void {    var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator);    defer arena_state.deinit();    const arena = arena_state.allocator();     const output = try execute(arena, args_json);    try std.testing.expectEqualStrings(expected, output);} fn setTestHome(home: ?[]const u8) !void {    const map = try std.heap.c_allocator.create(std.process.Environ.Map);    map.* = std.process.Environ.Map.init(std.heap.c_allocator);    if (home) |value| try map.put("HOME", value);    io_mod.setEnvironMap(map);} test "memory owner rejects invalid JSON and action shape" {    try expectDecodeFailure("{", "memory arguments must be valid JSON");    try expectDecodeFailure("[]", "memory arguments must be an object");    try expectDecodeFailure("{}", "memory field \"action\" is required");    try expectDecodeFailure("{\"action\":1}", "memory field \"action\" must be a string");} test "memory owner preserves active output behavior" {    const alloc = std.testing.allocator;    try setTestHome(null);    defer setTestHome(null) catch {};    try expectMemoryOutput("{\"action\":\"list\"}", "memory unavailable: HOME not set");     var tmp = std.testing.tmpDir(.{});    defer tmp.cleanup();    try tmp.dir.createDirPath(io_mod.getIo(), "home");    const home = try io_mod.dirRealpathAlloc(alloc, tmp.dir, "home");    defer alloc.free(home);    try setTestHome(home);     try expectMemoryOutput("{\"action\":\"list\"}", "No saved memories");    try expectMemoryOutput("{\"action\":\"save\"}", "no fact provided");    try expectMemoryOutput("{\"action\":\"unknown\"}", "unknown memory action: unknown");    try expectMemoryOutput("{\"action\":\"save\",\"fact\":\"likes Zig\"}", "remembered");    try expectMemoryOutput("{\"action\":\"save\",\"fact\":\"likes Zig\"}", "remembered");    try expectMemoryOutput("{\"action\":\"list\"}", "- likes Zig\n");     const memories_path = try profile_paths.memoriesPath(alloc, home);    defer alloc.free(memories_path);    var file = try std.Io.Dir.openFileAbsolute(io_mod.getIo(), memories_path, .{});    const content = blk: {        defer file.close(io_mod.getIo());        break :blk try io_mod.readFileToEnd(alloc, &file, 4096);    };    defer alloc.free(content);    var parsed = try std.json.parseFromSlice(std.json.Value, alloc, content, .{});    defer parsed.deinit();    try std.testing.expectEqual(@as(usize, 1), parsed.value.array.items.len);    try std.testing.expectEqualStrings("likes Zig", parsed.value.array.items[0].string);     try expectMemoryOutput("{\"action\":\"clear\"}", "memories cleared");    try expectMemoryOutput("{\"action\":\"list\"}", "No saved memories");}