235 lines
8.0 KiB
Python
235 lines
8.0 KiB
Python
"""Frontend regressions for manual title-regeneration timeout sizing."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import shutil
|
|
import subprocess
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
SESSIONS_JS = (ROOT / "static" / "sessions.js").read_text(encoding="utf-8")
|
|
NODE = shutil.which("node")
|
|
|
|
|
|
def _title_regenerate_action_block() -> str:
|
|
marker = "t('session_title_regenerate')"
|
|
start = SESSIONS_JS.find(marker)
|
|
assert start >= 0, "manual title regenerate action not found"
|
|
end = SESSIONS_JS.find("if(!isExternalSession){", start)
|
|
assert end > start, "manual title regenerate action end marker not found"
|
|
return SESSIONS_JS[start:end]
|
|
|
|
|
|
def _title_timeout_support_block() -> str:
|
|
start = SESSIONS_JS.find("function _manualTitleAuxConfigFromPayload")
|
|
assert start >= 0, "manual title auxiliary config support not found"
|
|
end = SESSIONS_JS.find("function _formatSessionModelWithGateway", start)
|
|
assert end > start, "manual title auxiliary config support end marker not found"
|
|
return SESSIONS_JS[start:end]
|
|
|
|
|
|
def _run_script(script: str):
|
|
result = subprocess.run(
|
|
[NODE, "-e", script],
|
|
cwd=ROOT,
|
|
text=True,
|
|
capture_output=True,
|
|
check=False,
|
|
)
|
|
assert result.returncode == 0, result.stderr or result.stdout
|
|
return json.loads(result.stdout)
|
|
|
|
|
|
def _run_helper(setup: str = ""):
|
|
helper = _title_timeout_support_block()
|
|
script = f"""
|
|
(async()=>{{
|
|
{setup}
|
|
{helper}
|
|
const result=await _manualTitleRegenerateTimeoutMs();
|
|
process.stdout.write(JSON.stringify(result));
|
|
}})().catch(err=>{{
|
|
console.error(err&&err.stack?err.stack:err);
|
|
process.exit(1);
|
|
}});
|
|
"""
|
|
return _run_script(script)
|
|
|
|
|
|
@pytest.mark.skipif(NODE is None, reason="node not on PATH")
|
|
def test_manual_title_regenerate_timeout_uses_fetched_positive_aux_timeout():
|
|
assert _run_helper("async function api(){return {title_generation:{timeout:180}};}") == 185000
|
|
assert _run_helper("async function api(){return {title_generation:{timeout:'31'}};}") == 36000
|
|
|
|
|
|
@pytest.mark.skipif(NODE is None, reason="node not on PATH")
|
|
def test_manual_title_regenerate_timeout_falls_back_when_fetch_missing_or_invalid():
|
|
assert _run_helper("async function api(){throw new Error('missing mock should fall back');}") is None
|
|
for value in ("undefined", "null", "''", "'abc'", "0", "-4", "Infinity", "NaN"):
|
|
setup = f"async function api(){{return {{title_generation:{{timeout:{value}}}}};}}"
|
|
assert _run_helper(setup) is None
|
|
|
|
|
|
@pytest.mark.skipif(NODE is None, reason="node not on PATH")
|
|
def test_manual_title_regenerate_timeout_adds_slack_and_enforces_minimum():
|
|
assert _run_helper("async function api(){return {title_generation:{timeout:10}};}") == 30000
|
|
assert _run_helper("async function api(){return {title_generation:{timeout:25.1}};}") == 30100
|
|
|
|
|
|
@pytest.mark.skipif(NODE is None, reason="node not on PATH")
|
|
def test_manual_title_regenerate_timeout_fetches_aux_config_fresh_each_time():
|
|
setup = """
|
|
let calls=0;
|
|
async function api(path,opts){
|
|
calls++;
|
|
if(path!=='/api/model/auxiliary') throw new Error('wrong path '+path);
|
|
if(!opts||opts.retries!==0||opts.timeoutToast!==false) throw new Error('wrong fetch options');
|
|
return {tasks:[{task:'title_generation',timeout:180}]};
|
|
}
|
|
"""
|
|
helper = _title_timeout_support_block()
|
|
script = f"""
|
|
(async()=>{{
|
|
{setup}
|
|
{helper}
|
|
const first=await _manualTitleRegenerateTimeoutMs();
|
|
const second=await _manualTitleRegenerateTimeoutMs();
|
|
process.stdout.write(JSON.stringify({{first,second,calls}}));
|
|
}})().catch(err=>{{
|
|
console.error(err&&err.stack?err.stack:err);
|
|
process.exit(1);
|
|
}});
|
|
"""
|
|
assert _run_script(script) == {"first": 185000, "second": 185000, "calls": 2}
|
|
|
|
|
|
@pytest.mark.skipif(NODE is None, reason="node not on PATH")
|
|
def test_manual_title_regenerate_timeout_empty_tasks_normalizes_to_null():
|
|
setup = """
|
|
let calls=0;
|
|
async function api(){
|
|
calls++;
|
|
return {tasks:[]};
|
|
}
|
|
"""
|
|
helper = _title_timeout_support_block()
|
|
script = f"""
|
|
(async()=>{{
|
|
{setup}
|
|
{helper}
|
|
const normalized=_manualTitleAuxConfigFromPayload({{tasks:[]}});
|
|
const first=await _loadManualTitleAuxConfig();
|
|
const second=await _loadManualTitleAuxConfig();
|
|
process.stdout.write(JSON.stringify({{
|
|
normalized,
|
|
first,
|
|
second,
|
|
calls
|
|
}}));
|
|
}})().catch(err=>{{
|
|
console.error(err&&err.stack?err.stack:err);
|
|
process.exit(1);
|
|
}});
|
|
"""
|
|
assert _run_script(script) == {
|
|
"normalized": None,
|
|
"first": None,
|
|
"second": None,
|
|
"calls": 2,
|
|
}
|
|
|
|
|
|
@pytest.mark.skipif(NODE is None, reason="node not on PATH")
|
|
def test_manual_title_regenerate_timeout_fetch_failure_falls_back_then_retries():
|
|
setup = """
|
|
let calls=0;
|
|
async function api(){
|
|
calls++;
|
|
if(calls===1) throw new Error('network down');
|
|
return {tasks:[{task:'title_generation',timeout:180}]};
|
|
}
|
|
"""
|
|
helper = _title_timeout_support_block()
|
|
script = f"""
|
|
(async()=>{{
|
|
{setup}
|
|
{helper}
|
|
const first=await _manualTitleRegenerateTimeoutMs();
|
|
const second=await _manualTitleRegenerateTimeoutMs();
|
|
process.stdout.write(JSON.stringify({{first,second,calls}}));
|
|
}})().catch(err=>{{
|
|
console.error(err&&err.stack?err.stack:err);
|
|
process.exit(1);
|
|
}});
|
|
"""
|
|
assert _run_script(script) == {"first": None, "second": 185000, "calls": 2}
|
|
|
|
|
|
@pytest.mark.skipif(NODE is None, reason="node not on PATH")
|
|
def test_manual_title_regenerate_timeout_uses_each_fetched_value_after_profile_switch():
|
|
setup = """
|
|
let calls=0;
|
|
async function api(path,opts){
|
|
calls++;
|
|
if(path!=='/api/model/auxiliary') throw new Error('wrong path '+path);
|
|
if(!opts||opts.retries!==0||opts.timeoutToast!==false) throw new Error('wrong fetch options');
|
|
if(calls===1) return {tasks:[{task:'title_generation',timeout:180}]};
|
|
return {tasks:[{task:'title_generation',timeout:31}]};
|
|
}
|
|
"""
|
|
helper = _title_timeout_support_block()
|
|
script = f"""
|
|
(async()=>{{
|
|
{setup}
|
|
{helper}
|
|
const first=await _manualTitleRegenerateTimeoutMs();
|
|
const second=await _manualTitleRegenerateTimeoutMs();
|
|
process.stdout.write(JSON.stringify({{first,second,calls}}));
|
|
}})().catch(err=>{{
|
|
console.error(err&&err.stack?err.stack:err);
|
|
process.exit(1);
|
|
}});
|
|
"""
|
|
assert _run_script(script) == {"first": 185000, "second": 36000, "calls": 2}
|
|
|
|
|
|
@pytest.mark.skipif(NODE is None, reason="node not on PATH")
|
|
def test_manual_title_regenerate_timeout_invalid_fetched_timeout_falls_back_without_timeout():
|
|
setup = """
|
|
let calls=0;
|
|
async function api(){
|
|
calls++;
|
|
return {tasks:[{task:'title_generation',timeout:'abc'}]};
|
|
}
|
|
"""
|
|
helper = _title_timeout_support_block()
|
|
script = f"""
|
|
(async()=>{{
|
|
{setup}
|
|
{helper}
|
|
const first=await _manualTitleRegenerateTimeoutMs();
|
|
const second=await _manualTitleRegenerateTimeoutMs();
|
|
process.stdout.write(JSON.stringify({{first,second,calls}}));
|
|
}})().catch(err=>{{
|
|
console.error(err&&err.stack?err.stack:err);
|
|
process.exit(1);
|
|
}});
|
|
"""
|
|
assert _run_script(script) == {"first": None, "second": None, "calls": 2}
|
|
|
|
|
|
def test_manual_title_regenerate_api_call_uses_computed_timeout_option():
|
|
block = _title_regenerate_action_block()
|
|
request_pos = block.find("const requestOpts={method:'POST',body:JSON.stringify({session_id:session.session_id})};")
|
|
timeout_pos = block.find("const timeoutMs=await _manualTitleRegenerateTimeoutMs();")
|
|
assign_pos = block.find("if(timeoutMs) requestOpts.timeoutMs=timeoutMs;")
|
|
api_pos = block.find("api('/api/session/title/regenerate',requestOpts)")
|
|
|
|
assert -1 not in (request_pos, timeout_pos, assign_pos, api_pos)
|
|
assert request_pos < timeout_pos < assign_pos < api_pos
|
|
assert "timeoutMs:0" not in block
|