From 78e561ba7e455aa759709b53ffb8dd2ba9fff236 Mon Sep 17 00:00:00 2001 From: PJ Date: Thu, 13 Aug 2026 22:55:20 +0530 Subject: [PATCH] fix(verifier): name a web handle by the same ladder as a tree element The handle fallback read only text, which is textContent and therefore always empty for an input, so the model could not tell the amount field from the note field. It now mirrors visibleLabel's ladder rather than introducing a second naming scheme. Claude-Session: https://claude.ai/code/session_01A5KmftdEJ49A9z5mF5ESrX --- internal/verifier/llm.go | 48 ++++++++++++++---- internal/verifier/llm_test.go | 94 +++++++++++++++++++++++++++++++++++ 2 files changed, 133 insertions(+), 9 deletions(-) diff --git a/internal/verifier/llm.go b/internal/verifier/llm.go index 54851e1..6a2ae25 100644 --- a/internal/verifier/llm.go +++ b/internal/verifier/llm.go @@ -504,7 +504,7 @@ func (v *Verifier) resolveTarget(value goja.Value, labels labelContext) (resolve target.inputType = inputTypeHint(element) } if target.label == "" { - target.label = truncateLabel(stringField(object, labels.handleField())) + target.label = truncateLabel(v.handleLabel(object, labels)) } return target, true } @@ -721,15 +721,35 @@ func (l labelContext) label(element *hierarchy.Element) string { return visibleLabel(element, l.nodeIndex) } -// handleField is the ax-element handle field a label falls back to when the -// target's selector no longer resolves against the current tree. Reading the -// handle's text there would leak visible text into an identifier-labelled run, -// which is the one thing that arm must not see. -func (l labelContext) handleField() string { - if l.source == LabelSourceResourceID { - return "id" +// handleLabel names a target from the ax handle alone, for the web tick path +// where the handle was built in V8 and carries no selector to resolve against +// the tree. It walks visibleLabel's rungs over the fields a handle has: an +// editable field's hint names its purpose, its own text is the transient typed +// value. The identifier arm reads the handle's id and nothing a user could +// read, which is the one thing that arm must not see. +func (v *Verifier) handleLabel(object *goja.Object, labels labelContext) string { + if labels.source == LabelSourceResourceID { + return stringField(object, "id") } - return "text" + hint := v.handleAttribute(object, "hintText") + if hint != "" && boolField(object, "editable") { + return hint + } + if text := stringField(object, "text"); text != "" { + return text + } + if desc := stringField(object, "desc"); desc != "" { + return desc + } + return hint +} + +func (v *Verifier) handleAttribute(object *goja.Object, name string) string { + attrs := object.Get("attrs") + if attrs == nil || goja.IsUndefined(attrs) || goja.IsNull(attrs) { + return "" + } + return stringField(attrs.ToObject(v.runtime), name) } // resourceIdentifierLabel names a control by the identifier the app assigned it, @@ -841,6 +861,16 @@ func stringField(object *goja.Object, key string) string { return value.String() } +// boolField reads a boolean property off a goja object, returning false when +// absent, null, or undefined. +func boolField(object *goja.Object, key string) bool { + value := object.Get(key) + if value == nil || goja.IsUndefined(value) || goja.IsNull(value) { + return false + } + return value.ToBoolean() +} + // intField reads a numeric property off a goja object, returning 0 when absent, // null, or undefined. func intField(object *goja.Object, key string) int { diff --git a/internal/verifier/llm_test.go b/internal/verifier/llm_test.go index 30005eb..fb7180c 100644 --- a/internal/verifier/llm_test.go +++ b/internal/verifier/llm_test.go @@ -1,6 +1,7 @@ package verifier import ( + "encoding/json" "strings" "testing" @@ -739,3 +740,96 @@ func TestCandidatesAcceptASingleItemAuthoredSampler(t *testing.T) { t.Errorf("sampled tap missing: %v", descriptions(candidates)) } } + +// webFieldTreeJSON is the add-transaction screen as the chrome driver dumps it: +// two inputs a user tells apart by the