From 6e5953062c696595b73735fbf5d1a80af9ef6a86 Mon Sep 17 00:00:00 2001 From: Karel Ha Date: Sat, 18 Apr 2026 02:30:43 +0200 Subject: [PATCH 01/15] chore(deploy): Gate hosted SQLite recovery behind explicit flag #31 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- README.md | 7 ++++ core/tests/test_deployment.py | 60 +++++++++++++++++++++++++++++++++++ deep_workflow/settings.py | 10 ++++++ 3 files changed, 77 insertions(+) diff --git a/README.md b/README.md index 3419597..d12f381 100644 --- a/README.md +++ b/README.md @@ -153,6 +153,7 @@ The repository now includes the files needed to host the app on Vercel productio | `DJANGO_SECRET_KEY` | Required | Required | Use a unique secret per environment. Hosted builds fail fast until this is set. | | `DATABASE_URL` | Required | Required | Point previews at an isolated PostgreSQL database or branch database, not production. | | `APP_BASE_URL` | `https://deep-workflow.vercel.app` | Optional | Sets the canonical production URL and anchors the production host/origin configuration. | +| `DJANGO_ENABLE_HOSTED_SQLITE_FALLBACK` | Emergency only | Emergency only | Leave unset for normal deploys. Set to `1` only when intentionally activating the temporary hosted SQLite recovery mode. | | `DJANGO_SECURE_HSTS_PRELOAD` | Optional | Optional | Leave unset unless you intentionally want preload and already satisfy the preload requirements (`includeSubDomains` plus at least `31536000` seconds). | | `VERCEL_RUN_MIGRATIONS` | Set to `1` only for deliberate schema rollouts | Set to `1` only for isolated preview databases | Build-time migrations are always opt-in. | @@ -181,6 +182,12 @@ Vercel injects `VERCEL_ENV`, `VERCEL_URL`, `VERCEL_BRANCH_URL`, and `VERCEL_PROJ 4. Let pushes to non-`main` branches create Preview deployments automatically. Leave `VERCEL_RUN_MIGRATIONS` unset for normal deploys; only turn it on for isolated preview databases or a coordinated production schema rollout, then redeploy intentionally. 5. After each deployment, check `GET /health/ready/` for database readiness and `GET /health/live/` for the lightweight liveness probe. +#### Emergency hosted SQLite recovery + +Normal hosted deploys should continue to fail fast if `DATABASE_URL` is missing or broken. If you need a temporary outage recovery mode, you can opt into the hosted SQLite fallback by setting `DJANGO_ENABLE_HOSTED_SQLITE_FALLBACK=1` alongside a SQLite `DATABASE_URL`. + +This mode is for emergency recovery only. It moves sessions and flash messages to signed cookies so the app can run from a bundled SQLite snapshot on Vercel, but it should not replace the normal PostgreSQL-backed production path. + ### Health checks and monitoring hooks - `GET /health/live/` is a cheap liveness probe that does not touch the database. diff --git a/core/tests/test_deployment.py b/core/tests/test_deployment.py index b3d5768..dafe187 100644 --- a/core/tests/test_deployment.py +++ b/core/tests/test_deployment.py @@ -1,3 +1,8 @@ +import json +import os +import subprocess +import sys + from django.conf import settings from deep_workflow.deployment import ( @@ -117,3 +122,58 @@ def test_request_id_middleware_precedes_whitenoise() -> None: ) < settings.MIDDLEWARE.index( "whitenoise.middleware.WhiteNoiseMiddleware", ) + + +def load_hosted_sqlite_settings(*, enable_fallback: bool) -> tuple[str, str, str]: + env = os.environ.copy() + env.update( + { + "DJANGO_DEBUG": "False", + "DJANGO_SECRET_KEY": "x" * 64, + "VERCEL_ENV": "production", + "VERCEL": "1", + "DATABASE_URL": "sqlite:///db.sqlite3", + "DJANGO_ENABLE_HOSTED_SQLITE_FALLBACK": "1" if enable_fallback else "0", + } + ) + command = [ + sys.executable, + "-c", + ( + "import json; " + "import deep_workflow.settings as settings; " + "print(json.dumps([" + "str(settings.HOSTED_SQLITE_FALLBACK), " + "getattr(settings, 'SESSION_ENGINE', ''), " + "getattr(settings, 'MESSAGE_STORAGE', '')" + "]))" + ), + ] + result = subprocess.run( + command, + env=env, + check=True, + capture_output=True, + text=True, + ) + return tuple(json.loads(result.stdout)) + + +def test_hosted_sqlite_fallback_is_disabled_by_default() -> None: + hosted_sqlite_fallback, session_engine, message_storage = ( + load_hosted_sqlite_settings(enable_fallback=False) + ) + + assert hosted_sqlite_fallback == "False" + assert session_engine == "" + assert message_storage == "" + + +def test_hosted_sqlite_fallback_requires_explicit_opt_in() -> None: + hosted_sqlite_fallback, session_engine, message_storage = ( + load_hosted_sqlite_settings(enable_fallback=True) + ) + + assert hosted_sqlite_fallback == "True" + assert session_engine == "django.contrib.sessions.backends.signed_cookies" + assert message_storage == "django.contrib.messages.storage.cookie.CookieStorage" diff --git a/deep_workflow/settings.py b/deep_workflow/settings.py index ab95a21..0f953e9 100644 --- a/deep_workflow/settings.py +++ b/deep_workflow/settings.py @@ -32,6 +32,7 @@ DJANGO_DB_CONN_MAX_AGE=(int, 60), DJANGO_DB_SSL_MODE=(str, "require"), DJANGO_DEBUG=(bool, DEFAULT_DEBUG), + DJANGO_ENABLE_HOSTED_SQLITE_FALLBACK=(bool, False), DJANGO_LOG_LEVEL=(str, "DEBUG" if DEFAULT_DEBUG else "INFO"), DJANGO_REQUEST_LOG_LEVEL=(str, "WARNING"), DJANGO_SECRET_KEY=(str, "unsafe-local-development-key"), @@ -112,6 +113,11 @@ } DATABASES["default"]["CONN_MAX_AGE"] = env.int("DJANGO_DB_CONN_MAX_AGE", default=60) DATABASES["default"]["CONN_HEALTH_CHECKS"] = True +HOSTED_SQLITE_FALLBACK = ( + HOSTED_ENV + and env.bool("DJANGO_ENABLE_HOSTED_SQLITE_FALLBACK") + and DATABASES["default"]["ENGINE"] == "django.db.backends.sqlite3" +) if HOSTED_ENV and DATABASES["default"]["ENGINE"] == "django.db.backends.postgresql": DATABASES["default"].setdefault("OPTIONS", {}) @@ -211,6 +217,10 @@ LOGIN_REDIRECT_URL = "home" LOGOUT_REDIRECT_URL = "login" +if HOSTED_SQLITE_FALLBACK: + SESSION_ENGINE = "django.contrib.sessions.backends.signed_cookies" + MESSAGE_STORAGE = "django.contrib.messages.storage.cookie.CookieStorage" + LOG_LEVEL = env("DJANGO_LOG_LEVEL").upper() REQUEST_LOG_LEVEL = env("DJANGO_REQUEST_LOG_LEVEL").upper() LOGGING = { From 36cf212cb52963495bdd442d772355f31c79f2a1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 18 Apr 2026 00:42:36 +0000 Subject: [PATCH 02/15] fix(deploy): Address PR review feedback on hosted SQLite fallback gating Agent-Logs-Url: https://github.com/mathemage/deep-workflow/sessions/443c7d6a-3fb6-46e8-afcf-bf5946cd691a Co-authored-by: mathemage <3373514+mathemage@users.noreply.github.com> --- README.md | 2 +- core/tests/test_deployment.py | 49 ++++++++++++++++++++++++++++------- deep_workflow/settings.py | 10 +++++++ 3 files changed, 50 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index d12f381..610987e 100644 --- a/README.md +++ b/README.md @@ -186,7 +186,7 @@ Vercel injects `VERCEL_ENV`, `VERCEL_URL`, `VERCEL_BRANCH_URL`, and `VERCEL_PROJ Normal hosted deploys should continue to fail fast if `DATABASE_URL` is missing or broken. If you need a temporary outage recovery mode, you can opt into the hosted SQLite fallback by setting `DJANGO_ENABLE_HOSTED_SQLITE_FALLBACK=1` alongside a SQLite `DATABASE_URL`. -This mode is for emergency recovery only. It moves sessions and flash messages to signed cookies so the app can run from a bundled SQLite snapshot on Vercel, but it should not replace the normal PostgreSQL-backed production path. +This mode is for emergency recovery only. It moves sessions and flash messages to signed cookies so the app can run from a bundled SQLite snapshot on Vercel, but it should not replace the normal PostgreSQL-backed production path. Only use it when your session and message payloads are small enough to fit within browser cookie limits, and do not treat the cookie contents as secret: signed cookies are tamper-resistant, but their contents remain readable by the client. ### Health checks and monitoring hooks diff --git a/core/tests/test_deployment.py b/core/tests/test_deployment.py index dafe187..81a5a36 100644 --- a/core/tests/test_deployment.py +++ b/core/tests/test_deployment.py @@ -124,7 +124,11 @@ def test_request_id_middleware_precedes_whitenoise() -> None: ) -def load_hosted_sqlite_settings(*, enable_fallback: bool) -> tuple[str, str, str]: +def load_hosted_sqlite_settings( + *, + enable_fallback: bool, + database_url: str = "sqlite:///db.sqlite3", +) -> tuple[bool, str, str]: env = os.environ.copy() env.update( { @@ -132,7 +136,7 @@ def load_hosted_sqlite_settings(*, enable_fallback: bool) -> tuple[str, str, str "DJANGO_SECRET_KEY": "x" * 64, "VERCEL_ENV": "production", "VERCEL": "1", - "DATABASE_URL": "sqlite:///db.sqlite3", + "DATABASE_URL": database_url, "DJANGO_ENABLE_HOSTED_SQLITE_FALLBACK": "1" if enable_fallback else "0", } ) @@ -143,7 +147,7 @@ def load_hosted_sqlite_settings(*, enable_fallback: bool) -> tuple[str, str, str "import json; " "import deep_workflow.settings as settings; " "print(json.dumps([" - "str(settings.HOSTED_SQLITE_FALLBACK), " + "settings.HOSTED_SQLITE_FALLBACK, " "getattr(settings, 'SESSION_ENGINE', ''), " "getattr(settings, 'MESSAGE_STORAGE', '')" "]))" @@ -160,13 +164,25 @@ def load_hosted_sqlite_settings(*, enable_fallback: bool) -> tuple[str, str, str def test_hosted_sqlite_fallback_is_disabled_by_default() -> None: - hosted_sqlite_fallback, session_engine, message_storage = ( - load_hosted_sqlite_settings(enable_fallback=False) + env = os.environ.copy() + env.update( + { + "DJANGO_DEBUG": "False", + "DJANGO_SECRET_KEY": "x" * 64, + "VERCEL_ENV": "production", + "VERCEL": "1", + "DATABASE_URL": "sqlite:///db.sqlite3", + "DJANGO_ENABLE_HOSTED_SQLITE_FALLBACK": "0", + } ) - - assert hosted_sqlite_fallback == "False" - assert session_engine == "" - assert message_storage == "" + result = subprocess.run( + [sys.executable, "-c", "import deep_workflow.settings"], + env=env, + capture_output=True, + text=True, + ) + assert result.returncode != 0 + assert "ImproperlyConfigured" in result.stderr def test_hosted_sqlite_fallback_requires_explicit_opt_in() -> None: @@ -174,6 +190,19 @@ def test_hosted_sqlite_fallback_requires_explicit_opt_in() -> None: load_hosted_sqlite_settings(enable_fallback=True) ) - assert hosted_sqlite_fallback == "True" + assert hosted_sqlite_fallback is True assert session_engine == "django.contrib.sessions.backends.signed_cookies" assert message_storage == "django.contrib.messages.storage.cookie.CookieStorage" + + +def test_hosted_sqlite_fallback_flag_ignored_for_postgresql() -> None: + hosted_sqlite_fallback, session_engine, message_storage = ( + load_hosted_sqlite_settings( + enable_fallback=True, + database_url="postgres://user:pass@localhost:5432/mydb", + ) + ) + + assert hosted_sqlite_fallback is False + assert session_engine == "" + assert message_storage == "" diff --git a/deep_workflow/settings.py b/deep_workflow/settings.py index 0f953e9..980419b 100644 --- a/deep_workflow/settings.py +++ b/deep_workflow/settings.py @@ -119,6 +119,16 @@ and DATABASES["default"]["ENGINE"] == "django.db.backends.sqlite3" ) +if ( + HOSTED_ENV + and DATABASES["default"]["ENGINE"] == "django.db.backends.sqlite3" + and not HOSTED_SQLITE_FALLBACK +): + raise ImproperlyConfigured( + "Hosted deployments must not use SQLite unless " + "DJANGO_ENABLE_HOSTED_SQLITE_FALLBACK is enabled." + ) + if HOSTED_ENV and DATABASES["default"]["ENGINE"] == "django.db.backends.postgresql": DATABASES["default"].setdefault("OPTIONS", {}) DATABASES["default"]["OPTIONS"].setdefault( From f90ebe32efde5d5454a74eabf3000be64aa8670e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 18 Apr 2026 00:43:52 +0000 Subject: [PATCH 03/15] refactor(deploy): Rename helper to load_hosted_settings and use placeholder DB creds Agent-Logs-Url: https://github.com/mathemage/deep-workflow/sessions/443c7d6a-3fb6-46e8-afcf-bf5946cd691a Co-authored-by: mathemage <3373514+mathemage@users.noreply.github.com> --- core/tests/test_deployment.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/core/tests/test_deployment.py b/core/tests/test_deployment.py index 81a5a36..cb76114 100644 --- a/core/tests/test_deployment.py +++ b/core/tests/test_deployment.py @@ -124,7 +124,7 @@ def test_request_id_middleware_precedes_whitenoise() -> None: ) -def load_hosted_sqlite_settings( +def load_hosted_settings( *, enable_fallback: bool, database_url: str = "sqlite:///db.sqlite3", @@ -186,8 +186,8 @@ def test_hosted_sqlite_fallback_is_disabled_by_default() -> None: def test_hosted_sqlite_fallback_requires_explicit_opt_in() -> None: - hosted_sqlite_fallback, session_engine, message_storage = ( - load_hosted_sqlite_settings(enable_fallback=True) + hosted_sqlite_fallback, session_engine, message_storage = load_hosted_settings( + enable_fallback=True ) assert hosted_sqlite_fallback is True @@ -196,11 +196,9 @@ def test_hosted_sqlite_fallback_requires_explicit_opt_in() -> None: def test_hosted_sqlite_fallback_flag_ignored_for_postgresql() -> None: - hosted_sqlite_fallback, session_engine, message_storage = ( - load_hosted_sqlite_settings( - enable_fallback=True, - database_url="postgres://user:pass@localhost:5432/mydb", - ) + hosted_sqlite_fallback, session_engine, message_storage = load_hosted_settings( + enable_fallback=True, + database_url="postgres://testuser:testpass@localhost:5432/testdb", ) assert hosted_sqlite_fallback is False From 681514183f17a47f6ce27a8c47203ac9b6da9213 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 18 Apr 2026 22:11:07 +0000 Subject: [PATCH 04/15] test(deploy): Tighten hosted SQLite guard assertions and cover unset flag Agent-Logs-Url: https://github.com/mathemage/deep-workflow/sessions/849021ef-a502-4f93-918d-5088bc04d46c Co-authored-by: mathemage <3373514+mathemage@users.noreply.github.com> --- core/tests/test_deployment.py | 39 ++++++++++++++++++++++++++++++++--- deep_workflow/settings.py | 5 +++-- 2 files changed, 39 insertions(+), 5 deletions(-) diff --git a/core/tests/test_deployment.py b/core/tests/test_deployment.py index cb76114..b2a2c98 100644 --- a/core/tests/test_deployment.py +++ b/core/tests/test_deployment.py @@ -126,7 +126,7 @@ def test_request_id_middleware_precedes_whitenoise() -> None: def load_hosted_settings( *, - enable_fallback: bool, + enable_fallback: bool | None, database_url: str = "sqlite:///db.sqlite3", ) -> tuple[bool, str, str]: env = os.environ.copy() @@ -137,9 +137,12 @@ def load_hosted_settings( "VERCEL_ENV": "production", "VERCEL": "1", "DATABASE_URL": database_url, - "DJANGO_ENABLE_HOSTED_SQLITE_FALLBACK": "1" if enable_fallback else "0", } ) + if enable_fallback is None: + env.pop("DJANGO_ENABLE_HOSTED_SQLITE_FALLBACK", None) + else: + env["DJANGO_ENABLE_HOSTED_SQLITE_FALLBACK"] = "1" if enable_fallback else "0" command = [ sys.executable, "-c", @@ -163,7 +166,7 @@ def load_hosted_settings( return tuple(json.loads(result.stdout)) -def test_hosted_sqlite_fallback_is_disabled_by_default() -> None: +def test_hosted_sqlite_fallback_is_disabled_by_default_when_flag_is_zero() -> None: env = os.environ.copy() env.update( { @@ -183,6 +186,36 @@ def test_hosted_sqlite_fallback_is_disabled_by_default() -> None: ) assert result.returncode != 0 assert "ImproperlyConfigured" in result.stderr + assert "Hosted deployments require a valid PostgreSQL DATABASE_URL" in result.stderr + assert "Do not use SQLite unless DJANGO_ENABLE_HOSTED_SQLITE_FALLBACK" in ( + result.stderr + ) + + +def test_hosted_sqlite_fallback_is_disabled_by_default_when_flag_is_unset() -> None: + env = os.environ.copy() + env.update( + { + "DJANGO_DEBUG": "False", + "DJANGO_SECRET_KEY": "x" * 64, + "VERCEL_ENV": "production", + "VERCEL": "1", + "DATABASE_URL": "sqlite:///db.sqlite3", + } + ) + env.pop("DJANGO_ENABLE_HOSTED_SQLITE_FALLBACK", None) + result = subprocess.run( + [sys.executable, "-c", "import deep_workflow.settings"], + env=env, + capture_output=True, + text=True, + ) + assert result.returncode != 0 + assert "ImproperlyConfigured" in result.stderr + assert "Hosted deployments require a valid PostgreSQL DATABASE_URL" in result.stderr + assert "Do not use SQLite unless DJANGO_ENABLE_HOSTED_SQLITE_FALLBACK" in ( + result.stderr + ) def test_hosted_sqlite_fallback_requires_explicit_opt_in() -> None: diff --git a/deep_workflow/settings.py b/deep_workflow/settings.py index 980419b..a2581f8 100644 --- a/deep_workflow/settings.py +++ b/deep_workflow/settings.py @@ -125,8 +125,9 @@ and not HOSTED_SQLITE_FALLBACK ): raise ImproperlyConfigured( - "Hosted deployments must not use SQLite unless " - "DJANGO_ENABLE_HOSTED_SQLITE_FALLBACK is enabled." + "Hosted deployments require a valid PostgreSQL DATABASE_URL. " + "Do not use SQLite unless DJANGO_ENABLE_HOSTED_SQLITE_FALLBACK " + "is enabled for emergency recovery." ) if HOSTED_ENV and DATABASES["default"]["ENGINE"] == "django.db.backends.postgresql": From e8b21d8d8c13f669c47ce2626f837279333fc19a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 18 Apr 2026 22:29:16 +0000 Subject: [PATCH 05/15] test(deploy): Cover missing and invalid hosted DATABASE_URL cases Agent-Logs-Url: https://github.com/mathemage/deep-workflow/sessions/8087ddb6-dfe7-43f3-b4ca-da2fca5c482a Co-authored-by: mathemage <3373514+mathemage@users.noreply.github.com> --- core/tests/test_deployment.py | 75 ++++++++++++++++++++++++++++++++++- deep_workflow/settings.py | 7 ++-- 2 files changed, 77 insertions(+), 5 deletions(-) diff --git a/core/tests/test_deployment.py b/core/tests/test_deployment.py index b2a2c98..0cb010d 100644 --- a/core/tests/test_deployment.py +++ b/core/tests/test_deployment.py @@ -127,7 +127,7 @@ def test_request_id_middleware_precedes_whitenoise() -> None: def load_hosted_settings( *, enable_fallback: bool | None, - database_url: str = "sqlite:///db.sqlite3", + database_url: str | None = "sqlite:///db.sqlite3", ) -> tuple[bool, str, str]: env = os.environ.copy() env.update( @@ -136,9 +136,12 @@ def load_hosted_settings( "DJANGO_SECRET_KEY": "x" * 64, "VERCEL_ENV": "production", "VERCEL": "1", - "DATABASE_URL": database_url, } ) + if database_url is None: + env.pop("DATABASE_URL", None) + else: + env["DATABASE_URL"] = database_url if enable_fallback is None: env.pop("DJANGO_ENABLE_HOSTED_SQLITE_FALLBACK", None) else: @@ -218,6 +221,32 @@ def test_hosted_sqlite_fallback_is_disabled_by_default_when_flag_is_unset() -> N ) +def test_hosted_sqlite_fallback_is_disabled_when_database_url_is_unset() -> None: + env = os.environ.copy() + env.update( + { + "DJANGO_DEBUG": "False", + "DJANGO_SECRET_KEY": "x" * 64, + "VERCEL_ENV": "production", + "VERCEL": "1", + } + ) + env.pop("DATABASE_URL", None) + env.pop("DJANGO_ENABLE_HOSTED_SQLITE_FALLBACK", None) + result = subprocess.run( + [sys.executable, "-c", "import deep_workflow.settings"], + env=env, + capture_output=True, + text=True, + ) + assert result.returncode != 0 + assert "ImproperlyConfigured" in result.stderr + assert "Hosted deployments require a valid PostgreSQL DATABASE_URL" in result.stderr + assert "Do not use SQLite unless DJANGO_ENABLE_HOSTED_SQLITE_FALLBACK" in ( + result.stderr + ) + + def test_hosted_sqlite_fallback_requires_explicit_opt_in() -> None: hosted_sqlite_fallback, session_engine, message_storage = load_hosted_settings( enable_fallback=True @@ -228,6 +257,19 @@ def test_hosted_sqlite_fallback_requires_explicit_opt_in() -> None: assert message_storage == "django.contrib.messages.storage.cookie.CookieStorage" +def test_hosted_sqlite_fallback_supports_unset_database_url_with_explicit_opt_in() -> ( + None +): + hosted_sqlite_fallback, session_engine, message_storage = load_hosted_settings( + enable_fallback=True, + database_url=None, + ) + + assert hosted_sqlite_fallback is True + assert session_engine == "django.contrib.sessions.backends.signed_cookies" + assert message_storage == "django.contrib.messages.storage.cookie.CookieStorage" + + def test_hosted_sqlite_fallback_flag_ignored_for_postgresql() -> None: hosted_sqlite_fallback, session_engine, message_storage = load_hosted_settings( enable_fallback=True, @@ -237,3 +279,32 @@ def test_hosted_sqlite_fallback_flag_ignored_for_postgresql() -> None: assert hosted_sqlite_fallback is False assert session_engine == "" assert message_storage == "" + + +def test_hosted_sqlite_fallback_is_not_applied_to_invalid_database_url() -> None: + env = os.environ.copy() + env.update( + { + "DJANGO_DEBUG": "False", + "DJANGO_SECRET_KEY": "x" * 64, + "VERCEL_ENV": "production", + "VERCEL": "1", + "DATABASE_URL": "not-a-valid-database-url", + "DJANGO_ENABLE_HOSTED_SQLITE_FALLBACK": "1", + } + ) + result = subprocess.run( + [sys.executable, "-c", "import deep_workflow.settings"], + env=env, + capture_output=True, + text=True, + ) + + assert result.returncode != 0 + assert "ImproperlyConfigured" in result.stderr + assert "Hosted deployments require a valid PostgreSQL DATABASE_URL" in result.stderr + assert "Do not use SQLite unless DJANGO_ENABLE_HOSTED_SQLITE_FALLBACK" in ( + result.stderr + ) + assert "django.contrib.sessions.backends.signed_cookies" not in result.stderr + assert "django.contrib.messages.storage.cookie.CookieStorage" not in result.stderr diff --git a/deep_workflow/settings.py b/deep_workflow/settings.py index a2581f8..d7189b4 100644 --- a/deep_workflow/settings.py +++ b/deep_workflow/settings.py @@ -113,15 +113,16 @@ } DATABASES["default"]["CONN_MAX_AGE"] = env.int("DJANGO_DB_CONN_MAX_AGE", default=60) DATABASES["default"]["CONN_HEALTH_CHECKS"] = True +DATABASE_ENGINE = DATABASES["default"].get("ENGINE") HOSTED_SQLITE_FALLBACK = ( HOSTED_ENV and env.bool("DJANGO_ENABLE_HOSTED_SQLITE_FALLBACK") - and DATABASES["default"]["ENGINE"] == "django.db.backends.sqlite3" + and DATABASE_ENGINE == "django.db.backends.sqlite3" ) if ( HOSTED_ENV - and DATABASES["default"]["ENGINE"] == "django.db.backends.sqlite3" + and DATABASE_ENGINE != "django.db.backends.postgresql" and not HOSTED_SQLITE_FALLBACK ): raise ImproperlyConfigured( @@ -130,7 +131,7 @@ "is enabled for emergency recovery." ) -if HOSTED_ENV and DATABASES["default"]["ENGINE"] == "django.db.backends.postgresql": +if HOSTED_ENV and DATABASE_ENGINE == "django.db.backends.postgresql": DATABASES["default"].setdefault("OPTIONS", {}) DATABASES["default"]["OPTIONS"].setdefault( "sslmode", From 53adce71de59b66549badc85a8f7879f579ec18b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 18 Apr 2026 22:58:32 +0000 Subject: [PATCH 06/15] test(deploy): Clarify invalid hosted DATABASE_URL fixture #31 Agent-Logs-Url: https://github.com/mathemage/deep-workflow/sessions/44b7f3aa-afa7-4359-836d-87b4c24b5fab Co-authored-by: mathemage <3373514+mathemage@users.noreply.github.com> --- core/tests/test_deployment.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/tests/test_deployment.py b/core/tests/test_deployment.py index 0cb010d..defaa28 100644 --- a/core/tests/test_deployment.py +++ b/core/tests/test_deployment.py @@ -289,7 +289,7 @@ def test_hosted_sqlite_fallback_is_not_applied_to_invalid_database_url() -> None "DJANGO_SECRET_KEY": "x" * 64, "VERCEL_ENV": "production", "VERCEL": "1", - "DATABASE_URL": "not-a-valid-database-url", + "DATABASE_URL": "invalid://definitely-not-a-supported-database-url", "DJANGO_ENABLE_HOSTED_SQLITE_FALLBACK": "1", } ) From 9ae204a3f5f101cd4ab8b14876804d8441bf5763 Mon Sep 17 00:00:00 2001 From: Karel Ha Date: Sun, 19 Apr 2026 17:59:09 +0200 Subject: [PATCH 07/15] refactor(test_deployment): Re-format as a normal single-line `-> None:` signature Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- core/tests/test_deployment.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/core/tests/test_deployment.py b/core/tests/test_deployment.py index defaa28..52047ea 100644 --- a/core/tests/test_deployment.py +++ b/core/tests/test_deployment.py @@ -257,9 +257,7 @@ def test_hosted_sqlite_fallback_requires_explicit_opt_in() -> None: assert message_storage == "django.contrib.messages.storage.cookie.CookieStorage" -def test_hosted_sqlite_fallback_supports_unset_database_url_with_explicit_opt_in() -> ( - None -): +def test_hosted_sqlite_fallback_supports_unset_database_url_with_explicit_opt_in() -> None: hosted_sqlite_fallback, session_engine, message_storage = load_hosted_settings( enable_fallback=True, database_url=None, From 512524f3eb6a7a7e174b104bdb49019e4302ea5e Mon Sep 17 00:00:00 2001 From: Karel Ha Date: Sun, 19 Apr 2026 18:02:54 +0200 Subject: [PATCH 08/15] test(deploy): Format hosted SQLite fallback tests #31 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- core/tests/test_deployment.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/core/tests/test_deployment.py b/core/tests/test_deployment.py index 52047ea..defaa28 100644 --- a/core/tests/test_deployment.py +++ b/core/tests/test_deployment.py @@ -257,7 +257,9 @@ def test_hosted_sqlite_fallback_requires_explicit_opt_in() -> None: assert message_storage == "django.contrib.messages.storage.cookie.CookieStorage" -def test_hosted_sqlite_fallback_supports_unset_database_url_with_explicit_opt_in() -> None: +def test_hosted_sqlite_fallback_supports_unset_database_url_with_explicit_opt_in() -> ( + None +): hosted_sqlite_fallback, session_engine, message_storage = load_hosted_settings( enable_fallback=True, database_url=None, From 98f11681ebf1504e9edd31457d3b28b255124faa Mon Sep 17 00:00:00 2001 From: Karel Ha Date: Sun, 19 Apr 2026 18:06:16 +0200 Subject: [PATCH 09/15] fix(deploy): Require explicit SQLite recovery URL #31 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- core/tests/test_deployment.py | 34 ++++++++++++++++++++++++---------- deep_workflow/settings.py | 2 ++ 2 files changed, 26 insertions(+), 10 deletions(-) diff --git a/core/tests/test_deployment.py b/core/tests/test_deployment.py index defaa28..c2b832f 100644 --- a/core/tests/test_deployment.py +++ b/core/tests/test_deployment.py @@ -257,23 +257,37 @@ def test_hosted_sqlite_fallback_requires_explicit_opt_in() -> None: assert message_storage == "django.contrib.messages.storage.cookie.CookieStorage" -def test_hosted_sqlite_fallback_supports_unset_database_url_with_explicit_opt_in() -> ( - None -): - hosted_sqlite_fallback, session_engine, message_storage = load_hosted_settings( - enable_fallback=True, - database_url=None, +def test_hosted_sqlite_fallback_requires_explicit_database_url() -> None: + env = os.environ.copy() + env.update( + { + "DJANGO_DEBUG": "False", + "DJANGO_SECRET_KEY": "x" * 64, + "VERCEL_ENV": "production", + "VERCEL": "1", + "DJANGO_ENABLE_HOSTED_SQLITE_FALLBACK": "1", + } + ) + env.pop("DATABASE_URL", None) + result = subprocess.run( + [sys.executable, "-c", "import deep_workflow.settings"], + env=env, + capture_output=True, + text=True, ) - assert hosted_sqlite_fallback is True - assert session_engine == "django.contrib.sessions.backends.signed_cookies" - assert message_storage == "django.contrib.messages.storage.cookie.CookieStorage" + assert result.returncode != 0 + assert "ImproperlyConfigured" in result.stderr + assert "Hosted deployments require a valid PostgreSQL DATABASE_URL" in result.stderr + assert "Do not use SQLite unless DJANGO_ENABLE_HOSTED_SQLITE_FALLBACK" in ( + result.stderr + ) def test_hosted_sqlite_fallback_flag_ignored_for_postgresql() -> None: hosted_sqlite_fallback, session_engine, message_storage = load_hosted_settings( enable_fallback=True, - database_url="postgres://testuser:testpass@localhost:5432/testdb", + database_url="postgresql://testuser:testpass@localhost:5432/testdb", ) assert hosted_sqlite_fallback is False diff --git a/deep_workflow/settings.py b/deep_workflow/settings.py index d7189b4..7054945 100644 --- a/deep_workflow/settings.py +++ b/deep_workflow/settings.py @@ -113,10 +113,12 @@ } DATABASES["default"]["CONN_MAX_AGE"] = env.int("DJANGO_DB_CONN_MAX_AGE", default=60) DATABASES["default"]["CONN_HEALTH_CHECKS"] = True +RAW_DATABASE_URL = os.environ.get("DATABASE_URL", "").strip() DATABASE_ENGINE = DATABASES["default"].get("ENGINE") HOSTED_SQLITE_FALLBACK = ( HOSTED_ENV and env.bool("DJANGO_ENABLE_HOSTED_SQLITE_FALLBACK") + and bool(RAW_DATABASE_URL) and DATABASE_ENGINE == "django.db.backends.sqlite3" ) From abde7c71776043aeac14ffb1326a052530f6d728 Mon Sep 17 00:00:00 2001 From: Karel Ha Date: Sun, 19 Apr 2026 18:16:14 +0200 Subject: [PATCH 10/15] test(deploy): Isolate hosted fallback settings tests #31 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- core/tests/test_deployment.py | 71 ++++++++++++++++++++++++++++------- deep_workflow/settings.py | 12 +++--- 2 files changed, 63 insertions(+), 20 deletions(-) diff --git a/core/tests/test_deployment.py b/core/tests/test_deployment.py index c2b832f..affb4f4 100644 --- a/core/tests/test_deployment.py +++ b/core/tests/test_deployment.py @@ -16,6 +16,17 @@ hsts_preload_enabled, ) +IMPORT_HOSTED_SETTINGS_COMMAND = ( + "import environ; " + "environ.Env.read_env = staticmethod(lambda *args, **kwargs: None); " + "import deep_workflow.settings as settings; " + "print(json.dumps([" + "settings.HOSTED_SQLITE_FALLBACK, " + "getattr(settings, 'SESSION_ENGINE', ''), " + "getattr(settings, 'MESSAGE_STORAGE', '')" + "]))" +) + def test_build_allowed_hosts_adds_vercel_runtime_hosts() -> None: environ = { @@ -149,15 +160,7 @@ def load_hosted_settings( command = [ sys.executable, "-c", - ( - "import json; " - "import deep_workflow.settings as settings; " - "print(json.dumps([" - "settings.HOSTED_SQLITE_FALLBACK, " - "getattr(settings, 'SESSION_ENGINE', ''), " - "getattr(settings, 'MESSAGE_STORAGE', '')" - "]))" - ), + f"import json; {IMPORT_HOSTED_SETTINGS_COMMAND}", ] result = subprocess.run( command, @@ -182,7 +185,15 @@ def test_hosted_sqlite_fallback_is_disabled_by_default_when_flag_is_zero() -> No } ) result = subprocess.run( - [sys.executable, "-c", "import deep_workflow.settings"], + [ + sys.executable, + "-c", + ( + "import environ; " + "environ.Env.read_env = staticmethod(lambda *args, **kwargs: None); " + "import deep_workflow.settings" + ), + ], env=env, capture_output=True, text=True, @@ -208,7 +219,15 @@ def test_hosted_sqlite_fallback_is_disabled_by_default_when_flag_is_unset() -> N ) env.pop("DJANGO_ENABLE_HOSTED_SQLITE_FALLBACK", None) result = subprocess.run( - [sys.executable, "-c", "import deep_workflow.settings"], + [ + sys.executable, + "-c", + ( + "import environ; " + "environ.Env.read_env = staticmethod(lambda *args, **kwargs: None); " + "import deep_workflow.settings" + ), + ], env=env, capture_output=True, text=True, @@ -234,7 +253,15 @@ def test_hosted_sqlite_fallback_is_disabled_when_database_url_is_unset() -> None env.pop("DATABASE_URL", None) env.pop("DJANGO_ENABLE_HOSTED_SQLITE_FALLBACK", None) result = subprocess.run( - [sys.executable, "-c", "import deep_workflow.settings"], + [ + sys.executable, + "-c", + ( + "import environ; " + "environ.Env.read_env = staticmethod(lambda *args, **kwargs: None); " + "import deep_workflow.settings" + ), + ], env=env, capture_output=True, text=True, @@ -270,7 +297,15 @@ def test_hosted_sqlite_fallback_requires_explicit_database_url() -> None: ) env.pop("DATABASE_URL", None) result = subprocess.run( - [sys.executable, "-c", "import deep_workflow.settings"], + [ + sys.executable, + "-c", + ( + "import environ; " + "environ.Env.read_env = staticmethod(lambda *args, **kwargs: None); " + "import deep_workflow.settings" + ), + ], env=env, capture_output=True, text=True, @@ -308,7 +343,15 @@ def test_hosted_sqlite_fallback_is_not_applied_to_invalid_database_url() -> None } ) result = subprocess.run( - [sys.executable, "-c", "import deep_workflow.settings"], + [ + sys.executable, + "-c", + ( + "import environ; " + "environ.Env.read_env = staticmethod(lambda *args, **kwargs: None); " + "import deep_workflow.settings" + ), + ], env=env, capture_output=True, text=True, diff --git a/deep_workflow/settings.py b/deep_workflow/settings.py index 7054945..d375dde 100644 --- a/deep_workflow/settings.py +++ b/deep_workflow/settings.py @@ -113,18 +113,18 @@ } DATABASES["default"]["CONN_MAX_AGE"] = env.int("DJANGO_DB_CONN_MAX_AGE", default=60) DATABASES["default"]["CONN_HEALTH_CHECKS"] = True -RAW_DATABASE_URL = os.environ.get("DATABASE_URL", "").strip() -DATABASE_ENGINE = DATABASES["default"].get("ENGINE") +raw_database_url = os.environ.get("DATABASE_URL", "").strip() +database_engine = DATABASES["default"].get("ENGINE") HOSTED_SQLITE_FALLBACK = ( HOSTED_ENV and env.bool("DJANGO_ENABLE_HOSTED_SQLITE_FALLBACK") - and bool(RAW_DATABASE_URL) - and DATABASE_ENGINE == "django.db.backends.sqlite3" + and bool(raw_database_url) + and database_engine == "django.db.backends.sqlite3" ) if ( HOSTED_ENV - and DATABASE_ENGINE != "django.db.backends.postgresql" + and database_engine != "django.db.backends.postgresql" and not HOSTED_SQLITE_FALLBACK ): raise ImproperlyConfigured( @@ -133,7 +133,7 @@ "is enabled for emergency recovery." ) -if HOSTED_ENV and DATABASE_ENGINE == "django.db.backends.postgresql": +if HOSTED_ENV and database_engine == "django.db.backends.postgresql": DATABASES["default"].setdefault("OPTIONS", {}) DATABASES["default"]["OPTIONS"].setdefault( "sslmode", From 6680cf6eb8053c67ddcf89e0c4647e43a0eed76a Mon Sep 17 00:00:00 2001 From: Karel Ha Date: Sun, 19 Apr 2026 18:39:05 +0200 Subject: [PATCH 11/15] fix(deploy): Normalize hosted database config errors #31 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- deep_workflow/settings.py | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/deep_workflow/settings.py b/deep_workflow/settings.py index d375dde..8a175a0 100644 --- a/deep_workflow/settings.py +++ b/deep_workflow/settings.py @@ -64,6 +64,12 @@ if not DEBUG and SECRET_KEY == "unsafe-local-development-key": raise ImproperlyConfigured("Set DJANGO_SECRET_KEY before disabling DJANGO_DEBUG.") +HOSTED_DATABASE_CONFIGURATION_ERROR = ( + "Hosted deployments require a valid PostgreSQL DATABASE_URL. " + "Do not use SQLite unless DJANGO_ENABLE_HOSTED_SQLITE_FALLBACK " + "is enabled for emergency recovery." +) + INSTALLED_APPS = [ "django.contrib.admin", "django.contrib.auth", @@ -108,9 +114,17 @@ WSGI_APPLICATION = "deep_workflow.wsgi.application" -DATABASES = { - "default": env.db("DATABASE_URL", default=f"sqlite:///{BASE_DIR / 'db.sqlite3'}") -} +try: + default_database = env.db( + "DATABASE_URL", + default=f"sqlite:///{BASE_DIR / 'db.sqlite3'}", + ) +except ImproperlyConfigured as exc: + if HOSTED_ENV: + raise ImproperlyConfigured(HOSTED_DATABASE_CONFIGURATION_ERROR) from exc + raise + +DATABASES = {"default": default_database} DATABASES["default"]["CONN_MAX_AGE"] = env.int("DJANGO_DB_CONN_MAX_AGE", default=60) DATABASES["default"]["CONN_HEALTH_CHECKS"] = True raw_database_url = os.environ.get("DATABASE_URL", "").strip() @@ -127,11 +141,7 @@ and database_engine != "django.db.backends.postgresql" and not HOSTED_SQLITE_FALLBACK ): - raise ImproperlyConfigured( - "Hosted deployments require a valid PostgreSQL DATABASE_URL. " - "Do not use SQLite unless DJANGO_ENABLE_HOSTED_SQLITE_FALLBACK " - "is enabled for emergency recovery." - ) + raise ImproperlyConfigured(HOSTED_DATABASE_CONFIGURATION_ERROR) if HOSTED_ENV and database_engine == "django.db.backends.postgresql": DATABASES["default"].setdefault("OPTIONS", {}) From bbe98c19a97a6e223d538f0120675a5b1ad10324 Mon Sep 17 00:00:00 2001 From: Karel Ha Date: Sun, 19 Apr 2026 18:48:54 +0200 Subject: [PATCH 12/15] test(deploy): Assert effective hosted fallback backends #31 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- core/tests/test_deployment.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/core/tests/test_deployment.py b/core/tests/test_deployment.py index affb4f4..2b55ae4 100644 --- a/core/tests/test_deployment.py +++ b/core/tests/test_deployment.py @@ -20,10 +20,11 @@ "import environ; " "environ.Env.read_env = staticmethod(lambda *args, **kwargs: None); " "import deep_workflow.settings as settings; " + "from django.conf import settings as django_settings; " "print(json.dumps([" "settings.HOSTED_SQLITE_FALLBACK, " - "getattr(settings, 'SESSION_ENGINE', ''), " - "getattr(settings, 'MESSAGE_STORAGE', '')" + "django_settings.SESSION_ENGINE, " + "django_settings.MESSAGE_STORAGE" "]))" ) @@ -144,6 +145,7 @@ def load_hosted_settings( env.update( { "DJANGO_DEBUG": "False", + "DJANGO_SETTINGS_MODULE": "deep_workflow.settings", "DJANGO_SECRET_KEY": "x" * 64, "VERCEL_ENV": "production", "VERCEL": "1", @@ -326,8 +328,8 @@ def test_hosted_sqlite_fallback_flag_ignored_for_postgresql() -> None: ) assert hosted_sqlite_fallback is False - assert session_engine == "" - assert message_storage == "" + assert session_engine != "django.contrib.sessions.backends.signed_cookies" + assert message_storage != "django.contrib.messages.storage.cookie.CookieStorage" def test_hosted_sqlite_fallback_is_not_applied_to_invalid_database_url() -> None: From 428d813a2adbae5e73830dc86f92ec37f2cfe2a7 Mon Sep 17 00:00:00 2001 From: Karel Ha Date: Sun, 19 Apr 2026 19:10:46 +0200 Subject: [PATCH 13/15] test(deploy): Simplify hosted fallback helper usage #31 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- core/tests/test_deployment.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/core/tests/test_deployment.py b/core/tests/test_deployment.py index 2b55ae4..bc264fe 100644 --- a/core/tests/test_deployment.py +++ b/core/tests/test_deployment.py @@ -277,9 +277,8 @@ def test_hosted_sqlite_fallback_is_disabled_when_database_url_is_unset() -> None def test_hosted_sqlite_fallback_requires_explicit_opt_in() -> None: - hosted_sqlite_fallback, session_engine, message_storage = load_hosted_settings( - enable_fallback=True - ) + hosted_settings = load_hosted_settings(enable_fallback=True) + hosted_sqlite_fallback, session_engine, message_storage = hosted_settings assert hosted_sqlite_fallback is True assert session_engine == "django.contrib.sessions.backends.signed_cookies" From 542db2c029e09751db2834376cba89c6f162da15 Mon Sep 17 00:00:00 2001 From: Karel Ha Date: Sun, 19 Apr 2026 19:24:05 +0200 Subject: [PATCH 14/15] docs(settings.py): explicitly state the two valid hosted configurations: (1) PostgreSQL `DATABASE_URL`, or (2) `DJANGO_ENABLE_HOSTED_SQLITE_FALLBACK=1` + a SQLite `DATABASE_URL` (emergency only) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- deep_workflow/settings.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/deep_workflow/settings.py b/deep_workflow/settings.py index 8a175a0..c70ecf2 100644 --- a/deep_workflow/settings.py +++ b/deep_workflow/settings.py @@ -65,9 +65,10 @@ raise ImproperlyConfigured("Set DJANGO_SECRET_KEY before disabling DJANGO_DEBUG.") HOSTED_DATABASE_CONFIGURATION_ERROR = ( - "Hosted deployments require a valid PostgreSQL DATABASE_URL. " - "Do not use SQLite unless DJANGO_ENABLE_HOSTED_SQLITE_FALLBACK " - "is enabled for emergency recovery." + "Hosted deployments require one of these database configurations: " + "(1) a valid PostgreSQL DATABASE_URL; or " + "(2) for emergency recovery only, DJANGO_ENABLE_HOSTED_SQLITE_FALLBACK=1 " + "together with an explicit SQLite DATABASE_URL." ) INSTALLED_APPS = [ From a9e8fcb56824e72120c5bed899500b96ce4642ce Mon Sep 17 00:00:00 2001 From: Karel Ha Date: Sun, 19 Apr 2026 19:35:40 +0200 Subject: [PATCH 15/15] test(deploy): Share hosted settings import helper #31 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- core/tests/test_deployment.py | 142 +++++++++++----------------------- 1 file changed, 46 insertions(+), 96 deletions(-) diff --git a/core/tests/test_deployment.py b/core/tests/test_deployment.py index bc264fe..bc5d2e1 100644 --- a/core/tests/test_deployment.py +++ b/core/tests/test_deployment.py @@ -27,6 +27,11 @@ "django_settings.MESSAGE_STORAGE" "]))" ) +IMPORT_HOSTED_SETTINGS_SNIPPET = ( + "import environ; " + "environ.Env.read_env = staticmethod(lambda *args, **kwargs: None); " + "import deep_workflow.settings" +) def test_build_allowed_hosts_adds_vercel_runtime_hosts() -> None: @@ -174,7 +179,11 @@ def load_hosted_settings( return tuple(json.loads(result.stdout)) -def test_hosted_sqlite_fallback_is_disabled_by_default_when_flag_is_zero() -> None: +def run_hosted_settings_import( + *, + database_url: str | None, + enable_fallback: bool | None, +) -> subprocess.CompletedProcess[str]: env = os.environ.copy() env.update( { @@ -182,98 +191,61 @@ def test_hosted_sqlite_fallback_is_disabled_by_default_when_flag_is_zero() -> No "DJANGO_SECRET_KEY": "x" * 64, "VERCEL_ENV": "production", "VERCEL": "1", - "DATABASE_URL": "sqlite:///db.sqlite3", - "DJANGO_ENABLE_HOSTED_SQLITE_FALLBACK": "0", } ) - result = subprocess.run( - [ - sys.executable, - "-c", - ( - "import environ; " - "environ.Env.read_env = staticmethod(lambda *args, **kwargs: None); " - "import deep_workflow.settings" - ), - ], + if database_url is None: + env.pop("DATABASE_URL", None) + else: + env["DATABASE_URL"] = database_url + if enable_fallback is None: + env.pop("DJANGO_ENABLE_HOSTED_SQLITE_FALLBACK", None) + else: + env["DJANGO_ENABLE_HOSTED_SQLITE_FALLBACK"] = "1" if enable_fallback else "0" + return subprocess.run( + [sys.executable, "-c", IMPORT_HOSTED_SETTINGS_SNIPPET], env=env, capture_output=True, text=True, ) + + +def test_hosted_sqlite_fallback_is_disabled_by_default_when_flag_is_zero() -> None: + result = run_hosted_settings_import( + database_url="sqlite:///db.sqlite3", + enable_fallback=False, + ) assert result.returncode != 0 assert "ImproperlyConfigured" in result.stderr - assert "Hosted deployments require a valid PostgreSQL DATABASE_URL" in result.stderr - assert "Do not use SQLite unless DJANGO_ENABLE_HOSTED_SQLITE_FALLBACK" in ( + assert "Hosted deployments require one of these database configurations" in ( result.stderr ) + assert "DJANGO_ENABLE_HOSTED_SQLITE_FALLBACK=1" in result.stderr def test_hosted_sqlite_fallback_is_disabled_by_default_when_flag_is_unset() -> None: - env = os.environ.copy() - env.update( - { - "DJANGO_DEBUG": "False", - "DJANGO_SECRET_KEY": "x" * 64, - "VERCEL_ENV": "production", - "VERCEL": "1", - "DATABASE_URL": "sqlite:///db.sqlite3", - } - ) - env.pop("DJANGO_ENABLE_HOSTED_SQLITE_FALLBACK", None) - result = subprocess.run( - [ - sys.executable, - "-c", - ( - "import environ; " - "environ.Env.read_env = staticmethod(lambda *args, **kwargs: None); " - "import deep_workflow.settings" - ), - ], - env=env, - capture_output=True, - text=True, + result = run_hosted_settings_import( + database_url="sqlite:///db.sqlite3", + enable_fallback=None, ) assert result.returncode != 0 assert "ImproperlyConfigured" in result.stderr - assert "Hosted deployments require a valid PostgreSQL DATABASE_URL" in result.stderr - assert "Do not use SQLite unless DJANGO_ENABLE_HOSTED_SQLITE_FALLBACK" in ( + assert "Hosted deployments require one of these database configurations" in ( result.stderr ) + assert "DJANGO_ENABLE_HOSTED_SQLITE_FALLBACK=1" in result.stderr def test_hosted_sqlite_fallback_is_disabled_when_database_url_is_unset() -> None: - env = os.environ.copy() - env.update( - { - "DJANGO_DEBUG": "False", - "DJANGO_SECRET_KEY": "x" * 64, - "VERCEL_ENV": "production", - "VERCEL": "1", - } - ) - env.pop("DATABASE_URL", None) - env.pop("DJANGO_ENABLE_HOSTED_SQLITE_FALLBACK", None) - result = subprocess.run( - [ - sys.executable, - "-c", - ( - "import environ; " - "environ.Env.read_env = staticmethod(lambda *args, **kwargs: None); " - "import deep_workflow.settings" - ), - ], - env=env, - capture_output=True, - text=True, + result = run_hosted_settings_import( + database_url=None, + enable_fallback=None, ) assert result.returncode != 0 assert "ImproperlyConfigured" in result.stderr - assert "Hosted deployments require a valid PostgreSQL DATABASE_URL" in result.stderr - assert "Do not use SQLite unless DJANGO_ENABLE_HOSTED_SQLITE_FALLBACK" in ( + assert "Hosted deployments require one of these database configurations" in ( result.stderr ) + assert "DJANGO_ENABLE_HOSTED_SQLITE_FALLBACK=1" in result.stderr def test_hosted_sqlite_fallback_requires_explicit_opt_in() -> None: @@ -286,38 +258,16 @@ def test_hosted_sqlite_fallback_requires_explicit_opt_in() -> None: def test_hosted_sqlite_fallback_requires_explicit_database_url() -> None: - env = os.environ.copy() - env.update( - { - "DJANGO_DEBUG": "False", - "DJANGO_SECRET_KEY": "x" * 64, - "VERCEL_ENV": "production", - "VERCEL": "1", - "DJANGO_ENABLE_HOSTED_SQLITE_FALLBACK": "1", - } - ) - env.pop("DATABASE_URL", None) - result = subprocess.run( - [ - sys.executable, - "-c", - ( - "import environ; " - "environ.Env.read_env = staticmethod(lambda *args, **kwargs: None); " - "import deep_workflow.settings" - ), - ], - env=env, - capture_output=True, - text=True, + result = run_hosted_settings_import( + database_url=None, + enable_fallback=True, ) - assert result.returncode != 0 assert "ImproperlyConfigured" in result.stderr - assert "Hosted deployments require a valid PostgreSQL DATABASE_URL" in result.stderr - assert "Do not use SQLite unless DJANGO_ENABLE_HOSTED_SQLITE_FALLBACK" in ( + assert "Hosted deployments require one of these database configurations" in ( result.stderr ) + assert "DJANGO_ENABLE_HOSTED_SQLITE_FALLBACK=1" in result.stderr def test_hosted_sqlite_fallback_flag_ignored_for_postgresql() -> None: @@ -360,9 +310,9 @@ def test_hosted_sqlite_fallback_is_not_applied_to_invalid_database_url() -> None assert result.returncode != 0 assert "ImproperlyConfigured" in result.stderr - assert "Hosted deployments require a valid PostgreSQL DATABASE_URL" in result.stderr - assert "Do not use SQLite unless DJANGO_ENABLE_HOSTED_SQLITE_FALLBACK" in ( + assert "Hosted deployments require one of these database configurations" in ( result.stderr ) + assert "DJANGO_ENABLE_HOSTED_SQLITE_FALLBACK=1" in result.stderr assert "django.contrib.sessions.backends.signed_cookies" not in result.stderr assert "django.contrib.messages.storage.cookie.CookieStorage" not in result.stderr