ed9bade863
project-files/: - CLAUDE.md.template (266 lines) - docs/Pravila_raboty_Claude.template.md (720 lines) - docs/Plugin_stack_rules.template.md (916 lines) - docs/Tooling.template.md (613 lines) - docs/CHANGELOG_claude_md.template.md - docs/visualizations/hooks-skills-plugins-map.html (3122 lines) - .mcp.json.template (universal: playwright/github/semgrep; laravel-boost dropped) user-level-files/: - hooks/ (10 Python files: skill-marker, skill-check, economy-* x8) - settings-fragment.json (enabledPlugins + permissions + hooks only) - marketplaces.json (3 sources) - plugins-manifest.json (4 plugins pinned with gitCommitSha) - mcp-user.template.json (magic with <<MAGIC_API_KEY>> placeholder) Gitleaks scan: 0 findings. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
158 lines
6.7 KiB
Python
158 lines
6.7 KiB
Python
"""Permanent test suite for economy-mode hook.
|
||
|
||
Tests via subprocess to verify end-to-end behavior including stdin
|
||
encoding, regex parsing, discussion-context filtering, and multi-match
|
||
handling. Run with: python ~/.claude/hooks/economy-mode-test.py
|
||
|
||
Exit code 0 = all green, 1 = any failure."""
|
||
import json
|
||
import os
|
||
import re
|
||
import subprocess
|
||
import sys
|
||
|
||
try:
|
||
sys.stdout.reconfigure(encoding="utf-8")
|
||
except Exception:
|
||
pass
|
||
|
||
SCRIPT = os.path.expanduser("~/.claude/hooks/economy-mode.py")
|
||
|
||
|
||
def parse_level(prompt):
|
||
"""Run hook with given prompt. Return:
|
||
- int 0-100 if explicit activation
|
||
- None if default (no keyword matched, or matched in discussion context)
|
||
"""
|
||
payload = json.dumps({"prompt": prompt}, ensure_ascii=False).encode("utf-8")
|
||
r = subprocess.run(
|
||
["python", SCRIPT],
|
||
input=payload,
|
||
capture_output=True,
|
||
timeout=10,
|
||
)
|
||
if not r.stdout:
|
||
return None
|
||
try:
|
||
d = json.loads(r.stdout.decode("utf-8"))
|
||
ctx = d["hookSpecificOutput"]["additionalContext"]
|
||
except Exception:
|
||
return None
|
||
# "(default" or "не указал уровень" both indicate non-explicit
|
||
if "не указал уровень" in ctx or "(default" in ctx:
|
||
return None
|
||
m = re.search(r"ECONOMY MODE: (\d+)%", ctx)
|
||
return int(m.group(1)) if m else None
|
||
|
||
|
||
# (prompt, expected_level_or_None, description)
|
||
TESTS = [
|
||
# --- Russian inflection: ALL forms must activate ---
|
||
("экономия 75%", 75, "Nominative"),
|
||
("экономии 75%", 75, "Genitive"),
|
||
("экономию 75%", 75, "Accusative"),
|
||
("экономией 75%", 75, "Instrumental"),
|
||
("экономиями 75%", 75, "Plural instrumental"),
|
||
("Экономия 75%", 75, "Capitalized"),
|
||
("ЭКОНОМИЯ 75%", 75, "All caps"),
|
||
|
||
# --- Separators: must accept space, colon, dash, em-dash, equals, comma, parens ---
|
||
("экономия 75%", 75, "Space sep"),
|
||
("экономия: 75%", 75, "Colon sep"),
|
||
("экономия - 75%", 75, "Hyphen sep"),
|
||
("экономия — 75%", 75, "Em-dash sep"),
|
||
("экономия = 75%", 75, "Equals sep"),
|
||
("экономия,75%", 75, "Comma sep"),
|
||
("экономия75%", 75, "No sep (digit right after)"),
|
||
("экономия (75%)", 75, "Parens"),
|
||
|
||
# --- Numbers: integer, decimal, with/without space before % ---
|
||
("экономия 0%", 0, "Zero"),
|
||
("экономия 100%", 100, "Hundred"),
|
||
("экономия 75 %", 75, "Space before %"),
|
||
("экономия 75.5%", 75, "Decimal point"),
|
||
("экономия 75,5%", 75, "Decimal comma"),
|
||
("экономия 75.0%", 75, "Trailing .0"),
|
||
("экономия 0.0%", 0, "0.0"),
|
||
("экономия 200%", 100, "Out of range — clamp 100"),
|
||
|
||
# --- Word boundary: must NOT match when preceded by word char ---
|
||
("1экономия 75%", None, "Preceded by digit"),
|
||
("пэкономия 75%", None, "Preceded by Cyrillic letter"),
|
||
|
||
# --- Discussion contexts: must NOT activate ---
|
||
("как работает экономия 75%?", None, "Question with ?"),
|
||
("что даст экономия 75%", None, "'что даст' prefix"),
|
||
("что покрывает экономия 0%", None, "'что покрывает' prefix"),
|
||
("что такое экономия 75%", None, "'что такое' prefix"),
|
||
("не активируй экономия 75%", None, "Negation 'не'"),
|
||
("забудь про экономия 75%", None, "'забудь' prefix"),
|
||
("отбой экономия 75%", None, "'отбой' prefix"),
|
||
("пример: экономия 75%", None, "'пример' prefix"),
|
||
|
||
# --- Multi-match: last non-discussion match wins ---
|
||
("экономия 75%, потом экономия 0%", 0, "Last match wins"),
|
||
("не экономия 75%, а экономия 0%", 0, "Skip negated first, take last"),
|
||
("экономия 75% (передумал) экономия 0%", 0, "Mid-prompt change"),
|
||
|
||
# --- User's actual command from this turn ---
|
||
(
|
||
"тестирую все и снести изменения в хук, что он должен делать "
|
||
"при команде экономия 0% все для максимального результата и с "
|
||
"максимальным свеобъемливающим качеством. экономия 0%",
|
||
0,
|
||
"User's real command (this turn)",
|
||
),
|
||
|
||
# --- Empty / edge cases ---
|
||
("", None, "Empty"),
|
||
(" ", None, "Whitespace only"),
|
||
("просто задача без ключа", None, "No keyword"),
|
||
("экономия %", None, "Missing number"),
|
||
("75%", None, "Missing keyword"),
|
||
|
||
# === END-OF-PROMPT contract (NEW in v3) ===
|
||
("задача X. экономия 75%", 75, "Trailer style at end"),
|
||
("задача X. экономия 75%.", 75, "End with trailing period"),
|
||
("задача X. экономия 75%!", 75, "End with exclamation"),
|
||
("задача X. экономия 75% ", 75, "End with trailing whitespace"),
|
||
("делай X.\nэкономия 75%", 75, "Trailer on separate last line"),
|
||
("экономия 75% делай задачу X", None, "Pattern in middle, content after"),
|
||
("экономия 75% (срочно) делай X", None, "Pattern in middle with parens"),
|
||
("при команде экономия 75% что-то делать", None, "Pattern in middle of description"),
|
||
("экономия 75% потом экономия 0%", 0, "Last is at end"),
|
||
("экономия 0% (передумал) экономия 75% работать", None, "Last not at end"),
|
||
|
||
# === Subset of v2 tests revisited ===
|
||
("экономия 75%, потом экономия 0%", 0, "Last wins (still applies)"),
|
||
("не экономия 75%, а экономия 0%", 0, "Last is at end after negation"),
|
||
]
|
||
|
||
|
||
def main() -> int:
|
||
passed, failed, failures = 0, 0, []
|
||
for prompt, expected, desc in TESTS:
|
||
actual = parse_level(prompt)
|
||
ok = actual == expected
|
||
status = "PASS" if ok else "FAIL"
|
||
# Ascii-safe printing for prompt (truncate)
|
||
short = (prompt[:60] + "...") if len(prompt) > 60 else prompt
|
||
print(f" [{status}] {desc:40s} | exp={expected!s:5s} got={actual!s:5s} | {short!r}")
|
||
if ok:
|
||
passed += 1
|
||
else:
|
||
failed += 1
|
||
failures.append((desc, prompt, expected, actual))
|
||
|
||
print(f"\n=== {passed}/{passed+failed} PASSED, {failed} FAILED ===")
|
||
if failures:
|
||
print("\nFailures detail:")
|
||
for desc, prompt, exp, got in failures:
|
||
print(f" {desc}: expected={exp}, got={got}")
|
||
print(f" prompt={prompt!r}")
|
||
return 0 if failed == 0 else 1
|
||
|
||
|
||
if __name__ == "__main__":
|
||
sys.exit(main())
|