Initial Commit
This commit is contained in:
@@ -0,0 +1,206 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Each box can be clicked shut, and the live line is revealed, not dumped.
|
||||
|
||||
python3 tests/test_vm_live_view_collapse.py
|
||||
|
||||
WHAT WAS ASKED FOR
|
||||
|
||||
* the live line above the task list should arrive a few characters at a
|
||||
time, so that its speed is the token rate rather than a poll artefact;
|
||||
* clicking the task list should leave two lines - that live line, and
|
||||
the step the session is on;
|
||||
* clicking the screen should halve it, on black;
|
||||
* clicking the feed should halve its height;
|
||||
* clicking a second time should put any of them back.
|
||||
|
||||
THREE THINGS HERE WERE MEASURED IN A BROWSER, NOT REASONED ABOUT
|
||||
|
||||
1. `window.name` SURVIVES THE srcdoc SWAP AND NOTHING ELSE DOES. The view
|
||||
is re-sent every couple of seconds and each send replaces the frame's
|
||||
srcdoc, destroying the document. The frame is sandboxed without
|
||||
`allow-same-origin`, so it has an opaque origin: localStorage and
|
||||
cookies throw and there is no listener in Open WebUI to postMessage
|
||||
to. `window.name` belongs to the browsing context, which survives.
|
||||
Measured in that exact sandbox: the third load read back the name its
|
||||
two predecessors wrote. Without it a box springs open a second after
|
||||
being clicked shut.
|
||||
|
||||
2. THE STATE IS KEYED BY WHICH BOX IT IS. The screen box appears
|
||||
mid-session and pushes the feed down a slot, so the frame that was the
|
||||
feed becomes the screen - and would inherit its state.
|
||||
|
||||
3. `flex:none` ON THE FEED ROWS IS LOAD-BEARING. The rows sit in a flex
|
||||
column so a collapsed feed keeps its newest lines against the bottom.
|
||||
Flex items shrink by default, so without it the rows compressed into
|
||||
the smaller box and the text overlapped itself: measured, twenty lines
|
||||
crushed into ten lines of space instead of ten lines shown and ten
|
||||
lost off the top.
|
||||
|
||||
Browser-verified, in a real sandboxed frame, before this file was
|
||||
written: task box 196px -> 51px and back; screen 500px -> 269px with the
|
||||
image exactly half its width on black; feed 364px -> 190px showing ten
|
||||
rows of an unchanged 17.4px. All three survived two srcdoc swaps.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import types
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
FILTER = os.path.join(HERE, "..", "owui-functions", "vm_agent_progress.py")
|
||||
|
||||
FAILS: list[str] = []
|
||||
|
||||
|
||||
def check(label: str, condition: bool, detail: str = "") -> None:
|
||||
if condition:
|
||||
print(f"ok {label}")
|
||||
else:
|
||||
FAILS.append(f"{label}: {detail}")
|
||||
print(f"FAIL {label}\n {detail}")
|
||||
|
||||
|
||||
src = open(FILTER, encoding="utf-8").read()
|
||||
|
||||
aiohttp = types.ModuleType("aiohttp")
|
||||
aiohttp.ClientError = Exception
|
||||
aiohttp.ClientSession = object
|
||||
aiohttp.ClientTimeout = object
|
||||
sys.modules["aiohttp"] = aiohttp
|
||||
pydantic = types.ModuleType("pydantic")
|
||||
|
||||
|
||||
class _BaseModel:
|
||||
def __init__(self, **kw):
|
||||
for key, value in kw.items():
|
||||
setattr(self, key, value)
|
||||
|
||||
|
||||
pydantic.BaseModel = _BaseModel
|
||||
pydantic.Field = lambda default=None, **kw: default
|
||||
sys.modules["pydantic"] = pydantic
|
||||
|
||||
namespace: dict = {}
|
||||
exec(compile(src, FILTER, "exec"), namespace)
|
||||
Filter = namespace["Filter"]
|
||||
|
||||
|
||||
class Valves:
|
||||
show_terminal = True
|
||||
show_screenshots = True
|
||||
|
||||
|
||||
DATA = {
|
||||
"tasks": "\n".join(f"[ ] {i + 1}. step {i + 1}" for i in range(6)),
|
||||
"status": "3/6 · the step it is on",
|
||||
"speech": "the model's own voice, mid-sentence",
|
||||
"shot": "data:image/jpeg;base64,AAAA",
|
||||
"caption": "the desktop",
|
||||
"terminal": ["$ one", "$ two"],
|
||||
}
|
||||
|
||||
out: list = []
|
||||
|
||||
|
||||
async def _run():
|
||||
async def emitter(event):
|
||||
out.append(event)
|
||||
await Filter._embed(emitter, DATA, Valves())
|
||||
|
||||
|
||||
asyncio.run(_run())
|
||||
boxes = out[-1]["data"]["embeds"]
|
||||
check("three boxes once there is a screenshot", len(boxes) == 3, str(len(boxes)))
|
||||
TASKS, SCREEN, FEED = boxes
|
||||
|
||||
|
||||
def config(box: str) -> dict:
|
||||
found = re.search(r'<script type="application/json" id="cfg">(.*?)</script>', box, re.S)
|
||||
return json.loads(found.group(1).replace("<\\/", "</")) if found else {}
|
||||
|
||||
|
||||
# --- every box says which one it is ---------------------------------------
|
||||
check("each box carries its own config", all(config(b) for b in boxes))
|
||||
check("and they are the three kinds, in order",
|
||||
[config(b).get("k") for b in boxes] == ["tasks", "screen", "feed"],
|
||||
str([config(b).get("k") for b in boxes]))
|
||||
check("the config is JSON, not generated JavaScript",
|
||||
'type="application/json"' in TASKS,
|
||||
"the live line is the model's own words - a quote in it would end the script")
|
||||
raw_cfg = re.search(r'id="cfg">(.*?)</script>', TASKS, re.S).group(1)
|
||||
check("and a closing tag inside it cannot end the script early",
|
||||
"</" not in raw_cfg,
|
||||
"the HTML parser ends the block at the first </script>, wherever it appears, "
|
||||
"so the JSON escapes it as <\\/ - which JSON reads back as a plain slash")
|
||||
|
||||
# --- the task box collapses to two lines ----------------------------------
|
||||
check("the task box has a live line, the list, and the current step",
|
||||
all(x in TASKS for x in ('id="live"', 'id="full"', 'id="cur"')))
|
||||
check("the current step is the one the collapsed box will show",
|
||||
"the step it is on" in TASKS,
|
||||
"collapsed, the second line is the status - so it has to be in the box")
|
||||
check("the step line is hidden until the box is clicked",
|
||||
'id="cur" style="display:none' in TASKS)
|
||||
check("the live line is right-anchored, because the server sends the tail",
|
||||
"justify-content:flex-end" in TASKS,
|
||||
"the end of the sentence is the part that is moving")
|
||||
check("and it holds its line even before the model has spoken",
|
||||
"min-height" in TASKS,
|
||||
"otherwise the collapsed box is one line until the first token")
|
||||
|
||||
# --- the screen box halves onto black -------------------------------------
|
||||
check("the screen box has a wrapper to blacken and an image to shrink",
|
||||
'id="wrap"' in SCREEN and 'id="shot"' in SCREEN)
|
||||
|
||||
# --- the feed box halves its height ---------------------------------------
|
||||
rows = FEED.count('<div style="white-space:pre;overflow:hidden;')
|
||||
check("the feed is exactly as many rows as it claims",
|
||||
rows == namespace["TERMINAL_LINES"], str(rows))
|
||||
check("the rows are bottom-anchored, so a half feed keeps the newest lines",
|
||||
"justify-content:flex-end" in FEED and 'id="rows"' in FEED)
|
||||
check("AND THE ROWS CANNOT SHRINK",
|
||||
FEED.count("flex:none") == namespace["TERMINAL_LINES"],
|
||||
"flex items shrink by default: without this the twenty rows compress "
|
||||
"into the smaller box and the text overlaps itself")
|
||||
check("the feed knows how many rows it has, for the half",
|
||||
config(FEED).get("n") == namespace["TERMINAL_LINES"])
|
||||
|
||||
# --- the script: state that outlives the document -------------------------
|
||||
script = namespace["_VIEW_SCRIPT"]
|
||||
check("state is read from and written to window.name",
|
||||
"window.name" in script and "JSON.stringify(st)" in script,
|
||||
"the only store that survives a srcdoc swap in an opaque origin")
|
||||
check("state is discarded when the box is a different one",
|
||||
"st.k!==kind" in script.replace(" ", ""),
|
||||
"the screen box appears mid-session and pushes the feed down a slot")
|
||||
check("a click toggles it, saves it, applies it and re-reports the height",
|
||||
all(x in script.replace(" ", "") for x in
|
||||
("addEventListener('click'", "st.c=st.c?0:1", "save();apply();post();")),
|
||||
"the frame sizes itself, so a box that changes height must say so")
|
||||
check("the height is still reported the only way that works here",
|
||||
"iframe:height" in script and "postMessage" in script,
|
||||
"contentDocument is cross-origin in this sandbox and fails silently")
|
||||
check("no tooltip was added", "title=" not in TASKS and "title=" not in FEED)
|
||||
|
||||
# --- the script: the live line is revealed, not dumped --------------------
|
||||
check("the line is appended a few characters at a time",
|
||||
"setInterval" in script and "pending.slice" in script)
|
||||
check("it continues the previous line rather than retyping it",
|
||||
"st.d" in script,
|
||||
"the text so far rides in window.name across the swap")
|
||||
check("it works out what is new from the overlap with what is shown",
|
||||
"target.slice(0,k)" in script.replace(" ", ""),
|
||||
"the server sends a sliding window of the tail, not a growing string")
|
||||
check("and the buffer it keeps is bounded",
|
||||
"CAP" in script and "slice(-CAP)" in script.replace(" ", ""))
|
||||
|
||||
print()
|
||||
if FAILS:
|
||||
print(f"{len(FAILS)} failed")
|
||||
for line in FAILS:
|
||||
print(" -", line)
|
||||
sys.exit(1)
|
||||
print("every box can be clicked shut, and the live line arrives at the rate it was written")
|
||||
Reference in New Issue
Block a user