KeyHunter parser v10 + 18 scanners + tests (synthetic fixtures only, no keys/db/tokens)
This commit is contained in:
411
tests/test_key_patterns.py
Normal file
411
tests/test_key_patterns.py
Normal file
@@ -0,0 +1,411 @@
|
||||
"""Fixture tests for the merged key_patterns module (no network).
|
||||
|
||||
Run: python tests/test_key_patterns.py (exit 1 on any failure)
|
||||
|
||||
Covers both public APIs that exist in the two trees:
|
||||
root lanes -> PATTERNS / scan_text
|
||||
src modules -> KEY_PATTERNS / ENV_RE / KIMI_PATTERN / extract_all_keys
|
||||
"""
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
sys.path.insert(0, os.path.dirname(HERE))
|
||||
import key_patterns as K # noqa: E402
|
||||
|
||||
DS = "sk-3f9a1c7e5b2d8460a1c3e5f7091b2d4c" # 32 hex, high entropy
|
||||
DS2 = "sk-7d2f4a6b8c0e1d3f5a7b9c1e3d5f7a9b"
|
||||
KIMI = "sk-" + "Qw3rTy7uIo9pAs2dFg6hJk1lZx5cVb8nMq4wEr0tYu3iOp1a" # 48 alnum
|
||||
ZAI = "9c1f7a3e5b8d2046f1a3c5e7b9d1f3a5.RtY7uI2oP4aS6dF8"
|
||||
LOW_ENTROPY = "sk-aabbccddeeff00112233445566778899" # passes _is_junk today
|
||||
CODING_PLAN = "sk-sp-Kf83Jd92Ls04Mx71Qw63Er25Ty88Ua40"
|
||||
|
||||
FAILS = []
|
||||
|
||||
|
||||
def check(name, cond, detail=""):
|
||||
if cond:
|
||||
print(f"ok {name}")
|
||||
else:
|
||||
print(f"FAIL {name} {detail}")
|
||||
FAILS.append(name)
|
||||
|
||||
|
||||
def providers(text, fn):
|
||||
return {p for p, _ in fn(text)}
|
||||
|
||||
|
||||
def values(text, fn):
|
||||
return {k for _, k in fn(text)}
|
||||
|
||||
|
||||
def test_both_apis_exist():
|
||||
check("api.PATTERNS", isinstance(K.PATTERNS, list) and len(K.PATTERNS) > 100,
|
||||
f"len={len(getattr(K, 'PATTERNS', []))}")
|
||||
check("api.scan_text", callable(getattr(K, "scan_text", None)))
|
||||
check("api.KEY_PATTERNS", isinstance(getattr(K, "KEY_PATTERNS", None), dict)
|
||||
and len(K.KEY_PATTERNS) > 50, f"len={len(getattr(K,'KEY_PATTERNS',[]) or [])}")
|
||||
check("api.extract_all_keys", callable(getattr(K, "extract_all_keys", None)))
|
||||
check("api.ENV_RE", getattr(K, "ENV_RE", None) is not None)
|
||||
check("api.KIMI_PATTERN", getattr(K, "KIMI_PATTERN", None) is not None)
|
||||
|
||||
|
||||
def test_dashscope_env_context():
|
||||
text = f"# .env\nDASHSCOPE_API_KEY={DS}\nOTHER=1\n"
|
||||
check("dashscope.env.scan_text", DS in values(text, K.scan_text),
|
||||
str(sorted(values(text, K.scan_text)))[:120])
|
||||
check("dashscope.env.extract_all", DS in values(text, K.extract_all_keys))
|
||||
check("dashscope.env.prov", "DASHSCOPE" in providers(text, K.scan_text))
|
||||
|
||||
|
||||
def test_dashscope_bare():
|
||||
text = '{"model":"qwen","api_key":"%s"}' % DS2
|
||||
check("dashscope.bare.scan_text", DS2 in values(text, K.scan_text))
|
||||
check("dashscope.bare.extract_all", DS2 in values(text, K.extract_all_keys))
|
||||
|
||||
|
||||
def test_placeholder_and_junk_dropped():
|
||||
text = ("DASHSCOPE_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n"
|
||||
"KIMI_API_KEY=sk-your_api_key_here_1234567890abcdefghij\n")
|
||||
v = values(text, K.scan_text) | values(text, K.extract_all_keys)
|
||||
check("junk.placeholder_dropped", not any("xxxx" in x for x in v), str(v)[:120])
|
||||
check("junk.your_key_dropped", not any("your_api_key" in x for x in v))
|
||||
|
||||
|
||||
def test_low_entropy_dropped():
|
||||
text = "DASHSCOPE_API_KEY=%s" % LOW_ENTROPY
|
||||
v = values(text, K.scan_text) | values(text, K.extract_all_keys)
|
||||
check("template.pair_repeat_dropped", LOW_ENTROPY not in v,
|
||||
"low-entropy key accepted (need Shannon prefilter)")
|
||||
|
||||
|
||||
def test_coding_plan_pattern():
|
||||
text = '{"coding_plan_key": "%s"}' % CODING_PLAN
|
||||
v = values(text, K.scan_text) | values(text, K.extract_all_keys)
|
||||
check("pattern.sk-sp_coding_plan", CODING_PLAN in v,
|
||||
"sk-sp- Coding Plan shape not matched")
|
||||
|
||||
|
||||
def test_kimi_and_zai():
|
||||
text = "KIMI_API_KEY=%s\nZHIPU_API_KEY=%s\n" % (KIMI, ZAI)
|
||||
v = values(text, K.scan_text) | values(text, K.extract_all_keys)
|
||||
check("kimi.51char_found", KIMI in v)
|
||||
check("zai.dot_form_found", ZAI in v)
|
||||
|
||||
|
||||
def test_grouped_patterns_do_not_crash():
|
||||
"""A PATTERNS entry with a capture group makes findall() yield tuples;
|
||||
scan_text must flatten them instead of raising on .strip()."""
|
||||
grouped = [(p, rx) for p, rx in K.PATTERNS if getattr(rx, "groups", 0)]
|
||||
if not grouped:
|
||||
print("skip grouped_patterns (no grouped pattern in PATTERNS)")
|
||||
return
|
||||
prov, rx = grouped[0]
|
||||
sample = None
|
||||
for cand in (
|
||||
'SUNO_API_KEY = "sk-Ab3xK9mQ2wE5rT7yU1iO"',
|
||||
'token = "pk-lf-Ab3xK9mQ2wE5rT7yU1iO4pA8sD2fG6hJ"',
|
||||
'sk-proj-Ab3xK9mQ2wE5rT7yU1iO4pA8sD2fG6hJk0Lz9Xc8Vb7Nm6Qa5Ws4Ed3Rf2Tg1Yh0',
|
||||
):
|
||||
try:
|
||||
if rx.search(cand):
|
||||
sample = cand
|
||||
break
|
||||
except Exception:
|
||||
continue
|
||||
if sample is None:
|
||||
print(f"skip grouped_patterns fixture (no sample matched {prov})")
|
||||
return
|
||||
try:
|
||||
out = K.scan_text(sample)
|
||||
ok = all(isinstance(k, str) for _, k in out)
|
||||
check("grouped.no_crash_and_str_keys", ok, str(out)[:120])
|
||||
except Exception as e:
|
||||
check("grouped.no_crash_and_str_keys", False, f"{type(e).__name__}: {e}")
|
||||
|
||||
|
||||
def test_lowercase_env_attribution():
|
||||
"""IGNORECASE ENV_RE must still attribute the provider, not 'ENV'."""
|
||||
text = "dashscope_api_token=Ab3xK9mQ2wE5rT7yU1iO4pA8sD2fG6hJzX7cV9bN\n"
|
||||
hits = K.scan_text(text)
|
||||
provs = {p for p, _ in hits}
|
||||
check("env.lowercase_provider_not_ENV", "DASHSCOPE" in provs,
|
||||
str(hits)[:120])
|
||||
check("env.lowercase_no_bare_ENV", "ENV" not in provs, str(hits)[:120])
|
||||
|
||||
|
||||
def test_key_patterns_dict_coverage():
|
||||
"""KEY_PATTERNS[prov] must match EVERY alternative of that provider."""
|
||||
check("dict.DASHSCOPE_coding_plan",
|
||||
bool(K.KEY_PATTERNS["DASHSCOPE"].search(CODING_PLAN)))
|
||||
check("dict.DASHSCOPE_hex32", bool(K.KEY_PATTERNS["DASHSCOPE"].search(DS)))
|
||||
kimi_rx = K.KEY_PATTERNS.get("KIMI")
|
||||
check("dict.KIMI_narrow48", kimi_rx is not None
|
||||
and bool(kimi_rx.fullmatch("sk-" + "aB3xK9mQ2wE5rT7yU1iO4pA8sD2fG6hJzX7cV9bNmN2qR4tY"))
|
||||
and not kimi_rx.search("sk-short123"))
|
||||
|
||||
|
||||
def test_one_pair_per_value():
|
||||
"""A value matching two same-shape providers (KIMI/SILICONFLOW sk-48)
|
||||
must come back exactly once — first (specific-first) provider wins."""
|
||||
val = "sk-" + "aB3xK9mQ2wE5rT7yU1iO4pA8sD2fG6hJzX7cV9bNmN2qR4tY"
|
||||
hits = K.scan_text("SOME_CONTEXT '%s'" % val)
|
||||
same = [h for h in hits if h[1] == val]
|
||||
check("dedup.one_pair_per_value", len(same) == 1, str(hits)[:140])
|
||||
|
||||
|
||||
# ── v6.4 placeholder junk fixture tests ─────────────────────────────
|
||||
|
||||
NPM_PLACEHOLDER = "npm_XXXX_XXXX_XXXX_XXXX_XXXX"
|
||||
INSERT_YOUR = "ghp_INSERT_YOUR_TOKEN_HERE_1234567890abcdef"
|
||||
SK_UPPERCASE_X = "sk-" + "X" * 32 # 32 uppercase X
|
||||
REAL_SK = "sk-3f9a1c7e5b2d8460a1c3e5f7091b2d4c" # 32 hex, same as DS
|
||||
REAL_GHP = "ghp_" + "aB3xK9mQ2wE5rT7yU1iO4pA8sD2fG6hJzX7cV9bN" # valid shape
|
||||
|
||||
|
||||
def test_npm_placeholder_rejected():
|
||||
"""npm_XXXX_XXXX_... must be rejected by _is_junk (v6.4)."""
|
||||
text = f'NPM_TOKEN={NPM_PLACEHOLDER}\n'
|
||||
v = values(text, K.scan_text) | values(text, K.extract_all_keys)
|
||||
check("junk.npm_placeholder_rejected",
|
||||
NPM_PLACEHOLDER not in v, str(v)[:120])
|
||||
|
||||
|
||||
def test_insert_your_token_rejected():
|
||||
"""INSERT_YOUR_TOKEN_HERE must be rejected by _is_junk (v6.4)."""
|
||||
text = f'GITHUB_TOKEN={INSERT_YOUR}\n'
|
||||
v = values(text, K.scan_text) | values(text, K.extract_all_keys)
|
||||
check("junk.insert_your_token_rejected",
|
||||
INSERT_YOUR not in v, str(v)[:120])
|
||||
|
||||
|
||||
def test_sk_uppercase_x_rejected():
|
||||
"""sk-XXXX... (all uppercase X) must be rejected by _is_junk (v6.4)."""
|
||||
text = f'DASHSCOPE_API_KEY={SK_UPPERCASE_X}\n'
|
||||
v = values(text, K.scan_text) | values(text, K.extract_all_keys)
|
||||
check("junk.sk_uppercase_x_rejected",
|
||||
SK_UPPERCASE_X not in v, str(v)[:120])
|
||||
|
||||
|
||||
def test_real_sk_accepted():
|
||||
"""Real sk-<32hex> key must NOT be rejected."""
|
||||
text = f'DASHSCOPE_API_KEY={REAL_SK}\n'
|
||||
v = values(text, K.scan_text) | values(text, K.extract_all_keys)
|
||||
check("real.sk_accepted", REAL_SK in v, str(v)[:120])
|
||||
|
||||
|
||||
def test_real_ghp_accepted():
|
||||
"""Real ghp_ token must NOT be rejected."""
|
||||
text = f'GITHUB_TOKEN={REAL_GHP}\n'
|
||||
v = values(text, K.scan_text) | values(text, K.extract_all_keys)
|
||||
check("real.ghp_accepted", REAL_GHP in v, str(v)[:120])
|
||||
|
||||
|
||||
import hashlib as _hl
|
||||
|
||||
def _filler(seed, n, alpha="0123456789abcdef"):
|
||||
out = ""
|
||||
i = 0
|
||||
while len(out) < n:
|
||||
out += _hl.sha256((seed + str(i)).encode()).hexdigest()
|
||||
i += 1
|
||||
return "".join(ch for ch in out if ch in alpha)[:n]
|
||||
|
||||
_BECH32 = "QPZRY9X8GF2TVDW0S3JN54KHCE6MUA7L"
|
||||
def _bech_filler(n):
|
||||
dg = b""
|
||||
i = 0
|
||||
while len(dg) < n:
|
||||
dg += _hl.sha256(b"age" + str(i).encode()).digest(); i += 1
|
||||
return "".join(_BECH32[b % 32] for b in dg[:n])
|
||||
|
||||
NEW_PAT_FIXTURES = [
|
||||
("GROK", "xai-" + _filler("xai", 40, "0123456789abcdefghijklmnopqrstuvwxyz") + "_" + _filler("xai2", 39, "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")),
|
||||
("LANGSMITH", "lsv2_pt_" + _filler("lsp1", 32) + "_" + _filler("lsp2", 10)),
|
||||
("OPENAI_ADMIN", "sk-admin-" + _filler("oa1", 58, "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_-") + "T3BlbkFJ" + _filler("oa2", 58, "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_-")),
|
||||
("DATABRICKS", "dapi" + _filler("db", 32)),
|
||||
("DOPPLER", "dp.pt." + _filler("dp", 43, "0123456789abcdefghijklmnopqrstuvwxyz")),
|
||||
("ARTIFACTORY", "AKCp" + _filler("art", 69, "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")),
|
||||
("ALIBABA_LTAI", "LTAI" + _filler("ltai", 20, "0123456789abcdefghijklmnopqrstuvwxyz")),
|
||||
("AGE_KEY", "AGE-SECRET-KEY-1" + _bech_filler(58)),
|
||||
("ONEPASSWORD", "A3-7K2M9X-4Q8W2E7R1T5-B3N6M-K9P2Q-7X4Z8"),
|
||||
("AZURE_AD", "k7Q" + "4Q~" + _filler("az", 32, "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_~.-")),
|
||||
]
|
||||
|
||||
|
||||
def test_v65_merged_patterns():
|
||||
"""v6.5: 10 rules merged from gitleaks v8 + trufflehog detectors."""
|
||||
for prov, sample in NEW_PAT_FIXTURES:
|
||||
text = "export SOME_CTX='%s'\n" % sample
|
||||
v = values(text, K.scan_text) | values(text, K.extract_all_keys)
|
||||
p = providers(text, K.scan_text)
|
||||
check("v65." + prov, sample in v and prov in p,
|
||||
"in_values=%s provs=%s" % (sample in v, sorted(p)[:4]))
|
||||
|
||||
|
||||
def test_elevenlabs_boundary():
|
||||
"""Boundary: XI_API_KEY matches, PERPLEXITY/complexity/maxi_token do not."""
|
||||
import hashlib, string
|
||||
def _body(seed, n, alpha=string.ascii_letters + string.digits):
|
||||
out, i = '', 0
|
||||
while len(out) < n:
|
||||
for b in hashlib.sha256(f'{seed}:{i}'.encode()).digest():
|
||||
out += alpha[b % len(alpha)]
|
||||
if len(out) == n: return out
|
||||
i += 1
|
||||
|
||||
h32 = _body('el', 32, '0123456789abcdef') # pure hex for xi key
|
||||
pplx = _body('pplx', 56, string.ascii_lowercase + string.digits) # exactly 56 lowercase alnum
|
||||
|
||||
# Positive: XI_API_KEY=<hex32> → ELEVENLABS
|
||||
pos_txt = f"XI_API_KEY={h32}"
|
||||
check("EL.xi.positive", any(
|
||||
prov == 'ELEVENLABS' for prov, key in K.scan_text(pos_txt)
|
||||
), f"XI_API_KEY={h32[:6]}...")
|
||||
|
||||
# Negative: PERPLEXITY_API_KEY=<32 hex> → not ELEVENLABS
|
||||
neg1 = f"PERPLEXITY_API_KEY={h32}"
|
||||
el_hits = [prov for prov, key in K.scan_text(neg1) if prov == 'ELEVENLABS']
|
||||
check("EL.perplexity.not_xi", len(el_hits) == 0,
|
||||
f"Got ELEVENLABS hits on PERPLEXITY: {el_hits}")
|
||||
|
||||
# Negative: pplx-<56 lnum> → PERPLEXITY, not ELEVENLABS
|
||||
ppx_txt = f"PERPLEXITY_API_KEY=pplx-{pplx}"
|
||||
hits = list(K.scan_text(ppx_txt))
|
||||
provs = {p for p, _ in hits}
|
||||
check("EL.pplx.is_perplexity", 'PERPLEXITY' in provs,
|
||||
f"PERPLEXITY not in {provs}")
|
||||
check("EL.pplx.not_elevenlabs", 'ELEVENLABS' not in provs,
|
||||
f"ELEVENLABS falsely detected: {provs}")
|
||||
|
||||
# Negative: word-internal xi → no match
|
||||
for word in ('complexity', 'maxi_token'):
|
||||
neg_txt = f"{word}: {h32}"
|
||||
hits = [prov for _, prov, _ in K.scan_text(neg_txt) if prov == 'ELEVENLABS']
|
||||
check(f"EL.{word}.not_xi", len(hits) == 0,
|
||||
f"FP on '{word}': {hits}")
|
||||
|
||||
# Drift guard: key_patterns ELEVENLABS regex == eternal_v10 PROVIDERS regex
|
||||
# Textual grep avoids importing eternal_v10 (which shells out to gh, connects sockets, etc.)
|
||||
EV10 = os.path.join(os.path.dirname(HERE), 'src', 'eternal_v10.py')
|
||||
kp_pat = dict(K.PATTERNS)['ELEVENLABS'].pattern
|
||||
if not os.path.exists(EV10):
|
||||
check("EL.drift_guard", True, f"skip: no {EV10}")
|
||||
else:
|
||||
# Extract the ELEVENLABS regex string from eternal_v10 by finding the
|
||||
# block line-by-line, avoiding the nested-quote escaping problem.
|
||||
lines = open(EV10, encoding='utf-8').readlines()
|
||||
ev_pat = None
|
||||
for i, line in enumerate(lines):
|
||||
if '"ELEVENLABS"' not in line:
|
||||
continue
|
||||
for j in range(i, min(i + 30, len(lines))):
|
||||
m = re.search(r'_re\(\s*r[\'"](.+?)[\'"]\s*,?\s*$', lines[j])
|
||||
if m:
|
||||
ev_pat = m.group(1)
|
||||
break
|
||||
break
|
||||
if ev_pat is None:
|
||||
check("EL.drift_guard", False, "could not extract PROVIDERS['ELEVENLABS']['regex']")
|
||||
else:
|
||||
check("EL.drift_guard", ev_pat == kp_pat,
|
||||
f"PATTERNS={kp_pat!r} PROVIDERS={ev_pat!r}")
|
||||
|
||||
|
||||
def test_dashscope_ws_extraction():
|
||||
"""sk-ws- keys (115-200 chars, dots/dashes) → DASHSCOPE (v11.1)."""
|
||||
import hashlib
|
||||
def _ws_body(n):
|
||||
out, i = '', 0
|
||||
alpha = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_."
|
||||
while len(out) < n:
|
||||
for b in hashlib.sha256(f'ws:{i}'.encode()).digest():
|
||||
out += alpha[b % len(alpha)]
|
||||
if len(out) == n: return out
|
||||
i += 1
|
||||
|
||||
ws_body = _ws_body(108) # 108 chars after sk-ws- prefix (total 115)
|
||||
ws_key = f"sk-ws-{ws_body}"
|
||||
text = f"DASHSCOPE_API_KEY={ws_key}"
|
||||
hits = list(K.scan_text(text))
|
||||
provs = {p for p, _ in hits}
|
||||
vals = {k for _, k in hits}
|
||||
check("DS.ws.extracted", ws_key in vals,
|
||||
f"sk-ws- key not in values, got: {sorted(vals)}")
|
||||
check("DS.ws.attributed", 'DASHSCOPE' in provs,
|
||||
f"DASHSCOPE not in providers, got: {provs}")
|
||||
|
||||
|
||||
def test_dashscope_long_hex_not_truncated():
|
||||
"""40-hex sk- key must NOT be truncated to 32 chars (v11.1 boundary)."""
|
||||
import hashlib, string
|
||||
def _body(seed, n, alpha):
|
||||
out, i = '', 0
|
||||
while len(out) < n:
|
||||
for b in hashlib.sha256(f'{seed}:{i}'.encode()).digest():
|
||||
out += alpha[b % len(alpha)]
|
||||
if len(out) == n: return out
|
||||
i += 1
|
||||
|
||||
long_hex = _body('ds40', 40, '0123456789abcdef')
|
||||
full_key = f"sk-{long_hex}"
|
||||
assert len(full_key) == 43, f"expected 43, got {len(full_key)}" # sk- + 40 hex
|
||||
text = f"DASHSCOPE_API_KEY={full_key}"
|
||||
|
||||
hits = list(K.scan_text(text))
|
||||
vals = {k for _, k in hits}
|
||||
|
||||
# The full 40-hex key should NOT appear (doesn't match 32-hex pattern)
|
||||
# BUT it should NOT produce a truncated 32-char fake
|
||||
truncated = full_key[:35] # sk- + 32 hex
|
||||
fake_hits = [v for v in vals if len(v) == 35 and v.startswith('sk-')]
|
||||
check("DS.notrunc.no32fake", len(fake_hits) == 0,
|
||||
f"Truncated 32-hex fake extracted: {fake_hits}")
|
||||
|
||||
# sk-ws- pattern should NOT match (this is hex, not ws format)
|
||||
ws_hits = [v for v in vals if v.startswith('sk-ws-')]
|
||||
check("DS.notrunc.no_ws_misattrib", len(ws_hits) == 0,
|
||||
f"sk-ws- misattributed on hex key: {ws_hits}")
|
||||
|
||||
# Known limitation: 40-hex key not captured by any DASHSCOPE alt
|
||||
# (newer shapes >32 hex need a separate probe path via endpoint auto-detection)
|
||||
check("DS.notrunc.accepted_limitation", len(vals) == 0,
|
||||
f"40-hex key unexpectedly matched: {vals} (known: >32 hex needs probe path)")
|
||||
|
||||
|
||||
def main():
|
||||
for fn in (test_both_apis_exist, test_dashscope_env_context,
|
||||
test_dashscope_bare, test_placeholder_and_junk_dropped,
|
||||
test_low_entropy_dropped, test_coding_plan_pattern,
|
||||
test_kimi_and_zai, test_grouped_patterns_do_not_crash,
|
||||
test_lowercase_env_attribution,
|
||||
test_key_patterns_dict_coverage,
|
||||
test_one_pair_per_value,
|
||||
test_npm_placeholder_rejected,
|
||||
test_insert_your_token_rejected,
|
||||
test_sk_uppercase_x_rejected,
|
||||
test_real_sk_accepted,
|
||||
test_real_ghp_accepted,
|
||||
test_v65_merged_patterns,
|
||||
test_elevenlabs_boundary,
|
||||
test_dashscope_ws_extraction,
|
||||
test_dashscope_long_hex_not_truncated):
|
||||
fn()
|
||||
_nextra = len(getattr(K, 'EXTRA_PATTERNS', {}))
|
||||
print(f"\nPATTERNS={len(K.PATTERNS)} KEY_PATTERNS={len(K.KEY_PATTERNS)} "
|
||||
f"EXTRA={_nextra}")
|
||||
# loud gate: пустой EXTRA-hook (нет patterns_extra.py рядом с key_patterns)
|
||||
# НЕ должен проходить молча — batch-2 и kh_* семьи исчезнут из lanes.
|
||||
check("extras-loaded", _nextra >= 17,
|
||||
f"EXTRA_PATTERNS={_nextra} — patterns_extra.py не загружается рядом с этим деревом")
|
||||
if FAILS:
|
||||
print(f"RED: {len(FAILS)} failing -> {FAILS}")
|
||||
return 1
|
||||
|
||||
print("GREEN: all key_patterns fixture tests passed")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user