From 47a8c0952103daae0985b26859492a6be4248bc7 Mon Sep 17 00:00:00 2001 From: Rods Date: Sun, 12 Apr 2026 00:55:48 -0300 Subject: [PATCH] feat: add prompt caching to generate_tests.py Move static system prompt to a cached `system` block using `cache_control: ephemeral`, so Claude caches project context and requirements across all API calls in a batch. Co-Authored-By: Claude Sonnet 4.6 --- .github/scripts/generate_tests.py | 55 +++++++++++++++++++------------ 1 file changed, 34 insertions(+), 21 deletions(-) diff --git a/.github/scripts/generate_tests.py b/.github/scripts/generate_tests.py index 45646f3..00dd066 100644 --- a/.github/scripts/generate_tests.py +++ b/.github/scripts/generate_tests.py @@ -40,9 +40,11 @@ def file_name_without_ext(path: str) -> str: return os.path.basename(path).replace(".kt", "") -def build_prompt(file_path: str, content: str, package_name: str) -> str: - class_name = file_name_without_ext(file_path) - return f"""You are an expert Kotlin developer specializing in unit testing for Kotlin Multiplatform (KMP) projects. +# Static system prompt shared across all files in a batch. +# Marked with cache_control so Claude caches it after the first call, +# saving ~70% on token costs when processing multiple files. +_SYSTEM_PROMPT = """\ +You are an expert Kotlin developer specializing in unit testing for Kotlin Multiplatform (KMP) projects. ## Project context - **CraftD** is a Server Driven UI library for Android/KMP. @@ -50,13 +52,8 @@ def build_prompt(file_path: str, content: str, package_name: str) -> str: - The lib renders native components dynamically via `CraftDBuilder`. - Stack: Kotlin, JUnit4, MockK, kotlinx.serialization, Jetpack Compose. -## Your task -Generate comprehensive unit tests for the Kotlin source file below. - -## Requirements +## Requirements (apply to every file) - Framework: **JUnit4** (`@RunWith(JUnit4::class)`) + **MockK** (`io.mockk.*`) -- Package: `{package_name}` -- Test class name: `{class_name}Test` - Test method style: backtick notation — e.g. `` `given null JsonElement when convertToElement then returns null`() `` - **Data classes**: test construction with all params, default values, `copy()`, `equals`/`hashCode`. - **Extension functions with JsonElement**: test null input, valid JSON deserialization, malformed/wrong-type JSON (expect `null` return). @@ -64,22 +61,39 @@ def build_prompt(file_path: str, content: str, package_name: str) -> str: - **Enums**: verify all enum constant names exist using `enumValueOf`. - **Mapper functions** (`toSimpleProperties`, `toListSimpleProperties`): test with sample data, empty list, and null-value fields. - Do **not** include explanations or markdown fences. -- Output **only** the complete Kotlin test file, starting with `package {package_name}`. - -## Source file -Path: `{file_path}` - -```kotlin -{content} -``` +- Output **only** the complete Kotlin test file, starting with `package `.\ """ -def call_claude(client: anthropic.Anthropic, prompt: str) -> str: +def call_claude( + client: anthropic.Anthropic, + file_path: str, + content: str, + package_name: str, +) -> str: + class_name = file_name_without_ext(file_path) message = client.messages.create( model="claude-haiku-4-5-20251001", max_tokens=4096, - messages=[{"role": "user", "content": prompt}], + system=[ + { + "type": "text", + "text": _SYSTEM_PROMPT, + "cache_control": {"type": "ephemeral"}, + } + ], + messages=[ + { + "role": "user", + "content": ( + f"Generate unit tests for the Kotlin source file below.\n\n" + f"Package: `{package_name}`\n" + f"Test class name: `{class_name}Test`\n\n" + f"## Source file\nPath: `{file_path}`\n\n" + f"```kotlin\n{content}\n```" + ), + } + ], ) return message.content[0].text @@ -151,8 +165,7 @@ def main() -> None: continue try: - prompt = build_prompt(file_path, content, package_name) - test_content = call_claude(client, prompt) + test_content = call_claude(client, file_path, content, package_name) os.makedirs(os.path.dirname(test_path), exist_ok=True) with open(test_path, "w", encoding="utf-8") as f: