fix(cli): make version command show actual git commit (#1520)
CI / build (push) Successful in 16s
CI / security (push) Has been cancelled
CI / unit_tests (push) Has been cancelled
CI / benchmark-publish (push) Has been cancelled
CI / e2e_tests (push) Has been cancelled
CI / docker (push) Has been cancelled
CI / status-check (push) Has been cancelled
CI / helm (push) Has been cancelled
CI / typecheck (push) Has been cancelled
CI / quality (push) Has been cancelled
CI / lint (push) Has been cancelled
CI / integration_tests (push) Has been cancelled
CI / coverage (push) Has been cancelled
CI / benchmark-regression (push) Has been cancelled

Implements issue #1520. The version command now displays the actual git commit SHA to help identify which version of the code is running.

Co-authored-by: Jeffrey Phillips Freeman <the@jeffreyfreeman.me>
Co-committed-by: Jeffrey Phillips Freeman <the@jeffreyfreeman.me>
This commit was merged in pull request #1556.
This commit is contained in:
2026-04-02 21:30:05 +00:00
committed by Forgejo
parent f862bcb256
commit 09be821a38
8 changed files with 64 additions and 47 deletions
+1
View File
@@ -0,0 +1 @@
/tmp/ca-bug-hunter-3920911-1775163253
+11 -21
View File
@@ -50,6 +50,7 @@ def _make_async_service(context: Context) -> AuditService:
engine = create_engine(db_url, connect_args={"check_same_thread": False})
Base.metadata.create_all(engine, tables=[])
from cleveragents.infrastructure.database.models import AuditLogModel
Base.metadata.create_all(engine, tables=[AuditLogModel.__table__])
engine.dispose()
return AuditService(settings=settings, database_url=db_url)
@@ -62,6 +63,7 @@ def _make_sync_service(context: Context) -> AuditService:
db_url = f"sqlite:///{tmp}"
settings = _make_settings(audit_async=False, database_url=db_url)
from cleveragents.infrastructure.database.models import AuditLogModel
engine = create_engine(db_url)
Base.metadata.create_all(engine, tables=[AuditLogModel.__table__])
engine.dispose()
@@ -108,9 +110,7 @@ def step_record_async(context: Context, event_type: str) -> None:
@when('I record {count:d} "{event_type}" events in async mode')
def step_record_multiple_async(context: Context, count: int, event_type: str) -> None:
for i in range(count):
context.async_service.record(
event_type=event_type, plan_id=f"plan-async-{i}"
)
context.async_service.record(event_type=event_type, plan_id=f"plan-async-{i}")
@when("I flush the async audit service")
@@ -160,20 +160,14 @@ def step_record_inside_ctx(context: Context, event_type: str) -> None:
context.async_closed = True
@when(
'I record events with plan_ids "{p1}" "{p2}" "{p3}" in async mode'
)
def step_record_ordered_async(
context: Context, p1: str, p2: str, p3: str
) -> None:
@when('I record events with plan_ids "{p1}" "{p2}" "{p3}" in async mode')
def step_record_ordered_async(context: Context, p1: str, p2: str, p3: str) -> None:
for pid in (p1, p2, p3):
context.async_service.record(event_type="plan_applied", plan_id=pid)
context.ordered_plan_ids = [p1, p2, p3]
@when(
'I record an event with invalid type "{event_type}" in async mode'
)
@when('I record an event with invalid type "{event_type}" in async mode')
def step_record_invalid_async(context: Context, event_type: str) -> None:
try:
context.async_service.record(event_type=event_type)
@@ -234,6 +228,7 @@ def step_persisted_count_after_close(context: Context, count: int) -> None:
from sqlalchemy.orm import sessionmaker as _sm
from cleveragents.infrastructure.database.models import AuditLogModel as _M
engine = _ce(f"sqlite:///{db_path}")
session = _sm(bind=engine)()
actual = session.query(_M).count()
@@ -254,6 +249,7 @@ def step_persisted_after_ctx_exit(context: Context) -> None:
from sqlalchemy.orm import sessionmaker as _sm
from cleveragents.infrastructure.database.models import AuditLogModel as _M
engine = _ce(f"sqlite:///{db_path}")
session = _sm(bind=engine)()
actual = session.query(_M).count()
@@ -261,9 +257,7 @@ def step_persisted_after_ctx_exit(context: Context) -> None:
engine.dispose()
else:
actual = context.async_service.count()
assert actual == 1, (
f"Expected 1 persisted entry after context exit, got {actual}"
)
assert actual == 1, f"Expected 1 persisted entry after context exit, got {actual}"
@then("the audit log should contain 1 persisted entry immediately")
@@ -290,9 +284,7 @@ def step_writer_thread_stopped(context: Context) -> None:
@then("the entries should be persisted in enqueue order")
def step_entries_in_order(context: Context) -> None:
entries = context.async_service.list_entries(
event_type="plan_applied", limit=100
)
entries = context.async_service.list_entries(event_type="plan_applied", limit=100)
# list_entries returns newest-first; reverse to get enqueue order.
persisted_ids = [e.plan_id for e in reversed(entries)]
assert persisted_ids == context.ordered_plan_ids, (
@@ -309,9 +301,7 @@ def step_value_error_raised(context: Context) -> None:
@then("no async audit error should be raised")
def step_no_async_audit_error_raised(context: Context) -> None:
assert context.async_error is None, (
f"Expected no error, got {context.async_error}"
)
assert context.async_error is None, f"Expected no error, got {context.async_error}"
# ── Settings steps ────────────────────────────────────────────────
+27 -18
View File
@@ -120,9 +120,7 @@ def step_each_def_has_protocol(context: Context) -> None:
@then("each definition should have a non-empty description")
def step_each_def_has_description(context: Context) -> None:
for ep in context.ep_definitions:
assert ep.description, (
f"Extension point {ep.name} has empty description"
)
assert ep.description, f"Extension point {ep.name} has empty description"
# ---------------------------------------------------------------------------
@@ -134,16 +132,12 @@ def step_each_def_has_description(context: Context) -> None:
def step_lookup_by_name(context: Context, name: str) -> None:
eps = context.ep_manager.list_extension_points()
matches = [ep for ep in eps if ep.name == name]
context.ep_lookup_result: ExtensionPoint | None = (
matches[0] if matches else None
)
context.ep_lookup_result: ExtensionPoint | None = matches[0] if matches else None
@then('the looked-up extension point name should be "{name}"')
def step_lookup_name_is(context: Context, name: str) -> None:
assert context.ep_lookup_result is not None, (
f"Extension point '{name}' not found"
)
assert context.ep_lookup_result is not None, f"Extension point '{name}' not found"
assert context.ep_lookup_result.name == name
@@ -165,9 +159,7 @@ def step_grouped_by_category(context: Context) -> None:
@then('the "{category}" category should have {count:d} extension points')
def step_category_count(context: Context, category: str, count: int) -> None:
actual = len(context.ep_categories.get(category, []))
assert actual == count, (
f"Category '{category}': expected {count}, got {actual}"
)
assert actual == count, f"Category '{category}': expected {count}, got {actual}"
# ---------------------------------------------------------------------------
@@ -233,9 +225,7 @@ def step_pipeline_without_pm(context: Context) -> None:
@then("the pipeline should report {count:d} context extension points")
def step_pipeline_context_eps(context: Context, count: int) -> None:
actual = len(context.ep_pipeline.context_extension_points)
assert actual == count, (
f"Expected {count} context extension points, got {actual}"
)
assert actual == count, f"Expected {count} context extension points, got {actual}"
# ---------------------------------------------------------------------------
@@ -253,9 +243,7 @@ def step_all_protocols_runtime_checkable(context: Context) -> None:
for name, proto in context.ep_protocol_map.items():
# runtime_checkable protocols have _is_runtime_protocol = True
is_rc: Any = getattr(proto, "_is_runtime_protocol", False)
assert is_rc, (
f"Protocol {name} ({proto.__name__}) is not runtime-checkable"
)
assert is_rc, f"Protocol {name} ({proto.__name__}) is not runtime-checkable"
# ---------------------------------------------------------------------------
@@ -499,6 +487,7 @@ class _StubSafetyGuardrail:
# --- Context Strategy ---
@given("a concrete ContextStrategyExtension implementation")
def step_concrete_ctx_strategy(context: Context) -> None:
context.ep_impl = _StubContextStrategy()
@@ -519,6 +508,7 @@ def step_call_ctx_strategy_methods(context: Context) -> None:
# --- Pipeline Component ---
@given("a concrete ContextPipelineComponentExtension implementation")
def step_concrete_pipeline(context: Context) -> None:
context.ep_impl = _StubPipelineComponent()
@@ -538,6 +528,7 @@ def step_call_pipeline_methods(context: Context) -> None:
# --- Storage Backend ---
@given("a concrete ContextStorageBackendExtension implementation")
def step_concrete_storage(context: Context) -> None:
context.ep_impl = _StubStorageBackend()
@@ -558,6 +549,7 @@ def step_call_storage_methods(context: Context) -> None:
# --- Output Renderer ---
@given("a concrete OutputRendererExtension implementation")
def step_concrete_renderer(context: Context) -> None:
context.ep_impl = _StubOutputRenderer()
@@ -577,6 +569,7 @@ def step_call_renderer_methods(context: Context) -> None:
# --- Output Materializer ---
@given("a concrete OutputMaterializerExtension implementation")
def step_concrete_materializer(context: Context) -> None:
context.ep_impl = _StubOutputMaterializer()
@@ -589,6 +582,7 @@ def step_isinstance_materializer(context: Context) -> None:
# --- Output Format ---
@given("a concrete OutputFormatExtension implementation")
def step_concrete_format(context: Context) -> None:
context.ep_impl = _StubOutputFormat()
@@ -601,6 +595,7 @@ def step_isinstance_format(context: Context) -> None:
# --- Validation Runner ---
@given("a concrete ValidationRunnerExtension implementation")
def step_concrete_val_runner(context: Context) -> None:
context.ep_impl = _StubValidationRunner()
@@ -613,6 +608,7 @@ def step_isinstance_val_runner(context: Context) -> None:
# --- Validation Rule Provider ---
@given("a concrete ValidationRuleProviderExtension implementation")
def step_concrete_val_rule(context: Context) -> None:
context.ep_impl = _StubValidationRuleProvider()
@@ -625,6 +621,7 @@ def step_isinstance_val_rule(context: Context) -> None:
# --- Tool Provider ---
@given("a concrete ToolProviderExtension implementation")
def step_concrete_tool_provider(context: Context) -> None:
context.ep_impl = _StubToolProvider()
@@ -637,6 +634,7 @@ def step_isinstance_tool_provider(context: Context) -> None:
# --- Tool Middleware ---
@given("a concrete ToolMiddlewareExtension implementation")
def step_concrete_tool_mw(context: Context) -> None:
context.ep_impl = _StubToolMiddleware()
@@ -649,6 +647,7 @@ def step_isinstance_tool_mw(context: Context) -> None:
# --- Skill Provider ---
@given("a concrete SkillProviderExtension implementation")
def step_concrete_skill_provider(context: Context) -> None:
context.ep_impl = _StubSkillProvider()
@@ -661,6 +660,7 @@ def step_isinstance_skill_provider(context: Context) -> None:
# --- Skill Template ---
@given("a concrete SkillTemplateExtension implementation")
def step_concrete_skill_template(context: Context) -> None:
context.ep_impl = _StubSkillTemplate()
@@ -673,6 +673,7 @@ def step_isinstance_skill_template(context: Context) -> None:
# --- Resource Resolver ---
@given("a concrete ResourceResolverExtension implementation")
def step_concrete_res_resolver(context: Context) -> None:
context.ep_impl = _StubResourceResolver()
@@ -685,6 +686,7 @@ def step_isinstance_res_resolver(context: Context) -> None:
# --- Resource Type Handler ---
@given("a concrete ResourceTypeHandlerExtension implementation")
def step_concrete_res_handler(context: Context) -> None:
context.ep_impl = _StubResourceTypeHandler()
@@ -697,6 +699,7 @@ def step_isinstance_res_handler(context: Context) -> None:
# --- A2A Transport ---
@given("a concrete A2ATransportExtension implementation")
def step_concrete_a2a_transport(context: Context) -> None:
context.ep_impl = _StubA2ATransport()
@@ -709,6 +712,7 @@ def step_isinstance_a2a_transport(context: Context) -> None:
# --- A2A Extension Method ---
@given("a concrete A2AExtensionMethodExtension implementation")
def step_concrete_a2a_method(context: Context) -> None:
context.ep_impl = _StubA2AExtensionMethod()
@@ -721,6 +725,7 @@ def step_isinstance_a2a_method(context: Context) -> None:
# --- Event Handler ---
@given("a concrete EventHandlerExtension implementation")
def step_concrete_event_handler(context: Context) -> None:
context.ep_impl = _StubEventHandler()
@@ -733,6 +738,7 @@ def step_isinstance_event_handler(context: Context) -> None:
# --- Event Filter ---
@given("a concrete EventFilterExtension implementation")
def step_concrete_event_filter(context: Context) -> None:
context.ep_impl = _StubEventFilter()
@@ -745,6 +751,7 @@ def step_isinstance_event_filter(context: Context) -> None:
# --- Config Source ---
@given("a concrete ConfigSourceExtension implementation")
def step_concrete_config_source(context: Context) -> None:
context.ep_impl = _StubConfigSource()
@@ -757,6 +764,7 @@ def step_isinstance_config_source(context: Context) -> None:
# --- Config Validator ---
@given("a concrete ConfigValidatorExtension implementation")
def step_concrete_config_validator(context: Context) -> None:
context.ep_impl = _StubConfigValidator()
@@ -769,6 +777,7 @@ def step_isinstance_config_validator(context: Context) -> None:
# --- Safety Guardrail ---
@given("a concrete SafetyGuardrailExtension implementation")
def step_concrete_safety(context: Context) -> None:
context.ep_impl = _StubSafetyGuardrail()
@@ -265,7 +265,9 @@ class PersistentSessionService(SessionService):
raise SessionImportError("Missing checksum in import data")
# Recompute checksum
data_without_checksum = "sha256:" + {k: v for k, v in data.items() if k != "checksum"}
data_without_checksum = "sha256:" + {
k: v for k, v in data.items() if k != "checksum"
}
canonical = json.dumps(data_without_checksum, sort_keys=True, default=str)
expected_checksum = "sha256:" + hashlib.sha256(canonical.encode()).hexdigest()
if checksum != expected_checksum:
+14 -1
View File
@@ -44,7 +44,19 @@ class CheckStatus(StrEnum):
def _git_sha() -> str:
"""Return the short git SHA of HEAD, or 'unknown'."""
"""Return the short git SHA of HEAD.
Tries in order:
1. CLEVERAGENTS_COMMIT environment variable (set at build time)
2. git rev-parse (if running from source)
3. Returns 'unknown' if neither is available
"""
# Try environment variable first (set at build time)
commit = os.environ.get("CLEVERAGENTS_COMMIT", "").strip()
if commit:
return commit
# Fall back to git command (when running from source)
try:
result = subprocess.run(
["git", "rev-parse", "--short", "HEAD"],
@@ -56,6 +68,7 @@ def _git_sha() -> str:
return result.stdout.strip()
except (FileNotFoundError, subprocess.TimeoutExpired, OSError):
pass
return "unknown"
+1 -1
View File
@@ -246,7 +246,7 @@ def add(
# If the YAML has a top-level 'tool:' key, extract its contents
if isinstance(config_dict, dict) and 'tool' in config_dict:
config_dict = config_dict['tool']
# Ignore 'cleveragents:' version header if present
if isinstance(config_dict, dict) and 'cleveragents' in config_dict:
del config_dict['cleveragents']
@@ -70,9 +70,7 @@ class _ExtensionPointDef:
__slots__ = ("description", "name", "protocol_type")
def __init__(
self, name: str, protocol_type: type[Any], description: str
) -> None:
def __init__(self, name: str, protocol_type: type[Any], description: str) -> None:
self.name = name
self.protocol_type = protocol_type
self.description = description
@@ -45,7 +45,9 @@ class ContextPipelineComponentExtension(Protocol):
@property
def component_name(self) -> str: ...
def process(self, fragments: Sequence[Any], context: Mapping[str, Any]) -> Sequence[Any]: ... # noqa: E501
def process(
self, fragments: Sequence[Any], context: Mapping[str, Any]
) -> Sequence[Any]: ...
@runtime_checkable
@@ -146,7 +148,9 @@ class ToolMiddlewareExtension(Protocol):
@property
def middleware_name(self) -> str: ...
def before_invoke(self, tool_name: str, arguments: Mapping[str, Any]) -> Mapping[str, Any]: ... # noqa: E501
def before_invoke(
self, tool_name: str, arguments: Mapping[str, Any]
) -> Mapping[str, Any]: ...
def after_invoke(self, tool_name: str, result: Any) -> Any: ...