From a4d2e770c6fbe6ddff0bce69ef3ea99bdbbceba7 Mon Sep 17 00:00:00 2001 From: Rudy Mizrahi Celekli <47457359+rudycelekli@users.noreply.github.com> Date: Thu, 8 Oct 2026 18:31:26 -0400 Subject: [PATCH 1/3] test(workbook): qualify stable column ID precedence Add eight public HTTP and SQLite fixture cases without production changes. Signed-off-by: Rudy Celekli --- tests/test_workbook_views.py | 76 ++++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/tests/test_workbook_views.py b/tests/test_workbook_views.py index 8374a766..2b635be6 100644 --- a/tests/test_workbook_views.py +++ b/tests/test_workbook_views.py @@ -2739,3 +2739,79 @@ def test_saved_view_empty_comparison_still_matches_missing_values(client, compar rows = tc.get(f"/api/workbooks/{wid}", params={"view_id": created.json()["id"]}) assert rows.status_code == 200, rows.text assert [row["row_id"] for row in rows.json()["rows"]] == [blank] + +@pytest.mark.parametrize("computed_first", [True, False]) +@pytest.mark.parametrize("collision", [True, False]) +def test_failed_display_alias_preserves_public_input_id(client, computed_first, collision): + tc, Session, _ = client + wid = _mk_workbook(Session, []) + source = {"id": "b", "name": "Input B", "type": "input"} + computed = {"id": "a", "name": "b" if collision else "Computed A", + "type": "formula", "formula": "'OTHER'"} + columns = [computed, source] if computed_first else [source, computed] + columns.append({"id": "result", "name": "Result", "type": "formula", "formula": "{b}"}) + for column in columns: + added = tc.post(f"/api/workbooks/{wid}/columns", json={"column": column}) + assert added.status_code == 200, added.text + original = {"b": "500", "a": "STALE"} + failed = {"status": "error", "value": None, "error": "fixture_failure"} + row_id = _mk_row(Session, wid, original, {"a": failed}) + response = tc.post(f"/api/workbooks/{wid}/rows/{row_id}/cells/result/run", + json={"force": True}) + assert response.status_code == 200, response.text + assert response.json()["status"] == "complete", response.json() + assert response.json()["value"] == "500", response.json() + with Session() as db: + row = db.get(WorkbookRow, row_id) + assert row.data == original + assert row.enrichments["a"] == failed + assert row.enrichments["result"]["value"] == "500" + if not collision: + alias_column = {"id": "alias_result", "name": "Alias result", + "type": "formula", "formula": "{Input B}"} + added = tc.post(f"/api/workbooks/{wid}/columns", json={"column": alias_column}) + assert added.status_code == 200, added.text + alias = tc.post(f"/api/workbooks/{wid}/rows/{row_id}/cells/alias_result/run", + json={"force": True}) + assert alias.status_code == 200, alias.text + assert alias.json()["status"] == "complete", alias.json() + assert alias.json()["value"] == "500", alias.json() + + +@pytest.mark.parametrize("computed_first", [True, False]) +@pytest.mark.parametrize("collision", [True, False]) +def test_completed_display_alias_cannot_replace_public_condition_id(client, computed_first, collision): + tc, Session, _ = client + wid = _mk_workbook(Session, []) + source = {"id": "b", "name": "Input B", "type": "input"} + computed = {"id": "a", "name": "b" if collision else "Computed A", + "type": "formula", "formula": "'OTHER'"} + columns = [computed, source] if computed_first else [source, computed] + columns.append({"id": "result", "name": "Result", "type": "formula", + "formula": "{b}", "condition": '{b} == "500"'}) + for column in columns: + added = tc.post(f"/api/workbooks/{wid}/columns", json={"column": column}) + assert added.status_code == 200, added.text + original = {"b": "500", "a": "STALE"} + completed = {"status": "complete", "value": "OTHER"} + row_id = _mk_row(Session, wid, original, {"a": completed}) + response = tc.post(f"/api/workbooks/{wid}/rows/{row_id}/cells/result/run", + json={"force": True}) + assert response.status_code == 200, response.text + assert response.json()["status"] == "complete", response.json() + assert response.json()["value"] == "500", response.json() + with Session() as db: + row = db.get(WorkbookRow, row_id) + assert row.data == original + assert row.enrichments["a"] == completed + assert row.enrichments["result"]["value"] == "500" + if not collision: + alias_column = {"id": "alias_result", "name": "Alias result", + "type": "formula", "formula": "{Computed A}"} + added = tc.post(f"/api/workbooks/{wid}/columns", json={"column": alias_column}) + assert added.status_code == 200, added.text + alias = tc.post(f"/api/workbooks/{wid}/rows/{row_id}/cells/alias_result/run", + json={"force": True}) + assert alias.status_code == 200, alias.text + assert alias.json()["status"] == "complete", alias.json() + assert alias.json()["value"] == "OTHER", alias.json() From dfe1d35fea566066be74f6002465190ab4a28e67 Mon Sep 17 00:00:00 2001 From: Rudy Mizrahi Celekli <47457359+rudycelekli@users.noreply.github.com> Date: Thu, 8 Oct 2026 18:38:51 -0400 Subject: [PATCH 2/3] fix(workbook): preserve stable IDs across display aliases Hydrate stored inputs before failed computed snapshots invalidate materialized values. Keep condition IDs authoritative over display aliases. Signed-off-by: Rudy Celekli --- apps/api/services/workbook/cell_scope.py | 5 +++++ apps/api/services/workbook/conditions.py | 6 ++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/apps/api/services/workbook/cell_scope.py b/apps/api/services/workbook/cell_scope.py index 70cada89..ee463622 100644 --- a/apps/api/services/workbook/cell_scope.py +++ b/apps/api/services/workbook/cell_scope.py @@ -40,6 +40,8 @@ def row_execution_data(row, columns: list[dict] | None = None) -> dict: """Hydrate saved dependencies, then assert database execution identities.""" data = dict(row.data or {}) values, aliases = {}, {} + # Read every input before failed computed cells invalidate materialized data. + # A display alias may be another input's ID or mapped source field. for column in columns or []: cid = column.get("id") if column.get("type") in ("lead_field", "input"): @@ -48,7 +50,10 @@ def row_execution_data(row, columns: list[dict] | None = None) -> dict: values[cid] = data[field] if column.get("name"): aliases[column["name"]] = data[field] + for column in columns or []: + if column.get("type") in ("lead_field", "input"): continue + cid = column.get("id") cell = (row.enrichments or {}).get(cid) if isinstance(cell, dict) and (cell.get("status") != "complete" or cell.get("value") is None): # Imported/previously materialized row data can also contain an old diff --git a/apps/api/services/workbook/conditions.py b/apps/api/services/workbook/conditions.py index a0855330..ab7e291c 100644 --- a/apps/api/services/workbook/conditions.py +++ b/apps/api/services/workbook/conditions.py @@ -108,14 +108,16 @@ def _get_values(row_cells: dict, columns_config: list) -> Dict[str, str]: """Extract flat {col_id: value, col_name: value} dict.""" col_name_map = {c["id"]: c.get("name", c["id"]) for c in columns_config} values = {} + aliases = {} for col_id, cell in row_cells.items(): val = cell.get("value", "") if isinstance(cell, dict) else cell val_str = str(val) if val is not None else "" values[col_id] = val_str - values[col_name_map.get(col_id, col_id)] = val_str + aliases[col_name_map.get(col_id, col_id)] = val_str - return values + # IDs remain authoritative when a display name collides with another ID. + return {**aliases, **values} def _resolve_placeholders(condition: str, values: Dict[str, str]) -> str: From 46448825567569c624a660141efcf660403b1c62 Mon Sep 17 00:00:00 2001 From: Rudy Mizrahi Celekli <47457359+rudycelekli@users.noreply.github.com> Date: Thu, 8 Oct 2026 18:56:38 -0400 Subject: [PATCH 3/3] fix(workbook): retain alias order while preserving stable inputs Read input values from an immutable raw-data snapshot while preserving the original single-loop alias assignment order. Retain separate condition aliases so stable IDs take precedence. Freeze two intentional public ambiguous-name rejection controls after their native baseline pass; original eight stable-ID assertions remain unchanged. Supersedes the held two-pass hydration change without rewriting its evidence. Signed-off-by: Rudy Celekli --- apps/api/services/workbook/cell_scope.py | 14 +++++------ tests/test_workbook_views.py | 30 ++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 8 deletions(-) diff --git a/apps/api/services/workbook/cell_scope.py b/apps/api/services/workbook/cell_scope.py index ee463622..d2e93496 100644 --- a/apps/api/services/workbook/cell_scope.py +++ b/apps/api/services/workbook/cell_scope.py @@ -39,21 +39,19 @@ def allows_automatic_retry(column: dict) -> bool: def row_execution_data(row, columns: list[dict] | None = None) -> dict: """Hydrate saved dependencies, then assert database execution identities.""" data = dict(row.data or {}) + # Failed computed snapshots invalidate materialized values in data, while + # inputs read their original stored fields regardless of configuration order. + input_data = dict(data) values, aliases = {}, {} - # Read every input before failed computed cells invalidate materialized data. - # A display alias may be another input's ID or mapped source field. for column in columns or []: cid = column.get("id") if column.get("type") in ("lead_field", "input"): field = column.get("lead_field") or cid - if field in data: - values[cid] = data[field] + if field in input_data: + values[cid] = input_data[field] if column.get("name"): - aliases[column["name"]] = data[field] - for column in columns or []: - if column.get("type") in ("lead_field", "input"): + aliases[column["name"]] = input_data[field] continue - cid = column.get("id") cell = (row.enrichments or {}).get(cid) if isinstance(cell, dict) and (cell.get("status") != "complete" or cell.get("value") is None): # Imported/previously materialized row data can also contain an old diff --git a/tests/test_workbook_views.py b/tests/test_workbook_views.py index 2b635be6..27154dc5 100644 --- a/tests/test_workbook_views.py +++ b/tests/test_workbook_views.py @@ -2815,3 +2815,33 @@ def test_completed_display_alias_cannot_replace_public_condition_id(client, comp assert alias.status_code == 200, alias.text assert alias.json()["status"] == "complete", alias.json() assert alias.json()["value"] == "OTHER", alias.json() + +@pytest.mark.parametrize("computed_first", [True, False]) +def test_duplicate_display_alias_rejects_ambiguous_public_reference(client, computed_first): + tc, Session, _ = client + wid = _mk_workbook(Session, []) + source = {"id": "b", "name": "Shared", "type": "input"} + computed = {"id": "a", "name": "Shared", "type": "formula", + "formula": "'OTHER'"} + columns = [computed, source] if computed_first else [source, computed] + columns.append({"id": "result", "name": "Result", "type": "formula", + "formula": "{Shared}"}) + for column in columns: + added = tc.post(f"/api/workbooks/{wid}/columns", json={"column": column}) + assert added.status_code == 200, added.text + original = {"b": "500", "a": "STALE"} + completed = {"status": "complete", "value": "OTHER"} + row_id = _mk_row(Session, wid, original, {"a": completed}) + response = tc.post(f"/api/workbooks/{wid}/rows/{row_id}/cells/result/run", + json={"force": True}) + assert response.status_code == 200, response.text + assert response.json()["status"] == "error", response.json() + assert response.json()["error"] == "ambiguous_column_reference", response.json() + assert response.json()["value"] is None, response.json() + with Session() as db: + row = db.get(WorkbookRow, row_id) + assert row.data == original + assert row.enrichments["a"] == completed + assert row.enrichments["result"]["status"] == "error" + assert row.enrichments["result"]["error"] == "ambiguous_column_reference" + assert row.enrichments["result"]["value"] is None