"""Tests for prompt_constants module. Validates that all constants ported from npm src/constants/ are present, correctly typed, and that helper functions behave as expected. """ from __future__ import annotations import os import platform from unittest.mock import patch import pytest from src.prompt_constants import ( # Product metadata PRODUCT_URL, CLAUDE_AI_BASE_URL, # System prompt prefixes DEFAULT_SYSPROMPT_PREFIX, AGENT_SDK_CLAUDE_CODE_PRESET_PREFIX, AGENT_SDK_PREFIX, CLI_SYSPROMPT_PREFIXES, # Cyber risk CYBER_RISK_INSTRUCTION, # API limits API_IMAGE_MAX_BASE64_SIZE, IMAGE_TARGET_RAW_SIZE, IMAGE_MAX_WIDTH, IMAGE_MAX_HEIGHT, PDF_TARGET_RAW_SIZE, API_PDF_MAX_PAGES, PDF_EXTRACT_SIZE_THRESHOLD, PDF_MAX_EXTRACT_SIZE, PDF_MAX_PAGES_PER_READ, PDF_AT_MENTION_INLINE_THRESHOLD, API_MAX_MEDIA_PER_REQUEST, # Tool limits DEFAULT_MAX_RESULT_SIZE_CHARS, MAX_TOOL_RESULT_TOKENS, BYTES_PER_TOKEN, MAX_TOOL_RESULT_BYTES, MAX_TOOL_RESULTS_PER_MESSAGE_CHARS, TOOL_SUMMARY_MAX_LENGTH, # Spinner verbs SPINNER_VERBS, # Turn completion verbs TURN_COMPLETION_VERBS, # Figures BLACK_CIRCLE, BULLET_OPERATOR, TEARDROP_ASTERISK, UP_ARROW, DOWN_ARROW, LIGHTNING_BOLT, EFFORT_LOW, EFFORT_MEDIUM, EFFORT_HIGH, EFFORT_MAX, PLAY_ICON, PAUSE_ICON, REFRESH_ARROW, CHANNEL_ARROW, INJECTED_ARROW, FORK_GLYPH, DIAMOND_OPEN, DIAMOND_FILLED, REFERENCE_MARK, FLAG_ICON, BLOCKQUOTE_BAR, HEAVY_HORIZONTAL, BRIDGE_SPINNER_FRAMES, BRIDGE_READY_INDICATOR, BRIDGE_FAILED_INDICATOR, # XML tags COMMAND_NAME_TAG, COMMAND_MESSAGE_TAG, COMMAND_ARGS_TAG, BASH_INPUT_TAG, BASH_STDOUT_TAG, BASH_STDERR_TAG, LOCAL_COMMAND_STDOUT_TAG, LOCAL_COMMAND_STDERR_TAG, LOCAL_COMMAND_CAVEAT_TAG, TERMINAL_OUTPUT_TAGS, TICK_TAG, TASK_NOTIFICATION_TAG, TASK_ID_TAG, TOOL_USE_ID_TAG, TASK_TYPE_TAG, OUTPUT_FILE_TAG, STATUS_TAG, SUMMARY_TAG, REASON_TAG, WORKTREE_TAG, WORKTREE_PATH_TAG, WORKTREE_BRANCH_TAG, ULTRAPLAN_TAG, REMOTE_REVIEW_TAG, REMOTE_REVIEW_PROGRESS_TAG, TEAMMATE_MESSAGE_TAG, CHANNEL_MESSAGE_TAG, CHANNEL_TAG, CROSS_SESSION_MESSAGE_TAG, FORK_BOILERPLATE_TAG, FORK_DIRECTIVE_PREFIX, COMMON_HELP_ARGS, COMMON_INFO_ARGS, # Messages NO_CONTENT_MESSAGE, # Date utilities get_local_iso_date, get_session_start_date, reset_session_start_date, get_local_month_year, # System prompt section caching SystemPromptSection, system_prompt_section, dangerous_uncached_system_prompt_section, resolve_system_prompt_sections, clear_system_prompt_sections, # Output styles DEFAULT_OUTPUT_STYLE_NAME, OutputStyleConfig, OUTPUT_STYLE_CONFIGS, # Knowledge cutoff FRONTIER_MODEL_NAME, get_knowledge_cutoff, CLAUDE_MODEL_IDS, # Prompt sections HOOKS_SECTION, SYSTEM_REMINDERS_SECTION, SUMMARIZE_TOOL_RESULTS_SECTION, DEFAULT_AGENT_PROMPT, SYSTEM_PROMPT_DYNAMIC_BOUNDARY, get_language_section, get_output_style_section, get_scratchpad_instructions, # Error IDs E_TOOL_USE_SUMMARY_GENERATION_FAILED, ) # ========================================================================= # Product metadata # ========================================================================= class TestProductMetadata: def test_product_url(self): assert PRODUCT_URL == "https://claude.com/claude-code" def test_claude_ai_base_url(self): assert CLAUDE_AI_BASE_URL == "https://claude.ai" # ========================================================================= # System prompt prefixes # ========================================================================= class TestSystemPromptPrefixes: def test_default_prefix_content(self): assert "Claude Code" in DEFAULT_SYSPROMPT_PREFIX assert "Anthropic" in DEFAULT_SYSPROMPT_PREFIX def test_agent_sdk_prefix_content(self): assert "Agent SDK" in AGENT_SDK_PREFIX def test_cli_sysprompt_prefixes_is_frozenset(self): assert isinstance(CLI_SYSPROMPT_PREFIXES, frozenset) assert len(CLI_SYSPROMPT_PREFIXES) == 3 def test_all_prefixes_in_set(self): assert DEFAULT_SYSPROMPT_PREFIX in CLI_SYSPROMPT_PREFIXES assert AGENT_SDK_CLAUDE_CODE_PRESET_PREFIX in CLI_SYSPROMPT_PREFIXES assert AGENT_SDK_PREFIX in CLI_SYSPROMPT_PREFIXES # ========================================================================= # Cyber risk # ========================================================================= class TestCyberRisk: def test_instruction_mentions_ctf(self): assert "CTF" in CYBER_RISK_INSTRUCTION def test_instruction_mentions_dos(self): assert "DoS" in CYBER_RISK_INSTRUCTION # ========================================================================= # API limits # ========================================================================= class TestAPILimits: def test_image_base64_size(self): assert API_IMAGE_MAX_BASE64_SIZE == 5 * 1024 * 1024 def test_image_target_raw_size(self): assert IMAGE_TARGET_RAW_SIZE == (API_IMAGE_MAX_BASE64_SIZE * 3) // 4 def test_image_dimensions(self): assert IMAGE_MAX_WIDTH == 2000 assert IMAGE_MAX_HEIGHT == 2000 def test_pdf_target_raw_size(self): assert PDF_TARGET_RAW_SIZE == 20 * 1024 * 1024 def test_pdf_max_pages(self): assert API_PDF_MAX_PAGES == 100 def test_pdf_extract_threshold(self): assert PDF_EXTRACT_SIZE_THRESHOLD == 3 * 1024 * 1024 def test_pdf_max_extract_size(self): assert PDF_MAX_EXTRACT_SIZE == 100 * 1024 * 1024 def test_pdf_pages_per_read(self): assert PDF_MAX_PAGES_PER_READ == 20 def test_pdf_inline_threshold(self): assert PDF_AT_MENTION_INLINE_THRESHOLD == 10 def test_media_per_request(self): assert API_MAX_MEDIA_PER_REQUEST == 100 # ========================================================================= # Tool limits # ========================================================================= class TestToolLimits: def test_default_max_result_size(self): assert DEFAULT_MAX_RESULT_SIZE_CHARS == 50_000 def test_max_tool_result_tokens(self): assert MAX_TOOL_RESULT_TOKENS == 100_000 def test_bytes_per_token(self): assert BYTES_PER_TOKEN == 4 def test_max_tool_result_bytes_derived(self): assert MAX_TOOL_RESULT_BYTES == MAX_TOOL_RESULT_TOKENS * BYTES_PER_TOKEN assert MAX_TOOL_RESULT_BYTES == 400_000 def test_max_per_message_chars(self): assert MAX_TOOL_RESULTS_PER_MESSAGE_CHARS == 200_000 def test_tool_summary_max_length(self): assert TOOL_SUMMARY_MAX_LENGTH == 50 # ========================================================================= # Spinner verbs # ========================================================================= class TestSpinnerVerbs: def test_is_tuple(self): assert isinstance(SPINNER_VERBS, tuple) def test_count(self): assert len(SPINNER_VERBS) == 187 def test_first_verb(self): assert SPINNER_VERBS[0] == "Accomplishing" def test_last_verb(self): assert SPINNER_VERBS[-1] == "Zigzagging" def test_all_strings(self): for verb in SPINNER_VERBS: assert isinstance(verb, str) def test_contains_clauding(self): assert "Clauding" in SPINNER_VERBS def test_contains_thinking(self): assert "Thinking" in SPINNER_VERBS def test_no_duplicates(self): assert len(SPINNER_VERBS) == len(set(SPINNER_VERBS)) # ========================================================================= # Turn completion verbs # ========================================================================= class TestTurnCompletionVerbs: def test_is_tuple(self): assert isinstance(TURN_COMPLETION_VERBS, tuple) def test_count(self): assert len(TURN_COMPLETION_VERBS) == 8 def test_contains_worked(self): assert "Worked" in TURN_COMPLETION_VERBS def test_contains_baked(self): assert "Baked" in TURN_COMPLETION_VERBS def test_all_past_tense(self): # All end in 'd' (past tense) for verb in TURN_COMPLETION_VERBS: assert verb[-1] == "d", f"{verb} doesn't end with 'd'" # ========================================================================= # Figures / UI symbols # ========================================================================= class TestFigures: def test_black_circle_is_string(self): assert isinstance(BLACK_CIRCLE, str) assert len(BLACK_CIRCLE) == 1 def test_effort_symbols_are_distinct(self): symbols = {EFFORT_LOW, EFFORT_MEDIUM, EFFORT_HIGH, EFFORT_MAX} assert len(symbols) == 4 def test_arrows(self): assert UP_ARROW == "\u2191" assert DOWN_ARROW == "\u2193" def test_bridge_spinner_frames(self): assert isinstance(BRIDGE_SPINNER_FRAMES, tuple) assert len(BRIDGE_SPINNER_FRAMES) == 4 def test_play_pause_icons(self): assert PLAY_ICON == "\u25b6" assert PAUSE_ICON == "\u23f8" def test_diamond_symbols(self): assert DIAMOND_OPEN == "\u25c7" assert DIAMOND_FILLED == "\u25c6" # ========================================================================= # XML tag constants # ========================================================================= class TestXMLTags: def test_command_tags(self): assert COMMAND_NAME_TAG == "command-name" assert COMMAND_MESSAGE_TAG == "command-message" assert COMMAND_ARGS_TAG == "command-args" def test_bash_tags(self): assert BASH_INPUT_TAG == "bash-input" assert BASH_STDOUT_TAG == "bash-stdout" assert BASH_STDERR_TAG == "bash-stderr" def test_terminal_output_tags_tuple(self): assert isinstance(TERMINAL_OUTPUT_TAGS, tuple) assert len(TERMINAL_OUTPUT_TAGS) == 6 assert BASH_INPUT_TAG in TERMINAL_OUTPUT_TAGS assert LOCAL_COMMAND_STDOUT_TAG in TERMINAL_OUTPUT_TAGS def test_tick_tag(self): assert TICK_TAG == "tick" def test_task_tags(self): assert TASK_NOTIFICATION_TAG == "task-notification" assert TASK_ID_TAG == "task-id" assert TOOL_USE_ID_TAG == "tool-use-id" def test_worktree_tags(self): assert WORKTREE_TAG == "worktree" assert WORKTREE_PATH_TAG == "worktreePath" def test_fork_tags(self): assert FORK_BOILERPLATE_TAG == "fork-boilerplate" assert FORK_DIRECTIVE_PREFIX == "Your directive: " def test_common_help_args(self): assert isinstance(COMMON_HELP_ARGS, tuple) assert "help" in COMMON_HELP_ARGS assert "-h" in COMMON_HELP_ARGS assert "--help" in COMMON_HELP_ARGS def test_common_info_args(self): assert isinstance(COMMON_INFO_ARGS, tuple) assert "list" in COMMON_INFO_ARGS assert "status" in COMMON_INFO_ARGS assert "?" in COMMON_INFO_ARGS # ========================================================================= # Message constants # ========================================================================= class TestMessages: def test_no_content_message(self): assert NO_CONTENT_MESSAGE == "(no content)" # ========================================================================= # Date utilities # ========================================================================= class TestDateUtilities: def test_get_local_iso_date_format(self): d = get_local_iso_date() parts = d.split("-") assert len(parts) == 3 assert len(parts[0]) == 4 # year assert len(parts[1]) == 2 # month assert len(parts[2]) == 2 # day def test_get_local_iso_date_override(self): with patch.dict(os.environ, {"CLAUDE_CODE_OVERRIDE_DATE": "2025-01-15"}): assert get_local_iso_date() == "2025-01-15" def test_get_session_start_date_memoised(self): reset_session_start_date() d1 = get_session_start_date() d2 = get_session_start_date() assert d1 == d2 def test_reset_session_start_date(self): reset_session_start_date() d = get_session_start_date() assert isinstance(d, str) reset_session_start_date() # After reset, should still return valid date d2 = get_session_start_date() assert isinstance(d2, str) def test_get_local_month_year_format(self): result = get_local_month_year() parts = result.split() assert len(parts) == 2 assert parts[1].isdigit() assert len(parts[1]) == 4 def test_get_local_month_year_override(self): with patch.dict(os.environ, {"CLAUDE_CODE_OVERRIDE_DATE": "2026-02-15"}): assert get_local_month_year() == "February 2026" # ========================================================================= # System prompt section caching # ========================================================================= class TestSystemPromptSections: def setup_method(self): clear_system_prompt_sections() def test_system_prompt_section_creates_cached(self): s = system_prompt_section("test", lambda: "hello") assert s.name == "test" assert s.cache_break is False def test_dangerous_uncached_creates_volatile(self): s = dangerous_uncached_system_prompt_section("test", lambda: "hello", "reason") assert s.name == "test" assert s.cache_break is True def test_resolve_caches_sections(self): call_count = 0 def compute(): nonlocal call_count call_count += 1 return f"value-{call_count}" sections = [system_prompt_section("s1", compute)] r1 = resolve_system_prompt_sections(sections) r2 = resolve_system_prompt_sections(sections) assert r1 == ["value-1"] assert r2 == ["value-1"] # cached assert call_count == 1 def test_uncached_recomputes(self): call_count = 0 def compute(): nonlocal call_count call_count += 1 return f"value-{call_count}" sections = [dangerous_uncached_system_prompt_section("s2", compute, "test")] r1 = resolve_system_prompt_sections(sections) r2 = resolve_system_prompt_sections(sections) assert r1 == ["value-1"] assert r2 == ["value-2"] # recomputed assert call_count == 2 def test_clear_resets_cache(self): call_count = 0 def compute(): nonlocal call_count call_count += 1 return f"value-{call_count}" sections = [system_prompt_section("s3", compute)] resolve_system_prompt_sections(sections) clear_system_prompt_sections() r = resolve_system_prompt_sections(sections) assert r == ["value-2"] assert call_count == 2 def test_resolve_handles_none(self): sections = [system_prompt_section("nil", lambda: None)] r = resolve_system_prompt_sections(sections) assert r == [None] def test_multiple_sections(self): sections = [ system_prompt_section("a", lambda: "alpha"), system_prompt_section("b", lambda: "beta"), system_prompt_section("c", lambda: None), ] r = resolve_system_prompt_sections(sections) assert r == ["alpha", "beta", None] # ========================================================================= # Output styles # ========================================================================= class TestOutputStyles: def test_default_style_name(self): assert DEFAULT_OUTPUT_STYLE_NAME == "default" def test_default_style_is_none(self): assert OUTPUT_STYLE_CONFIGS[DEFAULT_OUTPUT_STYLE_NAME] is None def test_explanatory_exists(self): style = OUTPUT_STYLE_CONFIGS["Explanatory"] assert style is not None assert style.name == "Explanatory" assert "explains" in style.description def test_learning_exists(self): style = OUTPUT_STYLE_CONFIGS["Learning"] assert style is not None assert style.name == "Learning" assert "hands-on" in style.description def test_output_style_config_frozen(self): style = OutputStyleConfig( name="Test", description="desc", prompt="prompt" ) with pytest.raises(Exception): style.name = "other" # type: ignore[misc] # ========================================================================= # Knowledge cutoff # ========================================================================= class TestKnowledgeCutoff: def test_frontier_model_name(self): assert FRONTIER_MODEL_NAME == "Claude Opus 4.6" def test_opus_46_cutoff(self): assert get_knowledge_cutoff("claude-opus-4-6-20250601") == "May 2025" def test_sonnet_46_cutoff(self): assert get_knowledge_cutoff("claude-sonnet-4-6-20250801") == "August 2025" def test_opus_45_cutoff(self): assert get_knowledge_cutoff("claude-opus-4-5-20250601") == "May 2025" def test_haiku_cutoff(self): assert get_knowledge_cutoff("claude-haiku-4-20250201") == "February 2025" def test_sonnet_4_cutoff(self): assert get_knowledge_cutoff("claude-sonnet-4-20250114") == "January 2025" def test_unknown_model_returns_none(self): assert get_knowledge_cutoff("gpt-4-turbo") is None def test_claude_model_ids(self): assert "opus" in CLAUDE_MODEL_IDS assert "sonnet" in CLAUDE_MODEL_IDS assert "haiku" in CLAUDE_MODEL_IDS # ========================================================================= # Prompt section helpers # ========================================================================= class TestPromptSectionHelpers: def test_hooks_section_content(self): assert "hooks" in HOOKS_SECTION assert "user-prompt-submit-hook" in HOOKS_SECTION def test_system_reminders_section(self): assert "system-reminder" in SYSTEM_REMINDERS_SECTION def test_summarize_tool_results(self): assert "tool results" in SUMMARIZE_TOOL_RESULTS_SECTION def test_default_agent_prompt(self): assert "agent for Claude Code" in DEFAULT_AGENT_PROMPT def test_dynamic_boundary(self): assert SYSTEM_PROMPT_DYNAMIC_BOUNDARY == "__SYSTEM_PROMPT_DYNAMIC_BOUNDARY__" def test_language_section_none_when_no_preference(self): assert get_language_section(None) is None assert get_language_section("") is None def test_language_section_with_preference(self): result = get_language_section("Spanish") assert result is not None assert "Spanish" in result assert "# Language" in result def test_output_style_section_none_when_no_config(self): assert get_output_style_section(None) is None def test_output_style_section_with_config(self): config = OutputStyleConfig( name="TestStyle", description="A test style", prompt="Be concise.", ) result = get_output_style_section(config) assert result is not None assert "# Output Style: TestStyle" in result assert "Be concise." in result def test_scratchpad_none_when_no_dir(self): assert get_scratchpad_instructions(None) is None assert get_scratchpad_instructions("") is None def test_scratchpad_with_dir(self): result = get_scratchpad_instructions("/tmp/session-123") assert result is not None assert "/tmp/session-123" in result assert "# Scratchpad Directory" in result assert "temporary files" in result # ========================================================================= # Error IDs # ========================================================================= class TestErrorIDs: def test_tool_use_summary_error(self): assert E_TOOL_USE_SUMMARY_GENERATION_FAILED == 344