diff --git a/src/agent_env/providers/env_state/env_state_provider.py b/src/agent_env/providers/env_state/env_state_provider.py index f0e1ff93..a82b1c68 100644 --- a/src/agent_env/providers/env_state/env_state_provider.py +++ b/src/agent_env/providers/env_state/env_state_provider.py @@ -377,7 +377,9 @@ def get_init_script(environment_names: list[str]) -> str: SELECT schemaname, tablename FROM pg_tables WHERE schemaname = _schema_name - AND tablename != '_changelog' + -- Names starting with an underscore are a service's internal tables + -- (file bytes, caches), not domain rows; _changelog itself is one. + AND tablename NOT LIKE '\\_%' LOOP EXECUTE format( 'DROP TRIGGER IF EXISTS _changelog_trigger ON %I.%I', diff --git a/tst/integration/env/envs/service_db_changelog_test.py b/tst/integration/env/envs/service_db_changelog_test.py index 8ab477ad..337696cc 100644 --- a/tst/integration/env/envs/service_db_changelog_test.py +++ b/tst/integration/env/envs/service_db_changelog_test.py @@ -128,3 +128,30 @@ def test_changelog_row_id_composite_pk(pg_conn): f"WHERE schema_name='{schema}' AND table_name='daily' ORDER BY id DESC LIMIT 1", )[0][0] assert json.loads(row_id) == {"user_id": "u_jin", "day": "2025-11-05"} + + +def test_internal_tables_get_no_changelog_trigger(pg_conn): + """A service's internal tables (a leading underscore, like _changelog) hold no + domain rows: the installer skips them, so a blob written there is not copied + into changed_fields while the domain table beside it is still audited.""" + schema = "files_svc" + _exec(pg_conn, LocalPostgresStateProvider.get_init_script([schema])) + _exec(pg_conn, f'CREATE TABLE "{schema}".documents (id text PRIMARY KEY, title text)') + _exec(pg_conn, f'CREATE TABLE "{schema}"._files (path text PRIMARY KEY, content bytea)') + _exec(pg_conn, f"SELECT public._install_changelog_triggers('{schema}')") + + _exec(pg_conn, f"INSERT INTO \"{schema}\".documents VALUES ('d1', 'brief')") + _exec(pg_conn, f"INSERT INTO \"{schema}\"._files VALUES ('a/brief.pdf', '\\x255044462d')") + + rows = _rows( + pg_conn, + "SELECT table_name, operation FROM public._changelog " + f"WHERE schema_name='{schema}' ORDER BY id", + ) + assert rows == [("documents", "INSERT")] + triggers = _rows( + pg_conn, + "SELECT event_object_table FROM information_schema.triggers " + f"WHERE trigger_schema='{schema}' AND trigger_name='_changelog_trigger'", + ) + assert {t[0] for t in triggers} == {"documents"} diff --git a/tst/unit/providers/env_state/test_env_state_provider.py b/tst/unit/providers/env_state/test_env_state_provider.py index bac759bf..741e1579 100644 --- a/tst/unit/providers/env_state/test_env_state_provider.py +++ b/tst/unit/providers/env_state/test_env_state_provider.py @@ -180,6 +180,10 @@ def test_get_init_script_changelog_components(): for op in ("TG_OP = 'INSERT'", "TG_OP = 'DELETE'", "TG_OP = 'UPDATE'"): assert op in script assert "_install_changelog_triggers(_schema_name TEXT)" in script + # Internal tables (a leading underscore, like _changelog itself) carry no + # domain rows and get no trigger. + assert "tablename NOT LIKE '\\_%'" in script + assert "tablename != '_changelog'" not in script assert "WHERE schemaname = _schema_name" in script