490 lines
25 KiB
Zig
490 lines
25 KiB
Zig
const std = @import("std");
|
|
const context_mod = @import("context.zig");
|
|
const session_mod = @import("session.zig");
|
|
|
|
// Tiny local session protocol for trusted same-user tools.
|
|
// req: session/002, session/003, governance/003
|
|
|
|
test {
|
|
_ = handleLine;
|
|
}
|
|
|
|
pub const max_request_bytes = 4096;
|
|
|
|
pub fn replayToolLogLine(allocator: std.mem.Allocator, session: *session_mod.Session, log_line: []const u8) ![]u8 {
|
|
const parsed = parseToolLog(log_line) orelse return allocator.dupe(u8, "err invalid tool log\n");
|
|
if (!std.mem.eql(u8, parsed.status, "ok")) return allocator.dupe(u8, "err logged command failed\n");
|
|
return commandResponse(allocator, session, parsed.command);
|
|
}
|
|
|
|
pub fn handleLine(allocator: std.mem.Allocator, session: *session_mod.Session, line: []const u8) ![]u8 {
|
|
const trimmed = std.mem.trimEnd(u8, line, "\r\n");
|
|
if (std.mem.eql(u8, trimmed, "state")) return stateResponse(allocator, session);
|
|
if (std.mem.eql(u8, trimmed, "context")) return context_mod.sessionContextAlloc(allocator, session, null, null, .{});
|
|
if (std.mem.startsWith(u8, trimmed, "context ")) return contextResponse(allocator, session, trimmed[8..]);
|
|
if (std.mem.startsWith(u8, trimmed, "open ")) {
|
|
const bytes = trimmed[5..];
|
|
if (!std.unicode.utf8ValidateSlice(bytes)) return allocator.dupe(u8, "err invalid utf8\n");
|
|
try session.openFixture(bytes);
|
|
return stateResponse(allocator, session);
|
|
}
|
|
if (std.mem.startsWith(u8, trimmed, "command ")) {
|
|
return commandResponse(allocator, session, trimmed[8..]);
|
|
}
|
|
if (std.mem.startsWith(u8, trimmed, "tool ")) {
|
|
return toolResponse(allocator, session, trimmed[5..]);
|
|
}
|
|
return allocator.dupe(u8, "err unknown request\n");
|
|
}
|
|
|
|
fn contextResponse(allocator: std.mem.Allocator, session: *session_mod.Session, payload: []const u8) ![]u8 {
|
|
const split = std.mem.indexOfScalar(u8, payload, ' ');
|
|
if (split) |index| return context_mod.sessionContextAlloc(allocator, session, payload[0..index], payload[index + 1 ..], .{});
|
|
return context_mod.sessionContextAlloc(allocator, session, payload, null, .{});
|
|
}
|
|
|
|
fn toolResponse(allocator: std.mem.Allocator, session: *session_mod.Session, payload: []const u8) ![]u8 {
|
|
const split = std.mem.indexOfScalar(u8, payload, ' ') orelse return allocator.dupe(u8, "err invalid tool request\n");
|
|
const name = payload[0..split];
|
|
const command = payload[split + 1 ..];
|
|
if (!validToolName(name) or command.len == 0 or std.mem.startsWith(u8, command, "tool ")) return allocator.dupe(u8, "err invalid tool request\n");
|
|
if (!validToolPayload(command)) return allocator.dupe(u8, "err invalid tool request\n");
|
|
|
|
const command_response = try commandResponse(allocator, session, command);
|
|
defer allocator.free(command_response);
|
|
const status: []const u8 = if (std.mem.startsWith(u8, command_response, "ok ")) "ok" else "err";
|
|
return std.fmt.allocPrint(
|
|
allocator,
|
|
"tool-log\t{s}\t{s}\t{s}\n{s}",
|
|
.{ name, command, status, command_response },
|
|
);
|
|
}
|
|
|
|
fn validToolName(name: []const u8) bool {
|
|
if (name.len == 0 or name.len > 32) return false;
|
|
for (name) |byte| {
|
|
if (!((byte >= 'a' and byte <= 'z') or (byte >= 'A' and byte <= 'Z') or (byte >= '0' and byte <= '9') or byte == '_' or byte == '-')) return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
fn validToolPayload(command: []const u8) bool {
|
|
for (command) |byte| {
|
|
if (byte == 0 or byte == '\t' or byte == '\n' or byte == '\r') return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
fn parseToolLog(log_line: []const u8) ?struct { name: []const u8, command: []const u8, status: []const u8 } {
|
|
const trimmed = std.mem.trimEnd(u8, log_line, "\r\n");
|
|
if (!std.mem.startsWith(u8, trimmed, "tool-log\t")) return null;
|
|
var fields = std.mem.splitScalar(u8, trimmed, '\t');
|
|
_ = fields.next() orelse return null;
|
|
const name = fields.next() orelse return null;
|
|
const command = fields.next() orelse return null;
|
|
const status = fields.next() orelse return null;
|
|
if (fields.next() != null) return null;
|
|
if (!validToolName(name) or !validToolPayload(command)) return null;
|
|
return .{ .name = name, .command = command, .status = status };
|
|
}
|
|
|
|
fn commandResponse(allocator: std.mem.Allocator, session: *session_mod.Session, command: []const u8) ![]u8 {
|
|
if (std.mem.eql(u8, command, "move_left")) return dispatchAndRespond(allocator, session, .move_left);
|
|
if (std.mem.eql(u8, command, "move_right")) return dispatchAndRespond(allocator, session, .move_right);
|
|
if (std.mem.eql(u8, command, "move_up")) return dispatchAndRespond(allocator, session, .move_up);
|
|
if (std.mem.eql(u8, command, "move_down")) return dispatchAndRespond(allocator, session, .move_down);
|
|
if (std.mem.eql(u8, command, "move_word_forward")) return dispatchAndRespond(allocator, session, .move_word_forward);
|
|
if (std.mem.eql(u8, command, "move_word_back")) return dispatchAndRespond(allocator, session, .move_word_back);
|
|
if (std.mem.eql(u8, command, "move_word_end")) return dispatchAndRespond(allocator, session, .move_word_end);
|
|
if (std.mem.eql(u8, command, "move_line_start")) return dispatchAndRespond(allocator, session, .move_line_start);
|
|
if (std.mem.eql(u8, command, "move_line_end")) return dispatchAndRespond(allocator, session, .move_line_end);
|
|
if (std.mem.eql(u8, command, "delete_backward")) return dispatchAndRespond(allocator, session, .delete_backward);
|
|
if (std.mem.eql(u8, command, "delete_forward")) return dispatchAndRespond(allocator, session, .delete_forward);
|
|
if (std.mem.eql(u8, command, "delete_line")) return dispatchAndRespond(allocator, session, .delete_line);
|
|
if (std.mem.eql(u8, command, "change_line")) return dispatchAndRespond(allocator, session, .change_line);
|
|
if (std.mem.eql(u8, command, "open_line_below")) return dispatchAndRespond(allocator, session, .open_line_below);
|
|
if (std.mem.eql(u8, command, "open_line_above")) return dispatchAndRespond(allocator, session, .open_line_above);
|
|
if (std.mem.startsWith(u8, command, "replace_char ")) return dispatchAndRespond(allocator, session, .{ .replace_char = command[13..] });
|
|
if (std.mem.startsWith(u8, command, "insert ")) return dispatchAndRespond(allocator, session, .{ .insert = command[7..] });
|
|
if (std.mem.startsWith(u8, command, "panel_open ")) return panelRespond(allocator, session, .{ .open = command[11..] });
|
|
if (std.mem.startsWith(u8, command, "list_open ")) return listOpenRespond(allocator, session, command[10..]);
|
|
if (std.mem.startsWith(u8, command, "list_filter ")) return listRespond(allocator, session, .{ .filter = command[12..] });
|
|
if (std.mem.eql(u8, command, "list_down")) return listRespond(allocator, session, .down);
|
|
if (std.mem.eql(u8, command, "list_up")) return listRespond(allocator, session, .up);
|
|
if (std.mem.eql(u8, command, "list_select")) return listRespond(allocator, session, .select);
|
|
if (std.mem.eql(u8, command, "list_cancel")) return listRespond(allocator, session, .cancel);
|
|
if (std.mem.eql(u8, command, "panel_close")) return panelRespond(allocator, session, .close);
|
|
if (std.mem.eql(u8, command, "panel_next")) return panelRespond(allocator, session, .next);
|
|
if (std.mem.eql(u8, command, "panel_prev")) return panelRespond(allocator, session, .previous);
|
|
if (std.mem.startsWith(u8, command, "pair ")) {
|
|
const pair = pairByName(command[5..]) orelse return allocator.dupe(u8, "err unknown pair\n");
|
|
return dispatchAndRespond(allocator, session, .{ .insert_pair = pair });
|
|
}
|
|
return allocator.dupe(u8, "err unknown command\n");
|
|
}
|
|
|
|
fn pairByName(name: []const u8) ?session_mod.Pair {
|
|
if (std.mem.eql(u8, name, "parens")) return .{ .open = "(", .close = ")" };
|
|
if (std.mem.eql(u8, name, "brackets")) return .{ .open = "[", .close = "]" };
|
|
if (std.mem.eql(u8, name, "braces")) return .{ .open = "{", .close = "}" };
|
|
if (std.mem.eql(u8, name, "double_quote")) return .{ .open = "\"", .close = "\"" };
|
|
if (std.mem.eql(u8, name, "single_quote")) return .{ .open = "'", .close = "'" };
|
|
if (std.mem.eql(u8, name, "backtick")) return .{ .open = "`", .close = "`" };
|
|
return null;
|
|
}
|
|
|
|
fn dispatchAndRespond(allocator: std.mem.Allocator, session: *session_mod.Session, command: session_mod.Command) ![]u8 {
|
|
session.dispatch(command) catch |err| switch (err) {
|
|
error.InvalidUtf8Insertion => return allocator.dupe(u8, "err invalid utf8\n"),
|
|
error.NoBufferOpen => return allocator.dupe(u8, "err no buffer open\n"),
|
|
else => return err,
|
|
};
|
|
return stateResponse(allocator, session);
|
|
}
|
|
|
|
const PanelCommand = union(enum) {
|
|
open: []const u8,
|
|
close,
|
|
next,
|
|
previous,
|
|
};
|
|
|
|
fn panelRespond(allocator: std.mem.Allocator, session: *session_mod.Session, command: PanelCommand) ![]u8 {
|
|
switch (command) {
|
|
.open => |title| session.openPanel(title) catch |err| switch (err) {
|
|
error.InvalidPanelTitle => return allocator.dupe(u8, "err invalid panel title\n"),
|
|
else => return err,
|
|
},
|
|
.close => session.closePanel() catch |err| switch (err) {
|
|
error.NoPanelOpen => return allocator.dupe(u8, "err no panel open\n"),
|
|
else => return err,
|
|
},
|
|
.next => session.nextPanel() catch |err| switch (err) {
|
|
error.NoPanelOpen => return allocator.dupe(u8, "err no panel open\n"),
|
|
else => return err,
|
|
},
|
|
.previous => session.previousPanel() catch |err| switch (err) {
|
|
error.NoPanelOpen => return allocator.dupe(u8, "err no panel open\n"),
|
|
else => return err,
|
|
},
|
|
}
|
|
return stateResponse(allocator, session);
|
|
}
|
|
|
|
const ListCommand = union(enum) {
|
|
filter: []const u8,
|
|
down,
|
|
up,
|
|
select,
|
|
cancel,
|
|
};
|
|
|
|
fn listOpenRespond(allocator: std.mem.Allocator, session: *session_mod.Session, payload: []const u8) ![]u8 {
|
|
const split = std.mem.indexOfScalar(u8, payload, ' ') orelse return allocator.dupe(u8, "err invalid list open\n");
|
|
const title = payload[0..split];
|
|
const items_payload = payload[split + 1 ..];
|
|
var items = std.ArrayList([]const u8).empty;
|
|
defer items.deinit(allocator);
|
|
var iter = std.mem.splitScalar(u8, items_payload, '|');
|
|
while (iter.next()) |item| try items.append(allocator, item);
|
|
session.openListPanel(title, items.items) catch |err| switch (err) {
|
|
error.InvalidPanelTitle => return allocator.dupe(u8, "err invalid panel title\n"),
|
|
error.InvalidListItem => return allocator.dupe(u8, "err invalid list item\n"),
|
|
error.EmptyList => return allocator.dupe(u8, "err empty list\n"),
|
|
else => return err,
|
|
};
|
|
return stateResponse(allocator, session);
|
|
}
|
|
|
|
fn listRespond(allocator: std.mem.Allocator, session: *session_mod.Session, command: ListCommand) ![]u8 {
|
|
switch (command) {
|
|
.filter => |filter| session.filterListPanel(filter) catch |err| return listError(allocator, err),
|
|
.down => session.listPanelDown() catch |err| return listError(allocator, err),
|
|
.up => session.listPanelUp() catch |err| return listError(allocator, err),
|
|
.select => _ = session.selectListPanel() catch |err| return listError(allocator, err),
|
|
.cancel => session.cancelListPanel() catch |err| return listError(allocator, err),
|
|
}
|
|
return stateResponse(allocator, session);
|
|
}
|
|
|
|
fn listError(allocator: std.mem.Allocator, err: anyerror) ![]u8 {
|
|
return switch (err) {
|
|
error.NoPanelOpen => allocator.dupe(u8, "err no panel open\n"),
|
|
error.ActivePanelIsNotList => allocator.dupe(u8, "err active panel is not list\n"),
|
|
error.InvalidListFilter => allocator.dupe(u8, "err invalid list filter\n"),
|
|
error.EmptyList => allocator.dupe(u8, "err empty list\n"),
|
|
else => err,
|
|
};
|
|
}
|
|
|
|
fn stateResponse(allocator: std.mem.Allocator, session: *session_mod.Session) ![]u8 {
|
|
const snap = try session.snapshot();
|
|
const panel_path = try session.panelPathAlloc(allocator);
|
|
defer allocator.free(panel_path);
|
|
const panel_summary = try session.panelSummaryAlloc(allocator);
|
|
defer allocator.free(panel_summary);
|
|
return std.fmt.allocPrint(
|
|
allocator,
|
|
"ok state cursor_byte={d} cursor_cell={d} bytes_len={d} panel_depth={d} active_panel={s} panel_path={s} panel_summary={s}\n",
|
|
.{
|
|
snap.cursor_byte,
|
|
snap.cursor_cell,
|
|
snap.bytes.len,
|
|
snap.panel_depth,
|
|
if (snap.active_panel_title) |title| title else "-",
|
|
panel_path,
|
|
panel_summary,
|
|
},
|
|
);
|
|
}
|
|
|
|
test "regular: trusted tool command logs and replays deterministic state" {
|
|
var session = session_mod.Session.init(std.testing.allocator);
|
|
defer session.deinit();
|
|
try session.openFixture("ab");
|
|
|
|
const response = try handleLine(std.testing.allocator, &session, "tool pi insert c\n");
|
|
defer std.testing.allocator.free(response);
|
|
try std.testing.expect(std.mem.startsWith(u8, response, "tool-log\tpi\tinsert c\tok\n"));
|
|
try std.testing.expect(std.mem.indexOf(u8, response, "bytes_len=3") != null);
|
|
|
|
const log_end = std.mem.indexOfScalar(u8, response, '\n').?;
|
|
var replayed = session_mod.Session.init(std.testing.allocator);
|
|
defer replayed.deinit();
|
|
try replayed.openFixture("ab");
|
|
const replay_response = try replayToolLogLine(std.testing.allocator, &replayed, response[0..log_end]);
|
|
defer std.testing.allocator.free(replay_response);
|
|
try std.testing.expect(std.mem.indexOf(u8, replay_response, "bytes_len=3") != null);
|
|
}
|
|
|
|
test "adversarial: tool path rejects hidden or malformed mutations" {
|
|
var session = session_mod.Session.init(std.testing.allocator);
|
|
defer session.deinit();
|
|
try session.openFixture("ab");
|
|
|
|
{
|
|
const response = try handleLine(std.testing.allocator, &session, "tool bad* insert c\n");
|
|
defer std.testing.allocator.free(response);
|
|
try std.testing.expectEqualStrings("err invalid tool request\n", response);
|
|
}
|
|
|
|
{
|
|
const response = try handleLine(std.testing.allocator, &session, "tool pi tool nested\n");
|
|
defer std.testing.allocator.free(response);
|
|
try std.testing.expectEqualStrings("err invalid tool request\n", response);
|
|
}
|
|
|
|
{
|
|
const response = try replayToolLogLine(std.testing.allocator, &session, "tool-log\tpi\tinsert c\terr");
|
|
defer std.testing.allocator.free(response);
|
|
try std.testing.expectEqualStrings("err logged command failed\n", response);
|
|
}
|
|
}
|
|
|
|
test "regular: protocol context request returns compact agent context" {
|
|
var session = session_mod.Session.init(std.testing.allocator);
|
|
defer session.deinit();
|
|
try session.openFixture("abc");
|
|
try session.openListPanel("diag", &.{ "one", "two" });
|
|
|
|
const response = try handleLine(std.testing.allocator, &session, "context fix_bug inspect\n");
|
|
defer std.testing.allocator.free(response);
|
|
try std.testing.expect(std.mem.indexOf(u8, response, "context:v1\n") != null);
|
|
try std.testing.expect(std.mem.indexOf(u8, response, "active_panel=diag") != null);
|
|
try std.testing.expect(std.mem.indexOf(u8, response, "panel_summary=") != null);
|
|
try std.testing.expect(std.mem.indexOf(u8, response, "task=fix_bug") != null);
|
|
try std.testing.expect(std.mem.indexOf(u8, response, "intent=inspect") != null);
|
|
}
|
|
|
|
test "regular: protocol opens bytes, reports state, and dispatches commands" {
|
|
var session = session_mod.Session.init(std.testing.allocator);
|
|
defer session.deinit();
|
|
|
|
const open = try handleLine(std.testing.allocator, &session, "open café\n");
|
|
defer std.testing.allocator.free(open);
|
|
try std.testing.expectEqualStrings("ok state cursor_byte=0 cursor_cell=0 bytes_len=5 panel_depth=0 active_panel=- panel_path=- panel_summary=-\n", open);
|
|
|
|
const moved = try handleLine(std.testing.allocator, &session, "command move_right\n");
|
|
defer std.testing.allocator.free(moved);
|
|
try std.testing.expectEqualStrings("ok state cursor_byte=1 cursor_cell=1 bytes_len=5 panel_depth=0 active_panel=- panel_path=- panel_summary=-\n", moved);
|
|
|
|
const inserted = try handleLine(std.testing.allocator, &session, "command insert 🔥\n");
|
|
defer std.testing.allocator.free(inserted);
|
|
try std.testing.expectEqualStrings("ok state cursor_byte=5 cursor_cell=3 bytes_len=9 panel_depth=0 active_panel=- panel_path=- panel_summary=-\n", inserted);
|
|
}
|
|
|
|
test "regular: protocol delete command removes a whole UTF-8 codepoint" {
|
|
var session = session_mod.Session.init(std.testing.allocator);
|
|
defer session.deinit();
|
|
|
|
var response = try handleLine(std.testing.allocator, &session, "open aé\n");
|
|
std.testing.allocator.free(response);
|
|
response = try handleLine(std.testing.allocator, &session, "command move_right\n");
|
|
std.testing.allocator.free(response);
|
|
response = try handleLine(std.testing.allocator, &session, "command move_right\n");
|
|
std.testing.allocator.free(response);
|
|
response = try handleLine(std.testing.allocator, &session, "command delete_backward\n");
|
|
defer std.testing.allocator.free(response);
|
|
|
|
try std.testing.expectEqualStrings("ok state cursor_byte=1 cursor_cell=1 bytes_len=1 panel_depth=0 active_panel=- panel_path=- panel_summary=-\n", response);
|
|
}
|
|
|
|
test "adversarial: protocol rejects unknown requests and commands" {
|
|
var session = session_mod.Session.init(std.testing.allocator);
|
|
defer session.deinit();
|
|
|
|
const request = try handleLine(std.testing.allocator, &session, "subscribe everything\n");
|
|
defer std.testing.allocator.free(request);
|
|
try std.testing.expectEqualStrings("err unknown request\n", request);
|
|
|
|
try session.openFixture("abc");
|
|
const command = try handleLine(std.testing.allocator, &session, "command plugin.load\n");
|
|
defer std.testing.allocator.free(command);
|
|
try std.testing.expectEqualStrings("err unknown command\n", command);
|
|
}
|
|
|
|
test "adversarial: protocol reports no buffer and invalid utf8 without corrupting state" {
|
|
var session = session_mod.Session.init(std.testing.allocator);
|
|
defer session.deinit();
|
|
|
|
const no_buffer = try handleLine(std.testing.allocator, &session, "state\n");
|
|
defer std.testing.allocator.free(no_buffer);
|
|
try std.testing.expectEqualStrings("ok state cursor_byte=0 cursor_cell=0 bytes_len=0 panel_depth=0 active_panel=- panel_path=- panel_summary=-\n", no_buffer);
|
|
|
|
try session.openFixture("safe");
|
|
const before = try session.snapshot();
|
|
const bad = [_]u8{ 'c', 'o', 'm', 'm', 'a', 'n', 'd', ' ', 'i', 'n', 's', 'e', 'r', 't', ' ', 0xc3, 0x28 };
|
|
const invalid = try handleLine(std.testing.allocator, &session, &bad);
|
|
defer std.testing.allocator.free(invalid);
|
|
try std.testing.expectEqualStrings("err invalid utf8\n", invalid);
|
|
const after = try session.snapshot();
|
|
try std.testing.expectEqualStrings(before.bytes, after.bytes);
|
|
}
|
|
|
|
test "regular: protocol pair commands insert delimiters with cursor between them" {
|
|
var session = session_mod.Session.init(std.testing.allocator);
|
|
defer session.deinit();
|
|
|
|
var response = try handleLine(std.testing.allocator, &session, "open call\n");
|
|
std.testing.allocator.free(response);
|
|
response = try handleLine(std.testing.allocator, &session, "command move_right\n");
|
|
std.testing.allocator.free(response);
|
|
response = try handleLine(std.testing.allocator, &session, "command move_right\n");
|
|
std.testing.allocator.free(response);
|
|
response = try handleLine(std.testing.allocator, &session, "command move_right\n");
|
|
std.testing.allocator.free(response);
|
|
response = try handleLine(std.testing.allocator, &session, "command move_right\n");
|
|
std.testing.allocator.free(response);
|
|
response = try handleLine(std.testing.allocator, &session, "command pair parens\n");
|
|
defer std.testing.allocator.free(response);
|
|
|
|
try std.testing.expectEqualStrings("ok state cursor_byte=5 cursor_cell=5 bytes_len=6 panel_depth=0 active_panel=- panel_path=- panel_summary=-\n", response);
|
|
try std.testing.expectEqualStrings("call()", (try session.snapshot()).bytes);
|
|
}
|
|
|
|
test "adversarial: protocol rejects unknown pair names" {
|
|
var session = session_mod.Session.init(std.testing.allocator);
|
|
defer session.deinit();
|
|
try session.openFixture("abc");
|
|
|
|
const response = try handleLine(std.testing.allocator, &session, "command pair snippet_everything\n");
|
|
defer std.testing.allocator.free(response);
|
|
try std.testing.expectEqualStrings("err unknown pair\n", response);
|
|
try std.testing.expectEqualStrings("abc", (try session.snapshot()).bytes);
|
|
}
|
|
|
|
test "regular: protocol opens switches and closes generic panels" {
|
|
var session = session_mod.Session.init(std.testing.allocator);
|
|
defer session.deinit();
|
|
|
|
{
|
|
const response = try handleLine(std.testing.allocator, &session, "command panel_open files\n");
|
|
defer std.testing.allocator.free(response);
|
|
try std.testing.expectEqualStrings("ok state cursor_byte=0 cursor_cell=0 bytes_len=0 panel_depth=1 active_panel=files panel_path=[files] panel_summary=empty\n", response);
|
|
}
|
|
{
|
|
const response = try handleLine(std.testing.allocator, &session, "command panel_open diagnostics\n");
|
|
defer std.testing.allocator.free(response);
|
|
try std.testing.expectEqualStrings("ok state cursor_byte=0 cursor_cell=0 bytes_len=0 panel_depth=2 active_panel=diagnostics panel_path=files>[diagnostics] panel_summary=empty\n", response);
|
|
}
|
|
{
|
|
const response = try handleLine(std.testing.allocator, &session, "command panel_prev\n");
|
|
defer std.testing.allocator.free(response);
|
|
try std.testing.expectEqualStrings("ok state cursor_byte=0 cursor_cell=0 bytes_len=0 panel_depth=2 active_panel=files panel_path=[files]>diagnostics panel_summary=empty\n", response);
|
|
}
|
|
{
|
|
const response = try handleLine(std.testing.allocator, &session, "command panel_close\n");
|
|
defer std.testing.allocator.free(response);
|
|
try std.testing.expectEqualStrings("ok state cursor_byte=0 cursor_cell=0 bytes_len=0 panel_depth=1 active_panel=diagnostics panel_path=[diagnostics] panel_summary=empty\n", response);
|
|
}
|
|
}
|
|
|
|
test "adversarial: protocol rejects invalid panel titles and empty panel actions" {
|
|
var session = session_mod.Session.init(std.testing.allocator);
|
|
defer session.deinit();
|
|
|
|
{
|
|
const response = try handleLine(std.testing.allocator, &session, "command panel_open bad title\n");
|
|
defer std.testing.allocator.free(response);
|
|
try std.testing.expectEqualStrings("err invalid panel title\n", response);
|
|
}
|
|
{
|
|
const response = try handleLine(std.testing.allocator, &session, "command panel_close\n");
|
|
defer std.testing.allocator.free(response);
|
|
try std.testing.expectEqualStrings("err no panel open\n", response);
|
|
}
|
|
}
|
|
|
|
test "regular: protocol list panel filters moves selects and remains inspectable" {
|
|
var session = session_mod.Session.init(std.testing.allocator);
|
|
defer session.deinit();
|
|
|
|
{
|
|
const response = try handleLine(std.testing.allocator, &session, "command list_open files src/main.zig|src/panel.zig|README.md\n");
|
|
defer std.testing.allocator.free(response);
|
|
try std.testing.expectEqualStrings("ok state cursor_byte=0 cursor_cell=0 bytes_len=0 panel_depth=1 active_panel=files panel_path=[files] panel_summary=list:filter=-,cursor=0,visible=3,selected=-\n", response);
|
|
}
|
|
{
|
|
const response = try handleLine(std.testing.allocator, &session, "command list_filter src\n");
|
|
defer std.testing.allocator.free(response);
|
|
try std.testing.expectEqualStrings("ok state cursor_byte=0 cursor_cell=0 bytes_len=0 panel_depth=1 active_panel=files panel_path=[files] panel_summary=list:filter=src,cursor=0,visible=2,selected=-\n", response);
|
|
}
|
|
{
|
|
const response = try handleLine(std.testing.allocator, &session, "command list_down\n");
|
|
defer std.testing.allocator.free(response);
|
|
try std.testing.expectEqualStrings("ok state cursor_byte=0 cursor_cell=0 bytes_len=0 panel_depth=1 active_panel=files panel_path=[files] panel_summary=list:filter=src,cursor=1,visible=2,selected=-\n", response);
|
|
}
|
|
{
|
|
const response = try handleLine(std.testing.allocator, &session, "command list_select\n");
|
|
defer std.testing.allocator.free(response);
|
|
try std.testing.expectEqualStrings("ok state cursor_byte=0 cursor_cell=0 bytes_len=0 panel_depth=1 active_panel=files panel_path=[files] panel_summary=list:filter=src,cursor=1,visible=2,selected=src/panel.zig\n", response);
|
|
}
|
|
}
|
|
|
|
test "regular: protocol list cancel closes list panel" {
|
|
var session = session_mod.Session.init(std.testing.allocator);
|
|
defer session.deinit();
|
|
|
|
var response = try handleLine(std.testing.allocator, &session, "command list_open files a.zig|b.zig\n");
|
|
std.testing.allocator.free(response);
|
|
response = try handleLine(std.testing.allocator, &session, "command list_cancel\n");
|
|
defer std.testing.allocator.free(response);
|
|
try std.testing.expectEqualStrings("ok state cursor_byte=0 cursor_cell=0 bytes_len=0 panel_depth=0 active_panel=- panel_path=- panel_summary=-\n", response);
|
|
}
|
|
|
|
test "adversarial: protocol list errors are explicit" {
|
|
var session = session_mod.Session.init(std.testing.allocator);
|
|
defer session.deinit();
|
|
|
|
{
|
|
const response = try handleLine(std.testing.allocator, &session, "command list_open files bad item\n");
|
|
defer std.testing.allocator.free(response);
|
|
try std.testing.expectEqualStrings("err invalid list item\n", response);
|
|
}
|
|
{
|
|
const response = try handleLine(std.testing.allocator, &session, "command list_filter src\n");
|
|
defer std.testing.allocator.free(response);
|
|
try std.testing.expectEqualStrings("err no panel open\n", response);
|
|
}
|
|
}
|