From 03dc4146797e1d459da7c5749792ce33239cfec2 Mon Sep 17 00:00:00 2001 From: Edwin Gonzales Date: Fri, 24 Jul 2026 10:59:12 +0800 Subject: [PATCH 1/4] test: pin that 'me' is a CEL context identifier, not an undefined variable TDD red: the DCI resolver rewrites dotted accessors to metric('...', me) before the base resolver extracts identifiers, and 'me' is missing from CEL_CONTEXT_IDENTIFIERS, so validate_expression/validate_formula_expression report 'Undefined variables: me' for valid dotted DCI expressions. New tests assert me is recognised (base resolver + cel.service formula path + DCI dotted-accessor end-to-end). --- spp_cel_domain/tests/__init__.py | 1 + .../tests/test_cel_me_identifier.py | 46 +++++++++++++++++++ spp_dci_indicators/tests/__init__.py | 1 + .../tests/test_dci_cel_validation.py | 39 ++++++++++++++++ 4 files changed, 87 insertions(+) create mode 100644 spp_cel_domain/tests/test_cel_me_identifier.py create mode 100644 spp_dci_indicators/tests/test_dci_cel_validation.py diff --git a/spp_cel_domain/tests/__init__.py b/spp_cel_domain/tests/__init__.py index f9eb6bff3..828dfe71e 100644 --- a/spp_cel_domain/tests/__init__.py +++ b/spp_cel_domain/tests/__init__.py @@ -32,3 +32,4 @@ from . import test_cel_relational_predicate from . import test_cel_smart_op_lookup from . import test_cel_translator_cache +from . import test_cel_me_identifier diff --git a/spp_cel_domain/tests/test_cel_me_identifier.py b/spp_cel_domain/tests/test_cel_me_identifier.py new file mode 100644 index 000000000..594bd0145 --- /dev/null +++ b/spp_cel_domain/tests/test_cel_me_identifier.py @@ -0,0 +1,46 @@ +# Part of OpenSPP. See LICENSE file for full copyright and licensing details. +"""`me` is a record-root context identifier and must not be flagged as an +undefined variable during validation. + +The resolver rewrites cached variables into ``metric('', me)`` and +the DCI override rewrites dotted accessors the same way *before* the base +resolver extracts identifiers. ``me`` then appears as a bare identifier in the +scanned expression; unless it is a recognized context identifier, +``validate_expression`` / ``validate_formula_expression`` wrongly report +``Undefined variables: me``. +""" + +from odoo.tests import TransactionCase, tagged + + +@tagged("post_install", "-at_install") +class TestMeContextIdentifier(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.resolver = cls.env["spp.cel.variable.resolver"] + cls.service = cls.env["spp.cel.service"] + + def test_me_is_a_context_identifier(self): + from odoo.addons.spp_cel_domain.services.cel_parser import CEL_CONTEXT_IDENTIFIERS + + self.assertIn("me", CEL_CONTEXT_IDENTIFIERS) + + def test_expand_does_not_flag_me_as_missing(self): + result = self.resolver.expand_expression("metric('foo', me) == true") + self.assertNotIn("me", result["missing_variables"]) + + def test_validate_expression_accepts_bare_me(self): + result = self.resolver.validate_expression("metric('foo', me) == true") + self.assertTrue( + result["valid"], + f"expression with bare me should validate; errors: {result['errors']}", + ) + self.assertNotIn( + "Undefined variables: me", + " ".join(result["errors"]), + ) + + def test_validate_formula_expression_accepts_bare_me(self): + result = self.service.validate_formula_expression("metric('foo', me)", "individual") + self.assertNotIn("Missing variables: me", result.get("error") or "") diff --git a/spp_dci_indicators/tests/__init__.py b/spp_dci_indicators/tests/__init__.py index 15c858873..4f9558b07 100644 --- a/spp_dci_indicators/tests/__init__.py +++ b/spp_dci_indicators/tests/__init__.py @@ -10,3 +10,4 @@ from . import test_dci_cel_params from . import test_dci_cel_methods from . import test_dci_cel_fetcher_errors +from . import test_dci_cel_validation diff --git a/spp_dci_indicators/tests/test_dci_cel_validation.py b/spp_dci_indicators/tests/test_dci_cel_validation.py new file mode 100644 index 000000000..1faf59895 --- /dev/null +++ b/spp_dci_indicators/tests/test_dci_cel_validation.py @@ -0,0 +1,39 @@ +# Part of OpenSPP. See LICENSE file for full copyright and licensing details. +"""Validation must accept dotted DCI accessors. + +The DCI resolver rewrites a dotted cached accessor like ``r.dci.crvs.is_alive`` +into ``metric('r.dci.crvs.is_alive', me)`` before the base resolver extracts +identifiers, so ``me`` appears as a bare identifier in the scanned expression. +``validate_expression`` must not report it as an undefined variable. +""" + +from odoo.tests import TransactionCase, tagged + + +@tagged("post_install", "-at_install") +class TestDCIDottedValidation(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.resolver = cls.env["spp.cel.variable.resolver"] + # Seeded active ttl variable with a dotted accessor. + cls.var = cls.env.ref("spp_dci_indicators.var_dci_crvs_is_alive") + + def test_expand_dotted_accessor_does_not_flag_me(self): + result = self.resolver.expand_expression( + f"{self.var.cel_accessor} == true", context_type="individual" + ) + # The accessor was rewritten to a metric() call ... + self.assertIn(f"metric('{self.var.cel_accessor}', me)", result["expression"]) + # ... and me is not treated as an undefined variable. + self.assertNotIn("me", result["missing_variables"]) + + def test_validate_dotted_accessor_expression_is_valid(self): + result = self.resolver.validate_expression( + f"{self.var.cel_accessor} == true", context_type="individual" + ) + self.assertTrue( + result["valid"], + f"dotted DCI accessor should validate; errors: {result['errors']}", + ) + self.assertNotIn("Undefined variables: me", " ".join(result["errors"])) From 03674f3a96cbc7894fda71a7a51134c12bb5f742 Mon Sep 17 00:00:00 2001 From: Edwin Gonzales Date: Fri, 24 Jul 2026 11:07:16 +0800 Subject: [PATCH 2/4] fix(cel): recognise 'me' as a context identifier 'me' is the individual record proxy in the CEL eval context (bound as 'me': SafeRecordProxy(beneficiary) and treated as a record root in cel_predicate_guard), and the resolver emits metric('', me) for cached/dotted variables. Because 'me' was missing from CEL_CONTEXT_IDENTIFIERS, once the DCI override rewrites a dotted accessor into metric('...', me) before identifier extraction, validate_expression / validate_formula_expression reported valid expressions as 'Undefined variables: me' (compile_expression was unaffected as it does not reject missing variables). Add 'me' to CEL_CONTEXT_IDENTIFIERS so it is skipped as a reserved context identifier in both consumers (resolver + cel_expression). Bump spp_cel_domain to 19.0.2.1.1. --- spp_cel_domain/__manifest__.py | 2 +- spp_cel_domain/readme/HISTORY.md | 10 ++++++++++ spp_cel_domain/services/cel_parser.py | 1 + spp_dci_indicators/tests/test_dci_cel_validation.py | 8 ++------ 4 files changed, 14 insertions(+), 7 deletions(-) diff --git a/spp_cel_domain/__manifest__.py b/spp_cel_domain/__manifest__.py index 550c3d0f3..d53b3c3a2 100644 --- a/spp_cel_domain/__manifest__.py +++ b/spp_cel_domain/__manifest__.py @@ -2,7 +2,7 @@ { "name": "CEL Domain Query Builder", "summary": "Write simple CEL-like expressions to filter records (OpenSPP/OpenG2P friendly)", - "version": "19.0.2.1.0", + "version": "19.0.2.1.1", "license": "LGPL-3", "development_status": "Production/Stable", "author": "OpenSPP.org, OpenSPP Community", diff --git a/spp_cel_domain/readme/HISTORY.md b/spp_cel_domain/readme/HISTORY.md index 1b4fd10d3..c54c30ceb 100644 --- a/spp_cel_domain/readme/HISTORY.md +++ b/spp_cel_domain/readme/HISTORY.md @@ -1,3 +1,13 @@ +### 19.0.2.1.1 + +- fix: recognise `me` as a CEL context identifier. The resolver rewrites cached + variables (and the DCI override rewrites dotted accessors) into + `metric('', me)` before identifiers are extracted; because `me` was + missing from `CEL_CONTEXT_IDENTIFIERS`, `validate_expression` / + `validate_formula_expression` wrongly reported valid expressions as + `Undefined variables: me`. `me` is the individual record proxy in the eval + context, so it is now a recognised context identifier. + ### 19.0.2.1.0 - feat(sql): compile CEL ternary expressions to SQL CASE via `to_sql_case`, with `case_when`/`comparison` builders and a right-associative ternary parsing fix diff --git a/spp_cel_domain/services/cel_parser.py b/spp_cel_domain/services/cel_parser.py index 8222781bd..6d3e4b397 100644 --- a/spp_cel_domain/services/cel_parser.py +++ b/spp_cel_domain/services/cel_parser.py @@ -227,6 +227,7 @@ def __init__(self, kind: str, value: Any, pos: int): # ADR-008: Added 'r' as the standard prefix for current record access CEL_CONTEXT_IDENTIFIERS = { "m", + "me", "e", "r", "members", diff --git a/spp_dci_indicators/tests/test_dci_cel_validation.py b/spp_dci_indicators/tests/test_dci_cel_validation.py index 1faf59895..96b042bba 100644 --- a/spp_dci_indicators/tests/test_dci_cel_validation.py +++ b/spp_dci_indicators/tests/test_dci_cel_validation.py @@ -20,18 +20,14 @@ def setUpClass(cls): cls.var = cls.env.ref("spp_dci_indicators.var_dci_crvs_is_alive") def test_expand_dotted_accessor_does_not_flag_me(self): - result = self.resolver.expand_expression( - f"{self.var.cel_accessor} == true", context_type="individual" - ) + result = self.resolver.expand_expression(f"{self.var.cel_accessor} == true", context_type="individual") # The accessor was rewritten to a metric() call ... self.assertIn(f"metric('{self.var.cel_accessor}', me)", result["expression"]) # ... and me is not treated as an undefined variable. self.assertNotIn("me", result["missing_variables"]) def test_validate_dotted_accessor_expression_is_valid(self): - result = self.resolver.validate_expression( - f"{self.var.cel_accessor} == true", context_type="individual" - ) + result = self.resolver.validate_expression(f"{self.var.cel_accessor} == true", context_type="individual") self.assertTrue( result["valid"], f"dotted DCI accessor should validate; errors: {result['errors']}", From 8b83a4f160f7abe382bb40f6eff2a941cad8fbdc Mon Sep 17 00:00:00 2001 From: Edwin Gonzales Date: Fri, 24 Jul 2026 11:56:35 +0800 Subject: [PATCH 3/4] docs(spp_cel_domain): regenerate README from fragments (CI oca-gen output) --- spp_cel_domain/README.rst | 12 ++++++++++++ spp_cel_domain/static/description/index.html | 15 ++++++++++++++- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/spp_cel_domain/README.rst b/spp_cel_domain/README.rst index 9a72fa2b4..fa6e8998d 100644 --- a/spp_cel_domain/README.rst +++ b/spp_cel_domain/README.rst @@ -142,6 +142,18 @@ Dependencies Changelog ========= +19.0.2.1.1 +~~~~~~~~~~ + +- fix: recognise ``me`` as a CEL context identifier. The resolver + rewrites cached variables (and the DCI override rewrites dotted + accessors) into ``metric('', me)`` before identifiers are + extracted; because ``me`` was missing from + ``CEL_CONTEXT_IDENTIFIERS``, ``validate_expression`` / + ``validate_formula_expression`` wrongly reported valid expressions as + ``Undefined variables: me``. ``me`` is the individual record proxy in + the eval context, so it is now a recognised context identifier. + 19.0.2.1.0 ~~~~~~~~~~ diff --git a/spp_cel_domain/static/description/index.html b/spp_cel_domain/static/description/index.html index f280ce2a1..db79f7d9d 100644 --- a/spp_cel_domain/static/description/index.html +++ b/spp_cel_domain/static/description/index.html @@ -522,6 +522,19 @@

