diff --git a/src/agent/Agent.zig b/src/agent/Agent.zig index a9f1d837..d92c8a6a 100644 --- a/src/agent/Agent.zig +++ b/src/agent/Agent.zig @@ -588,9 +588,7 @@ fn runHealTurn(self: *Self, prompt: []const u8, arena: std.mem.Allocator) ![]Com } if (result.text) |text| { - std.debug.print("\n", .{}); self.terminal.printAssistant(text); - std.debug.print("\n", .{}); } return cmds.toOwnedSlice(arena) catch &.{}; @@ -686,9 +684,7 @@ fn processUserMessage(self: *Self, user_input: []const u8, record_comment: []con } if (result.text) |text| { - std.debug.print("\n", .{}); self.terminal.printAssistant(text); - std.debug.print("\n", .{}); } else { self.terminal.printInfo("(no response from model)"); } diff --git a/src/agent/CommandExecutor.zig b/src/agent/CommandExecutor.zig index 1308baa7..8e51dea3 100644 --- a/src/agent/CommandExecutor.zig +++ b/src/agent/CommandExecutor.zig @@ -106,36 +106,3 @@ fn buildJson(arena: std.mem.Allocator, value: anytype) []const u8 { std.json.Stringify.value(value, .{}, &aw.writer) catch return "{}"; return aw.written(); } - -test "substituteEnvVars no vars" { - const result = substituteEnvVars(std.testing.allocator, "hello world"); - try std.testing.expectEqualStrings("hello world", result); -} - -test "substituteEnvVars with HOME" { - // Use arena since substituteEnvVars makes intermediate allocations (dupeZ) - var arena = std.heap.ArenaAllocator.init(std.testing.allocator); - defer arena.deinit(); - const a = arena.allocator(); - - const result = substituteEnvVars(a, "dir=$HOME/test"); - // Result should not contain $HOME literally (it got substituted) - try std.testing.expect(std.mem.indexOf(u8, result, "$HOME") == null); - try std.testing.expect(std.mem.indexOf(u8, result, "/test") != null); -} - -test "substituteEnvVars missing var kept literal" { - var arena = std.heap.ArenaAllocator.init(std.testing.allocator); - defer arena.deinit(); - - const result = substituteEnvVars(arena.allocator(), "$UNLIKELY_VAR_12345"); - try std.testing.expectEqualStrings("$UNLIKELY_VAR_12345", result); -} - -test "substituteEnvVars bare dollar" { - var arena = std.heap.ArenaAllocator.init(std.testing.allocator); - defer arena.deinit(); - - const result = substituteEnvVars(arena.allocator(), "price is $ 5"); - try std.testing.expectEqualStrings("price is $ 5", result); -} diff --git a/src/agent/Verifier.zig b/src/agent/Verifier.zig index 3dc9eaa5..96045622 100644 --- a/src/agent/Verifier.zig +++ b/src/agent/Verifier.zig @@ -43,15 +43,10 @@ pub fn verify(self: *Self, arena: std.mem.Allocator, cmd: Command.Command, pre: } fn verifyFill(self: *Self, arena: std.mem.Allocator, selector: []const u8, expected_value: []const u8) VerifyResult { - const script = std.fmt.allocPrint( - arena, - "(function(){{ var el = document.querySelector({s}); return el ? el.value : null; }})()", - .{jsonQuote(arena, selector)}, - ) catch return .{ .result = .inconclusive }; - - const actual = self.tool_executor.callEval(arena, script) orelse return .{ .result = .inconclusive }; - + // Secret env-var references can't be compared literally — just + // verify the field isn't empty after substitution. if (std.mem.indexOf(u8, expected_value, "$LP_") != null) { + const actual = self.queryElementProperty(arena, selector, "value") orelse return .{ .result = .inconclusive }; if (actual.len == 0 or std.mem.eql(u8, actual, "null")) return .{ .result = .failed, @@ -59,46 +54,36 @@ fn verifyFill(self: *Self, arena: std.mem.Allocator, selector: []const u8, expec }; return .{ .result = .passed }; } - - if (!std.mem.eql(u8, actual, expected_value)) - return .{ - .result = .failed, - .reason = std.fmt.allocPrint(arena, "element value is \"{s}\" after fill (expected \"{s}\")", .{ actual, expected_value }) catch null, - }; - return .{ .result = .passed }; + return self.verifyElementValue(arena, selector, "value", expected_value, "value"); } fn verifyCheck(self: *Self, arena: std.mem.Allocator, selector: []const u8, expected: bool) VerifyResult { - const script = std.fmt.allocPrint( - arena, - "(function(){{ var el = document.querySelector({s}); return el ? String(el.checked) : null; }})()", - .{jsonQuote(arena, selector)}, - ) catch return .{ .result = .inconclusive }; - - const actual = self.tool_executor.callEval(arena, script) orelse return .{ .result = .inconclusive }; const expected_str: []const u8 = if (expected) "true" else "false"; - if (!std.mem.eql(u8, actual, expected_str)) + return self.verifyElementValue(arena, selector, "String(el.checked)", expected_str, "checked state"); +} + +fn verifySelect(self: *Self, arena: std.mem.Allocator, selector: []const u8, expected_value: []const u8) VerifyResult { + return self.verifyElementValue(arena, selector, "value", expected_value, "selected value"); +} + +/// Shared verification: query a DOM property and compare against an expected value. +fn verifyElementValue(self: *Self, arena: std.mem.Allocator, selector: []const u8, js_property: []const u8, expected: []const u8, label: []const u8) VerifyResult { + const actual = self.queryElementProperty(arena, selector, js_property) orelse return .{ .result = .inconclusive }; + if (!std.mem.eql(u8, actual, expected)) return .{ .result = .failed, - .reason = std.fmt.allocPrint(arena, "element checked state is {s} (expected {s})", .{ actual, expected_str }) catch null, + .reason = std.fmt.allocPrint(arena, "element {s} is \"{s}\" (expected \"{s}\")", .{ label, actual, expected }) catch null, }; return .{ .result = .passed }; } -fn verifySelect(self: *Self, arena: std.mem.Allocator, selector: []const u8, expected_value: []const u8) VerifyResult { +fn queryElementProperty(self: *Self, arena: std.mem.Allocator, selector: []const u8, js_property: []const u8) ?[]const u8 { const script = std.fmt.allocPrint( arena, - "(function(){{ var el = document.querySelector({s}); return el ? el.value : null; }})()", - .{jsonQuote(arena, selector)}, - ) catch return .{ .result = .inconclusive }; - - const actual = self.tool_executor.callEval(arena, script) orelse return .{ .result = .inconclusive }; - if (!std.mem.eql(u8, actual, expected_value)) - return .{ - .result = .failed, - .reason = std.fmt.allocPrint(arena, "element selected value is \"{s}\" (expected \"{s}\")", .{ actual, expected_value }) catch null, - }; - return .{ .result = .passed }; + "(function(){{ var el = document.querySelector({s}); return el ? {s} : null; }})()", + .{ jsonQuote(arena, selector), js_property }, + ) catch return null; + return self.tool_executor.callEval(arena, script); } fn verifyClick(self: *Self, arena: std.mem.Allocator, pre: PreState) VerifyResult { diff --git a/src/browser/tools.zig b/src/browser/tools.zig index 767a6568..813ed094 100644 --- a/src/browser/tools.zig +++ b/src/browser/tools.zig @@ -959,3 +959,33 @@ pub fn substituteEnvVars(arena: std.mem.Allocator, input: []const u8) []const u8 } return result.toOwnedSlice(arena) catch input; } + +test "substituteEnvVars no vars" { + const r = substituteEnvVars(std.testing.allocator, "hello world"); + try std.testing.expectEqualStrings("hello world", r); +} + +test "substituteEnvVars with HOME" { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + + const r = substituteEnvVars(arena.allocator(), "dir=$HOME/test"); + try std.testing.expect(std.mem.indexOf(u8, r, "$HOME") == null); + try std.testing.expect(std.mem.indexOf(u8, r, "/test") != null); +} + +test "substituteEnvVars missing var kept literal" { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + + const r = substituteEnvVars(arena.allocator(), "$UNLIKELY_VAR_12345"); + try std.testing.expectEqualStrings("$UNLIKELY_VAR_12345", r); +} + +test "substituteEnvVars bare dollar" { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + + const r = substituteEnvVars(arena.allocator(), "price is $ 5"); + try std.testing.expectEqualStrings("price is $ 5", r); +}