Текстовый файл из учебного комплекта, не свидетельство установленного runtime. Вернуться к учебнику · Исходный файл
1 """Независимая приёмка единой редакции: контракты, старые ссылки и команды. 2 3 Сеть, настоящее хранилище входа, Desktop и микрофон не используются. 4 """ 5 import ast 6 from collections import Counter 7 import hashlib 8 import html 9 from html.parser import HTMLParser 10 import importlib.util 11 import json 12 import os 13 from pathlib import Path 14 import re 15 import shutil 16 import subprocess 17 import tempfile 18 import unittest 19 20 ROOT = Path(__file__).resolve().parents[2] 21 BOOK = ROOT / 'docs/codex-cli-voice-textbook.html' 22 23 24 def load(name): 25 """Загружает проверяемый генератор, не вызывая его main.""" 26 spec=importlib.util.spec_from_file_location(name,ROOT/'scripts/audio-textbook'/(name+'.py')) 27 module=importlib.util.module_from_spec(spec);spec.loader.exec_module(module) 28 return module 29 30 31 def blocks(source): 32 """Возвращает исходный текст, а не HTML-теги синтаксической подсветки.""" 33 return [(lang,html.unescape(re.sub('<[^>]+>','',value))) for lang,value in re.findall(r'<pre><code class="language-([^"]+)"[^>]*>(.*?)</code></pre>',source,re.S)] 34 35 36 class RevisionTests(unittest.TestCase): 37 @classmethod 38 def setUpClass(cls): 39 cls.source=BOOK.read_text() 40 cls.inventory=json.loads((ROOT/'scripts/audio-textbook/legacy-links.json').read_text()) 41 cls.ids=set(re.findall(r'\bid="([^"]+)"',cls.source)) 42 43 def test_native_v3_selector(self): 44 # Проверяем значение независимо от HTML ↔ собственного генератора. 45 module=load('annotate-contracts') 46 params=module.CONTRACTS['rpc']['sent']['params'] 47 self.assertEqual(params['version'],'v3') 48 self.assertEqual(params['transport']['type'],'webrtc') 49 source=(ROOT/'docs/audio-source-snapshots/codex-rs/tui/src/app_server_session/realtime.rs').read_text() 50 self.assertIn('RealtimeConversationVersion::V3',source) 51 replies=module.CONTRACTS['rpc']['received'] 52 self.assertEqual(replies[0],{'id':10,'result':{}}) 53 self.assertEqual(replies[1]['method'],'thread/realtime/sdp') 54 55 def test_native_transport_source(self): 56 base=ROOT/'docs/audio-source-snapshots/codex-rs' 57 tui=(base/'tui/src/lib.rs').read_text() 58 client=(base/'app-server-client/src/lib.rs').read_text() 59 for marker in ['AppServerTarget::Embedded','AppServerClient::InProcess','RemoteAppServerEndpoint::UnixSocket']: 60 self.assertIn(marker,tui) 61 self.assertIn('InProcessAppServerClient',client) 62 self.assertIn('mpsc::channel',client) 63 module=load('annotate-contracts') 64 self.assertNotIn('stdio://',module.CONTRACTS['rpc']['endpoint']) 65 self.assertIn('stdio://',module.CONTRACTS['speech-rpc']['endpoint']) 66 67 def test_bash_auth_argument(self): 68 # Stub python только записывает argv, не выполняет вложенный клиент. 69 script=ROOT/'scripts/voice-probes/chatgpt-transcribe.sh' 70 with tempfile.TemporaryDirectory(prefix='auth путь ') as folder: 71 root=Path(folder);stub=root/'python3' 72 stub.write_text('#!/bin/bash\nprintf "%s\\n" "$@"\n') 73 stub.chmod(0o755) 74 env={**os.environ,'PATH':str(root)+':'+os.environ['PATH'],'CODEX_HOME':str(root/'wrong default')} 75 audio=str(root/'запись.flac');auth=str(root/'выбранный профиль.json') 76 for args in [[audio,'--auth-file',auth],['--auth-file',auth,audio]]: 77 result=subprocess.run(['bash',str(script),*args],env=env,text=True,capture_output=True) 78 self.assertEqual(result.returncode,0,result.stderr) 79 self.assertEqual(result.stdout.splitlines(),['-',audio,auth]) 80 old=subprocess.run(['bash',str(script),audio],env=env,text=True,capture_output=True) 81 self.assertEqual(old.stdout.splitlines(),['-',audio,str(root/'wrong default/auth.json')]) 82 for args in [[],['--auth-file'],[audio,audio],['--unknown',audio]]: 83 bad=subprocess.run(['bash',str(script),*args],env=env,text=True,capture_output=True) 84 self.assertEqual(bad.returncode,2) 85 86 def test_summary_contract(self): 87 source=(ROOT/'docs/tts-in-fork.md').read_text() 88 body=re.search(r'JSON: (\{[^\n]+\})',source[source.index('audio_summary/synthesize'):])[1] 89 self.assertIn(json.loads(body)['format'],{'podcast','executive_briefing','study_guide'}) 90 self.assertIn('422',source);self.assertIn('404',source) 91 92 def test_early_final_not_full_completion(self): 93 # Контрпример показывает границу прежнего критерия: sticky final 94 # может случиться до EOF. Он не доказывает весь будущий ответ. 95 file_done,assistant_done=False,False 96 assistant_done=True 97 self.assertFalse(file_done and assistant_done) 98 file_done=True 99 self.assertTrue(file_done and assistant_done) 100 for relative in ['webrtc-voice-isolated/voice-console.py','webrtc-voice-proxy/voice_core.py']: 101 source=(ROOT/'scripts'/relative).read_text() 102 self.assertIn('не доказывает ответ на весь файл',source) 103 self.assertNotIn('нет полного ответа до timeout',source) 104 self.assertIn('одной реплик',self.source) 105 self.assertIn('не доказывает полный media drain',self.source) 106 107 def test_all_legacy_targets_and_schemes(self): 108 self.assertEqual(len(self.inventory['schemes']),23) 109 for filename,mapping in self.inventory['links'].items(): 110 for old,new in mapping.items(): 111 self.assertIn(new,self.ids,(filename,old,new)) 112 for row in self.inventory['schemes']: 113 self.assertIn(row['target'],self.ids) 114 self.assertIn(row['target']+'-wire',self.ids) 115 for filename in ['codex-voice-proxy-textbook.html','audio-ui-entrypoints.html']: 116 source=(ROOT/'docs'/filename).read_text() 117 self.assertNotIn('<section',source) 118 self.assertEqual(set(re.findall(r'<li id="([^"]+)"',source)),set(self.inventory['links'][filename])) 119 self.assertIn('location.replace',source) 120 121 def test_all_headings_in_menu_and_markers_are_real(self): 122 nav=re.search(r'<nav class="chapters".*?</nav>',self.source,re.S)[0] 123 body=re.search(r'<main.*?</main>',self.source,re.S)[0] 124 headings=re.findall(r'<h[234][^>]*id="([^"]+)"',body) 125 for id_ in headings:self.assertTrue('href="#'+id_+'"' in nav,id_) 126 counts=Counter(re.findall(r'\bid="([^"]+)"',self.source)) 127 self.assertEqual([id_ for id_,count in counts.items() if count>1],[]) 128 markers=set(re.findall(r'<marker\b[^>]*id="([^"]+)"',self.source)) 129 for reference in re.findall(r'url\(#([^)]+)\)',body):self.assertIn(reference,markers) 130 figure=re.search(r'<figure\b[^>]*id="overview-flow".*?</figure>',body,re.S)[0] 131 local=re.search(r'<a href="([^"]+)"[^>]*>(?:(?!</a>).)*Ваш компьютер',figure,re.S)[1] 132 self.assertEqual(local,'#native-machine') 133 remote=re.search(r'<a href="([^"]+)"[^>]*>(?:(?!</a>).)*Голосовой сервис',figure,re.S)[1] 134 self.assertEqual(remote,'#remote-service') 135 ui=re.search(r'<figure\b[^>]*id="ui-routes".*?</figure>',body,re.S)[0] 136 for rect in re.findall(r'<rect\b[^>]*>',ui): 137 self.assertIn('fill=',rect) 138 for path in re.findall(r'<path\b[^>]*>',ui): 139 self.assertIn('stroke=',path) 140 141 def test_audit_questions_cover_every_chapter_and_ui_section(self): 142 module=load('revise-textbook') 143 self.assertEqual(set(module.REVIEWS),set(module.ORDER)) 144 chapters=re.findall(r'data-reviewed-chapter="([^"]+)"',self.source) 145 self.assertEqual(chapters,module.ORDER) 146 self.assertEqual(len(re.findall(r'data-reviewed-ui="([^"]+)"',self.source)),12) 147 for question,answer,target in module.REVIEWS.values(): 148 self.assertIn(target,self.ids) 149 self.assertGreater(len(answer),70) 150 # Без JavaScript ни одна глава не должна остаться скрытой. 151 for opening in re.findall(r'<section\b[^>]*class="chapter"[^>]*>',self.source): 152 self.assertNotRegex(opening,r'\bhidden(?:=|\s|>)') 153 154 def test_all_blocks_have_language_and_parseable_payload(self): 155 values=blocks(self.source) 156 self.assertEqual(len(values),self.source.count('<pre>')) 157 self.assertEqual(self.source.count('class="copy"'),len(values)) 158 for lang,value in values: 159 if lang=='json':json.loads(value) 160 if lang=='bash': 161 syntax=subprocess.run(['bash','-n'],input=value,text=True,capture_output=True) 162 self.assertEqual(syntax.returncode,0,syntax.stderr) 163 js=[value for lang,value in values if 'startDictation' in value] 164 self.assertTrue(js) 165 self.assertTrue(all(lang=='javascript' for lang,value in values if 'startDictation' in value)) 166 167 def test_commands_choose_auth_without_conflicting_key(self): 168 names=['chatgpt-transcribe.sh','chatgpt-dictation-file.py','http-tts.py','chatgpt-read-aloud.py','realtime-tts.py','appserver-tts.py','realtime-transcribe.py','voice-console.py','proxy-voice.py','run-proxy.sh'] 169 for lang,value in blocks(self.source): 170 if lang!='bash':continue 171 for command in value.replace('\\\n',' ').splitlines(): 172 # setup/cp и комментарий-объяснение не являются живым клиентом. 173 if command.lstrip().startswith('#') or re.match(r'\s*(cp|pip|bash -n|python3 -m)',command):continue 174 if not any(name in command for name in names):continue 175 if '--api-key-file' in command: 176 self.assertNotIn('--auth-file',command,command) 177 else:self.assertIn('--auth-file "$auth_file"',command,command) 178 if 'realtime-tts.py' in command: 179 self.assertNotIn('$TTS_DEMO_CACHE/venv/bin/python',command) 180 181 def test_snapshot_manifest_covers_every_source(self): 182 directory=ROOT/'docs/audio-source-snapshots' 183 manifest=json.loads((directory/'manifest.json').read_text()) 184 paths={item['path'] for item in manifest['files']} 185 self.assertEqual(paths,{p.relative_to(directory).as_posix() for p in directory.rglob('*.rs')}|{'LICENSE'}) 186 for item in manifest['files']: 187 self.assertEqual(hashlib.sha256((directory/item['path']).read_bytes()).hexdigest(),item['sha256']) 188 189 def test_offline_highlight_preserves_exact_code(self): 190 module=load('revise-textbook') 191 for lang,text in [('bash','echo "<tag>" # русский комментарий'),('json','{"n":24,"s":"текст"}'),('python','return "<unsafe>"')]: 192 value=module.highlight(text,lang) 193 self.assertEqual(html.unescape(re.sub('<[^>]+>','',value)),text) 194 195 def test_markdown_bash_examples_are_valid(self): 196 # Проверка синтаксиса не запускает команды установки или live-клиент. 197 names=['AUDIO-README.md','docs/chatgpt-audio-http.md','docs/voice-app-server.md','docs/tts-in-fork.md'] 198 names += ['scripts/'+folder+'/README.md' for folder in ['voice-probes','tts-demos','realtime-stt-isolated','remote-tts-isolated','webrtc-voice-isolated','webrtc-voice-proxy']] 199 count=0 200 for name in names: 201 for code in re.findall(r'```bash\n(.*?)\n```',(ROOT/name).read_text(),re.S): 202 result=subprocess.run(['bash','-n'],input=code,text=True,capture_output=True) 203 self.assertEqual(result.returncode,0,name+': '+result.stderr) 204 count+=1 205 self.assertGreaterEqual(count,66) 206 207 def test_revision_is_idempotent_in_temporary_copy(self): 208 module=load('revise-textbook') 209 names=['docs/codex-cli-voice-textbook.html','docs/codex-voice-proxy-textbook.html','docs/audio-ui-entrypoints.html','scripts/audio-textbook/legacy-links.json', 210 'docs/chatgpt-audio-http.md','docs/voice-app-server.md','docs/tts-in-fork.md'] 211 names += ['scripts/'+folder+'/README.md' for folder in ['voice-probes','tts-demos','realtime-stt-isolated','remote-tts-isolated','webrtc-voice-isolated','webrtc-voice-proxy']] 212 with tempfile.TemporaryDirectory(prefix='редакция с пробелами ') as folder: 213 root=Path(folder) 214 for name in names: 215 (root/name).parent.mkdir(parents=True,exist_ok=True) 216 shutil.copyfile(ROOT/name,root/name) 217 module.ROOT=root 218 module.DOCS=root/'docs' 219 module.polish();module.polish_guides() 220 first={name:hashlib.sha256((root/name).read_bytes()).hexdigest() for name in names} 221 module.polish();module.polish_guides() 222 second={name:hashlib.sha256((root/name).read_bytes()).hexdigest() for name in names} 223 self.assertEqual(first,second) 224 225 def test_own_demo_modules_contain_russian_explanations(self): 226 for folder in ['voice-probes','tts-demos','realtime-stt-isolated','remote-tts-isolated','webrtc-voice-isolated','webrtc-voice-proxy']: 227 for file in (ROOT/'scripts'/folder).glob('*.py'): 228 if file.name.startswith('test_'):continue 229 source=file.read_text();ast.parse(source) 230 self.assertRegex(source,r'[А-Яа-я]',str(file)) 231 232 233 if __name__=='__main__': 234 unittest.main()