Changelog

+

19.0.2.1.1

+
    +
  • fix: recognise me as a CEL context identifier. The resolver +rewrites cached variables (and the DCI override rewrites dotted +accessors) into metric('<accessor>', me) before identifiers are +extracted; because me was missing from +CEL_CONTEXT_IDENTIFIERS, validate_expression / +validate_formula_expression wrongly reported valid expressions as +Undefined variables: me. me is the individual record proxy in +the eval context, so it is now a recognised context identifier.
  • +
+
+

19.0.2.1.0

  • feat(sql): compile CEL ternary expressions to SQL CASE via @@ -533,7 +546,7 @@

    19.0.2.1.0

  • test(translator): add coverage for the CEL translation cache helpers
-
+

19.0.2.0.0

  • Initial migration to OpenSPP2
  • From 35c281419e8b87201bf0a1d1bdd12be815779902 Mon Sep 17 00:00:00 2001 From: Edwin Gonzales Date: Fri, 24 Jul 2026 12:11:49 +0800 Subject: [PATCH 4/4] fix(cel): include 'me' in the resolver's ImportError fallback reserved set Staff-review nit: the fallback set used when the cel_parser import fails omitted 'me', so the fix would not hold on that (catastrophic) path. Mirror 'me' into the fallback for consistency. --- spp_cel_domain/models/cel_variable_resolver.py | 1 + 1 file changed, 1 insertion(+) diff --git a/spp_cel_domain/models/cel_variable_resolver.py b/spp_cel_domain/models/cel_variable_resolver.py index be60996d4..eb2672e4a 100644 --- a/spp_cel_domain/models/cel_variable_resolver.py +++ b/spp_cel_domain/models/cel_variable_resolver.py @@ -109,6 +109,7 @@ def _get_reserved_words(self): "or", "r", "m", + "me", "members", "enrollments", "entitlements",