| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565 |
- """
- C04 PromptManager self-test script.
- Tests all test cases from TC-C04-API-001 through TC-C04-ERROR-003.
- Run from project root: python core/debug/test_prompt_manager.py
- """
- import sys
- import os
- import json
- import shutil
- import traceback
- PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
- if PROJECT_ROOT not in sys.path:
- sys.path.insert(0, PROJECT_ROOT)
- from core.debug.prompt_manager import (
- PromptManager,
- PROMPT_DIR,
- VERSIONS_DIR,
- PROMPT_FILE_MAP,
- CHAINS,
- _extract_variables,
- _make_diff_lines,
- )
- PASS = 0
- FAIL = 0
- ERRORS = []
- def check(cond, msg):
- global PASS, FAIL
- if cond:
- PASS += 1
- print(f' [PASS] {msg}')
- else:
- FAIL += 1
- print(f' [FAIL] {msg}')
- ERRORS.append(msg)
- def section(title):
- print(f'\n{"=" * 60}')
- print(f' {title}')
- print(f'{"=" * 60}')
- # ================================================================
- # Setup: backup existing versions directory
- # ================================================================
- section('Setup test environment')
- versions_backup = None
- if os.path.exists(VERSIONS_DIR):
- import tempfile
- versions_backup = VERSIONS_DIR + '_backup_' + os.urandom(4).hex()
- shutil.copytree(VERSIONS_DIR, versions_backup)
- shutil.rmtree(VERSIONS_DIR)
- print(f' Backed up old versions dir to: {versions_backup}')
- manager = PromptManager()
- # Capture initial state for later verification (before any save/activate modifies it)
- INITIAL_CC_V1 = manager._load_version_file('completeness_check', 'v1.0')
- INITIAL_CC_MAIN = manager._read_current_from_main('completeness_check')
- print(f' PROMPT_DIR: {PROMPT_DIR}')
- print(f' VERSIONS_DIR: {VERSIONS_DIR}')
- print(f' Total prompts: {len(PROMPT_FILE_MAP)}')
- print(f' Chains: {CHAINS}')
- # ================================================================
- # TC-C04-API-001: Get prompt list with version info
- # ================================================================
- section('TC-C04-API-001: Get prompt list')
- try:
- all_prompts = manager.get_all_prompts()
- check(len(all_prompts) >= 8, f'items count >= 8 (actual: {len(all_prompts)})')
- if all_prompts:
- item = all_prompts[0]
- check('name' in item, 'items[0] has name')
- check('version' in item, 'items[0] has version')
- check('time' in item, 'items[0] has time')
- check('chain' in item, 'items[0] has chain')
- check('is_current' in item, 'items[0] has is_current')
- check('note' in item, 'items[0] has note')
- names = [i['name'] for i in all_prompts]
- check('completeness_check' in names, 'list contains completeness_check')
- check(len(CHAINS) >= 7, f'chains count {len(CHAINS)} >= 7')
- check('完整性' in CHAINS, 'chains contains 完整性')
- check('时效性' in CHAINS, 'chains contains 时效性')
- check('规范性' in CHAINS, 'chains contains 规范性')
- check('敏感词' in CHAINS, 'chains contains 敏感词')
- check('语义逻辑' in CHAINS, 'chains contains 语义逻辑')
- check('语法' in CHAINS, 'chains contains 语法')
- check('专业性' in CHAINS, 'chains contains 专业性')
- except Exception as e:
- print(f' [EXCEPTION] {e}')
- traceback.print_exc()
- FAIL += 1
- ERRORS.append(f'TC-C04-API-001 exception: {e}')
- # ================================================================
- # TC-C04-API-002: Get prompt detail
- # ================================================================
- section('TC-C04-API-002: Get prompt detail')
- try:
- detail = manager.get_prompt_detail('completeness_check')
- check(detail is not None, 'get_prompt_detail returns non-None')
- if detail:
- check('name' in detail and detail['name'] == 'completeness_check', 'detail has name')
- check('version' in detail, 'detail has version')
- check('time' in detail, 'detail has time')
- check('chain' in detail, 'detail has chain')
- check('is_current' in detail, 'detail has is_current')
- check('system_prompt' in detail and len(detail['system_prompt']) > 0, 'system_prompt not empty')
- check('user_prompt' in detail and len(detail['user_prompt']) > 0, 'user_prompt not empty')
- check('note' in detail, 'detail has note')
- check('variables' in detail, 'detail has variables')
- check('file_path' in detail, 'detail has file_path')
- variables = detail.get('variables', [])
- check(len(variables) > 0, f'variables has {len(variables)} items: {variables}')
- # review_content comes from user_prompt; review_references is in system_prompt only
- check('review_content' in variables, 'variables contains review_content')
- except Exception as e:
- print(f' [EXCEPTION] {e}')
- traceback.print_exc()
- FAIL += 1
- ERRORS.append(f'TC-C04-API-002 exception: {e}')
- # ================================================================
- # TC-C04-API-003: Save new version and set as current
- # ================================================================
- section('TC-C04-API-003: Save new version')
- try:
- detail_before = manager.get_prompt_detail('completeness_check')
- print(f' Version before save: {detail_before["version"] if detail_before else "N/A"}')
- new_system = '你是一个专业的施工方案完整性审查专家。测试新版本内容。'
- new_user = '请审查以下施工方案内容的完整性:\n方案内容:{review_content}\n参考依据:{review_references}'
- result = manager.save_new_version(
- 'completeness_check',
- system_prompt=new_system,
- user_prompt=new_user,
- note='测试保存新版本',
- set_current=True,
- )
- check(result is not None, 'save_new_version returns non-None')
- version = result.get('version', '')
- check(version.startswith('v'), f'version format: {version}')
- major = int(version.lstrip('v').split('.')[0])
- check(major >= 2, f'version incremented: {version} (major >= 2)')
- check('name' in result and result['name'] == 'completeness_check', 'result has name')
- import glob
- ver_files = glob.glob(os.path.join(VERSIONS_DIR, 'completeness_check', '*.yaml'))
- check(len(ver_files) >= 2, f'version files >= 2 (actual: {len(ver_files)})')
- detail_after = manager.get_prompt_detail('completeness_check')
- if detail_after:
- check(detail_after['system_prompt'] == new_system, 'main file system_prompt updated')
- check(detail_after['user_prompt'] == new_user, 'main file user_prompt updated')
- new_ver_data = manager._load_version_file('completeness_check', version)
- if new_ver_data:
- check(new_ver_data.get('is_current') == True, f'version file {version} is_current=true')
- print(f' Version after save: {version}')
- except Exception as e:
- print(f' [EXCEPTION] {e}')
- traceback.print_exc()
- FAIL += 1
- ERRORS.append(f'TC-C04-API-003 exception: {e}')
- # ================================================================
- # TC-C04-API-004: Version comparison (Diff)
- # ================================================================
- section('TC-C04-API-004: Version comparison (Diff)')
- try:
- versions = manager.get_versions('completeness_check')
- check(len(versions) >= 2, f'at least 2 versions (actual: {len(versions)})')
- if len(versions) >= 2:
- v1 = versions[-1]['version']
- v2 = versions[0]['version']
- diff_result = manager.compare_versions('completeness_check', v1, v2)
- check(diff_result is not None, 'compare_versions returns non-None')
- check('name' in diff_result, 'diff has name')
- check('base_version' in diff_result, 'diff has base_version')
- check('target_version' in diff_result, 'diff has target_version')
- check('diffs' in diff_result, 'diff has diffs')
- diffs = diff_result.get('diffs', [])
- check(len(diffs) == 2, f'diffs has 2 sections (actual: {len(diffs)})')
- for d in diffs:
- check('section' in d, 'diff item has section')
- check('type' in d, 'diff item has type')
- check('lines' in d, 'diff item has lines')
- check(d['type'] == 'text_diff', f'diff type is text_diff')
- sys_diff = [d for d in diffs if d['section'] == 'system_prompt']
- if sys_diff:
- lines = sys_diff[0]['lines']
- check(len(lines) > 0, f'system_prompt diff lines > 0 (actual: {len(lines)})')
- for line in lines[:3]:
- check('type' in line and line['type'] in ('add', 'del', 'ctx'),
- f'line has valid type ({line.get("type")})')
- check('text' in line, 'line has text')
- except Exception as e:
- print(f' [EXCEPTION] {e}')
- traceback.print_exc()
- FAIL += 1
- ERRORS.append(f'TC-C04-API-004 exception: {e}')
- # ================================================================
- # TC-C04-API-005: Activate version
- # ================================================================
- section('TC-C04-API-005: Activate version')
- try:
- versions = manager.get_versions('completeness_check')
- check(len(versions) >= 2, f'at least 2 versions (actual: {len(versions)})')
- if len(versions) >= 2:
- oldest_ver = versions[-1]['version']
- print(f' Activating version: {oldest_ver}')
- old_detail = manager.get_prompt_detail('completeness_check', version=oldest_ver)
- check(old_detail is not None, f'can read {oldest_ver} version')
- if old_detail:
- old_system = old_detail['system_prompt']
- act_result = manager.activate_version('completeness_check', oldest_ver)
- check(act_result.get('success') == True, 'activate_version returns success=true')
- check(act_result.get('name') == 'completeness_check', 'result has name')
- check(act_result.get('version') == oldest_ver, f'result version is {oldest_ver}')
- current = manager.get_prompt_detail('completeness_check')
- if current and old_detail:
- check(current['system_prompt'] == old_detail['system_prompt'],
- 'main file system_prompt matches activated version')
- if old_detail:
- ver_data = manager._load_version_file('completeness_check', oldest_ver)
- if ver_data:
- check(ver_data.get('is_current') == True,
- f'{oldest_ver} has is_current=true')
- except Exception as e:
- print(f' [EXCEPTION] {e}')
- traceback.print_exc()
- FAIL += 1
- ERRORS.append(f'TC-C04-API-005 exception: {e}')
- # ================================================================
- # TC-C04-API-006: Rollback version
- # ================================================================
- section('TC-C04-API-006: Rollback version')
- try:
- rollback_result = manager.rollback_version('completeness_check', 'v1.0')
- check(rollback_result.get('success') == True, 'rollback_version returns success=true')
- check(rollback_result.get('name') == 'completeness_check', 'result has name')
- check(rollback_result.get('version') == 'v1.0', 'result version is v1.0')
- current = manager.get_prompt_detail('completeness_check')
- v1_detail = manager.get_prompt_detail('completeness_check', version='v1.0')
- if current and v1_detail:
- check(current['system_prompt'] == v1_detail['system_prompt'],
- 'after rollback, main file system_prompt matches v1.0')
- check(current['user_prompt'] == v1_detail['user_prompt'],
- 'after rollback, main file user_prompt matches v1.0')
- except Exception as e:
- print(f' [EXCEPTION] {e}')
- traceback.print_exc()
- FAIL += 1
- ERRORS.append(f'TC-C04-API-006 exception: {e}')
- # ================================================================
- # TC-C04-EDGE-001: No version specified returns current
- # ================================================================
- section('TC-C04-EDGE-001: No version returns current')
- try:
- result = manager.save_new_version(
- 'completeness_check',
- system_prompt='EDGE-001 test new version content',
- user_prompt='Review content: {review_content}',
- note='EDGE-001 test',
- set_current=True,
- )
- print(f' Current version: {result["version"]}')
- detail_no_ver = manager.get_prompt_detail('completeness_check')
- if detail_no_ver:
- check(detail_no_ver['is_current'] == True, 'no version param: is_current=True')
- check(detail_no_ver['version'] == result['version'],
- f'returns current version ({detail_no_ver["version"]})')
- detail_with_ver = manager.get_prompt_detail('completeness_check', version='v1.0')
- if detail_with_ver:
- check(detail_with_ver['is_current'] == False,
- 'with v1.0 param: is_current=False (current is later version)')
- check(detail_with_ver['version'] == 'v1.0',
- f'with v1.0 param: version=v1.0')
- except Exception as e:
- print(f' [EXCEPTION] {e}')
- traceback.print_exc()
- FAIL += 1
- ERRORS.append(f'TC-C04-EDGE-001 exception: {e}')
- # ================================================================
- # TC-C04-EDGE-002: Special characters in prompt name
- # ================================================================
- section('TC-C04-EDGE-002: Special chars in prompt name')
- try:
- detail_special = manager.get_prompt_detail('non_parameter_compliance_check')
- check(detail_special is not None,
- 'get_prompt_detail("non_parameter_compliance_check") returns non-None')
- if detail_special:
- check(len(detail_special.get('system_prompt', '')) > 0, 'system_prompt not empty')
- check(len(detail_special.get('user_prompt', '')) > 0, 'user_prompt not empty')
- ver_files = manager._list_version_files('non_parameter_compliance_check')
- check(len(ver_files) >= 1,
- f'version files >= 1 (actual: {len(ver_files)})')
- expected_dir = os.path.join(VERSIONS_DIR, 'non_parameter_compliance_check')
- check(os.path.exists(expected_dir),
- f'version dir exists: {expected_dir}')
- if ver_files:
- ver_path = os.path.join(expected_dir, ver_files[0])
- check(os.path.exists(ver_path), f'version file exists: {ver_files[0]}')
- except Exception as e:
- print(f' [EXCEPTION] {e}')
- traceback.print_exc()
- FAIL += 1
- ERRORS.append(f'TC-C04-EDGE-002 exception: {e}')
- # ================================================================
- # TC-C04-ERROR-001: Empty system prompt handled by API layer
- # ================================================================
- section('TC-C04-ERROR-001: Empty system prompt')
- try:
- try:
- manager.save_new_version(
- name='nonexistent_prompt',
- system_prompt='test',
- user_prompt='test',
- note='',
- )
- check(False, 'nonexistent prompt should raise ValueError')
- except ValueError:
- check(True, 'nonexistent prompt raises ValueError')
- except Exception as e:
- print(f' [EXCEPTION] {e}')
- traceback.print_exc()
- FAIL += 1
- ERRORS.append(f'TC-C04-ERROR-001 exception: {e}')
- # ================================================================
- # TC-C04-ERROR-002: Non-existent prompt name returns None/empty
- # ================================================================
- section('TC-C04-ERROR-002: Non-existent prompt name')
- try:
- detail = manager.get_prompt_detail('nonexistent_check')
- check(detail is None, 'nonexistent prompt detail returns None')
- versions = manager.get_versions('nonexistent_check')
- check(isinstance(versions, list), 'nonexistent prompt versions returns list')
- check(len(versions) == 0, 'nonexistent prompt versions returns empty list')
- except Exception as e:
- print(f' [EXCEPTION] {e}')
- traceback.print_exc()
- FAIL += 1
- ERRORS.append(f'TC-C04-ERROR-002 exception: {e}')
- # ================================================================
- # TC-C04-ERROR-003: Corrupted version file gracefully skipped
- # ================================================================
- section('TC-C04-ERROR-003: Corrupted version file')
- try:
- completeness_dir = os.path.join(VERSIONS_DIR, 'completeness_check')
- bad_file = os.path.join(completeness_dir, 'corrupt.yaml')
- with open(bad_file, 'w', encoding='utf-8') as f:
- f.write('{invalid: yaml: content\n broken indent\n')
- try:
- versions = manager.get_versions('completeness_check')
- check(True, f'corrupted file skipped gracefully ({len(versions)} valid versions)')
- for v in versions:
- check(v['version'] != 'corrupt', 'corrupt file not in version list')
- except Exception as e:
- check(False, f'corrupted file should not raise exception: {e}')
- if os.path.exists(bad_file):
- os.remove(bad_file)
- except Exception as e:
- print(f' [EXCEPTION] {e}')
- traceback.print_exc()
- FAIL += 1
- ERRORS.append(f'TC-C04-ERROR-003 exception: {e}')
- # ================================================================
- # Extra: _extract_variables
- # ================================================================
- section('Extra: _extract_variables utility')
- try:
- vars = _extract_variables('Hello {name}, age is {age}')
- check('name' in vars and 'age' in vars, f'extracted: {vars}')
- vars_empty = _extract_variables('no variables')
- check(len(vars_empty) == 0, 'no variables returns empty list')
- vars_multi = _extract_variables('{a} {b} {c} {a}')
- check(len(set(vars_multi)) == 3, f'3 unique vars: {set(vars_multi)}')
- except Exception as e:
- print(f' [EXCEPTION] {e}')
- traceback.print_exc()
- FAIL += 1
- ERRORS.append(f'_extract_variables exception: {e}')
- # ================================================================
- # Extra: _make_diff_lines
- # ================================================================
- section('Extra: _make_diff_lines utility')
- try:
- a = 'line1\nline2\nline3'
- b = 'line1\nline2_modified\nline3'
- lines = _make_diff_lines(a, b)
- check(len(lines) > 0, f'Diff has {len(lines)} lines')
- has_ctx = any(l['type'] == 'ctx' for l in lines)
- has_del = any(l['type'] == 'del' for l in lines)
- has_add = any(l['type'] == 'add' for l in lines)
- check(has_ctx, 'Diff has ctx lines')
- check(has_del, 'Diff has del lines')
- check(has_add, 'Diff has add lines')
- same_diff = _make_diff_lines('abc', 'abc')
- check(len(same_diff) == 0, 'identical content => empty diff')
- except Exception as e:
- print(f' [EXCEPTION] {e}')
- traceback.print_exc()
- FAIL += 1
- ERRORS.append(f'_make_diff_lines exception: {e}')
- # ================================================================
- # Extra: get_all_prompts filtering
- # ================================================================
- section('Extra: get_all_prompts filtering')
- try:
- all_items = manager.get_all_prompts()
- completeness_items = manager.get_all_prompts(chain_filter='完整性')
- for item in completeness_items:
- check(item['chain'] == '完整性', f'filtered chain 完整性 (actual: {item["chain"]})')
- search_items = manager.get_all_prompts(search='completeness')
- for item in search_items:
- check('completeness' in item['name'].lower(),
- f'search result contains completeness (actual: {item["name"]})')
- except Exception as e:
- print(f' [EXCEPTION] {e}')
- traceback.print_exc()
- FAIL += 1
- ERRORS.append(f'Filtering exception: {e}')
- # ================================================================
- # Extra: First run initialization (run early, before state changes)
- # ================================================================
- section('Extra: First run initialization')
- try:
- for prompt_name in PROMPT_FILE_MAP:
- version_dir = os.path.join(VERSIONS_DIR, prompt_name)
- check(os.path.exists(version_dir), f'{prompt_name} version dir exists')
- v1_file = os.path.join(version_dir, 'v1.0.yaml')
- check(os.path.exists(v1_file), f'{prompt_name} v1.0.yaml exists')
- # Verify v1.0 matches main file at initialization time
- # (Note: run this BEFORE save/activate tests that modify main file)
- if INITIAL_CC_V1 and INITIAL_CC_MAIN:
- check(INITIAL_CC_V1['system_prompt'] == INITIAL_CC_MAIN['system_prompt'],
- 'v1.0 system_prompt matches main file at init')
- check(INITIAL_CC_V1['user_prompt_template'] == INITIAL_CC_MAIN['user_prompt'],
- 'v1.0 user_prompt matches main file at init')
- except Exception as e:
- print(f' [EXCEPTION] {e}')
- traceback.print_exc()
- FAIL += 1
- ERRORS.append(f'Initialization exception: {e}')
- # ================================================================
- # Summary
- # ================================================================
- section('Test Results')
- total = PASS + FAIL
- print(f' Total: {total} Passed: {PASS} Failed: {FAIL}')
- if ERRORS:
- print(f'\n Failed details:')
- for i, err in enumerate(ERRORS, 1):
- print(f' {i}. {err}')
- print(f'\n {"=" * 20} {"ALL PASSED!" if FAIL == 0 else "SOME FAILED!"} {"=" * 20}')
- # ================================================================
- # Cleanup: restore original versions directory
- # ================================================================
- if versions_backup and os.path.exists(versions_backup):
- if os.path.exists(VERSIONS_DIR):
- shutil.rmtree(VERSIONS_DIR)
- shutil.copytree(versions_backup, VERSIONS_DIR)
- shutil.rmtree(versions_backup)
- print(f'\n Restored original versions directory')
- else:
- print(f'\n Note: test-created version files left at {VERSIONS_DIR}')
- sys.exit(0 if FAIL == 0 else 1)
|