mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-06 17:57:43 +02:00
[eric] browser: interactives render in DOCUMENT order (rank still picks who survives the cap, but display is top-to-bottom page order); the scrambled list forced the model to burn orientation turns reading page text to recover position on ordinal tasks ('the 4th story' landed at [48]), the real cause of the slow HN planning
This commit is contained in:
@@ -37,14 +37,26 @@ test('non-consecutive same name is preserved (real list items)', () => {
|
||||
assert.equal(carts.length, 3);
|
||||
});
|
||||
|
||||
test('ranks by role priority: input > button/link > toggle > option', () => {
|
||||
test('display order is DOCUMENT order, not rank order (so ordinals can be counted)', () => {
|
||||
const { shown } = rankAndCapInteractives([
|
||||
mk('option', 'opt', 1),
|
||||
mk('checkbox', 'agree', 2),
|
||||
mk('button', 'Go', 3),
|
||||
mk('textbox', 'email', 4),
|
||||
]);
|
||||
assert.deepEqual(shown.map((x) => x.role), ['textbox', 'button', 'checkbox', 'option']);
|
||||
// all survive the cap; they render top-to-bottom as they appear on the page,
|
||||
// NOT re-sorted by role (which would scramble "the 4th thing")
|
||||
assert.deepEqual(shown.map((x) => x.backendNodeId), [1, 2, 3, 4]);
|
||||
});
|
||||
|
||||
test('rank still decides cap SURVIVAL: a high-priority input buried in options is kept', () => {
|
||||
const items = [
|
||||
...Array.from({ length: 40 }, (_, i) => mk('option', `opt${i}`, i)),
|
||||
mk('textbox', 'email', 900),
|
||||
...Array.from({ length: 40 }, (_, i) => mk('option', `optb${i}`, 100 + i)),
|
||||
];
|
||||
const { shown } = rankAndCapInteractives(items, { cap: 30 });
|
||||
assert.ok(shown.some((x) => x.backendNodeId === 900), 'the input survives the cap by rank');
|
||||
});
|
||||
|
||||
test('preserves document order within the same priority tier', () => {
|
||||
@@ -71,31 +83,23 @@ test('cap of 0 means no cap', () => {
|
||||
assert.equal(truncated, 0);
|
||||
});
|
||||
|
||||
test('goal-matched elements float to the top, above role priority', () => {
|
||||
const { shown } = rankAndCapInteractives([
|
||||
mk('textbox', 'Search', 1),
|
||||
mk('link', 'Account settings', 2),
|
||||
mk('button', 'Save', 3),
|
||||
], { goal: 'open the settings page' });
|
||||
// "Account settings" matches "settings" and jumps ahead of the textbox
|
||||
assert.equal(shown[0].backendNodeId, 2);
|
||||
});
|
||||
|
||||
test('goal match survives the cap even when buried deep', () => {
|
||||
test('goal-matched element survives the cap and displays IN PLACE (document order)', () => {
|
||||
const items = Array.from({ length: 100 }, (_, i) => mk('link', `Item ${i}`, i));
|
||||
items.push(mk('button', 'Checkout now', 999));
|
||||
items.push(mk('button', 'Checkout now', 999)); // last on the page
|
||||
const { shown } = rankAndCapInteractives(items, { cap: 30, goal: 'click checkout' });
|
||||
// it survives the cap (rank kept it)...
|
||||
assert.ok(shown.some((x) => x.backendNodeId === 999), 'checkout should be retained');
|
||||
assert.equal(shown[0].backendNodeId, 999);
|
||||
// ...and renders at its real position (last), not floated to [0]
|
||||
assert.equal(shown[shown.length - 1].backendNodeId, 999);
|
||||
});
|
||||
|
||||
test('no goal leaves pure role-priority ordering', () => {
|
||||
test('display order stays document order with no goal', () => {
|
||||
const { shown } = rankAndCapInteractives([
|
||||
mk('option', 'opt', 1),
|
||||
mk('button', 'Go', 2),
|
||||
mk('textbox', 'email', 3),
|
||||
]);
|
||||
assert.deepEqual(shown.map((x) => x.role), ['textbox', 'button', 'option']);
|
||||
assert.deepEqual(shown.map((x) => x.backendNodeId), [1, 2, 3]);
|
||||
});
|
||||
|
||||
test('goalKeywords strips stopwords, action verbs, and short tokens', () => {
|
||||
@@ -110,11 +114,12 @@ test('empty input yields empty result', () => {
|
||||
assert.equal(truncated, 0);
|
||||
});
|
||||
|
||||
test('unknown role falls into the middle tier, not dropped', () => {
|
||||
test('unknown role is not dropped (survives the cap), displayed in document order', () => {
|
||||
const { shown } = rankAndCapInteractives([
|
||||
mk('option', 'opt', 1),
|
||||
mk('weirdrole', 'mystery', 2),
|
||||
mk('textbox', 'field', 3),
|
||||
]);
|
||||
assert.deepEqual(shown.map((x) => x.role), ['textbox', 'weirdrole', 'option']);
|
||||
assert.deepEqual(shown.map((x) => x.backendNodeId), [1, 2, 3]);
|
||||
assert.ok(shown.some((x) => x.role === 'weirdrole'));
|
||||
});
|
||||
|
||||
@@ -77,17 +77,22 @@ export function rankAndCapInteractives(
|
||||
const cap = opts.cap ?? DEFAULT_INTERACTIVE_CAP;
|
||||
const keywords = opts.goal ? goalKeywords(opts.goal) : [];
|
||||
const deduped = dedupeConsecutive(items);
|
||||
// Sort: goal-matched first, then role priority, tiebroken on original document order so the result is deterministic regardless of engine sort.
|
||||
const ranked = deduped
|
||||
.map((it, i) => ({ it, i, m: matchesGoal(it.name, keywords) ? 0 : 1 }))
|
||||
.sort((a, b) => {
|
||||
if (a.m !== b.m) return a.m - b.m;
|
||||
const pa = rolePriority(a.it.role);
|
||||
const pb = rolePriority(b.it.role);
|
||||
if (pa !== pb) return pa - pb;
|
||||
return a.i - b.i;
|
||||
})
|
||||
.map((x) => x.it);
|
||||
const shown = cap > 0 ? ranked.slice(0, cap) : ranked;
|
||||
const scored = deduped.map((it, i) => ({ it, i, m: matchesGoal(it.name, keywords) ? 0 : 1 }));
|
||||
// Rank picks WHICH items survive the cap (goal-matched first, then role priority),
|
||||
// so the thing the model wants is never truncated away.
|
||||
const ranked = [...scored].sort((a, b) => {
|
||||
if (a.m !== b.m) return a.m - b.m;
|
||||
const pa = rolePriority(a.it.role);
|
||||
const pb = rolePriority(b.it.role);
|
||||
if (pa !== pb) return pa - pb;
|
||||
return a.i - b.i;
|
||||
});
|
||||
const selected = cap > 0 ? ranked.slice(0, cap) : ranked;
|
||||
// But the DISPLAY order is document order (by original index), so the numbered
|
||||
// list reads top-to-bottom the way the page looks. A rank-sorted list scrambled
|
||||
// ordinals ("the 4th story" landed at [48], out of order), forcing the model to
|
||||
// burn a turn reading page text just to recover position; document order lets it
|
||||
// count directly. The high-signal-subset win (from the cap) is untouched.
|
||||
const shown = selected.sort((a, b) => a.i - b.i).map((x) => x.it);
|
||||
return { shown, truncated: Math.max(0, ranked.length - shown.length) };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user