#!/usr/bin/env python3 """llm_call_trace_io 行为验证(stub harness,不依赖真实 DB/宿主)。 覆盖场景: 1. 无 id / 超长 id → Message 提示 2. 查无记录 → Message 提示 3. 机构隔离:普通机构看别家行 → 拒绝;平台 org '0' → 放行;本机构 → 放行 4. 正常行 → TabPanel 三 tab(summary/req/resp),CodeEditor readonly,摘要含 usage 上下文 + 同批次行 5. 路径穿越:req_path='../../etc/passwd' → [非法路径,拒绝访问] 6. 文件缺失 → 提示无原文文件 """ import asyncio import json import os import sys import tempfile sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) # ---- stub: 假 filesroot + 真实 trace 文件 ---- tmp = tempfile.mkdtemp() FILESROOT = tmp trace_rel = 'llm_trace/20260911/callA_000' os.makedirs(os.path.join(FILESROOT, 'llm_trace/20260911'), exist_ok=True) with open(os.path.join(FILESROOT, trace_rel + '.req.json'), 'w') as f: json.dump({'url': 'https://up/v1/chat', 'method': 'POST', 'kind': 'chat', 'headers': {'Authorization': '[REDACTED]'}, 'body': {'model': 'm1', 'messages': [{'role': 'user', 'content': 'hi '}]}}, f) with open(os.path.join(FILESROOT, trace_rel + '.resp.json'), 'w') as f: json.dump({'status_code': 200, 'elapsed_ms': 123, 'error': '', 'body': {'choices': [{'message': {'content': 'hello'}}]}}, f) # ---- stub: appPublic.jsonConfig.getConfig 返回假 filesroot ---- import types fake_appPublic = types.ModuleType('appPublic') fake_jc = types.ModuleType('appPublic.jsonConfig') class _Cfg: filesroot = FILESROOT fake_jc.getConfig = lambda *a, **k: _Cfg() sys.modules['appPublic.jsonConfig'] = fake_jc # ---- stub: DBPools / sqlor ---- TRACE_ROW = { 'id': 'tid001', 'call_id': 'callA', 'seq': 0, 'kind': 'chat', 'url': 'https://up/v1/chat/completions', 'method': 'POST', 'status_code': 200, 'elapsed_ms': 123, 'req_path': trace_rel + '.req.json', 'resp_path': trace_rel + '.resp.json', 'org_id': 'org9', 'note': '', 'created_at': '2026-09-11 10:00:00', } BATCH_ROWS = [TRACE_ROW, {**TRACE_ROW, 'id': 'tid002', 'seq': 1, 'kind': 'query', 'status_code': 200, 'note': ''}] USAGE_ROWS = [{'status': 'SUCCEEDED', 'accounting_status': 'accounted', 'cost': '0.01', 'charge': '0.02', 'task_ref': 'sess1', 'project_id': '0', 'model_name': 'qwen-x'}] class FakeSor: def __init__(self, db): self.db = db async def sqlExe(self, sql, ns): self.db.calls.append((sql, ns)) if 'FROM llm_call_trace WHERE id=' in sql: return [dict(TRACE_ROW)] if not self.db.no_row else [] if 'FROM llm_call_trace WHERE call_id=' in sql: return [dict(r) for r in BATCH_ROWS] if 'FROM llm_usage u LEFT JOIN llm_model' in sql: return [dict(r) for r in USAGE_ROWS] return [] async def __aenter__(self): return self async def __aexit__(self, *a): return False class FakeDB: def __init__(self): self.databases = {}; self.no_row = False; self.calls = [] def sqlorContext(self, dbname): return FakeSor(self) _FAKE_DB = FakeDB() import sqlor.dbpools as dbpools dbpools.DBPools = lambda *a, **k: _FAKE_DB # ---- stub: ahserver.serverenv.ServerEnv ---- class FakeEnv(dict): _inst = None def __new__(cls): if FakeEnv._inst is None: FakeEnv._inst = super().__new__(cls) return FakeEnv._inst def __getattr__(self, k): try: return self[k] except KeyError: raise AttributeError(k) import ahserver.serverenv as serverenv serverenv.ServerEnv = FakeEnv # ---- stub: gateway._get_db(trace.py/accounting 用)---- import pipeline_llm.gateway as gw gw._get_db = lambda: (_FAKE_DB, 'pipeline') import pipeline_llm.init as m FAILS = [] def check(name, cond, detail=''): print(('PASS' if cond else 'FAIL'), name, detail if not cond else '') if not cond: FAILS.append(name) def find_widget(w, wtype): if isinstance(w, dict): if w.get('widgettype') == wtype: return w for v in w.values(): r = find_widget(v, wtype) if r: return r elif isinstance(w, list): for v in w: r = find_widget(v, wtype) if r: return r return None async def main(): # 1. 缺 id / 超长 id r = await m.llm_call_trace_io({}, '0') check('缺id → Message', r['widgettype'] == 'Message') r = await m.llm_call_trace_io({'id': 'x' * 40}, '0') check('超长id → Message', r['widgettype'] == 'Message') # 2. 查无记录 _FAKE_DB.no_row = True r = await m.llm_call_trace_io({'id': 'tid001'}, '0') check('查无 → Message', r['widgettype'] == 'Message') _FAKE_DB.no_row = False # 3. 机构隔离 r = await m.llm_call_trace_io({'id': 'tid001'}, 'other_org') check('他机构 → 拒绝', r['widgettype'] == 'Message' and '无权' in r['options']['message']) r = await m.llm_call_trace_io({'id': 'tid001'}, '0') check("平台org'0' → 放行", r['widgettype'] == 'TabPanel') r = await m.llm_call_trace_io({'id': 'tid001'}, 'org9') check('本机构 → 放行', r['widgettype'] == 'TabPanel') # 4. TabPanel 结构 items = r['options']['items'] check('3个tab', len(items) == 3) check('tab名', [t['name'] for t in items] == ['summary', 'req', 'resp']) ce_req = items[1]['content'] check('req是CodeEditor', ce_req['widgettype'] == 'CodeEditor') check('readonly', ce_req['options']['readonly'] is True) check('mode null', ce_req['options']['mode'] == 'null') val = json.loads(ce_req['options']['value']) check('req原文含REDACTED头', val['headers']['Authorization'] == '[REDACTED]') check('req原文含body', val['body']['messages'][0]['content'] == 'hi ') resp_val = json.loads(items[2]['content']['options']['value']) check('resp原文body', resp_val['body']['choices'][0]['message']['content'] == 'hello') summary_html = find_widget(items[0]['content'], 'Html')['options']['html'] check('摘要含usage模型名', 'qwen-x' in summary_html) check('摘要含批次往返数', '同批次往返(2 次)' in summary_html) # HTML 转义:摘要走 Html widget,正文里任何 < > 必须被 escape(防注入) TRACE_ROW['note'] = '' r2 = await m.llm_call_trace_io({'id': 'tid001'}, '0') s2 = find_widget(r2['options']['items'][0]['content'], 'Html')['options']['html'] check('note