* perf(media): preload visible chat videos * fix(media): ignore detached visibility callbacks * fix(ui): require provable live owner for live-turn preservation (#6948) A completed assistant turn could render twice: when the stream ended (S.activeStreamId cleared) but INFLIGHT[sid] was not yet cleaned, the preserve guard treated bare rendered content (.msg-body presence) as proof of liveness and re-attached the stale live DOM node OVER the rebuilt settled transcript — a second copy of the same message. Data was always clean (state.db, sidecar, /api/session each hold one row); the duplicate existed only in the rendered DOM. Preserve the live turn across the wipe only while there is a provable live owner: an active stream (S.activeStreamId, the #3877 mid-stream case) or explicit live-assistant evidence in the current message projection (S.messages carrying a client-side _live / _activityBurstId / _liveSegmentSeq marker — the reconnect/terminal-projection case). A settled transcript has neither, so the durable settled render wins and appears exactly once. The #5390 blank-turn guard (对话消失) is preserved: a dead empty shell has no live projection either and is still dropped. * changelog: exp batch — #7109 double-render fix + #7076 video preload --------- Co-authored-by: allenliang2022 <allenliang2022@users.noreply.github.com> Co-authored-by: webtecnica <webtecnica@gmail.com> Co-authored-by: n <a@n> Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
135 lines
4.5 KiB
Python
135 lines
4.5 KiB
Python
"""Behavioral tests for visibility-driven chat video preloading."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import subprocess
|
|
from pathlib import Path
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
UI_JS = (ROOT / "static" / "ui.js").read_text(encoding="utf-8")
|
|
|
|
|
|
def _extract_function(name: str) -> str:
|
|
start = UI_JS.index(f"function {name}(")
|
|
brace = UI_JS.index("{", start)
|
|
depth = 1
|
|
i = brace + 1
|
|
while depth:
|
|
if UI_JS[i] == "{":
|
|
depth += 1
|
|
elif UI_JS[i] == "}":
|
|
depth -= 1
|
|
i += 1
|
|
return UI_JS[start:i]
|
|
|
|
|
|
def _run_node(body: str) -> dict:
|
|
script = "\n".join(
|
|
[
|
|
"let _mediaVisibilityObserver=null;",
|
|
_extract_function("_promoteVisibleVideoPreload"),
|
|
_extract_function("_observeVideoPreload"),
|
|
_extract_function("_unobserveVideoPreload"),
|
|
_extract_function("_initMediaVisibilityObserver"),
|
|
body,
|
|
]
|
|
)
|
|
result = subprocess.run(
|
|
["node", "-e", script],
|
|
cwd=ROOT,
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=30,
|
|
)
|
|
assert result.returncode == 0, result.stderr
|
|
return json.loads(result.stdout)
|
|
|
|
|
|
def test_offscreen_video_stays_metadata_only_until_first_intersection():
|
|
result = _run_node(
|
|
r"""
|
|
let callback=null;
|
|
const observed=[];
|
|
const unobserved=[];
|
|
global.IntersectionObserver=class {
|
|
constructor(cb,options){callback=cb;this.options=options;}
|
|
observe(v){observed.push(v);}
|
|
unobserve(v){unobserved.push(v);}
|
|
};
|
|
const video={
|
|
dataset:{}, preload:'metadata', paused:true, readyState:1, loads:0,
|
|
matches:s=>s==='.msg-media-video', load(){this.loads+=1;}
|
|
};
|
|
_initMediaVisibilityObserver();
|
|
_observeVideoPreload(video);
|
|
const before={preload:video.preload,loads:video.loads,observed:observed.length};
|
|
callback([{target:video,isIntersecting:false}]);
|
|
const offscreen={preload:video.preload,loads:video.loads,unobserved:unobserved.length};
|
|
callback([{target:video,isIntersecting:true}]);
|
|
const first={preload:video.preload,loads:video.loads,unobserved:unobserved.length};
|
|
callback([{target:video,isIntersecting:true}]);
|
|
const second={preload:video.preload,loads:video.loads,unobserved:unobserved.length};
|
|
console.log(JSON.stringify({before,offscreen,first,second,options:_mediaVisibilityObserver.options}));
|
|
"""
|
|
)
|
|
|
|
assert result["before"] == {"preload": "metadata", "loads": 0, "observed": 1}
|
|
assert result["offscreen"] == {"preload": "metadata", "loads": 0, "unobserved": 0}
|
|
assert result["first"] == {"preload": "auto", "loads": 1, "unobserved": 1}
|
|
assert result["second"] == {"preload": "auto", "loads": 1, "unobserved": 2}
|
|
assert result["options"]["rootMargin"] == "300px 0px"
|
|
|
|
|
|
def test_removed_video_is_unobserved_before_it_intersects():
|
|
script = "\n".join(
|
|
[
|
|
"let _mediaVisibilityObserver=null;",
|
|
_extract_function("_promoteVisibleVideoPreload"),
|
|
_extract_function("_observeVideoPreload"),
|
|
_extract_function("_unobserveVideoPreload"),
|
|
_extract_function("_initMediaVisibilityObserver"),
|
|
_extract_function("_initMediaPlaybackObserver"),
|
|
r"""
|
|
let ioCallback=null,mutationCallback=null;
|
|
const observed=[],unobserved=[];
|
|
global.IntersectionObserver=class {
|
|
constructor(cb){ioCallback=cb;}
|
|
observe(v){observed.push(v);}
|
|
unobserve(v){unobserved.push(v);}
|
|
};
|
|
global.MutationObserver=class {
|
|
constructor(cb){mutationCallback=cb;}
|
|
observe(){}
|
|
};
|
|
const documentVideos=[];
|
|
global.document={
|
|
body:{}, readyState:'complete',
|
|
querySelectorAll:()=>documentVideos,
|
|
addEventListener:()=>{}
|
|
};
|
|
global.window={};
|
|
global._applyMediaPlaybackRate=()=>{};
|
|
const video={nodeType:1,isConnected:true,dataset:{},preload:'metadata',loads:0,load(){this.loads+=1;},matches:s=>s==='.msg-media-video'||s==='audio,video',querySelectorAll:()=>[]};
|
|
_initMediaPlaybackObserver();
|
|
mutationCallback([{addedNodes:[video],removedNodes:[]}]);
|
|
mutationCallback([{addedNodes:[],removedNodes:[video]}]);
|
|
video.isConnected=false;
|
|
ioCallback([{target:video,isIntersecting:true}]);
|
|
console.log(JSON.stringify({observed:observed.length,unobserved:unobserved.length,preload:video.preload,loads:video.loads,marker:video.dataset.visiblePreload||null}));
|
|
""",
|
|
]
|
|
)
|
|
result = subprocess.run(
|
|
["node", "-e", script], cwd=ROOT, capture_output=True, text=True, timeout=30
|
|
)
|
|
assert result.returncode == 0, result.stderr
|
|
assert json.loads(result.stdout) == {
|
|
"observed": 1,
|
|
"unobserved": 2,
|
|
"preload": "metadata",
|
|
"loads": 0,
|
|
"marker": None,
|
|
}
|