From bd2b71f936a86d82d370b5b13398ca151895ffd3 Mon Sep 17 00:00:00 2001 From: Javier Garcia Ordonez Date: Thu, 8 Oct 2026 16:53:02 +0200 Subject: [PATCH 1/5] fix(flaskapi): enforce V4 enum range on SERVICE_MODE/PERMISSIONS/DEPLOYMENT_MODE getters; loose placeholder tests become out-of-range guards (fork #80) + sync uv.lock to 1.6.4.dev1 --- .../mmux_flaskapi/blueprints/deployment.py | 19 ++- flaskapi/tests/test_flask_deployment.py | 128 +++++++++--------- flaskapi/uv.lock | 2 +- 3 files changed, 84 insertions(+), 65 deletions(-) diff --git a/flaskapi/src/mmux_flaskapi/blueprints/deployment.py b/flaskapi/src/mmux_flaskapi/blueprints/deployment.py index 5d78b83c..b227a9c9 100644 --- a/flaskapi/src/mmux_flaskapi/blueprints/deployment.py +++ b/flaskapi/src/mmux_flaskapi/blueprints/deployment.py @@ -11,14 +11,31 @@ deployment_bp = Blueprint("deployment", __name__) +# SPEC.md §V4 (fork issue #80): these three env vars are enums, not free text. +# Presence-only checking let `SERVICE_MODE=FOO` pass with 200, so a typo'd +# service definition or a misconfigured compose env silently produced an app +# that served a mode no view implements. Out-of-range → the same JSON error +# path as "not set" (KeyError → api_endpoint surface → 500), in every caller +# of these getters (deployment endpoints AND sampling's DEPLOYMENT_MODE read). +_VALID_ENV_VALUES: dict[str, frozenset[str]] = { + "SERVICE_MODE": frozenset({"UQ", "SUMO", "MOGA"}), + "PERMISSIONS": frozenset({"READ-ONLY", "WRITE"}), + "DEPLOYMENT_MODE": frozenset({"LOCAL", "OSPARC"}), +} + + def _get_required_env_var(name: str) -> str: try: value = os.environ[name] _logger.info("%s: %s", name, value) - return value except KeyError as exc: _logger.error("%s environment variable is not set.", name) raise KeyError(f"{name} not set") from exc + allowed = _VALID_ENV_VALUES.get(name) + if allowed is not None and value not in allowed: + _logger.error("%s: %r is not one of %s", name, value, sorted(allowed)) + raise KeyError(f"{name} '{value}' is not one of {sorted(allowed)}") + return value def get_service_mode_value() -> str: diff --git a/flaskapi/tests/test_flask_deployment.py b/flaskapi/tests/test_flask_deployment.py index 10d0203c..45867f46 100644 --- a/flaskapi/tests/test_flask_deployment.py +++ b/flaskapi/tests/test_flask_deployment.py @@ -35,25 +35,34 @@ def test_health_check_method_not_allowed(self, test_client): response = test_client.post("/flask/deployment/health") assert response.status_code == 405 # Method Not Allowed - @patch.dict(os.environ, {"SERVICE_MODE": "development"}) - def test_service_mode_development(self, test_client): - """Test service mode retrieval for development environment.""" + @patch.dict(os.environ, {"SERVICE_MODE": "SUMO"}) + def test_service_mode_sumo(self, test_client): + """Test service mode retrieval for the SUMO mode (§V4 legal value).""" response = test_client.get("/flask/deployment/service-mode") assert response.status_code == 200 data = response.get_json() assert "serviceMode" in data - assert data["serviceMode"] == "development" + assert data["serviceMode"] == "SUMO" - @patch.dict(os.environ, {"SERVICE_MODE": "testing"}) - def test_service_mode_testing(self, test_client): - """Test service mode retrieval for testing environment.""" - response = test_client.get("/flask/deployment/service-mode") + @pytest.mark.parametrize("mode", ["UQ", "SUMO", "MOGA"]) + def test_service_mode_all_legal_modes_200(self, test_client, mode): + """§V4: every SERVICE_MODE ∈ {UQ,SUMO,MOGA} is served.""" + with patch.dict(os.environ, {"SERVICE_MODE": mode}): + response = test_client.get("/flask/deployment/service-mode") assert response.status_code == 200 - + assert response.get_json()["serviceMode"] == mode + + @pytest.mark.parametrize("mode", ["development", "testing", "production", "sumo", ""]) + def test_service_mode_out_of_range_errors(self, test_client, mode): + """§V4 (fork issue #80): values outside {UQ,SUMO,MOGA} ! 200 — the + pre-guard code checked presence only, so `SERVICE_MODE=development` + silently passed (these tests used to assert that loose behavior).""" + with patch.dict(os.environ, {"SERVICE_MODE": mode}): + response = test_client.get("/flask/deployment/service-mode") + assert response.status_code == 500 data = response.get_json() - assert "serviceMode" in data - assert data["serviceMode"] == "testing" + assert "SERVICE_MODE" in data["error"] @patch.dict(os.environ, {}, clear=True) def test_service_mode_missing_env_var(self, test_client): @@ -74,35 +83,34 @@ def test_service_mode_method_not_allowed(self, test_client): response = test_client.post("/flask/deployment/service-mode") assert response.status_code == 405 # Method Not Allowed - @patch.dict(os.environ, {"PERMISSIONS": "read-write"}) + @patch.dict(os.environ, {"PERMISSIONS": "WRITE"}) def test_permissions_success(self, test_client): - """Test successful permissions retrieval.""" + """Test successful permissions retrieval (§V4 legal value).""" response = test_client.get("/flask/deployment/permissions") assert response.status_code == 200 data = response.get_json() assert "permissions" in data - assert data["permissions"] == "read-write" + assert data["permissions"] == "WRITE" - @patch.dict(os.environ, {"PERMISSIONS": "read-only"}) + @patch.dict(os.environ, {"PERMISSIONS": "READ-ONLY"}) def test_permissions_read_only(self, test_client): - """Test permissions retrieval for read-only configuration.""" - response = test_client.get("/flask/deployment/permissions") - assert response.status_code == 200 - - data = response.get_json() - assert "permissions" in data - assert data["permissions"] == "read-only" - - @patch.dict(os.environ, {"PERMISSIONS": "admin"}) - def test_permissions_admin(self, test_client): - """Test permissions retrieval for admin configuration.""" + """Test permissions retrieval for the READ-ONLY configuration.""" response = test_client.get("/flask/deployment/permissions") assert response.status_code == 200 data = response.get_json() assert "permissions" in data - assert data["permissions"] == "admin" + assert data["permissions"] == "READ-ONLY" + + @pytest.mark.parametrize("permission", ["admin", "read-write", "read-only", ""]) + def test_permissions_out_of_range_errors(self, test_client, permission): + """§V4: values outside {READ-ONLY,WRITE} → error (incl. the lowercase + near-miss), not a silent 200.""" + with patch.dict(os.environ, {"PERMISSIONS": permission}): + response = test_client.get("/flask/deployment/permissions") + assert response.status_code == 500 + assert "PERMISSIONS" in response.get_json()["error"] @patch.dict(os.environ, {}, clear=True) def test_permissions_missing_env_var(self, test_client): @@ -143,15 +151,14 @@ def test_deployment_mode_osparc(self, test_client): assert "deploymentMode" in data assert data["deploymentMode"] == "OSPARC" - @patch.dict(os.environ, {"DEPLOYMENT_MODE": "DOCKER"}) - def test_deployment_mode_docker(self, test_client): - """Test deployment mode retrieval for Docker environment.""" - response = test_client.get("/flask/deployment/mode") - assert response.status_code == 200 - - data = response.get_json() - assert "deploymentMode" in data - assert data["deploymentMode"] == "DOCKER" + @pytest.mark.parametrize("mode", ["DOCKER", "local", "KUBERNETES", ""]) + def test_deployment_mode_out_of_range_errors(self, test_client, mode): + """§V4: values outside {LOCAL,OSPARC} → error. `DOCKER` used to be + asserted as a valid 200 here; it is ∈ neither deployment channel.""" + with patch.dict(os.environ, {"DEPLOYMENT_MODE": mode}): + response = test_client.get("/flask/deployment/mode") + assert response.status_code == 500 + assert "DEPLOYMENT_MODE" in response.get_json()["error"] @patch.dict(os.environ, {}, clear=True) def test_deployment_mode_missing_env_var(self, test_client): @@ -174,7 +181,7 @@ def test_deployment_mode_method_not_allowed(self, test_client): @patch.dict( os.environ, - {"SERVICE_MODE": "production", "PERMISSIONS": "read-write", "DEPLOYMENT_MODE": "OSPARC"}, + {"SERVICE_MODE": "MOGA", "PERMISSIONS": "WRITE", "DEPLOYMENT_MODE": "OSPARC"}, ) def test_all_environment_variables_set(self, test_client): """Test that all endpoints work when all environment variables are properly set.""" @@ -186,13 +193,13 @@ def test_all_environment_variables_set(self, test_client): response = test_client.get("/flask/deployment/service-mode") assert response.status_code == 200 data = response.get_json() - assert data["serviceMode"] == "production" + assert data["serviceMode"] == "MOGA" # Test permissions response = test_client.get("/flask/deployment/permissions") assert response.status_code == 200 data = response.get_json() - assert data["permissions"] == "read-write" + assert data["permissions"] == "WRITE" # Test deployment mode response = test_client.get("/flask/deployment/mode") @@ -207,33 +214,31 @@ def test_invalid_endpoint(self, test_client): @patch.dict(os.environ, {"SERVICE_MODE": ""}) def test_service_mode_empty_string(self, test_client): - """Test service mode endpoint with empty string value.""" + """§V4: empty SERVICE_MODE is out of range → error, ⊥ a 200 carrying "" + (pre-guard presence-only behavior; see fork issue #80).""" response = test_client.get("/flask/deployment/service-mode") - assert response.status_code == 200 + assert response.status_code == 500 data = response.get_json() - assert "serviceMode" in data - assert data["serviceMode"] == "" + assert "SERVICE_MODE" in data["error"] @patch.dict(os.environ, {"PERMISSIONS": ""}) def test_permissions_empty_string(self, test_client): - """Test permissions endpoint with empty string value.""" + """§V4: empty PERMISSIONS → error (pre-guard behavior was 200).""" response = test_client.get("/flask/deployment/permissions") - assert response.status_code == 200 + assert response.status_code == 500 data = response.get_json() - assert "permissions" in data - assert data["permissions"] == "" + assert "PERMISSIONS" in data["error"] @patch.dict(os.environ, {"DEPLOYMENT_MODE": ""}) def test_deployment_mode_empty_string(self, test_client): - """Test deployment mode endpoint with empty string value.""" + """§V4: empty DEPLOYMENT_MODE → error (pre-guard behavior was 200).""" response = test_client.get("/flask/deployment/mode") - assert response.status_code == 200 + assert response.status_code == 500 data = response.get_json() - assert "deploymentMode" in data - assert data["deploymentMode"] == "" + assert "DEPLOYMENT_MODE" in data["error"] def test_deployment_endpoint_url_prefix(self, test_client): """Test that all deployment endpoints use the correct URL prefix.""" @@ -252,30 +257,27 @@ def test_deployment_endpoint_url_prefix(self, test_client): @patch.dict(os.environ, {"SERVICE_MODE": "special@chars!123"}) def test_service_mode_special_characters(self, test_client): - """Test service mode endpoint with special characters.""" + """§V4: garbage SERVICE_MODE → error, ⊥ echoed back with 200.""" response = test_client.get("/flask/deployment/service-mode") - assert response.status_code == 200 + assert response.status_code == 500 data = response.get_json() - assert "serviceMode" in data - assert data["serviceMode"] == "special@chars!123" + assert "SERVICE_MODE" in data["error"] @patch.dict(os.environ, {"PERMISSIONS": "custom-permission-level"}) def test_permissions_custom_value(self, test_client): - """Test permissions endpoint with custom permission value.""" + """§V4: invented PERMISSIONS value → error, ⊥ accepted.""" response = test_client.get("/flask/deployment/permissions") - assert response.status_code == 200 + assert response.status_code == 500 data = response.get_json() - assert "permissions" in data - assert data["permissions"] == "custom-permission-level" + assert "PERMISSIONS" in data["error"] @patch.dict(os.environ, {"DEPLOYMENT_MODE": "CUSTOM_DEPLOYMENT"}) def test_deployment_mode_custom_value(self, test_client): - """Test deployment mode endpoint with custom deployment value.""" + """§V4: invented DEPLOYMENT_MODE value → error, ⊥ accepted.""" response = test_client.get("/flask/deployment/mode") - assert response.status_code == 200 + assert response.status_code == 500 data = response.get_json() - assert "deploymentMode" in data - assert data["deploymentMode"] == "CUSTOM_DEPLOYMENT" + assert "DEPLOYMENT_MODE" in data["error"] diff --git a/flaskapi/uv.lock b/flaskapi/uv.lock index 80f477c9..fe9fd0ed 100644 --- a/flaskapi/uv.lock +++ b/flaskapi/uv.lock @@ -464,7 +464,7 @@ wheels = [ [[package]] name = "mmux-flaskapi" -version = "1.6.4" +version = "1.6.4.dev1" source = { editable = "." } dependencies = [ { name = "flask" }, From f221a47996ed7257f27eab3532d9fcf9fd8a9e79 Mon Sep 17 00:00:00 2001 From: Javier Garcia Ordonez Date: Thu, 8 Oct 2026 16:53:08 +0200 Subject: [PATCH 2/5] test(flaskapi): mark test_api_error_surface as unit tier (V32qt, fork #82) --- flaskapi/tests/test_api_error_surface.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/flaskapi/tests/test_api_error_surface.py b/flaskapi/tests/test_api_error_surface.py index 9a300cc2..e12afe7a 100644 --- a/flaskapi/tests/test_api_error_surface.py +++ b/flaskapi/tests/test_api_error_surface.py @@ -8,6 +8,11 @@ from typing import Any +import pytest + +# §V32qt (fork issue #82): every flaskapi test case carries ≥1 tier marker. +pytestmark = pytest.mark.unit + def assert_error_json(response: Any, status: int) -> dict[str, Any]: assert response.status_code == status, response.get_data(as_text=True)[:200] From 151160d22986993e54283260170a0e9c092ce465 Mon Sep 17 00:00:00 2001 From: Javier Garcia Ordonez Date: Thu, 8 Oct 2026 16:53:08 +0200 Subject: [PATCH 3/5] test(flaskapi): per-test tmp persistence dirs + B15 regression (T26, V26) - suite green against dirty repo runs_local --- SPEC.md | 2 +- flaskapi/tests/conftest.py | 30 +++++++++++ flaskapi/tests/test_test_isolation.py | 72 +++++++++++++++++++++++++++ 3 files changed, 103 insertions(+), 1 deletion(-) create mode 100644 flaskapi/tests/test_test_isolation.py diff --git a/SPEC.md b/SPEC.md index 79a93ebe..b8358bf6 100644 --- a/SPEC.md +++ b/SPEC.md @@ -131,7 +131,7 @@ T19|x|RELEASE-2 frontend CSV upload (new focused PR after T18): CSV upload UI→ T20|x|RELEASE-2 collaborator preview v1.6.0: prereq T18+T19+CI green on develop; optionally include #469 if node §B8-§B9 fixed → PR merge develop→main → `make version-minor` → verify V16 → oSPARC publish; goal: CSV upload+log-scale live for collaborator testing. SHIPPED: v1.6.0 tagged+released 2026-07-06 (`6c69488d`, stable #486); log-scale in (`58146f41`); repo since advanced to 1.6.4|V5,V9,V16,node/SPEC.md B8,B9 T21|x|REMOVE vendored `mmux_python` dep: inlined 6 used modules (`lhs`,`dakota_object`,`funs_create_dakota_conf`,`funs_data_processing`,`funs_evaluate`,`wiofiles`; dropped 3 unused: `dakota_object_map`,`funs_git`,`funs_plotting`) into `flaskapi/src/mmux_flaskapi/dakota/`; rewired blueprint imports + test mocks; removed `mmux-python` workspace dep + 6 dead transitive deps (gitpython,httpx,ipykernel,matplotlib,seaborn,tqdm) + `[tool.uv.workspace]`/`[tool.uv.sources]`/coverage-omit from `flaskapi/pyproject.toml`; stripped `setup-mmux-python`/`get-access-write-on-mmux-python` Makefile targets + `MMUX_PYTHON_TAG` pin + gitignore entries; `rm -rf flaskapi/mmux_python/`; eliminates the T1 drift class entirely (no more separate versioned dep to go stale)|V5,flaskapi/SPEC.md T15 T25|.|Document or script WSL2 Windows portproxy setup for local fallback app ports 8889-8892: discover WSL IP, delete stale rules, add rules, show rules, mention admin shell + firewall caveat|V20,V21 -T26|.|backend test isolation: autouse pytest fixture allocates per-run `tmp_path` dirs for `LOCAL_STORE_DIR` + `TEXT_FILES_DIR` before `create_flask_app()`; reset persistence between tests; add regression run from dirty `runs_local` clone|V26,B25,flaskapi/SPEC.md V17 +T26|x|backend test isolation: autouse pytest fixture allocates per-run `tmp_path` dirs for `LOCAL_STORE_DIR` + `TEXT_FILES_DIR` before `create_flask_app()`; reset persistence between tests; add regression run from dirty `runs_local` clone. DONE: `conftest.py::isolate_persistence_dirs` (autouse, patches `ljs.LOCAL_STORE_{DIR,FILE}` + `textfile.FILES_STORAGE_DIR` globals + env; fresh dir/test = reset by construction) + `tests/test_test_isolation.py` (B15 27-collection repro, repo store byte-stability, real text-file write → tmp). ORACLE: full suite **600 passed with the 13:54 dirty `runs_local` store present** (same file that failed 9 tests this morning, no env workaround)|V26,B25,flaskapi/SPEC.md V17 T27|x|Node deprecation cleanup: upgrade ESLint/toolchain off deprecated ESLint 8 dependency chain (`inflight`, `@humanwhocodes/*` [i.e. eslint-8's `config-array`/`object-schema`], `rimraf@3`, old nested `glob`); clean `npm ci` emits no listed deprecation warnings. DONE: eslint v9.39.5, lock grep `inflight`=0/`rimraf`=0/config-array absent; `npm ci --dry-run` → 0 deprecation warnings (fresh `rm -rf node_modules && npm ci` clean 2026-10-08). NB: `@humanwhocodes/{module-importer,retry}` remain — current ESLint-9 deps, ⊥ deprecated chain|— T28|.|incrementally fix the 30 `react-hooks/set-state-in-effect` warnings surfaced by eslint-plugin-react-hooks v7 (rule downgraded error→warn in T27 to unblock the ESLint v9 upgrade w/o a risky bulk behavioral refactor); mostly persistence/context-hydration + job-status polling patterns, needs case-by-case triage (derive-during-render vs justified external-sync exception)|node/eslint.config.js T29|x|Correlation and Sobol sensitivity-analysis backend endpoints, SciPy computation, response-key preservation, and numerical regression tests|flaskapi/SPEC.md T25 diff --git a/flaskapi/tests/conftest.py b/flaskapi/tests/conftest.py index 59c2706f..a93f1402 100644 --- a/flaskapi/tests/conftest.py +++ b/flaskapi/tests/conftest.py @@ -62,6 +62,36 @@ def mock_test_env_vars(): yield +@pytest.fixture(autouse=True) +def isolate_persistence_dirs(tmp_path, monkeypatch): + """Root SPEC.md §V26 / §T26 (B15): every test gets per-run temp dirs for the + local job store and the text-file storage, before any app or persistence + code can read them. + + B15 bit again on 2026-10-08: a `make run-*` dev stack left 3 collections in + the repository `flaskapi/runs_local/` and 9 list-endpoint tests failed + (`assert 3 == 0`) until the store was manually isolated. The blueprints + read these module globals at request time, so patching the attributes is + what the request path actually sees; the env vars are patched too for any + code that re-reads them (e.g. a re-import). Fresh dir per test ⇒ persistence + is reset between tests by construction. + """ + from mmux_flaskapi.blueprints import textfile + from mmux_flaskapi.utils import local_job_store as ljs + + store_dir = tmp_path / "runs_local" + files_dir = tmp_path / "text_files" + files_dir.mkdir() + monkeypatch.setattr(ljs, "LOCAL_STORE_DIR", store_dir) + monkeypatch.setattr(ljs, "LOCAL_STORE_FILE", store_dir / "uploaded_job_collections_store.json") + monkeypatch.setattr(textfile, "FILES_STORAGE_DIR", files_dir) + with patch.dict( + "os.environ", + {"LOCAL_STORE_DIR": str(store_dir), "TEXT_FILES_DIR": str(files_dir)}, + ): + yield + + @pytest.fixture(autouse=True) def default_osparc_reachable(): """Default the oSPARC connectivity probe (`OsparcApi.is_connected()`) to "reachable" diff --git a/flaskapi/tests/test_test_isolation.py b/flaskapi/tests/test_test_isolation.py new file mode 100644 index 00000000..4b44d9d3 --- /dev/null +++ b/flaskapi/tests/test_test_isolation.py @@ -0,0 +1,72 @@ +"""§V26 / §T26 (root SPEC.md B15): backend tests never read or write repository +persistence state. + +B15 recurred on 2026-10-08: a `make run-*` dev stack left collections in the +repository `flaskapi/runs_local/` store and 9 list-endpoint tests failed with +`assert 3 == 0` until the store was manually redirected. The autouse +`isolate_persistence_dirs` fixture in conftest.py is the invariant; these +tests pin it (per-run temp dirs, dirty-state scenario from B15, repo file +byte-stable across a full persistence round-trip). +""" + +from pathlib import Path + +import pytest + +from mmux_flaskapi.blueprints import textfile +from mmux_flaskapi.utils import local_job_store as ljs + +pytestmark = pytest.mark.unit + +# The default (unpatched) store location: _default_store_dir anchors to +# src/mmux_flaskapi/utils/ -> parents[3] == flaskapi/ (flaskapi §B1/V17). +REPO_STORE_FILE = ( + Path(ljs.__file__).resolve().parents[3] / "runs_local" / ("uploaded_job_collections_store.json") +) + + +def test_v26_store_and_text_dirs_point_at_tmp(tmp_path): + """The active persistence roots are the per-test tmp dirs, ⊥ repo defaults.""" + assert ljs.LOCAL_STORE_DIR == tmp_path / "runs_local" + assert ljs.LOCAL_STORE_FILE.parent == tmp_path / "runs_local" + assert textfile.FILES_STORAGE_DIR == tmp_path / "text_files" + assert tmp_path in ljs.LOCAL_STORE_DIR.parents + + +def test_v26_b15_dirty_store_never_leaks_into_repo(test_client, patch_list_functions_success): + """B15 repro: seed 27 collections (the 'expected 3 -> actual 27' scenario). + + The endpoint must see the seeded isolated store (isolation still exercises + the real merge path, not a mocked-out store) while the repository store + file stays byte-identical. + """ + repo_before = REPO_STORE_FILE.read_bytes() if REPO_STORE_FILE.exists() else None + + fun = ljs.create_local_function(title="B15 repro fn", input_vars=["x"], output_vars=["y"]) + for i in range(27): + ljs.create_local_job_collection( + function_uid=fun["uid"], + title=f"B15 JC {i}", + rows=[{"inputs": {"x": 1.0}, "outputs": {"y": 2.0}}], + ) + + assert len(ljs.list_local_job_collections()) == 27 + + response = test_client.get("/flask/osparc/list_functions") + assert response.status_code == 200 + data = response.get_json() + assert any(f["uid"] == fun["uid"] for f in data), "endpoint must read the isolated store" + + repo_after = REPO_STORE_FILE.read_bytes() if REPO_STORE_FILE.exists() else None + assert repo_after == repo_before, "repository runs_local store must be untouched by tests" + # And the write landed in the tmp store: + assert ljs.LOCAL_STORE_FILE.exists() + + +def test_v26_text_file_write_lands_in_tmp_dir(test_client, tmp_path): + """A real (unmocked) text-file POST writes into the tmp dir; the container + default `/text-files` or repo paths are never touched.""" + response = test_client.post("/flask/text-file/", json={"filename": "v26.txt", "content": "hi"}) + assert response.status_code == 200 + assert (tmp_path / "text_files" / "v26.txt").read_text(encoding="utf-8") == "hi" + assert not Path("/text-files/v26.txt").exists() From ed1328f9746a58ed045095c464e58e29578be295 Mon Sep 17 00:00:00 2001 From: Javier Garcia Ordonez Date: Thu, 8 Oct 2026 18:21:35 +0200 Subject: [PATCH 4/5] chore(flaskapi): sync uv.lock to 1.6.4.dev3 (auto-tag bot stamps pyproject only, V34lt) --- flaskapi/uv.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flaskapi/uv.lock b/flaskapi/uv.lock index fe9fd0ed..e412254a 100644 --- a/flaskapi/uv.lock +++ b/flaskapi/uv.lock @@ -464,7 +464,7 @@ wheels = [ [[package]] name = "mmux-flaskapi" -version = "1.6.4.dev1" +version = "1.6.4.dev3" source = { editable = "." } dependencies = [ { name = "flask" }, From 66578fa555d9c967891c10f53a716f508e0017cd Mon Sep 17 00:00:00 2001 From: Javier Garcia Ordonez Date: Thu, 8 Oct 2026 20:19:01 +0200 Subject: [PATCH 5/5] =?UTF-8?q?test(flaskapi):=20drop=20non-hermetic=20/te?= =?UTF-8?q?xt-files=20assert=20from=20=C2=A7V26=20guard=20(GH-Copilot=20#7?= =?UTF-8?q?15,=20B27rt)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The isolation guard itself read SHARED container state (assert /text-files/v26.txt never exists) — exactly the pre-existing external state V26/B15 isolation must tolerate: a dev stack that had landed that file would fail a green suite. The successful read from the patched tmp dir is the hermetic proof the request used the isolated destination; reading the unpatched path adds external state, not coverage. Root SPEC §B27rt ledgers it. Suite: 600 passed, 3 skipped. --- SPEC.md | 1 + flaskapi/tests/test_test_isolation.py | 10 +++++++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/SPEC.md b/SPEC.md index b8358bf6..e48b7306 100644 --- a/SPEC.md +++ b/SPEC.md @@ -175,3 +175,4 @@ B23gu|2026-09-23|backend prod entrypoint used `uvx gunicorn`, bypassing declared B26qf|2026-09-23|PR #622 CI `npm ci` rejected the committed coverage dependency addition as out of sync, so both `node-tests` and `prek` failed before their actual checks; a clean install against the pushed lockfile passed locally|V44qf B24kp|2026-09-29|PR #648 Copilot review: V31vr's `user: "${UID:-1000}:${GID:-1000}"` never actually pinned the host identity — `UID`/`GID` are unexported bash-internal variables (and make runs recipes via `/bin/sh`, which doesn't set them either), so every `docker compose` launch fell back to `1000:1000` on non-1000 hosts while the substring regression test kept passing on the placeholder text; launchers must pass explicit `HOST_UID/HOST_GID` and tests must enforce both sides of the interpolation|V31vr B25|2026-10-01|#664 shipped the bounds-editor header, yet the committed Sobol baseline LACKED it and the e2e verify passed twice locally + CI: header band + ~39px plot shift fell just under `maxDiffPixelRatio` 0.01 on the 1920×1080 viewport — a band meant to absorb font/AA drift silently absorbed a REAL UI change (surfaced only when #665 regenerated baselines in the pinned image)|strict 0.0 tolerance (root V10 re-worded): ⊥ band can re-absorb a semantic diff; the pinned image (V12) owns determinism, so the contract is ZERO comparator-COUNTED differing pixels (per V10: 0.2 color floor + AA classification deliberately ⊥ byte-identity) — if the image changes, baselines are regenerated IN it|V10,V12,https://github.com/ITISFoundation/mmux_vite/pull/665 +B27rt|2026-10-08|GH-Copilot #715 review: the §V26 isolation guard's text-file test ALSO asserted `/text-files/v26.txt` never exists — a READ of SHARED container state, i.e. the very class of pre-existing external state V26/B15 isolation exists to tolerate; a dev stack that had landed that file would fail an otherwise-green suite (hermeticity broken in the GUARD itself)|dropped the shared-path existence assert: the successful read from the patched tmp dir is the hermetic proof the request used the isolated destination (⊥ reading the unpatched path adds external state, not coverage); docstring names the rule|V26,T26,B15,https://github.com/ITISFoundation/mmux_vite/pull/715 diff --git a/flaskapi/tests/test_test_isolation.py b/flaskapi/tests/test_test_isolation.py index 4b44d9d3..12d1dfa9 100644 --- a/flaskapi/tests/test_test_isolation.py +++ b/flaskapi/tests/test_test_isolation.py @@ -64,9 +64,13 @@ def test_v26_b15_dirty_store_never_leaks_into_repo(test_client, patch_list_funct def test_v26_text_file_write_lands_in_tmp_dir(test_client, tmp_path): - """A real (unmocked) text-file POST writes into the tmp dir; the container - default `/text-files` or repo paths are never touched.""" + """A real (unmocked) text-file POST writes into the tmp dir. + + Landing in the patched per-test dir is the hermetic proof that the request + used the isolated destination. The guard deliberately ⊥ also assert that the + SHARED `/text-files` default stayed clean: reading pre-existing external + state is exactly what §V26 isolation exists to tolerate (GH-Copilot #715). + """ response = test_client.post("/flask/text-file/", json={"filename": "v26.txt", "content": "hi"}) assert response.status_code == 200 assert (tmp_path / "text_files" / "v26.txt").read_text(encoding="utf-8") == "hi" - assert not Path("/text-files/v26.txt").exists()