Skip to content

test(suite): guard live /a0/usr writes and fix cross-module test pollution - #1799

Open
Zenetusken wants to merge 4 commits into
agent0ai:mainfrom
Zenetusken:fix/test-suite-live-usr-guard
Open

test(suite): guard live /a0/usr writes and fix cross-module test pollution#1799
Zenetusken wants to merge 4 commits into
agent0ai:mainfrom
Zenetusken:fix/test-suite-live-usr-guard

Conversation

@Zenetusken

@Zenetusken Zenetusken commented Aug 2, 2026

Copy link
Copy Markdown

What this PR does

Keeps the pytest suite from writing into the live /a0/usr tree (a persistent Docker volume holding real user chats, settings and secrets), and repairs the cross-module test pollution found while proving it.

The guard (tests/conftest.py)

  • An autouse function-scoped fixture monkeypatches the helpers.files write functions (write_file, write_file_bin, write_file_base64, delete_file, delete_dir), helpers.dotenv.save_dotenv_value, and by-value imports of those names in already-loaded helper modules (helpers.localization, helpers.task_scheduler, …). Any write resolving under realpath(<base_dir>/usr) raises RuntimeError — a loud test failure, never a silent skip.
  • Test-only: conftest.py is loaded by pytest alone; zero production-code changes (git diff <base>..<head> -- . ':!tests' is empty).
  • Documented scope: direct open()/Path.write_text/shutil/subprocess writes are not intercepted (that is why test_time_travel.py fixtures were redirected to tmp_path instead).

Live-write vectors found and fixed

  • tests/test_time_travel.py wrote <repo>/usr/time-travel-tests/... — redirected to tmp_path (fixtures only, no assertions touched).
  • tests/test_snapshot_parity.py / test_snapshot_schema_v1.py reached build_snapshot → Localization.set_timezone → save_dotenv_value, writing DEFAULT_USER_TIMEZONE into the real /a0/usr/.env — env persistence is now stubbed in those modules.
  • Guard bypass via collection-time from helpers.files import write_file in helpers/task_scheduler.py (scheduler persistence targets usr/scheduler/tasks.json) — closed by re-binding by-value imports in the fixture; verified with a probe test that now raises.

Pollution repairs (found while making the suite green)

  • helpers/__init__.py: without it helpers is a namespace package (no __file__), and tests/test_a0_connector_prompt_gating.py's sys.modules purge then deletes every loaded helpers.* module, splitting extension-registry state depending on collection order.
  • tests/test_browser_agent_regressions.py: unconditional sys.modules.setdefault stubs poisoned every later import of agent/helpers.ws* (6 collection errors + fallout); now _import_or_stub() uses the real module when importable, with stubs only as fallback in dependency-missing environments. Both regimes verified passing.
  • tests/test_model_config_project_presets.py: autouse fixture clears runtime caches poisoned by _base_dir redirection (27 failures at parent → 0).
  • tests/test_docker_release_plan.py: assertions updated to the current docker-publish.yml (ready branch, TARGET_TAG re-resolution) — was broken by workflow changes that landed after the test was written.

Verification (Docker agent0ai/agent-zero:v2.6, agent_zero_usr mounted read-only)

  • Targeted files: snapshot parity/schema, time-travel, scheduler-timezone — all green under the ro mount.
  • Full suite: 1236 passed, 7 failed, 1 skipped; the 7 failures (test_defer_lifecycle, test_http_auth_csrf ×2, test_parallel_tool ×2, test_speech_plugin_split, test_welcome_composer_static) are pre-existing upstream breakage that fails identically on the base commit and is unrelated to this PR.
  • Honest caveat: ~126 tests still read live usr content (they fail against a pristine volume). Read-side hermeticity is out of scope here; a session-level _base_dir redirect with opt-in is the suggested follow-up.

tests/test_time_travel.py used PROJECT_ROOT/usr as scratch space. That
is harmless on a dev checkout, but inside the deployed Docker container
<a0>/usr is the persistent volume holding real chats, model presets,
.env secrets and time-travel history - a plain pytest run destroyed
user data (observed: model presets replaced with fixture content, all
scoped model selections reset to Default on the next boot migration).

