#!/usr/bin/env python3 """Regression tests for the Chat Skill's bundled Python scripts.""" import contextlib import importlib.util import io import json import re import types import unittest from pathlib import Path from unittest import mock ROOT = Path(__file__).resolve().parents[2] MULTI_ROOT = ROOT / 'skills' / 'multi' / 'dingtalk-chat' MONO_ROOT = ROOT / 'skills' / 'mono' SCRIPT_NAMES = ( 'chat_export_messages.py', 'chat_history_with_user.py', 'bot_broadcast.py', ) def load_script(name: str): path = MULTI_ROOT / 'scripts' / f'{name}.py' spec = importlib.util.spec_from_file_location(f'chat_skill_{name}', path) if spec is None or spec.loader is None: raise RuntimeError(f'cannot load {path}') module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) return module class ChatSkillScriptPathTest(unittest.TestCase): def test_shortcuts_are_self_contained_in_multi_skill(self): text = (MULTI_ROOT / 'SKILL.md').read_text(encoding='utf-8') block = text.split('', 1)[1].split( '', 1 )[0] catalog = json.loads( (ROOT / 'docs' / 'shortcut-public-catalog.json').read_text( encoding='utf-8' ) ) chat_rows = [ row for row in catalog['results'] if row.get('service') == 'chat' ] table_rows = re.findall( r'\|\s*`(\+[a-z0-9-]+)`\s*\|\s*([^|\n]+)\s*\|', block, ) self.assertEqual( {row['command'] for row in chat_rows}, {command for command, _ in table_rows}, ) self.assertTrue(all(description.strip() for _, description in table_rows)) self.assertTrue( all( re.search(r'[\u4e00-\u9fff]', description) for _, description in table_rows ), 'every shortcut description must retain a Chinese intent cue', ) self.assertIn('| Shortcut | 适用场景 |', block) self.assertNotIn('| Shortcut | 风险 |', block) self.assertNotIn('dws chat +', block) for required in ( '+messages-send', '+chat-messages', '+search-msg', '+conversation-clear-messages', ): self.assertIn(required, block) high_risk = { row['command'] for row in chat_rows if row.get('risk') == 'high-risk-write' } high_risk_line = next( line for line in block.splitlines() if line.startswith('`risk=high-risk-write`') ) self.assertEqual( high_risk, set(re.findall(r'`(\+[a-z0-9-]+)`', high_risk_line)), ) for required in ( 'This is the only Shortcut entry point for multi/chat', 'exact script/recipe > matching public Shortcut > atomic command', 'dws shortcut list --service chat --format json', 'never guess names', 'confirmation=user_required', '--idempotency-key', '--download-resources', '--receiver-open-dingtalk-id', ): self.assertIn(required, text) def test_chat_references_are_atomic_only(self): references = list((MULTI_ROOT / 'references').rglob('*.md')) self.assertTrue(references) for path in references: text = path.read_text(encoding='utf-8') with self.subTest(path=path.relative_to(MULTI_ROOT)): self.assertNotRegex(text, r'(?i)\bshortcut\b') self.assertNotRegex(text, r'\+[a-z][a-z0-9-]+') chat_ref = ( MULTI_ROOT / 'references' / 'chat.md' ).read_text(encoding='utf-8') message_ref = ( MULTI_ROOT / 'references' / 'chat' / 'chat-message.md' ).read_text(encoding='utf-8') conversation_ref = ( MULTI_ROOT / 'references' / 'chat' / 'chat-conversation.md' ).read_text(encoding='utf-8') for required in ( 'message send --group', 'message search-advanced', 'message send-by-bot', 'message send-by-webhook', ): self.assertIn(required, chat_ref) for required in ( 'message send-card', 'message update-card', '--flow-status', 'message download-media', '--file-path', ): self.assertIn(required, message_ref) for required in ( 'clear-messages', 'category delete', '按 leaf Schema 的确认语义执行', ): self.assertIn(required, conversation_ref) def test_documented_chat_commands_do_not_pass_literal_newline_escapes(self): docs = [MULTI_ROOT / 'SKILL.md'] docs.extend((MULTI_ROOT / 'references').rglob('*.md')) offenders = [] for path in docs: for line_number, line in enumerate( path.read_text(encoding='utf-8').splitlines(), start=1, ): if 'dws chat ' in line and r'\n' in line: offenders.append( f'{path.relative_to(ROOT)}:{line_number}: {line}' ) self.assertEqual([], offenders) def test_markdown_python_links_resolve(self): link_re = re.compile(r'\[[^\]]+\.py\]\(([^)]+\.py)\)') docs = list(MULTI_ROOT.rglob('*.md')) docs.extend((MONO_ROOT / 'references').rglob('*.md')) missing = [] for doc in docs: text = doc.read_text(encoding='utf-8') for target in link_re.findall(text): resolved = (doc.parent / target).resolve() if not resolved.is_file(): missing.append(f'{doc.relative_to(ROOT)} -> {target}') self.assertEqual([], missing) def test_documented_invocations_use_python3_and_skill_relative_paths(self): docs = { MULTI_ROOT: ( MULTI_ROOT / 'SKILL.md', MULTI_ROOT / 'references' / '01-messaging.md', MULTI_ROOT / 'references' / 'chat.md', MULTI_ROOT / 'references' / 'chat' / 'chat-workflows.md', ), MONO_ROOT: ( MONO_ROOT / 'references' / 'best_practices' / '01-messaging.md', MONO_ROOT / 'references' / 'products' / 'chat.md', ), } bad_python = re.compile(r'(? {target} is missing' ) self.assertEqual([], errors) def test_mono_and_multi_script_copies_match(self): bad_python = re.compile(r'(?