Spaces:
Running
Running
siddhm11 Claude Opus 5.5 commited on
Commit Β·
0cb3a12
1
Parent(s): 0ef8102
test(e2e): briefing and Ask in the browser; fix an outline race
Browse filesThe seeded world gains a cached briefing and two cached answers for
SatNav. New browser tests: the briefing's links and outline entries, a
suggested question echoed then answered with a cite, Enter sends while
Shift+Enter doesn't, a missing model says busy, the phone sheet opens and
closes on Escape with focus back on its button, and the lightbox asks about
its figure. Fixed on the way: the section observer could override the last
outline entry at the bottom of the page.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- app/static/paper.js +11 -4
- scripts/seed_e2e.py +42 -2
- tests/test_e2e_paper.py +69 -1
app/static/paper.js
CHANGED
|
@@ -247,8 +247,18 @@
|
|
| 247 |
});
|
| 248 |
}
|
| 249 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 250 |
if (links.length && 'IntersectionObserver' in window) {
|
| 251 |
var observer = new IntersectionObserver(function (entries) {
|
|
|
|
|
|
|
| 252 |
entries.forEach(function (en) { if (en.isIntersecting) markActive(en.target.id); });
|
| 253 |
}, { rootMargin: '-20% 0px -70% 0px' });
|
| 254 |
/* A short last section never reaches that band, so at the bottom of the
|
|
@@ -259,10 +269,7 @@
|
|
| 259 |
ticking = true;
|
| 260 |
requestAnimationFrame(function () {
|
| 261 |
ticking = false;
|
| 262 |
-
|
| 263 |
-
if (window.innerHeight + window.scrollY >= doc.scrollHeight - 4) {
|
| 264 |
-
markActive(links[links.length - 1].getAttribute('data-sec'));
|
| 265 |
-
}
|
| 266 |
});
|
| 267 |
}, { passive: true });
|
| 268 |
function watch() {
|
|
|
|
| 247 |
});
|
| 248 |
}
|
| 249 |
|
| 250 |
+
function atBottom() {
|
| 251 |
+
return window.innerHeight + window.scrollY >= document.documentElement.scrollHeight - 4;
|
| 252 |
+
}
|
| 253 |
+
function lastVisibleLink() {
|
| 254 |
+
var shown = Array.prototype.filter.call(links, function (a) { return !a.hidden; });
|
| 255 |
+
return shown[shown.length - 1];
|
| 256 |
+
}
|
| 257 |
+
|
| 258 |
if (links.length && 'IntersectionObserver' in window) {
|
| 259 |
var observer = new IntersectionObserver(function (entries) {
|
| 260 |
+
/* At the bottom the last section wins, whatever crosses the band. */
|
| 261 |
+
if (atBottom()) { markActive(lastVisibleLink().getAttribute('data-sec')); return; }
|
| 262 |
entries.forEach(function (en) { if (en.isIntersecting) markActive(en.target.id); });
|
| 263 |
}, { rootMargin: '-20% 0px -70% 0px' });
|
| 264 |
/* A short last section never reaches that band, so at the bottom of the
|
|
|
|
| 269 |
ticking = true;
|
| 270 |
requestAnimationFrame(function () {
|
| 271 |
ticking = false;
|
| 272 |
+
if (atBottom()) markActive(lastVisibleLink().getAttribute('data-sec'));
|
|
|
|
|
|
|
|
|
|
| 273 |
});
|
| 274 |
}, { passive: true });
|
| 275 |
function watch() {
|
scripts/seed_e2e.py
CHANGED
|
@@ -25,7 +25,7 @@ values in .env.local can't leak in (load_dotenv never overrides a set name):
|
|
| 25 |
DB_PATH=/tmp/e2e.db METADATA_SIDECAR_PATH=/tmp/e2e_sidecar.sqlite \\
|
| 26 |
RESEARCHIT_ENV=test ARXIV_HTML_BASE=http://127.0.0.1:8799 \\
|
| 27 |
TURSO_URL= TURSO_DB_TOKEN= QDRANT_URL= GROQ_API_KEY= TURSO_SYNC_DISABLED=1 \\
|
| 28 |
-
PAPER_PAGE_V2=on FIGURES=on PAPER_SOURCES=on PORT=7861 python run.py
|
| 29 |
"""
|
| 30 |
from __future__ import annotations
|
| 31 |
|
|
@@ -122,8 +122,31 @@ def build_site(site: Path, parsed: dict) -> int:
|
|
| 122 |
return n
|
| 123 |
|
| 124 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 125 |
async def seed_db(db_path: Path, parsed: dict) -> None:
|
| 126 |
-
|
|
|
|
| 127 |
config.DB_PATH = db.DB_PATH = str(db_path)
|
| 128 |
await db.init_db()
|
| 129 |
for aid in PAPERS:
|
|
@@ -133,6 +156,23 @@ async def seed_db(db_path: Path, parsed: dict) -> None:
|
|
| 133 |
abstracts[MATHS_ID] = MATHS_ABSTRACT
|
| 134 |
for aid, text in SUMMARIES.items():
|
| 135 |
await db.save_explanation(groq_svc.explain_cache_key(aid, abstracts[aid]), aid, text, "seed")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 136 |
|
| 137 |
|
| 138 |
def main(argv=None) -> int:
|
|
|
|
| 25 |
DB_PATH=/tmp/e2e.db METADATA_SIDECAR_PATH=/tmp/e2e_sidecar.sqlite \\
|
| 26 |
RESEARCHIT_ENV=test ARXIV_HTML_BASE=http://127.0.0.1:8799 \\
|
| 27 |
TURSO_URL= TURSO_DB_TOKEN= QDRANT_URL= GROQ_API_KEY= TURSO_SYNC_DISABLED=1 \\
|
| 28 |
+
PAPER_PAGE_V2=on FIGURES=on PAPER_SOURCES=on BRIEFING=on ASK=on PORT=7861 python run.py
|
| 29 |
"""
|
| 30 |
from __future__ import annotations
|
| 31 |
|
|
|
|
| 122 |
return n
|
| 123 |
|
| 124 |
|
| 125 |
+
SATNAV_BRIEFING = {
|
| 126 |
+
"things": [
|
| 127 |
+
{"lead": "118K episodes from satellite imagery.", "text": "Built across 59 scenes in 18 cities.",
|
| 128 |
+
"anchor": "abstract", "label": "Abstract"},
|
| 129 |
+
{"lead": "City-scale navigation is still hard.", "text": "Agents struggle on long routes.",
|
| 130 |
+
"anchor": "S1.p2", "label": "Β§1"},
|
| 131 |
+
{"lead": "Memory matters.", "text": "Removing long-term memory hurts.", "anchor": "S4.SS1.p1", "label": "Β§4.1"},
|
| 132 |
+
],
|
| 133 |
+
"how": [{"name": "Pair the maps", "text": "Satellite images aligned with map data."},
|
| 134 |
+
{"name": "Write the tasks", "text": "Routes become instructions."},
|
| 135 |
+
{"name": "Navigate", "text": "The agent picks one of four actions."}],
|
| 136 |
+
"use": "Teams training drones that follow spoken directions.",
|
| 137 |
+
"dropped": [],
|
| 138 |
+
}
|
| 139 |
+
SATNAV_ANSWERS = {
|
| 140 |
+
"limits": {"kind": "answer", "answer": "The authors list three limitations, starting with discrete moves at a fixed altitude.",
|
| 141 |
+
"quotes": [], "cites": [{"anchor": "S6.p1", "label": "Β§6"}], "dropped": {"quotes": 0, "cites": 0}},
|
| 142 |
+
"new": {"kind": "answer", "answer": "A navigation benchmark built from satellite images instead of 3D models.",
|
| 143 |
+
"quotes": [], "cites": [{"anchor": "S1.p4", "label": "Β§1"}], "dropped": {"quotes": 0, "cites": 0}},
|
| 144 |
+
}
|
| 145 |
+
|
| 146 |
+
|
| 147 |
async def seed_db(db_path: Path, parsed: dict) -> None:
|
| 148 |
+
import json as _json
|
| 149 |
+
from app import ask_svc, briefing_svc, config, db, groq_svc, paper_source_svc
|
| 150 |
config.DB_PATH = db.DB_PATH = str(db_path)
|
| 151 |
await db.init_db()
|
| 152 |
for aid in PAPERS:
|
|
|
|
| 156 |
abstracts[MATHS_ID] = MATHS_ABSTRACT
|
| 157 |
for aid, text in SUMMARIES.items():
|
| 158 |
await db.save_explanation(groq_svc.explain_cache_key(aid, abstracts[aid]), aid, text, "seed")
|
| 159 |
+
# A cached briefing and two cached suggested answers for SatNav, under the
|
| 160 |
+
# keys the server will compute with the default model chain.
|
| 161 |
+
version = parsed["2609.31507"].version
|
| 162 |
+
import aiosqlite
|
| 163 |
+
async with aiosqlite.connect(config.DB_PATH) as conn:
|
| 164 |
+
await conn.execute(
|
| 165 |
+
"INSERT INTO paper_briefings (cache_key, arxiv_id, status, briefing_json, provider, model, prompt_version) "
|
| 166 |
+
"VALUES (?, ?, 'ok', ?, 'seed', 'seed', ?)",
|
| 167 |
+
(briefing_svc.cache_key("2609.31507", version), "2609.31507", _json.dumps(SATNAV_BRIEFING),
|
| 168 |
+
briefing_svc.PROMPT_VERSION))
|
| 169 |
+
for key, answer in SATNAV_ANSWERS.items():
|
| 170 |
+
await conn.execute(
|
| 171 |
+
"INSERT INTO paper_answers (cache_key, arxiv_id, question_key, answer_json, model, prompt_version) "
|
| 172 |
+
"VALUES (?, ?, ?, ?, 'seed', ?)",
|
| 173 |
+
(ask_svc.cache_key("2609.31507", version, key), "2609.31507", key, _json.dumps(answer),
|
| 174 |
+
ask_svc.PROMPT_VERSION))
|
| 175 |
+
await conn.commit()
|
| 176 |
|
| 177 |
|
| 178 |
def main(argv=None) -> int:
|
tests/test_e2e_paper.py
CHANGED
|
@@ -124,7 +124,7 @@ def test_the_outline_marks_the_section_in_view(satnav):
|
|
| 124 |
# At the very bottom the last section is the one in view, however short.
|
| 125 |
satnav.evaluate("window.scrollTo(0, document.documentElement.scrollHeight)")
|
| 126 |
satnav.wait_for_timeout(400)
|
| 127 |
-
last = satnav.evaluate("[...document.querySelectorAll('.outline nav a')].at(-1).dataset.sec")
|
| 128 |
assert active() == [last]
|
| 129 |
|
| 130 |
|
|
@@ -187,3 +187,71 @@ def test_a_paper_without_arxiv_html_shows_no_figure_sections(browser):
|
|
| 187 |
ctx, page = open_page(browser, f"/p/{MATHS}")
|
| 188 |
assert page.locator("#sec-fig").count() == 0 and page.locator("#sec-figs").count() == 0
|
| 189 |
ctx.close()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 124 |
# At the very bottom the last section is the one in view, however short.
|
| 125 |
satnav.evaluate("window.scrollTo(0, document.documentElement.scrollHeight)")
|
| 126 |
satnav.wait_for_timeout(400)
|
| 127 |
+
last = satnav.evaluate("[...document.querySelectorAll('.outline nav a')].filter(a => !a.hidden).at(-1).dataset.sec")
|
| 128 |
assert active() == [last]
|
| 129 |
|
| 130 |
|
|
|
|
| 187 |
ctx, page = open_page(browser, f"/p/{MATHS}")
|
| 188 |
assert page.locator("#sec-fig").count() == 0 and page.locator("#sec-figs").count() == 0
|
| 189 |
ctx.close()
|
| 190 |
+
|
| 191 |
+
|
| 192 |
+
# ββ briefing and Ask (server started with BRIEFING=on ASK=on) ββββββββββββββββ
|
| 193 |
+
|
| 194 |
+
def _needs(page, selector, switch):
|
| 195 |
+
if page.locator(selector).count() == 0:
|
| 196 |
+
pytest.skip(f"server not started with {switch}=on")
|
| 197 |
+
|
| 198 |
+
|
| 199 |
+
def test_the_briefing_arrives_with_links_to_each_passage(satnav):
|
| 200 |
+
_needs(satnav, ".brief-slot, #sec-things", "BRIEFING")
|
| 201 |
+
satnav.wait_for_selector("#sec-things li")
|
| 202 |
+
assert satnav.locator("#sec-things li").count() == 3
|
| 203 |
+
href = satnav.get_attribute("#sec-things a.src", "href")
|
| 204 |
+
assert href == f"https://arxiv.org/html/{SATNAV}v1#abstract"
|
| 205 |
+
assert satnav.locator("#sec-how li").count() == 3 and satnav.is_visible("#sec-use")
|
| 206 |
+
# The outline's briefing links appear once the sections exist.
|
| 207 |
+
satnav.wait_for_function("!document.querySelector('.outline a[data-sec=\"sec-things\"]').hidden")
|
| 208 |
+
|
| 209 |
+
|
| 210 |
+
def test_a_suggested_question_is_echoed_then_answered_with_a_cite(satnav):
|
| 211 |
+
_needs(satnav, "#chat", "ASK")
|
| 212 |
+
satnav.click('[data-ask-key="limits"]')
|
| 213 |
+
satnav.wait_for_selector("#chat-log .msg-user")
|
| 214 |
+
assert satnav.inner_text("#chat-log .msg-user") == "What are the limitations?"
|
| 215 |
+
satnav.wait_for_selector("#chat-log .cite")
|
| 216 |
+
assert "three limitations" in satnav.inner_text("#chat-log")
|
| 217 |
+
assert satnav.get_attribute("#chat-log .cite", "href") == f"https://arxiv.org/html/{SATNAV}v1#S6.p1"
|
| 218 |
+
assert satnav.locator('[data-ask-key="limits"]').count() == 0 # the chip is used up
|
| 219 |
+
|
| 220 |
+
|
| 221 |
+
def test_a_typed_question_sends_on_enter_and_a_missing_model_says_busy(satnav):
|
| 222 |
+
_needs(satnav, "#chat", "ASK")
|
| 223 |
+
satnav.fill("#chat-input", "How many cities are there?")
|
| 224 |
+
satnav.press("#chat-input", "Enter")
|
| 225 |
+
satnav.wait_for_selector("#chat-log .msg-bot >> nth=1")
|
| 226 |
+
assert "busy right now" in satnav.inner_text("#chat-log")
|
| 227 |
+
assert satnav.input_value("#chat-input") == ""
|
| 228 |
+
|
| 229 |
+
|
| 230 |
+
def test_shift_enter_starts_a_new_line_instead_of_sending(satnav):
|
| 231 |
+
_needs(satnav, "#chat", "ASK")
|
| 232 |
+
satnav.fill("#chat-input", "First line")
|
| 233 |
+
satnav.press("#chat-input", "Shift+Enter")
|
| 234 |
+
satnav.wait_for_timeout(300)
|
| 235 |
+
assert satnav.locator("#chat-log .msg-user").count() == 0
|
| 236 |
+
|
| 237 |
+
|
| 238 |
+
def test_on_a_phone_ask_opens_as_a_sheet_and_closes_on_escape(browser):
|
| 239 |
+
ctx, page = open_page(browser, f"/p/{SATNAV}", width=390, height=844)
|
| 240 |
+
_needs(page, "#chat", "ASK")
|
| 241 |
+
assert not page.is_visible("#chat") and page.is_visible(".ask-fab")
|
| 242 |
+
page.click(".ask-fab")
|
| 243 |
+
assert page.is_visible("#chat") and page.evaluate("document.activeElement.id") == "chat-input"
|
| 244 |
+
page.keyboard.press("Escape")
|
| 245 |
+
assert not page.is_visible("#chat")
|
| 246 |
+
assert page.evaluate("document.activeElement.classList.contains('ask-fab')")
|
| 247 |
+
assert page.errors == []
|
| 248 |
+
ctx.close()
|
| 249 |
+
|
| 250 |
+
|
| 251 |
+
def test_ask_about_this_figure_asks_in_the_panel(satnav):
|
| 252 |
+
_needs(satnav, "#chat", "ASK")
|
| 253 |
+
satnav.click("#sec-fig [data-fig]")
|
| 254 |
+
satnav.wait_for_function("document.getElementById('lb').open")
|
| 255 |
+
satnav.click("#lb-ask")
|
| 256 |
+
satnav.wait_for_selector("#chat-log .msg-user")
|
| 257 |
+
assert satnav.inner_text("#chat-log .msg-user") == "Explain figure 1"
|