@@ -959,13 +959,9 @@ def _run_hook(tmp_path, agent_id, client, configs=None, extra_args=()):
959959 return rc , stream .getvalue ()
960960
961961
962- def _last_run (tmp_path , agent_id = 'claude-code' ):
963- path = tmp_path / '.claude' / 'settings.json'
964- if agent_id != 'claude-code' :
965- path = hooks .hook_config_path (
966- next (c for c in AGENT_CONFIGS if c .id == agent_id )
967- )
968- return state .get_last_hook_run (str (path ))
962+ def _last_run (tmp_path , agent_dir = '.test-agent' ):
963+ root = os .path .realpath (tmp_path / agent_dir / 'skills' )
964+ return state .get_last_hook_run (root )
969965
970966
971967def test_hook_does_nothing_when_auto_update_off (tmp_path ):
@@ -1028,8 +1024,8 @@ def test_hook_output_per_agent(tmp_path, auto_update_on, agent_id, expected):
10281024 assert json .loads (output ) == expected
10291025 else :
10301026 assert output == expected
1031- # Recorded against the agent's hook file even though it isn't detected .
1032- assert _last_run (tmp_path , agent_id )['is_successful' ] is True
1027+ # Recorded against the skills root, whichever agent's hook ran .
1028+ assert _last_run (tmp_path )['is_successful' ] is True
10331029
10341030
10351031def test_hook_prints_nothing_when_up_to_date (tmp_path , auto_update_on ):
@@ -1044,9 +1040,11 @@ def test_hook_prints_nothing_when_up_to_date(tmp_path, auto_update_on):
10441040def test_hook_prints_nothing_without_skills_or_agents (
10451041 tmp_path , auto_update_on
10461042):
1047- rc , output = _run_hook (tmp_path , 'claude-code' , _make_skill_client ())
1043+ client = _make_skill_client ()
1044+ rc , output = _run_hook (tmp_path , 'claude-code' , client )
10481045 assert (rc , output ) == (0 , '' )
1049- assert _last_run (tmp_path )['is_successful' ] is True
1046+ assert not client .get_latest_skill_version .called
1047+ assert state .get_last_hook_runs () == {}
10501048
10511049
10521050def test_hook_reports_failures (tmp_path , auto_update_on ):
@@ -1059,7 +1057,7 @@ def test_hook_reports_failures(tmp_path, auto_update_on):
10591057 'AWS agent skills updated: aws-cdk. Could not update aws-s3; run '
10601058 '"aws agent-toolkit update-skill --all" for details.\n '
10611059 )
1062- run = _last_run (tmp_path , 'codex' )
1060+ run = _last_run (tmp_path )
10631061 assert run ['is_successful' ] is False
10641062 assert run ['summary' ] == (
10651063 '1 updated, 0 skipped, 1 failed, 0 already up to date'
@@ -1213,10 +1211,13 @@ def test_hook_record_failure_is_ignored(tmp_path, auto_update_on):
12131211 rc , output = _run_hook (tmp_path , 'codex' , _make_skill_client ())
12141212 assert rc == 0
12151213 assert output == 'AWS agent skills updated: aws-s3.\n '
1216- assert record .call_args .kwargs == {
1217- 'is_successful' : True ,
1218- 'summary' : '1 updated, 0 skipped, 0 failed, 0 already up to date' ,
1219- }
1214+ assert record .call_args .args == (
1215+ os .path .realpath (tmp_path / '.test-agent' / 'skills' ),
1216+ )
1217+ assert record .call_args .kwargs ['is_successful' ] is True
1218+ assert record .call_args .kwargs ['summary' ] == (
1219+ '1 updated, 0 skipped, 0 failed, 0 already up to date'
1220+ )
12201221
12211222
12221223def test_hook_accepts_all (tmp_path , auto_update_on ):
@@ -1336,7 +1337,7 @@ def test_hook_reports_core_skill_install_failure(tmp_path, auto_update_on):
13361337 '--skill-name aws-iam" for details.\n '
13371338 )
13381339 assert not _skill_dir (tmp_path , 'aws-iam' ).exists ()
1339- run = _last_run (tmp_path , 'codex' )
1340+ run = _last_run (tmp_path )
13401341 assert run ['is_successful' ] is False
13411342 assert run ['summary' ] == (
13421343 '0 updated, 0 skipped, 1 failed, 1 already up to date'
@@ -1399,35 +1400,104 @@ def test_hook_run_right_after_another_does_nothing(tmp_path, auto_update_on):
13991400 _install_skill_at_version (tmp_path , '.test-agent' , 'aws-s3' , 'v1' )
14001401 _ , output = _run_hook (tmp_path , 'kiro' , _make_skill_client ())
14011402 assert output == 'AWS agent skills updated: aws-s3.\n '
1402- first = _last_run (tmp_path , 'kiro' )
1403+ first = _last_run (tmp_path )
14031404 _install_skill_at_version (tmp_path , '.test-agent' , 'aws-s3' , 'v1' )
14041405 client = _make_skill_client ()
14051406 rc , output = _run_hook (tmp_path , 'kiro' , client )
14061407 assert (rc , output ) == (0 , '' )
14071408 assert not client .get_latest_skill_version .called
1408- assert _last_run (tmp_path , 'kiro' ) == first
1409+ assert _last_run (tmp_path ) == first
14091410
14101411
1411- def test_hook_run_for_another_agent_is_not_skipped (tmp_path , auto_update_on ):
1412+ def test_hook_run_for_another_agent_skips_checked_root (
1413+ tmp_path , auto_update_on
1414+ ):
1415+ # The skills root was just checked through another agent's hook.
14121416 _install_skill_at_version (tmp_path , '.test-agent' , 'aws-s3' , 'v1' )
14131417 _run_hook (tmp_path , 'kiro' , _make_skill_client ())
1414- _install_skill_at_version ( tmp_path , '.test-agent' , 'aws-s3' , 'v1' )
1415- _ , output = _run_hook (tmp_path , 'codex' , _make_skill_client () )
1416- assert output == 'AWS agent skills updated: aws-s3. \n '
1418+ client = _make_skill_client ( )
1419+ assert _run_hook (tmp_path , 'codex' , client ) == ( 0 , '' )
1420+ assert not client . get_latest_skill_version . called
14171421
14181422
1419- def test_hook_runs_again_once_last_run_is_old (tmp_path , auto_update_on ):
1420- _install_skill_at_version (tmp_path , '.test-agent' , 'aws-s3' , 'v1' )
1421- config = next (c for c in AGENT_CONFIGS if c .id == 'kiro' )
1423+ def _record_check (tmp_path , age , is_successful = True , agent_dir = '.test-agent' ):
14221424 state .record_hook_run (
1423- hooks . hook_config_path ( config ),
1424- is_successful = True ,
1425+ os . path . realpath ( tmp_path / agent_dir / 'skills' ),
1426+ is_successful = is_successful ,
14251427 summary = 'old' ,
1426- timestamp = datetime .datetime .now (datetime .timezone .utc )
1427- - datetime .timedelta (seconds = update_skill .HOOK_RECENT_RUN_SECONDS + 1 ),
1428+ timestamp = datetime .datetime .now (datetime .timezone .utc ) - age ,
14281429 )
1429- _ , output = _run_hook (tmp_path , 'kiro' , _make_skill_client ())
1430- assert output == 'AWS agent skills updated: aws-s3.\n '
1430+
1431+
1432+ @pytest .mark .parametrize (
1433+ 'age,is_successful,is_checked' ,
1434+ [
1435+ (update_skill .HOOK_CHECK_INTERVAL , True , True ),
1436+ (
1437+ update_skill .HOOK_CHECK_INTERVAL - datetime .timedelta (minutes = 1 ),
1438+ True ,
1439+ False ,
1440+ ),
1441+ (update_skill .HOOK_RETRY_INTERVAL , False , True ),
1442+ (
1443+ update_skill .HOOK_RETRY_INTERVAL - datetime .timedelta (minutes = 1 ),
1444+ False ,
1445+ False ,
1446+ ),
1447+ # A check recorded in the future means the clock moved back.
1448+ (- datetime .timedelta (days = 7 ), True , True ),
1449+ ],
1450+ )
1451+ def test_hook_checks_root_once_interval_passed (
1452+ tmp_path , auto_update_on , age , is_successful , is_checked
1453+ ):
1454+ _install_skill_at_version (tmp_path , '.test-agent' , 'aws-s3' , 'v1' )
1455+ _record_check (tmp_path , age , is_successful )
1456+ client = _make_skill_client ()
1457+ _ , output = _run_hook (tmp_path , 'kiro' , client )
1458+ assert client .get_latest_skill_version .called is is_checked
1459+ expected = 'AWS agent skills updated: aws-s3.\n ' if is_checked else ''
1460+ assert output == expected
1461+
1462+
1463+ def test_hook_checks_only_roots_that_are_due (tmp_path , auto_update_on ):
1464+ _install_skill_at_version (tmp_path , '.test-agent' , 'aws-s3' , 'v1' )
1465+ _install_skill_at_version (tmp_path , '.claude' , 'aws-cdk' , 'v1' )
1466+ _record_check (tmp_path , datetime .timedelta (minutes = 1 ))
1467+ _ , output = _run_hook (tmp_path , 'claude-code' , _make_skill_client ())
1468+ assert json .loads (output )['systemMessage' ] == (
1469+ 'AWS agent skills updated: aws-cdk.'
1470+ )
1471+ # The root that wasn't due keeps its record and its old skill.
1472+ assert _last_run (tmp_path )['summary' ] == 'old'
1473+ marker = _skill_dir (tmp_path ) / SKILL_METADATA_FILENAME
1474+ assert json .loads (marker .read_text ()) == {'version' : 'v1' }
1475+ assert _last_run (tmp_path , '.claude' )['is_successful' ] is True
1476+
1477+
1478+ def test_hook_records_failure_only_for_root_with_failed_skill (
1479+ tmp_path , auto_update_on
1480+ ):
1481+ _install_skill_at_version (tmp_path , '.test-agent' , 'aws-s3' , 'v1' )
1482+ _install_skill_at_version (tmp_path , '.claude' , 'aws-cdk' , 'v1' )
1483+ client = _make_skill_client (bad_checksum_skill = 'aws-s3' )
1484+ _run_hook (tmp_path , 'claude-code' , client )
1485+ assert _last_run (tmp_path )['is_successful' ] is False
1486+ assert _last_run (tmp_path , '.claude' )['is_successful' ] is True
1487+
1488+
1489+ def test_hook_records_failure_for_every_root_on_error (
1490+ tmp_path , auto_update_on
1491+ ):
1492+ _install_skill_at_version (tmp_path , '.test-agent' , 'aws-s3' , 'v1' )
1493+ _install_skill_at_version (tmp_path , '.claude' , 'aws-cdk' , 'v1' )
1494+ client = _make_skill_client ()
1495+ client .get_latest_skill_version .side_effect = EndpointConnectionError (
1496+ endpoint_url = 'https://example.com'
1497+ )
1498+ _run_hook (tmp_path , 'claude-code' , client )
1499+ assert _last_run (tmp_path )['is_successful' ] is False
1500+ assert _last_run (tmp_path , '.claude' )['is_successful' ] is False
14311501
14321502
14331503def test_hook_does_nothing_while_another_run_holds_the_lock (
0 commit comments