Files
CyberGril/web/tests/import_check.py
T

106 lines
8.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Browser checks for destructive confirmations and unsafe file handling; no backend."""
import functools
import http.server
import json
from pathlib import Path
import threading
from playwright.sync_api import sync_playwright, expect
ROOT=Path(__file__).resolve().parents[1]
class Quiet(http.server.SimpleHTTPRequestHandler):
def log_message(self,*_): pass
server=http.server.ThreadingHTTPServer(('127.0.0.1',0),functools.partial(Quiet,directory=str(ROOT/'dist')))
thread=threading.Thread(target=server.serve_forever);thread.start()
url=f'http://127.0.0.1:{server.server_port}'
identity={'id':'demo-person','name':'小满','birth':'2000-01-01','hour':'亥时','relation':'朋友','nickname':'','boundary':'尊重彼此'}
state={'version':1,'identity':identity,'settings':{'city':'杭州','appearance':'reference-v1','reduced':False,'services':{'对话模型':'','ASR':'','TTS':'','视觉观察':''}},'events':[],'memories':[],'messages':[],'lastVisit':9999999999999}
checks=[]
def fileinput(page,label,data):
page.get_by_label(label,exact=True).set_input_files({'name':'demo.json','mimeType':'application/json','buffer':json.dumps(data).encode()})
def click(page,name): page.get_by_role('button',name=name,exact=True).click()
def section(page,name):page.get_by_role('navigation',name='设置分区').get_by_role('button',name=name,exact=True).click()
try:
with sync_playwright() as p:
browser=p.chromium.launch(headless=True,args=['--no-sandbox'])
ctx=browser.new_context(viewport={'width':1280,'height':800},accept_downloads=True)
ctx.add_init_script('if(!localStorage.getItem("cybergril.demo.v1")) localStorage.setItem("cybergril.demo.v1",'+json.dumps(json.dumps(state))+')')
page=ctx.new_page();page.set_default_timeout(7000)
outgoing=[];page.on('request',lambda r:outgoing.append(r.url))
page.goto(url+'/?demo=1')
page.get_by_role('navigation',name='主导航').get_by_role('button',name='设置',exact=True).click()
section(page,'本地演示导出')
label='恢复同一位她的本地演示 JSON'
other={**state,'identity':{**identity,'id':'other'}}
fileinput(page,label,other);expect(page.get_by_role('alert')).to_contain_text('另一位')
imported={**state,'settings':{**state['settings'],'city':'成都'},'messages':[{'id':'hostile','role':'companion','text':'<img src=x onerror=alert(1)>','lifeIds':[]}]}
fileinput(page,label,imported);click(page,'检查通过,继续恢复')
dialog=page.get_by_role('dialog');expect(dialog.get_by_role('button',name='确认',exact=True)).to_be_disabled()
page.get_by_label('输入“恢复”以继续',exact=True).fill('恢复')
with page.expect_download() as d:dialog.get_by_role('button',name='确认',exact=True).click()
assert d.value.suggested_filename.startswith('CyberGril-demo-')
expect(page.get_by_role('status').filter(has_text='本地演示资料已恢复')).to_be_visible()
section(page,'她');expect(page.locator('.kv')).to_contain_text('小满')
checks.append('Reject foreign identity; same-identity restore requires typed confirmation and pre-restore export')
section(page,'外观素材')
fileinput(page,'选择本地素材清单(演示 JSON)',{'manifest_version':1,'appearance_id':'<script>','clips':[1]})
expect(page.get_by_role('alert')).to_contain_text('检查没通过')
fileinput(page,'选择本地素材清单(演示 JSON)',{'manifest_version':1,'appearance_id':'new-look','clips':[{'src':'https://example.invalid/evil.js'}]})
expect(page.get_by_role('status').filter(has_text='未真实检测')).to_be_visible()
click(page,'应用演示外观标识');click(page,'取消')
expect(page.get_by_text('当前素材:reference-v1',exact=False)).to_be_visible()
click(page,'应用演示外观标识');page.get_by_role('dialog').get_by_role('button',name='确认',exact=True).click()
expect(page.get_by_text('当前素材:new-look',exact=False)).to_be_visible()
original=page.evaluate('JSON.parse(localStorage.getItem("cybergril.demo.v1")).identity')
assert original==identity
section(page,'外接服务')
page.get_by_label('对话模型地址',exact=True).fill('https://example.invalid/model')
page.locator('.svc').first.get_by_role('button',name='测试连接',exact=True).click()
expect(page.get_by_role('status').filter(has_text='未真实检测')).to_be_visible()
assert not [r for r in outgoing if not r.startswith(url)],outgoing
checks.append('Invalid manifests rejected; external media never loaded; appearance confirm/cancel preserves identity; service test has no network')
# Cancel a save by switching section while its simulated request is outstanding.
section(page,'城市');page.locator('.city-list button').filter(has_text='上海').click();click(page,'保存');section(page,'她')
page.wait_for_timeout(700)
assert page.evaluate('JSON.parse(localStorage.getItem("cybergril.demo.v1")).settings.city')=='成都'
section(page,'城市');click(page,'保存');expect(page.get_by_role('status').filter(has_text='已保存在本机演示')).to_be_visible()
assert page.evaluate('JSON.parse(localStorage.getItem("cybergril.demo.v1")).settings.services["对话模型"]')==''
checks.append('Changing section aborts outstanding save; saving city does not commit an unsaved service draft')
page.get_by_role('navigation',name='主导航').get_by_role('button',name='交谈',exact=True).click()
leave=page.get_by_role('dialog').get_by_role('button',name='放弃修改并离开',exact=True)
if leave.count(): leave.click()
click(page,'完整记录')
expect(page.locator('.log-panel')).to_contain_text('<img src=x onerror=alert(1)>')
expect(page.locator('.log-panel img')).to_have_count(0)
checks.append('Imported model text rendered as text, never as HTML')
page.get_by_role('navigation',name='主导航').get_by_role('button',name='此刻',exact=True).click()
page.locator('.demo > button').click();click(page,'长记录');page.locator('.demo > button').click()
page.get_by_role('navigation',name='主导航').get_by_role('button',name='日子',exact=True).click()
expect(page.locator('.ev')).to_have_count(18)
click(page,'看更早的日子');expect(page.locator('.ev')).to_have_count(30)
page.locator('.ev').nth(10).click()
scroll=page.locator('.list').evaluate('el=>el.scrollTop')
click(page,'在交谈里看这段');click(page,'结束交谈 · 回到日子')
assert page.locator('.list').evaluate('el=>el.scrollTop')==scroll
checks.append('Long timeline paginates 18 then 12 and retains exact scroll position after chat return')
ctx.close()
bad=browser.new_context();bad.add_init_script('localStorage.setItem("cybergril.demo.v1","{broken")');page=bad.new_page();page.goto(url+'/?demo=1')
expect(page.get_by_role('heading',name='资料暂时读不出来')).to_be_visible();expect(page.locator('.create')).to_have_count(0)
assert page.evaluate('localStorage.getItem("cybergril.demo.v1")')=='{broken'
checks.append('Corrupt persistence never falls back to overwriting first creation')
bad.close()
plain=browser.new_context(viewport={'width':1280,'height':800})
plain.add_init_script('Object.defineProperty(crypto,"randomUUID",{value:undefined})')
page=plain.new_page();page.goto(url+'/?demo=1')
page.get_by_label('她的名字',exact=True).fill('初夏')
click(page,'下一步');click(page,'下一步');click(page,'朋友');click(page,'下一步')
page.get_by_role('checkbox').check();click(page,'确认创建');click(page,'去看看她')
expect(page.locator('.who .name')).to_have_text('初夏')
page.get_by_role('navigation',name='主导航').get_by_role('button',name='交谈',exact=True).click()
page.get_by_role('textbox',name='想和她说的话').fill('HTTP 预览兼容')
click(page,'发送');expect(page.locator('.stage .line[data-role="companion"]')).to_contain_text('我听见了',timeout=7000)
checks.append('Creation and chat work when secure-context randomUUID is unavailable')
plain.close();browser.close()
finally:
server.shutdown();server.server_close();thread.join()
(ROOT.parents[0]/'docs/frontend/import-results.json').write_text(json.dumps(checks,ensure_ascii=False,indent=2))
for c in checks:print('PASS',c)