Add visible v1 diagnostics guardrails
This commit is contained in:
@@ -0,0 +1,48 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
// Local v1 guardrails for SSH/iPhone use: visible rows instead of silent stalls.
|
||||||
|
// req: testing/001, testing/002, testing/003, testing/004
|
||||||
|
pub const max_file_bytes: usize = 1024 * 1024;
|
||||||
|
pub const max_search_rows: usize = 64;
|
||||||
|
pub const process_stdout_limit: usize = 256 * 1024;
|
||||||
|
pub const process_stderr_limit: usize = 256 * 1024;
|
||||||
|
|
||||||
|
pub fn searchTruncatedRowAlloc(allocator: std.mem.Allocator, shown: usize, limit: usize) ![]u8 {
|
||||||
|
return std.fmt.allocPrint(allocator, "diagnostic:search_truncated:showing_{d}_of_limit_{d}", .{ shown, limit });
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn fileTooLargeRowAlloc(allocator: std.mem.Allocator, bytes: usize, limit: usize) ![]u8 {
|
||||||
|
return std.fmt.allocPrint(allocator, "diagnostic:file_too_large:{d}_bytes_limit_{d}", .{ bytes, limit });
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn unsupportedFileRowAlloc(allocator: std.mem.Allocator, reason: []const u8) ![]u8 {
|
||||||
|
return std.fmt.allocPrint(allocator, "diagnostic:unsupported_file:{s}", .{reason});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn processLimitRowAlloc(allocator: std.mem.Allocator, kind: []const u8, stdout_limit: usize, stderr_limit: usize) ![]u8 {
|
||||||
|
return std.fmt.allocPrint(allocator, "diagnostic:{s}_limits:stdout_{d}:stderr_{d}", .{ kind, stdout_limit, stderr_limit });
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn saveFailedRowAlloc(allocator: std.mem.Allocator, reason: []const u8) ![]u8 {
|
||||||
|
return std.fmt.allocPrint(allocator, "diagnostic:save_failed:{s}", .{reason});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn sanitizeReason(reason: []const u8) []const u8 {
|
||||||
|
if (reason.len == 0) return "unknown";
|
||||||
|
for (reason) |byte| {
|
||||||
|
if (byte <= 0x20 or byte == ':' or byte == '|') return "invalid";
|
||||||
|
}
|
||||||
|
return reason;
|
||||||
|
}
|
||||||
|
|
||||||
|
test "regular: diagnostic rows are compact and actionable" {
|
||||||
|
const row = try searchTruncatedRowAlloc(std.testing.allocator, 64, 64);
|
||||||
|
defer std.testing.allocator.free(row);
|
||||||
|
try std.testing.expectEqualStrings("diagnostic:search_truncated:showing_64_of_limit_64", row);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "adversarial: unsafe diagnostic reasons are sanitized" {
|
||||||
|
try std.testing.expectEqualStrings("invalid", sanitizeReason("two words"));
|
||||||
|
try std.testing.expectEqualStrings("unknown", sanitizeReason(""));
|
||||||
|
try std.testing.expectEqualStrings("spawn_error", sanitizeReason("spawn_error"));
|
||||||
|
}
|
||||||
+5
-2
@@ -1,4 +1,5 @@
|
|||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
|
const diagnostics = @import("diagnostics.zig");
|
||||||
|
|
||||||
// Explicit local build/test job execution for the shared panel model.
|
// Explicit local build/test job execution for the shared panel model.
|
||||||
// req: repo/002, ui/002, testing/001, testing/002, testing/003, testing/004
|
// req: repo/002, ui/002, testing/001, testing/002, testing/003, testing/004
|
||||||
@@ -57,10 +58,11 @@ fn runRowsWithPreviewAlloc(
|
|||||||
const result = std.process.run(allocator, io, .{
|
const result = std.process.run(allocator, io, .{
|
||||||
.argv = argv,
|
.argv = argv,
|
||||||
.cwd = .{ .path = cwd },
|
.cwd = .{ .path = cwd },
|
||||||
.stdout_limit = .limited(256 * 1024),
|
.stdout_limit = .limited(diagnostics.process_stdout_limit),
|
||||||
.stderr_limit = .limited(256 * 1024),
|
.stderr_limit = .limited(diagnostics.process_stderr_limit),
|
||||||
}) catch |err| {
|
}) catch |err| {
|
||||||
try rows.append(allocator, try std.fmt.allocPrint(allocator, "{s}:status:spawn_error_{s}", .{ kind, @errorName(err) }));
|
try rows.append(allocator, try std.fmt.allocPrint(allocator, "{s}:status:spawn_error_{s}", .{ kind, @errorName(err) }));
|
||||||
|
try rows.append(allocator, try diagnostics.processLimitRowAlloc(allocator, kind, diagnostics.process_stdout_limit, diagnostics.process_stderr_limit));
|
||||||
return rows.toOwnedSlice(allocator);
|
return rows.toOwnedSlice(allocator);
|
||||||
};
|
};
|
||||||
defer allocator.free(result.stdout);
|
defer allocator.free(result.stdout);
|
||||||
@@ -70,6 +72,7 @@ fn runRowsWithPreviewAlloc(
|
|||||||
try appendOutputRows(allocator, &rows, "stdout", result.stdout);
|
try appendOutputRows(allocator, &rows, "stdout", result.stdout);
|
||||||
try appendOutputRows(allocator, &rows, "stderr", result.stderr);
|
try appendOutputRows(allocator, &rows, "stderr", result.stderr);
|
||||||
if (rows.items.len == 2) try rows.append(allocator, try std.fmt.allocPrint(allocator, "{s}:output_empty", .{kind}));
|
if (rows.items.len == 2) try rows.append(allocator, try std.fmt.allocPrint(allocator, "{s}:output_empty", .{kind}));
|
||||||
|
try rows.append(allocator, try diagnostics.processLimitRowAlloc(allocator, kind, diagnostics.process_stdout_limit, diagnostics.process_stderr_limit));
|
||||||
return rows.toOwnedSlice(allocator);
|
return rows.toOwnedSlice(allocator);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
|
const diagnostics = @import("diagnostics.zig");
|
||||||
|
|
||||||
// Minimal LSP process lifecycle + document sync transport.
|
// Minimal LSP process lifecycle + document sync transport.
|
||||||
// req: coding/003, repo/002, testing/001, testing/002, testing/003, testing/004
|
// req: coding/003, repo/002, testing/001, testing/002, testing/003, testing/004
|
||||||
@@ -313,6 +314,7 @@ pub fn runDocumentSyncRowsAlloc(
|
|||||||
.stderr = .ignore,
|
.stderr = .ignore,
|
||||||
}) catch |err| {
|
}) catch |err| {
|
||||||
try rows.append(allocator, try std.fmt.allocPrint(allocator, "lsp:status:spawn_error_{s}", .{@errorName(err)}));
|
try rows.append(allocator, try std.fmt.allocPrint(allocator, "lsp:status:spawn_error_{s}", .{@errorName(err)}));
|
||||||
|
try rows.append(allocator, try diagnostics.processLimitRowAlloc(allocator, "lsp", diagnostics.process_stdout_limit, diagnostics.process_stderr_limit));
|
||||||
return rows.toOwnedSlice(allocator);
|
return rows.toOwnedSlice(allocator);
|
||||||
};
|
};
|
||||||
defer child.kill(io);
|
defer child.kill(io);
|
||||||
@@ -320,6 +322,7 @@ pub fn runDocumentSyncRowsAlloc(
|
|||||||
child.stdin.?.writeStreamingAll(io, transcript) catch |err| {
|
child.stdin.?.writeStreamingAll(io, transcript) catch |err| {
|
||||||
child.stdin.?.close(io);
|
child.stdin.?.close(io);
|
||||||
try rows.append(allocator, try std.fmt.allocPrint(allocator, "lsp:status:write_error_{s}", .{@errorName(err)}));
|
try rows.append(allocator, try std.fmt.allocPrint(allocator, "lsp:status:write_error_{s}", .{@errorName(err)}));
|
||||||
|
try rows.append(allocator, try diagnostics.processLimitRowAlloc(allocator, "lsp", diagnostics.process_stdout_limit, diagnostics.process_stderr_limit));
|
||||||
_ = child.wait(io) catch {};
|
_ = child.wait(io) catch {};
|
||||||
return rows.toOwnedSlice(allocator);
|
return rows.toOwnedSlice(allocator);
|
||||||
};
|
};
|
||||||
@@ -333,10 +336,12 @@ pub fn runDocumentSyncRowsAlloc(
|
|||||||
|
|
||||||
const term = child.wait(io) catch |err| {
|
const term = child.wait(io) catch |err| {
|
||||||
try rows.append(allocator, try std.fmt.allocPrint(allocator, "lsp:status:wait_error_{s}", .{@errorName(err)}));
|
try rows.append(allocator, try std.fmt.allocPrint(allocator, "lsp:status:wait_error_{s}", .{@errorName(err)}));
|
||||||
|
try rows.append(allocator, try diagnostics.processLimitRowAlloc(allocator, "lsp", diagnostics.process_stdout_limit, diagnostics.process_stderr_limit));
|
||||||
return rows.toOwnedSlice(allocator);
|
return rows.toOwnedSlice(allocator);
|
||||||
};
|
};
|
||||||
child.id = null;
|
child.id = null;
|
||||||
try appendTermRow(allocator, &rows, term);
|
try appendTermRow(allocator, &rows, term);
|
||||||
|
try rows.append(allocator, try diagnostics.processLimitRowAlloc(allocator, "lsp", diagnostics.process_stdout_limit, diagnostics.process_stderr_limit));
|
||||||
return rows.toOwnedSlice(allocator);
|
return rows.toOwnedSlice(allocator);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const context_mod = @import("context.zig");
|
const context_mod = @import("context.zig");
|
||||||
|
const diagnostics = @import("diagnostics.zig");
|
||||||
const input = @import("input.zig");
|
const input = @import("input.zig");
|
||||||
const job = @import("job.zig");
|
const job = @import("job.zig");
|
||||||
const layout = @import("layout.zig");
|
const layout = @import("layout.zig");
|
||||||
@@ -224,6 +225,7 @@ fn collectRemainingArgs(allocator: std.mem.Allocator, args: *std.process.Args.It
|
|||||||
|
|
||||||
test {
|
test {
|
||||||
_ = context_mod;
|
_ = context_mod;
|
||||||
|
_ = diagnostics;
|
||||||
_ = input;
|
_ = input;
|
||||||
_ = job;
|
_ = job;
|
||||||
_ = layout;
|
_ = layout;
|
||||||
|
|||||||
+15
-2
@@ -1,4 +1,5 @@
|
|||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
|
const diagnostics = @import("diagnostics.zig");
|
||||||
|
|
||||||
// Minimal repo index for file picker/tree behavior.
|
// Minimal repo index for file picker/tree behavior.
|
||||||
// req: repo/001, ui/002, testing/001, testing/002, testing/003, testing/004
|
// req: repo/001, ui/002, testing/001, testing/002, testing/003, testing/004
|
||||||
@@ -88,16 +89,24 @@ pub const Index = struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn searchRowsAlloc(self: *const Index, allocator: std.mem.Allocator, query: []const u8, include_ignored: bool) ![][]const u8 {
|
pub fn searchRowsAlloc(self: *const Index, allocator: std.mem.Allocator, query: []const u8, include_ignored: bool) ![][]const u8 {
|
||||||
|
return self.searchRowsLimitedAlloc(allocator, query, include_ignored, diagnostics.max_search_rows);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn searchRowsLimitedAlloc(self: *const Index, allocator: std.mem.Allocator, query: []const u8, include_ignored: bool, limit: usize) ![][]const u8 {
|
||||||
try validateQuery(query);
|
try validateQuery(query);
|
||||||
|
if (limit == 0) return Error.InvalidQuery;
|
||||||
var rows = std.ArrayList([]const u8).empty;
|
var rows = std.ArrayList([]const u8).empty;
|
||||||
errdefer {
|
errdefer {
|
||||||
for (rows.items) |row| allocator.free(row);
|
for (rows.items) |row| allocator.free(row);
|
||||||
rows.deinit(allocator);
|
rows.deinit(allocator);
|
||||||
}
|
}
|
||||||
|
var truncated = false;
|
||||||
for (self.entries.items) |entry| {
|
for (self.entries.items) |entry| {
|
||||||
if (!include_ignored and self.isIgnored(entry.path)) continue;
|
if (!include_ignored and self.isIgnored(entry.path)) continue;
|
||||||
try appendSearchRowsForEntry(allocator, &rows, entry, query);
|
try appendSearchRowsForEntry(allocator, &rows, entry, query, limit, &truncated);
|
||||||
|
if (truncated) break;
|
||||||
}
|
}
|
||||||
|
if (truncated) try rows.append(allocator, try diagnostics.searchTruncatedRowAlloc(allocator, rows.items.len, limit));
|
||||||
return rows.toOwnedSlice(allocator);
|
return rows.toOwnedSlice(allocator);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -147,11 +156,15 @@ fn validateQuery(query: []const u8) !void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn appendSearchRowsForEntry(allocator: std.mem.Allocator, rows: *std.ArrayList([]const u8), entry: Entry, query: []const u8) !void {
|
fn appendSearchRowsForEntry(allocator: std.mem.Allocator, rows: *std.ArrayList([]const u8), entry: Entry, query: []const u8, limit: usize, truncated: *bool) !void {
|
||||||
var line_no: usize = 1;
|
var line_no: usize = 1;
|
||||||
var lines = std.mem.splitScalar(u8, entry.content, '\n');
|
var lines = std.mem.splitScalar(u8, entry.content, '\n');
|
||||||
while (lines.next()) |line| : (line_no += 1) {
|
while (lines.next()) |line| : (line_no += 1) {
|
||||||
if (std.mem.indexOf(u8, line, query)) |column_zero| {
|
if (std.mem.indexOf(u8, line, query)) |column_zero| {
|
||||||
|
if (rows.items.len >= limit) {
|
||||||
|
truncated.* = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
const preview = try previewAlloc(allocator, line);
|
const preview = try previewAlloc(allocator, line);
|
||||||
defer allocator.free(preview);
|
defer allocator.free(preview);
|
||||||
try rows.append(allocator, try std.fmt.allocPrint(
|
try rows.append(allocator, try std.fmt.allocPrint(
|
||||||
|
|||||||
+28
-3
@@ -1,5 +1,6 @@
|
|||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const context_mod = @import("context.zig");
|
const context_mod = @import("context.zig");
|
||||||
|
const diagnostics_mod = @import("diagnostics.zig");
|
||||||
const input = @import("input.zig");
|
const input = @import("input.zig");
|
||||||
const job_mod = @import("job.zig");
|
const job_mod = @import("job.zig");
|
||||||
const leader_mod = @import("leader.zig");
|
const leader_mod = @import("leader.zig");
|
||||||
@@ -327,8 +328,20 @@ pub const Client = struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn addRepoFile(self: *Client, payload: []const u8) !void {
|
fn addRepoFile(self: *Client, payload: []const u8) !void {
|
||||||
const separator = std.mem.indexOfScalar(u8, payload, '=') orelse return repo_mod.Error.InvalidPath;
|
const separator = std.mem.indexOfScalar(u8, payload, '=') orelse {
|
||||||
try self.repo.addFile(payload[0..separator], payload[separator + 1 ..]);
|
self.message = "diagnostic:unsupported_file:invalid_repo_file_payload";
|
||||||
|
return repo_mod.Error.InvalidPath;
|
||||||
|
};
|
||||||
|
const file_bytes = payload[separator + 1 ..];
|
||||||
|
if (file_bytes.len > diagnostics_mod.max_file_bytes) {
|
||||||
|
self.message = "diagnostic:file_too_large";
|
||||||
|
return Error.ProtocolRejected;
|
||||||
|
}
|
||||||
|
self.repo.addFile(payload[0..separator], file_bytes) catch |err| {
|
||||||
|
self.message = "diagnostic:unsupported_file";
|
||||||
|
return err;
|
||||||
|
};
|
||||||
|
self.message = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn openRepoList(self: *Client, title: []const u8, tree: bool, include_ignored: bool) !void {
|
fn openRepoList(self: *Client, title: []const u8, tree: bool, include_ignored: bool) !void {
|
||||||
@@ -355,8 +368,9 @@ pub const Client = struct {
|
|||||||
for (rows) |row| self.allocator.free(row);
|
for (rows) |row| self.allocator.free(row);
|
||||||
self.allocator.free(rows);
|
self.allocator.free(rows);
|
||||||
}
|
}
|
||||||
|
const truncated = rowsContain(rows, "diagnostic:search_truncated");
|
||||||
try self.session.openListPanel("search", rows);
|
try self.session.openListPanel("search", rows);
|
||||||
self.message = null;
|
self.message = if (truncated) "diagnostic:search_truncated" else null;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn openSelectedSearchResult(self: *Client) !void {
|
fn openSelectedSearchResult(self: *Client) !void {
|
||||||
@@ -669,6 +683,10 @@ pub const Client = struct {
|
|||||||
|
|
||||||
fn save(self: *Client) !void {
|
fn save(self: *Client) !void {
|
||||||
const snap = try self.session.snapshot();
|
const snap = try self.session.snapshot();
|
||||||
|
if (snap.bytes.len > diagnostics_mod.max_file_bytes) {
|
||||||
|
self.message = "diagnostic:save_failed:file_too_large";
|
||||||
|
return Error.ProtocolRejected;
|
||||||
|
}
|
||||||
const copy = try self.allocator.dupe(u8, snap.bytes);
|
const copy = try self.allocator.dupe(u8, snap.bytes);
|
||||||
if (self.saved_bytes) |old| self.allocator.free(old);
|
if (self.saved_bytes) |old| self.allocator.free(old);
|
||||||
self.saved_bytes = copy;
|
self.saved_bytes = copy;
|
||||||
@@ -807,6 +825,13 @@ fn assertLinesFit(frame: []const u8, width: usize) !void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn rowsContain(rows: []const []const u8, needle: []const u8) bool {
|
||||||
|
for (rows) |row| {
|
||||||
|
if (std.mem.indexOf(u8, row, needle) != null) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
fn freeOwnedRows(allocator: std.mem.Allocator, rows: []const []const u8) void {
|
fn freeOwnedRows(allocator: std.mem.Allocator, rows: []const []const u8) void {
|
||||||
for (rows) |row| allocator.free(row);
|
for (rows) |row| allocator.free(row);
|
||||||
allocator.free(rows);
|
allocator.free(rows);
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
|
|
||||||
|
const diagnostics = @import("diagnostics.zig");
|
||||||
|
const job = @import("job.zig");
|
||||||
const lsp = @import("lsp.zig");
|
const lsp = @import("lsp.zig");
|
||||||
const profile = @import("profile.zig");
|
const profile = @import("profile.zig");
|
||||||
const repo = @import("repo.zig");
|
const repo = @import("repo.zig");
|
||||||
@@ -107,6 +109,81 @@ test "adversarial: v1 smoke catches realistic profile and boundary failures" {
|
|||||||
try std.testing.expect(std.mem.indexOf(u8, status, "plugin_host=not_built") != null);
|
try std.testing.expect(std.mem.indexOf(u8, status, "plugin_host=not_built") != null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test "regular: v1 smoke exposes bounded search, job, LSP, and save diagnostics" {
|
||||||
|
var client = try tui.Client.init(allocator, .{ .width = 72, .height = 12 });
|
||||||
|
defer client.deinit();
|
||||||
|
|
||||||
|
try client.handleTraceLine("open ready");
|
||||||
|
var line_buf: [64]u8 = undefined;
|
||||||
|
var i: usize = 0;
|
||||||
|
while (i < diagnostics.max_search_rows + 3) : (i += 1) {
|
||||||
|
const line = try std.fmt.bufPrint(&line_buf, "repo_file src/file{d}.zig=needle_{d}", .{ i, i });
|
||||||
|
try client.handleTraceLine(line);
|
||||||
|
}
|
||||||
|
try client.handleTraceLine("search_text needle");
|
||||||
|
{
|
||||||
|
const frame = try client.render(allocator);
|
||||||
|
defer allocator.free(frame);
|
||||||
|
try std.testing.expect(std.mem.indexOf(u8, frame, "diagnostic:search_truncated") != null);
|
||||||
|
}
|
||||||
|
|
||||||
|
const job_rows = try job.runRowsAlloc(allocator, std.Io.failing, ".", &.{"definitely-missing-mim-command"});
|
||||||
|
defer freeRows(job_rows);
|
||||||
|
try std.testing.expect(rowsContain(job_rows, "diagnostic:job_limits"));
|
||||||
|
try std.testing.expect(rowsContain(job_rows, "job:status:spawn_error"));
|
||||||
|
|
||||||
|
const lsp_rows = try lsp.runDocumentSyncRowsAlloc(allocator, std.Io.failing, ".", &.{"definitely-missing-lsp"}, .{
|
||||||
|
.uri = "file:///repo/src/main.zig",
|
||||||
|
.language_id = "zig",
|
||||||
|
.text = "pub fn main() void {}",
|
||||||
|
});
|
||||||
|
defer freeRows(lsp_rows);
|
||||||
|
try std.testing.expect(rowsContain(lsp_rows, "diagnostic:lsp_limits"));
|
||||||
|
try std.testing.expect(rowsContain(lsp_rows, "lsp:status:spawn_error"));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "adversarial: v1 smoke surfaces large or unsupported file and save failures" {
|
||||||
|
var client = try tui.Client.init(allocator, .{ .width = 72, .height = 6 });
|
||||||
|
defer client.deinit();
|
||||||
|
|
||||||
|
try std.testing.expectError(repo.Error.InvalidPath, client.handleTraceLine("repo_file ../secret=hidden"));
|
||||||
|
{
|
||||||
|
const frame = try client.render(allocator);
|
||||||
|
defer allocator.free(frame);
|
||||||
|
try std.testing.expect(std.mem.indexOf(u8, frame, "diagnostic:unsupported_file") != null);
|
||||||
|
}
|
||||||
|
|
||||||
|
const large_bytes = try allocator.alloc(u8, diagnostics.max_file_bytes + 1);
|
||||||
|
defer allocator.free(large_bytes);
|
||||||
|
@memset(large_bytes, 'x');
|
||||||
|
const large_command = try std.fmt.allocPrint(allocator, "repo_file src/large.txt={s}", .{large_bytes});
|
||||||
|
defer allocator.free(large_command);
|
||||||
|
try std.testing.expectError(tui.Error.ProtocolRejected, client.handleTraceLine(large_command));
|
||||||
|
{
|
||||||
|
const frame = try client.render(allocator);
|
||||||
|
defer allocator.free(frame);
|
||||||
|
try std.testing.expect(std.mem.indexOf(u8, frame, "diagnostic:file_too_large") != null);
|
||||||
|
}
|
||||||
|
|
||||||
|
try client.handleTraceLine("open ok");
|
||||||
|
const insert_command = try std.fmt.allocPrint(allocator, "insert {s}", .{large_bytes});
|
||||||
|
defer allocator.free(insert_command);
|
||||||
|
try client.handleTraceLine(insert_command);
|
||||||
|
try std.testing.expectError(tui.Error.ProtocolRejected, client.handleTraceLine("save"));
|
||||||
|
{
|
||||||
|
const frame = try client.render(allocator);
|
||||||
|
defer allocator.free(frame);
|
||||||
|
try std.testing.expect(std.mem.indexOf(u8, frame, "diagnostic:save_failed:file_too_large") != null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rowsContain(rows: []const []const u8, needle: []const u8) bool {
|
||||||
|
for (rows) |row| {
|
||||||
|
if (std.mem.indexOf(u8, row, needle) != null) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
fn freeRows(rows: [][]const u8) void {
|
fn freeRows(rows: [][]const u8) void {
|
||||||
for (rows) |row| allocator.free(row);
|
for (rows) |row| allocator.free(row);
|
||||||
allocator.free(rows);
|
allocator.free(rows);
|
||||||
|
|||||||
Reference in New Issue
Block a user