- tests/test_time_travel.py: redirect helpers.files._base_dir to
  tmp_path in the workspace fixture and the symlink-alias test, so
  /a0/usr display paths resolve into tmp while display semantics stay
  identical. The module now passes with <repo>/usr mounted read-only.
- tests/conftest.py (new): session bootstrap that
  * fails any test writing under the real <repo>/usr via the
    helpers.files write/delete functions or the dotenv/localization
    save path, turning silent live-data corruption into loud failures
  * bootstraps the telegram plugin's lazy aiogram dependency before
    collection, excluding test_telegram_* loudly when offline
  * excludes two legacy manual scripts that error at collection
Bootstrap conftest, add helpers package init, and redirect regression
test temp writes so the framework suite never corrupts the persistent
usr volume.
Adversarial review follow-ups to the live-usr guard:

- helpers.task_scheduler imports write_file by value at module import
  time, so modules imported at collection time (test_task_scheduler_
  timezone.py, test_timezone_regressions.py) bypassed the monkeypatched
  helpers.files guard and could write usr/scheduler/tasks.json straight
  through. The guard fixture now re-binds by-value imports in already
  imported modules (data-driven list; helpers.localization's
  save_dotenv_value folded into the same loop).
- test_snapshot_parity.py / test_snapshot_schema_v1.py: build_snapshot
  applies the request timezone via Localization.set_timezone, which
  persists DEFAULT_USER_TIMEZONE into usr/.env whenever the persisted
  timezone or offset differs (latent live-volume write, e.g. after
  load_dotenv or DST drift). Stub save_dotenv_value in both modules;
  assertions unchanged.
- conftest docstring: state the real guard scope (helpers.files and
  helpers.dotenv write paths) and note direct open()/Path/shutil/os.
  makedirs/subprocess writes are not intercepted.
- Telegram dependency bootstrap: warn via warnings.warn (surfaces in the
  pytest summary) instead of print, catch OSError for a missing/broken
  uv binary, and comment the intentional collection-time install.
@Zenetusken Zenetusken changed the title test(suite): keep tests away from live /a0/usr tree test(suite): guard live /a0/usr writes and fix cross-module test pollution Aug 2, 2026
…UI source changes

- test_speech_plugin_split: send-button icon migrated from x-text to
  <x-icon :name=...> in upstream 93d1131 (Unify WebUI icons)
- test_parallel_tool: .chat-list-button padding is now 8px 6px (same commit)
- test_welcome_composer_static: drop background assertion; upstream bebe682
  deliberately removed the flat background from .welcome-container
  (radial-gradient guard retained)
@Zenetusken

Copy link
Copy Markdown
Author

Added commit 187faf84: aligns the 3 stale static-marker assertions that fail at upstream base (5ff106a2):

  • test_speech_plugin_split: send-button icon migrated x-text<x-icon :name=".chatInput.sendButtonIcon"> in upstream 93d1131c (Unify WebUI icons)
  • test_parallel_tool: .chat-list-button padding is now 8px 6px (same upstream commit)
  • test_welcome_composer_static: drops the background: var(--color-background); assertion — upstream bebe6826 deliberately removed the flat background from .welcome-container; the radial-gradient guard is retained

Validated: the 3 targeted tests pass; full-suite failure set without this commit = 7 (these 3 + 4 pre-existing environment-dependent ones), with it = 4 (only the pre-existing ones, which depend on a writable /a0/usr and are resolved by the .env-write guards in this PR + #1800).

@Zenetusken

Copy link
Copy Markdown
Author

Cross-PR note: this branch's tests/conftest.py is an improved superset of the guard introduced in #1798 (rebased to bec6ea5f). If #1798 merges first, this PR's version of tests/conftest.py should win any conflict — it adds the helpers.task_scheduler write-path re-bind and the broader scope documentation.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant