From c27288ee47187d17c45ac0ad2983fa68652895fa Mon Sep 17 00:00:00 2001 From: Joongheon Park Date: Wed, 27 May 2026 11:05:06 +0900 Subject: [PATCH 1/2] fix(scaffold): make generated projects pass tests out of the box MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Six bugs in the cookiecutter scaffold caused `uv run pytest` to fail immediately after `act new` + `act cast`. After this change, freshly scaffolded projects produce 4/4 passing tests with no manual edits. Bug fixes: 1. tests/node_tests/__init.py → __init__.py Filename typo prevented Python from treating the directory as a package under strict module discovery. 2. pyproject.toml: add [tool.pytest.ini_options] Without `pythonpath`/`testpaths`, pytest climbed past fa/ to a parent pyproject and failed with `ModuleNotFoundError: No module named 'casts'` for all three test files. 3. SampleNode wrote only `messages`, but OutputState declared only `result: str` → graph.invoke() returned None. Now writes both `result` (exposed) and `messages` (accumulated via add_messages), demonstrating multi-key state updates. 4. Test assertions did not match node output: - test_node.py asserted {"message": str} (singular) vs actual {"messages": [AIMessage]}. - {cast}_test.py asserted result["messages"] == str against a None result (#3 cascade) → TypeError. Both updated to match the new node contract. 5. asyncio test support: add `pytest-asyncio` + asyncio_mode="auto" so async def test_* functions are collected without per-test decorators. 6. Docstring placeholders hardcoded "Sam"/"Sample Cast"/"sam graphs" in nodes.py / state.py / middlewares.py / agents.py / tests/node_tests/test_node.py. Replaced with `{{ cookiecutter.cast_name }}` so each cast renders correctly. Verified by regenerating with `python -m act_operator new` + `act_operator cast` and running `uv run pytest -v`: 4 passed in 0.66s. `uv run ruff check .` also passes. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../modules/agents.py | 2 +- .../modules/middlewares.py | 2 +- .../modules/nodes.py | 20 +++++++++++++++---- .../modules/state.py | 2 +- .../pyproject.toml | 10 ++++++++-- .../{{ cookiecutter.cast_snake }}_test.py | 8 ++++---- .../node_tests/{__init.py => __init__.py} | 0 .../tests/node_tests/test_node.py | 12 ++++++++--- 8 files changed, 40 insertions(+), 16 deletions(-) rename act_operator/act_operator/scaffold/{{ cookiecutter.act_slug }}/tests/node_tests/{__init.py => __init__.py} (100%) diff --git a/act_operator/act_operator/scaffold/{{ cookiecutter.act_slug }}/casts/{{ cookiecutter.cast_snake }}/modules/agents.py b/act_operator/act_operator/scaffold/{{ cookiecutter.act_slug }}/casts/{{ cookiecutter.cast_snake }}/modules/agents.py index 5b5d814..acd06c3 100644 --- a/act_operator/act_operator/scaffold/{{ cookiecutter.act_slug }}/casts/{{ cookiecutter.cast_snake }}/modules/agents.py +++ b/act_operator/act_operator/scaffold/{{ cookiecutter.act_slug }}/casts/{{ cookiecutter.cast_snake }}/modules/agents.py @@ -1,4 +1,4 @@ -"""[Optional] Construct agents required by the Sample Cast graph. +"""[Optional] Construct agents required by the {{ cookiecutter.cast_name }} graph. Guidelines: - Create agents using the `langchain.agents` module. diff --git a/act_operator/act_operator/scaffold/{{ cookiecutter.act_slug }}/casts/{{ cookiecutter.cast_snake }}/modules/middlewares.py b/act_operator/act_operator/scaffold/{{ cookiecutter.act_slug }}/casts/{{ cookiecutter.cast_snake }}/modules/middlewares.py index 120cfc9..9fb12e9 100644 --- a/act_operator/act_operator/scaffold/{{ cookiecutter.act_slug }}/casts/{{ cookiecutter.cast_snake }}/modules/middlewares.py +++ b/act_operator/act_operator/scaffold/{{ cookiecutter.act_slug }}/casts/{{ cookiecutter.cast_snake }}/modules/middlewares.py @@ -1,4 +1,4 @@ -"""[Optional] Middleware Classes for Sam graphs. +"""[Optional] Middleware Classes for the {{ cookiecutter.cast_name }} graph. Guidelines: - Use built-in middleware (e.g., PIIMiddleware) for common use cases. diff --git a/act_operator/act_operator/scaffold/{{ cookiecutter.act_slug }}/casts/{{ cookiecutter.cast_snake }}/modules/nodes.py b/act_operator/act_operator/scaffold/{{ cookiecutter.act_slug }}/casts/{{ cookiecutter.cast_snake }}/modules/nodes.py index 0e8256d..24a96b3 100644 --- a/act_operator/act_operator/scaffold/{{ cookiecutter.act_slug }}/casts/{{ cookiecutter.cast_snake }}/modules/nodes.py +++ b/act_operator/act_operator/scaffold/{{ cookiecutter.act_slug }}/casts/{{ cookiecutter.cast_snake }}/modules/nodes.py @@ -37,9 +37,15 @@ def execute(self, state): state: Current graph state. Returns: - dict: State updates (must be a dict) + dict: State updates (must be a dict). Writes both ``result`` + (exposed by OutputState) and ``messages`` (accumulated via + MessagesState's ``add_messages`` reducer). """ - return {"messages": [AIMessage(content="Welcome to the Act! by Sync Node")]} + welcome = "Welcome to the Act!" + return { + "result": welcome, + "messages": [AIMessage(content=f"{welcome} by Sync Node")], + } class AsyncSampleNode(AsyncBaseNode): @@ -60,6 +66,12 @@ async def execute(self, state): state: Current graph state. Returns: - dict: State updates (must be a dict) + dict: State updates (must be a dict). Writes both ``result`` + (exposed by OutputState) and ``messages`` (accumulated via + MessagesState's ``add_messages`` reducer). """ - return {"messages": [AIMessage(content="Welcome to the Act! by Async Node")]} + welcome = "Welcome to the Act!" + return { + "result": welcome, + "messages": [AIMessage(content=f"{welcome} by Async Node")], + } diff --git a/act_operator/act_operator/scaffold/{{ cookiecutter.act_slug }}/casts/{{ cookiecutter.cast_snake }}/modules/state.py b/act_operator/act_operator/scaffold/{{ cookiecutter.act_slug }}/casts/{{ cookiecutter.cast_snake }}/modules/state.py index 5e42491..de19ec8 100644 --- a/act_operator/act_operator/scaffold/{{ cookiecutter.act_slug }}/casts/{{ cookiecutter.cast_snake }}/modules/state.py +++ b/act_operator/act_operator/scaffold/{{ cookiecutter.act_slug }}/casts/{{ cookiecutter.cast_snake }}/modules/state.py @@ -1,4 +1,4 @@ -"""[Required] State definition shared across sam graphs. +"""[Required] State definition for the {{ cookiecutter.cast_name }} graph. Guidelines: - Create TypedDict classes for input, output, overall state, and any other state you need. diff --git a/act_operator/act_operator/scaffold/{{ cookiecutter.act_slug }}/pyproject.toml b/act_operator/act_operator/scaffold/{{ cookiecutter.act_slug }}/pyproject.toml index a55afb0..216e52e 100644 --- a/act_operator/act_operator/scaffold/{{ cookiecutter.act_slug }}/pyproject.toml +++ b/act_operator/act_operator/scaffold/{{ cookiecutter.act_slug }}/pyproject.toml @@ -12,6 +12,7 @@ dependencies = [ [dependency-groups] test = [ "pytest", + "pytest-asyncio", "langgraph-cli[inmem]", ] lint = [ @@ -27,11 +28,16 @@ dev = [ [tool.pyright] include = ["casts", "tests"] exclude = [ - "**/__pycache__/*", - ".venv", + "**/__pycache__/*", + ".venv", "**/.venv" ] +[tool.pytest.ini_options] +pythonpath = ["."] +testpaths = ["tests"] +asyncio_mode = "auto" + [tool.uv.workspace] members = ["casts/*"] exclude = [ diff --git a/act_operator/act_operator/scaffold/{{ cookiecutter.act_slug }}/tests/cast_tests/{{ cookiecutter.cast_snake }}_test.py b/act_operator/act_operator/scaffold/{{ cookiecutter.act_slug }}/tests/cast_tests/{{ cookiecutter.cast_snake }}_test.py index 18c7e07..b4940ed 100644 --- a/act_operator/act_operator/scaffold/{{ cookiecutter.act_slug }}/tests/cast_tests/{{ cookiecutter.cast_snake }}_test.py +++ b/act_operator/act_operator/scaffold/{{ cookiecutter.act_slug }}/tests/cast_tests/{{ cookiecutter.cast_snake }}_test.py @@ -11,9 +11,9 @@ def test_graph_produces_message() -> None: graph = {{ cookiecutter.cast_snake }}_graph() - # 최소 상태로 그래프 실행 + # 최소 상태로 그래프 실행 — OutputState filter가 적용되어 ``result`` 키만 노출됨 result = graph.invoke({"query": "I'm joining Act"}) - # SampleNode가 message 키를 생성하는지 확인 - assert "messages" in result - assert result["messages"] == "Welcome to the Act!" + # SampleNode가 result 키를 생성하는지 확인 + assert "result" in result + assert result["result"] == "Welcome to the Act!" diff --git a/act_operator/act_operator/scaffold/{{ cookiecutter.act_slug }}/tests/node_tests/__init.py b/act_operator/act_operator/scaffold/{{ cookiecutter.act_slug }}/tests/node_tests/__init__.py similarity index 100% rename from act_operator/act_operator/scaffold/{{ cookiecutter.act_slug }}/tests/node_tests/__init.py rename to act_operator/act_operator/scaffold/{{ cookiecutter.act_slug }}/tests/node_tests/__init__.py diff --git a/act_operator/act_operator/scaffold/{{ cookiecutter.act_slug }}/tests/node_tests/test_node.py b/act_operator/act_operator/scaffold/{{ cookiecutter.act_slug }}/tests/node_tests/test_node.py index 0e1d40d..82aea2c 100644 --- a/act_operator/act_operator/scaffold/{{ cookiecutter.act_slug }}/tests/node_tests/test_node.py +++ b/act_operator/act_operator/scaffold/{{ cookiecutter.act_slug }}/tests/node_tests/test_node.py @@ -1,4 +1,4 @@ -"""Test the nodes for the Sam graph. +"""Test the nodes for the {{ cookiecutter.cast_name }} graph. Official document URL: https://docs.langchain.com/oss/python/langgraph/test""" @@ -10,10 +10,16 @@ def test_base_node_calls_execute() -> None: node = SampleNode() result = node.execute({"query": "I'm joining Act"}) - assert result == {"message": "Welcome to the Act!"} + + assert result["result"] == "Welcome to the Act!" + assert len(result["messages"]) == 1 + assert result["messages"][0].content == "Welcome to the Act! by Sync Node" async def test_async_base_node_calls_execute() -> None: node = AsyncSampleNode() result = await node.execute({"query": "I'm joining Act"}) - assert result == {"message": "Welcome to the Act!"} + + assert result["result"] == "Welcome to the Act!" + assert len(result["messages"]) == 1 + assert result["messages"][0].content == "Welcome to the Act! by Async Node" From cb872928f707966fc84bce81b28c816b62bd74d7 Mon Sep 17 00:00:00 2001 From: Joongheon Park Date: Wed, 27 May 2026 11:09:15 +0900 Subject: [PATCH 2/2] fix(scaffold): replace remaining Korean comments with English Two code/config files had Korean comments without language conditioning, so projects scaffolded with `-l en` still contained Korean text: - tests/cast_tests/{cast}_test.py: two inline comments added in the previous commit (regression on my part). - .pre-commit-config.yaml: three hook description comments hardcoded in Korean. In-code/config comments now consistently English per the project convention (`.env.example`, `README.md`, `TEMPLATE_README.md`, and `casts/{cast}/README.md` already gate their content on `cookiecutter.language` and remain language-aware). Verified by regenerating with `-l en`: `grep -P '[\x{ac00}-\x{d7a3}]'` returns 0 matches across the generated project, and `uv run pytest -v` still reports `4 passed in 0.47s`. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../{{ cookiecutter.act_slug }}/.pre-commit-config.yaml | 6 +++--- .../tests/cast_tests/{{ cookiecutter.cast_snake }}_test.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/act_operator/act_operator/scaffold/{{ cookiecutter.act_slug }}/.pre-commit-config.yaml b/act_operator/act_operator/scaffold/{{ cookiecutter.act_slug }}/.pre-commit-config.yaml index 679f7c5..94027a1 100644 --- a/act_operator/act_operator/scaffold/{{ cookiecutter.act_slug }}/.pre-commit-config.yaml +++ b/act_operator/act_operator/scaffold/{{ cookiecutter.act_slug }}/.pre-commit-config.yaml @@ -5,14 +5,14 @@ repos: rev: 0.9.2 hooks: - id: uv-lock - # 의존성 락 파일 동기화 + # Keep the dependency lock file in sync - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.12.0 hooks: - id: ruff-check - # 코드 품질 점검 및 임포트 정리 + # Lint and tidy imports types_or: [python, pyi] args: [--fix] - id: ruff-format - # 코드 포맷팅 + # Format code types_or: [python, pyi] diff --git a/act_operator/act_operator/scaffold/{{ cookiecutter.act_slug }}/tests/cast_tests/{{ cookiecutter.cast_snake }}_test.py b/act_operator/act_operator/scaffold/{{ cookiecutter.act_slug }}/tests/cast_tests/{{ cookiecutter.cast_snake }}_test.py index b4940ed..ec6b81e 100644 --- a/act_operator/act_operator/scaffold/{{ cookiecutter.act_slug }}/tests/cast_tests/{{ cookiecutter.cast_snake }}_test.py +++ b/act_operator/act_operator/scaffold/{{ cookiecutter.act_slug }}/tests/cast_tests/{{ cookiecutter.cast_snake }}_test.py @@ -11,9 +11,9 @@ def test_graph_produces_message() -> None: graph = {{ cookiecutter.cast_snake }}_graph() - # 최소 상태로 그래프 실행 — OutputState filter가 적용되어 ``result`` 키만 노출됨 + # Invoke with minimal state — OutputState filter exposes only the ``result`` key. result = graph.invoke({"query": "I'm joining Act"}) - # SampleNode가 result 키를 생성하는지 확인 + # Verify SampleNode populated ``result``. assert "result" in result assert result["result"] == "Welcome to the Act!"