Skip to content

Commit 2c05b9c

Browse files
committed
Address PR feedback: rollback partial env, fail on bad templates
- TmpEnv.__enter__ rolls back already-set env vars if swap_env raises mid-loop, preventing leaked env state on enter failures - Remove silent TemplateError swallow: malformed templates now raise LookupError with the source string and error detail instead of silently passing through as literals - Convert tests to plain pytest classes (drop unittest.TestCase) - Add test_tmpenv_rollback_on_error for partial modification safety
1 parent 5fd61b2 commit 2c05b9c

2 files changed

Lines changed: 25 additions & 14 deletions

File tree

src/seclab_taskflow_agent/env_utils.py

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,7 @@ def swap_env(s: str, context: dict[str, Any] | None = None) -> str:
3131
LookupError: If a required environment variable or template
3232
variable is not found during rendering.
3333
"""
34-
# Quick check if templating needed
35-
if '{{' not in s:
36-
return s
37-
3834
try:
39-
# Import here to avoid circular dependency
4035
from .template_utils import create_jinja_environment
4136
from .available_tools import AvailableTools
4237

@@ -52,11 +47,9 @@ def swap_env(s: str, context: dict[str, Any] | None = None) -> str:
5247
}
5348
return template.render(**render_context)
5449
except jinja2.UndefinedError as e:
55-
# Convert Jinja undefined to LookupError for compatibility
5650
raise LookupError(str(e))
57-
except jinja2.TemplateError:
58-
# Not a template or failed to render, return as-is
59-
return s
51+
except jinja2.TemplateError as e:
52+
raise LookupError(f"Template rendering failed for: {s!r}: {e}")
6053

6154

6255
class TmpEnv:
@@ -69,8 +62,18 @@ def __init__(self, env: dict[str, str],
6962
self.restore_env = dict(os.environ)
7063

7164
def __enter__(self) -> None:
72-
for k, v in self.env.items():
73-
os.environ[k] = swap_env(v, self.context)
65+
applied: list[str] = []
66+
try:
67+
for k, v in self.env.items():
68+
os.environ[k] = swap_env(v, self.context)
69+
applied.append(k)
70+
except Exception:
71+
for k in applied:
72+
if k in self.restore_env:
73+
os.environ[k] = self.restore_env[k]
74+
else:
75+
os.environ.pop(k, None)
76+
raise
7477

7578
def __exit__(self, exc_type: type | None, exc_val: BaseException | None, exc_tb: Any | None) -> None:
7679
for k, v in self.env.items():

tests/test_env_utils.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,13 @@
44
"""Tests for env_utils: swap_env and TmpEnv with globals context."""
55

66
import os
7-
import unittest
87

98
import pytest
109

1110
from seclab_taskflow_agent.env_utils import TmpEnv, swap_env
1211

1312

14-
class TestSwapEnv(unittest.TestCase):
13+
class TestSwapEnv:
1514
"""Tests for swap_env template rendering."""
1615

1716
def test_plain_string_unchanged(self):
@@ -51,7 +50,7 @@ def test_no_context_backward_compat(self):
5150
assert swap_env("plain") == "plain"
5251

5352

54-
class TestTmpEnv(unittest.TestCase):
53+
class TestTmpEnv:
5554
"""Tests for TmpEnv context manager with globals."""
5655

5756
def test_globals_rendered_in_env_block(self):
@@ -77,3 +76,12 @@ def test_tmpenv_restores_original(self):
7776
assert os.environ["RESTORE_TEST"] == "overwritten"
7877
assert os.environ["RESTORE_TEST"] == "original"
7978
del os.environ["RESTORE_TEST"]
79+
80+
def test_tmpenv_rollback_on_error(self):
81+
"""Partial env modification is rolled back if swap_env raises."""
82+
env = {"GOOD_KEY": "value", "BAD_KEY": "{{ globals.missing }}"}
83+
with pytest.raises(LookupError):
84+
with TmpEnv(env):
85+
pass
86+
assert "GOOD_KEY" not in os.environ
87+
assert "BAD_KEY" not in os.environ

0 commit comments

Comments
 (0)