From b50f8363f469565a5888f31773415e8c80605b35 Mon Sep 17 00:00:00 2001 From: ehsan amiri Date: Thu, 3 Sep 2026 02:54:36 +0330 Subject: [PATCH 1/2] Add centralized tests/ layout and move Python suite out of the package. Introduces tests/python with shared conftest, fixtures, runner scripts, pytest.ini, and CI ruff/pytest integration so binding tests no longer ship inside fusion_framework. Co-authored-by: Cursor --- .../skills/fusion-bindings-parity/SKILL.md | 2 +- .agents/skills/fusion-http-routes/SKILL.md | 2 +- .agents/skills/fusion-testing/SKILL.md | 29 +++- .github/workflows/ci.yml | 6 +- .../fusion_framework/test_middleware.py | 133 ------------------ .../python/fusion_framework/test_templates.py | 115 --------------- pytest.ini | 7 + scripts/README.md | 2 +- scripts/dev-install-python.sh | 2 +- tests/README.md | 57 ++++++++ tests/fixtures/templates/sample/page.html | 1 + tests/python/conftest.py | 20 +++ tests/python/integration/README.md | 9 ++ .../python/unit}/test_http_route.py | 16 +-- tests/python/unit/test_middleware.py | 106 ++++++++++++++ .../python/unit}/test_pagination.py | 12 +- .../python/unit}/test_reload.py | 0 .../python/unit}/test_swagger.py | 10 +- tests/python/unit/test_templates.py | 103 ++++++++++++++ tests/scripts/run-all.sh | 25 ++++ tests/scripts/run-python.sh | 12 ++ 21 files changed, 376 insertions(+), 293 deletions(-) delete mode 100644 crates/fusion-py/python/fusion_framework/test_middleware.py delete mode 100644 crates/fusion-py/python/fusion_framework/test_templates.py create mode 100644 pytest.ini create mode 100644 tests/README.md create mode 100644 tests/fixtures/templates/sample/page.html create mode 100644 tests/python/conftest.py create mode 100644 tests/python/integration/README.md rename {crates/fusion-py/python/fusion_framework => tests/python/unit}/test_http_route.py (86%) create mode 100644 tests/python/unit/test_middleware.py rename {crates/fusion-py/python/fusion_framework => tests/python/unit}/test_pagination.py (90%) rename {crates/fusion-py/python/fusion_framework => tests/python/unit}/test_reload.py (100%) rename {crates/fusion-py/python/fusion_framework => tests/python/unit}/test_swagger.py (96%) create mode 100644 tests/python/unit/test_templates.py create mode 100755 tests/scripts/run-all.sh create mode 100755 tests/scripts/run-python.sh diff --git a/.agents/skills/fusion-bindings-parity/SKILL.md b/.agents/skills/fusion-bindings-parity/SKILL.md index b050822..24d6e3e 100644 --- a/.agents/skills/fusion-bindings-parity/SKILL.md +++ b/.agents/skills/fusion-bindings-parity/SKILL.md @@ -34,7 +34,7 @@ cargo test -p fusion-core naming cargo check -p fusion-py node --check crates/fusion-node/index.js dotnet build bindings/csharp/FusionFramework/FusionFramework.csproj -python -m pytest crates/fusion-py/python/fusion_framework/test_http_route.py -q +python -m pytest tests/python -q ``` ## Style diff --git a/.agents/skills/fusion-http-routes/SKILL.md b/.agents/skills/fusion-http-routes/SKILL.md index 459cb06..1d9a48c 100644 --- a/.agents/skills/fusion-http-routes/SKILL.md +++ b/.agents/skills/fusion-http-routes/SKILL.md @@ -63,4 +63,4 @@ public class UserModule : FusionBaseApi { ## Tests - Rust: `cargo test -p fusion-core naming` -- Python: `test_http_route.py` +- Python: `pytest tests/python` diff --git a/.agents/skills/fusion-testing/SKILL.md b/.agents/skills/fusion-testing/SKILL.md index 8d8ad4c..31779ec 100644 --- a/.agents/skills/fusion-testing/SKILL.md +++ b/.agents/skills/fusion-testing/SKILL.md @@ -8,14 +8,31 @@ description: >- # Testing & Verification +## Full suite (recommended) + +```bash +./tests/scripts/run-all.sh +``` + ## Quick smoke (after route/API changes) ```bash cargo test -p fusion-core naming cargo check -p fusion-py node --check crates/fusion-node/index.js -dotnet build bindings/csharp/FusionFramework/FusionFramework.csproj --no-restore 2>/dev/null || dotnet build bindings/csharp/FusionFramework/FusionFramework.csproj -python -m pytest crates/fusion-py/python/fusion_framework/test_http_route.py -q +dotnet build bindings/csharp/FusionFramework/FusionFramework.csproj +./tests/scripts/run-python.sh -q +``` + +## Python (pytest) + +Tests live under `tests/python/` (not inside the installable package). + +```bash +./scripts/dev-install-python.sh --venv .venv +source .venv/bin/activate +pytest # uses pytest.ini at repo root +pytest tests/python/unit/test_http_route.py -q ``` ## Full Rust workspace @@ -29,14 +46,18 @@ cargo test --workspace | Binding | Command | |---------|---------| -| Python package | `cd crates/fusion-py && maturin develop` (if building extension) | +| Python | `pytest tests/python` (after `dev-install-python.sh`) | | Node | `node --check crates/fusion-node/index.js` | | C# | `dotnet build bindings/csharp/FusionFramework/FusionFramework.csproj` | +## Layout + +See `tests/README.md` for folder structure and conventions. + ## What to exclude from git - `bindings/csharp/**/bin/`, `obj/` -- `target/`, `node_modules/`, `__pycache__/` +- `target/`, `node_modules/`, `__pycache__/`, `.pytest_cache/` - Local `.pdb` changes from debug builds ## When tests fail diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 14d4365..a19a793 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -44,10 +44,10 @@ jobs: run: python -m pip install ruff - name: ruff check - run: ruff check crates/fusion-py/python + run: ruff check crates/fusion-py/python tests/python - name: ruff format - run: ruff format --check crates/fusion-py/python + run: ruff format --check crates/fusion-py/python tests/python # Node binding is a thin CommonJS wrapper over N-API; no eslint config in this repo. - uses: actions/setup-node@v4 @@ -100,7 +100,9 @@ jobs: shell: bash run: | pip install dist/*.whl + pip install pytest python -c "from fusion_framework.api import FusionBaseApi; from fusion_framework.route import router; from fusion_framework import settings; print('ok')" + pytest tests/python -q node: name: Node addon (${{ matrix.settings.target }}) diff --git a/crates/fusion-py/python/fusion_framework/test_middleware.py b/crates/fusion-py/python/fusion_framework/test_middleware.py deleted file mode 100644 index 82cc48b..0000000 --- a/crates/fusion-py/python/fusion_framework/test_middleware.py +++ /dev/null @@ -1,133 +0,0 @@ -"""Unit tests for middleware chain (no server required).""" - -import asyncio -import inspect - -from fusion_framework.middleware import ( - bearer_jwt, - clear_active_global, - cors, - dispatch_route, - framework_headers, - request_id, - require_roles, - set_active_global, -) - - -def _handler(request): - return {"status": 200, "body": {"state": request.get("state", {})}} - - -async def _async_handler(request): - return {"status": 200, "body": {"ok": True, "path": request.get("path")}} - - -def _sync_invoker_like(request): - """Mirrors PyO3 HandlerInvoker: sync ``__call__`` that may return a coroutine.""" - return _async_handler(request) - - -def test_require_roles_allows_matching_role(): - request = {"headers": {}, "state": {"jwt": {"roles": ["admin"]}}} - chain = [require_roles("admin", "super_admin")] - result = dispatch_route(request, _handler, chain) - assert result["status"] == 200 - - -def test_require_roles_blocks_missing_role(): - request = {"headers": {}, "state": {"jwt": {"roles": ["user"]}}} - chain = [require_roles("admin")] - result = dispatch_route(request, _handler, chain) - assert result["status"] == 403 - - -def test_bearer_jwt_populates_state(): - token = "eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzdWIiOiIxIiwicm9sZXMiOlsiYWRtaW4iXX0." - request = {"headers": {"Authorization": f"Bearer {token}"}} - set_active_global([bearer_jwt()]) - try: - result = dispatch_route(request, _handler, []) - assert result["status"] == 200 - assert result["body"]["state"]["jwt"]["sub"] == "1" - finally: - clear_active_global() - - -def test_no_middleware_by_default(): - """FusionApp does not inject framework headers unless explicitly added.""" - clear_active_global() - set_active_global([]) - try: - request = {"path": "/", "headers": {}, "method": "GET"} - result = dispatch_route(request, _handler, []) - assert result["status"] == 200 - headers = {str(k).lower(): v for k, v in (result.get("headers") or {}).items()} - assert "x-powered-by" not in headers - finally: - clear_active_global() - - -def test_request_id_header(): - clear_active_global() - set_active_global([request_id()]) - try: - request = {"path": "/", "headers": {}, "method": "GET"} - result = dispatch_route(request, _handler, []) - headers = {str(k).lower(): v for k, v in (result.get("headers") or {}).items()} - assert "x-request-id" in headers - assert result["body"]["state"]["request_id"] == headers["x-request-id"] - finally: - clear_active_global() - - -def test_cors_options_preflight(): - clear_active_global() - set_active_global([cors()]) - try: - request = { - "path": "/api", - "headers": {"Origin": "https://example.com"}, - "method": "OPTIONS", - } - result = dispatch_route(request, _handler, []) - assert result["status"] == 204 - headers = {str(k).lower(): v for k, v in (result.get("headers") or {}).items()} - assert headers.get("access-control-allow-origin") - finally: - clear_active_global() - - -def test_framework_headers_awaits_async_handler(): - """Regression: sync framework_headers must not stringify coroutine bodies. - - HandlerInvoker is sync, so the chain stays sync; call_next returns a raw - coroutine that must be awaited before headers are merged. - """ - clear_active_global() - set_active_global([framework_headers()]) - try: - request = {"path": "/membership", "headers": {}, "method": "GET"} - result = dispatch_route(request, _sync_invoker_like, []) - assert inspect.isawaitable(result), "async handler result must stay awaitable for Rust" - resolved = asyncio.run(result) - assert resolved["status"] == 200 - assert resolved["body"] == {"ok": True, "path": "/membership"} - headers = {str(k).lower(): v for k, v in (resolved.get("headers") or {}).items()} - assert "x-powered-by" in headers - body = resolved.get("body") - assert not (isinstance(body, str) and body.startswith("}}', - encoding="utf-8", - ) - html = render_template("page.html", {}, templates_root=root) - assert "fusion-btn" in html - assert "Go" in html - - -def test_fusion_base_template_context(): - class Page(FusionBaseTemplate): - template = "hello.html" - - def context(self): - return {"name": "Fusion"} - - with tempfile.TemporaryDirectory() as tmp: - root = Path(tmp) - (root / "hello.html").write_text("

Hello {{ name }}!

", encoding="utf-8") - - page = Page({"method": "GET", "path": "/"}) - page.templates_dir = str(root) - out = page.render() - assert out["status"] == 200 - assert out["headers"]["content-type"].startswith("text/html") - assert "Hello Fusion!" in out["body"] - - -def test_template_name_required(): - class Bad(FusionBaseTemplate): - pass - - with pytest.raises(ValueError, match="template"): - Bad({}).template_name() - - -def test_template_get_returns_json_with_accept(): - class Page(FusionBaseTemplate): - template = "hello.html" - - def context(self): - return {"name": "Fusion"} - - with tempfile.TemporaryDirectory() as tmp: - root = Path(tmp) - (root / "hello.html").write_text("

Hello {{ name }}!

", encoding="utf-8") - - page = Page( - { - "method": "GET", - "path": "/pages/home", - "headers": {"accept": "application/json"}, - } - ) - page.templates_dir = str(root) - assert page.get() == {"name": "Fusion"} - - -def test_template_get_returns_json_with_format_query(): - class Page(FusionBaseTemplate): - template = "hello.html" - - def context(self): - return {"name": "Fusion"} - - page = Page( - { - "method": "GET", - "path": "/pages/home", - "query": {"format": "json"}, - } - ) - assert page.get() == {"name": "Fusion"} - - -def test_template_get_returns_html_for_browser_accept(): - class Page(FusionBaseTemplate): - template = "hello.html" - - def context(self): - return {"name": "Fusion"} - - with tempfile.TemporaryDirectory() as tmp: - root = Path(tmp) - (root / "hello.html").write_text("

Hello {{ name }}!

", encoding="utf-8") - - page = Page( - { - "method": "GET", - "path": "/pages/home", - "headers": { - "accept": "text/html,application/xhtml+xml,application/xml;q=0.9" - }, - } - ) - page.templates_dir = str(root) - out = page.get() - assert out["status"] == 200 - assert out["headers"]["content-type"].startswith("text/html") - assert "Hello Fusion!" in out["body"] diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..d12fb50 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,7 @@ +[pytest] +testpaths = tests/python +pythonpath = crates/fusion-py/python +addopts = -ra --strict-markers +markers = + integration: tests that require a running HTTP server + slow: long-running tests (opt-in) diff --git a/scripts/README.md b/scripts/README.md index 97f0588..d62c3e5 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -155,7 +155,7 @@ Each script runs a small smoke check before it finishes. After install, you can ```bash python -c "from fusion_framework import settings; print('python ok')" python examples/template_demo.py -python -m pytest crates/fusion-py/python/fusion_framework/test_http_route.py -q +python -m pytest tests/python -q ``` **Node** diff --git a/scripts/dev-install-python.sh b/scripts/dev-install-python.sh index 0405887..ef1572b 100755 --- a/scripts/dev-install-python.sh +++ b/scripts/dev-install-python.sh @@ -94,5 +94,5 @@ cat <}} diff --git a/tests/python/conftest.py b/tests/python/conftest.py new file mode 100644 index 0000000..c691af9 --- /dev/null +++ b/tests/python/conftest.py @@ -0,0 +1,20 @@ +"""Shared pytest fixtures for Fusion Framework Python tests.""" + +from __future__ import annotations + +import pytest + +pytest.importorskip("fusion_framework") + +from fusion_framework._fusion import clear_routes +from fusion_framework.middleware import clear_active_global + + +@pytest.fixture(autouse=True) +def _isolate_fusion_state(): + """Reset route registry and global middleware between tests.""" + clear_routes() + clear_active_global() + yield + clear_routes() + clear_active_global() diff --git a/tests/python/integration/README.md b/tests/python/integration/README.md new file mode 100644 index 0000000..3fb8614 --- /dev/null +++ b/tests/python/integration/README.md @@ -0,0 +1,9 @@ +# Integration tests (future) + +HTTP-level tests that start a real `FusionApp` and hit endpoints with `httpx` or `curl` belong here. + +Mark with `@pytest.mark.integration` and run: + +```bash +pytest tests/python/integration -m integration +``` diff --git a/crates/fusion-py/python/fusion_framework/test_http_route.py b/tests/python/unit/test_http_route.py similarity index 86% rename from crates/fusion-py/python/fusion_framework/test_http_route.py rename to tests/python/unit/test_http_route.py index b7e06b6..2fd5306 100644 --- a/crates/fusion-py/python/fusion_framework/test_http_route.py +++ b/tests/python/unit/test_http_route.py @@ -1,19 +1,11 @@ """Tests for custom HTTP method routes (@http_get / HttpGet).""" -from fusion_framework._fusion import clear_routes +from fusion_framework._fusion import openapi_spec from fusion_framework.api import FusionBaseApi from fusion_framework.http_route import http_get from fusion_framework.route import route -def setup_function(): - clear_routes() - - -def teardown_function(): - clear_routes() - - def test_custom_http_get_with_action_token(): @route("/api/[module]") class UserModule(FusionBaseApi): @@ -21,8 +13,6 @@ class UserModule(FusionBaseApi): def UserAction(self): return {"ok": True} - from fusion_framework._fusion import openapi_spec - spec = openapi_spec() assert "/api/user/test/user" in spec["paths"] assert "get" in spec["paths"]["/api/user/test/user"] @@ -39,8 +29,6 @@ def get(self): def ListAction(self): return {"mode": "custom"} - from fusion_framework._fusion import openapi_spec - spec = openapi_spec() assert "/api/product" in spec["paths"] assert "get" in spec["paths"]["/api/product"] @@ -61,8 +49,6 @@ def CatalogAction(self): def AdminAction(self): return {"ok": True} - from fusion_framework._fusion import openapi_spec - spec = openapi_spec() convention = spec["paths"]["/api/product"]["get"] catalog = spec["paths"]["/api/product/catalog/catalog"]["get"] diff --git a/tests/python/unit/test_middleware.py b/tests/python/unit/test_middleware.py new file mode 100644 index 0000000..d7234bd --- /dev/null +++ b/tests/python/unit/test_middleware.py @@ -0,0 +1,106 @@ +"""Unit tests for middleware chain (no server required).""" + +import asyncio +import inspect + +from fusion_framework.middleware import ( + bearer_jwt, + clear_active_global, + cors, + dispatch_route, + framework_headers, + request_id, + require_roles, + set_active_global, +) + + +def _handler(request): + return {"status": 200, "body": {"state": request.get("state", {})}} + + +async def _async_handler(request): + return {"status": 200, "body": {"ok": True, "path": request.get("path")}} + + +def _sync_invoker_like(request): + """Mirrors PyO3 HandlerInvoker: sync ``__call__`` that may return a coroutine.""" + return _async_handler(request) + + +def test_require_roles_allows_matching_role(): + request = {"headers": {}, "state": {"jwt": {"roles": ["admin"]}}} + chain = [require_roles("admin", "super_admin")] + result = dispatch_route(request, _handler, chain) + assert result["status"] == 200 + + +def test_require_roles_blocks_missing_role(): + request = {"headers": {}, "state": {"jwt": {"roles": ["user"]}}} + chain = [require_roles("admin")] + result = dispatch_route(request, _handler, chain) + assert result["status"] == 403 + + +def test_bearer_jwt_populates_state(): + token = "eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzdWIiOiIxIiwicm9sZXMiOlsiYWRtaW4iXX0." + request = {"headers": {"Authorization": f"Bearer {token}"}} + set_active_global([bearer_jwt()]) + result = dispatch_route(request, _handler, []) + assert result["status"] == 200 + assert result["body"]["state"]["jwt"]["sub"] == "1" + + +def test_no_middleware_by_default(): + """FusionApp does not inject framework headers unless explicitly added.""" + set_active_global([]) + request = {"path": "/", "headers": {}, "method": "GET"} + result = dispatch_route(request, _handler, []) + assert result["status"] == 200 + headers = {str(k).lower(): v for k, v in (result.get("headers") or {}).items()} + assert "x-powered-by" not in headers + + +def test_request_id_header(): + set_active_global([request_id()]) + request = {"path": "/", "headers": {}, "method": "GET"} + result = dispatch_route(request, _handler, []) + headers = {str(k).lower(): v for k, v in (result.get("headers") or {}).items()} + assert "x-request-id" in headers + assert result["body"]["state"]["request_id"] == headers["x-request-id"] + + +def test_cors_options_preflight(): + set_active_global([cors()]) + request = { + "path": "/api", + "headers": {"Origin": "https://example.com"}, + "method": "OPTIONS", + } + result = dispatch_route(request, _handler, []) + assert result["status"] == 204 + headers = {str(k).lower(): v for k, v in (result.get("headers") or {}).items()} + assert headers.get("access-control-allow-origin") + + +def test_framework_headers_awaits_async_handler(): + """Regression: sync framework_headers must not stringify coroutine bodies.""" + set_active_global([framework_headers()]) + request = {"path": "/membership", "headers": {}, "method": "GET"} + result = dispatch_route(request, _sync_invoker_like, []) + assert inspect.isawaitable(result), "async handler result must stay awaitable for Rust" + resolved = asyncio.run(result) + assert resolved["status"] == 200 + assert resolved["body"] == {"ok": True, "path": "/membership"} + headers = {str(k).lower(): v for k, v in (resolved.get("headers") or {}).items()} + assert "x-powered-by" in headers + body = resolved.get("body") + assert not (isinstance(body, str) and body.startswith("Hello {{ name }}!

", encoding="utf-8") + + page = Page({"method": "GET", "path": "/"}) + page.templates_dir = str(tmp_path) + out = page.render() + assert out["status"] == 200 + assert out["headers"]["content-type"].startswith("text/html") + assert "Hello Fusion!" in out["body"] + + +def test_template_name_required(): + class Bad(FusionBaseTemplate): + pass + + with pytest.raises(ValueError, match="template"): + Bad({}).template_name() + + +def test_template_get_returns_json_with_accept(tmp_path: Path): + class Page(FusionBaseTemplate): + template = "hello.html" + + def context(self): + return {"name": "Fusion"} + + (tmp_path / "hello.html").write_text("

Hello {{ name }}!

", encoding="utf-8") + + page = Page( + { + "method": "GET", + "path": "/pages/home", + "headers": {"accept": "application/json"}, + } + ) + page.templates_dir = str(tmp_path) + assert page.get() == {"name": "Fusion"} + + +def test_template_get_returns_json_with_format_query(): + class Page(FusionBaseTemplate): + template = "hello.html" + + def context(self): + return {"name": "Fusion"} + + page = Page( + { + "method": "GET", + "path": "/pages/home", + "query": {"format": "json"}, + } + ) + assert page.get() == {"name": "Fusion"} + + +def test_template_get_returns_html_for_browser_accept(tmp_path: Path): + class Page(FusionBaseTemplate): + template = "hello.html" + + def context(self): + return {"name": "Fusion"} + + (tmp_path / "hello.html").write_text("

Hello {{ name }}!

", encoding="utf-8") + + page = Page( + { + "method": "GET", + "path": "/pages/home", + "headers": { + "accept": "text/html,application/xhtml+xml,application/xml;q=0.9" + }, + } + ) + page.templates_dir = str(tmp_path) + out = page.get() + assert out["status"] == 200 + assert out["headers"]["content-type"].startswith("text/html") + assert "Hello Fusion!" in out["body"] diff --git a/tests/scripts/run-all.sh b/tests/scripts/run-all.sh new file mode 100755 index 0000000..6029a91 --- /dev/null +++ b/tests/scripts/run-all.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash +# Run the full Fusion Framework test suite (local). +set -euo pipefail + +ROOT="$(cd "$(dirname "$0")/../.." && pwd)" +cd "$ROOT" + +echo "==> Rust (fusion-core)" +cargo test -p fusion-core + +echo "==> Node syntax" +node --check crates/fusion-node/index.js + +echo "==> C# build" +dotnet build bindings/csharp/FusionFramework/FusionFramework.csproj --no-restore 2>/dev/null \ + || dotnet build bindings/csharp/FusionFramework/FusionFramework.csproj + +echo "==> Python (pytest)" +if ! python3 -c "import fusion_framework" 2>/dev/null; then + echo " fusion_framework not installed — run: ./scripts/dev-install-python.sh --venv .venv" + exit 1 +fi +python3 -m pytest tests/python -q + +echo "==> All checks passed" diff --git a/tests/scripts/run-python.sh b/tests/scripts/run-python.sh new file mode 100755 index 0000000..0a9f49f --- /dev/null +++ b/tests/scripts/run-python.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT="$(cd "$(dirname "$0")/../.." && pwd)" +cd "$ROOT" + +if ! python3 -c "import fusion_framework" 2>/dev/null; then + echo "fusion_framework not installed — run: ./scripts/dev-install-python.sh --venv .venv" >&2 + exit 1 +fi + +python3 -m pytest tests/python "$@" From b7421ccfbce4ceec204a89e9093c003ef02dac7e Mon Sep 17 00:00:00 2001 From: ehsan amiri Date: Thu, 3 Sep 2026 16:20:50 +0330 Subject: [PATCH 2/2] Add agent skills, coding standards, and fusion-cli documentation. Codify parity, examples, comments, and git staging rules for agents working on the framework. Co-authored-by: Cursor --- .agents/README.md | 21 ++- .agents/skills/fusion-architecture/SKILL.md | 28 ++- .../skills/fusion-bindings-parity/SKILL.md | 37 +++- .agents/skills/fusion-cli/SKILL.md | 167 ++++++++++++++++++ .../skills/fusion-coding-standards/SKILL.md | 82 +++++++++ .agents/skills/fusion-http-routes/SKILL.md | 4 + .agents/skills/fusion-release/SKILL.md | 2 + .agents/skills/fusion-testing/SKILL.md | 57 ++++-- .cursor/rules/fusion-engineering.mdc | 53 ++++++ 9 files changed, 417 insertions(+), 34 deletions(-) create mode 100644 .agents/skills/fusion-cli/SKILL.md create mode 100644 .agents/skills/fusion-coding-standards/SKILL.md create mode 100644 .cursor/rules/fusion-engineering.mdc diff --git a/.agents/README.md b/.agents/README.md index 80b4d28..b36056f 100644 --- a/.agents/README.md +++ b/.agents/README.md @@ -6,18 +6,27 @@ Project-local skills for Cursor agents working on this repository. ``` .agents/skills//SKILL.md +.cursor/rules/*.mdc # always-on / scoped project rules ``` -Each skill teaches the agent domain-specific workflows for Fusion (Rust core + Python / Node / C# bindings). +Each skill teaches domain-specific workflows for Fusion (Rust core + Python / Node / C# bindings) and the companion `fusion` CLI. ## Available skills | Skill | Use when | |-------|----------| -| `fusion-architecture` | Understanding repo layout, binding layers, where logic belongs | -| `fusion-bindings-parity` | Changing behavior that must stay aligned across Python, Node, C# | -| `fusion-http-routes` | Routes, `@http_get` / `[HttpGet]`, `[module]`, `[action]`, Swagger | +| `fusion-architecture` | Repo layout, binding layers, where logic belongs | +| `fusion-bindings-parity` | Feature must land in Python **and** Node **and** C# | +| `fusion-coding-standards` | Comments, tests preference, git staging, skill/doc hygiene | +| `fusion-cli` | `fusion init` / commands / scaffold tree / CLI ↔ framework | +| `fusion-http-routes` | Routes, `http_get` / `[HttpGet]`, `[module]`, `[action]`, Swagger | | `fusion-release` | Version bumps, manifests, publish prep | -| `fusion-testing` | Running checks and binding-specific tests | +| `fusion-testing` | Running checks; investigating failed tests | -Skills are loaded when the task matches the skill description (or when you name the skill explicitly). +## Always-on rules + +`.cursor/rules/fusion-engineering.mdc` applies every session: parity across bindings, **examples in all three languages for new features**, function comments, prefer tests, never `git add .`, investigate failures, update skills when needed. + +## Hygiene + +When you add a new concept agents must remember, either extend an existing skill or add `.agents/skills//SKILL.md` and a row in this table. diff --git a/.agents/skills/fusion-architecture/SKILL.md b/.agents/skills/fusion-architecture/SKILL.md index 4f85506..8efcc50 100644 --- a/.agents/skills/fusion-architecture/SKILL.md +++ b/.agents/skills/fusion-architecture/SKILL.md @@ -1,9 +1,10 @@ --- name: fusion-architecture description: >- - Explains Fusion Framework repository layout, crate boundaries, and where - new logic belongs (fusion-core vs Python/Node/C# bindings). Use when - navigating the codebase, adding features, or deciding which layer to change. + Explains Fusion Framework repository layout, crate boundaries, where new + logic belongs (fusion-core vs Python/Node/C# bindings), tests layout, and + relationship to the fusion CLI. Use when navigating the codebase, adding + features, or deciding which layer to change. --- # Fusion Architecture @@ -12,18 +13,24 @@ description: >- | Path | Role | |------|------| -| `crates/fusion-core/` | Shared Rust: naming, route tokens, HTTP conventions | +| `crates/fusion-core/` | Shared Rust: naming, route tokens, HTTP conventions, settings helpers | | `crates/fusion-py/` | Python binding (PyO3) + `python/fusion_framework/` package | | `crates/fusion-node/` | Node binding (`index.js`, N-API) | -| `bindings/csharp/FusionFramework/` | C# binding (source of truth for NuGet layout) | +| `crates/fusion-ffi/` | C ABI for the C# binding | +| `bindings/csharp/FusionFramework/` | C# binding (NuGet layout source of truth) | +| `tests/` | Executable tests (Python / Node / C#) — not inside installable packages | | `examples/` | Runnable samples per binding | -| `scripts/` | Release tooling (`set-version.sh`) | +| `scripts/` | Dev install, version bumps (`set-version.sh`) | +| `.agents/skills/` | Agent skills for this repo | + +**Related external repo:** [fusion-tool](https://github.com/cipherunits/fusion-tool) — `fusion` CLI that scaffolds apps. See `fusion-cli` skill. ## Layering rules 1. **Put shared semantics in `fusion-core`** — route token resolution (`[module]`, `[action]`), path joining, handler naming. Bindings should call Rust helpers via FFI where possible. 2. **Bindings mirror behavior** — Python decorators, Node functions, C# attributes must produce the same mount paths and OpenAPI shapes. 3. **Do not duplicate business logic in three languages** — only binding-specific glue (decorators, reflection, module registration). +4. **Tests live under `tests/`** — do not add `test_*.py` inside `fusion_framework/` package sources. ## Key entry points @@ -33,6 +40,9 @@ description: >- ## When adding a feature -1. Identify if it is cross-binding (yes → start in `fusion-core`). -2. Implement mount + OpenAPI in all three bindings in one PR when possible. -3. Add or extend an example under `examples/`. +1. Identify if it is cross-binding (yes → start in `fusion-core` when semantics are shared). +2. Implement in **Python, Node, and C#** in one change set (see `fusion-bindings-parity`). +3. Add tests under `tests/` (preferred) and/or Rust unit tests. +4. Add **usage examples in all three languages** under `examples/` (`.py` / `.mjs` / `.cs`) so the API shape is visible. +5. If scaffolds or env JSON contracts change, update the `fusion-cli` skill and consider fusion-tool templates. +6. Comment new functions; document dense logic (see `fusion-coding-standards`). diff --git a/.agents/skills/fusion-bindings-parity/SKILL.md b/.agents/skills/fusion-bindings-parity/SKILL.md index 24d6e3e..6ef408b 100644 --- a/.agents/skills/fusion-bindings-parity/SKILL.md +++ b/.agents/skills/fusion-bindings-parity/SKILL.md @@ -2,20 +2,39 @@ name: fusion-bindings-parity description: >- Keeps Python, Node, and C# Fusion bindings aligned when changing APIs, routes, - Swagger, or middleware. Use when editing more than one binding or adding - cross-language behavior. + Swagger, middleware, or permissions. Use when editing more than one binding + or when the user asks to add a feature (always implement all three languages + unless they limit scope). --- # Bindings Parity +## Hard rule + +If the user says “add X” (permissions, middleware, route option, Swagger behavior, settings key, etc.) and X is framework surface area, implement it for: + +1. **Python** +2. **Node** +3. **C#** + +in the **same** change set unless they explicitly say “only Python” (or only one binding). + +Do not leave one language behind “for later” without saying so and getting confirmation. + ## Checklist (every cross-binding change) - [ ] `fusion-core` updated if semantics are shared -- [ ] Python: `fusion_framework/` + `crates/fusion-py/src/api_types.rs` -- [ ] Node: `crates/fusion-node/index.js` +- [ ] Python: `fusion_framework/` + `crates/fusion-py/src/api_types.rs` as needed +- [ ] Node: `crates/fusion-node/index.js` (+ `index.d.ts` if public types change) - [ ] C#: `bindings/csharp/FusionFramework/*.cs` -- [ ] Example snippet in `examples/` (at least one runnable file + others documented) +- [ ] Tests under `tests/python/`, `tests/node/`, and/or `tests/csharp/` when behavior is testable +- [ ] **Examples in all three languages** under `examples/` (`.py`, `.mjs`, `.cs`) showing how to use the new API - [ ] README in C# binding updated if public API changed +- [ ] Skills/docs updated if agents need new knowledge (`fusion-cli`, `fusion-http-routes`, …) + +## Examples rule + +New public surface → show usage in **Python + Node + C#**. Prefer the same basename for the trio (see `custom_http_routes.*`, `pagination.*`). Examples should be short and runnable enough to see the API shape, not full apps. ## Parity matrix @@ -24,8 +43,11 @@ description: >- | Module route | `@route("/api/[module]")` | `route('/api/[module]')(Cls)` | `[Route("/api/[module]")]` | | Convention HTTP | `def get(self)` | `get()` method | `Get()` method | | Custom HTTP | `@http_get("path/[action]")` | `httpGet('path/[action]')(proto.method)` | `[HttpGet("path/[action]")]` | +| Middleware | `middleware.py` factories | factories in `index.js` | `Middleware.cs` | +| Permissions | `permissions=` / `require_permissions` | `permissions` / `requirePermissions` | `PermissionTypes` / `RequirePermissions` | | OpenAPI / Swagger | `app.py` + `api_types.rs` | `buildOpenApi` in `index.js` | `Swagger.cs` | | Version navbar | per-version OpenAPI routes | same | same | +| Template routes | omit from OpenAPI | omit | omit | ## Verification commands @@ -34,10 +56,13 @@ cargo test -p fusion-core naming cargo check -p fusion-py node --check crates/fusion-node/index.js dotnet build bindings/csharp/FusionFramework/FusionFramework.csproj -python -m pytest tests/python -q +./tests/scripts/run-python.sh -q +./tests/scripts/run-node.sh +./tests/scripts/run-csharp.sh -q ``` ## Style - Match existing naming in each language (snake_case Python, camelCase Node helpers, PascalCase C#). - Prefer minimal diffs; do not refactor unrelated binding code. +- Comment new exported helpers (see `fusion-coding-standards`). diff --git a/.agents/skills/fusion-cli/SKILL.md b/.agents/skills/fusion-cli/SKILL.md new file mode 100644 index 0000000..7575ea9 --- /dev/null +++ b/.agents/skills/fusion-cli/SKILL.md @@ -0,0 +1,167 @@ +--- +name: fusion-cli +description: >- + Documents the Fusion Tool CLI (fusion init, command, module, add, update), + the scaffolded project tree, and how the CLI relates to this framework repo. + Use when explaining project layout, scaffolding, env JSON, or when framework + API changes must stay compatible with fusion-tool generators. +--- + +# Fusion CLI (fusion-tool) + +Apps are usually created with **Fusion Tool** (`fusion`), a separate repo: +https://github.com/cipherunits/fusion-tool + +This skill describes the CLI from the **framework** side so agents know what +generated projects look like and what must stay compatible. + +## Install & entry + +```bash +fusion --help +fusion --version +``` + +Binary name: `fusion`. Source of truth for generators: `fusion-tool` (`src/command/`, `src/setting/structure.rs`, `src/setting/environment.rs`). + +## Commands overview + +| Command | Purpose | +|---------|---------| +| `fusion init` | Scaffold a new Fusion app (Python / TypeScript / ASP.NET Core) | +| `fusion command ` | Run a named command from `fusion..json` | +| `fusion load-env` | Load `fusion..json` into the process environment | +| `fusion module init` | Scaffold a **publishable library package** (not an app route module) | +| `fusion add --github OWNER/REPO` | Vendor a module into the current app | +| `fusion update` | Self-update the CLI binary | + +### `fusion init` + +```bash +fusion init +fusion init my-app +fusion init --lang python --name myproject --description "…" +``` + +| Flag / arg | Values | +|------------|--------| +| `[DIRECTORY]` | Target dir (created if missing; default = cwd) | +| `--lang` | `python`, `typescript`, `asp-core` | +| `--name` | Project name | +| `--description` | Short description | + +Writes `fusion-framework.toml`, `fusion.{dev,stage,prod}.json`, `.gitignore`, language entrypoint, sample route module, templates, and dependency pins to the framework version. + +### `fusion command` + +Commands live under the `commands` object in `fusion..json`. + +```bash +fusion command run # default env: FUSION_ENV or `dev` +fusion command run --stage +fusion command run:stage # same +fusion command run --prod +fusion command run --env test +fusion command --stage # list commands for that env +``` + +Runs via the shell from the project root with `FUSION_ENV` set so `core/settings` loads the matching file. + +### Modules vs route modules + +| Term | Meaning | +|------|---------| +| Route module | App code: `FusionBaseApi` / template under `src/modules/…` | +| Library module | Separate package from `fusion module init` (`fusion.module.toml`), installed with `fusion add` | + +Do not confuse the two when naming APIs or writing docs. + +### `fusion module init` / `fusion add` + +```bash +fusion module init --lang python --name example --description "…" +fusion add --github OWNER/MODULE_NAME +fusion add --github OWNER/MODULE_NAME@v1.0.0 +``` + +Vendors under `.fusion/modules//` and records `[[modules]]` in `fusion-framework.toml`. + +## Scaffolded app layout (`fusion init`) + +Python shown; TypeScript/C# use the same tree with language extensions. + +```text +/ +├── core/ +│ └── settings.py # Overlay (RELOAD, TEMPLATES_DIR, …) +├── src/ +│ └── modules/ +│ └── products/ +│ └── products.py # HomePage (template) + ProductModule (API) +├── templates/ +│ └── home/ +│ ├── index.html +│ └── style.css +├── main.py # Register middleware + FusionApp.listen() +├── requirements.txt # Python pin (or package.json / *.csproj) +├── pyproject.toml # Python only +├── fusion-framework.toml # Project metadata + tool/framework versions +├── fusion.dev.json # env=dev, port 8080, swagger on, reload +├── fusion.stage.json # port 8081 +├── fusion.prod.json # port 9090 +└── .gitignore +``` + +TypeScript: `main.ts`, `core/settings.ts`, `package.json`, `tsconfig.json`. +C# (`asp-core`): `main.cs`, `*.csproj` (`net10.0`), `[Route]` / `[HttpGet]`. + +### What the starter demonstrates + +- `FusionBaseTemplate` at `/` (Tera templates; **not** listed in Swagger). +- `FusionBaseApi` at `api/[module]` with `version="v1"` → `/v1/api/product/…`. +- Convention verbs (`get` / `post` / …) plus one custom slot (`http_get` / `httpGet` / `[HttpGet]` with `[action]`). +- Opt-in middleware list in `main` (e.g. `request_id`, `cors`, `cache_headers`, `security_headers`, `framework_headers`). Framework does **not** auto-enable middleware; the scaffold opts in. + +### Default ports + +| Env | Port | +|-----|------| +| dev | 8080 | +| stage | 8081 | +| prod | 9090 | + +### Environment JSON shape + +```json +{ + "env": "dev", + "config": { + "host": "127.0.0.1", + "port": 8080, + "debug": true, + "fingerprint": { "enabled": false }, + "swagger": { "enabled": true, "path": "/swagger" } + }, + "commands": { + "run": "python main.py" + } +} +``` + +`FUSION_ENV` selects `fusion..json` (default `dev`). Unresolved `HOST` placeholders must not crash listen — framework resolves safe defaults. + +## Compatibility duties (framework ↔ CLI) + +When changing public Fusion APIs used by scaffolds: + +1. Prefer keeping generated starter patterns working (or update **fusion-tool** templates in a follow-up / paired PR). +2. Do not invent decorators/config keys that only exist in one binding. +3. After middleware / route / settings changes, check whether `fusion-tool` `structure.rs` / `environment.rs` comments or defaults need updates. +4. Version pin in the CLI (`FUSION_FRAMEWORK_VERSION`) is separate from this repo’s version bump (`./scripts/set-version.sh`). + +## Related + +- Framework layout: `fusion-architecture` +- Binding alignment: `fusion-bindings-parity` +- Routes: `fusion-http-routes` +- CLI repo skills (if editing the tool itself): fusion-tool `.agents/skills/fusion-cli*` diff --git a/.agents/skills/fusion-coding-standards/SKILL.md b/.agents/skills/fusion-coding-standards/SKILL.md new file mode 100644 index 0000000..321c0ae --- /dev/null +++ b/.agents/skills/fusion-coding-standards/SKILL.md @@ -0,0 +1,82 @@ +--- +name: fusion-coding-standards +description: >- + Coding standards for Fusion Framework: function comments, clarifying + comments for complex code, preferred tests, failure investigation, and when + to update skills/docs. Use when writing or reviewing code in this repo. +--- + +# Coding standards + +## Comments + +### Functions / methods + +Every **new** public or non-trivial private function, method, or exported helper must have a one-line (or short) comment/docstring that states **what it does**. + +| Language | Prefer | +|----------|--------| +| Python | Docstring or `#` above `def` | +| JavaScript | `/** … */` or `//` above the function | +| C# | `/// ` or `//` above the member | +| Rust | `///` for public items; `//` for local helpers when non-obvious | + +Do not restate the name alone (`// get user` on `get_user`). Say the behavior or contract. + +### Dense / hard sections + +When code becomes branching-heavy, protocol-sensitive, or easy to break (OpenAPI fill, middleware chain, route slot mounting, FFI): + +- Add short **why** comments at the tricky points. +- Prefer extracting a named helper with a docstring over a wall of uncommented logic. + +## Tests (prefer writing them) + +When you add behavior: + +1. Prefer a test under `tests/python/`, `tests/node/`, `tests/csharp/`, or Rust `#[cfg(test)]`. +2. Mirror coverage across bindings when the feature is cross-binding (see `fusion-bindings-parity`). +3. Run the relevant script from `fusion-testing` before claiming done. + +### When a test fails + +1. Read the failure output (assertion, path, expected vs actual). +2. Trace to implementation (wrong name stripping, async not awaited, header case, version prefix, etc.). +3. Tell the user **what broke and why** in plain language. +4. Fix the code or the incorrect expectation — never silently weaken assertions without saying so. + +## Git + +- Stage **individual files only**. Never `git add .` / `git add -A`. +- Only commit when the user asks. + +## Docs & skills hygiene + +After introducing something agents or developers must know later: + +| Change type | Update | +|-------------|--------| +| New public API / middleware / route option | Binding parity + **examples in all three languages** (`examples/.py`, `.mjs`, `.cs`) + relevant skill | +| New test layout or runner | `tests/README.md` + `fusion-testing` | +| CLI-facing scaffold contract | `fusion-cli` skill; coordinate with fusion-tool if generators break | +| Entirely new workflow | New `.agents/skills//SKILL.md` + row in `.agents/README.md` | + +### Examples (required for new features) + +Always add side-by-side usage demos so humans/agents can see the API shape: + +```text +examples/.py +examples/.mjs +examples/.cs +``` + +Follow existing trios (`custom_http_routes.*`, `pagination.*`). Do not leave a language without an example when the feature exists in that binding. + +Keep skills concise; link to code paths instead of pasting large dumps. + +## Style reminders + +- Match existing naming (snake_case Python, camelCase Node, PascalCase C#). +- Minimal diffs; no drive-by refactors. +- Shared logic → `fusion-core` when possible. diff --git a/.agents/skills/fusion-http-routes/SKILL.md b/.agents/skills/fusion-http-routes/SKILL.md index 1d9a48c..b68186b 100644 --- a/.agents/skills/fusion-http-routes/SKILL.md +++ b/.agents/skills/fusion-http-routes/SKILL.md @@ -64,3 +64,7 @@ public class UserModule : FusionBaseApi { - Rust: `cargo test -p fusion-core naming` - Python: `pytest tests/python` +- Node: `./tests/scripts/run-node.sh` +- C#: `./tests/scripts/run-csharp.sh` + +Cross-binding route/Swagger changes must update **all three** languages (`fusion-bindings-parity`). diff --git a/.agents/skills/fusion-release/SKILL.md b/.agents/skills/fusion-release/SKILL.md index e3d92c7..f32d243 100644 --- a/.agents/skills/fusion-release/SKILL.md +++ b/.agents/skills/fusion-release/SKILL.md @@ -34,3 +34,5 @@ Updates (when present): - Keep all bindings on the **same version** for a release. - Do not commit `bin/`, `obj/`, or `.pdb` artifacts from local `dotnet build`. - Changelog/README updates only when the user requests documentation. +- Stage release files **individually** (never `git add .`). +- After a release that changes public APIs used by scaffolds, note whether fusion-tool’s `FUSION_FRAMEWORK_VERSION` / templates need a bump (see `fusion-cli`). diff --git a/.agents/skills/fusion-testing/SKILL.md b/.agents/skills/fusion-testing/SKILL.md index 31779ec..5a8a07e 100644 --- a/.agents/skills/fusion-testing/SKILL.md +++ b/.agents/skills/fusion-testing/SKILL.md @@ -2,12 +2,25 @@ name: fusion-testing description: >- Runs Fusion Framework verification across Rust, Python, Node, and C#. Use - after code changes, before commits, or when CI-like validation is needed - locally. + after code changes, before commits, when CI-like validation is needed, or + when tests fail and need root-cause investigation. --- # Testing & Verification +## Prefer writing tests + +New behavior should land with tests when practical: + +| Layer | Where | +|-------|--------| +| Rust shared logic | `#[cfg(test)]` in `crates/fusion-core` (and related crates) | +| Python | `tests/python/unit/` (pytest) | +| Node | `tests/node/unit/*.test.js` (`node --test`) | +| C# | `tests/csharp/FusionFramework.Tests/` (xUnit) | + +Do **not** put `test_*.py` under `crates/fusion-py/python/fusion_framework/`. + ## Full suite (recommended) ```bash @@ -26,15 +39,28 @@ dotnet build bindings/csharp/FusionFramework/FusionFramework.csproj ## Python (pytest) -Tests live under `tests/python/` (not inside the installable package). - ```bash ./scripts/dev-install-python.sh --venv .venv source .venv/bin/activate -pytest # uses pytest.ini at repo root +pytest +./tests/scripts/run-python.sh pytest tests/python/unit/test_http_route.py -q ``` +## Node + +```bash +cd crates/fusion-node && npm install && npm run build:debug +./tests/scripts/run-node.sh +``` + +## C# + +```bash +./tests/scripts/run-csharp.sh +# builds fusion-ffi then: dotnet test … -c Release +``` + ## Full Rust workspace ```bash @@ -47,21 +73,26 @@ cargo test --workspace | Binding | Command | |---------|---------| | Python | `pytest tests/python` (after `dev-install-python.sh`) | -| Node | `node --check crates/fusion-node/index.js` | -| C# | `dotnet build bindings/csharp/FusionFramework/FusionFramework.csproj` | +| Node | `./tests/scripts/run-node.sh` | +| C# | `./tests/scripts/run-csharp.sh` | ## Layout See `tests/README.md` for folder structure and conventions. +## When tests fail (required) + +1. **Investigate** — read assertion, stack, expected vs actual. +2. **Explain** — tell the user the root cause (e.g. class name → `[module]` stripping, async middleware not awaited, CORS header casing, version prefix missing). +3. **Fix** — correct implementation or wrong expectation; do not hide failures. +4. Order of suspicion for route/OpenAPI issues: + - `fusion-core` naming + - Python registry (`api_types.rs`) as reference + - Node / C# mount + OpenAPI fill + ## What to exclude from git - `bindings/csharp/**/bin/`, `obj/` - `target/`, `node_modules/`, `__pycache__/`, `.pytest_cache/` - Local `.pdb` changes from debug builds - -## When tests fail - -1. Fix `fusion-core` first if naming/route tests fail. -2. Then fix Python registry (`api_types.rs`) — often the reference implementation. -3. Align Node and C# mount/OpenAPI with Python behavior. +- Scratch `.nuget/` / `.tmp/` caches if created locally diff --git a/.cursor/rules/fusion-engineering.mdc b/.cursor/rules/fusion-engineering.mdc new file mode 100644 index 0000000..2f4c2c8 --- /dev/null +++ b/.cursor/rules/fusion-engineering.mdc @@ -0,0 +1,53 @@ +--- +description: Core Fusion Framework engineering rules — parity, comments, tests, git, docs +alwaysApply: true +--- + +# Fusion engineering (always) + +## Cross-binding parity + +When the user asks to add or change a framework feature (routes, middleware, permissions, Swagger, settings, etc.), implement it in **all three** bindings in the same change set unless they explicitly limit scope: + +- Python (`crates/fusion-py/`) +- Node (`crates/fusion-node/`) +- C# (`bindings/csharp/FusionFramework/`) + +Shared semantics belong in `crates/fusion-core/` first. Follow the `fusion-bindings-parity` skill. + +## Examples (every new public feature) + +Whenever a **new** user-facing feature or API is added (middleware, route options, permissions, pagination helpers, settings keys, etc.), add **usage examples in all three languages** under `examples/` so the shape is visible side by side: + +- Python: `examples/.py` +- Node: `examples/.mjs` +- C#: `examples/.cs` (or a small folder if a project is required) + +Match existing naming (`custom_http_routes.py` / `.mjs` / `.cs`, `pagination.py` / `.mjs` / `.cs`). Extend an existing trio when the feature fits; otherwise create a new trio. Do not ship Python-only demos for cross-binding APIs. + +## Comments + +- Every **new or meaningfully changed** function/method must have a short comment or docstring stating what it does. +- When logic is dense, non-obvious, or branched, add inline comments that explain **why**, not just what the syntax does. + +## Tests + +- Prefer adding or updating tests for new behavior under `tests/` (Python / Node / C#) or `#[cfg(test)]` in Rust. +- If a test fails: **investigate**, report the root cause clearly, then fix. Do not ignore or skip without explaining why. + +## Git staging + +- **Never** run `git add .` or `git add -A` / `git add --all`. +- Stage files **one path at a time** (`git add path/to/file`). + +## Documentation & skills + +When you add a user-facing or agent-facing concept (new CLI-related behavior, new API surface, new test layout, new workflow): + +- Update the relevant skill under `.agents/skills/` and/or `.agents/README.md`. +- Add a new skill if no existing one covers it. +- Keep framework docs/examples aligned when public API changes. + +## Related skills + +Read when relevant: `fusion-architecture`, `fusion-bindings-parity`, `fusion-http-routes`, `fusion-testing`, `fusion-cli`, `fusion-coding-standards`, `fusion-release`.