fix: track org config validation timestamp

This commit is contained in:
Abhishek Kumar 2026-07-09 19:31:57 +05:30
parent d22c073cb5
commit c32b0ccb2f
5 changed files with 101 additions and 13 deletions

View file

@ -0,0 +1,29 @@
"""add last_validated_at on org config
Revision ID: c52dc3cccfb0
Revises: b7e3c9a1d2f4
Create Date: 2026-07-09 19:18:29.550267
"""
from typing import Sequence, Union
import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic.
revision: str = "c52dc3cccfb0"
down_revision: Union[str, None] = "b7e3c9a1d2f4"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
op.add_column(
"organization_configurations",
sa.Column("last_validated_at", sa.DateTime(timezone=True), nullable=True),
)
def downgrade() -> None:
op.drop_column("organization_configurations", "last_validated_at")

View file

@ -205,11 +205,8 @@ class OrganizationConfigurationModel(Base):
key = Column(String, nullable=False)
value = Column(JSON, nullable=False, default=dict)
created_at = Column(DateTime(timezone=True), default=lambda: datetime.now(UTC))
updated_at = Column(
DateTime(timezone=True),
default=lambda: datetime.now(UTC),
onupdate=lambda: datetime.now(UTC),
)
updated_at = Column(DateTime(timezone=True), default=lambda: datetime.now(UTC))
last_validated_at = Column(DateTime(timezone=True), nullable=True)
# Relationships
organization = relationship("OrganizationModel", back_populates="configurations")

View file

@ -34,10 +34,15 @@ class OrganizationConfigurationClient(BaseDBClient):
return result.scalars().all()
async def upsert_configuration(
self, organization_id: int, key: str, value: Any
self,
organization_id: int,
key: str,
value: Any,
last_validated_at: datetime | None = None,
) -> OrganizationConfigurationModel:
"""Create or update a configuration for an organization."""
async with self.async_session() as session:
now = datetime.now(UTC)
# First try to get existing configuration
result = await session.execute(
select(OrganizationConfigurationModel).where(
@ -50,12 +55,16 @@ class OrganizationConfigurationClient(BaseDBClient):
if config:
# Update existing configuration
config.value = value
config.updated_at = now
config.last_validated_at = last_validated_at
else:
# Create new configuration
config = OrganizationConfigurationModel(
organization_id=organization_id,
key=key,
value=value,
updated_at=now,
last_validated_at=last_validated_at,
)
session.add(config)
@ -67,10 +76,10 @@ class OrganizationConfigurationClient(BaseDBClient):
await session.refresh(config)
return config
async def touch_configuration(
async def mark_configuration_validated(
self, organization_id: int, key: str
) -> Optional[OrganizationConfigurationModel]:
"""Update the timestamp for an existing organization configuration."""
"""Update the validation timestamp for an existing organization configuration."""
async with self.async_session() as session:
result = await session.execute(
select(OrganizationConfigurationModel).where(
@ -82,7 +91,7 @@ class OrganizationConfigurationClient(BaseDBClient):
if not config:
return None
config.updated_at = datetime.now(UTC)
config.last_validated_at = datetime.now(UTC)
try:
await session.commit()
except Exception as e:

View file

@ -2,6 +2,7 @@ from __future__ import annotations
import copy
from dataclasses import dataclass
from datetime import UTC, datetime
from typing import Literal
from loguru import logger
@ -72,7 +73,9 @@ async def get_resolved_ai_model_configuration(
if organization_configuration is not None:
effective = compile_ai_model_configuration_v2(organization_configuration)
if organization_configuration_row is not None:
effective.last_validated_at = organization_configuration_row.updated_at
effective.last_validated_at = (
organization_configuration_row.last_validated_at
)
return ResolvedAIModelConfiguration(
effective=effective,
source="organization_v2",
@ -118,7 +121,7 @@ async def get_organization_ai_model_configuration_v2(
async def update_organization_ai_model_configuration_last_validated_at(
organization_id: int,
) -> None:
await db_client.touch_configuration(
await db_client.mark_configuration_validated(
organization_id,
OrganizationConfigurationKey.MODEL_CONFIGURATION_V2.value,
)
@ -159,6 +162,7 @@ async def upsert_organization_ai_model_configuration_v2(
organization_id,
OrganizationConfigurationKey.MODEL_CONFIGURATION_V2.value,
configuration.model_dump(mode="json", exclude_none=True),
last_validated_at=datetime.now(UTC),
)
return configuration

View file

@ -19,6 +19,7 @@ from api.services.configuration.ai_model_configuration import (
mask_ai_model_configuration_v2,
merge_ai_model_configuration_v2_secrets,
migrate_workflow_configuration_model_override_to_v2,
upsert_organization_ai_model_configuration_v2,
)
from api.services.configuration.check_validity import UserConfigurationValidator
from api.services.configuration.masking import mask_key
@ -150,7 +151,7 @@ async def test_byok_realtime_validator_does_not_require_stt_or_tts():
@pytest.mark.asyncio
async def test_resolved_org_v2_uses_configuration_updated_at_as_validation_cache(
async def test_resolved_org_v2_uses_last_validated_at_as_validation_cache(
monkeypatch,
):
from datetime import UTC, datetime
@ -164,7 +165,7 @@ async def test_resolved_org_v2_uses_configuration_updated_at_as_validation_cache
)
row = SimpleNamespace(
value=config.model_dump(mode="json", exclude_none=True),
updated_at=last_validated_at,
last_validated_at=last_validated_at,
)
monkeypatch.setattr(
ai_model_configuration.db_client,
@ -178,6 +179,54 @@ async def test_resolved_org_v2_uses_configuration_updated_at_as_validation_cache
assert resolved.effective.last_validated_at == last_validated_at
@pytest.mark.asyncio
async def test_upsert_org_v2_marks_configuration_validated(monkeypatch):
from api.services.configuration import ai_model_configuration
config = OrganizationAIModelConfigurationV2(
mode="dograh",
dograh=DograhManagedAIModelConfiguration(api_key="mps-secret"),
)
upsert = AsyncMock()
monkeypatch.setattr(
ai_model_configuration.db_client,
"upsert_configuration",
upsert,
)
result = await upsert_organization_ai_model_configuration_v2(42, config)
assert result is config
upsert.assert_awaited_once()
args = upsert.await_args.args
kwargs = upsert.await_args.kwargs
assert args == (
42,
"MODEL_CONFIGURATION_V2",
config.model_dump(mode="json", exclude_none=True),
)
assert kwargs["last_validated_at"].tzinfo is not None
def test_org_config_validation_timestamp_update_does_not_touch_updated_at():
from datetime import UTC, datetime
from sqlalchemy import update
from sqlalchemy.dialects import postgresql
from api.db.models import OrganizationConfigurationModel
stmt = (
update(OrganizationConfigurationModel)
.where(OrganizationConfigurationModel.id == 1)
.values(last_validated_at=datetime.now(UTC))
)
compiled = str(stmt.compile(dialect=postgresql.dialect()))
assert "last_validated_at" in compiled
assert "updated_at" not in compiled
@pytest.mark.asyncio
async def test_pipeline_validator_requires_stt_and_tts_when_not_realtime():
effective = EffectiveAIModelConfiguration(