Files
wehub-resource-sync 09e9f3545f
Test / Code Quality (push) Has been cancelled
Test / Test (macos-latest, Python 3.10) (push) Has been cancelled
Test / Test (macos-latest, Python 3.11) (push) Has been cancelled
Test / Test (macos-latest, Python 3.12) (push) Has been cancelled
Test / Test (macos-latest, Python 3.13) (push) Has been cancelled
Test / Test (macos-latest, Python 3.14) (push) Has been cancelled
Test / Test (ubuntu-latest, Python 3.10) (push) Has been cancelled
Test / Test (ubuntu-latest, Python 3.11) (push) Has been cancelled
Test / Test (ubuntu-latest, Python 3.12) (push) Has been cancelled
Test / Test (ubuntu-latest, Python 3.13) (push) Has been cancelled
Test / Test (ubuntu-latest, Python 3.14) (push) Has been cancelled
Test / Test (windows-latest, Python 3.10) (push) Has been cancelled
Test / Test (windows-latest, Python 3.11) (push) Has been cancelled
Test / Test (windows-latest, Python 3.12) (push) Has been cancelled
Test / Test (windows-latest, Python 3.13) (push) Has been cancelled
Test / Test (windows-latest, Python 3.14) (push) Has been cancelled
CodeQL / Analyze (push) Has been cancelled
dependency-audit / pip-audit (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:30:13 +08:00

1117 lines
46 KiB
Python

"""Tests for the canonical cassette sanitization registry.
The registry lives in :mod:`tests.cassette_patterns` and exports a single
:func:`scrub_string` sanitizer plus an :func:`is_clean` validator. These
tests assert:
- Every cookie shape we know about scrubs cleanly (positive)
- Scrubbing is idempotent on already-scrubbed input (no double-scrub)
- Every placeholder in ``SCRUB_PLACEHOLDERS`` is recognised as clean
- A real cookie value starting with ``S`` IS still flagged as a leak —
the legacy bash guard used a ``[^S"]`` character class that exempted
any real secret whose first character was ``S``; the consolidated
registry uses exact-match placeholder membership to close that hole.
- Registry stays in sync with :mod:`tests.vcr_config`
- Bad-cassette regressions (the shape-lint inputs) are caught by
:func:`is_clean`. Inline payloads are used so this test does not depend
on filesystem fixtures.
"""
from __future__ import annotations
from pathlib import Path
import pytest
from tests import vcr_config
from tests.cassette_patterns import (
DISPLAY_NAME_FALSE_POSITIVES,
EMAIL_PROVIDERS,
HOST_COOKIES,
OPTIONAL_COOKIES,
SCRUB_PLACEHOLDERS,
SECURE_COOKIES,
SESSION_COOKIES,
find_credential_leaks,
find_high_entropy_leaks,
is_clean,
scrub_string,
)
REPO_ROOT = Path(__file__).resolve().parents[2]
# A synthetic Google API key whose *shape* (``AIza`` + 35 ``[A-Za-z0-9_-]``
# chars) matches the canonical Google API-key pattern the registry scrubs, but
# which is built by concatenation at runtime so no contiguous 39-char key
# literal ever appears in this source file — embedding a real-looking key here
# would itself trip GitHub secret scanning. The value is obviously fake.
FAKE_GOOGLE_API_KEY = "AIza" + "FAKE0" * 7
assert len(FAKE_GOOGLE_API_KEY) == 39 # AIza + 35
# ---------------------------------------------------------------------------
# Exports
# ---------------------------------------------------------------------------
def test_registry_exports_required_constants() -> None:
"""Every constant called out in the sanitization-registry spec is exported."""
assert isinstance(SESSION_COOKIES, list) and SESSION_COOKIES
assert isinstance(SECURE_COOKIES, list) and SECURE_COOKIES
assert isinstance(HOST_COOKIES, list) and HOST_COOKIES
assert isinstance(OPTIONAL_COOKIES, list) # allowed empty in theory
assert isinstance(EMAIL_PROVIDERS, list) and EMAIL_PROVIDERS
assert isinstance(SCRUB_PLACEHOLDERS, frozenset) and SCRUB_PLACEHOLDERS
def test_session_cookies_contains_expected_names() -> None:
"""Lock the canonical SID-family cookie names."""
for name in ("SID", "HSID", "SSID", "APISID", "SAPISID", "SIDCC", "OSID", "NID"):
assert name in SESSION_COOKIES
# ---------------------------------------------------------------------------
# scrub_string — positive: every cookie shape we redact
# ---------------------------------------------------------------------------
@pytest.mark.parametrize(
"name", ["SID", "HSID", "SSID", "APISID", "SAPISID", "SIDCC", "OSID", "NID"]
)
def test_session_cookie_header_form_is_scrubbed(name: str) -> None:
"""``Cookie: NAME=secret; ...`` → ``Cookie: NAME=SCRUBBED; ...``"""
header = f"Cookie: foo=bar; {name}=ABCDEF1234567890; baz=qux"
scrubbed = scrub_string(header)
assert "ABCDEF1234567890" not in scrubbed
assert f"{name}=SCRUBBED" in scrubbed
@pytest.mark.parametrize(
"name",
[
"__Secure-1PSID",
"__Secure-3PSID",
"__Secure-1PSIDCC",
"__Secure-3PSIDTS",
"__Host-GAPS",
],
)
def test_secure_and_host_cookies_are_scrubbed(name: str) -> None:
"""``__Secure-*`` / ``__Host-*`` umbrella scrubs the value, keeps the name."""
header = f"Cookie: {name}=REAL_SECRET_HERE; other=keep"
scrubbed = scrub_string(header)
assert "REAL_SECRET_HERE" not in scrubbed
assert f"{name}=SCRUBBED" in scrubbed
@pytest.mark.parametrize("name", ["SID", "SAPISID", "__Secure-1PSID"])
def test_storage_state_name_first_is_scrubbed(name: str) -> None:
"""Playwright storage_state ``{"name":..., "value":...}`` shape is scrubbed."""
text = f'{{"name":"{name}","value":"REAL_VALUE_HERE","domain":".google.com"}}'
scrubbed = scrub_string(text)
assert "REAL_VALUE_HERE" not in scrubbed
assert '"value":"SCRUBBED"' in scrubbed
@pytest.mark.parametrize("name", ["SID", "SAPISID", "__Secure-1PSID"])
def test_storage_state_value_first_is_scrubbed(name: str) -> None:
"""Defensive ordering: ``"value":..., "name":...`` is also scrubbed."""
text = f'{{"value":"REAL_VALUE_HERE","name":"{name}","domain":".google.com"}}'
scrubbed = scrub_string(text)
assert "REAL_VALUE_HERE" not in scrubbed
assert '"value":"SCRUBBED"' in scrubbed
def test_url_session_id_is_scrubbed() -> None:
"""``f.sid=...`` in URL query params is scrubbed."""
url = "https://notebooklm.google.com/_/?f.sid=ABCDE12345&f.cv=1"
assert "ABCDE12345" not in scrub_string(url)
assert "f.sid=SCRUBBED" in scrub_string(url)
def test_at_csrf_token_is_scrubbed_in_body() -> None:
"""``at=...`` form parameter is scrubbed to ``at=SCRUBBED_CSRF``."""
body = "f.req=..&at=AABBCCDD-EEFF-GGHH&other=keep"
scrubbed = scrub_string(body)
assert "AABBCCDD-EEFF-GGHH" not in scrubbed
assert "at=SCRUBBED_CSRF" in scrubbed
@pytest.mark.parametrize("provider", EMAIL_PROVIDERS)
def test_email_is_scrubbed_quoted(provider: str) -> None:
"""JSON-quoted emails at any supported provider are scrubbed."""
text = f'{{"email":"alice.example+tag@{provider}.com"}}'
scrubbed = scrub_string(text)
assert provider not in scrubbed
assert '"SCRUBBED_EMAIL@example.com"' in scrubbed
@pytest.mark.parametrize("provider", EMAIL_PROVIDERS)
def test_email_is_scrubbed_unquoted(provider: str) -> None:
"""Bare ``user@provider.com`` in HTML / JS contexts is scrubbed."""
text = f'<a href="mailto:alice.example+tag@{provider}.com">mail me</a>'
scrubbed = scrub_string(text)
assert provider not in scrubbed
assert "SCRUBBED_EMAIL@example.com" in scrubbed
# ---------------------------------------------------------------------------
# scrub_string — negative: legitimate content survives unchanged
# ---------------------------------------------------------------------------
def test_bsid_cookie_substring_is_not_scrubbed() -> None:
"""A benign cookie named ``BSID`` containing the ``SID`` suffix survives.
The negative lookbehind on each cookie-header pattern anchors at a
cookie-name boundary; without it the regex would eat the ``SID=...`` tail
of ``BSID=...``.
"""
header = "Cookie: BSID=PUBLIC_VALUE_HERE; other=keep"
assert scrub_string(header) == header
def test_legitimate_two_word_source_title_not_scrubbed() -> None:
"""A non-displayName JSON key with a two-Capitalized-word value survives."""
text = '{"title": "Source Title"}'
assert scrub_string(text) == text
def test_unknown_email_provider_not_scrubbed() -> None:
"""An email at a provider we do NOT cover (``@corp.internal``) is preserved."""
text = '{"contact":"bob@corp.internal"}'
assert scrub_string(text) == text
@pytest.mark.parametrize("param", ["flat", "rate", "format", "stat"])
def test_at_lookbehind_protects_param_names_ending_in_at(param: str) -> None:
"""Params whose names *end* in ``at`` are not eaten by the ``at=`` scrubber.
Without the negative-lookbehind anchor, ``at=[A-Za-z0-9_-]+`` would match
the substring ``at=VALUE`` inside ``flat=VALUE`` / ``rate=VALUE`` and
corrupt the URL or form body.
"""
body = f"foo=1&{param}=PUBLIC_VALUE&bar=2"
assert scrub_string(body) == body
# ---------------------------------------------------------------------------
# scrub_string — idempotence
# ---------------------------------------------------------------------------
def test_scrub_is_idempotent_on_already_scrubbed_cookie_header() -> None:
text = "Cookie: SID=SCRUBBED; __Secure-1PSID=SCRUBBED"
once = scrub_string(text)
twice = scrub_string(once)
assert once == twice
assert once == text # nothing changed on the first pass either
def test_scrub_is_idempotent_on_already_scrubbed_email() -> None:
"""``SCRUBBED_EMAIL@example.com`` survives a second scrub pass unchanged."""
once = scrub_string('{"email":"alice@gmail.com"}')
twice = scrub_string(once)
assert once == twice
assert '"SCRUBBED_EMAIL@example.com"' in twice
def test_scrub_is_idempotent_on_already_scrubbed_storage_state() -> None:
text = '{"name":"SID","value":"SCRUBBED","domain":".google.com"}'
once = scrub_string(text)
twice = scrub_string(once)
assert once == twice == text
@pytest.mark.parametrize(
"field,placeholder",
[
("SNlM0e", "SCRUBBED_CSRF"),
("FdrFJe", "SCRUBBED_SESSION"),
("oPEP7c", "SCRUBBED_EMAIL"),
("S06Grb", "SCRUBBED_USER_ID"),
("B8SWKb", "SCRUBBED_API_KEY"),
("JrWMbf", "SCRUBBED_API_KEY"),
("at", "SCRUBBED_CSRF"),
],
)
def test_token_field_scrubs_value_with_escaped_quote(field: str, placeholder: str) -> None:
"""JSON token values containing ``\\"`` are scrubbed in full, not truncated.
Regression test for the naive ``[^"]+`` value match: without the escape-
aware idiom, the scrub stops at the first ``\\"`` and leaves the suffix of
the secret in the cassette while ``is_clean`` is fooled by the leading
placeholder. The new ``(?:[^"\\\\]|\\\\.)*`` idiom matches across escape
sequences so the entire JSON string value is replaced.
"""
text = f'{{"{field}":"REAL_PREFIX\\"REAL_SUFFIX"}}'
scrubbed = scrub_string(text)
assert "REAL_PREFIX" not in scrubbed, scrubbed
assert "REAL_SUFFIX" not in scrubbed, scrubbed
assert f'"{field}":"{placeholder}"' in scrubbed
# ---------------------------------------------------------------------------
# is_clean — positive: every known placeholder is accepted
# ---------------------------------------------------------------------------
@pytest.mark.parametrize("placeholder", sorted(SCRUB_PLACEHOLDERS))
def test_is_clean_accepts_known_placeholder_in_cookie_header(placeholder: str) -> None:
"""Every member of ``SCRUB_PLACEHOLDERS`` is recognised as a clean value."""
# Single-name placeholders are valid cookie values; the email placeholder
# contains '@' which we test via the JSON-key shape (cookies in JSON-key
# shape are explicitly enumerated below).
if "@" in placeholder:
pytest.skip("header form doesn't apply to email-shaped placeholder")
header = f"Cookie: SID={placeholder}"
ok, leaks = is_clean(header)
assert ok, leaks
def test_is_clean_accepts_scrubbed_storage_state_value() -> None:
text = '{"name":"SID","value":"SCRUBBED","domain":".google.com"}'
ok, leaks = is_clean(text)
assert ok, leaks
def test_is_clean_accepts_scrubbed_cookie_json_key() -> None:
text = '{"SID":"SCRUBBED","__Secure-1PSID":"SCRUBBED"}'
ok, leaks = is_clean(text)
assert ok, leaks
def test_is_clean_accepts_scrubbed_email_placeholder() -> None:
text = '{"email":"SCRUBBED_EMAIL@example.com"}'
ok, leaks = is_clean(text)
assert ok, leaks
# ---------------------------------------------------------------------------
# is_clean — negative: real leaks are detected
# ---------------------------------------------------------------------------
def test_is_clean_flags_real_email() -> None:
"""A real ``@gmail.com`` address survives a missing-scrub pass."""
ok, leaks = is_clean('{"email":"realname@gmail.com"}')
assert not ok
assert any("realname" in leak or "email" in leak.lower() for leak in leaks)
def test_is_clean_flags_sid_starting_with_S_in_header_form() -> None:
"""Cookie-leak heuristic: a real SID value starting with ``S`` is detected.
The legacy bash guard used a ``[^S"]`` character class on the cookie value,
which exempted any real secret whose first character was ``S``. The new
registry uses an exact-match :data:`SCRUB_PLACEHOLDERS` allowlist so the
starting character is irrelevant — anything not in the allowlist is a leak.
"""
text = "Set-Cookie: SID=S_REAL_LEAKED_TOKEN; Path=/"
ok, leaks = is_clean(text)
assert not ok, "S-prefixed real cookie value should be flagged"
assert any("SID" in leak for leak in leaks)
def test_is_clean_flags_sid_starting_with_S_in_storage_state() -> None:
"""Same cookie-leak heuristic in Playwright ``storage_state.json`` shape."""
text = '{"name":"SID","value":"S_REAL_VALUE","domain":".google.com"}'
ok, leaks = is_clean(text)
assert not ok
assert any("SID" in leak for leak in leaks)
def test_is_clean_flags_sid_starting_with_S_in_json_key() -> None:
"""Same cookie-leak heuristic in the JSON-dict-with-cookie-name-as-key shape."""
text = '{"SAPISID": "S_real_leaked_token_here"}'
ok, _ = is_clean(text)
assert not ok
def test_is_clean_flags_short_one_char_cookie_value() -> None:
"""A single-character non-scrubbed leak (``"SID": "x"``) is detected."""
text = '{"SID": "x"}'
ok, _ = is_clean(text)
assert not ok
def test_is_clean_flags_wiz_global_data_unscrubbed_csrf() -> None:
"""``SNlM0e`` left at its real value is flagged as a leak."""
text = '{"SNlM0e":"AB-some-real-CSRF-token-value-12345"}'
ok, leaks = is_clean(text)
assert not ok
assert any("SNlM0e" in leak or "CSRF" in leak.upper() for leak in leaks)
def test_is_clean_flags_jrwmbf_unscrubbed_api_key() -> None:
"""``JrWMbf`` left at its real Google API key value is flagged as a leak.
Regression for the ``generate_mind_map_interactive`` /
``mind_maps_interactive`` cassette leak: the NotebookLM web API key rode in
the ``JrWMbf`` WIZ field, which had no scrubber, so the key round-tripped
into committed cassettes unredacted.
"""
text = f'{{"JrWMbf":"{FAKE_GOOGLE_API_KEY}"}}'
ok, leaks = is_clean(text)
assert not ok
assert any("JrWMbf" in leak or "Google API key" in leak for leak in leaks)
def test_is_clean_flags_bare_google_api_key_in_unknown_field() -> None:
"""A Google API-key shape is flagged even outside any known WIZ field.
The field-name-agnostic catch-all detector is the backstop that closes the
``JrWMbf`` gap class: a key in any future/unknown field is still caught.
"""
text = f'{{"SomeUnknownField":"{FAKE_GOOGLE_API_KEY}"}}'
ok, leaks = is_clean(text)
assert not ok
assert any("Google API key" in leak for leak in leaks)
def test_scrub_removes_google_api_key_in_unknown_field() -> None:
"""The catch-all scrubber collapses an ``AIza`` key in any field."""
text = f'{{"SomeUnknownField":"{FAKE_GOOGLE_API_KEY}"}}'
scrubbed = scrub_string(text)
assert FAKE_GOOGLE_API_KEY not in scrubbed
assert "SCRUBBED_API_KEY" in scrubbed
def test_longer_than_canonical_api_key_is_fully_scrubbed_no_partial_leak() -> None:
"""A key with MORE than 35 tail chars is scrubbed in full (no trailing leak).
Regression for the exact-``{35}``-quantifier partial-leak class: with an
exact quantifier, ``AIza`` + 36 chars in an unknown field would scrub only
the first 39 chars and leave a trailing fragment that ``SCRUBBED_API_KEY``
no longer re-matches — silently leaking the tail. The greedy ``{35,}`` tail
consumes the whole contiguous key-char run.
"""
# Tail char ``Z`` is absent from the ``SCRUBBED_API_KEY`` sentinel, so its
# presence in the output can only mean an un-scrubbed key fragment survived.
long_key = "AIza" + "Z" * 40 # 4 + 40 = 44 chars, well over the canonical 39
text = f'{{"SomeUnknownField":"{long_key}"}}'
scrubbed = scrub_string(text)
# No fragment of the original key survives (not even a trailing remainder).
assert "Z" not in scrubbed, scrubbed
assert "AIza" not in scrubbed, scrubbed
assert "SCRUBBED_API_KEY" in scrubbed
# And the validator agrees the scrubbed output is clean.
assert is_clean(scrubbed)[0]
# The raw long key is itself flagged before scrubbing.
assert find_credential_leaks(text)
# ---------------------------------------------------------------------------
# find_credential_leaks — high-severity-only subset (for fixture scanning)
# ---------------------------------------------------------------------------
def test_find_credential_leaks_flags_google_api_key() -> None:
"""A Google API-key shape is reported by the credential-only scanner."""
leaks = find_credential_leaks(f'{{"JrWMbf":"{FAKE_GOOGLE_API_KEY}"}}')
assert any("Google API key" in leak for leak in leaks)
def test_find_credential_leaks_flags_auth_token() -> None:
"""A raw ``g.a000-`` auth token is reported by the credential-only scanner."""
leaks = find_credential_leaks("Cookie: SID=g.a000-abcdefghijklmnop")
assert any("auth token" in leak for leak in leaks)
def test_find_credential_leaks_ignores_placeholder_fixture_content() -> None:
"""Placeholder content that trips ``is_clean`` is NOT flagged here.
This is the property that makes ``--secrets-only`` safe to run over
``tests/fixtures/`` — escaped display names, test emails, and scrubbed
cookie sentinels are all ignored; only real credential shapes match.
"""
fixture_like = (
'[\\"Scrubbed Note Title\\",\\"alice@gmail.com\\"]'
' {"SID":"SCRUBBED"} {"oPEP7c":"SCRUBBED_EMAIL"}'
)
# is_clean WOULD flag the escaped display name / email here ...
assert not is_clean(fixture_like)[0]
# ... but the credential-only scanner stays silent.
assert find_credential_leaks(fixture_like) == []
# ---------------------------------------------------------------------------
# Generic high-entropy / unknown-field credential scan (issue #1382)
# ---------------------------------------------------------------------------
#
# The targeted detectors above are name-anchored or known-shape, making the
# guard necessary-but-not-sufficient: a NOVEL token shape, or a known secret in
# an un-targeted field, passes silently (the LSID / JrWMbf leaks). These tests
# pin the field-agnostic backstop: it WOULD catch a planted novel token, yet
# produces ZERO false positives on every committed cassette.
# Synthetic novel credentials assembled at runtime from shorter chunks so this
# source file carries NO contiguous static credential-shaped literal — a 64-char
# hex or 40+ char base64 string written inline would itself trip secret scanning
# (Betterleaks flags it as a generic API key). The shapes are also distinct from
# the known g.a000-/sidts-/ya29./AIza prefixes, and the deterministic mixes keep
# the base64 entropy well above the 4.0 bits/char floor.
NOVEL_BASE64_TOKEN = "".join(
("kJ8sLm2NpQr5TvWx", "Yz0AbCdEfGhIjKlM", "nOpQrStUvWxYz123", "45678AbCdEf")
)
NOVEL_HEX_TOKEN = "".join(
("9f8e7d6c5b4a3928", "17065fe4d3c2b1a0", "9f8e7d6c5b4a3928", "17065fe4d3c2b1a0")
)
assert len(NOVEL_BASE64_TOKEN) >= 40
assert len(NOVEL_HEX_TOKEN) >= 64
def test_entropy_scan_flags_novel_base64_token_in_unknown_field() -> None:
"""A long high-entropy base64 token in an UNKNOWN field is flagged.
This is the residual-risk shape the name-anchored detectors miss: a credential
family the registry does not yet know about, riding in a field that is not on
any allowlist. The generic entropy scan catches it by shape alone.
"""
payload = f'{{"sessionBlob":"{NOVEL_BASE64_TOKEN}"}}'
leaks = find_high_entropy_leaks(payload)
assert any("high-entropy token" in leak for leak in leaks), leaks
# It also surfaces through both public entry points.
assert any("high-entropy token" in leak for leak in find_credential_leaks(payload))
assert not is_clean(payload)[0]
def test_entropy_scan_flags_novel_hex_token_in_unknown_field() -> None:
"""A long pure-hex token (64+ chars) in an unknown field is flagged.
Hex entropy caps at log2(16) == 4.0 bits/char, so the base64 entropy bar can
never reliably fire on it — the scan routes pure-hex tokens onto a dedicated
64-char length floor instead.
"""
payload = f'{{"legacyToken":"{NOVEL_HEX_TOKEN}"}}'
assert any("high-entropy token" in leak for leak in find_high_entropy_leaks(payload))
assert not is_clean(payload)[0]
def test_entropy_scan_ignores_internal_uuid() -> None:
"""A 36-char UUID (artifact / source / conversation ID) is NOT flagged.
UUIDs are NotebookLM-internal identifiers the scrubbers deliberately
preserve. They fall under both the 40-char base64 length floor and the
explicit UUID-shape allowlist, so the scan must stay silent.
"""
payload = '{"artifactId":"06f0c5bd-108f-4c8b-8911-34b2acc656de"}'
assert find_high_entropy_leaks(payload) == []
assert is_clean(payload)[0]
def test_entropy_scan_ignores_scrub_placeholders() -> None:
"""Canonical scrub placeholders are never flagged (idempotent validation)."""
for placeholder in SCRUB_PLACEHOLDERS:
payload = f'{{"field":"{placeholder}"}}'
assert find_high_entropy_leaks(payload) == [], placeholder
def test_entropy_scan_ignores_short_token() -> None:
"""A token below the 40-char base64 length floor is not flagged."""
payload = '{"x":"abcdef1234567890ABCDEF"}' # 22 chars, high entropy
assert find_high_entropy_leaks(payload) == []
def test_entropy_scan_ignores_unquoted_high_entropy_content() -> None:
"""High-entropy content that is NOT a quoted scalar is out of scope.
The scan deliberately anchors on a quoted JSON string scalar — the shape a
leaked credential takes in an unknown field — so the legitimate high-entropy
text that pervades real cassettes (CSS class runs, base64 query blobs split
by ``=``/``&``, hex mind-map data) stays out of scope. This documents that
blind spot as a deliberate decision, not an accident.
"""
# Mirrors a real cassette ``context=eJw...`` zlib-base64 query param: the
# token is preceded by ``context=`` and is NOT inside quotes.
blob = "kJ8sLm2NpQr5TvWxYz0AbCdEfGhIjKlMnOpQrStUvWxYz12345678AbCdEf"
assert find_high_entropy_leaks(f"context={blob}&next=1") == []
def test_all_committed_cassettes_pass_entropy_scan() -> None:
"""ZERO false positives: every committed cassette passes the entropy scan.
Calibration guard. The thresholds are tuned against the real corpus; if a
future cassette legitimately carries a quoted high-entropy scalar this test
fails loudly so the threshold/allowlist is revisited deliberately rather
than the scan silently regressing into noise.
"""
cassette_dir = REPO_ROOT / "tests" / "cassettes"
offenders: list[str] = []
for cassette in sorted(cassette_dir.rglob("*.yaml")):
# ``examples/`` holds illustrative fixtures with hand-edited placeholder
# content, excluded from the real-recording guard for the same reason
# the checker excludes them.
if "examples" in cassette.parts:
continue
text = cassette.read_text(encoding="utf-8", errors="replace")
leaks = find_high_entropy_leaks(text)
if leaks:
offenders.append(f"{cassette.name}: {leaks[:3]}")
assert not offenders, "entropy scan false-positives on committed cassettes:\n" + "\n".join(
offenders
)
# ---------------------------------------------------------------------------
# Round-trip: scrub_string(x) is always is_clean
# ---------------------------------------------------------------------------
@pytest.mark.parametrize(
"leak_input",
[
"Cookie: SID=ABCDEF1234567890",
"Cookie: __Secure-1PSID=REAL_VALUE; Path=/",
"Set-Cookie: SAPISID=S_REAL_TOKEN; Path=/",
'{"name":"SID","value":"S_REAL_VALUE"}',
'{"value":"S_REAL_VALUE","name":"__Secure-1PSID"}',
# Direct JSON-key cookie shape (round-trip via the rule added to
# close the validator/sanitizer asymmetry).
'{"SID": "S_REAL_LEAKED_TOKEN"}',
'{"SAPISID":"S_real_leaked_token_here"}',
'{"__Secure-1PSID": "S_REAL_VALUE"}',
'{"email":"alice@gmail.com"}',
'{"SNlM0e":"real-csrf-here"}',
'{"FdrFJe":"real-session-here"}',
'{"oPEP7c":"alice@gmail.com"}',
'{"S06Grb":"123456789012345678901"}',
'{"W3Yyqf":"123456789012345678901"}',
'{"qDCSke":"123456789012345678901"}',
'{"B8SWKb":"AIzaSyAREAL_API_KEY_HERE"}',
'{"VqImj":"AIzaSyAREAL_API_KEY_HERE"}',
f'{{"JrWMbf":"{FAKE_GOOGLE_API_KEY}"}}',
# Bare Google API-key shape outside any known WIZ field — the
# field-name-agnostic catch-all must still scrub it.
f'{{"SomeUnknownField":"{FAKE_GOOGLE_API_KEY}"}}',
'{"QGcrse":"real-client-id"}',
'{"iQJtYd":"real-project-id"}',
"f.sid=REAL_SESSION_TOKEN",
"at=REAL_CSRF_TOKEN",
],
)
def test_scrub_then_is_clean_round_trip(leak_input: str) -> None:
"""Anything :func:`scrub_string` produces must satisfy :func:`is_clean`."""
scrubbed = scrub_string(leak_input)
ok, leaks = is_clean(scrubbed)
assert ok, f"scrubbed output still leaks: {leaks}"
# ---------------------------------------------------------------------------
# Registry ↔ vcr_config sync
# ---------------------------------------------------------------------------
def test_vcr_config_uses_registry_scrub_string() -> None:
"""``vcr_config.scrub_string`` is sourced from ``cassette_patterns``.
``vcr_config`` loads ``cassette_patterns`` via ``importlib.util.spec_from_
file_location`` (a separate module identity from the ``sys.path``-import
used by the test harness), so we cannot use ``is`` identity. Instead we
pin both the source-file location and the byte-for-byte source of the
bound function — if a future refactor reintroduces an inline pattern list
in ``vcr_config`` and reassigns ``scrub_string`` to a local definition,
either check fails.
"""
import inspect
assert inspect.getfile(vcr_config.scrub_string).endswith("cassette_patterns.py")
assert inspect.getsource(vcr_config.scrub_string) == inspect.getsource(scrub_string)
def test_vcr_config_has_no_inline_sensitive_patterns() -> None:
"""``vcr_config`` MUST NOT define its own ``SENSITIVE_PATTERNS`` list.
Guards against drift between recorder and guard. The registry is the
single source of truth; this test fails if vcr_config recreates a local
copy.
"""
assert not hasattr(vcr_config, "SENSITIVE_PATTERNS")
def test_registry_session_cookies_all_scrubbed_by_vcr_config() -> None:
"""Each :data:`SESSION_COOKIES` name has a working scrubber.
This is the registry-sync invariant: if either the registry's cookie list
or ``vcr_config``'s scrubber pipeline drifts so that a declared cookie
name no longer gets its value scrubbed, this test fails.
"""
for name in SESSION_COOKIES:
header = f"Cookie: {name}=REAL_SECRET_TOKEN_HERE"
scrubbed = vcr_config.scrub_string(header)
assert "REAL_SECRET_TOKEN_HERE" not in scrubbed, (
f"{name} declared in SESSION_COOKIES but not scrubbed by vcr_config"
)
assert f"{name}=SCRUBBED" in scrubbed
def test_registry_secure_cookies_all_scrubbed_by_vcr_config() -> None:
"""Each :data:`SECURE_COOKIES` name is caught by the umbrella scrubber."""
for name in SECURE_COOKIES:
header = f"Cookie: {name}=REAL_SECRET"
scrubbed = vcr_config.scrub_string(header)
assert "REAL_SECRET" not in scrubbed
assert f"{name}=SCRUBBED" in scrubbed
def test_filter_headers_disjoint_from_cookies() -> None:
"""VCR ``filter_headers`` covers HTTP-header-only entries — not cookies.
Cookies are scrubbed via :func:`scrub_string`, not dropped via
``filter_headers``. If a future change moves an SID-family name into
``filter_headers`` (which would silently drop the entire ``Cookie``
header from every cassette and break replay), this assertion catches it.
"""
cookie_names = set(SESSION_COOKIES) | set(SECURE_COOKIES) | set(HOST_COOKIES)
filter_headers = set(vcr_config.notebooklm_vcr.filter_headers)
overlap = cookie_names & filter_headers
assert not overlap, (
f"cookie names found in vcr filter_headers (should be scrubbed, not dropped): {overlap}"
)
# ---------------------------------------------------------------------------
# Bad-cassette regression sanity check (inline payloads so this test does
# not depend on filesystem fixtures)
# ---------------------------------------------------------------------------
def test_bad_cassette_byte_count_payload_with_email_is_flagged() -> None:
"""A synthetic bad-cassette body with a leaked email is flagged."""
body = '12\n{"u":"alice@gmail.com"}\n'
ok, _ = is_clean(body)
assert not ok
def test_bad_cassette_cookie_header_payload_is_flagged() -> None:
"""A synthetic bad-cassette body with a leaked cookie value is flagged."""
body = "Set-Cookie: SID=S_REAL_LEAK; Path=/\n"
ok, _ = is_clean(body)
assert not ok
# ---------------------------------------------------------------------------
# upload + Drive token scrubbing
#
# These tests pin down two invariants:
#
# 1. Upload tokens (X-GUploader-UploadID headers, upload_id query params,
# full upload URLs) and Drive tokens (AONS permission tokens, Drive
# file IDs inside ``"file_id":`` JSON or ``/drive/v3/files/`` URLs)
# scrub to their canonical placeholders.
#
# 2. The DRIVE_FILE_ID regex is context-anchored so bare 36-char UUIDs
# elsewhere (artifact IDs, source IDs, conversation IDs) are NOT
# scrubbed — those are NotebookLM-internal identifiers and matching
# them would corrupt cassette replay. The negative regression block
# is the load-bearing test here.
# ---------------------------------------------------------------------------
def test_upload_id_header_scrubbed() -> None:
header = "X-GUploader-UploadID: ABPj22qXYZ_-abc123def456"
scrubbed = scrub_string(header)
assert scrubbed == "X-GUploader-UploadID: SCRUBBED_UPLOAD_ID"
assert "ABPj22qXYZ" not in scrubbed
def test_upload_url_full_scrubbed() -> None:
"""A full notebooklm upload URL preserves host/path and scrubs the token."""
url = (
"https://notebooklm.google.com/upload/_/?authuser=0&upload_id=AJRbA5XZXPNXlxYzAbcdef_-12345"
)
scrubbed = scrub_string(url)
assert scrubbed == "https://notebooklm.google.com/upload/_/?upload_id=SCRUBBED_UPLOAD_ID"
assert "AJRbA5XZXPNXlx" not in scrubbed
def test_upload_id_query_param_scrubbed() -> None:
"""A bare ``upload_id=...`` query param outside the full URL is scrubbed."""
text = "POST /resume?upload_id=AJRbA5XZ_-1234567890 HTTP/1.1"
scrubbed = scrub_string(text)
assert "AJRbA5XZ_-1234567890" not in scrubbed
assert "upload_id=SCRUBBED_UPLOAD_ID" in scrubbed
def test_drive_aons_token_scrubbed() -> None:
"""A real-world Drive AONS token (50+ chars) is scrubbed."""
aons = "AONSffzuRealTokenValueWithLotsOfCharsAndDigits1234567890"
scrubbed = scrub_string(aons)
assert scrubbed == "SCRUBBED_AONS"
def test_drive_aons_short_prefix_not_matched() -> None:
"""The literal string ``AONS`` (and short tails) is NOT scrubbed.
The 20-char tail threshold avoids matching incidental ``AONS`` mentions
in code, comments, or short identifiers.
"""
assert scrub_string("AONS") == "AONS"
assert scrub_string("AONSshort") == "AONSshort"
def test_drive_file_id_in_json_key_scrubbed() -> None:
"""File ID inside a ``"file_id": "..."`` JSON key is scrubbed."""
text = '{"file_id": "1NFoP9ORcSIk_dTXwMhZWRHfX6JvqYdRqmzYp8LqLi-s"}'
scrubbed = scrub_string(text)
assert "1NFoP9ORcSIk" not in scrubbed
assert '"file_id": "SCRUBBED_DRIVE_FILE_ID"' in scrubbed
def test_drive_file_id_in_drive_url_scrubbed() -> None:
"""File ID inside a ``/drive/v3/files/<id>`` URL is scrubbed."""
url = "https://www.googleapis.com/drive/v3/files/1A2B3C4D5E6F7G8H9I0J1K2L3M4N5O6P7Q8R"
scrubbed = scrub_string(url)
assert "1A2B3C4D5E6F7G8H9I" not in scrubbed
assert "/drive/v3/files/SCRUBBED_DRIVE_FILE_ID" in scrubbed
def test_drive_file_id_negative_regression_non_drive_ids_not_scrubbed() -> None:
"""Bare 36-char UUIDs outside Drive contexts must NOT be scrubbed.
This is the load-bearing test for Drive-file-ID scrubbing. The codebase
emits many ``[A-Za-z0-9-]`` identifiers that LOOK like Drive file IDs but are
NotebookLM-internal (artifact IDs, conversation IDs, source IDs). A
naive ``[A-Za-z0-9_-]{33,44}`` pattern would corrupt cassette replay by
mangling them; DRIVE_FILE_ID is intentionally anchored to ``"file_id":``
JSON keys and ``/drive/v3/files/`` URLs only.
"""
non_drive_ids = [
"71669a91-d5f0-4298-913e-9193178ec62c", # notebook ID
"62e5c8db-3dd2-407c-8d19-32ae4ae799db", # artifact ID
"f66923f0-1df4-4ffe-9822-3ed63c558b1c", # conversation ID
"953b658a-579b-4b3c-b280-43b3781babf3", # source ID
"c3f6285f-1709-44c4-9cd6-e95cf0ea4f5e", # share ID
"167481cd-23a3-4331-9a45-c8948900bf91", # another notebook ID
]
for non_drive_id in non_drive_ids:
wrappers = [
f'{{"artifact_id": "{non_drive_id}"}}',
f'{{"conversation_id": "{non_drive_id}"}}',
f'{{"source_id": "{non_drive_id}"}}',
f"/v1/notebooks/{non_drive_id}/sources",
f"/v1/artifacts/{non_drive_id}",
]
for wrapped in wrappers:
assert scrub_string(wrapped) == wrapped, (
f"DRIVE_FILE_ID false-positive on non-Drive identifier: {wrapped!r}"
)
def test_upload_drive_scrubbing_is_idempotent() -> None:
"""Scrubbing an already-scrubbed string is a no-op for upload/Drive patterns."""
inputs = [
"X-GUploader-UploadID: ABPj22qXYZ_-abc123",
"https://notebooklm.google.com/upload/_/?upload_id=AJRbA5XZ_-12345",
"POST /resume?upload_id=AJRbA5XZ_-1234567890 HTTP/1.1",
"AONSffzuTokenWithEnoughCharsToMatch12345",
'{"file_id": "1NFoP9ORcSIk_dTXwMhZWRHfX6JvqYdRqmzYp8LqLi-s"}',
"/drive/v3/files/1A2B3C4D5E6F7G8H9I0J1K2L3M4N5O6P7Q8R",
]
for raw in inputs:
once = scrub_string(raw)
twice = scrub_string(once)
assert once == twice, f"Scrubbing not idempotent for: {raw!r}"
def test_is_clean_flags_unscrubbed_upload_id() -> None:
"""is_clean must catch an unscrubbed upload token."""
dirty = "X-GUploader-UploadID: ABPj22qXYZ_-abc123def456"
ok, leaks = is_clean(dirty)
assert not ok
assert any("ABPj22qXYZ" in leak for leak in leaks)
def test_is_clean_flags_unscrubbed_aons() -> None:
"""is_clean must catch an unscrubbed Drive AONS token."""
dirty = "value=AONSffzuRealTokenValueWithLotsOfChars1234567890"
ok, leaks = is_clean(dirty)
assert not ok
assert any("AONS" in leak for leak in leaks)
def test_is_clean_flags_unscrubbed_drive_file_id_in_json() -> None:
"""is_clean must catch an unscrubbed Drive file ID inside a JSON key."""
dirty = '{"file_id": "1NFoP9ORcSIk_dTXwMhZWRHfX6JvqYdRqmzYp8LqLi-s"}'
ok, leaks = is_clean(dirty)
assert not ok
assert any("1NFoP9ORcSIk" in leak for leak in leaks)
def test_is_clean_flags_unscrubbed_upload_url() -> None:
"""is_clean must catch an unscrubbed full upload URL."""
dirty = "https://notebooklm.google.com/upload/_/?upload_id=AJRbA5XZ_-12345"
ok, leaks = is_clean(dirty)
assert not ok
def test_is_clean_recognizes_upload_drive_placeholders() -> None:
"""The four upload + Drive placeholders register as clean."""
for placeholder in [
"X-GUploader-UploadID: SCRUBBED_UPLOAD_ID",
"upload_id=SCRUBBED_UPLOAD_ID",
"https://notebooklm.google.com/upload/_/?upload_id=SCRUBBED_UPLOAD_ID",
"SCRUBBED_UPLOAD_URL",
"SCRUBBED_AONS",
'{"file_id": "SCRUBBED_DRIVE_FILE_ID"}',
"/drive/v3/files/SCRUBBED_DRIVE_FILE_ID",
]:
ok, leaks = is_clean(placeholder)
assert ok, f"Placeholder form wrongly flagged as leak: {placeholder!r} -> {leaks}"
# ---------------------------------------------------------------------------
# display-name + avatar scrub patterns
#
# These tests pin down four invariants:
#
# 1. Escaped JSON display-name literals (``\"First Last\"`` inside a double-
# encoded WRB payload) scrub to ``\"SCRUBBED_NAME\"``.
# 2. ``lh3.googleusercontent.com/(?:a|ogw)/<token>`` avatar URLs scrub to
# ``SCRUBBED_AVATAR_URL`` for BOTH path forms and BOTH sizing suffix
# variants (``=s512`` and ``=s32-c-mo``).
# 3. Legitimate two-Capitalized-word strings (font families like Google Sans,
# UI titles like Account Information, test-corpus artifact / notebook
# titles, AND bare-quoted source titles like ``"Source Title"``) are NEVER
# scrubbed. This is the LOAD-BEARING test: a broad
# ``>[A-Z][a-z]+\\s[A-Z][a-z]+<`` regex without the false-positive
# allowlist + escape anchoring would corrupt cassette replay. The audit
# explicitly flagged this as the failure mode to prevent.
# 4. Scrubbing is idempotent on already-scrubbed input; ``is_clean`` flags
# raw leaks and accepts the canonical placeholders as clean.
# ---------------------------------------------------------------------------
# Helper: a literal backslash-quote pair as it would appear inside a JSON-
# stringified WRB payload. Using a tiny helper keeps the test bodies readable
# (without it every assertion would be a forest of ``\\\\"``).
def _esc(s: str) -> str:
"""Wrap ``s`` in the cassette's escaped-quote form ``\\"...\\"``."""
return f'\\"{s}\\"'
def test_display_name_escaped_literal_scrubbed() -> None:
"""A real escaped display-name literal scrubs to the SCRUBBED_NAME sentinel."""
raw = _esc("Alice Smith")
scrubbed = scrub_string(raw)
assert scrubbed == _esc("SCRUBBED_NAME")
assert "Alice Smith" not in scrubbed
def test_display_name_three_word_escaped_literal_scrubbed() -> None:
"""Three-word names (e.g. middle name present) are also scrubbed."""
raw = _esc("Mary Ann Smith")
scrubbed = scrub_string(raw)
assert scrubbed == _esc("SCRUBBED_NAME")
assert "Mary Ann Smith" not in scrubbed
def test_display_name_inside_wrb_payload_scrubbed() -> None:
"""The exact shape from sharing_get_status.yaml is scrubbed correctly.
This is the realistic payload form: a stringified JSON list inside a
WRB envelope, with the display name as a positional list element rather
than under a structured ``"displayName": "..."`` key. The A4 patterns
do NOT fire on this shape — that's the gap A6a closes.
"""
payload = (
'[[[\\"SCRUBBED_EMAIL@example.com\\",1,[],'
'[\\"People Conf\\",\\"https://lh3.googleusercontent.com/a/ACg8ocXYZabc=s512\\"]]]'
)
scrubbed = scrub_string(payload)
assert "People Conf" not in scrubbed
assert "ACg8ocXYZabc" not in scrubbed
assert _esc("SCRUBBED_NAME") in scrubbed
assert "SCRUBBED_AVATAR_URL" in scrubbed
def test_avatar_url_a_path_scrubbed() -> None:
"""The ``/a/`` avatar URL form is scrubbed to SCRUBBED_AVATAR_URL."""
url = "https://lh3.googleusercontent.com/a/ACg8ocImrMoQR5mQnUHZzuc6Tat88aWfSwMre0nCoCanft5bLuZ3dTV0=s512"
scrubbed = scrub_string(url)
assert scrubbed == "SCRUBBED_AVATAR_URL"
assert "ACg8oc" not in scrubbed
def test_avatar_url_ogw_path_scrubbed() -> None:
"""The ``/ogw/`` avatar URL form is scrubbed to SCRUBBED_AVATAR_URL."""
url = "https://lh3.googleusercontent.com/ogw/AF2bZyi16LQ_0jOcB_3NwTmyCfSFpN74FaCfwF0mWwtxF--cwSQ=s192-c-mo"
scrubbed = scrub_string(url)
assert scrubbed == "SCRUBBED_AVATAR_URL"
assert "AF2bZyi" not in scrubbed
def test_avatar_url_sizing_suffix_variants_scrubbed() -> None:
"""Both ``=s512`` and ``=s32-c-mo`` style sizing suffixes are captured."""
for tail in ("=s32", "=s64-c-mo", "=s83-c-mo", "=s512"):
url = f"https://lh3.googleusercontent.com/ogw/AF2bZyABC123_-xyz{tail}"
scrubbed = scrub_string(url)
assert scrubbed == "SCRUBBED_AVATAR_URL", (
f"Sizing suffix {tail!r} not fully scrubbed: {scrubbed!r}"
)
def test_display_name_false_positive_allowlist_preserved() -> None:
"""Every entry in DISPLAY_NAME_FALSE_POSITIVES is preserved verbatim.
This is part of the load-bearing negative regression: font family
strings (``Google Sans``), UI page titles (``Account Information``),
and artifact / notebook titles from the test corpus must never be
scrubbed even though they match the two-Capitalized-word shape.
"""
for fp in DISPLAY_NAME_FALSE_POSITIVES:
raw = _esc(fp)
scrubbed = scrub_string(raw)
assert scrubbed == raw, (
f"False-positive allowlist breach: {fp!r} got scrubbed to {scrubbed!r}"
)
def test_display_name_negative_regression_bare_source_titles_preserved() -> None:
"""Bare-quoted two-Capitalized-word source titles must NOT be scrubbed.
THIS IS THE LOAD-BEARING TEST. The audit explicitly warned that a broad
``>[A-Z][a-z]+\\s[A-Z][a-z]+<`` regex would corrupt source-rename and
artifact-list cassettes. The A6a scrubber anchors on the escape-quote
form ``\\"...\\"``, so bare-quoted ``"Source Title"`` (which is how
artifact / notebook / source titles actually appear in cassettes) is
safe — both the escape anchoring AND the false-positive allowlist
cooperate to prevent corruption.
"""
bare_titles = [
'"Source Title"',
'"Apple Pie"',
'"Recipe Book"',
'"Quantum Physics"',
'"Machine Learning"',
'"Web Development"',
'"Data Science"',
'"Climate Change"',
]
for bare in bare_titles:
scrubbed = scrub_string(bare)
assert scrubbed == bare, (
f"Bare-quoted source title wrongly scrubbed: {bare!r} -> {scrubbed!r}"
)
def test_display_name_negative_regression_escaped_titles_in_allowlist_preserved() -> None:
"""Escaped artifact/notebook titles from the test corpus are preserved.
A cassette body containing ``\\"Agent Quiz\\"`` (e.g. an artifact title)
must NOT be scrubbed — the false-positive allowlist protects it. Without
this protection, the escape-anchored regex would clobber the title and
cassette replay would fail signature matching against the recorded
request bodies.
"""
titles = [
"Agent Quiz",
"Agent Flashcards",
"Agent Development Tutorials",
"Slide Deck",
"Tool Use Loop",
"Claude Code",
]
for title in titles:
raw = _esc(title)
scrubbed = scrub_string(raw)
assert scrubbed == raw, (
f"Test-corpus title wrongly scrubbed: {title!r} ({raw!r}) -> {scrubbed!r}"
)
def test_display_name_and_avatar_scrubbing_is_idempotent() -> None:
"""Scrubbing an already-scrubbed string is a no-op for display-name/avatar patterns."""
inputs = [
_esc("Alice Smith"),
_esc("People Conf"),
"https://lh3.googleusercontent.com/a/ACg8ocXYZ=s512",
"https://lh3.googleusercontent.com/ogw/AF2bZy_-cwSQ=s32-c-mo",
# Realistic combined payload.
f'[[[\\"SCRUBBED_EMAIL@example.com\\",1,[],[{_esc("People Conf")},'
f'\\"https://lh3.googleusercontent.com/a/ACg8ocXYZ=s512\\"]]]',
]
for raw in inputs:
once = scrub_string(raw)
twice = scrub_string(once)
assert once == twice, f"Scrubbing not idempotent for: {raw!r}"
def test_is_clean_flags_unscrubbed_escaped_display_name() -> None:
"""is_clean must catch an unscrubbed escaped display-name literal."""
dirty = _esc("Alice Smith")
ok, leaks = is_clean(dirty)
assert not ok
assert any("Alice Smith" in leak for leak in leaks)
def test_is_clean_ignores_false_positive_escaped_literals() -> None:
"""is_clean must NOT flag false-positive allowlist entries.
The detector consults DISPLAY_NAME_FALSE_POSITIVES at the call site,
so escaped ``\\"Google Sans\\"`` and friends don't surface as leaks
even though they match the two-Capitalized-word regex shape.
"""
for fp in DISPLAY_NAME_FALSE_POSITIVES:
ok, leaks = is_clean(_esc(fp))
# Filter to only display-name leaks; the email/cookie detectors
# don't fire on these inputs but be explicit anyway.
display_leaks = [leak for leak in leaks if "escaped display name" in leak]
assert not display_leaks, (
f"False-positive {fp!r} wrongly flagged as display-name leak: {display_leaks}"
)
def test_is_clean_flags_unscrubbed_avatar_url() -> None:
"""is_clean must catch an unscrubbed avatar URL (both /a/ and /ogw/)."""
for url in (
"https://lh3.googleusercontent.com/a/ACg8ocXYZabc=s512",
"https://lh3.googleusercontent.com/ogw/AF2bZy_-cwSQ=s32-c-mo",
):
ok, leaks = is_clean(url)
assert not ok
assert any("avatar URL" in leak for leak in leaks)
def test_is_clean_recognizes_display_name_avatar_placeholders() -> None:
"""The display-name + avatar placeholders register as clean."""
placeholders = [
"SCRUBBED_AVATAR_URL",
_esc("SCRUBBED_NAME"),
# Realistic combined: an already-scrubbed sharing payload.
f'[[[\\"SCRUBBED_EMAIL@example.com\\",1,[],[{_esc("SCRUBBED_NAME")},'
f'\\"SCRUBBED_AVATAR_URL\\"]]]',
]
for placeholder in placeholders:
ok, leaks = is_clean(placeholder)
assert ok, f"Placeholder form wrongly flagged as leak: {placeholder!r} -> {leaks}"
def test_display_name_false_positives_mirror_shape_lint() -> None:
"""The scrub-registry allowlist must stay in sync with the shape-lint allowlist.
The shape lint carries the same set under the same name
(``DISPLAY_NAME_FALSE_POSITIVES``) with each entry wrapped in the
cassette's escape-quote form. If the two drift, a real cassette could pass
shape-lint but trip the scrub detector (or vice versa) — this test forces
both lists to be updated together.
"""
# The shape-lint allowlist now lives in the shared non-test helper
# ``_guardrails._cassette_shape_lint`` (issue #1431); importing it from
# that ``_``-prefixed module avoids a test-imports-test dependency on
# ``test_cassette_shapes``. ``tests/`` is on ``sys.path`` so the
# ``_guardrails`` package resolves.
from tests._guardrails._cassette_shape_lint import (
DISPLAY_NAME_FALSE_POSITIVES as SHAPE_LINT_FPS,
)
# Shape-lint stores entries as ``\"Name\"``; the scrub registry stores
# bare names. Strip the escape wrapping to compare apples-to-apples.
shape_lint_bare = frozenset(
entry.removeprefix('\\"').removesuffix('\\"') for entry in SHAPE_LINT_FPS
)
assert shape_lint_bare == DISPLAY_NAME_FALSE_POSITIVES, (
"Display-name false-positive allowlist drifted from shape-lint allowlist; "
"update BOTH tests/cassette_patterns.py and "
"tests/_guardrails/_cassette_shape_lint.py"
)