#!/bin/bash # Cupcake Policy Test Suite for NoteFlow # Tests the .cupcake/policies/opencode/ policy rules set -euo pipefail SCRIPT_DIR="$(dirname "$0")" REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" cd "$SCRIPT_DIR" TMP_DIR=$(mktemp -d) trap 'rm -rf "$TMP_DIR"' EXIT PASSED=0 FAILED=0 FAILED_TESTS="" CWD="$REPO_ROOT" run_test() { local name="$1" local expected="$2" local tool="$3" local args_json="$4" local test_file="$TMP_DIR/${name}.json" # Build JSON using jq for proper escaping jq -n \ --arg tool "$tool" \ --argjson args "$args_json" \ --arg cwd "$CWD" \ '{ hook_event_name: "PreToolUse", tool: $tool, args: $args, session_id: "test", cwd: $cwd }' > "$test_file" local output output=$(cd "$REPO_ROOT" && cupcake eval --harness opencode < "$test_file" 2>/dev/null | grep -E "^\{" | head -1) || true local decision decision=$(echo "$output" | jq -r '.decision // "error"' 2>/dev/null) || decision="error" if [[ "$decision" == "$expected" ]]; then echo "PASS: $name" PASSED=$((PASSED + 1)) else echo "FAIL: $name (expected=$expected, got=$decision)" FAILED=$((FAILED + 1)) FAILED_TESTS="${FAILED_TESTS}\n - $name" fi } echo "Running Cupcake Policy Tests..." echo "================================" # === block_no_verify.rego === run_test "block_no_verify_deny" "deny" "bash" \ '{"command": "git commit --no-verify"}' run_test "block_no_verify_allow" "allow" "bash" \ '{"command": "git commit -m fix"}' # === block_makefile_edit.rego === run_test "block_makefile_edit_deny" "deny" "edit" \ "{\"file_path\": \"$CWD/Makefile\", \"new_string\": \"test:\"}" # === block_linter_config_edit.rego === run_test "block_linter_config_pyproject_deny" "deny" "edit" \ "{\"file_path\": \"$CWD/pyproject.toml\", \"new_string\": \"[tool]\"}" run_test "block_linter_config_pyrightconfig_deny" "deny" "edit" \ "{\"file_path\": \"$CWD/pyrightconfig.json\", \"new_string\": \"{}\"}" run_test "block_linter_config_biome_deny" "deny" "edit" \ "{\"file_path\": \"$CWD/client/biome.json\", \"new_string\": \"{}\"}" run_test "block_linter_config_tsconfig_deny" "deny" "edit" \ "{\"file_path\": \"$CWD/client/tsconfig.json\", \"new_string\": \"{}\"}" # === block_tests_quality_edit.rego === run_test "block_tests_quality_deny" "deny" "edit" \ "{\"file_path\": \"$CWD/tests/quality/test_smells.py\", \"new_string\": \"#\"}" # === warn_baselines_edit.rego === run_test "warn_baselines_edit_deny" "deny" "edit" \ "{\"file_path\": \"$CWD/tests/quality/baselines.json\", \"new_string\": \"{}\"}" # === block_code_quality_test_edit.rego === run_test "block_code_quality_test_edit_deny" "deny" "edit" \ "{\"file_path\": \"$CWD/src/test/code-quality.test.ts\", \"new_string\": \"//\"}" run_test "block_code_quality_test_bash_deny" "deny" "bash" \ '{"command": "sed -i s/x/y/ src/test/code-quality.test.ts"}' # === block_biome_ignore.rego === run_test "block_biome_ignore_deny" "deny" "edit" \ "{\"file_path\": \"$CWD/client/src/App.tsx\", \"new_string\": \"// biome-ignore lint: reason\"}" run_test "block_ts_ignore_deny" "deny" "edit" \ "{\"file_path\": \"$CWD/client/src/App.tsx\", \"new_string\": \"// @ts-ignore\"}" run_test "block_biome_ignore_allow" "allow" "edit" \ "{\"file_path\": \"$CWD/client/src/App.tsx\", \"new_string\": \"const x = 1;\"}" run_test "block_biome_ignore_bash_deny" "deny" "bash" \ '{"command": "echo // @ts-ignore >> client/src/App.tsx"}' # === block_magic_numbers.rego === run_test "block_magic_numbers_deny" "deny" "edit" \ "{\"file_path\": \"$CWD/src/noteflow/service.py\", \"new_string\": \"timeout = 30\"}" run_test "block_magic_numbers_constants_allow" "allow" "edit" \ "{\"file_path\": \"$CWD/src/noteflow/constants.py\", \"new_string\": \"TIMEOUT = 30\"}" # === ban_stdlib_logger.rego === run_test "ban_stdlib_logger_deny" "deny" "write" \ "{\"file_path\": \"$CWD/src/noteflow/test.py\", \"content\": \"import logging\\nlogger = logging.getLogger(__name__)\"}" run_test "ban_stdlib_logger_allow" "allow" "write" \ "{\"file_path\": \"$CWD/src/noteflow/test.py\", \"content\": \"from noteflow.log import get_logger\"}" # === prevent_any_type.rego === run_test "prevent_any_type_deny" "deny" "write" \ "{\"file_path\": \"$CWD/src/noteflow/types.py\", \"content\": \"from typing import Any\\ndef f(x: Any): pass\"}" run_test "prevent_any_type_allow" "allow" "write" \ "{\"file_path\": \"$CWD/src/noteflow/types.py\", \"content\": \"from typing import TypeVar\\nT = TypeVar(\\\"T\\\")\"}" run_test "prevent_any_type_edit_newString_deny" "deny" "edit" \ "{\"filePath\": \"$CWD/test_any_import.py\", \"oldString\": \"def store(items: dict[str, Any]) -> None:\\n pass\", \"newString\": \"from typing import Any\\n\\n\"}" # === prevent_type_suppression.rego === run_test "prevent_type_suppression_type_ignore_deny" "deny" "edit" \ "{\"file_path\": \"$CWD/src/noteflow/service.py\", \"new_string\": \"x = val # type: ignore\"}" run_test "prevent_type_suppression_pyright_deny" "deny" "edit" \ "{\"file_path\": \"$CWD/src/noteflow/service.py\", \"new_string\": \"x = val # pyright: ignore\"}" run_test "prevent_type_suppression_noqa_deny" "deny" "edit" \ "{\"file_path\": \"$CWD/src/noteflow/service.py\", \"new_string\": \"from x import * # noqa\"}" run_test "prevent_type_suppression_ruff_noqa_deny" "deny" "edit" \ "{\"file_path\": \"$CWD/src/noteflow/service.py\", \"new_string\": \"# ruff: noqa: F401, E501\"}" run_test "prevent_type_suppression_allow" "allow" "edit" \ "{\"file_path\": \"$CWD/src/noteflow/service.py\", \"new_string\": \"x: str = val\"}" # === block_broad_exception_handler.rego === run_test "block_broad_exception_handler_deny" "deny" "write" \ "{\"file_path\": \"$CWD/src/noteflow/handler.py\", \"content\": \"try:\\n x()\\nexcept Exception as e:\\n logger.error(e)\"}" run_test "block_broad_exception_handler_allow" "allow" "write" \ "{\"file_path\": \"$CWD/src/noteflow/handler.py\", \"content\": \"try:\\n x()\\nexcept ValueError as e:\\n raise\"}" # === block_silent_none_return.rego === run_test "block_silent_none_return_deny" "deny" "write" \ "{\"file_path\": \"$CWD/src/noteflow/fetch.py\", \"content\": \"try:\\n return get()\\nexcept NetworkError as e:\\n logger.error(e)\\n return None\"}" # === block_datetime_now_fallback.rego === run_test "block_datetime_now_fallback_deny" "deny" "write" \ "{\"file_path\": \"$CWD/src/noteflow/time.py\", \"content\": \"def ts():\\n return datetime.now()\"}" # === block_assertion_roulette.rego === run_test "block_assertion_roulette_deny" "deny" "write" \ "{\"file_path\": \"$CWD/tests/test_example.py\", \"content\": \"assert a == 1\\nassert b == 2\"}" # === block_test_loops.rego === run_test "block_test_loops_deny" "deny" "write" \ "{\"file_path\": \"$CWD/tests/test_items.py\", \"content\": \"def test_all():\\n for i in items:\\n assert i.valid\"}" # === block_duplicate_fixtures.rego === run_test "block_duplicate_fixtures_deny" "deny" "write" \ "{\"file_path\": \"$CWD/tests/unit/test_x.py\", \"content\": \"import pytest\\n\\n@pytest.fixture\\ndef mock_uow():\\n return None\"}" run_test "block_duplicate_fixtures_conftest_allow" "allow" "write" \ "{\"file_path\": \"$CWD/tests/conftest.py\", \"content\": \"import pytest\\n\\n@pytest.fixture\\ndef custom():\\n return None\"}" echo "" echo "========================================" echo "Results: $PASSED passed, $FAILED failed" echo "========================================" if [[ $FAILED -gt 0 ]]; then echo -e "\nFailed tests:$FAILED_TESTS" exit 1 fi echo "All tests passed!"