Download scripts/test-protocol-processor.py from arach/training-lab: direct link, hf CLI and curl.
- Browser
- Download file 13.8 kB
-
https://huggingface.co/arach/training-lab/resolve/5f2705b4e16401cc00a1249883d3e56ffecf2b03/scripts/test-protocol-processor.py
- Command line
-
hf download hf://arach/training-lab@5f2705b4e16401cc00a1249883d3e56ffecf2b03/scripts/test-protocol-processor.py
-
curl -L -o test-protocol-processor.py https://huggingface.co/arach/training-lab/resolve/5f2705b4e16401cc00a1249883d3e56ffecf2b03/scripts/test-protocol-processor.py
13.8 kB
| #!/usr/bin/env python3 | |
| """Test harness for the procedural dictation protocol processor. | |
| Tests the deterministic symbol/number/casing engine that converts | |
| dictated protocol words into actual syntax β no LLM needed. | |
| Covers: | |
| - Symbol mapping: "dash" β "-", "dot" β ".", "slash" β "/", etc. | |
| - Two-word symbols: "open paren" β "(", "dash dash" β "--", etc. | |
| - Three-word symbols: "two redirect ampersand" β "2>&" | |
| - Number words: "eight zero eight zero" β "8080", "forty two" β "42" | |
| - Casing: "capital A" β "A", "all caps POST" β "POST" | |
| - Casing directives: "camel case get user" β "getUser" | |
| - Spacing: "space" as explicit token separator | |
| - Full commands: git, docker, curl, ssh, etc. | |
| Usage: | |
| python3 test-protocol-processor.py # run full suite | |
| python3 test-protocol-processor.py --category git # filter by category | |
| python3 test-protocol-processor.py --verbose # show all cases, not just errors | |
| python3 test-protocol-processor.py --unit # run unit tests only (no eval file) | |
| python3 test-protocol-processor.py --eval FILE # use custom eval file | |
| """ | |
| import json | |
| import re | |
| import sys | |
| import time | |
| from pathlib import Path | |
| from collections import defaultdict | |
| # Import processor | |
| sys.path.insert(0, str(Path(__file__).parent)) | |
| from importlib import import_module | |
| _proc = import_module("procedural-processor") | |
| process_dictation = _proc.process_dictation | |
| consume_number = _proc.consume_number | |
| consume_casing = _proc.consume_casing | |
| SYMBOLS = _proc.SYMBOLS | |
| TWO_WORD_SYMBOLS = _proc.TWO_WORD_SYMBOLS | |
| THREE_WORD_SYMBOLS = _proc.THREE_WORD_SYMBOLS | |
| # ββ Colors ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| GREEN = "\033[92m" | |
| RED = "\033[91m" | |
| YELLOW = "\033[93m" | |
| DIM = "\033[2m" | |
| BOLD = "\033[1m" | |
| RESET = "\033[0m" | |
| # ββ Unit Tests ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # These test individual features of the processor in isolation. | |
| UNIT_TESTS = { | |
| "symbols_basic": [ | |
| ("dash", "-"), | |
| ("dot", "."), | |
| ("slash", "/"), | |
| ("pipe", "|"), | |
| ("tilde", "~"), | |
| ("hash", "#"), | |
| ("at", "@"), | |
| ("dollar", "$"), | |
| ("percent", "%"), | |
| ("caret", "^"), | |
| ("ampersand", "&"), | |
| ("equals", "="), | |
| ("plus", "+"), | |
| ("colon", ":"), | |
| ("semicolon", ";"), | |
| ("underscore", "_"), | |
| ("comma", ","), | |
| ("backslash", "\\"), | |
| ("quote", '"'), | |
| ("backtick", "`"), | |
| ("star", "*"), | |
| ("bang", "!"), | |
| ("redirect", ">"), | |
| ], | |
| "symbols_synonyms": [ | |
| ("minus", "-"), | |
| ("hyphen", "-"), | |
| ("period", "."), | |
| ("asterisk", "*"), | |
| ("hashtag", "#"), | |
| ], | |
| "two_word_symbols": [ | |
| ("single quote", "'"), | |
| ("open paren", "("), | |
| ("close paren", ")"), | |
| ("open brace", "{"), | |
| ("close brace", "}"), | |
| ("open bracket", "["), | |
| ("close bracket", "]"), | |
| ("open angle", "<"), | |
| ("close angle", ">"), | |
| ("open curly", "{"), | |
| ("close curly", "}"), | |
| ("less than", "<"), | |
| ("question mark", "?"), | |
| ("dash dash", "--"), | |
| ("double dash", "--"), | |
| ("and and", "&&"), | |
| ("pipe pipe", "||"), | |
| ("dot dot", ".."), | |
| ("forward slash", "/"), | |
| ("back slash", "\\"), | |
| ("equals sign", "="), | |
| ("at sign", "@"), | |
| ("dollar sign", "$"), | |
| ("open parenthesis", "("), | |
| ("close parenthesis", ")"), | |
| ("new line", "\n"), | |
| ], | |
| "three_word_symbols": [ | |
| ("two redirect ampersand", "2>&"), | |
| ], | |
| "numbers_single": [ | |
| ("zero", "0"), | |
| ("one", "1"), | |
| ("five", "5"), | |
| ("nine", "9"), | |
| ("ten", "10"), | |
| ("twelve", "12"), | |
| ("nineteen", "19"), | |
| ], | |
| "numbers_compound": [ | |
| ("twenty", "20"), | |
| ("forty two", "42"), | |
| ("ninety nine", "99"), | |
| ], | |
| "numbers_multiplier": [ | |
| ("one hundred", "100"), | |
| ("three thousand", "3000"), | |
| ("five hundred", "500"), | |
| ], | |
| "numbers_digit_sequence": [ | |
| ("one nine two", "192"), | |
| ("eight zero eight zero", "8080"), | |
| ("one two seven dot zero dot zero dot one", "127.0.0.1"), | |
| ("four four three", "443"), | |
| ], | |
| "casing_capital": [ | |
| ("capital a", "A"), | |
| ("capital x", "X"), | |
| ("capital hello", "Hello"), | |
| ], | |
| "casing_all_caps": [ | |
| ("all caps post", "POST"), | |
| ("all caps get", "GET"), | |
| ("all caps node", "NODE"), | |
| ], | |
| "casing_directives": [ | |
| ("camel case get user profile", "getUserProfile"), | |
| ("snake case api key", "api_key"), | |
| ("pascal case my component", "MyComponent"), | |
| ("kebab case my component", "my-component"), | |
| ("screaming case max retries", "MAX_RETRIES"), | |
| ], | |
| "space_handling": [ | |
| ("git space push", "git push"), | |
| ("ls space dash la", "ls -la"), | |
| ("a space b space c", "a b c"), | |
| ], | |
| "mixed_basic": [ | |
| ("git space commit space dash m space quote hello quote", 'git commit -m "hello"'), | |
| ("echo space dollar all caps home", "echo $HOME"), | |
| ("cd space tilde slash projects", "cd ~/projects"), | |
| ("chmod space seven five five space script dot sh", "chmod 755 script.sh"), | |
| ], | |
| "append_redirect": [ | |
| ("echo space hello space append space log dot txt", "echo hello >> log.txt"), | |
| ("cat space file space redirect space output", "cat file > output"), | |
| ], | |
| } | |
| def run_unit_tests(verbose=False): | |
| """Run isolated unit tests for each processor feature.""" | |
| print(f"\n{BOLD}UNIT TESTS{RESET}") | |
| print("=" * 70) | |
| total = 0 | |
| passed = 0 | |
| failed_cases = [] | |
| for group_name, cases in UNIT_TESTS.items(): | |
| group_pass = 0 | |
| group_total = len(cases) | |
| for dictated, expected in cases: | |
| total += 1 | |
| got = process_dictation(dictated) | |
| # Normalize whitespace for comparison | |
| got_norm = re.sub(r'\s+', ' ', got.strip()) | |
| exp_norm = re.sub(r'\s+', ' ', expected.strip()) | |
| if got_norm == exp_norm: | |
| group_pass += 1 | |
| passed += 1 | |
| if verbose: | |
| print(f" {GREEN}PASS{RESET} [{group_name}] {dictated!r} β {got!r}") | |
| else: | |
| failed_cases.append((group_name, dictated, expected, got)) | |
| print(f" {RED}FAIL{RESET} [{group_name}] {dictated!r}") | |
| print(f" expected: {expected!r}") | |
| print(f" got: {got!r}") | |
| if not verbose and group_pass == group_total: | |
| print(f" {GREEN}PASS{RESET} {group_name}: {group_pass}/{group_total}") | |
| print(f"\n Total: {passed}/{total} passed", end="") | |
| if failed_cases: | |
| print(f", {RED}{len(failed_cases)} failed{RESET}") | |
| else: | |
| print(f" {GREEN}(all pass){RESET}") | |
| return passed, total, failed_cases | |
| def run_eval_tests(eval_file, category=None, verbose=False): | |
| """Run the processor against the eval dataset.""" | |
| print(f"\n{BOLD}EVAL TESTS{RESET} β {eval_file}") | |
| print("=" * 70) | |
| with open(eval_file) as f: | |
| data = json.load(f) | |
| if category: | |
| data = [d for d in data if d.get("category", "") == category] | |
| print(f" Filtered to category={category}: {len(data)} cases") | |
| total = len(data) | |
| exact = 0 | |
| ws_match = 0 | |
| ws_case_match = 0 | |
| failed_cases = [] | |
| by_category = defaultdict(lambda: {"exact": 0, "total": 0}) | |
| timings = [] | |
| for d in data: | |
| dictated = d["dictated"] | |
| expected = d["expected"] | |
| cat = d.get("category", "unknown") | |
| t0 = time.perf_counter() | |
| got = process_dictation(dictated) | |
| elapsed_us = (time.perf_counter() - t0) * 1_000_000 | |
| timings.append(elapsed_us) | |
| got_ws = re.sub(r'\s+', ' ', got.strip()) | |
| exp_ws = re.sub(r'\s+', ' ', expected.strip()) | |
| is_exact = got == expected | |
| is_ws = got_ws == exp_ws | |
| is_wsc = got_ws.lower() == exp_ws.lower() | |
| by_category[cat]["total"] += 1 | |
| if is_exact: | |
| exact += 1 | |
| by_category[cat]["exact"] += 1 | |
| if verbose: | |
| print(f" {GREEN}PASS{RESET} [{cat}] {dictated[:60]}") | |
| else: | |
| failed_cases.append({ | |
| "category": cat, | |
| "dictated": dictated, | |
| "expected": expected, | |
| "got": got, | |
| "ws_match": is_ws, | |
| "wsc_match": is_wsc, | |
| }) | |
| if is_ws: | |
| ws_match += 1 | |
| if verbose: | |
| print(f" {YELLOW}WS{RESET} [{cat}] whitespace diff only") | |
| elif is_wsc: | |
| ws_case_match += 1 | |
| if is_ws: | |
| ws_match += (1 if is_exact else 0) # already counted | |
| if is_wsc: | |
| ws_case_match += (1 if is_exact else 0) | |
| # Recount properly | |
| ws_match = sum(1 for d in data if re.sub(r'\s+', ' ', process_dictation(d["dictated"]).strip()) == re.sub(r'\s+', ' ', d["expected"].strip())) | |
| ws_case_match = sum(1 for d in data if re.sub(r'\s+', ' ', process_dictation(d["dictated"]).strip()).lower() == re.sub(r'\s+', ' ', d["expected"].strip()).lower()) | |
| # Summary | |
| print(f"\n {BOLD}ACCURACY{RESET}") | |
| print(f" Exact match: {exact}/{total} ({exact/total*100:.1f}%)") | |
| print(f" Whitespace-norm: {ws_match}/{total} ({ws_match/total*100:.1f}%)") | |
| print(f" WS+case-norm: {ws_case_match}/{total} ({ws_case_match/total*100:.1f}%)") | |
| # Timing | |
| if timings: | |
| timings.sort() | |
| median = timings[len(timings)//2] | |
| p95 = timings[int(len(timings) * 0.95)] | |
| p99 = timings[int(len(timings) * 0.99)] | |
| print(f"\n {BOLD}TIMING{RESET} (per command)") | |
| print(f" Median: {median:.0f}us P95: {p95:.0f}us P99: {p99:.0f}us") | |
| # Per category | |
| if len(by_category) > 1: | |
| print(f"\n {BOLD}BY CATEGORY{RESET}") | |
| for cat in sorted(by_category.keys()): | |
| r = by_category[cat] | |
| pct = r["exact"] / r["total"] * 100 if r["total"] else 0 | |
| bar = GREEN if pct == 100 else (YELLOW if pct >= 80 else RED) | |
| print(f" {cat:>14}: {bar}{r['exact']}/{r['total']}{RESET} ({pct:.0f}%)") | |
| # Errors | |
| if failed_cases: | |
| print(f"\n {BOLD}FAILURES{RESET} ({len(failed_cases)}):") | |
| print(" " + "-" * 66) | |
| for e in failed_cases[:20]: | |
| ws_note = f" {DIM}(whitespace only){RESET}" if e["ws_match"] else "" | |
| wsc_note = f" {DIM}(case only){RESET}" if e["wsc_match"] and not e["ws_match"] else "" | |
| print(f" [{e['category']}]{ws_note}{wsc_note}") | |
| print(f" input: {e['dictated'][:70]}") | |
| print(f" expected: {e['expected'][:70]}") | |
| print(f" got: {e['got'][:70]}") | |
| # Show character-level diff for near-misses | |
| exp, got = e["expected"], e["got"] | |
| if len(exp) < 80 and len(got) < 80: | |
| diff_pos = next((i for i in range(min(len(exp), len(got))) if exp[i] != got[i]), min(len(exp), len(got))) | |
| if diff_pos < len(exp) or diff_pos < len(got): | |
| print(f" diff at {diff_pos}: expected {exp[diff_pos:diff_pos+10]!r} got {got[diff_pos:diff_pos+10]!r}") | |
| print() | |
| if len(failed_cases) > 20: | |
| print(f" ... and {len(failed_cases) - 20} more") | |
| return exact, total, failed_cases | |
| def main(): | |
| import argparse | |
| parser = argparse.ArgumentParser(description="Test the procedural dictation processor") | |
| parser.add_argument("--unit", action="store_true", help="Run unit tests only") | |
| parser.add_argument("--eval", type=str, default=None, help="Eval JSON file (default: eval-independent.json)") | |
| parser.add_argument("--category", type=str, help="Filter eval cases by category") | |
| parser.add_argument("--verbose", "-v", action="store_true", help="Show all cases, not just failures") | |
| args = parser.parse_args() | |
| print("=" * 70) | |
| print(f" PROTOCOL PROCESSOR TEST HARNESS") | |
| print("=" * 70) | |
| # Always run unit tests | |
| unit_passed, unit_total, unit_failures = run_unit_tests(verbose=args.verbose) | |
| eval_passed = eval_total = 0 | |
| eval_failures = [] | |
| if not args.unit: | |
| eval_file = args.eval or str(Path(__file__).parent / "eval-independent.json") | |
| if Path(eval_file).exists(): | |
| eval_passed, eval_total, eval_failures = run_eval_tests( | |
| eval_file, category=args.category, verbose=args.verbose | |
| ) | |
| else: | |
| print(f"\n {YELLOW}SKIP{RESET} eval tests β {eval_file} not found") | |
| # Grand summary | |
| print(f"\n{'=' * 70}") | |
| print(f" {BOLD}GRAND SUMMARY{RESET}") | |
| print(f"{'=' * 70}") | |
| print(f" Unit tests: {unit_passed}/{unit_total}", end="") | |
| print(f" {GREEN}PASS{RESET}" if not unit_failures else f" {RED}{len(unit_failures)} FAIL{RESET}") | |
| if eval_total: | |
| print(f" Eval tests: {eval_passed}/{eval_total} exact ({eval_passed/eval_total*100:.1f}%)", end="") | |
| print(f" {GREEN}PASS{RESET}" if not eval_failures else f" β {YELLOW}{len(eval_failures)} diff{RESET}") | |
| total_pass = unit_passed + eval_passed | |
| total_all = unit_total + eval_total | |
| print(f" Combined: {total_pass}/{total_all} ({total_pass/total_all*100:.1f}%)") | |
| sys.exit(0 if not unit_failures else 1) | |
| if __name__ == "__main__": | |
| main() | |