@@ -3041,3 +3041,254 @@ def test_prompt_change_detected_even_after_crash_workflow(pdd_test_environment):
30413041 f"Expected 'generate' or 'auto-deps' due to prompt change, got '{ decision .operation } '"
30423042 assert 'prompt' in decision .reason .lower (), \
30433043 f"Reason should mention prompt change: { decision .reason } "
3044+
3045+
3046+ # --- Issue #203: Auto-update tests based on prompt changes ---
3047+
3048+ class TestIssue203FingerprintTestPromptHash :
3049+ """Tests for the test_prompt_hash field in Fingerprint dataclass (Issue #203)."""
3050+
3051+ def test_fingerprint_has_test_prompt_hash_field (self ):
3052+ """Fingerprint dataclass should have test_prompt_hash field."""
3053+ fp = Fingerprint (
3054+ pdd_version = "1.0.0" ,
3055+ timestamp = "2024-01-01T00:00:00Z" ,
3056+ command = "test" ,
3057+ prompt_hash = "prompt_hash_123" ,
3058+ code_hash = "code_hash_456" ,
3059+ example_hash = "example_hash_789" ,
3060+ test_hash = "test_hash_abc" ,
3061+ test_files = None ,
3062+ test_prompt_hash = "prompt_hash_123" ,
3063+ )
3064+ assert hasattr (fp , 'test_prompt_hash' )
3065+ assert fp .test_prompt_hash == "prompt_hash_123"
3066+
3067+ def test_fingerprint_test_prompt_hash_defaults_to_none (self ):
3068+ """test_prompt_hash should default to None for backward compatibility."""
3069+ fp = Fingerprint (
3070+ pdd_version = "1.0.0" ,
3071+ timestamp = "2024-01-01T00:00:00Z" ,
3072+ command = "generate" ,
3073+ prompt_hash = "hash1" ,
3074+ code_hash = "hash2" ,
3075+ example_hash = "hash3" ,
3076+ test_hash = "hash4" ,
3077+ )
3078+ assert fp .test_prompt_hash is None
3079+
3080+ def test_fingerprint_serialization_includes_test_prompt_hash (self ):
3081+ """asdict should include test_prompt_hash in serialized output."""
3082+ from dataclasses import asdict
3083+ fp = Fingerprint (
3084+ pdd_version = "1.0.0" ,
3085+ timestamp = "2024-01-01T00:00:00Z" ,
3086+ command = "test" ,
3087+ prompt_hash = "p1" ,
3088+ code_hash = "c1" ,
3089+ example_hash = "e1" ,
3090+ test_hash = "t1" ,
3091+ test_files = None ,
3092+ test_prompt_hash = "p1" ,
3093+ )
3094+ data = asdict (fp )
3095+ assert 'test_prompt_hash' in data
3096+ assert data ['test_prompt_hash' ] == "p1"
3097+
3098+
3099+ class TestIssue203ReadFingerprintTestPromptHash :
3100+ """Tests for reading test_prompt_hash from fingerprint files (Issue #203)."""
3101+
3102+ def test_read_fingerprint_with_test_prompt_hash (self , pdd_test_environment ):
3103+ """read_fingerprint should correctly read test_prompt_hash field."""
3104+ fingerprint_data = {
3105+ "pdd_version" : "1.0.0" ,
3106+ "timestamp" : "2024-01-01T00:00:00Z" ,
3107+ "command" : "test" ,
3108+ "prompt_hash" : "prompt_abc" ,
3109+ "code_hash" : "code_def" ,
3110+ "example_hash" : "example_ghi" ,
3111+ "test_hash" : "test_jkl" ,
3112+ "test_files" : None ,
3113+ "test_prompt_hash" : "prompt_abc" ,
3114+ }
3115+ fp_file = get_meta_dir () / "issue203_python.json"
3116+ create_fingerprint_file (fp_file , fingerprint_data )
3117+
3118+ fp = read_fingerprint ("issue203" , "python" )
3119+
3120+ assert fp is not None
3121+ assert fp .test_prompt_hash == "prompt_abc"
3122+
3123+ def test_read_fingerprint_backward_compat_without_test_prompt_hash (self , pdd_test_environment ):
3124+ """read_fingerprint should handle old fingerprints without test_prompt_hash."""
3125+ old_fingerprint_data = {
3126+ "pdd_version" : "0.99.0" ,
3127+ "timestamp" : "2024-01-01T00:00:00Z" ,
3128+ "command" : "generate" ,
3129+ "prompt_hash" : "old_prompt" ,
3130+ "code_hash" : "old_code" ,
3131+ "example_hash" : "old_example" ,
3132+ "test_hash" : "old_test" ,
3133+ "test_files" : None ,
3134+ # No test_prompt_hash field - simulating old format
3135+ }
3136+ fp_file = get_meta_dir () / "oldmod203_python.json"
3137+ create_fingerprint_file (fp_file , old_fingerprint_data )
3138+
3139+ fp = read_fingerprint ("oldmod203" , "python" )
3140+
3141+ assert fp is not None
3142+ assert fp .test_prompt_hash is None
3143+
3144+
3145+ class TestIssue203StaleTestDetection :
3146+ """Tests for sync_determine_operation detecting stale tests (Issue #203)."""
3147+
3148+ @patch ('sync_determine_operation.construct_paths' )
3149+ def test_detects_stale_tests_when_test_prompt_hash_differs (self , mock_construct , pdd_test_environment ):
3150+ """Should return 'test' operation when test_prompt_hash doesn't match current prompt."""
3151+ prompts_dir = pdd_test_environment / "prompts"
3152+
3153+ # Create all required files
3154+ p_hash = create_file (prompts_dir / f"{ BASENAME } _{ LANGUAGE } .prompt" , "NEW prompt content for 203" )
3155+ c_hash = create_file (pdd_test_environment / f"{ BASENAME } .py" , "# regenerated code" )
3156+ e_hash = create_file (pdd_test_environment / f"{ BASENAME } _example.py" , "# example" )
3157+ t_hash = create_file (pdd_test_environment / f"test_{ BASENAME } .py" , "# old tests" )
3158+
3159+ mock_construct .return_value = (
3160+ {}, {},
3161+ {
3162+ 'code_file' : str (pdd_test_environment / f"{ BASENAME } .py" ),
3163+ 'example_file' : str (pdd_test_environment / f"{ BASENAME } _example.py" ),
3164+ 'test_file' : str (pdd_test_environment / f"test_{ BASENAME } .py" )
3165+ },
3166+ LANGUAGE
3167+ )
3168+
3169+ # Create fingerprint with OLD test_prompt_hash (different from current prompt)
3170+ old_prompt_hash = "old_prompt_hash_before_change_203"
3171+ fp_path = get_meta_dir () / f"{ BASENAME } _{ LANGUAGE } .json"
3172+ create_fingerprint_file (fp_path , {
3173+ "pdd_version" : "1.0" ,
3174+ "timestamp" : "t" ,
3175+ "command" : "test" ,
3176+ "prompt_hash" : p_hash ,
3177+ "code_hash" : c_hash ,
3178+ "example_hash" : e_hash ,
3179+ "test_hash" : t_hash ,
3180+ "test_files" : None ,
3181+ "test_prompt_hash" : old_prompt_hash , # OLD - different from current!
3182+ })
3183+
3184+ # Create run_report (coverage above TARGET_COVERAGE=90.0 to avoid test_extend)
3185+ rr_path = get_meta_dir () / f"{ BASENAME } _{ LANGUAGE } _run.json"
3186+ create_run_report_file (rr_path , {
3187+ "timestamp" : "t" ,
3188+ "exit_code" : 0 ,
3189+ "tests_passed" : 5 ,
3190+ "tests_failed" : 0 ,
3191+ "coverage" : 95.0 ,
3192+ "test_hash" : t_hash ,
3193+ })
3194+
3195+ decision = sync_determine_operation (BASENAME , LANGUAGE , TARGET_COVERAGE , prompts_dir = str (prompts_dir ))
3196+
3197+ assert decision .operation == 'test'
3198+ assert 'outdated' in decision .reason .lower ()
3199+ assert decision .details .get ('tests_stale' ) is True
3200+
3201+ @patch ('sync_determine_operation.construct_paths' )
3202+ def test_no_stale_test_detection_when_test_prompt_hash_matches (self , mock_construct , pdd_test_environment ):
3203+ """Should return 'nothing' when test_prompt_hash matches current prompt."""
3204+ prompts_dir = pdd_test_environment / "prompts"
3205+
3206+ p_hash = create_file (prompts_dir / f"{ BASENAME } _{ LANGUAGE } .prompt" , "synced prompt 203" )
3207+ c_hash = create_file (pdd_test_environment / f"{ BASENAME } .py" , "# synced code" )
3208+ e_hash = create_file (pdd_test_environment / f"{ BASENAME } _example.py" , "# example" )
3209+ t_hash = create_file (pdd_test_environment / f"test_{ BASENAME } .py" , "# synced tests" )
3210+
3211+ mock_construct .return_value = (
3212+ {}, {},
3213+ {
3214+ 'code_file' : str (pdd_test_environment / f"{ BASENAME } .py" ),
3215+ 'example_file' : str (pdd_test_environment / f"{ BASENAME } _example.py" ),
3216+ 'test_file' : str (pdd_test_environment / f"test_{ BASENAME } .py" )
3217+ },
3218+ LANGUAGE
3219+ )
3220+
3221+ fp_path = get_meta_dir () / f"{ BASENAME } _{ LANGUAGE } .json"
3222+ create_fingerprint_file (fp_path , {
3223+ "pdd_version" : "1.0" ,
3224+ "timestamp" : "t" ,
3225+ "command" : "test" ,
3226+ "prompt_hash" : p_hash ,
3227+ "code_hash" : c_hash ,
3228+ "example_hash" : e_hash ,
3229+ "test_hash" : t_hash ,
3230+ "test_files" : None ,
3231+ "test_prompt_hash" : p_hash , # MATCHES current prompt hash
3232+ })
3233+
3234+ rr_path = get_meta_dir () / f"{ BASENAME } _{ LANGUAGE } _run.json"
3235+ create_run_report_file (rr_path , {
3236+ "timestamp" : "t" ,
3237+ "exit_code" : 0 ,
3238+ "tests_passed" : 10 ,
3239+ "tests_failed" : 0 ,
3240+ "coverage" : 95.0 , # Above TARGET_COVERAGE=90.0
3241+ "test_hash" : t_hash ,
3242+ })
3243+
3244+ decision = sync_determine_operation (BASENAME , LANGUAGE , TARGET_COVERAGE , prompts_dir = str (prompts_dir ))
3245+
3246+ assert decision .operation == 'nothing'
3247+
3248+ @patch ('sync_determine_operation.construct_paths' )
3249+ def test_no_stale_test_detection_when_test_prompt_hash_is_none (self , mock_construct , pdd_test_environment ):
3250+ """Should NOT trigger stale test detection when test_prompt_hash is None (backward compat)."""
3251+ prompts_dir = pdd_test_environment / "prompts"
3252+
3253+ p_hash = create_file (prompts_dir / f"{ BASENAME } _{ LANGUAGE } .prompt" , "legacy prompt 203" )
3254+ c_hash = create_file (pdd_test_environment / f"{ BASENAME } .py" , "# code" )
3255+ e_hash = create_file (pdd_test_environment / f"{ BASENAME } _example.py" , "# example" )
3256+ t_hash = create_file (pdd_test_environment / f"test_{ BASENAME } .py" , "# tests" )
3257+
3258+ mock_construct .return_value = (
3259+ {}, {},
3260+ {
3261+ 'code_file' : str (pdd_test_environment / f"{ BASENAME } .py" ),
3262+ 'example_file' : str (pdd_test_environment / f"{ BASENAME } _example.py" ),
3263+ 'test_file' : str (pdd_test_environment / f"test_{ BASENAME } .py" )
3264+ },
3265+ LANGUAGE
3266+ )
3267+
3268+ fp_path = get_meta_dir () / f"{ BASENAME } _{ LANGUAGE } .json"
3269+ create_fingerprint_file (fp_path , {
3270+ "pdd_version" : "0.99" ,
3271+ "timestamp" : "t" ,
3272+ "command" : "test" ,
3273+ "prompt_hash" : p_hash ,
3274+ "code_hash" : c_hash ,
3275+ "example_hash" : e_hash ,
3276+ "test_hash" : t_hash ,
3277+ # No test_prompt_hash - legacy fingerprint
3278+ })
3279+
3280+ rr_path = get_meta_dir () / f"{ BASENAME } _{ LANGUAGE } _run.json"
3281+ create_run_report_file (rr_path , {
3282+ "timestamp" : "t" ,
3283+ "exit_code" : 0 ,
3284+ "tests_passed" : 5 ,
3285+ "tests_failed" : 0 ,
3286+ "coverage" : 95.0 , # Above TARGET_COVERAGE=90.0
3287+ "test_hash" : t_hash ,
3288+ })
3289+
3290+ decision = sync_determine_operation (BASENAME , LANGUAGE , TARGET_COVERAGE , prompts_dir = str (prompts_dir ))
3291+
3292+ # Should NOT trigger stale test detection for legacy fingerprints
3293+ assert decision .operation == 'nothing'
3294+ assert decision .details .get ('tests_stale' ) is not True
0 commit comments