Files
noteflow/scripts/run_cupcake_tests.sh
2026-01-24 10:40:30 +00:00

191 lines
7.7 KiB
Bash
Executable File

#!/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!"