sources/historical-doc-tests/test_revision.py

Текстовый файл из учебного комплекта, не свидетельство установленного 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()