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 diagnostics = @import("diagnostics.zig");
|
||||
|
||||
// Explicit local build/test job execution for the shared panel model.
|
||||
// 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, .{
|
||||
.argv = argv,
|
||||
.cwd = .{ .path = cwd },
|
||||
.stdout_limit = .limited(256 * 1024),
|
||||
.stderr_limit = .limited(256 * 1024),
|
||||
.stdout_limit = .limited(diagnostics.process_stdout_limit),
|
||||
.stderr_limit = .limited(diagnostics.process_stderr_limit),
|
||||
}) catch |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);
|
||||
};
|
||||
defer allocator.free(result.stdout);
|
||||
@@ -70,6 +72,7 @@ fn runRowsWithPreviewAlloc(
|
||||
try appendOutputRows(allocator, &rows, "stdout", result.stdout);
|
||||
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}));
|
||||
try rows.append(allocator, try diagnostics.processLimitRowAlloc(allocator, kind, diagnostics.process_stdout_limit, diagnostics.process_stderr_limit));
|
||||
return rows.toOwnedSlice(allocator);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
const std = @import("std");
|
||||
const diagnostics = @import("diagnostics.zig");
|
||||
|
||||
// Minimal LSP process lifecycle + document sync transport.
|
||||
// req: coding/003, repo/002, testing/001, testing/002, testing/003, testing/004
|
||||
@@ -313,6 +314,7 @@ pub fn runDocumentSyncRowsAlloc(
|
||||
.stderr = .ignore,
|
||||
}) catch |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);
|
||||
};
|
||||
defer child.kill(io);
|
||||
@@ -320,6 +322,7 @@ pub fn runDocumentSyncRowsAlloc(
|
||||
child.stdin.?.writeStreamingAll(io, transcript) catch |err| {
|
||||
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 diagnostics.processLimitRowAlloc(allocator, "lsp", diagnostics.process_stdout_limit, diagnostics.process_stderr_limit));
|
||||
_ = child.wait(io) catch {};
|
||||
return rows.toOwnedSlice(allocator);
|
||||
};
|
||||
@@ -333,10 +336,12 @@ pub fn runDocumentSyncRowsAlloc(
|
||||
|
||||
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 diagnostics.processLimitRowAlloc(allocator, "lsp", diagnostics.process_stdout_limit, diagnostics.process_stderr_limit));
|
||||
return rows.toOwnedSlice(allocator);
|
||||
};
|
||||
child.id = null;
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
const std = @import("std");
|
||||
const context_mod = @import("context.zig");
|
||||
const diagnostics = @import("diagnostics.zig");
|
||||
const input = @import("input.zig");
|
||||
const job = @import("job.zig");
|
||||
const layout = @import("layout.zig");
|
||||
@@ -224,6 +225,7 @@ fn collectRemainingArgs(allocator: std.mem.Allocator, args: *std.process.Args.It
|
||||
|
||||
test {
|
||||
_ = context_mod;
|
||||
_ = diagnostics;
|
||||
_ = input;
|
||||
_ = job;
|
||||
_ = layout;
|
||||
|
||||
+15
-2
@@ -1,4 +1,5 @@
|
||||
const std = @import("std");
|
||||
const diagnostics = @import("diagnostics.zig");
|
||||
|
||||
// Minimal repo index for file picker/tree behavior.
|
||||
// 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 {
|
||||
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);
|
||||
if (limit == 0) return Error.InvalidQuery;
|
||||
var rows = std.ArrayList([]const u8).empty;
|
||||
errdefer {
|
||||
for (rows.items) |row| allocator.free(row);
|
||||
rows.deinit(allocator);
|
||||
}
|
||||
var truncated = false;
|
||||
for (self.entries.items) |entry| {
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -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 lines = std.mem.splitScalar(u8, entry.content, '\n');
|
||||
while (lines.next()) |line| : (line_no += 1) {
|
||||
if (std.mem.indexOf(u8, line, query)) |column_zero| {
|
||||
if (rows.items.len >= limit) {
|
||||
truncated.* = true;
|
||||
return;
|
||||
}
|
||||
const preview = try previewAlloc(allocator, line);
|
||||
defer allocator.free(preview);
|
||||
try rows.append(allocator, try std.fmt.allocPrint(
|
||||
|
||||
+28
-3
@@ -1,5 +1,6 @@
|
||||
const std = @import("std");
|
||||
const context_mod = @import("context.zig");
|
||||
const diagnostics_mod = @import("diagnostics.zig");
|
||||
const input = @import("input.zig");
|
||||
const job_mod = @import("job.zig");
|
||||
const leader_mod = @import("leader.zig");
|
||||
@@ -327,8 +328,20 @@ pub const Client = struct {
|
||||
}
|
||||
|
||||
fn addRepoFile(self: *Client, payload: []const u8) !void {
|
||||
const separator = std.mem.indexOfScalar(u8, payload, '=') orelse return repo_mod.Error.InvalidPath;
|
||||
try self.repo.addFile(payload[0..separator], payload[separator + 1 ..]);
|
||||
const separator = std.mem.indexOfScalar(u8, payload, '=') orelse {
|
||||
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 {
|
||||
@@ -355,8 +368,9 @@ pub const Client = struct {
|
||||
for (rows) |row| self.allocator.free(row);
|
||||
self.allocator.free(rows);
|
||||
}
|
||||
const truncated = rowsContain(rows, "diagnostic:search_truncated");
|
||||
try self.session.openListPanel("search", rows);
|
||||
self.message = null;
|
||||
self.message = if (truncated) "diagnostic:search_truncated" else null;
|
||||
}
|
||||
|
||||
fn openSelectedSearchResult(self: *Client) !void {
|
||||
@@ -669,6 +683,10 @@ pub const Client = struct {
|
||||
|
||||
fn save(self: *Client) !void {
|
||||
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);
|
||||
if (self.saved_bytes) |old| self.allocator.free(old);
|
||||
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 {
|
||||
for (rows) |row| allocator.free(row);
|
||||
allocator.free(rows);
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
const std = @import("std");
|
||||
|
||||
const diagnostics = @import("diagnostics.zig");
|
||||
const job = @import("job.zig");
|
||||
const lsp = @import("lsp.zig");
|
||||
const profile = @import("profile.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);
|
||||
}
|
||||
|
||||
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 {
|
||||
for (rows) |row| allocator.free(row);
|
||||
allocator.free(rows);
|
||||
|
||||
Reference in New Issue
Block a user