保存 NAS 健康系统 UI7 代码与本机维护基线

This commit is contained in:
weiyuqing committed 2026-10-17 13:29:12 +08:00
1 parent 1767b4921a
commit 8546ba23ff
80 files changed
+4984 -381

No files matched your search

@@ -0,0 +1,49 @@
"""本机修改网页资源,保留文件编码和换行。"""
from pathlib import Path
import re
base=Path('work/mi-fitness-mcp-cn/src/mi_fitness_mcp/web_assets')
def edit(path,fn):
raw=path.read_bytes();crlf=b'\r\n' in raw
text=raw.decode('utf-8').replace('\r\n','\n')
updated=fn(text)
path.write_bytes((updated.replace('\n','\r\n') if crlf else updated).encode('utf-8'))
def app(s):
s=s.replace(" inspectorRequestId: 0,", " inspectorRequestId: 0,\n healthRequestId: 0,\n healthViews: {},\n healthController: null,")
s=s.replace(" if (state.activeTab === 'overview') return loadOverview();", " return reloadActiveView();",1)
s=s.replace("function switchTab(tabId) {\n state.activeTab = tabId;", "function switchTab(tabId) {\n invalidateHealthQuery();\n clearInspector();\n state.activeTab = tabId;")
s=s.replace(" if (tabId === 'overview') return loadOverview();", " return reloadActiveView();",1)
s=s.replace(" headers: getHeaders(),\n };", " headers: getHeaders(),\n ...(view.signal ? {signal: view.signal} : {}),\n };",1)
replacements={
'querySummary':"return loadHealthView('activity');",
'queryHeartRate':"return loadHealthView('heartrate');",
'querySleep':"return loadHealthView('sleep');",
'queryWorkouts':"return loadHealthView('workouts');",
'queryBody':"return loadHealthView('body');",
'querySpo2':"return loadHealthView('spo2');",
'queryStress':"return loadHealthView('stress');",
'queryAbnormalHr':"return loadHealthView('abnormal');",
}
for name,body in replacements.items():
s,n=re.subn(r'async function '+name+r'\(\) \{.*?\n\}',f'async function {name}() {{\n {body}\n}}',s,count=1,flags=re.S)
assert n==1,name
s=s.replace(' initTheme();\n initDates();',' initTheme();\n initHealthViews();\n initDates();',1)
s=s.replace(" if (state.activeTab === 'overview') loadOverview();", " clearInspector();\n reloadActiveView();",1)
return s
def html(s):
s=s.replace('20261017-ui3','20261017-ui4')
s=s.replace('<div class="main-layout">','<div class="main-layout inspector-collapsed" id="mainLayout">',1)
s=s.replace('<aside class="app-inspector">','<aside class="app-inspector" id="inspectorPanel" hidden>',1)
s=s.replace('<button class="preset-pill" data-preset="month" id="preset-month">本月</button>', '<button class="preset-pill" data-preset="month" id="preset-month">本月</button>\n <button class="btn btn-outline btn-sm" id="inspectorToggle" onclick="toggleInspector()" aria-expanded="false" aria-controls="inspectorPanel" style="margin-left:auto">展开控制台</button>')
for kind in ['activity','heartrate','sleep','workouts','body','spo2','stress','abnormal']:
pattern=r'(<section class="tab-pane" id="tab-'+kind+r'">)(.*?)(\n </section>)'
s,n=re.subn(pattern,lambda m:m[1]+m[2]+'\n <div class="health-results" id="healthView-'+kind+'" aria-live="polite"></div>'+m[3],s,count=1,flags=re.S)
assert n==1,kind
s=s.replace('<div id="summaryVisualContainer"','<div hidden id="summaryVisualContainer"').replace('<div id="sleepTimelineWrapper"','<div hidden id="sleepTimelineWrapper"')
s=s.replace('<div class="btn-group">','<div class="btn-group">\n <button class="btn btn-secondary btn-sm" onclick="setInspectorCollapsed(true)" title="收起控制台">收起</button>',1)
s=s.replace('<script src="/static/js/app.js', '<script src="/static/js/health-views.js?v=20261017-ui4"></script>\n <script src="/static/js/health-ui.js?v=20261017-ui4"></script>\n <script src="/static/js/app.js',1)
return s
edit(base/'static/js/app.js',app)
edit(base/'templates/index.html',html)
@@ -0,0 +1,61 @@
// 本机 Edge 实测,固定合成数据,不访问 NAS。
const fs = require('node:fs');
const path = require('node:path');
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
async function main() {
const tabs = await (await fetch('http://127.0.0.1:18382/json')).json();
const tab = tabs.find(tab => tab.type === 'page');
const ws = new WebSocket(tab.webSocketDebuggerUrl);
await new Promise((resolve,reject) => {ws.onopen=resolve;ws.onerror=reject;});
let id=0;
const pending = new Map();
ws.onmessage = event => {
const result=JSON.parse(event.data);
if (result.id && pending.has(result.id)) {
const {resolve,reject}=pending.get(result.id);pending.delete(result.id);
result.error ? reject(result.error) : resolve(result.result);
}
};
function call(method,params={}) {return new Promise((resolve,reject)=>{const next=++id;pending.set(next,{resolve,reject});ws.send(JSON.stringify({id:next,method,params}));});}
await call('Page.enable');await call('Runtime.enable');await call('Network.enable');
await call('Network.setBlockedURLs',{urls:['https://*']});
await call('Emulation.setDeviceMetricsOverride',{width:1400,height:900,deviceScaleFactor:1,mobile:false});
const version=process.argv[2] || 'before';
const preset=process.argv[3] || '30d';
await call('Page.navigate',{url:'http://127.0.0.1:18363/?version='+version});
let ready=false;
for(let i=0;i<100;i++) {
await sleep(100);
const result=await call('Runtime.evaluate',{expression:"document.readyState==='complete' && document.getElementById('statSpo2')?.textContent==='97%'",returnByValue:true});
if(result.result.value){ready=true;break;}
}
if(!ready)throw new Error('Fixture dashboard did not become ready');
const measured=await call('Runtime.evaluate',{expression:`(async()=>{
const measurements={version:${JSON.stringify(version)},renders:0,render_ms:0,max_inspector_nodes:0};
const original=renderJsonViewer;
renderJsonViewer=function(data){const start=performance.now();original(data);measurements.render_ms+=performance.now()-start;measurements.renders++;measurements.max_inspector_nodes=Math.max(measurements.max_inspector_nodes,document.getElementById('inspectorBody').querySelectorAll('*').length);};
const tasks=[];const observer=new PerformanceObserver(list=>tasks.push(...list.getEntries().map(entry=>entry.duration)));
observer.observe({type:'longtask'});
const start=performance.now();await setDatePreset(${JSON.stringify(preset)});
await new Promise(resolve=>requestAnimationFrame(()=>requestAnimationFrame(resolve)));
measurements.elapsed_ms=performance.now()-start;
measurements.steps=document.getElementById('statSteps').textContent;
measurements.spo2=document.getElementById('statSpo2').textContent;
measurements.stress=document.getElementById('statStress').textContent;
measurements.inspector_nodes=document.getElementById('inspectorBody').querySelectorAll('*').length;
measurements.header_backdrop=getComputedStyle(document.querySelector('.top-header')).backdropFilter;
measurements.inspector_visible=getComputedStyle(document.querySelector('.app-inspector')).display!=='none';
await new Promise(resolve=>setTimeout(resolve,100));observer.disconnect();
measurements.long_tasks=tasks;renderJsonViewer=original;
return measurements;
})()`,awaitPromise:true,returnByValue:true});
if(measured.exceptionDetails)throw new Error(JSON.stringify(measured.exceptionDetails));
const result=measured.result.value;
result.preset=preset;
const target=path.resolve('work/perf-validation/'+version+'-'+preset+'.json');
fs.writeFileSync(target,JSON.stringify(result,null,2)+'\n','utf8');
const screenshot=await call('Page.captureScreenshot',{format:'png',captureBeyondViewport:false});
fs.writeFileSync(path.resolve('work/perf-validation/'+version+'-'+preset+'.png'),Buffer.from(screenshot.data,'base64'));
console.log(JSON.stringify(result));ws.close();
}
main().catch(error=>{console.error(error);process.exit(1);});
@@ -0,0 +1,30 @@
"""对本机隔离 Linux 容器做鉴权及持久化验收。"""
import json
import sqlite3
from pathlib import Path
import httpx
ROOT = Path(__file__).resolve().parents[2]
with httpx.Client(base_url='http://127.0.0.1:18372', trust_env=False, timeout=20) as client:
assert client.get('/').status_code == 401
assert client.get('/proxy/api/auth/keys').status_code == 401
assert client.get('/', auth=('admin', 'wrong')).status_code == 401
client.auth = ('admin', 'synthetic-web-for-local-test')
page = client.get('/')
assert page.status_code == 200 and '20261016-ui2' in page.text
response = client.get('/proxy/api/summary', params={'start_date': '2026-10-15', 'end_date': '2026-10-15'},
headers={'X-API-Key': 'mif_sk_syntheticlocal'})
assert response.status_code == 200, response.status_code
assert response.json()['data'][0]['steps'] == 4691
assert response.json()['data'][0]['active_kcal'] == 596
assert response.headers['Cache-Control'] == 'no-store'
assert client.get('/proxy/api/auth/keys').status_code == 200
assert client.get('/proxy/api/auth/keys', headers={'X-API-Key': 'mif_sk_invalid'}).status_code == 200
assert client.delete('/proxy/api/auth/keys/mif_sk_syntheticlocal', headers={'Origin': 'https://untrusted.invalid'}).status_code == 403
assert client.get('/proxy/api/summary', params={'start_date': '2026-10-15', 'end_date': '2026-10-15'},
headers={'X-API-Key': 'mif_sk_invalid'}).status_code == 401
with sqlite3.connect(ROOT / 'work/audit2-container-validation/state/data/mi_fitness.db') as conn:
assert conn.execute("SELECT count(*) FROM api_keys WHERE pass_token!=''").fetchone()[0] == 0
assert conn.execute('PRAGMA quick_check').fetchone()[0] == 'ok'
print(json.dumps({'browser_auth': 'ok', 'query': 'ok', 'csrf_block': 'ok', 'invalid_api_key': 'denied',
'sqlite_integrity': 'ok', 'sqlite_plaintext_tokens': 0, 'fixture': 'synthetic only'}, indent=2))
@@ -0,0 +1,26 @@
"""仅针对本机合成容器库核查备份和只读查询。"""
import json
import sqlite3
import sys
from pathlib import Path
sys.path.insert(0, '/opt/health')
import runtime
from readonly import ReadOnlyDatabase, query_records
source = Path('/state/data/mi_fitness.db')
backup = runtime.backup_database(source, '/state/backups')
with sqlite3.connect(backup) as conn:
assert conn.execute('SELECT steps FROM daily_activity').fetchone()[0] == 4691
assert conn.execute('SELECT count(*) FROM api_keys').fetchone()[0] == 0
assert conn.execute('PRAGMA quick_check').fetchone()[0] == 'ok'
assert query_records(source, 'daily_activity', '2026-10-15', '2026-10-15')[0]['steps'] == 4691
db = ReadOnlyDatabase(source)
with db._get_connection() as conn:
for statement in ('SELECT * FROM api_keys', 'DELETE FROM daily_activity'):
try:
conn.execute(statement)
except sqlite3.DatabaseError:
continue
raise AssertionError('Read-only boundary bypass')
print(json.dumps({'backup_restore': 'ok', 'backup_credentials_removed': True,
'readonly_health_query': 'ok', 'credential_table_denied': True, 'database_write_denied': True}))
@@ -0,0 +1,58 @@
"""在本机 Linux 候选镜像内核查数据边界;无真实账号或网络。"""
import asyncio
import json
import tempfile
from datetime import datetime, timedelta, timezone
from pathlib import Path
from mi_fitness_mcp import api
from mi_fitness_mcp.models import DailyActivity, StressSample, Workout
from mi_fitness_mcp.storage import Database
from mi_fitness_mcp.services.sync_service import SyncService
CN = timezone(timedelta(hours=8))
with tempfile.TemporaryDirectory(prefix='audit3-edges-') as temporary:
db = Database(Path(temporary) / 'synthetic.sqlite')
item = DailyActivity(id='day-A', provider='mi_fitness', source_type='cloud_session', user_id='A',
date='2026-10-16', steps=1111, active_kcal=100, distance_m=1000)
db.insert_daily_activity(item)
with db._get_connection() as conn:
conn.execute('CREATE TABLE api_keys (key TEXT,label TEXT,user_id TEXT,pass_token TEXT,region TEXT,created_at TEXT,revoked INTEGER DEFAULT 0)')
conn.commit()
try:
db.insert_daily_activity(item.model_copy(update={'user_id':'B','steps':2222}))
except ValueError:
pass
else:
raise AssertionError('Account collision not blocked')
try:
api._create_key_record(db, 'B', 'synthetic', 'cn', 'other')
except api.HTTPException as exc:
assert exc.status_code == 409
else:
raise AssertionError('Second owner authorization accepted')
assert db.query_daily_activity('A','2026-10-16','2026-10-16')[0]['steps'] == 1111
db.update_sync_state('heart_rate', datetime(2026,10,16,9,tzinfo=CN), user_id='A')
assert db.get_sync_state('heart_rate', user_id='B') is None
class Adapter:
user_id='A'
region='cn'
calls=[]
def is_connected(self): return True
async def iter_heart_rate(self,start,end):
self.calls.append((start,end))
if False: yield None
adapter=Adapter()
result=asyncio.run(SyncService(adapter,db).sync_data_type('heart_rate',end_date='2026-10-16'))
assert result['status']=='ok' and adapter.calls==[('2026-10-16','2026-10-16')]
for index,hour in enumerate((1,9)):
db.insert_stress_sample(StressSample(id=f'stress-{index}',provider='mi_fitness',source_type='cloud_session',user_id='A',
timestamp=datetime(2026,10,16,hour,tzinfo=CN),stress_score=30,level='medium'))
coverage=next(row for row in db.get_data_coverage('A') if row['data_type']=='stress')
assert coverage['days_with_data']==1 and coverage['first_date']=='2026-10-16'
exercise=Workout(id='exercise',workout_id='exercise',provider='mi_fitness',source_type='cloud_session',user_id='A',
activity_type='running',start_at=datetime(2026,10,16,9,tzinfo=CN),end_at=datetime(2026,10,16,9,30,tzinfo=CN),duration_minutes=30)
db.insert_workout(exercise)
db.insert_workout(exercise.model_copy(update={'total_steps':4000,'avg_pace_sec_per_km':300}))
row=db.query_workouts('A','2026-10-16','2026-10-16')[0]
assert row['total_steps']==4000 and row['avg_pace_sec_per_km']==300
print(json.dumps({'account_isolation':'ok','cursor_isolation':'ok','same_day_incremental':'ok','coverage_dates':'ok','workout_updates':'ok'}))
@@ -0,0 +1,55 @@
import asyncio
import json
import os
import sqlite3
import tempfile
import time
from datetime import datetime, timedelta, timezone
from pathlib import Path
import httpx
root=Path(tempfile.mkdtemp())
os.environ['XDG_CONFIG_HOME']=str(root/'config')
os.environ['XDG_DATA_HOME']=str(root/'data')
os.environ['HOME']=str(root)
from mi_fitness_mcp.api import app
from mi_fitness_mcp.models import HeartRateSample
from mi_fitness_mcp.services.sync_service import SyncService
from mi_fitness_mcp.storage import Database
class SlowDisk(Database):
def insert_heart_rate_sample(self, sample, **kwargs):
time.sleep(0.01)
return super().insert_heart_rate_sample(sample, **kwargs)
class SyntheticAdapter:
def is_connected(self): return True
async def iter_heart_rate(self,start,end):
stamp=datetime(2026,10,15,tzinfo=timezone.utc)
for i in range(40):
yield HeartRateSample(id=f'local-proof-{i}',provider='mi_fitness',source_type='cloud_session',user_id='synthetic-only',timestamp=stamp+timedelta(minutes=i),bpm=70)
async def main():
db=SlowDisk(root/'health.sqlite')
service=SyncService(SyntheticAdapter(),db)
task=asyncio.create_task(service.sync_data_type('heart_rate','2026-10-15','2026-10-15'))
await asyncio.sleep(0.02)
async with httpx.AsyncClient(transport=httpx.ASGITransport(app=app),base_url='http://127.0.0.1') as client:
started=time.perf_counter()
r=await client.get('/')
elapsed=time.perf_counter()-started
responsive=not task.done()
result=await task
with sqlite3.connect(db.db_path) as c:
rows=c.execute('SELECT count(*) FROM heart_rate_samples').fetchone()[0]
integrity=c.execute('PRAGMA quick_check').fetchone()[0]
report={'linux_http_status_during_disk_wait':r.status_code,'request_returned_while_sync_active':responsive,'local_request_ms':round(elapsed*1000,1),'synthetic_records_stored':rows,'sync_status':result['status'],'database_quick_check':integrity}
print(json.dumps(report))
assert r.status_code==200 and responsive and rows==40 and integrity=='ok'
asyncio.run(main())
+53
View File
@@ -0,0 +1,53 @@
"""容器内执行的真实链路检查,只输出验收布尔值与任务状态。"""
import gzip
import json
import sqlite3
import sys
from pathlib import Path
import httpx
sys.path.insert(0, '/opt/health')
import runtime
db_path = Path('/state/data/mi_fitness.db')
key = runtime.load_single_key(db_path)
mode = sys.argv[1]
with httpx.Client(base_url='http://127.0.0.1:8321',headers={'X-API-Key':key},trust_env=False,timeout=25) as client:
if mode == 'start':
r=client.post('/api/sync',json={'start_date':'2026-10-15','end_date':'2026-10-16','background':True})
r.raise_for_status()
Path('/state/linux-check-task.json').write_text(json.dumps(r.json()),encoding='utf-8')
print(json.dumps({'status':r.json()['status']}))
elif mode == 'poll':
task=json.loads(Path('/state/linux-check-task.json').read_text(encoding='utf-8'))
r=client.get('/api/sync/'+task['sync_id'])
r.raise_for_status()
result=r.json()
print(json.dumps({'status':result['status'],'types':[{'type':t['data_type'],'status':t['status']} for t in result.get('results',[])]}))
elif mode == 'verify':
params={'start_date':'2026-10-15','end_date':'2026-10-16'}
summaries=client.get('/api/summary',params=params); summaries.raise_for_status()
yesterday=next(d for d in summaries.json()['data'] if d['date']=='2026-10-15')
sleep=client.get('/api/sleep',params=params);sleep.raise_for_status()
sessions=sleep.json()['data']
weights=client.get('/api/body-measurements',params=params);weights.raise_for_status()
print(json.dumps({'daily_matches_phone':yesterday['steps']==4691 and yesterday['active_kcal']==596,
'sleep_matches_phone':len(sessions)==2 and sorted(s['duration_minutes'] for s in sessions)==[474,517],
'weight_matches_phone':any(w.get('weight_kg')==87.3 for w in weights.json()['data'])}))
raw_count=0
paths=set()
for f in Path('/state/raw').glob('*.jsonl.gz'):
with gzip.open(f,'rt',encoding='utf-8') as lines:
for line in lines:
entry=json.loads(line);raw_count+=1;paths.add(entry['path'])
backup=runtime.backup_database(db_path,Path('/state/backups'))
with sqlite3.connect(backup) as c:
clean=c.execute('SELECT count(*) FROM api_keys').fetchone()[0]==0
good=c.execute('PRAGMA quick_check').fetchone()[0]=='ok'
rows=c.execute('SELECT count(*) FROM daily_activity').fetchone()[0]
with sqlite3.connect(db_path) as c:
plaintext=c.execute("SELECT count(*) FROM api_keys WHERE pass_token!=''").fetchone()[0]
print(json.dumps({'raw_health_responses_archived':raw_count,'only_allowed_health_paths':paths<=runtime.HEALTH_PATHS,'backup_quick_check_ok':good,'backup_excludes_api_credentials':clean,'backup_daily_rows':rows,'plaintext_token_rows':plaintext}))
else:
raise SystemExit('未知模式')
+34
View File
@@ -0,0 +1,34 @@
import asyncio
import json
from pathlib import Path
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
async def main():
root = Path(__file__).resolve().parents[2]
data = root / 'work/local-production-validation/state/data'
parameters = StdioServerParameters(
command=r'C:/Users/35861/AppData/Local/Programs/DockerDesktop/resources/bin/docker.exe',
args=['--context','desktop-linux','run','--rm','-i','--network','none','--read-only',
'--mount',f'type=bind,source={data},target=/health,readonly',
'--entrypoint','python','local/mi-fitness-nas:20261016','/opt/health/readonly.py'])
async with stdio_client(parameters) as (read, write):
async with ClientSession(read,write) as session:
await session.initialize()
tools = await session.list_tools()
result = await session.call_tool('get_health_records',arguments={'kind':'daily_activity','start_date':'2026-10-15','end_date':'2026-10-15'})
payload = result.structuredContent or json.loads(result.content[0].text)
denied = await session.call_tool('get_health_records',arguments={'kind':'api_keys','start_date':'2026-10-15','end_date':'2026-10-15'})
report = {'mcp_handshake':True,'tools':[t.name for t in tools.tools],
'read_only_annotation':all(t.annotations.readOnlyHint for t in tools.tools),
'real_daily_matches_phone':not result.isError and payload['data'][0]['steps']==4691 and payload['data'][0]['active_kcal']==596,
'credential_query_denied':bool(denied.isError),'container_network':'none','database_mount':'read_only'}
print(json.dumps(report))
(root/'work/deployment-tests/mcp-proof.json').write_text(json.dumps(report,indent=2),encoding='utf-8')
if report['tools']!=['get_health_records'] or not report['real_daily_matches_phone'] or not denied.isError:
raise SystemExit(1)
asyncio.run(main())
@@ -0,0 +1,21 @@
// 只读接口响应 -> 实际已部署脚本 -> 卡片数值,不读取浏览器凭据。
const fs=require('node:fs');
const vm=require('node:vm');
const payload=JSON.parse(fs.readFileSync(0,'utf8'));
const nodes=new Map();
const get=id=>{
if(!nodes.has(id)) nodes.set(id,{textContent:'--',innerHTML:'',value:'2026-10-16'});
return nodes.get(id);
};
const context=vm.createContext({document:{getElementById:get,querySelectorAll(){return [];},addEventListener(){}},localStorage:{getItem(){return '';},setItem(){}},responses:payload.responses,setTimeout(){},clearTimeout(){},setInterval(){},clearInterval(){},console});
vm.runInContext(payload.javascript,context);
vm.runInContext("showToast=()=>{}; api=async(method,path)=>responses[path.split('?')[0]];",context);
(async()=>{
await vm.runInContext('loadOverview()',context);
const cards=Object.fromEntries(['statSteps','statCalories','statDistance','statHr','statSleep','statSpo2','statStress'].map(id=>[id,get(id).textContent]));
const source=payload.responses['/api/summary'].data;
const known=source.find(row=>row.date==='2026-10-15');
const report={deployed_script_populates_cards:cards.statSteps!=='--'&&cards.statCalories!=='--'&&cards.statSleep!=='--',phone_confirmed_day_unchanged:known?.steps===4691&&known?.active_kcal===596,cards};
console.log(JSON.stringify(report));
if(!report.deployed_script_populates_cards||!report.phone_confirmed_day_unchanged)process.exitCode=1;
})().catch(()=>{console.log(JSON.stringify({card_inspection_error:true}));process.exitCode=1;});
@@ -0,0 +1,56 @@
"""仅本机 Linux 容器验收,使用临时虚构凭据,不连接 NAS。"""
import hashlib
import json
import os
from pathlib import Path
import subprocess
docker='C:/Users/35861/AppData/Local/Programs/DockerDesktop/resources/bin/docker.exe'
prefix=[docker,'--context','desktop-linux']
image='local/mi-fitness-nas:20261017-ui4'
name='mi-fitness-ui4-local-check'
env={**os.environ,'MI_FITNESS_WEB_PASSWORD':'Local-UI4-Validation-Only','MI_FITNESS_ADMIN_KEY':'local-validation-admin-key'}
def run(args,stdin=None):
p=subprocess.run(prefix+args,input=stdin.encode('utf-8') if stdin is not None else None,capture_output=True,env=env)
if p.returncode:raise RuntimeError(p.stderr.decode('utf-8')[-1800:])
return p.stdout.decode('utf-8')
check=r'''
import os,json,time,base64,urllib.request,urllib.error
token=base64.b64encode(('admin:'+os.environ['MI_FITNESS_WEB_PASSWORD']).encode()).decode()
opener=urllib.request.build_opener(urllib.request.ProxyHandler({}))
def get(path,auth=True):
r=urllib.request.Request('http://127.0.0.1:8322'+path,headers={'Authorization':'Basic '+token} if auth else {})
with opener.open(r,timeout=3) as response:return response.status,response.read().decode('utf-8')
for i in range(30):
try:
status,html=get('/');break
except OSError:time.sleep(.5)
else:raise RuntimeError('网页容器未就绪')
assert status==200 and '20261017-ui4' in html
for kind in ['activity','heartrate','sleep','workouts','body','spo2','stress','abnormal']:
assert 'id="healthView-'+kind+'"' in html
assets={}
for path,marker in [('/static/js/health-views.js','HealthViews'),('/static/js/health-ui.js','loadHealthView'),('/static/css/health.css','health-cards'),('/static/js/app.js','reloadActiveView')]:
status,text=get(path);assert status==200 and marker in text;assets[path]=len(text)
try:get('/',False)
except urllib.error.HTTPError as e:assert e.code==401
else:raise RuntimeError('匿名访问未被拦截')
assert 'inspector-collapsed' in html and 'id="inspectorPanel" hidden' in html
print(json.dumps({'authenticated_http':200,'anonymous_http':401,'query_views':8,'console_default_collapsed':True,'assets':assets}))
'''
try:
run(['run','-d','--name',name,'--network','none','--read-only','--tmpfs','/tmp:rw,noexec,nosuid,size=32m','--cap-drop','ALL','--security-opt','no-new-privileges:true','--memory','256m','--cpus','0.5','-e','MI_FITNESS_WEB_PASSWORD','-e','MI_FITNESS_ADMIN_KEY',image,'web'])
result=json.loads(run(['exec','-i',name,'python','-'],check))
config=json.loads(run(['inspect',name]))[0]
assert config['HostConfig']['ReadonlyRootfs'] and config['HostConfig']['NetworkMode']=='none'
assert not config['HostConfig']['PortBindings']
result['network']='none';result['host_ports']=[]
run(['exec','-i',name,'sh','-n'],Path('outputs/health-query-ui4/update-web.sh').read_text(encoding='utf-8'))
result['installer_shell_syntax']='ok'
result['image_id']=json.loads(run(['image','inspect',image]))[0]['Id']
Path('outputs/health-query-ui4/local-container-check.json').write_text(json.dumps(result,ensure_ascii=False,indent=2)+'\n',encoding='utf-8')
print(json.dumps(result,ensure_ascii=False))
finally:
# 只删除本次创建、含虚构配置且无持久卷的验收容器。
subprocess.run(prefix+['rm','-f',name],capture_output=True)
@@ -0,0 +1,56 @@
"""仅本机 Linux 容器验收,使用临时虚构凭据,不连接 NAS。"""
import hashlib
import json
import os
from pathlib import Path
import subprocess
docker='C:/Users/35861/AppData/Local/Programs/DockerDesktop/resources/bin/docker.exe'
prefix=[docker,'--context','desktop-linux']
image='local/mi-fitness-nas:20261017-ui5'
name='mi-fitness-ui5-local-check'
env={**os.environ,'MI_FITNESS_WEB_PASSWORD':'Local-UI4-Validation-Only','MI_FITNESS_ADMIN_KEY':'local-validation-admin-key'}
def run(args,stdin=None):
p=subprocess.run(prefix+args,input=stdin.encode('utf-8') if stdin is not None else None,capture_output=True,env=env)
if p.returncode:raise RuntimeError(p.stderr.decode('utf-8')[-1800:])
return p.stdout.decode('utf-8')
check=r'''
import os,json,time,base64,urllib.request,urllib.error
token=base64.b64encode(('admin:'+os.environ['MI_FITNESS_WEB_PASSWORD']).encode()).decode()
opener=urllib.request.build_opener(urllib.request.ProxyHandler({}))
def get(path,auth=True):
r=urllib.request.Request('http://127.0.0.1:8322'+path,headers={'Authorization':'Basic '+token} if auth else {})
with opener.open(r,timeout=3) as response:return response.status,response.read().decode('utf-8')
for i in range(30):
try:
status,html=get('/');break
except OSError:time.sleep(.5)
else:raise RuntimeError('网页容器未就绪')
assert status==200 and '20261017-ui5' in html
for kind in ['activity','heartrate','sleep','workouts','body','spo2','stress','abnormal']:
assert 'id="healthView-'+kind+'"' in html
assets={}
for path,marker in [('/static/js/health-views.js','HealthViews'),('/static/js/health-ui.js','loadHealthView'),('/static/css/health.css','health-cards'),('/static/js/app.js','reloadActiveView')]:
status,text=get(path);assert status==200 and marker in text;assets[path]=len(text)
try:get('/',False)
except urllib.error.HTTPError as e:assert e.code==401
else:raise RuntimeError('匿名访问未被拦截')
assert 'inspector-collapsed' in html and 'id="inspectorPanel" hidden' in html
print(json.dumps({'authenticated_http':200,'anonymous_http':401,'query_views':8,'console_default_collapsed':True,'assets':assets}))
'''
try:
run(['run','-d','--name',name,'--network','none','--read-only','--tmpfs','/tmp:rw,noexec,nosuid,size=32m','--cap-drop','ALL','--security-opt','no-new-privileges:true','--memory','256m','--cpus','0.5','-e','MI_FITNESS_WEB_PASSWORD','-e','MI_FITNESS_ADMIN_KEY',image,'web'])
result=json.loads(run(['exec','-i',name,'python','-'],check))
config=json.loads(run(['inspect',name]))[0]
assert config['HostConfig']['ReadonlyRootfs'] and config['HostConfig']['NetworkMode']=='none'
assert not config['HostConfig']['PortBindings']
result['network']='none';result['host_ports']=[]
run(['exec','-i',name,'sh','-n'],Path('outputs/health-reference-ui5/update-web.sh').read_text(encoding='utf-8'))
result['installer_shell_syntax']='ok'
result['image_id']=json.loads(run(['image','inspect',image]))[0]['Id']
Path('outputs/health-reference-ui5/local-container-check.json').write_text(json.dumps(result,ensure_ascii=False,indent=2)+'\n',encoding='utf-8')
print(json.dumps(result,ensure_ascii=False))
finally:
# 只删除本次创建、含虚构配置且无持久卷的验收容器。
subprocess.run(prefix+['rm','-f',name],capture_output=True)
@@ -0,0 +1,56 @@
"""仅本机 Linux 容器验收,使用临时虚构凭据,不连接 NAS。"""
import hashlib
import json
import os
from pathlib import Path
import subprocess
docker='C:/Users/35861/AppData/Local/Programs/DockerDesktop/resources/bin/docker.exe'
prefix=[docker,'--context','desktop-linux']
image='local/mi-fitness-nas:20261017-ui6'
name='mi-fitness-ui6-local-check'
env={**os.environ,'MI_FITNESS_WEB_PASSWORD':'Local-UI4-Validation-Only','MI_FITNESS_ADMIN_KEY':'local-validation-admin-key'}
def run(args,stdin=None):
p=subprocess.run(prefix+args,input=stdin.encode('utf-8') if stdin is not None else None,capture_output=True,env=env)
if p.returncode:raise RuntimeError(p.stderr.decode('utf-8')[-1800:])
return p.stdout.decode('utf-8')
check=r'''
import os,json,time,base64,urllib.request,urllib.error
token=base64.b64encode(('admin:'+os.environ['MI_FITNESS_WEB_PASSWORD']).encode()).decode()
opener=urllib.request.build_opener(urllib.request.ProxyHandler({}))
def get(path,auth=True):
r=urllib.request.Request('http://127.0.0.1:8322'+path,headers={'Authorization':'Basic '+token} if auth else {})
with opener.open(r,timeout=3) as response:return response.status,response.read().decode('utf-8')
for i in range(30):
try:
status,html=get('/');break
except OSError:time.sleep(.5)
else:raise RuntimeError('网页容器未就绪')
assert status==200 and '20261017-ui6' in html
for kind in ['activity','heartrate','sleep','workouts','body','spo2','stress','abnormal']:
assert 'id="healthView-'+kind+'"' in html
assets={}
for path,marker in [('/static/js/health-views.js','HealthViews'),('/static/js/health-ui.js','loadHealthView'),('/static/css/health.css','health-cards'),('/static/js/app.js','reloadActiveView')]:
status,text=get(path);assert status==200 and marker in text;assets[path]=len(text)
try:get('/',False)
except urllib.error.HTTPError as e:assert e.code==401
else:raise RuntimeError('匿名访问未被拦截')
assert 'inspector-collapsed' in html and 'id="inspectorPanel" hidden' in html
print(json.dumps({'authenticated_http':200,'anonymous_http':401,'query_views':8,'console_default_collapsed':True,'assets':assets}))
'''
try:
run(['run','-d','--name',name,'--network','none','--read-only','--tmpfs','/tmp:rw,noexec,nosuid,size=32m','--cap-drop','ALL','--security-opt','no-new-privileges:true','--memory','256m','--cpus','0.5','-e','MI_FITNESS_WEB_PASSWORD','-e','MI_FITNESS_ADMIN_KEY',image,'web'])
result=json.loads(run(['exec','-i',name,'python','-'],check))
config=json.loads(run(['inspect',name]))[0]
assert config['HostConfig']['ReadonlyRootfs'] and config['HostConfig']['NetworkMode']=='none'
assert not config['HostConfig']['PortBindings']
result['network']='none';result['host_ports']=[]
run(['exec','-i',name,'sh','-n'],Path('outputs/health-axis-ui6/update-web.sh').read_text(encoding='utf-8'))
result['installer_shell_syntax']='ok'
result['image_id']=json.loads(run(['image','inspect',image]))[0]['Id']
Path('outputs/health-axis-ui6/local-container-check.json').write_text(json.dumps(result,ensure_ascii=False,indent=2)+'\n',encoding='utf-8')
print(json.dumps(result,ensure_ascii=False))
finally:
# 只删除本次创建、含虚构配置且无持久卷的验收容器。
subprocess.run(prefix+['rm','-f',name],capture_output=True)
@@ -0,0 +1,58 @@
"""仅本机 Linux 容器验收,使用临时虚构凭据,不连接 NAS。"""
import hashlib
import json
import os
from pathlib import Path
import subprocess
docker='C:/Users/35861/AppData/Local/Programs/DockerDesktop/resources/bin/docker.exe'
prefix=[docker,'--context','desktop-linux']
image='local/mi-fitness-nas:20261017-ui7'
name='mi-fitness-ui7-local-check'
env={**os.environ,'MI_FITNESS_WEB_PASSWORD':'Local-UI4-Validation-Only','MI_FITNESS_ADMIN_KEY':'local-validation-admin-key'}
def run(args,stdin=None):
p=subprocess.run(prefix+args,input=stdin.encode('utf-8') if stdin is not None else None,capture_output=True,env=env)
if p.returncode:raise RuntimeError(p.stderr.decode('utf-8')[-1800:])
return p.stdout.decode('utf-8')
check=r'''
import os,json,time,base64,urllib.request,urllib.error
token=base64.b64encode(('admin:'+os.environ['MI_FITNESS_WEB_PASSWORD']).encode()).decode()
opener=urllib.request.build_opener(urllib.request.ProxyHandler({}))
def get(path,auth=True):
r=urllib.request.Request('http://127.0.0.1:8322'+path,headers={'Authorization':'Basic '+token} if auth else {})
with opener.open(r,timeout=3) as response:return response.status,response.read().decode('utf-8')
for i in range(30):
try:
status,html=get('/');break
except OSError:time.sleep(.5)
else:raise RuntimeError('网页容器未就绪')
assert status==200 and '20261017-ui7' in html
calendar=json.loads(get('/api/calendar?start=2026&end=2027')[1])
assert '2026' in calendar['years'] and '2027' not in calendar['years']
for kind in ['activity','heartrate','sleep','workouts','body','spo2','stress','abnormal']:
assert 'id="healthView-'+kind+'"' in html
assets={}
for path,marker in [('/static/js/health-views.js','HealthViews'),('/static/js/health-ui.js','loadHealthView'),('/static/css/health.css','health-cards'),('/static/js/app.js','reloadActiveView')]:
status,text=get(path);assert status==200 and marker in text;assets[path]=len(text)
try:get('/',False)
except urllib.error.HTTPError as e:assert e.code==401
else:raise RuntimeError('匿名访问未被拦截')
assert 'inspector-collapsed' in html and 'id="inspectorPanel" hidden' in html
print(json.dumps({'authenticated_http':200,'anonymous_http':401,'query_views':8,'console_default_collapsed':True,'assets':assets}))
'''
try:
run(['run','-d','--name',name,'--network','none','--read-only','--tmpfs','/tmp:rw,noexec,nosuid,size=32m','--cap-drop','ALL','--security-opt','no-new-privileges:true','--memory','256m','--cpus','0.5','-e','MI_FITNESS_WEB_PASSWORD','-e','MI_FITNESS_ADMIN_KEY',image,'web'])
result=json.loads(run(['exec','-i',name,'python','-'],check))
config=json.loads(run(['inspect',name]))[0]
assert config['HostConfig']['ReadonlyRootfs'] and config['HostConfig']['NetworkMode']=='none'
assert not config['HostConfig']['PortBindings']
result['network']='none';result['host_ports']=[]
run(['exec','-i',name,'sh','-n'],Path('outputs/health-calendar-ui7/update-web.sh').read_text(encoding='utf-8'))
result['installer_shell_syntax']='ok'
result['image_id']=json.loads(run(['image','inspect',image]))[0]['Id']
Path('outputs/health-calendar-ui7/local-container-check.json').write_text(json.dumps(result,ensure_ascii=False,indent=2)+'\n',encoding='utf-8')
print(json.dumps(result,ensure_ascii=False))
finally:
# 只删除本次创建、含虚构配置且无持久卷的验收容器。
subprocess.run(prefix+['rm','-f',name],capture_output=True)
@@ -0,0 +1,58 @@
const test=require('node:test');
const assert=require('node:assert/strict');
const vm=require('node:vm');
const fs=require('node:fs');
function fixture(){
const nodes=new Map();
const get=id=>{if(!nodes.has(id))nodes.set(id,{textContent:'--',innerHTML:'',value:id==='startDate'?'2026-09-18':id==='endDate'?'2026-10-17':'',classList:{add(){},remove(){}}});return nodes.get(id);};
const records=Array.from({length:3000},(_,i)=>({timestamp:`2026-10-16T09:${String(i%60).padStart(2,'0')}:00+08:00`,stress_score:30}));
const responses={summary:[{date:'2026-10-16',steps:6000,active_kcal:500,distance_m:4000}],
'heart-rate':[{bpm:62}],sleep:[{end_at:'2026-10-16T08:00:00+08:00',duration_minutes:450}],spo2:[{spo2_pct:97}],stress:records};
const copied=[];
const context=vm.createContext({document:{getElementById:get,querySelectorAll(){return [];},addEventListener(){}},
localStorage:{getItem(){return '';},setItem(){}},performance,console,setTimeout(){},setInterval(){},clearTimeout(){},clearInterval(){},
navigator:{clipboard:{writeText(text){copied.push(text);return Promise.resolve();}}},
fetch:async url=>({status:200,ok:true,text:async()=>JSON.stringify({status:'ok',data:responses[url.split('?')[0].split('/').pop()]||[]})})});
vm.runInContext(fs.readFileSync('work/mi-fitness-mcp-cn/src/mi_fitness_mcp/web_assets/static/js/health-ui.js','utf8'),context);
vm.runInContext(fs.readFileSync('work/mi-fitness-mcp-cn/src/mi_fitness_mcp/web_assets/static/js/app.js','utf8'),context);
vm.runInContext('showToast=()=>{}; renderCount=0; originalRender=renderJsonViewer; renderJsonViewer=(data)=>{renderCount++;originalRender(data);};',context);
return {context,get,copied};
}
test('overview paints one small response instead of five full datasets',async()=>{
const ui=fixture();await vm.runInContext('loadOverview()',ui.context);
assert.equal(vm.runInContext('renderCount',ui.context),1);
assert.equal(ui.get('statStress').textContent,'30');
assert.equal(ui.get('statSteps').textContent,(6000).toLocaleString());
assert.equal(vm.runInContext('state.lastRequestInfo.path.split("?")[0]',ui.context),'/api/summary');
});
test('silent and superseded requests do not overwrite inspector state',async()=>{
const ui=fixture();ui.get('inspectorBody').innerHTML='keep-current';
await vm.runInContext("api('GET','/api/stress',null,{inspect:false})",ui.context);
await vm.runInContext("api('GET','/api/stress',null,{isCurrent:()=>false})",ui.context);
assert.equal(ui.get('inspectorBody').innerHTML,'keep-current');
assert.equal(vm.runInContext('state.lastResponse',ui.context),null);
});
test('large detail preview is bounded while copy keeps every record',async()=>{
const ui=fixture();await vm.runInContext("api('GET','/api/stress')",ui.context);
const html=ui.get('inspectorBody').innerHTML;
assert.ok((html.match(/<span /g)||[]).length<=1000);
assert.match(html,/预览/);
assert.equal(vm.runInContext('state.lastResponse.data.length',ui.context),3000);
vm.runInContext('copyResponse()',ui.context);
assert.equal(JSON.parse(ui.copied[0]).data.length,3000);
vm.runInContext("filterInspectorJson('timestamp')",ui.context);
assert.ok((ui.get('inspectorBody').innerHTML.match(/<span /g)||[]).length<=1000);
});
test('a late overview response cannot overwrite a newer manual inspector query',async()=>{
const ui=fixture();
const pending={};
ui.context.fetch=url=>new Promise(resolve=>{pending[url]=resolve;});
const first=vm.runInContext("api('GET','/api/summary',null,{isCurrent:()=>true})",ui.context);
const second=vm.runInContext("api('GET','/api/coverage')",ui.context);
const response=marker=>({status:200,ok:true,text:async()=>JSON.stringify({marker})});
pending['/proxy/api/coverage'](response('new'));await second;
pending['/proxy/api/summary'](response('old'));await first;
assert.equal(vm.runInContext('state.lastResponse.marker',ui.context),'new');
assert.equal(vm.runInContext('state.lastRequestInfo.path',ui.context),'/api/coverage');
});
+119
View File
@@ -0,0 +1,119 @@
const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const vm = require('node:vm');
const file = path.resolve('work/mi-fitness-mcp-cn/src/mi_fitness_mcp/web_assets/static/js/app.js');
function dashboard(responses) {
const nodes = new Map();
const get = id => {
if (!nodes.has(id)) nodes.set(id, {textContent: '--', innerHTML: '', value: id === 'startDate' ? '2026-10-15' : '2026-10-16', classList: {add(){}, remove(){}}});
return nodes.get(id);
};
const context = vm.createContext({
localStorage: {getItem() {return '';}, setItem() {}},
document: {getElementById: get, querySelectorAll() {return [];}, addEventListener() {}},
responses, console, setTimeout() {}, clearTimeout() {}, setInterval() {}, clearInterval() {},
});
vm.runInContext(fs.readFileSync('work/mi-fitness-mcp-cn/src/mi_fitness_mcp/web_assets/static/js/health-ui.js','utf8'),context);
vm.runInContext(fs.readFileSync(file, 'utf8'), context);
vm.runInContext("showToast = () => {}; api = async (method, url) => responses[url.split('?')[0]] || {status:'ok',data:[]};", context);
return {context, get};
}
test('overview aggregates the selected range instead of taking the latest day', async () => {
const ui = dashboard({
'/api/summary': {status:'ok',data:[{date:'2026-10-16',steps:4063,active_kcal:250,distance_m:2385},{date:'2026-10-15',steps:4691,active_kcal:596,distance_m:2979}]},
'/api/heart-rate': {status:'ok',data:[{timestamp:'2026-10-16T09:00:00+08:00',bpm:62}]},
'/api/sleep': {status:'ok',data:[{end_at:'2026-10-16T08:54:00+08:00',duration_minutes:517},{end_at:'2026-10-15T07:55:00+08:00',duration_minutes:474}]},
'/api/spo2': {status:'ok',data:[{timestamp:'2026-10-16T09:00:00+08:00',spo2_pct:98}]},
'/api/stress': {status:'ok',data:[{timestamp:'2026-10-16T09:00:00+08:00',stress_score:0}]},
});
await vm.runInContext('loadOverview()',ui.context);
assert.equal(ui.get('statSteps').textContent, (8754).toLocaleString());
assert.equal(String(ui.get('statCalories').textContent),'846');
assert.equal(ui.get('statDistance').textContent,'5.36');
assert.equal(String(ui.get('statHr').textContent),'62');
assert.equal(ui.get('statSleep').textContent,'8.3');
assert.equal(ui.get('statSpo2').textContent,'98%');
assert.equal(String(ui.get('statStress').textContent),'0');
});
test('range averages include all samples and group naps by wake date without filling missing days', async () => {
const ui=dashboard({
'/api/heart-rate':{status:'ok',data:[{bpm:60},{bpm:70}]},
'/api/spo2':{status:'ok',data:[{spo2_pct:96},{spo2_pct:98}]},
'/api/stress':{status:'ok',data:[{stress_score:0},{stress_score:60}]},
'/api/sleep':{status:'ok',data:[{end_at:'2026-10-15T07:55:00+08:00',duration_minutes:474},{end_at:'2026-10-16T08:54:00+08:00',duration_minutes:517},{end_at:'2026-10-16T14:00:00+08:00',duration_minutes:60,is_nap:true}]},
});
ui.get('startDate').value='2026-10-01';
await vm.runInContext('loadOverview()',ui.context);
assert.equal(String(ui.get('statHr').textContent),'65');
assert.equal(ui.get('statSpo2').textContent,'97%');
assert.equal(String(ui.get('statStress').textContent),'30');
assert.equal(ui.get('statSleep').textContent,'8.8');
assert.match(ui.get('statSleepCoverage').textContent,/2/);
});
test('30 day preset is inclusive and refreshes the overview without manual reload', async () => {
const ui=dashboard({});
vm.runInContext(`Date = class extends Date { constructor(...args) { super(...(args.length ? args : ['2026-10-16T12:00:00+08:00'])); } }; calls=[]; api=async(method,path)=>{ calls.push(path); return {status:'ok',data:[]}; };`,ui.context);
await vm.runInContext("setDatePreset('30d')",ui.context);
assert.equal(ui.get('startDate').value,'2026-09-17');
assert.equal(ui.get('endDate').value,'2026-10-16');
const calls=vm.runInContext('calls',ui.context);
assert.equal(calls.length,5);
assert.ok(calls.every(path=>!path.includes('limit=1')));
await vm.runInContext("setDatePreset('yesterday')",ui.context);
assert.equal(ui.get('startDate').value,'2026-10-15');
assert.equal(ui.get('endDate').value,'2026-10-16');
});
test('empty date range clears old values rather than showing stale measurements', async () => {
const ui=dashboard({});
for (const id of ['statSteps','statCalories','statDistance','statHr','statSleep','statSpo2','statStress']) ui.get(id).textContent='old-value';
await vm.runInContext('loadOverview()',ui.context);
for (const id of ['statSteps','statCalories','statDistance','statHr','statSleep','statSpo2','statStress']) assert.equal(ui.get(id).textContent,'--');
});
test('daily table and metric chart consume response envelope without losing units', () => {
const ui=dashboard({});
ui.context.payload={status:'ok',data:[{date:'2026-10-15',steps:4691,active_kcal:596,distance_m:2979}]};
vm.runInContext('renderSummaryVisual(payload)',ui.context);
assert.match(ui.get('summaryVisualContainer').innerHTML,/596 kcal/);
assert.match(ui.get('summaryVisualContainer').innerHTML,/2\.98 km/);
ui.context.payload={status:'ok',data:[{date:'2026-10-15',value:4691}]};
vm.runInContext("renderMetricChart(payload,'steps')",ui.context);
assert.match(ui.get('metricChartContainer').innerHTML,/4,691/);
});
test('sleep timeline reads stage list and separately reported awake minutes', () => {
const ui=dashboard({});
ui.context.payload={status:'ok',data:[{end_at:'2026-10-16T08:54:00+08:00',duration_minutes:517,time_awake_minutes:85,stages:[{stage:'deep',minutes:120},{stage:'light',minutes:300},{stage:'rem',minutes:97}]}]};
vm.runInContext('renderSleepTimeline(payload)',ui.context);
const html=ui.get('sleepTimelineWrapper').innerHTML;
assert.match(html,/深睡 120m/);
assert.match(html,/浅睡 300m/);
assert.match(html,/REM 97m/);
assert.match(html,/清醒 85m/);
});
test('failed synchronization does not show a success message', async () => {
const ui=dashboard({});
vm.runInContext("notices=[]; showToast=(...args)=>notices.push(args); api=async()=>({status:'error'});",ui.context);
await vm.runInContext('triggerSync(false)',ui.context);
const notices=vm.runInContext('notices',ui.context);
assert.ok(!notices.some(args=>args[2]==='success'));
assert.ok(notices.some(args=>args[2]==='error'));
});
test('key list reads masked keys from envelope and safely renders labels', () => {
const ui=dashboard({});
ui.context.payload={status:'ok',data:[{key_masked:'mif_sk_abcde…1234',label:'<script>example</script>',user_id:'synthetic',created_at:'2026-10-16',revoked:false}]};
vm.runInContext('renderKeysTable(payload)',ui.context);
const html=ui.get('keysTableWrapper').innerHTML;
assert.match(html,/mif_sk_abcde…1234/);
assert.ok(!html.includes('<script>'));
assert.match(html,/revokeKeyByPrefix\('mif_sk_abcde'\)/);
});
@@ -0,0 +1,32 @@
const test=require('node:test');const assert=require('node:assert/strict');
const h=require('../mi-fitness-mcp-cn/src/mi_fitness_mcp/web_assets/static/js/health-views.js');
const range={start:'2026-10-01',end:'2026-10-30'};
test('体重40、血氧80、心率40为固定起点,上限继续按实际数据决定',()=>{
for(const [kind,row,min,max] of [
['body',{timestamp:'2026-10-01T08:00:00+08:00',weight_kg:87.3},40,90],
['spo2',{timestamp:'2026-10-01T08:00:00+08:00',spo2_pct:97},80,100],
['heartrate',{timestamp:'2026-10-01T08:00:00+08:00',bpm:77,sample_type:'passive'},40,80]]){
const m=h.build(kind,[row],range),layout=h.referenceLayout(m);
assert.equal(layout.min,min);assert.equal(layout.max,max);
assert.match(h.render(m),new RegExp('text-anchor="end">'+min+'<'));
}
});
test('参考线和柱高使用相同的新轴比例,170不撑大心率轴',()=>{
const m=h.build('body',[{timestamp:'2026-10-01T08:00:00+08:00',weight_kg:87.3}],range);
const line=h.referenceLayout(m).inside.find(r=>r.value===65);
assert.equal(line.lineY,93);
const hr=h.referenceLayout(h.build('heartrate',[{timestamp:'2026-10-01T08:00:00+08:00',bpm:77,sample_type:'passive'}],range));
assert.deepEqual(hr.outside.map(r=>r.value),[170]);assert.equal(hr.min,40);
});
test('低于下限的记录保留原值,图上明确标记且不产生负高度',()=>{
const m=h.build('heartrate',[{timestamp:'2026-10-01T08:00:00+08:00',bpm:35,sample_type:'resting'}],range);
const html=h.render(m);
assert.equal(m.records[0].bpm,35);assert.equal(m.cards[1].raw,35);
assert.match(html,/低于纵轴下限/);assert.ok(!/height="-/.test(html));
assert.ok(h.referenceLayout(m).max>40);
});
test('其他图表继续从0开始,压力低于25时仍保持紧凑纵轴',()=>{
for(const kind of ['activity','sleep','workouts','stress','abnormal'])assert.equal(h.referenceLayout(h.build(kind,[],range)).min,0);
const m=h.build('stress',[{timestamp:'2026-10-01T08:00:00+08:00',stress_score:18}],range);
assert.equal(h.referenceLayout(m).max,20);
});
@@ -0,0 +1,30 @@
const test=require('node:test');const assert=require('node:assert/strict');
const h=require('../mi-fitness-mcp-cn/src/mi_fitness_mcp/web_assets/static/js/health-views.js');
const calendar={'2026':{year:2026,days:[{date:'2026-09-20',isOffDay:false},{date:'2026-10-01',isOffDay:true},{date:'2026-10-10',isOffDay:false}],papers:['https://www.gov.cn/example']}};
test('补班周末为工作日,法定假日为非工作日,未知年份明确降级',()=>{
assert.equal(h.calendarDay('2026-09-20',calendar).off,false);
assert.equal(h.calendarDay('2026-10-01',calendar).off,true);
assert.equal(h.calendarDay('2026-10-10',calendar).off,false);
assert.equal(h.calendarDay('2026-10-11',calendar).off,true);
assert.equal(h.calendarDay('2027-01-02',calendar).provisional,true);
assert.equal(h.calendarDay('2027-01-02',calendar).off,true);
});
test('每天短横线高度相同,橙色工作日、绿色非工作日,缺记录日期也标记',()=>{
const m=h.build('activity',[{date:'2026-10-01',steps:1234}],{start:'2026-10-01',end:'2026-10-11',calendar});
const html=h.render(m),marks=[...html.matchAll(/<line class="health-calendar-mark"[^>]+/g)].map(x=>x[0]);
assert.equal(marks.length,11);assert.ok(marks.every(x=>x.includes('y1="207" y2="207"')));
assert.ok(marks[0].includes('stroke="#22a06b"'));assert.ok(marks[9].includes('stroke="#e78325"'));
assert.match(html,/工作日/);assert.match(html,/非工作日/);assert.equal(m.cards[0].raw,1234);
});
test('跨年查询分别判断日历,未收录年份给出提示,睡眠按北京时间醒来日',()=>{
const m=h.build('sleep',[{start_at:'2026-12-31T23:00:00+08:00',end_at:'2027-01-01T00:30:00Z',time_asleep_minutes:480}],{start:'2026-12-31',end:'2027-01-02',calendar});
assert.equal(m.points[1].date,'2027-01-01');assert.equal(m.points[1].values.reduce((a,b)=>a+(b||0),0),8);
assert.match(h.render(m),/2027 年调休安排未更新/);
const updated={...calendar,'2027':{year:2027,days:[{date:'2027-01-01',isOffDay:true}],papers:['https://www.gov.cn/example']}};
assert.equal(h.calendarDay('2027-01-01',updated).off,true);
assert.doesNotMatch(h.render(h.build('sleep',m.records,{...m.range,calendar:updated})),/调休安排未更新/);
});
test('多天合并柱不冒充单日属性,标记数量始终受限',()=>{
const html=h.render(h.build('activity',[{date:'2026-01-01',steps:100}],{start:'2026-01-01',end:'2026-12-31',calendar}));
assert.doesNotMatch(html,/class="health-calendar-mark"/);assert.match(html,/多日汇总不标注单日/);
});
@@ -0,0 +1,46 @@
const test=require('node:test');
const assert=require('node:assert/strict');
const vm=require('node:vm');
const fs=require('node:fs');
const HealthViews=require('../mi-fitness-mcp-cn/src/mi_fitness_mcp/web_assets/static/js/health-views.js');
function fixture(){
const nodes=new Map(),saved={};let callback;
const get=id=>{if(!nodes.has(id))nodes.set(id,{value:({startDate:'2026-10-15',endDate:'2026-10-17',napsSelect:'true'})[id]||'',textContent:'',innerHTML:'',checked:false,dataset:{},setAttribute(k,v){this[k]=v;},classList:{toggle(){},add(){},remove(){}}});return nodes.get(id);};
const c=vm.createContext({HealthViews,AbortController,console,performance,document:{getElementById:get,querySelectorAll(){return [];},addEventListener(name,fn){callback=fn;},documentElement:{setAttribute(){}}},localStorage:{getItem(k){return saved[k]||'';},setItem(k,v){saved[k]=v;}},setTimeout(){},setInterval(){},clearTimeout(){},clearInterval(){}});
vm.runInContext(fs.readFileSync('work/mi-fitness-mcp-cn/src/mi_fitness_mcp/web_assets/static/js/health-ui.js','utf8'),c);
vm.runInContext(fs.readFileSync('work/mi-fitness-mcp-cn/src/mi_fitness_mcp/web_assets/static/js/app.js','utf8'),c);
vm.runInContext('showToast=()=>{};',c);
return{c,get,saved};
}
test('切换到睡眠页自动查询并清空上个接口,修改日期会重新加载',async()=>{
const f=fixture();vm.runInContext('calls=[];api=async(method,path)=>{calls.push(path);return {status:"ok",data:[]};};state.lastResponse={old:true};',f.c);
await vm.runInContext("switchTab('sleep')",f.c);
assert.equal(vm.runInContext('state.lastResponse',f.c),null);
assert.equal(vm.runInContext('calls.length',f.c),1);
assert.match(f.get('healthView-sleep').innerHTML,/睡眠分析明细/);
await vm.runInContext("setDatePreset('7d')",f.c);
assert.equal(vm.runInContext('calls.length',f.c),2);
});
test('后返回的旧日期请求不会覆盖新日期结果或控制台',async()=>{
const f=fixture();const waits=[];f.c.pending=waits;
vm.runInContext('state.activeTab="sleep";api=(method,path,body,view)=>new Promise(resolve=>pending.push({resolve,view}));',f.c);
const first=vm.runInContext('querySleep()',f.c);
f.get('startDate').value='2026-10-16';
const second=vm.runInContext('querySleep()',f.c);
assert.equal(waits[0].view.isCurrent(),false);
waits[1].resolve({status:'ok',data:[{end_at:'2026-10-16T08:00:00+08:00',time_asleep_minutes:400}]});await second;
waits[0].resolve({status:'ok',data:[{end_at:'2026-10-15T08:00:00+08:00',time_asleep_minutes:100}]});await first;
assert.equal(vm.runInContext('state.healthViews.sleep.model.records[0].time_asleep_minutes',f.c),400);
});
test('失败与无数据分别显示,控制台开关会记忆选择',async()=>{
const f=fixture();vm.runInContext('state.activeTab="spo2";api=async()=>null;',f.c);
await vm.runInContext('querySpo2()',f.c);
assert.match(f.get('healthView-spo2').innerHTML,/查询失败/);
vm.runInContext('api=async()=>({status:"ok",data:[]});',f.c);
await vm.runInContext('querySpo2()',f.c);
assert.match(f.get('healthView-spo2').innerHTML,/未保存/);
vm.runInContext('setInspectorCollapsed(true)',f.c);
assert.equal(f.saved.mi_fitness_inspector_collapsed,'true');
assert.equal(f.get('inspectorToggle').textContent,'展开控制台');
assert.equal(f.get('inspectorToggle')['aria-expanded'],'false');
});
@@ -0,0 +1,54 @@
const test=require('node:test');
const assert=require('node:assert/strict');
const h=require('../mi-fitness-mcp-cn/src/mi_fitness_mcp/web_assets/static/js/health-views.js');
const range={start:'2026-10-01',end:'2026-10-30'};
const stress=values=>values.map((v,i)=>({timestamp:`2026-10-${String(i+1).padStart(2,'0')}T08:00:00+08:00`,stress_score:v,level:'old-source-level'}));
test('所有压力低于25时保持20以内的纵轴,25/50/80仅在上方标注,平均值在图内',()=>{
const m=h.build('stress',stress([5,12,18]),range);
assert.ok(Array.isArray(m.references),'参考线尚未实现');
const layout=h.referenceLayout(m);
assert.equal(layout.max,20);
assert.deepEqual(layout.outside.map(r=>r.value),[25,50,80]);
assert.deepEqual(layout.inside.map(r=>r.value),[35/3]);
const html=h.render(m);
assert.equal((html.match(/class="health-reference"/g)||[]).length,1);
assert.match(html,/平均/);assert.match(html,/放松 1–25/);assert.match(html,/重度 81–100/);
});
test('58的压力数据纵轴至60,25/50在图内,80在上方;25恰在上界时不隐藏',()=>{
const m=h.build('stress',stress([10,58]),range),layout=h.referenceLayout(m);
assert.equal(layout.max,60);
assert.ok(layout.inside.some(r=>r.value===25));assert.ok(layout.inside.some(r=>r.value===50));
assert.deepEqual(layout.outside.map(r=>r.value),[80]);
const edge=h.referenceLayout(h.build('stress',stress([25]),range));
assert.equal(edge.max,25);assert.ok(edge.inside.some(r=>r.key==='stress-25'));
});
test('六类页面平均值按日期和筛选结果计算,心率170不撑高纵轴',()=>{
const activity=h.build('activity',[{date:'2026-10-01',steps:4000},{date:'2026-10-02',steps:8000}],range);
assert.equal(activity.references.find(r=>r.key==='average').value,6000);
assert.equal(activity.references.find(r=>r.key==='fixed').value,7000);
const seven=h.build('activity',[{date:'2026-10-01',steps:4000},{date:'2026-10-02',steps:8000}],{start:'2026-10-02',end:'2026-10-08'});
assert.equal(seven.references.find(r=>r.key==='average').value,8000);
const hr=h.build('heartrate',[{timestamp:'2026-10-01T08:00:00+08:00',bpm:60,sample_type:'resting'},{timestamp:'2026-10-01T08:01:00+08:00',bpm:100,sample_type:'passive'},{timestamp:'2026-10-01T08:02:00+08:00',bpm:110,sample_type:'passive'}],range);
assert.equal(hr.references.find(r=>r.key==='average').value,90);
assert.equal(hr.references.find(r=>r.key==='resting-average').value,60);
assert.ok(h.referenceLayout(hr).max<170);assert.equal(h.referenceLayout(hr).outside[0].value,170);
const sleep=h.build('sleep',[{end_at:'2026-10-01T08:00:00+08:00',time_asleep_minutes:420},{end_at:'2026-10-01T14:00:00+08:00',time_asleep_minutes:30,is_nap:1},{end_at:'2026-10-02T08:00:00+08:00',time_asleep_minutes:510}],range);
assert.equal(sleep.references.find(r=>r.key==='average').value,8);assert.equal(sleep.references.find(r=>r.key==='fixed').value,8);
const body=h.build('body',[{timestamp:'2026-10-01T08:00:00+08:00',weight_kg:80},{timestamp:'2026-10-02T08:00:00+08:00',weight_kg:84}],range);
assert.equal(body.references.find(r=>r.key==='average').value,82);assert.equal(body.references.find(r=>r.key==='fixed').value,65);
const spo2=h.build('spo2',[{timestamp:'2026-10-01T08:00:00+08:00',spo2_pct:94},{timestamp:'2026-10-02T08:00:00+08:00',spo2_pct:98}],range);
assert.equal(spo2.references.length,1);assert.equal(spo2.references[0].value,96);
});
test('压力四档含边界,0保持未分类;筛选和导出不改源等级',()=>{
for(const [v,name]of [[1,'放松'],[25,'放松'],[26,'轻度'],[50,'轻度'],[51,'中度'],[80,'中度'],[81,'重度'],[100,'重度'],[0,'未分类']])assert.equal(h.pressureBand(v).label,name);
const m=h.build('stress',stress([0,25,26,50,51,80,81,100]),{...range,pressureLevel:'moderate',limit:1});
assert.equal(m.records.length,1);assert.equal(m.records[0].stress_score,80);assert.equal(m.cards[0].raw,80);
assert.match(h.csv(m),/old-source-level/);assert.match(h.csv(m),/压力等级(当前分界)/);assert.match(h.csv(m),/中度/);assert.match(h.detail(m),/中度/);
});
test('重叠均值和固定值的标注分开摆放;无数值不生成伪均值',()=>{
const m=h.build('sleep',[{end_at:'2026-10-01T08:00:00+08:00',time_asleep_minutes:480}],range);
const layout=h.referenceLayout(m);
assert.equal(layout.inside.length,2);assert.ok(Math.abs(layout.inside[0].labelY-layout.inside[1].labelY)>=18);
const empty=h.build('stress',[],range);assert.ok(!empty.references.some(r=>r.key==='average'));assert.ok(!h.render(empty).includes('class="health-reference"'));
const zero=h.build('stress',stress([0]),range);assert.equal(zero.references.find(r=>r.key==='average').value,0);
});
@@ -0,0 +1,48 @@
const test=require('node:test');
const assert=require('node:assert/strict');
const fs=require('node:fs');
const file='work/mi-fitness-mcp-cn/src/mi_fitness_mcp/web_assets/static/js/health-views.js';
const load=()=>{assert.ok(fs.existsSync(file),'健康数据展示模块尚未实现');return require('../../'+file);};
const range={start:'2026-10-15',end:'2026-10-17'};
test('睡眠按北京时间醒来日汇总并包含小睡,不以缺失补零',()=>{
const h=load();const m=h.build('sleep',[
{start_at:'2026-10-14T23:00:00+08:00',end_at:'2026-10-15T07:00:00+08:00',time_asleep_minutes:474,duration_minutes:480,time_awake_minutes:6,stages:[{stage:'deep',minutes:120},{stage:'light',minutes:300},{stage:'rem',minutes:54}]},
{end_at:'2026-10-15T14:00:00+08:00',time_asleep_minutes:30,is_nap:1},
{end_at:'2026-10-16T00:37:00Z',time_asleep_minutes:517}],range);
assert.equal(m.cards[0].raw,510.5);assert.equal(m.cards[1].raw,2);assert.equal(m.cards[3].raw,1);
assert.deepEqual(m.points.map(p=>p.values.reduce((s,x)=>s+(x??0),0)),[8.4,517/60,0]);
assert.equal(m.points[2].missing,true);
});
test('心率均值不混合静息和被动记录,0压力保留且缺失不补零',()=>{
const h=load();const m=h.build('heartrate',[{timestamp:'2026-10-15T08:00:00+08:00',bpm:60,sample_type:'resting'},{timestamp:'2026-10-15T08:01:00+08:00',bpm:100,sample_type:'passive'}],range);
assert.equal(m.cards[0].raw,100);assert.equal(m.cards[1].raw,60);
assert.deepEqual(m.points[0].values,[100,60,null]);
const s=h.build('stress',[{timestamp:'2026-10-15T08:00:00+08:00',stress_score:0}],range);
assert.equal(s.cards[0].raw,0);assert.equal(s.points[1].missing,true);
});
test('八类页面都有四项概览、图表和分页明细,空记录不代表正常',()=>{
const h=load();for(const kind of h.kinds){
const m=h.build(kind,[],range);assert.equal(m.cards.length,4);assert.ok(m.columns.length>=3);
const html=h.render(m,1);assert.match(html,/health-trend/);assert.match(html,/health-details/);assert.match(html,/未保存/);
}
});
test('大数据只渲染一页,图表点数有上限,完整导出不会漏行且阻止表格公式注入',()=>{
const h=load();const rows=Array.from({length:10000},(_,i)=>({timestamp:'2026-10-15T08:00:00+08:00',bpm:60+i%20,sample_type:i===0?'=DANGEROUS()':'passive'}));
const m=h.build('heartrate',rows,range);const html=h.render(m,1);
assert.equal((html.match(/<tr>/g)||[]).length,11);assert.ok(html.length<22000);
assert.ok(m.points.length<=120);assert.equal(m.records.length,10000);
const csv=h.csv(m);assert.equal(csv.split('\r\n').length,10001);assert.match(csv,/'=DANGEROUS\(\)/);
const last=h.render(m,1000);assert.match(last,/1000 \/ 1000/);
});
test('源文本转义,最新体重按时间确定,有限记录明确标注',()=>{
const h=load();const m=h.build('body',[{timestamp:'2026-10-16T09:00:00+08:00',weight_kg:87.3},{timestamp:'2026-10-15T09:00:00+08:00',weight_kg:88}],{...range,note:'仅返回最新一条'});
assert.equal(m.cards[0].raw,87.3);assert.match(h.render(m),/仅返回最新一条/);
const a=h.build('workouts',[{start_at:'2026-10-15T08:00:00+08:00',activity_type:'<img src=x onerror=alert(1)>'}],range);
assert.ok(!h.render(a).includes('<img'));
});
test('长范围体重分桶选桶内最近测量,卡片明细和图表都按北京时间日期过滤',()=>{
const h=load();const m=h.build('body',[{timestamp:'2026-01-01T09:00:00+08:00',weight_kg:80},{timestamp:'2026-01-02T09:00:00+08:00',weight_kg:75}],{start:'2026-01-01',end:'2026-12-31'});
assert.equal(m.points[0].values[0],75);
const boundary=h.build('heartrate',[{timestamp:'2026-10-14T20:00:00Z',bpm:70,sample_type:'passive'},{timestamp:'2026-10-17T20:00:00Z',bpm:110,sample_type:'passive'}],range);
assert.equal(boundary.records.length,1);assert.equal(boundary.cards[0].raw,70);
});
@@ -0,0 +1,48 @@
"""同一台本机内把已授权凭据从 Windows 密钥环传入加密容器;从不输出凭据。"""
import json
import sqlite3
import subprocess
import sys
from pathlib import Path
root = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(root / 'work/mi-fitness-mcp-cn/src'))
from mi_fitness_mcp.security import load_api_key_secret
with sqlite3.connect(root / 'work/local-validation/state/mi_fitness.db') as conn:
rows = conn.execute('SELECT key,user_id,region,pass_token FROM api_keys WHERE revoked=0').fetchall()
if len(rows) != 1 or rows[0][3]:
raise SystemExit('本机授权状态不符合预期,停止')
key, user, region, _ = rows[0]
token = load_api_key_secret(key)
if not token:
raise SystemExit('无法读取本机已授权凭据,停止')
code = r'''
import sys,json,sqlite3
sys.path.insert(0,'/opt/health')
import runtime
runtime.setup_credentials()
from mi_fitness_mcp.api import _create_key_record
from mi_fitness_mcp.storage import Database
from mi_fitness_mcp.security import load_api_key_secret
data=json.load(sys.stdin)
db=Database('/state/data/mi_fitness.db')
with db._get_connection() as c:
if c.execute('SELECT count(*) FROM api_keys WHERE revoked=0').fetchone()[0]:
raise SystemExit('目标不是空授权库,停止')
new_key=_create_key_record(db,data['user_id'],data['pass_token'],data['region'],'local-linux-verification')
with db._get_connection() as c:
clear=c.execute("SELECT count(*) FROM api_keys WHERE pass_token!=''").fetchone()[0]
encrypted=load_api_key_secret(new_key)==data['pass_token']
not_plain=data['pass_token'] not in open('/state/credentials/encrypted-keyring.cfg',encoding='utf-8').read()
print(json.dumps({'active_authorizations':1,'plaintext_account_token_rows':clear,'encrypted_keyring_round_trip':encrypted,'token_absent_from_keyring_plaintext':not_plain}))
if clear or not encrypted or not not_plain: raise SystemExit(1)
'''
docker = r'C:/Users/35861/AppData/Local/Programs/DockerDesktop/resources/bin/docker.exe'
result = subprocess.run([docker,'--context','desktop-linux','exec','-i','mi-fitness-production-local-backend-1','python','-c',code],input=json.dumps({'user_id':user,'pass_token':token,'region':region}),capture_output=True,text=True,encoding='utf-8',timeout=60)
if result.returncode:
# 错误输出可能包含第三方异常上下文,因此不直接转发。
raise SystemExit('容器授权导入未通过,错误内容留在进程内,未输出凭据')
report = json.loads(result.stdout)
print(json.dumps(report))
(root / 'work/deployment-tests/linux-credential-proof.json').write_text(json.dumps(report,indent=2),encoding='utf-8')
@@ -0,0 +1,28 @@
"""创建本机专用合成数据库;运行中不调用云端接口。"""
import sqlite3
import json
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT / 'work/mi-fitness-mcp-cn/src'))
from mi_fitness_mcp.models import DailyActivity
from mi_fitness_mcp.storage import Database
target = ROOT / 'work/audit2-container-validation'
target.mkdir(exist_ok=True)
(target / 'state/data').mkdir(parents=True, exist_ok=True)
(target / 'state/credentials').mkdir(parents=True, exist_ok=True)
(target / 'state/config/mi-fitness-mcp').mkdir(parents=True, exist_ok=True)
(target / 'state/config/mi-fitness-mcp/config.json').write_text(json.dumps({
'mode': 'mi_fitness_cloud', 'region': 'cn', 'timezone': 'Asia/Shanghai',
'database_path': '/state/data/mi_fitness.db', 'auto_sync_on_start': False}), encoding='utf-8')
(target / 'keyring_master').write_text('synthetic-only-keyring-unlock-value-20261017', encoding='utf-8')
db = Database(target / 'state/data/mi_fitness.db')
db.insert_daily_activity(DailyActivity(id='synthetic-day', provider='mi_fitness', source_type='cloud_session',
user_id='synthetic-owner', date='2026-10-15', steps=4691, active_kcal=596, distance_m=2979))
with db._get_connection() as conn:
conn.execute('CREATE TABLE IF NOT EXISTS api_keys (key TEXT PRIMARY KEY,label TEXT,user_id TEXT NOT NULL,pass_token TEXT NOT NULL,region TEXT NOT NULL DEFAULT \'cn\',created_at TEXT NOT NULL,last_used_at TEXT,revoked INTEGER NOT NULL DEFAULT 0)')
conn.execute("INSERT OR IGNORE INTO api_keys(key,label,user_id,pass_token,region,created_at) VALUES ('mif_sk_syntheticlocal','synthetic only','synthetic-owner','synthetic-token','cn','2026-10-17')")
conn.commit()
print('Synthetic container fixture ready; no real health records or credentials used.')
@@ -0,0 +1,95 @@
"""第二轮补充审查复现;仅本机临时 SQLite 与合成账号,不访问网络。"""
import asyncio
import json
import sys
import tempfile
from datetime import datetime, timedelta, timezone
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT / 'work/mi-fitness-mcp-cn/src'))
from mi_fitness_mcp.adapters.mi_fitness_cloud import MiFitnessCloudAdapter
from mi_fitness_mcp.models import StressSample, Workout
from mi_fitness_mcp.services.query_service import QueryService
from mi_fitness_mcp.services.sync_service import SyncService
from mi_fitness_mcp.storage import Database
CN = timezone(timedelta(hours=8))
async def daily_for(owner, steps):
adapter = MiFitnessCloudAdapter(user_id=owner, pass_token='synthetic', region='cn')
adapter._connected = True
adapter._client = object()
async def reports(key, start, end):
value = {'steps': steps, 'distance': steps} if key == 'steps' else {'calories': 100}
return [{'time': int(datetime(2026, 10, 16, tzinfo=CN).timestamp()), 'value': value}]
adapter._fetch_daily_reports = reports
return [row async for row in adapter.iter_daily_activity('2026-10-16', '2026-10-16')][0]
async def check(directory):
db = Database(directory / 'synthetic.sqlite')
first = await daily_for('synthetic-A', 1111)
second = await daily_for('synthetic-B', 2222)
db.insert_daily_activity(first)
db.insert_daily_activity(second)
a = QueryService(db, 'synthetic-A').get_daily_summaries('2026-10-16', '2026-10-16')
b = QueryService(db, 'synthetic-B').get_daily_summaries('2026-10-16', '2026-10-16')
assert first.id == second.id
assert a[0]['steps'] == 2222 and b == []
db.update_sync_state('heart_rate', datetime(2026, 10, 16, 9, tzinfo=CN))
adapter = MiFitnessCloudAdapter(user_id='synthetic-A', pass_token='synthetic', region='cn')
adapter._connected = True
adapter._client = object()
try:
await SyncService(adapter, db).sync_data_type('heart_rate', end_date='2026-10-16')
except ValueError as exc:
assert str(exc) == 'start_date must not be after end_date'
sync_error = str(exc)
else:
raise AssertionError('Expected same-day incremental rejection was not reproduced')
db.update_sync_state('daily_activity', datetime(2026, 10, 15, tzinfo=CN))
seen_ranges = []
class SecondAccountAdapter:
user_id = 'synthetic-B'
def is_connected(self): return True
async def iter_daily_activity(self, start, end):
seen_ranges.append((start, end))
if False:
yield None
await SyncService(SecondAccountAdapter(), db).sync_data_type('daily_activity', end_date='2026-10-16')
assert seen_ranges[0][0] == '2026-10-15'
for index, hour in enumerate((1, 9)):
db.insert_stress_sample(StressSample(id=f'synthetic-stress-{index}', provider='mi_fitness',
source_type='cloud_session', user_id='synthetic-A', timestamp=datetime(2026, 10, 16, hour, tzinfo=CN),
stress_score=30, level='medium'))
service = QueryService(db, 'synthetic-A')
samples = service.get_stress_samples('2026-10-16', '2026-10-16')
coverage = next(row for row in service.get_data_coverage() if row['data_type'] == 'stress')
assert len(samples) == 2
assert coverage['first_date'] == '2026-10-15' and coverage['days_with_data'] == 2
exercise = Workout(id='synthetic-workout', workout_id='synthetic-workout', provider='mi_fitness',
source_type='cloud_session', user_id='synthetic-A', activity_type='running',
start_at=datetime(2026, 10, 16, 9, tzinfo=CN), end_at=datetime(2026, 10, 16, 9, 30, tzinfo=CN),
duration_minutes=30)
db.insert_workout(exercise)
db.insert_workout(exercise.model_copy(update={'total_steps': 4000, 'avg_pace_sec_per_km': 300}))
updated = service.get_workouts('2026-10-16', '2026-10-16')[0]
assert updated['total_steps'] is None and updated['avg_pace_sec_per_km'] is None
return {
'cross_account_collision': {'owner_A_steps_after_owner_B_sync': a[0]['steps'], 'owner_B_records': len(b)},
'same_day_incremental_sync': sync_error,
'shared_cursor': {'new_account_first_sync_start': seen_ranges[0][0], 'expected_without_another_account_cursor': '2026-09-17'},
'coverage_timezone_mismatch': {'requested_day_records': len(samples), 'first_date': coverage['first_date'], 'days_with_data': coverage['days_with_data']},
'workout_correction_lost': {'supplied_steps': 4000, 'stored_steps': updated['total_steps'], 'supplied_pace': 300, 'stored_pace': updated['avg_pace_sec_per_km']},
}
if __name__ == '__main__':
with tempfile.TemporaryDirectory(prefix='health-edge-review-') as temporary:
print(json.dumps(asyncio.run(check(Path(temporary))), indent=2))
@@ -0,0 +1,55 @@
"""本机隔离复现审查发现;仅使用合成数据,不连接 NAS 或小米云。"""
import asyncio
import json
import sys
import tempfile
from datetime import datetime, timezone
from pathlib import Path
from unittest.mock import patch
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT / 'work/mi-fitness-mcp-cn/src'))
from mi_fitness_mcp.adapters.mi_fitness_cloud import MiFitnessCloudAdapter
from mi_fitness_mcp.models import BodyMeasurement
from mi_fitness_mcp.storage import Database
from mi_fitness_mcp.web import create_app
async def pagination():
adapter = MiFitnessCloudAdapter(user_id='synthetic', pass_token='synthetic', region='cn')
async def request(*args):
return {'data_list': [{'time': 123}], 'has_more': True}
adapter._request = request
rows = await adapter._fetch_key('heart_rate', '2026-10-15', '2026-10-16')
return {'has_more_without_cursor_silently_accepted': len(rows) == 1}
def corrections():
with tempfile.TemporaryDirectory(prefix='health-review-') as directory:
db = Database(Path(directory) / 'synthetic.sqlite')
first = BodyMeasurement(id='synthetic-weight', provider='mi_fitness',
source_type='cloud_session', user_id='synthetic', timestamp=datetime.now(timezone.utc),
weight_kg=87.3, bone_mass_kg=None, basal_metabolism_kcal=None)
db.insert_body_measurement(first)
db.insert_body_measurement(first.model_copy(update={'bone_mass_kg': 3.1, 'basal_metabolism_kcal': 1800}))
record = db.query_body_measurements('synthetic', '2000-01-01', '2100-01-01')[0]
return {'late_body_fields_not_updated': record['bone_mass_kg'] is None and record['basal_metabolism_kcal'] is None}
def proxy_privilege():
seen = []
class FakeClient:
def __init__(self, **kwargs):
pass
def request(self, **kwargs):
seen.append(kwargs)
import httpx
return httpx.Response(200, json={'status': 'ok'})
with patch('mi_fitness_mcp.web.httpx.Client', FakeClient):
app = create_app(backend_url='http://backend:8321', admin_key='SYNTHETIC_ADMIN')
response = app.test_client().delete('/proxy/api/auth/keys/mif_sk_demo')
return {'unauthenticated_browser_request_gets_admin_header': response.status_code == 200 and seen[0]['headers'].get('X-Admin-Key') == 'SYNTHETIC_ADMIN'}
if __name__ == '__main__':
print(json.dumps({**asyncio.run(pagination()), **corrections(), **proxy_privilege()}, indent=2))
@@ -0,0 +1,40 @@
"""仅本机界面核验:使用固定示例响应,不连接 NAS 或小米云。"""
import logging
import sys
from pathlib import Path
root=Path(__file__).resolve().parents[2]
sys.path.insert(0,str(root/'work/mi-fitness-mcp-cn/src'))
from flask import jsonify, request
from mi_fitness_mcp.web import create_app
logging.getLogger('werkzeug').disabled=True
app=create_app(backend_url='http://127.0.0.1:1')
fixtures={
'summary':[{'date':'2026-10-15','steps':4691,'active_kcal':596,'distance_m':2979},{'date':'2026-10-16','steps':4063,'active_kcal':250,'distance_m':2385}],
'heart-rate':[{'timestamp':'2026-10-16T09:00:00+08:00','bpm':62},{'timestamp':'2026-10-15T09:00:00+08:00','bpm':58}],
'sleep':[{'end_at':'2026-10-16T08:54:00+08:00','duration_minutes':517},{'end_at':'2026-10-15T07:55:00+08:00','duration_minutes':474}],
'spo2':[{'timestamp':'2026-10-16T09:00:00+08:00','spo2_pct':98},{'timestamp':'2026-10-15T09:00:00+08:00','spo2_pct':96}],
'stress':[{'timestamp':'2026-10-16T09:00:00+08:00','stress_score':0},{'timestamp':'2026-10-15T09:00:00+08:00','stress_score':60}],
}
@app.before_request
def local_fixture():
if request.path=='/api/proxy_status':
return jsonify({'backend_reachable':True,'backend_latency_ms':1})
if request.path.startswith('/proxy/api/'):
rows=fixtures.get(request.path.rsplit('/',1)[-1],[])
return jsonify({'status':'ok','count':len(rows),'data':rows})
@app.after_request
def exercise_preset(response):
if response.mimetype=='text/html' and request.args.get('preset')=='30d':
html=response.get_data(as_text=True)
clock="<script>const RealTestDate=Date; window.Date=class extends RealTestDate {constructor(...args){super(...(args.length?args:['2026-10-16T12:00:00+08:00']));}static now(){return new RealTestDate('2026-10-16T12:00:00+08:00').getTime();}};</script>"
click="<script>document.addEventListener('DOMContentLoaded',()=>setTimeout(()=>document.getElementById('preset-30d').click(),100));</script>"
html=html.replace('<script src="/static/js/app.js',clock+'<script src="/static/js/app.js',1)
html=html.replace('</body>',click+'</body>')
response.set_data(html)
return response
app.run(host='127.0.0.1',port=18362,debug=False,use_reloader=False)
@@ -0,0 +1,46 @@
"""本机界面验收用合成数据,不连接 NAS 或小米云。"""
import datetime as dt
import logging
import sys
from pathlib import Path
root=Path(__file__).resolve().parents[2]
sys.path.insert(0,str(root/'work/mi-fitness-mcp-cn/src'))
from flask import jsonify,request
from mi_fitness_mcp.web import create_app
logging.getLogger('werkzeug').disabled=True
app=create_app(backend_url='http://127.0.0.1:1')
app.config['TEMPLATES_AUTO_RELOAD']=True
days=[dt.date(2026,9,18)+dt.timedelta(days=i) for i in range(30)]
fixtures={'summary':[],'sleep':[],'heart-rate':[],'spo2':[],'stress':[], 'workouts':[], 'body-measurements':[], 'abnormal-heart-beat':[]}
for i,day in enumerate(days):
key=day.isoformat()
fixtures['summary'].append({'date':key,'steps':3500+i*173%7000,'distance_m':3000+i*101,'active_kcal':300+i*11})
if i<29:
minutes=390+i*17%160
fixtures['sleep'].append({'start_at':f'{day-dt.timedelta(days=1)}T23:10:00+08:00','end_at':key+'T07:20:00+08:00','time_asleep_minutes':minutes,'duration_minutes':minutes,'time_awake_minutes':12,'sleep_score':80+i%9,'is_nap':False,'stages':[{'stage':'deep','minutes':120+i%25},{'stage':'light','minutes':minutes-210-i%25},{'stage':'rem','minutes':90}]})
fixtures['heart-rate'].append({'timestamp':key+'T08:00:00+08:00','bpm':55+i%17,'sample_type':'resting'})
for j in range(300):
time=(dt.datetime.combine(day,dt.time(9))+dt.timedelta(minutes=j)).isoformat()+'+08:00'
fixtures['heart-rate'].append({'timestamp':time,'bpm':60+(i+j)%35,'sample_type':'passive'})
if j%4==0:fixtures['spo2'].append({'timestamp':time,'spo2_pct':95+j%5})
if j%3==0:fixtures['stress'].append({'timestamp':time,'stress_score':j%60,'level':'low' if j%60<30 else 'medium'})
fixtures['body-measurements']=[{'timestamp':'2026-10-15T09:33:00+08:00','weight_kg':87.3,'bmi':28.2,'body_fat_pct':26.3,'muscle_mass_kg':69.8,'water_pct':51.7,'bone_mass_kg':3.4}]
@app.before_request
def fixture():
if request.path=='/api/proxy_status':return jsonify({'backend_reachable':True,'backend_latency_ms':2})
if request.path.startswith('/proxy/api/'):
rows=fixtures.get(request.path.rsplit('/',1)[-1],[])
lo,hi=request.args.get('start_date',''),request.args.get('end_date','9999')
rows=[r for r in rows if lo<=str(r.get('date') or r.get('timestamp') or r.get('end_at') or r.get('start_at'))[:10]<=hi]
sample=request.args.get('sample_type')
if sample:rows=[r for r in rows if r.get('sample_type')==sample]
level=request.args.get('level')
if level:rows=[r for r in rows if r.get('level')==level]
if request.args.get('latest_only')=='true':rows=rows[-1:]
if request.args.get('limit'):rows=rows[:int(request.args['limit'])]
return jsonify({'status':'ok','count':len(rows),'data':rows})
app.run(host='127.0.0.1',port=18364,debug=False,use_reloader=False)
@@ -0,0 +1,52 @@
"""仅本机浏览器性能复现,固定合成30天记录,无真实账号或云端请求。"""
import logging
import sys
from datetime import datetime, timedelta, timezone
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT / 'work/mi-fitness-mcp-cn/src'))
from flask import jsonify, request, send_file
from mi_fitness_mcp.web import create_app
logging.getLogger('werkzeug').disabled = True
app = create_app(backend_url='http://127.0.0.1:1')
start = datetime(2026, 9, 18, tzinfo=timezone(timedelta(hours=8)))
def samples(count, field, value):
return [{'timestamp': (start + timedelta(seconds=i*30*86400/count)).isoformat(), field: value} for i in range(count)]
fixtures = {
'summary': [{'date': (start+timedelta(days=i)).date().isoformat(), 'steps': 6000+i, 'active_kcal': 500+i, 'distance_m': 4000+i} for i in range(30)],
'heart-rate': samples(19, 'bpm', 62),
'sleep': [{'end_at': (start+timedelta(days=i,hours=8)).isoformat(), 'duration_minutes': 450} for i in range(30)],
'spo2': samples(2000, 'spo2_pct', 97),
'stress': samples(3000, 'stress_score', 30),
}
@app.before_request
def local_fixture():
if request.path == '/api/proxy_status':
return jsonify({'backend_reachable': True, 'backend_latency_ms': 1})
if request.path.startswith('/proxy/api/'):
rows = fixtures.get(request.path.rsplit('/',1)[-1], [])
sd, ed = request.args.get('start_date'), request.args.get('end_date')
if sd and ed:
rows = [row for row in rows if sd <= (row.get('date') or row.get('end_at') or row.get('timestamp'))[:10] <= ed]
return jsonify({'status':'ok','count':len(rows),'data':rows})
@app.route('/benchmark/before.js')
def baseline_script():
return send_file(ROOT/'work/perf-validation/app.before.js', mimetype='application/javascript')
@app.route('/benchmark/before.css')
def baseline_styles():
return send_file(ROOT/'work/perf-validation/style.before.css', mimetype='text/css')
@app.after_request
def baseline_version(response):
if response.mimetype == 'text/html' and request.args.get('version') == 'before':
import re
text = re.sub(r'/static/js/app\.js\?v=[^"\s]+', '/benchmark/before.js', response.get_data(as_text=True))
response.set_data(re.sub(r'/static/css/style\.css(?:\?v=[^"\s]+)?', '/benchmark/before.css', text))
return response
app.run(host='127.0.0.1', port=18363, debug=False, use_reloader=False)
@@ -0,0 +1,97 @@
import importlib.util
import sqlite3
import sys
import threading
import time
from concurrent.futures import ThreadPoolExecutor
from pathlib import Path
from types import SimpleNamespace
import pytest
ROOT=Path(__file__).resolve().parents[2]
sys.path.insert(0,str(ROOT/'work/mi-fitness-mcp-cn/src'))
from mi_fitness_mcp.services.query_service import QueryService
@pytest.mark.parametrize('method',['_aggregate_by_week','_aggregate_by_month'])
def test_latest_aggregation_uses_latest_date_instead_of_sum(method):
service=QueryService(None,'synthetic')
result=getattr(service,method)([{'date':'2026-10-16','value':9},{'date':'2026-10-15','value':2}],'latest')
assert result[0]['value']==9
def runtime():
spec=importlib.util.spec_from_file_location('audit_runtime',ROOT/'outputs/nas-deployment/runtime.py')
module=importlib.util.module_from_spec(spec);spec.loader.exec_module(module)
return module
def test_repeated_login_for_same_account_keeps_sync_but_multiple_accounts_are_rejected(tmp_path):
db=tmp_path/'health.sqlite'
with sqlite3.connect(db) as c:
c.execute('CREATE TABLE api_keys (key TEXT,user_id TEXT,region TEXT,created_at TEXT,revoked INTEGER)')
c.executemany('INSERT INTO api_keys VALUES (?,?,?,?,0)',[('old','owner','cn','2026-10-15'),('new','owner','cn','2026-10-16')])
assert runtime().load_single_key(db)=='new'
with sqlite3.connect(db) as c:c.execute("INSERT INTO api_keys VALUES ('other','another-owner','cn','2026-10-17',0)")
with pytest.raises(RuntimeError):runtime().load_single_key(db)
def test_cached_credential_does_not_repeat_expensive_unlock_or_bypass_revocation(tmp_path,monkeypatch):
from mi_fitness_mcp import api
from mi_fitness_mcp.storage import Database
from starlette.requests import Request
db=Database(tmp_path/'health.sqlite')
with db._get_connection() as c:
c.execute('CREATE TABLE api_keys (key TEXT,user_id TEXT,region TEXT,pass_token TEXT,revoked INTEGER,last_used_at TEXT)')
c.execute("INSERT INTO api_keys VALUES ('synthetic-key','owner','cn','',0,NULL)")
c.commit()
state=SimpleNamespace(default_context=None,db=db,config=None,contexts={},key_contexts={},key_context_lock=threading.RLock())
request=Request({'type':'http','headers':[(b'x-api-key',b'synthetic-key')],'app':SimpleNamespace(state=state)})
calls=[]
clock=[100.0]
monkeypatch.setattr(api,'time',SimpleNamespace(monotonic=lambda:clock[0]))
def unlock(key):
calls.append(key)
time.sleep(0.05)
return 'synthetic-account-token'
monkeypatch.setattr(api,'load_api_key_secret',unlock)
monkeypatch.setattr(api,'_build_context',lambda *args:SimpleNamespace(query_service=object()))
barrier=threading.Barrier(2)
def resolve():
barrier.wait()
return api._resolve_context(request)
with ThreadPoolExecutor(max_workers=2) as pool:
list(pool.map(lambda _:resolve(),range(2)))
assert len(calls)==1, '每个请求都执行昂贵的密钥环解锁,概览五个请求被串行拖慢'
clock[0]+=61
api._resolve_context(request)
assert len(calls)==2, '短期缓存到期后必须重新读取密钥环'
with db._get_connection() as c:
c.execute("UPDATE api_keys SET revoked=1 WHERE key='synthetic-key'");c.commit()
with pytest.raises(api.HTTPException):api._resolve_context(request)
state.default_context=SimpleNamespace(query_service=object())
bad=Request({'type':'http','headers':[(b'x-api-key',b'unknown-key')],'app':SimpleNamespace(state=state)})
with pytest.raises(api.HTTPException):api._resolve_context(bad)
@pytest.mark.asyncio
async def test_ambiguous_key_prefix_cannot_revoke_multiple_authorizations(tmp_path,monkeypatch):
from mi_fitness_mcp import api
from mi_fitness_mcp.storage import Database
from starlette.requests import Request
db=Database(tmp_path/'keys.sqlite')
with db._get_connection() as c:
c.execute('CREATE TABLE api_keys (key TEXT,revoked INTEGER)')
c.executemany('INSERT INTO api_keys VALUES (?,0)',[('mif_sk_abcde0000',),('mif_sk_abcde1111',)])
c.commit()
deleted=[]
monkeypatch.setattr(api,'_require_admin',lambda request:None)
monkeypatch.setattr(api,'delete_api_key_secret',deleted.append)
request=Request({'type':'http','headers':[],'app':SimpleNamespace(state=SimpleNamespace(db=db))})
with pytest.raises(api.HTTPException) as error:
await api.revoke_key('mif_sk_abcde',request)
assert error.value.status_code==409
assert not deleted
with db._get_connection() as c:assert c.execute('SELECT sum(revoked) FROM api_keys').fetchone()[0]==0
@@ -0,0 +1,69 @@
import asyncio
import sqlite3
import sys
import time
from datetime import datetime, timedelta, timezone
from pathlib import Path
import pytest
ROOT=Path(__file__).resolve().parents[2]
sys.path.insert(0,str(ROOT/'work/mi-fitness-mcp-cn/src'))
from mi_fitness_mcp import storage
from mi_fitness_mcp.models import HeartRateSample
from mi_fitness_mcp.services.sync_service import SyncService
def samples(count):
start=datetime(2026,10,15,tzinfo=timezone.utc)
return [HeartRateSample(id=f'synthetic-{i}',provider='mi_fitness',source_type='cloud_session',user_id='synthetic-user',timestamp=start+timedelta(minutes=i),bpm=70) for i in range(count)]
def test_bulk_records_commit_once_remain_idempotent_and_rollback_together(tmp_path,monkeypatch):
db=storage.Database(tmp_path/'health.sqlite')
assert callable(getattr(db,'insert_records_batch',None)), '缺少批量事务,每条记录都在提交'
statements=[]
connect=sqlite3.connect
def traced(*args,**kwargs):
conn=connect(*args,**kwargs)
conn.set_trace_callback(statements.append)
return conn
monkeypatch.setattr(storage.sqlite3,'connect',traced)
records=samples(250)
db.insert_records_batch('heart_rate',records)
assert sum(s=='COMMIT' for s in statements)==1
db.insert_records_batch('heart_rate',records)
with db._get_connection() as c:
assert c.execute('SELECT count(*) FROM heart_rate_samples').fetchone()[0]==250
valid=samples(1)[0].model_copy(update={'id':'rollback-proof'})
invalid=valid.model_copy(update={'id':'invalid','user_id':None})
with pytest.raises(sqlite3.IntegrityError):
db.insert_records_batch('heart_rate',[valid,invalid])
with db._get_connection() as c:
assert c.execute("SELECT count(*) FROM heart_rate_samples WHERE id='rollback-proof'").fetchone()[0]==0
@pytest.mark.asyncio
async def test_disk_wait_during_sync_does_not_block_request_event_loop(tmp_path):
class SlowDiskDatabase(storage.Database):
def insert_heart_rate_sample(self, sample, **kwargs):
time.sleep(0.01)
return super().insert_heart_rate_sample(sample,**kwargs)
class Adapter:
def is_connected(self): return True
async def iter_heart_rate(self,start,end):
for record in samples(40): yield record
db=SlowDiskDatabase(tmp_path/'health.sqlite')
service=SyncService(Adapter(),db)
task=asyncio.create_task(service.sync_data_type('heart_rate','2026-10-15','2026-10-15'))
await asyncio.sleep(0.01)
responsive=not task.done()
result=await task
assert responsive, '磁盘写入阻塞了整个请求事件循环'
assert result['status']=='ok'
with db._get_connection() as c:
assert c.execute('SELECT count(*) FROM heart_rate_samples').fetchone()[0]==40
@@ -0,0 +1,184 @@
"""数据边界修复回归:全部为本机合成数据。"""
import asyncio
import sys
from datetime import datetime, timedelta, timezone
from pathlib import Path
from types import SimpleNamespace
import pytest
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT / 'work/mi-fitness-mcp-cn/src'))
from mi_fitness_mcp import api
from mi_fitness_mcp.models import DailyActivity, StressSample, Workout, SleepSession
from mi_fitness_mcp.services.query_service import QueryService
from mi_fitness_mcp.services.sync_service import SyncService
from mi_fitness_mcp.storage import Database
CN = timezone(timedelta(hours=8))
def database(tmp_path):
db = Database(tmp_path / 'synthetic.sqlite')
with db._get_connection() as conn:
conn.execute("CREATE TABLE api_keys (key TEXT PRIMARY KEY,label TEXT,user_id TEXT,pass_token TEXT,region TEXT,created_at TEXT,last_used_at TEXT,revoked INTEGER DEFAULT 0)")
conn.commit()
return db
@pytest.mark.parametrize('existing', ['revoked_key', 'health_data'])
def test_new_authorization_cannot_change_database_owner(tmp_path, monkeypatch, existing):
db = database(tmp_path)
if existing == 'revoked_key':
with db._get_connection() as conn:
conn.execute("INSERT INTO api_keys VALUES ('old','old','A','','cn','2026-10-01',NULL,1)")
conn.commit()
else:
db.insert_daily_activity(DailyActivity(id='day-A', provider='mi_fitness', source_type='cloud_session',
user_id='A', date='2026-10-16', steps=1111, active_kcal=100, distance_m=1000))
stored = []
monkeypatch.setattr(api, 'store_api_key_secret', lambda *args: stored.append(args) or True)
with pytest.raises(api.HTTPException) as exc:
api._create_key_record(db, 'B', 'synthetic', 'cn', 'other')
assert exc.value.status_code == 409
assert not stored
def test_same_owner_reauthorization_allowed_but_region_switch_rejected(tmp_path, monkeypatch):
db = database(tmp_path)
monkeypatch.setattr(api, 'store_api_key_secret', lambda *args: True)
first = api._create_key_record(db, 'A', 'synthetic', 'cn', 'first')
second = api._create_key_record(db, 'A', 'synthetic', 'cn', 'second')
assert first != second
with pytest.raises(api.HTTPException) as exc:
api._create_key_record(db, 'A', 'synthetic', 'us', 'other-region')
assert exc.value.status_code == 409
@pytest.mark.asyncio
async def test_manual_sync_refuses_context_for_another_data_owner(tmp_path):
db = database(tmp_path)
db.insert_daily_activity(DailyActivity(id='day-A', provider='mi_fitness', source_type='cloud_session',
user_id='A', date='2026-10-16', steps=1111, active_kcal=100, distance_m=1000))
ctx = SimpleNamespace(user_id='B', region='cn', sync_service=SimpleNamespace(db=db), adapter=None)
with pytest.raises(api.HTTPException) as exc:
await api._run_sync(ctx, None, {'sync_id': 'synthetic'})
assert exc.value.status_code == 409
class Adapter:
def __init__(self, owner):
self.user_id = owner
self.calls = []
def is_connected(self): return True
async def iter_heart_rate(self, start, end):
self.calls.append((start, end))
if False: yield None
@pytest.mark.asyncio
async def test_same_day_incremental_uses_calendar_day(tmp_path):
db = database(tmp_path)
adapter = Adapter('A')
# 旧版全局游标也必须安全,不能将当天 09:00 与午夜比较后拒绝。
db.update_sync_state('heart_rate', datetime(2026, 10, 16, 9, tzinfo=CN))
db.update_sync_state('heart_rate', datetime(2026, 10, 16, 9, tzinfo=CN), user_id='A')
await SyncService(adapter, db).sync_data_type('heart_rate', end_date='2026-10-16')
assert adapter.calls
assert adapter.calls[-1][1] == '2026-10-16'
def test_direct_and_batch_writes_cannot_overwrite_another_owner(tmp_path):
db = database(tmp_path)
item = DailyActivity(id='same-day', provider='mi_fitness', source_type='cloud_session', user_id='A',
date='2026-10-16', steps=1111, active_kcal=100, distance_m=1000)
db.insert_daily_activity(item)
other = item.model_copy(update={'user_id': 'B', 'steps': 2222})
with pytest.raises(ValueError, match='账号'):
db.insert_daily_activity(other)
with pytest.raises(ValueError, match='账号'):
db.insert_records_batch('daily_activity', [other])
assert QueryService(db, 'A').get_daily_summaries('2026-10-16', '2026-10-16')[0]['steps'] == 1111
def test_concurrent_first_authorizations_bind_exactly_one_owner(tmp_path, monkeypatch):
from concurrent.futures import ThreadPoolExecutor
import threading
db = database(tmp_path)
monkeypatch.setattr(api, 'store_api_key_secret', lambda *args: True)
barrier = threading.Barrier(2)
def create(owner):
barrier.wait()
try:
api._create_key_record(db, owner, 'synthetic', 'cn', owner)
return 'ok'
except api.HTTPException as exc:
return exc.status_code
with ThreadPoolExecutor(max_workers=2) as pool:
assert sorted(pool.map(create, ['A', 'B']), key=str) == [409, 'ok']
with db._get_connection() as conn:
assert conn.execute('SELECT count(*) FROM api_keys').fetchone()[0] == 1
@pytest.mark.asyncio
async def test_another_account_cursor_is_not_used_for_first_incremental_sync(tmp_path):
db = database(tmp_path)
db.update_sync_state('heart_rate', datetime(2026, 10, 15, tzinfo=CN))
adapter = Adapter('B')
await SyncService(adapter, db).sync_data_type('heart_rate', end_date='2026-10-16')
assert adapter.calls[0][0] == '2026-09-17'
def test_cursor_storage_is_scoped_by_owner(tmp_path):
db = database(tmp_path)
stamp = datetime(2026, 10, 16, 9, tzinfo=CN)
db.update_sync_state('heart_rate', stamp, user_id='A')
assert db.get_sync_state('heart_rate', user_id='A')['last_record_timestamp'] == stamp.isoformat()
assert db.get_sync_state('heart_rate', user_id='B') is None
@pytest.mark.asyncio
async def test_native_mcp_status_reads_current_owner_cursor(tmp_path, monkeypatch):
from mi_fitness_mcp import server
db = database(tmp_path)
db.update_sync_state('heart_rate', datetime(2026,10,16,9,tzinfo=CN), user_id='A')
class DisconnectedAdapter:
user_id = 'A'
last_error = None
last_health_check_at = None
async def health_check(self): return False
monkeypatch.setattr(server, 'db', db)
monkeypatch.setattr(server, 'adapter', DisconnectedAdapter())
monkeypatch.setattr(server, 'sync_service', None)
monkeypatch.setattr(server, 'config', SimpleNamespace(mode='mi_fitness_cloud',region='cn',health_check_timeout_seconds=1))
result = await server._handle_get_connection_status()
assert result['last_sync_at'] is not None
assert 'heart_rate' in result['available_data_types']
def test_coverage_counts_local_measurement_and_sleep_wake_days(tmp_path):
db = database(tmp_path)
for index, hour in enumerate((1, 9)):
db.insert_stress_sample(StressSample(id=f'stress-{index}', provider='mi_fitness', source_type='cloud_session',
user_id='A', timestamp=datetime(2026, 10, 16, hour, tzinfo=CN), stress_score=30, level='medium'))
db.insert_sleep_session(SleepSession(id='sleep-A', sleep_id='sleep-A', provider='mi_fitness', source_type='cloud_session',
user_id='A', start_at=datetime(2026, 10, 15, 23, tzinfo=CN), end_at=datetime(2026, 10, 16, 7, tzinfo=CN),
duration_minutes=480, time_asleep_minutes=480, time_awake_minutes=0))
coverage = {row['data_type']: row for row in db.get_data_coverage('A')}
for kind in ('stress', 'sleep'):
assert coverage[kind]['first_date'] == coverage[kind]['last_date'] == '2026-10-16'
assert coverage[kind]['days_with_data'] == 1
def test_repeated_workout_sync_updates_all_mutable_fields(tmp_path):
db = database(tmp_path)
item = Workout(id='exercise', workout_id='exercise', provider='mi_fitness', source_type='cloud_session', user_id='A',
activity_type='walking', start_at=datetime(2026, 10, 16, 9, tzinfo=CN),
end_at=datetime(2026, 10, 16, 9, 30, tzinfo=CN), duration_minutes=30)
db.insert_workout(item)
changes = {'total_steps': 4000, 'avg_pace_sec_per_km': 300, 'max_pace_sec_per_km': 240,
'activity_type': 'running', 'end_at': datetime(2026, 10, 16, 9, 40, tzinfo=CN)}
db.insert_workout(item.model_copy(update=changes))
result = QueryService(db, 'A').get_workouts('2026-10-16', '2026-10-16')[0]
for key, value in changes.items():
assert result[key] == (value.isoformat() if isinstance(value, datetime) else value)
+44
View File
@@ -0,0 +1,44 @@
import importlib.util
import sqlite3
import sys
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT / 'work/mi-fitness-mcp-cn/src'))
from mi_fitness_mcp.models import DailyActivity
from mi_fitness_mcp.storage import Database
def readonly():
path = ROOT / 'outputs/nas-deployment/readonly.py'
assert path.exists(), '缺少服务端强制只读查询入口'
spec = importlib.util.spec_from_file_location('health_readonly', path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
def test_readonly_database_denies_writes_and_credential_reads(tmp_path):
m = readonly()
path = tmp_path / 'health.sqlite'
db = Database(path)
db.insert_daily_activity(DailyActivity(id='proof',provider='mi_fitness',source_type='cloud_session',user_id='private-user',date='2026-10-15',steps=1000,distance_m=700,active_kcal=50))
with sqlite3.connect(path) as c:
c.execute('CREATE TABLE api_keys (key TEXT)')
c.execute("INSERT INTO api_keys VALUES ('private-key')")
ro = m.ReadOnlyDatabase(path)
with ro._get_connection() as c:
assert c.execute('SELECT steps FROM daily_activity').fetchone()[0] == 1000
with pytest.raises(sqlite3.DatabaseError):
c.execute('DELETE FROM daily_activity')
with pytest.raises(sqlite3.DatabaseError):
c.execute('SELECT key FROM api_keys')
records = m.query_records(path, 'daily_activity', '2026-10-15', '2026-10-15')
assert records[0]['steps'] == 1000
assert 'private-user' not in str(records)
with pytest.raises(ValueError):
m.query_records(path, 'api_keys', '2026-10-15', '2026-10-15')
with pytest.raises(ValueError):
m.query_records(path, 'daily_activity', '2020-01-01', '2026-10-15')
@@ -0,0 +1,155 @@
"""隔离回归:不使用真实账号、不访问 NAS 或云端。"""
import asyncio
import base64
import sys
from datetime import datetime, timezone
from pathlib import Path
import httpx
import pytest
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT / 'work/mi-fitness-mcp-cn/src'))
from mi_fitness_mcp.adapters.mi_fitness_cloud import MiFitnessCloudAdapter
from mi_fitness_mcp.models import BodyMeasurement
from mi_fitness_mcp.storage import Database
from mi_fitness_mcp.web import create_app
def basic(password):
return {'Authorization': 'Basic ' + base64.b64encode(('admin:' + password).encode()).decode()}
@pytest.mark.parametrize('path,method', [('/', 'get'), ('/api/proxy_status', 'get'),
('/proxy/api/auth/keys', 'get'), ('/proxy/api/auth/keys/mif_sk_demo', 'delete')])
def test_web_requires_browser_auth_before_injecting_admin(path, method):
client = create_app(admin_key='synthetic-admin').test_client()
result = getattr(client, method)(path)
assert result.status_code == 401
assert 'Basic' in result.headers['WWW-Authenticate']
def test_authenticated_browser_admin_proxy_keeps_credentials_local(monkeypatch):
monkeypatch.setenv('MI_FITNESS_WEB_PASSWORD', 'synthetic-web-password')
captured = []
class Client:
def __init__(self, **kwargs): pass
def request(self, **kwargs):
captured.append(kwargs)
return httpx.Response(200, json={'status': 'ok'})
monkeypatch.setattr('mi_fitness_mcp.web.httpx.Client', Client)
client = create_app(admin_key='synthetic-admin').test_client()
assert client.get('/', headers=basic('synthetic-admin')).status_code == 401
result = client.get('/proxy/api/auth/keys', headers=basic('synthetic-web-password'))
assert result.status_code == 200
assert captured[0]['headers']['X-Admin-Key'] == 'synthetic-admin'
assert not any(key.lower() == 'authorization' for key in captured[0]['headers'])
assert result.headers.get('Cache-Control') == 'no-store'
response = client.delete('/proxy/api/auth/keys/demo', headers={**basic('synthetic-web-password'), 'Origin': 'https://untrusted.example'})
assert response.status_code == 403
assert len(captured) == 1
def test_explicit_https_origin_supports_tls_termination_without_trusting_forwarded_headers(monkeypatch):
monkeypatch.setenv('MI_FITNESS_WEB_ORIGIN', 'https://health.example')
client = create_app(admin_key='synthetic-admin').test_client()
assert client.get('/', headers={**basic('synthetic-admin'), 'Origin': 'https://health.example'}).status_code == 200
assert client.get('/', headers={**basic('synthetic-admin'), 'Origin': 'https://other.example',
'X-Forwarded-Host': 'other.example', 'X-Forwarded-Proto': 'https'}).status_code == 403
def test_stale_health_api_key_does_not_lock_browser_out_of_reauthorization(monkeypatch):
captured = []
class Client:
def __init__(self, **kwargs): pass
def request(self, **kwargs):
captured.append(kwargs)
return httpx.Response(200, json={})
monkeypatch.setattr('mi_fitness_mcp.web.httpx.Client', Client)
client = create_app(admin_key='synthetic-admin').test_client()
headers = {**basic('synthetic-admin'), 'X-API-Key': 'mif_sk_revoked'}
client.post('/proxy/api/auth/qr/start', headers=headers)
assert not any(key.lower() == 'x-api-key' for key in captured[0]['headers'])
assert captured[0]['headers']['X-Admin-Key'] == 'synthetic-admin'
client.get('/proxy/api/summary', headers=headers)
assert any(key.lower() == 'x-api-key' and value == 'mif_sk_revoked' for key, value in captured[1]['headers'].items())
def test_body_measurement_later_sync_updates_all_body_fields(tmp_path):
db = Database(tmp_path / 'synthetic.sqlite')
item = BodyMeasurement(id='synthetic', provider='mi_fitness', source_type='cloud_session',
user_id='synthetic', timestamp=datetime(2026, 10, 15, 9, tzinfo=timezone.utc), weight_kg=87.3)
db.insert_body_measurement(item)
additions = {'bone_mass_kg': 3.1, 'visceral_fat_score': 9, 'basal_metabolism_kcal': 1800, 'metabolic_age': 30}
db.insert_body_measurement(item.model_copy(update=additions))
result = db.query_body_measurements('synthetic', '2026-10-15', '2026-10-15')[0]
assert {key: result[key] for key in additions} == additions
@pytest.mark.parametrize('method,list_name', [('_fetch_key', 'data_list'), ('_fetch_sport_records_by_time', 'sport_records')])
@pytest.mark.parametrize('bad_cursor', [None, '', 123])
def test_incomplete_pagination_fails_instead_of_succeeding(monkeypatch, method, list_name, bad_cursor):
adapter = MiFitnessCloudAdapter(user_id='synthetic', pass_token='synthetic', region='cn')
calls = []
async def request(*args):
calls.append(args)
return {list_name: [], 'has_more': True, 'next_key': bad_cursor}
monkeypatch.setattr(adapter, '_request', request)
args = ('heart_rate', '2026-10-15', '2026-10-16') if method == '_fetch_key' else ('2026-10-15', '2026-10-16')
with pytest.raises(ValueError, match='pagination cursor'):
asyncio.run(getattr(adapter, method)(*args))
assert len(calls) == 1
@pytest.mark.parametrize('method,list_name', [('_fetch_key', 'data_list'), ('_fetch_sport_records_by_time', 'sport_records')])
def test_valid_pagination_returns_all_pages(monkeypatch, method, list_name):
adapter = MiFitnessCloudAdapter(user_id='synthetic', pass_token='synthetic', region='cn')
async def request(base, path, payload):
if payload.get('next_key') == 'page2':
return {list_name: [{'id': 2}], 'has_more': False}
return {list_name: [{'id': 1}], 'has_more': True, 'next_key': 'page2'}
monkeypatch.setattr(adapter, '_request', request)
args = ('heart_rate', '2026-10-15', '2026-10-16') if method == '_fetch_key' else ('2026-10-15', '2026-10-16')
assert asyncio.run(getattr(adapter, method)(*args)) == [{'id': 1}, {'id': 2}]
@pytest.mark.parametrize('method,list_name', [('_fetch_key', 'data_list'), ('_fetch_sport_records_by_time', 'sport_records')])
def test_missing_record_list_is_not_treated_as_empty_success(monkeypatch, method, list_name):
adapter = MiFitnessCloudAdapter(user_id='synthetic', pass_token='synthetic', region='cn')
async def request(*args):
return {'has_more': False}
monkeypatch.setattr(adapter, '_request', request)
args = ('heart_rate', '2026-10-15', '2026-10-16') if method == '_fetch_key' else ('2026-10-15', '2026-10-16')
with pytest.raises(ValueError, match='response'):
asyncio.run(getattr(adapter, method)(*args))
def test_concurrent_qr_polls_issue_only_one_key(monkeypatch):
import time
from types import SimpleNamespace
from starlette.requests import Request
from mi_fitness_mcp import api
monkeypatch.setenv('MI_FITNESS_ADMIN_KEY', 'synthetic-admin')
state = SimpleNamespace(db=None, qr_sessions={'qr': {'status': 'waiting', 'lp': 'http://synthetic.invalid',
'created_at': time.time(), 'expires_in': 300, 'region': 'cn'}})
request = Request({'type': 'http', 'headers': [(b'x-admin-key', b'synthetic-admin')],
'app': SimpleNamespace(state=state)})
keys = []
class Client:
def __init__(self, **kwargs): pass
async def __aenter__(self): return self
async def __aexit__(self, *args): pass
async def get(self, *args):
await asyncio.sleep(0.01)
return httpx.Response(200, json={'code': 0, 'userId': 'synthetic', 'passToken': 'synthetic'})
def create(*args):
key = 'synthetic-' + str(len(keys))
keys.append(key)
return key
monkeypatch.setattr(api.httpx, 'AsyncClient', Client)
monkeypatch.setattr(api, '_create_key_record', create)
async def polls():
return await asyncio.gather(api.qr_poll('qr', request), api.qr_poll('qr', request))
results = asyncio.run(polls())
assert len(keys) == 1
assert results[0]['api_key'] == results[1]['api_key']
+104
View File
@@ -0,0 +1,104 @@
import gzip
import importlib.util
import json
import sqlite3
from datetime import date
from pathlib import Path
import pytest
RUNTIME = Path(__file__).resolve().parents[2] / 'outputs/nas-deployment/runtime.py'
def runtime():
assert RUNTIME.exists(), '缺少部署运行支持:安全归档、断线补偿和备份'
spec = importlib.util.spec_from_file_location('deployment_runtime', RUNTIME)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
def test_archive_preserves_health_versions_but_rejects_auth_endpoints(tmp_path):
m = runtime()
for steps in (1000, 1100):
m.archive_response(tmp_path, '/app/v1/data/get_aggregated_fitness_data_by_time', {'key': 'steps'}, {'steps': steps})
files = list(tmp_path.glob('*.jsonl.gz'))
assert len(files) == 1
with gzip.open(files[0], 'rt', encoding='utf-8') as f:
assert [json.loads(line)['result']['steps'] for line in f] == [1000, 1100]
with pytest.raises(ValueError):
m.archive_response(tmp_path, '/pass/serviceLoginAuth2', {}, {'passToken': 'never-archive'})
assert len(list(tmp_path.glob('*'))) == 1
def test_failed_sync_never_moves_success_cursor_and_recovery_covers_gap():
m = runtime()
state = {'last_success_date': '2026-10-01'}
assert m.sync_start(state, date(2026, 10, 16)) == '2026-09-29'
m.finish_sync(state, {'status': 'partial'}, date(2026, 10, 16))
assert state['last_success_date'] == '2026-10-01'
m.finish_sync(state, {'status': 'ok'}, date(2026, 10, 16))
assert state['last_success_date'] == '2026-10-16'
def test_online_backup_restores_without_credentials_and_without_touching_original(tmp_path):
m = runtime()
source = tmp_path / 'live.sqlite'
with sqlite3.connect(source) as c:
c.execute('CREATE TABLE daily_activity (steps INTEGER)')
c.execute('INSERT INTO daily_activity VALUES (1000)')
c.execute('CREATE TABLE api_keys (key TEXT, pass_token TEXT)')
c.execute("INSERT INTO api_keys VALUES ('test-api-key', 'test-account-token')")
backup = m.backup_database(source, tmp_path / 'backups')
with sqlite3.connect(backup) as c:
assert c.execute('PRAGMA quick_check').fetchone()[0] == 'ok'
assert c.execute('SELECT steps FROM daily_activity').fetchone()[0] == 1000
assert c.execute('SELECT count(*) FROM api_keys').fetchone()[0] == 0
with sqlite3.connect(source) as c:
assert c.execute('SELECT count(*) FROM api_keys').fetchone()[0] == 1
def test_worker_recovers_after_network_failure_without_losing_gap(tmp_path, monkeypatch):
import httpx
m = runtime()
m.STATE = tmp_path
db = tmp_path / 'data/mi_fitness.db'
db.parent.mkdir()
with sqlite3.connect(db) as c:
c.execute('CREATE TABLE daily_activity (steps INTEGER)')
c.execute('CREATE TABLE api_keys (key TEXT, revoked INTEGER, user_id TEXT, region TEXT, created_at TEXT)')
c.execute("INSERT INTO api_keys VALUES ('synthetic-key',0,'synthetic-user','cn','2026-10-01')")
status_file = tmp_path / 'automation/status.json'
m.atomic_json(status_file, {'last_success_date':'2026-10-01'})
original_client = httpx.Client
offline = True
observed_starts = []
def respond(request):
if offline:
raise httpx.ConnectError('synthetic offline', request=request)
if request.method == 'POST':
observed_starts.append(json.loads(request.content)['start_date'])
return httpx.Response(200, json={'sync_id':'synthetic-task','status':'accepted'})
return httpx.Response(200, json={'status':'ok'})
class StopWorker(Exception):
pass
def sleep(seconds):
if seconds in (300,1800):
raise StopWorker()
monkeypatch.setattr(httpx,'Client',lambda **kw: original_client(transport=httpx.MockTransport(respond),**kw))
monkeypatch.setattr(m.time,'sleep',sleep)
with pytest.raises(StopWorker):
m.worker()
failed=json.loads(status_file.read_text(encoding='utf-8'))
assert failed['status']=='error' and failed['last_success_date']=='2026-10-01'
offline=False
with pytest.raises(StopWorker):
m.worker()
recovered=json.loads(status_file.read_text(encoding='utf-8'))
assert recovered['status']=='ok' and recovered['last_success_date']!='2026-10-01'
assert observed_starts==['2026-09-29']
@@ -0,0 +1,46 @@
"""本机模拟容器切换时的短暂连接重置,不访问 NAS。"""
import importlib.util
from pathlib import Path
import urllib.error
import pytest
def helper():
path=Path(__file__).resolve().parents[2]/'outputs/frontend-performance-fix/wait-web-ready.py'
assert path.exists(), '更新就绪检查缺少容错重试'
spec=importlib.util.spec_from_file_location('web_ready',path)
module=importlib.util.module_from_spec(spec);spec.loader.exec_module(module)
return module
class Response:
status=200
def __init__(self,body): self.body=body
def __enter__(self): return self
def __exit__(self,*args): pass
def read(self,*args): return self.body
def test_readiness_retries_connection_reset_and_stale_page():
outcomes=[ConnectionResetError(),Response(b'old-ui'),Response(b'20261017-ui3')]
class Opener:
def open(self,*args,**kwargs):
value=outcomes.pop(0)
if isinstance(value,Exception): raise value
return value
helper().wait_for_ready(Opener(),object(),pause=lambda seconds:None)
assert not outcomes
def test_readiness_has_bounded_failure():
calls=[]
class Opener:
def open(self,*args,**kwargs):
calls.append(1);raise ConnectionResetError()
with pytest.raises(RuntimeError,match='就绪'):
helper().wait_for_ready(Opener(),object(),attempts=3,pause=lambda seconds:None)
assert len(calls)==3
def test_bad_password_is_not_treated_as_slow_startup():
class Opener:
def open(self,*args,**kwargs):
raise urllib.error.HTTPError('http://local',401,'denied',{},None)
with pytest.raises(RuntimeError,match='鉴权'):
helper().wait_for_ready(Opener(),object(),pause=lambda seconds:None)
@@ -0,0 +1,77 @@
"""日历缓存和失败恢复验证,仅使用本机临时目录与模拟下载。"""
import json
import sys
import tempfile
import unittest
from unittest.mock import patch
from pathlib import Path
sys.path.insert(0,str(Path(__file__).resolve().parents[1]/'mi-fitness-mcp-cn/src'))
from mi_fitness_mcp.workday_calendar import CalendarStore, validate_calendar
def data(year=2026):
return {'year':year,'papers':['https://www.gov.cn/zhengce/example.htm'],'days':[{'date':f'{year}-01-01','name':'元旦','isOffDay':True}]}
class CalendarTest(unittest.TestCase):
def test_reject_placeholder_wrong_year_and_malformed(self):
for value in [{'year':2027,'papers':[],'days':[]},{**data(),'year':2027},{**data(),'days':[{'date':'2026-02-30','isOffDay':True}]},{**data(),'days':[{'date':'2026-01-01','isOffDay':1}]}]:
with self.assertRaises(ValueError):validate_calendar(value,2026)
def test_persist_refresh_and_keep_good_cache_on_failure(self):
with tempfile.TemporaryDirectory() as tmp:
store=CalendarStore(Path(tmp),Path(tmp)/'seed',fetcher=lambda year:data(year))
store.refresh(2026)
self.assertEqual(store.get(2026)['days'][0]['isOffDay'],True)
restored=CalendarStore(Path(tmp),Path(tmp)/'seed',fetcher=lambda year:(_ for _ in ()).throw(OSError('offline')))
self.assertEqual(restored.get(2026)['year'],2026)
restored.refresh(2026)
self.assertEqual(restored.get(2026)['year'],2026)
restored.refresh(2027)
self.assertIsNone(restored.get(2027))
def test_empty_next_year_is_not_cached_as_published(self):
with tempfile.TemporaryDirectory() as tmp:
store=CalendarStore(Path(tmp),Path(tmp)/'seed',fetcher=lambda year:{'year':year,'days':[],'papers':[]})
store.refresh(2027)
self.assertIsNone(store.get(2027));self.assertFalse((Path(tmp)/'2027.json').exists())
def test_bundled_2026_is_available_without_network(self):
root=Path(__file__).resolve().parents[1]/'mi-fitness-mcp-cn/src/mi_fitness_mcp/web_assets/static/calendars'
with tempfile.TemporaryDirectory() as tmp:
store=CalendarStore(Path(tmp),root)
value=store.get(2026)
self.assertIsNotNone(value)
days={r['date']:r['isOffDay'] for r in value['days']}
self.assertFalse(days['2026-09-20']);self.assertFalse(days['2026-10-10']);self.assertTrue(days['2026-10-01'])
def test_schedule_is_nonblocking_deduplicated_and_future_limited(self):
import threading
import time
entered, release = threading.Event(), threading.Event()
calls=[]
def fetch(year):
calls.append(year);entered.set();release.wait(2);return data(year)
with tempfile.TemporaryDirectory() as tmp:
store=CalendarStore(Path(tmp),Path(tmp)/'seed',fetcher=fetch)
started=time.monotonic();store.schedule([2026,2099]);store.schedule([2026])
self.assertLess(time.monotonic()-started,.5);self.assertTrue(entered.wait(1))
release.set()
for _ in range(100):
if not store.running:break
time.sleep(.01)
self.assertEqual(calls,[2026]);self.assertIsNotNone(store.get(2026))
def test_calendar_route_uses_existing_auth_and_rejects_invalid_ranges(self):
from mi_fitness_mcp.web import create_app
import base64
with tempfile.TemporaryDirectory() as tmp, patch.dict('os.environ',{'MI_FITNESS_WEB_PASSWORD':'Synthetic-calendar-only','MI_FITNESS_CALENDAR_DIR':tmp}):
app=create_app();client=app.test_client()
headers={'Authorization':'Basic '+base64.b64encode(b'admin:Synthetic-calendar-only').decode()}
with patch.object(app.extensions['workday_calendar'],'schedule'):
self.assertEqual(client.get('/api/calendar?start=2026&end=2026').status_code,401)
result=client.get('/api/calendar?start=2026&end=2027',headers=headers)
self.assertEqual(result.status_code,200);self.assertIn('2026',result.json['years']);self.assertNotIn('2027',result.json['years'])
self.assertEqual(client.get('/api/calendar?start=2026&end=2100',headers=headers).status_code,400)
self.assertEqual(client.get('/api/calendar?start=2026&end=2026',headers={**headers,'Origin':'https://untrusted.example'}).status_code,403)
if __name__=='__main__':unittest.main()
@@ -0,0 +1,22 @@
const fs=require('node:fs');const path=require('node:path');const assert=require('node:assert/strict');
const pause=ms=>new Promise(r=>setTimeout(r,ms));
(async()=>{
const tabs=await(await fetch('http://127.0.0.1:18384/json')).json();
const ws=new WebSocket(tabs.find(t=>t.type==='page').webSocketDebuggerUrl);await new Promise(r=>ws.onopen=r);
let id=0;const pending=new Map(),errors=[];
ws.onmessage=e=>{const m=JSON.parse(e.data);if(m.method==='Runtime.exceptionThrown')errors.push(m.params.exceptionDetails.exception?.description||m.params.exceptionDetails.text);if(m.id&&pending.has(m.id)){const p=pending.get(m.id);pending.delete(m.id);m.error?p.reject(m.error):p.resolve(m.result);}};
const call=(method,params={})=>new Promise((resolve,reject)=>{const k=++id;pending.set(k,{resolve,reject});ws.send(JSON.stringify({id:k,method,params}));});
const evaluate=async expression=>{const r=await call('Runtime.evaluate',{expression,returnByValue:true,awaitPromise:true});if(r.exceptionDetails)throw Error(JSON.stringify(r.exceptionDetails));return r.result.value;};
const dir=path.resolve('work/axis-minimum-validation');fs.mkdirSync(dir,{recursive:true});
const shot=async name=>{await pause(400);const s=await call('Page.captureScreenshot',{format:'png',captureBeyondViewport:false});fs.writeFileSync(path.join(dir,name+'.png'),Buffer.from(s.data,'base64'));};
await call('Runtime.enable');await call('Page.enable');await call('Emulation.setDeviceMetricsOverride',{width:1600,height:1050,deviceScaleFactor:1,mobile:false});
await call('Page.navigate',{url:'http://127.0.0.1:18364/'});
for(let i=0;i<80;i++){await pause(100);if(await evaluate('document.readyState==="complete" && typeof HealthViews.referenceLayout==="function"'))break;}
await evaluate(`state.theme='light';initTheme();showToast=()=>{};$('toastContainer').innerHTML='';setInspectorCollapsed(true);$('startDate').value='2026-09-18';$('endDate').value='2026-10-17';$$('.preset-pill').forEach(p=>p.classList.remove('active'));$('preset-30d').classList.add('active');window.testOriginalFetch=fetch;window.pressurePeak=18;fetch=async(url,options)=>String(url).startsWith('/proxy/api/stress')?new Response(JSON.stringify({status:'ok',data:Array.from({length:30},(_,i)=>({timestamp:new Date(Date.UTC(2026,8,18+i,8)).toISOString(),stress_score:i===15?window.pressurePeak:5+i%12,level:'source-original'}))}),{status:200}):testOriginalFetch(url,options);`);
const results=[];
for(const [kind,min] of [['body',40],['spo2',80],['heartrate',40]]){
const actual=await evaluate(`(async()=>{await switchTab('${kind}');const m=state.healthViews['${kind}'].model;const layout=HealthViews.referenceLayout(m);return {kind:'${kind}',min:layout.min,max:layout.max,axisLabels:[...$('healthView-${kind}').querySelectorAll('svg text[text-anchor="end"]')].map(t=>t.textContent),bars:$('healthView-${kind}').querySelectorAll('rect:not(.health-reference-label)').length};})()`);
assert.equal(actual.min,min);assert.equal(Number(actual.axisLabels[0]),min);assert.ok(actual.max>min);assert.ok(actual.bars>0);results.push(actual);await shot(kind+'-axis');
}
assert.deepEqual(errors,[]);const result={results,errors};fs.writeFileSync(path.join(dir,'axis-browser-check.json'),JSON.stringify(result,null,2));console.log(JSON.stringify(result));ws.close();
})().catch(e=>{console.error(e);process.exit(1);});
@@ -0,0 +1,30 @@
const fs=require('node:fs');const path=require('node:path');const assert=require('node:assert/strict');
const pause=ms=>new Promise(r=>setTimeout(r,ms));
(async()=>{
const tabs=await(await fetch('http://127.0.0.1:18384/json')).json();
const ws=new WebSocket(tabs.find(t=>t.type==='page').webSocketDebuggerUrl);await new Promise(r=>ws.onopen=r);
let id=0;const pending=new Map(),errors=[];
ws.onmessage=e=>{const m=JSON.parse(e.data);if(m.method==='Runtime.exceptionThrown')errors.push(m.params.exceptionDetails.exception?.description||m.params.exceptionDetails.text);if(m.id&&pending.has(m.id)){const p=pending.get(m.id);pending.delete(m.id);m.error?p.reject(m.error):p.resolve(m.result);}};
const call=(method,params={})=>new Promise((resolve,reject)=>{const k=++id;pending.set(k,{resolve,reject});ws.send(JSON.stringify({id:k,method,params}));});
const evaluate=async expression=>{const r=await call('Runtime.evaluate',{expression,returnByValue:true,awaitPromise:true});if(r.exceptionDetails)throw Error(JSON.stringify(r.exceptionDetails));return r.result.value;};
const dir=path.resolve('work/calendar-validation');fs.mkdirSync(dir,{recursive:true});
const shot=async name=>{await pause(400);const s=await call('Page.captureScreenshot',{format:'png',captureBeyondViewport:false});fs.writeFileSync(path.join(dir,name+'.png'),Buffer.from(s.data,'base64'));};
await call('Runtime.enable');await call('Page.enable');await call('Emulation.setDeviceMetricsOverride',{width:1600,height:1050,deviceScaleFactor:1,mobile:false});
await call('Page.navigate',{url:'http://127.0.0.1:18364/'});
for(let i=0;i<80;i++){await pause(100);if(await evaluate('document.readyState==="complete" && typeof HealthViews.calendarDay==="function"'))break;}
await evaluate(`state.theme='light';initTheme();showToast=()=>{};$('toastContainer').innerHTML='';setInspectorCollapsed(true);$('startDate').value='2026-09-18';$('endDate').value='2026-10-17';$$('.preset-pill').forEach(p=>p.classList.remove('active'));$('preset-30d').classList.add('active');`);
const results=[];
for(const kind of ['activity','heartrate','sleep','body','spo2','stress']){
await evaluate(`switchTab('${kind}')`);
for(let i=0;i<40;i++){if(await evaluate(`!!state.healthViews['${kind}']?.model.range.calendar?.['2026']`))break;await pause(50);}
const actual=await evaluate(`(()=>{const el=$('healthView-${kind}'),marks=[...el.querySelectorAll('.health-calendar-mark')];return {kind:'${kind}',ys:[...new Set(marks.map(m=>m.getBoundingClientRect().y))],marks:marks.length,nodes:el.querySelectorAll('*').length,colors:Object.fromEntries(marks.map(m=>[m.dataset.date,m.getAttribute('stroke')])),warning:el.textContent.includes('调休安排未更新'),cards:el.querySelectorAll('.health-card').length};})()`);
assert.equal(actual.marks,30);assert.equal(actual.ys.length,1);assert.equal(actual.warning,false);assert.equal(actual.cards,4);assert.ok(actual.nodes<800);
assert.equal(actual.colors['2026-09-20'],'#e78325');assert.equal(actual.colors['2026-10-10'],'#e78325');assert.equal(actual.colors['2026-10-03'],'#22a06b');
results.push(actual);if(kind==='sleep')await shot('sleep-workday-markers');
}
await evaluate(`state.theme='dark';initTheme();`);await shot('stress-workday-markers-dark');
await evaluate(`$('startDate').value='2026-12-31';$('endDate').value='2027-01-02';const m=HealthViews.build('activity',[{date:'2027-01-01',steps:1234}],{start:'2026-12-31',end:'2027-01-02',calendar:state.healthViews.stress.model.range.calendar});$('healthView-stress').innerHTML=HealthViews.render(m);`);
assert.equal(await evaluate(`$('healthView-stress').textContent.includes('2027 年调休安排未更新')`),true);
assert.deepEqual(errors,[]);fs.writeFileSync(path.join(dir,'calendar-browser-check.json'),JSON.stringify({results,errors,cross_year_fallback:true},null,2));
console.log(JSON.stringify({views:results.map(r=>({kind:r.kind,marks:r.marks,y:r.ys,nodes:r.nodes})),errors,cross_year_fallback:true}));ws.close();
})().catch(e=>{console.error(e);process.exit(1);});
@@ -0,0 +1,43 @@
const fs=require('node:fs');
const path=require('node:path');
const assert=require('node:assert/strict');
const pause=ms=>new Promise(r=>setTimeout(r,ms));
(async()=>{
const tabs=await(await fetch('http://127.0.0.1:18384/json')).json();
const ws=new WebSocket(tabs.find(t=>t.type==='page').webSocketDebuggerUrl);
await new Promise(r=>ws.onopen=r);let id=0;const pending=new Map(),errors=[];
ws.onmessage=e=>{const m=JSON.parse(e.data);if(m.method==='Runtime.exceptionThrown')errors.push(m.params.exceptionDetails.exception?.description||m.params.exceptionDetails.text);if(m.id&&pending.has(m.id)){const p=pending.get(m.id);pending.delete(m.id);m.error?p.reject(m.error):p.resolve(m.result);}};
const call=(method,params={})=>new Promise((resolve,reject)=>{const k=++id;pending.set(k,{resolve,reject});ws.send(JSON.stringify({id:k,method,params}));});
const evaluate=async expression=>{const r=await call('Runtime.evaluate',{expression,returnByValue:true,awaitPromise:true});if(r.exceptionDetails)throw Error(JSON.stringify(r.exceptionDetails));return r.result.value;};
await call('Runtime.enable');await call('Page.enable');await call('Emulation.setDeviceMetricsOverride',{width:1600,height:1050,deviceScaleFactor:1,mobile:false});
await call('Page.navigate',{url:'http://127.0.0.1:18364/'});
for(let i=0;i<80;i++){await pause(100);if(await evaluate('document.readyState==="complete" && typeof loadHealthView==="function"'))break;}
await evaluate(`setInspectorCollapsed(true);localStorage.setItem('mi_fitness_theme','light');state.theme='light';initTheme();showToast=()=>{};$('toastContainer').innerHTML='';$$('.preset-pill').forEach(p=>p.classList.remove('active'));$('preset-30d').classList.add('active');$('startDate').value='2026-09-18';$('endDate').value='2026-10-17';`);
const results=[];const dir=path.resolve('work/health-ui-validation');fs.mkdirSync(dir,{recursive:true});
for(const kind of ['activity','heartrate','sleep','workouts','body','spo2','stress','abnormal']){
const r=await evaluate(`(async()=>{const start=performance.now();await switchTab('${kind}');const el=$('healthView-${kind}');const saved=state.healthViews['${kind}'];return {kind:'${kind}',ms:performance.now()-start,nodes:el.querySelectorAll('*').length,rows:el.querySelectorAll('tbody tr').length,records:saved?.model.records.length,empty:el.textContent.includes('未保存'),cards:el.querySelectorAll('.health-card').length,consoleHidden:$('inspectorPanel').hidden};})()`);
assert.equal(r.cards,4);assert.ok(r.rows<=10);assert.ok(r.nodes<750);assert.ok(r.consoleHidden);results.push(r);
if(['sleep','heartrate','body'].includes(kind)){
await evaluate('document.querySelector(".app-content").scrollTop=0');await pause(350);
const shot=await call('Page.captureScreenshot',{format:'png',captureBeyondViewport:false});fs.writeFileSync(path.join(dir,kind+'.png'),Buffer.from(shot.data,'base64'));
}
}
await evaluate("(async()=>{await switchTab('heartrate');healthPage('heartrate',2)})()");
assert.equal(await evaluate("state.healthViews.heartrate.page"),2);
assert.equal(await evaluate("$('healthView-heartrate').querySelectorAll('tbody tr').length"),10);
await evaluate('setInspectorCollapsed(false)');
assert.equal(await evaluate("getComputedStyle($('inspectorPanel')).display==='none'"),false);
await call('Page.reload');await pause(700);
assert.equal(await evaluate("$('inspectorPanel').hidden"),false);
await evaluate("(async()=>{setInspectorCollapsed(true);showToast=()=>{};$('toastContainer').innerHTML='';state.theme='dark';initTheme();$('startDate').value='2026-09-18';$('endDate').value='2026-10-17';await switchTab('sleep')})()");
await pause(350);let shot=await call('Page.captureScreenshot',{format:'png',captureBeyondViewport:false});fs.writeFileSync(path.join(dir,'sleep-dark.png'),Buffer.from(shot.data,'base64'));
await call('Emulation.setDeviceMetricsOverride',{width:1100,height:900,deviceScaleFactor:1,mobile:false});
assert.equal(await evaluate('document.documentElement.scrollWidth<=window.innerWidth'),true);
await call('Emulation.setDeviceMetricsOverride',{width:760,height:900,deviceScaleFactor:1,mobile:false});
const narrow=await evaluate('({width:document.documentElement.scrollWidth,viewport:window.innerWidth})');
assert.equal(narrow.width<=narrow.viewport,true);
await evaluate('setInspectorCollapsed(false)');
assert.equal(await evaluate("getComputedStyle($('inspectorPanel')).display==='none'"),false);
assert.deepEqual(errors,[]);
fs.writeFileSync(path.join(dir,'browser-check.json'),JSON.stringify({results,errors,narrow},null,2));console.log(JSON.stringify({results,errors,narrow}));ws.close();
})().catch(e=>{console.error(e);process.exit(1);});
@@ -0,0 +1,30 @@
const fs=require('node:fs');const path=require('node:path');const assert=require('node:assert/strict');
const pause=ms=>new Promise(r=>setTimeout(r,ms));
(async()=>{
const tabs=await(await fetch('http://127.0.0.1:18384/json')).json();
const ws=new WebSocket(tabs.find(t=>t.type==='page').webSocketDebuggerUrl);await new Promise(r=>ws.onopen=r);
let id=0;const pending=new Map(),errors=[];
ws.onmessage=e=>{const m=JSON.parse(e.data);if(m.method==='Runtime.exceptionThrown')errors.push(m.params.exceptionDetails.exception?.description||m.params.exceptionDetails.text);if(m.id&&pending.has(m.id)){const p=pending.get(m.id);pending.delete(m.id);m.error?p.reject(m.error):p.resolve(m.result);}};
const call=(method,params={})=>new Promise((resolve,reject)=>{const k=++id;pending.set(k,{resolve,reject});ws.send(JSON.stringify({id:k,method,params}));});
const evaluate=async expression=>{const r=await call('Runtime.evaluate',{expression,returnByValue:true,awaitPromise:true});if(r.exceptionDetails)throw Error(JSON.stringify(r.exceptionDetails));return r.result.value;};
const dir=path.resolve('work/reference-lines-validation');fs.mkdirSync(dir,{recursive:true});
const shot=async name=>{await pause(400);const s=await call('Page.captureScreenshot',{format:'png',captureBeyondViewport:false});fs.writeFileSync(path.join(dir,name+'.png'),Buffer.from(s.data,'base64'));};
await call('Runtime.enable');await call('Page.enable');await call('Emulation.setDeviceMetricsOverride',{width:1600,height:1050,deviceScaleFactor:1,mobile:false});
await call('Page.navigate',{url:'http://127.0.0.1:18364/'});
for(let i=0;i<80;i++){await pause(100);if(await evaluate('document.readyState==="complete" && typeof HealthViews.referenceLayout==="function"'))break;}
await evaluate(`state.theme='light';initTheme();showToast=()=>{};$('toastContainer').innerHTML='';setInspectorCollapsed(true);$('startDate').value='2026-09-18';$('endDate').value='2026-10-17';$$('.preset-pill').forEach(p=>p.classList.remove('active'));$('preset-30d').classList.add('active');window.testOriginalFetch=fetch;window.pressurePeak=18;fetch=async(url,options)=>String(url).startsWith('/proxy/api/stress')?new Response(JSON.stringify({status:'ok',data:Array.from({length:30},(_,i)=>({timestamp:new Date(Date.UTC(2026,8,18+i,8)).toISOString(),stress_score:i===15?window.pressurePeak:5+i%12,level:'source-original'}))}),{status:200}):testOriginalFetch(url,options);`);
const stress=await evaluate(`(async()=>{await switchTab('stress');const m=state.healthViews.stress.model;return {max:HealthViews.referenceLayout(m).max,inside:HealthViews.referenceLayout(m).inside.map(r=>r.value),outside:HealthViews.referenceLayout(m).outside.map(r=>r.value),renderedLines:$('healthView-stress').querySelectorAll('.health-reference').length,sourceKept:m.records[0].level,columns:m.columns.length};})()`);
assert.equal(stress.max,20);assert.deepEqual(stress.outside,[25,50,80]);assert.equal(stress.renderedLines,1);await shot('pressure-below25');
const higher=await evaluate(`(async()=>{window.pressurePeak=58;await queryStress();const m=state.healthViews.stress.model;return {max:HealthViews.referenceLayout(m).max,outside:HealthViews.referenceLayout(m).outside.map(r=>r.value),lines:$('healthView-stress').querySelectorAll('.health-reference').length};})()`);
assert.equal(higher.max,60);assert.deepEqual(higher.outside,[80]);assert.equal(higher.lines,3);await shot('pressure-below60');
const selected=await evaluate(`(async()=>{$('stressLevel').value='moderate';await queryStress();return {count:state.healthViews.stress.model.records.length,average:state.healthViews.stress.model.cards[0].raw,text:$('healthView-stress').textContent};})()`);
assert.equal(selected.count,1);assert.equal(selected.average,58);assert.match(selected.text,/中度/);
const heart=await evaluate(`(async()=>{await switchTab('heartrate');const m=state.healthViews.heartrate.model;return {max:HealthViews.referenceLayout(m).max,outside:HealthViews.referenceLayout(m).outside.map(r=>r.value),inside:HealthViews.referenceLayout(m).inside.map(r=>r.key),bars:$('healthView-heartrate').querySelectorAll('rect:not(.health-reference-label)').length};})()`);
assert.equal(heart.max,80);assert.deepEqual(heart.outside,[170]);assert.equal(heart.inside.length,2);assert.ok(heart.bars>30);await shot('heart-reference-lines');
for(const kind of ['activity','sleep','body','spo2']){await evaluate(`switchTab('${kind}')`);await shot(kind+'-reference-lines');}
await evaluate(`(async()=>{window.pressurePeak=18;$('stressLevel').value='';state.theme='dark';initTheme();await switchTab('stress');})()`);await shot('pressure-dark');
await call('Emulation.setDeviceMetricsOverride',{width:760,height:900,deviceScaleFactor:1,mobile:false});
assert.equal(await evaluate('document.documentElement.scrollWidth<=window.innerWidth'),true);
assert.deepEqual(errors,[]);
const result={stress,higher,selected:{count:selected.count,average:selected.average},heart,errors};fs.writeFileSync(path.join(dir,'reference-browser-check.json'),JSON.stringify(result,null,2));console.log(JSON.stringify(result));ws.close();
})().catch(e=>{console.error(e);process.exit(1);});