Files
aditya b25a886df8
CI / benchmark-publish (pull_request) Has been skipped
CI / quality (pull_request) Successful in 20s
CI / lint (pull_request) Successful in 24s
CI / build (pull_request) Successful in 27s
CI / typecheck (pull_request) Successful in 34s
CI / security (pull_request) Successful in 1m11s
CI / integration_tests (pull_request) Successful in 4m40s
CI / unit_tests (pull_request) Successful in 9m55s
CI / docker (pull_request) Successful in 1m0s
CI / benchmark-regression (pull_request) Successful in 25m30s
CI / coverage (pull_request) Successful in 1h11m59s
feat(skill): add MCP refresh hooks
Implement SkillRegistry.refresh(name) and refresh_all() to recompute
flattened tool sets on demand. Wire MCPToolAdapter.dispatch_notification()
to MCPRefreshHook, which debounces rapid notifications/tools/list_changed
bursts into a single refresh_all() call (default 0.5 s window).

Add safeguard that skips tool-ref validation when no ToolRegistry is
configured, emitting a single WARNING with recovery steps. Introduce
immutable SkillRefreshResult (refreshed/failed/skipped counts) with
to_summary() and merge() for CLI and log output.

New files:
- src/cleveragents/skills/refresh.py        (SkillRefreshResult)
- src/cleveragents/mcp/refresh_hook.py      (MCPRefreshHook)
- features/skill_refresh.feature            (19 Behave scenarios)
- features/steps/skill_refresh_steps.py
- robot/skill_refresh.robot                 (10 Robot tests)
- robot/helper_skill_refresh.py
- benchmarks/skill_refresh_bench.py         (ASV benchmarks)
- docs/reference/skill_refresh.md

Modified:
- src/cleveragents/skills/registry.py       (refresh, refresh_all)
- src/cleveragents/mcp/adapter.py           (notification listener API)
- src/cleveragents/skills/__init__.py       (export SkillRefreshResult)
- src/cleveragents/mcp/__init__.py          (export MCPRefreshHook)
- CHANGELOG.md

ISSUES CLOSED #168
2026-02-26 12:29:49 +00:00

120 lines
6.1 KiB
Plaintext

