#!/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_use_progressive_discovery_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' ] schema_catalog = json.loads( ( ROOT / 'internal' / 'cli' / 'schema_catalog' / 'tools' / 'chat.json' ).read_text(encoding='utf-8') ) schema_shortcuts = [ path for path in schema_catalog['tools'] if path.startswith('chat.shortcut_') ] exclusions = json.loads( ( ROOT / 'internal' / 'cli' / 'schema_command_exclusions.json' ).read_text(encoding='utf-8') ) pending_shortcuts = next( group['commands'] for group in exclusions['groups'] if group['id'] == 'chat-shortcuts-pending-schema-curation' ) self.assertEqual(97, len(chat_rows)) self.assertEqual(47, len(schema_shortcuts)) self.assertEqual(50, len(pending_shortcuts)) self.assertEqual( len(chat_rows), len(schema_shortcuts) + len(pending_shortcuts) ) self.assertIn('## Shortcut 发现(按需)', block) self.assertIn('`chat` 当前有 97 条公开 shortcut', block) self.assertIn( '完整清单保留在 Runtime Shortcut Catalog;已完成 Schema curation ' '的子集可通过 leaf Schema 查询。', block, ) self.assertNotIn('Runtime Catalog 与 Schema', block) self.assertIn( 'dws shortcut list --service chat --compact --format json', block, ) self.assertNotRegex(block, r'(?m)^\|\s*`\+[a-z0-9-]+`') self.assertNotRegex(block, r'(?m)^\|\s*`dws chat \+[a-z0-9-]+`') self.assertNotIn('dws chat +', block) for required in ( 'This is the only Shortcut entry point for multi/chat', 'exact recipe/runnable script > matching public Shortcut > atomic command', 'dws shortcut list --service chat --compact --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_optional_python_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_chat_workflows_do_not_require_python(self): multi_skill = (MULTI_ROOT / 'SKILL.md').read_text(encoding='utf-8') multi_recipes = ( MULTI_ROOT / 'references' / '01-messaging.md' ).read_text(encoding='utf-8') mono_recipes = ( MONO_ROOT / 'references' / 'best_practices' / '01-messaging.md' ).read_text(encoding='utf-8') self.assertRegex(multi_skill, r'must work without\s+Python') self.assertNotRegex(multi_skill, r'\|\s*“[^”]+”\s*\|\s*`python3 ') for recipes in (multi_recipes, mono_recipes): self.assertNotRegex(recipes, r'\*\*优先\*\*[^\\n]*python3') self.assertIn('chat message list', recipes) self.assertIn('chat message send-by-bot', recipes) def test_mono_and_multi_script_copies_match(self): bad_python = re.compile(r'(?