fix(agent): clean legacy Agent Soul files (#41024)

This commit is contained in:
盐粒 Yanli
2026-08-20 10:34:11 +00:00
committed by GitHub
parent 97a3039a2d
commit cbd78c0073
2 changed files with 180 additions and 0 deletions
@@ -0,0 +1,55 @@
"""clean legacy agent soul files
Revision ID: fbdfcf5f5a6e
Revises: 89919253ca7a
Create Date: 2026-08-20 09:38:36.827807
"""
from alembic import op
# revision identifiers, used by Alembic.
revision = "fbdfcf5f5a6e"
down_revision = "89919253ca7a"
branch_labels = None
depends_on = None
def _remove_legacy_files(table_name: str) -> None:
dialect_name = op.get_context().dialect.name
if dialect_name == "postgresql":
op.execute(
f"""UPDATE {table_name}
SET config_snapshot = (config_snapshot::jsonb - 'files')::text
WHERE config_snapshot::jsonb ? 'files'"""
)
return
if dialect_name == "mysql":
op.execute(
f"""UPDATE {table_name}
SET config_snapshot = JSON_REMOVE(config_snapshot, '$.files')
WHERE JSON_CONTAINS_PATH(config_snapshot, 'one', '$.files')"""
)
return
if dialect_name == "sqlite":
op.execute(
f"""UPDATE {table_name}
SET config_snapshot = json_remove(config_snapshot, '$.files')
WHERE json_type(config_snapshot, '$.files') IS NOT NULL"""
)
return
raise RuntimeError(f"unsupported database dialect: {dialect_name}")
def upgrade() -> None:
# The Agent Drive removal migration skipped its Python row rewrite when
# migrations were emitted as offline SQL. Run the cleanup again as native
# SQL so every deployment removes the retired AgentSoulConfig.files field.
_remove_legacy_files("agent_config_snapshots")
_remove_legacy_files("agent_config_drafts")
def downgrade() -> None:
# The retired files catalog cannot be reconstructed after Agent Drive data
# has been removed.
pass
@@ -0,0 +1,125 @@
from __future__ import annotations
import importlib.util
import json
from io import StringIO
from pathlib import Path
from types import ModuleType
import pytest
import sqlalchemy as sa
from alembic.migration import MigrationContext
from alembic.operations import Operations
from pydantic import ValidationError
from models.agent_config_entities import AgentSoulConfig
_MIGRATION_PATH = (
Path(__file__).resolve().parents[3]
/ "migrations/versions/2026_08_20_0938-fbdfcf5f5a6e_clean_legacy_agent_soul_files.py"
)
def _load_migration_module() -> ModuleType:
spec = importlib.util.spec_from_file_location("clean_legacy_agent_soul_files", _MIGRATION_PATH)
if spec is None or spec.loader is None:
raise RuntimeError("failed to load migration module")
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
def _run_upgrade(module: ModuleType, engine: sa.Engine) -> None:
with engine.begin() as connection:
operations = Operations(MigrationContext.configure(connection))
original_op = module.__dict__["op"]
module.__dict__["op"] = operations
try:
module.__dict__["upgrade"]()
finally:
module.__dict__["op"] = original_op
def test_upgrade_makes_legacy_build_draft_valid_for_first_message() -> None:
engine = sa.create_engine("sqlite:///:memory:")
metadata = sa.MetaData()
for table_name in ("agent_config_snapshots", "agent_config_drafts"):
sa.Table(
table_name,
metadata,
sa.Column("id", sa.String(36), primary_key=True),
sa.Column("config_snapshot", sa.Text(), nullable=False),
)
metadata.create_all(engine)
legacy_soul: dict[str, object] = {
"files": {"files": [], "skills": []},
"prompt": {"system_prompt": "Build mode"},
}
with pytest.raises(ValidationError) as exc_info:
AgentSoulConfig.model_validate(legacy_soul)
assert exc_info.value.errors(include_url=False)[0]["loc"] == ("files",)
assert exc_info.value.errors(include_url=False)[0]["type"] == "extra_forbidden"
with engine.begin() as connection:
for table_name in ("agent_config_snapshots", "agent_config_drafts"):
connection.execute(
sa.text(f"INSERT INTO {table_name} (id, config_snapshot) VALUES (:id, :config_snapshot)"),
{"id": table_name, "config_snapshot": json.dumps(legacy_soul)},
)
_run_upgrade(_load_migration_module(), engine)
with engine.begin() as connection:
stored_build_draft = connection.execute(sa.text("SELECT config_snapshot FROM agent_config_drafts")).scalar_one()
stored_snapshot = connection.execute(sa.text("SELECT config_snapshot FROM agent_config_snapshots")).scalar_one()
for stored_soul in (stored_build_draft, stored_snapshot):
value = json.loads(stored_soul)
assert "files" not in value
assert AgentSoulConfig.model_validate(value).prompt.system_prompt == "Build mode"
@pytest.mark.parametrize(
("dialect_name", "removal_expression", "presence_predicate", "other_dialect_expression"),
[
(
"postgresql",
"config_snapshot::jsonb - 'files'",
"config_snapshot::jsonb ? 'files'",
"JSON_REMOVE",
),
(
"mysql",
"JSON_REMOVE(config_snapshot, '$.files')",
"JSON_CONTAINS_PATH(config_snapshot, 'one', '$.files')",
"config_snapshot::jsonb",
),
],
)
def test_upgrade_emits_legacy_files_cleanup_in_offline_sql(
dialect_name: str,
removal_expression: str,
presence_predicate: str,
other_dialect_expression: str,
) -> None:
module = _load_migration_module()
output = StringIO()
migration_context = MigrationContext.configure(
dialect_name=dialect_name,
opts={"as_sql": True, "output_buffer": output},
)
operations = Operations(migration_context)
original_op = module.__dict__["op"]
module.__dict__["op"] = operations
try:
module.__dict__["upgrade"]()
finally:
module.__dict__["op"] = original_op
generated_sql = output.getvalue()
assert "UPDATE agent_config_snapshots" in generated_sql
assert "UPDATE agent_config_drafts" in generated_sql
assert generated_sql.count(removal_expression) == 2
assert generated_sql.count(presence_predicate) == 2
assert other_dialect_expression not in generated_sql