Fix Chrome session list API pagination in IDE beta 9

This commit is contained in:
yilsem committed 2026-09-07 13:58:03 +03:00
1 parent fa63e03eb7
commit ff2eb47302
7 files changed
+189 -8

No files matched your search

+5
View File
@@ -94,6 +94,11 @@ async function fixture(t) {
const http = createServer((request, response) => {
assert.equal(request.headers['x-hermes-session-token'], 'fixture-backend-secret')
response.setHeader('Content-Type', 'application/json')
if (request.url.startsWith('/api/sessions?')) {
const query = new URL(request.url, 'http://localhost').searchParams
assert.ok(Number(query.get('limit')) <= 100)
assert.equal(query.get('source'), 'desktop')
}
if (request.url.startsWith('/api/sessions?')) response.end(JSON.stringify({ sessions: [
{ id: 'stored-1', title: 'Birinci iş', source: 'desktop', model: 'fixture-model' },
{ id: 'stored-2', title: 'İkinci iş', source: 'desktop' },
+93
View File
@@ -0,0 +1,93 @@
"""Real session router + disposable SQLite stores for the JS bridge contract.
Run by sessions-api.test.mjs. Only service dependencies are replaced; query
validation, route handling, SQL filters and pagination are production code.
"""
import os
from pathlib import Path
import socket
import sys
import tempfile
import threading
import time
sys.path.insert(0, str(Path(__file__).resolve().parents[3]))
with tempfile.TemporaryDirectory(prefix="aiturk-chrome-api-") as directory:
os.environ["HERMES_HOME"] = directory
from fastapi import FastAPI, HTTPException, WebSocket
from fastapi.responses import JSONResponse
import uvicorn
from hermes_state import SessionDB
from hermes_cli.web_routers import sessions
paths = {name: Path(directory) / f"{name}.db" for name in ("default", "small", "empty")}
for profile, count in (("default", 230), ("small", 3), ("empty", 0)):
db = SessionDB(db_path=paths[profile])
for index in range(count):
sid = f"{profile}-desktop-{index:03d}"
db.create_session(sid, source="desktop")
db.set_session_title(sid, sid)
db.append_message(sid, role="user", content="Fixture context")
if count:
for index in range(240):
sid = f"{profile}-cron-{index:03d}"
db.create_session(sid, source="cron")
db.set_session_title(sid, sid)
db.append_message(sid, role="user", content="Not a desktop conversation")
db.create_session(f"{profile}-blank", source="desktop")
db.create_session(f"{profile}-archived", source="desktop")
db.append_message(f"{profile}-archived", role="user", content="Archived")
db._conn.execute("UPDATE sessions SET archived=1 WHERE id=?", (f"{profile}-archived",))
db._conn.commit()
db.close()
def profile_home(profile):
if profile not in paths:
raise HTTPException(status_code=404)
return profile, Path(directory)
sessions._cron_profile_home = profile_home
sessions._cron_default_profile = lambda: "default"
sessions._maybe_auto_archive_for_profile = lambda profile: None
sessions._open_session_db_for_profile = lambda profile, read_only=True: SessionDB(
db_path=paths[profile or "default"], read_only=read_only
)
# The fixture stores no system/model secrets; list projection is unrelated
# to this contract and must not import the full web-server startup path.
sessions._strip_session_list_rows = lambda rows: None
app = FastAPI()
app.include_router(sessions.list_router)
@app.middleware("http")
async def auth(request, call_next):
if request.headers.get("X-Hermes-Session-Token") != "fixture-only":
return JSONResponse({}, status_code=401)
return await call_next(request)
@app.websocket("/api/ws")
async def ws(websocket: WebSocket):
await websocket.accept()
try:
while True:
await websocket.receive_text()
except Exception:
pass
sock = socket.socket()
sock.bind(("127.0.0.1", 0))
port = sock.getsockname()[1]
server = uvicorn.Server(uvicorn.Config(app, log_level="critical"))
thread = threading.Thread(target=server.run, kwargs={"sockets": [sock]}, daemon=True)
thread.start()
deadline = time.monotonic() + 20
while not server.started:
if not thread.is_alive() or time.monotonic() > deadline:
raise RuntimeError("Fixture failed to start")
time.sleep(0.01)
print(f"READY {port}", flush=True)
# EOF shuts down cleanly, including Windows venv launcher/worker pairs.
sys.stdin.read()
server.should_exit = True
thread.join(timeout=15)
sock.close()
+57
View File
@@ -0,0 +1,57 @@
import assert from 'node:assert/strict'
import { spawn } from 'node:child_process'
import { once } from 'node:events'
import { fileURLToPath } from 'node:url'
import { test } from 'node:test'
import { ChromeGatewayClient } from '../../desktop/electron/chrome-bridge.mjs'
test('real FastAPI/SQLite: valid bounded desktop pages, profile scope and empty states', {
skip: !process.env.AITURK_TEST_PYTHON && 'Set AITURK_TEST_PYTHON to run the real API contract',
timeout: 90000,
}, async t => {
// No credentials/config inherited by the disposable Python service.
const env = Object.fromEntries(['PATH', 'SYSTEMROOT', 'TEMP', 'TMP', 'USERPROFILE', 'LOCALAPPDATA', 'APPDATA']
.filter(key => process.env[key]).map(key => [key, process.env[key]]))
env.PYTHONUTF8 = '1'
const child = spawn(process.env.AITURK_TEST_PYTHON,
[fileURLToPath(new URL('./sessions-api-fixture.py', import.meta.url))], { env, windowsHide: true })
const exited = once(child, 'exit')
let stdout = '', stderr = ''
child.stderr.on('data', chunk => { stderr += chunk })
t.after(async () => { child.stdin.end(); await exited })
const port = await new Promise((resolve, reject) => {
const timer = setTimeout(() => reject(new Error(`Fixture startup timeout: ${stderr}`)), 45000)
child.once('error', error => { clearTimeout(timer); reject(error) })
child.once('exit', () => { clearTimeout(timer); reject(new Error(`Fixture exited: ${stderr}`)) })
child.stdout.on('data', chunk => {
stdout += chunk
const match = stdout.match(/READY (\d+)/)
if (match) { clearTimeout(timer); resolve(Number(match[1])) }
})
})
const baseUrl = `http://127.0.0.1:${port}`
const old = await fetch(`${baseUrl}/api/sessions?limit=200`, { headers: { 'X-Hermes-Session-Token': 'fixture-only' } })
assert.equal(old.status, 422, 'production validation reproduces the reported failure')
assert.ok((await old.json()).detail.some(error => error.loc.includes('limit')))
for (const [profile, expected, requests] of [['default', 200, 2], ['small', 3, 1], ['empty', 0, 1]]) {
const client = new ChromeGatewayClient(async () => ({ baseUrl, wsUrl: `${baseUrl.replace('http:', 'ws:')}/api/ws`,
token: 'fixture-only', mode: 'local', profile }), () => {})
const paths = []
const realRest = client.rest.bind(client)
client.rest = path => { paths.push(path); return realRest(path) }
try {
const result = await client.handle({ id: 'contract', action: 'sessions.list' })
assert.equal(result.sessions.length, expected)
assert.equal(new Set(result.sessions.map(row => row.id)).size, expected)
assert.ok(result.sessions.every(row => row.id.startsWith(`${profile}-desktop-`)))
assert.equal(result.profile, profile)
assert.equal(paths.length, requests)
for (const [index, path] of paths.entries()) {
const query = new URL(path, baseUrl).searchParams
assert.equal(query.get('offset'), String(index * 100))
assert.equal(query.get('source'), 'desktop')
assert.equal(query.get('profile'), profile)
}
} finally { client.close() }
}
})