diff --git a/internal/verifier/llm.go b/internal/verifier/llm.go index dee3fb7..f5d2dc8 100644 --- a/internal/verifier/llm.go +++ b/internal/verifier/llm.go @@ -128,51 +128,6 @@ type ActionCandidate struct { // prob is the internal accumulated selection probability, summed across // dedup, then rounded into Weight. Not exposed in the prompt directly. prob float64 - - // The following are retained by the legacy AllCandidates enumeration. - Verb string - X, Y int - Width, Height int - Selector string -} - -// llmVerbs lists the verbs AllCandidates enumerates, in the order they are -// emitted per element. Mirrors verbAccepts; no new filtering logic. -var llmVerbs = []string{"taps", "doubleTaps", "longPresses", "typing", "scrolls", "swipes"} - -// AllCandidates flattens the per-verb candidate enumeration into one indexed -// list the LLM backend chooses from. It reuses scopedElements/verbAccepts/ -// selectorForElement exactly as the seeded picker does, walking the tree once -// and emitting an entry for every (in-scope element, applicable verb) pair. -func (v *Verifier) AllCandidates() []ActionCandidate { - if v.lastTree == nil { - return nil - } - scope := v.scopedElements() - var result []ActionCandidate - for _, element := range v.lastTree.Elements { - if !scope[element] { - continue - } - for _, verb := range llmVerbs { - if !verbAccepts(verb, element) { - continue - } - x, y := element.Bounds.Center() - result = append(result, ActionCandidate{ - Index: len(result), - Verb: verb, - Kind: verbActionKind(verb), - Label: candidateLabel(element), - X: x, - Y: y, - Width: element.Bounds.Width(), - Height: element.Bounds.Height(), - Selector: selectorForElement(v.lastTree, element), - }) - } - } - return result } // verbActionKind maps a picker verb to the action kind it dispatches. @@ -195,21 +150,6 @@ func verbActionKind(verb string) ActionKind { } } -// candidateLabel builds a short target description, preferring the most -// human-meaningful field available. -func candidateLabel(element *hierarchy.Element) string { - switch { - case element.Text != "": - return element.Text - case element.Description != "": - return element.Description - case element.ResourceID != "": - return element.ResourceID - default: - return element.Class - } -} - // maxLabelRunes caps a visible-text label so joined descendant text stays short // enough to render on one numbered line. const maxLabelRunes = 40 diff --git a/internal/verifier/llm_test.go b/internal/verifier/llm_test.go index 0a11aa4..0fcdbbb 100644 --- a/internal/verifier/llm_test.go +++ b/internal/verifier/llm_test.go @@ -1,7 +1,6 @@ package verifier import ( - "slices" "strings" "testing" @@ -248,86 +247,6 @@ func count(candidates []ActionCandidate, description string) int { return n } -// candidateTreeJSON is a small screen with one clickable button, one editable -// field, and one scrollable list. Every node has positive bounds, so each is -// additionally a swipe origin. -const candidateTreeJSON = `{ - "attributes": {"bounds": "[0,0,1080,2400]"}, - "children": [ - {"attributes": {"resource-id": "LoginSubmit", "text": "Sign in", "bounds": "[0,100,1080,200]"}, "clickable": true, "enabled": true, "children": []}, - {"attributes": {"resource-id": "EmailField", "class": "EditText", "bounds": "[0,300,1080,400]"}, "enabled": true, "children": []}, - {"attributes": {"resource-id": "List", "scrollable": "true", "bounds": "[0,500,1080,2000]"}, "children": []} - ] -}` - -func TestAllCandidatesUnionsVerbsWithIndicesAndLabels(t *testing.T) { - tree, err := hierarchy.Parse(candidateTreeJSON) - if err != nil { - t.Fatal(err) - } - v := &Verifier{lastTree: tree} - candidates := v.AllCandidates() - - // Indices are dense and ordered. - for i, candidate := range candidates { - if candidate.Index != i { - t.Errorf("candidate %d has Index %d", i, candidate.Index) - } - } - - // Collect verbs per label to assert the union without pinning swipe count. - byLabel := map[string][]string{} - for _, candidate := range candidates { - byLabel[candidate.Label] = append(byLabel[candidate.Label], candidate.Verb) - } - - // The clickable button is tap/doubleTap/longPress + swipe; its label is the - // visible text, not the resource-id. - submit := byLabel["Sign in"] - if !contains(submit, "taps") || !contains(submit, "doubleTaps") || !contains(submit, "longPresses") { - t.Errorf("Sign in verbs = %v, want tap family", submit) - } - if !contains(submit, "swipes") { - t.Errorf("Sign in verbs = %v, want swipes (positive bounds)", submit) - } - if contains(submit, "typing") { - t.Errorf("Sign in should not be typeable, got %v", submit) - } - - // The EditText is typeable (and a swipe origin); its label falls back to the - // resource-id since it has no text. - email := byLabel["EmailField"] - if !contains(email, "typing") { - t.Errorf("EmailField verbs = %v, want typing", email) - } - if contains(email, "taps") { - t.Errorf("EmailField is not clickable, got %v", email) - } - - // The scrollable list yields a scroll candidate. - list := byLabel["List"] - if !contains(list, "scrolls") { - t.Errorf("List verbs = %v, want scrolls", list) - } - - // Kinds map verbs to action kinds. - for _, candidate := range candidates { - if candidate.Verb == "typing" && candidate.Kind != ActionKindInputText { - t.Errorf("typing candidate kind = %q, want InputText", candidate.Kind) - } - if candidate.Verb == "taps" && candidate.Kind != ActionKindTap { - t.Errorf("taps candidate kind = %q, want Tap", candidate.Kind) - } - } -} - -func TestAllCandidatesNilTree(t *testing.T) { - v := &Verifier{} - if got := v.AllCandidates(); got != nil { - t.Errorf("AllCandidates with no tree = %v, want nil", got) - } -} - func TestLLMConfigDetectsMarker(t *testing.T) { v := newLoadedVerifier(t, `globalThis.generator = { kind: "llm", config: { model: "vendor/model" } };`) config, ok := v.LLMConfig() @@ -390,6 +309,4 @@ func newLoadedVerifier(t *testing.T, source string) *Verifier { return v } -func contains(items []string, want string) bool { - return slices.Contains(items, want) -} +