*** Settings ***
Documentation Integration tests for Skill Refresh hooks (feat #168)
... Covers SkillRefreshResult, SkillRegistry.refresh()/refresh_all(),
... and MCPRefreshHook notification-to-debounce-to-refresh wiring.
Resource ${CURDIR}/common.resource
Suite Setup Setup Test Environment
Suite Teardown Cleanup Test Environment
*** Variables ***
${HELPER} ${CURDIR}/helper_skill_refresh.py
*** Test Cases ***
Refresh Single Skill With All Tool Refs Present
[Documentation] refresh() returns skill in refreshed list when all tool refs resolve
${result}= Run Process ${PYTHON} ${HELPER} refresh-single-ok cwd=${WORKSPACE}
Log ${result.stdout}
Log ${result.stderr}
Should Be Equal As Integers ${result.rc} 0
Should Contain ${result.stdout} refreshed-count: 1
Should Contain ${result.stdout} failed-count: 0
Should Contain ${result.stdout} name-in-refreshed: True
Should Contain ${result.stdout} refresh-single-ok
Refresh Single Skill With Missing Tool Ref
[Documentation] refresh() returns skill in failed dict when a tool ref is absent from ToolRegistry
${result}= Run Process ${PYTHON} ${HELPER} refresh-single-missing-tool cwd=${WORKSPACE}
Log ${result.stdout}
Log ${result.stderr}
Should Be Equal As Integers ${result.rc} 0
Should Contain ${result.stdout} failed-count: 1
Should Contain ${result.stdout} has-error: True
Should Contain ${result.stdout} error-mentions-missing: True
Should Contain ${result.stdout} refresh-missing-tool-ok
Refresh Single Skill Without Tool Registry
[Documentation] refresh() skips validation and counts as refreshed when no ToolRegistry is configured
${result}= Run Process ${PYTHON} ${HELPER} refresh-single-no-registry cwd=${WORKSPACE}
Log ${result.stdout}
Log ${result.stderr}
Should Be Equal As Integers ${result.rc} 0
Should Contain ${result.stdout} refreshed-count: 1
Should Contain ${result.stdout} failed-count: 0
Should Contain ${result.stdout} name-in-refreshed: True
Should Contain ${result.stdout} refresh-no-registry-ok
Refresh All Skills With Mixed Results
[Documentation] refresh_all() aggregates success and failure across all skills
${result}= Run Process ${PYTHON} ${HELPER} refresh-all-mixed cwd=${WORKSPACE}
Log ${result.stdout}
Log ${result.stderr}
Should Be Equal As Integers ${result.rc} 0
Should Contain ${result.stdout} total-refreshed: 1
Should Contain ${result.stdout} total-failed: 1
Should Contain ${result.stdout} ok-refreshed: True
Should Contain ${result.stdout} bad-failed: True
Should Contain ${result.stdout} refresh-all-mixed-ok
Refresh Unknown Skill Raises Error
[Documentation] refresh() raises SkillExecutionError for an unregistered skill name
${result}= Run Process ${PYTHON} ${HELPER} refresh-unknown-skill cwd=${WORKSPACE}
Log ${result.stdout}
Log ${result.stderr}
Should Be Equal As Integers ${result.rc} 0
Should Contain ${result.stdout} error-type: SkillExecutionError
Should Contain ${result.stdout} error-contains-name: True
Should Contain ${result.stdout} refresh-unknown-ok
Skill Refresh Result Summary Format
[Documentation] SkillRefreshResult.to_summary() produces the expected one-liner
${result}= Run Process ${PYTHON} ${HELPER} result-summary cwd=${WORKSPACE}
Log ${result.stdout}
Log ${result.stderr}
Should Be Equal As Integers ${result.rc} 0
Should Contain ${result.stdout} has-refreshed: True
Should Contain ${result.stdout} has-failed: True
Should Contain ${result.stdout} has-skipped: True
Should Contain ${result.stdout} result-summary-ok
Skill Refresh Result Merge Combines Two Results
[Documentation] SkillRefreshResult.merge() joins refreshed, failed, and skipped lists correctly
${result}= Run Process ${PYTHON} ${HELPER} result-merge cwd=${WORKSPACE}
Log ${result.stdout}
Log ${result.stderr}
Should Be Equal As Integers ${result.rc} 0
Should Contain ${result.stdout} total-refreshed: 2
Should Contain ${result.stdout} total-failed: 1
Should Contain ${result.stdout} total-skipped: 1
Should Contain ${result.stdout} a-in-refreshed: True
Should Contain ${result.stdout} c-in-refreshed: True
Should Contain ${result.stdout} b-in-skipped: True
Should Contain ${result.stdout} d-in-failed: True
Should Contain ${result.stdout} result-merge-ok
MCP Refresh Hook Triggers Skill Registry Refresh
[Documentation] MCPRefreshHook wires notifications/tools/list_changed to refresh_all()
${result}= Run Process ${PYTHON} ${HELPER} hook-wiring cwd=${WORKSPACE}
Log ${result.stdout}
Log ${result.stderr}
Should Be Equal As Integers ${result.rc} 0
Should Contain ${result.stdout} refreshed-at-least-once: True
Should Contain ${result.stdout} hook-wiring-ok
MCP Refresh Hook Debounces Rapid Notifications
[Documentation] Multiple tool_list_changed notifications within the debounce window collapse to one refresh
${result}= Run Process ${PYTHON} ${HELPER} hook-debounce cwd=${WORKSPACE}
Log ${result.stdout}
Log ${result.stderr}
Should Be Equal As Integers ${result.rc} 0
Should Contain ${result.stdout} debounced-to-one: True
Should Contain ${result.stdout} hook-debounce-ok
MCP Refresh Hook Cancel Prevents Pending Refresh
[Documentation] hook.cancel() stops the pending debounced refresh before it fires
${result}= Run Process ${PYTHON} ${HELPER} hook-cancel cwd=${WORKSPACE}
Log ${result.stdout}
Log ${result.stderr}
Should Be Equal As Integers ${result.rc} 0
Should Contain ${result.stdout} no-refresh-after-cancel: True
Should Contain ${result.stdout} hook-cancel-ok