unsloth/tests/security
Daniel Han 135147814b
Fix CI on main: stale test doubles, a stale router stub, and two source defects (#8956)
* Fix CI on main: stale test doubles, a stale router stub, and two source defects

main has been red since Aug 14 and every open PR inherits it. Five clusters, none
of them caused by the PRs that were showing them.

context_length (13 tests, plus 4 more in disguise). #8700 added an unguarded
llama_backend.context_length read to the chat-completions path and updated five
test files, missing three. The real LlamaCppBackend has had the property for a
long time, so no user was ever affected; the doubles were simply
under-specified. The four gguf_stream_slot_release failures are the SAME bug:
those doubles reach the same line, but the AttributeError is swallowed into the
response task and surfaces as a 20 second timeout, which reads as a flake.

Nine hand-written doubles across five files each re-declared the same attribute
block with no shared base, so one new read broke whichever files happened not to
be updated. They now share FakeLlamaCppBackend, and a canary drives the real
route with a bare double so the next such read fails in one place, named, at the
point of the change. The stream waits no longer discard the driving task's
exception, so that class of failure cannot present as a bare timeout again.

youtube_router (2 tests). routes/__init__.py exports it and main.py imports it;
the app is fine. test_desktop_auth stubs sys.modules[routes] with a hardcoded
list of 17 routers, deliberately, to avoid importing the ML stack. #8648 added a
router and did not update it, the second time this has happened after
openai_codex_auth_router in #8511. The stub is now derived from main.py's own
import block, so it cannot go stale.

Repo tests (CPU), 6 failures, of which two are real source defects:
llama-extra-args.ts put the Studio brand into a user-visible validation message,
which the desktop branding contract forbids in runtime surfaces, and
test_playwright_server_lifecycle.py read checked-in files without an encoding,
which is a real Windows cp1252 crash the lint exists to catch. Both fixed in the
source. The other four are stale assertions chasing text that #8702 legitimately
moved or reflowed; they now assert the behaviour instead, via the real
override_lookup_candidates() and the element-scan pattern their own siblings
already use.

Not addressed here: pip scan-packages :: hf-stack reports 173 findings in
third-party deps under SCAN_ENFORCE=1 and is red on main too. Baselining a
supply-chain scanner to get green is the wrong reflex, so it wants its own look.

* Studio: break the settings/chat import cycle that stopped the UI rendering

#8932 added SIDEBAR_ORGANIZATION_STORAGE_KEY to the @/features/chat barrel and
had general-tab.tsx read it back out of that barrel. The key is used at MODULE
scope, in the storage-key list, and the barrel is part of an import cycle that
reaches this file, so the binding is still in its temporal dead zone when the
list is built:

  Cannot access 'SIDEBAR_ORGANIZATION_STORAGE_KEY' before initialization

That kills the whole module graph, so the page renders nothing. It is why
Frontend CI has been failing on main with a Playwright locator that finds no
elements, which reads as a flaky browser test rather than a module-init error.

Importing the key straight from its module breaks the cycle. Verified by
bisection with the real browser smoke: it passes at cfee13795 (before #8932),
fails on main with the TDZ error above, and passes again with this one-line
change. Typecheck clean, 2756 frontend tests pass.

#8932's own branch was already red with this exact failure before it merged.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Security: fix the filesystem-enumeration false positives in scan-packages

The hf-stack shard of pip scan-packages has been red on main. One CRITICAL
was blocking it, and tracking it down turned up a pattern bug behind nine of
the entries already in the baseline.

The blocking finding, unsloth-zoo/llama_cpp.py under "Harvests environment
variables/secrets AND makes network calls", is first-party: _github_auth_headers()
reads GH_TOKEN so the llama.cpp releases API call is not rate limited. That
file and check were already reviewed and baselined. It reopened because
unsloth-zoo 2026.8.12 replaced

    keynames = "\n" + "\n".join(os.environ.keys())

with a targeted _is_colab_environment() helper. Every other matched line is
byte identical to what was reviewed, so the current code is a strict subset of
the approved evidence. The entry is refreshed, with the hash generated by
--write-baseline rather than by hand. No PR caused this; it is the baseline's
reopen-on-change working as designed against an upstream release.

The pattern bug is in RE_FS_ENUM:

    r"|\bhistory\b.*\bread\b"  # reading shell history

Under re.DOTALL that .* spans the whole file, so any module containing the
word "history" anywhere before the word "read" anywhere is filesystem
enumeration, and with a network call in the same file that is a CRITICAL.
That is httpx's Response.history, retries.history in urllib3, IPython's
history module, torch's CUDA memory history. Nine of the eleven baselined
CRITICALs under this check were that one alternative, each suppressing a whole
file for the check.

Meanwhile the precise half was dead. \b\.bash_history\b puts \b between "/"
and "." in "~/.bash_history", where neither side is a word character, so it
could never match; same for \b\.zsh_history\b. This is the unsatisfiable-\b
bug already fixed once for /proc/self/status. Checked against the old pattern:
it matched none of five real history-file reads and all four benign cases.

Naming the files instead inverts that. The nine dead baseline entries are
removed, which narrows the allowlist rather than widening it, and three tests
pin both directions.

Verified by running all three shards locally against the same requirements
transform CI uses: hf-stack now exits 0 (was 1), studio and extras stay at 0,
and no removed entry resurfaced.

* Fix two more stale sidebar contract tests left by #8932

Both fail on pristine origin/main, so they are not from this branch. #8932
moved the sidebar's user-visible copy into the locale file and rewrote the
delete-switch predicate to cover its new bulk targets. Neither change breaks a
contract; both broke a grep of app-sidebar.tsx.

test_the_delete_switch_does_not_promise_project_files greps for the sentence
"This chat's own sandbox folder is removed from disk." It is still there,
verbatim, in studio/frontend/src/i18n/locales/en.ts. The promise is the
contract, not its address, so the test now searches the frontend sources and
survives the next move while still failing on a reworded promise.

test_the_delete_switch_reaches_a_chat_moved_into_a_project pinned one spelling
of deleteTargetHasFiles:

    -return target.kind === "project" || target.kind === "chat";
    +return target.kind !== "run";

Same answer for a chat and for a project, plus the new "chats" / "projects"
bulk kinds. What must hold is that a run is excluded and that project
membership is never consulted, so the test reads the brace-matched function
body and asserts that instead of the old one-liner.

Both were checked by mutation: reinstating the misleading copy, and gating
deleteTargetHasFiles on target.item.projectId, each fail the rewritten test.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Address the review: scope a contract test, keep a parity check, close a scanner gap

Four items, all confirmed against the code at head before changing anything.

1. tests/studio/test_model_picker_contracts.py: the rollback ordering assertion
searched the whole of chat-page.tsx, which takes the same snapshot in the hub
auto-load path at line 2558. The only applyModelLoadConfigToRuntime call is at
3262, so the index comparison was satisfied by the unrelated occurrence: deleting
the snapshot inside selectWithConfig outright still passed both assertions.
Verified by mutation. Now scoped to the selectWithConfig body, which fails on
that deletion.

2. studio/backend/tests/test_research_internal_call_tool_gate.py: dropping
perf_callback from the kwargs comparison hid presence as well as identity, so the
opt-out losing its callback on one path would have gone unnoticed and cost that
path its tok/s readout. Assert both are callable (or both absent) first, then
exclude. Verified by mutation at routes/inference.py:14512.

3. tests/studio/test_model_picker_contracts.py was source-only and ran without the
backend environment; calling the real ladder for a standalone .gguf pulled in
hub.utils.gguf, then loggers, then structlog, so a bare pytest run failed after
183 passes. CI installs studio.txt and is unaffected, so coverage there is
unchanged. The helper now skips on a missing third-party package only; a missing
first-party module still fails.

4. scripts/scan_packages.py: fish stores history at $XDG_DATA_HOME/fish/fish_history
with no leading dot (fishshell.com/docs/current/cmds/history.html), so the dotted
alternative could never match the real path and narrowing the pattern left an
exfiltration blind spot. Added a non-dotted form. The fish read plus a network call
is a CRITICAL again, the dotted shells still match, and the false positives stay
suppressed.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Tighten comments added by the CI repair

* Fix two more stale studio contract constants left by #8932

Both fail on pristine origin/main, so neither comes from this branch, and in
both the source is right and the test-side constant is stale.

INLINE_ROW_IDS in tests/studio/playwright_mac_tab_capabilities.py still listed
four rows. #7863 had put Video under "More" as layout v5, so the script dropped
it: an unpinned row renders no data-testid and every assertion on it silently
observes nothing. #8932 pins Video under Images again, and records it in the
migration history as v7, so it is deliberate and versioned rather than an
accidental revert. Video is observable again, so the script samples it again.
test_inline_row_ids_match_the_frontends_default_pinned_set exists precisely to
catch this tuple drifting from the store in either direction, and it did its job.

test_multi_chat_prompt_queue_contract.py pinned the zero-argument spelling
"return await clearStoredChats();". #8932 gave the call an options argument,
which changes nothing about the ordering the assertion is there to hold, so it
now matches on the call prefix, the same way the sibling assertion three lines
up already does.

Both mutation-checked: unpinning Video in the store fails the first, and moving
requestPromptQueueStop after clearStoredChats fails the second.

* Narrow a contract search I made too wide, and read it once

Self-audit of an earlier fix in this branch, under the same standard applied to
everything else here.

test_the_delete_switch_does_not_promise_project_files greps for a promise the
delete dialog must make. #8932 moved that copy into the locale file, so the
original grep of app-sidebar.tsx broke, and my fix widened the search to the
whole frontend tree. That is the same defect the review caught in the rollback
ordering assertion: a search wide enough to be satisfied by an unrelated
occurrence proves nothing. The sentence appearing in any of ~1200 files, a
comment or a dead module included, would have passed it.

Now scoped by intent. The promise the dialog MUST make is looked for where the
sidebar's user-visible copy lives, the locales and the component. The promise it
must NOT make is still looked for across all of src, since breadth only makes a
negative stricter.

Mutation-checked: rewording the real string and planting the original in an
unrelated module fails the test, where the whole-tree form passed.

The two scopes are also read through an lru_cache. The wide one concatenates
about 1200 files, and it was re-reading every one of them on each call.

* Address the second review: real scoping, not the appearance of it

Five items, each reproduced against the code at head before changing anything.
Three are cases where my own earlier fix looked scoped but was not.

deleteTargetHasFiles: the negative assertions did not establish the contract.
`return target.kind === "run";` -- the exact inversion, which hides the delete
switch for every chat and project -- mentions "run", mentions no projectId, and
contains neither prohibited expression, so it passed all four checks. Confirmed
by construction. Now the direction is pinned: run is the kind excluded, never
the one included.

The selectWithConfig slice ran to end of file, not to the callback's closing
brace: 13,566 characters rather than 579. Moving applyModelLoadConfigToRuntime
out of the callback while leaving the snapshot behind still passed. Brace-matched
now, and that mutation fails.

asgi_stream_helpers returned on the frame without inspecting the task, so a
send() that sets the event and then raises left both futures done, the frame
branch won, and the caller's gather(return_exceptions = True) discarded the
exception. That is the silence the helper exists to break. Reproduced directly.
The task is checked first now, and the message says whether the failure came
before or after the frame.

Two scanner gaps, both from narrowing RE_FS_ENUM. Constructed fish paths put a
quote rather than a separator before the basename, so Path.home() / "fish" /
"fish_history" and os.path.join(h, "fish", "fish_history") did not match. And the
dotted list omitted PowerShell's ConsoleHost_history.txt, Ruby's .irb_history and
SQLite's .sqlite_history. A quote now counts as a boundary and those names are
covered, case-insensitively for the Windows one. Ten read forms match, and the
httpx, urllib3, IPython and torch false positives stay suppressed.

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2026-08-16 02:02:47 -07:00
..
fixtures Reduce and tighten comments and docstrings across the test suite (#6429) 2026-06-18 01:07:09 -07:00
__init__.py security: NOT affected by Mini Shai-Hulud (May-12 wave) -- forward-looking hardening only (#5397) 2026-05-13 04:58:12 -07:00
conftest.py Scope the security suite's offline guard to the tests that want it (#8054) 2026-08-07 02:22:37 -07:00
test_desktop_release_resolver.py Revert "Desktop: ship a complete Linux AppImage (#8695)" (#8823) 2026-08-14 06:55:54 -07:00
test_desktop_updater_pointer.py Make publish-desktop-updater.yml manual-dispatch only (#8356) 2026-08-10 20:47:31 -07:00
test_lint_workflow_triggers.py Harden the workflow-trigger lint: scan .yaml, and host it outside the workflow it audits (#8545) 2026-08-12 09:55:26 -07:00
test_lockfile_supply_chain_audit.py security: lockfile audit must block non-registry sources and missing integrity by default (#8541) 2026-08-12 05:36:17 -07:00
test_network_blocker_does_not_leak.py Scope the security suite's offline guard to the tests that want it (#8054) 2026-08-07 02:22:37 -07:00
test_new_install_scripts.py Reduce and tighten comments and docstrings across the test suite (#6429) 2026-06-18 01:07:09 -07:00
test_release_desktop_appimage.py Revert "Desktop: ship a complete Linux AppImage (#8695)" (#8823) 2026-08-14 06:55:54 -07:00
test_release_desktop_integrity.py Scan Windows bundles with MpCmdRun when the Defender cmdlets are down (#8358) 2026-08-10 20:47:54 -07:00
test_release_desktop_notarization.py Retry macOS disk image stapling (#7787) 2026-08-04 09:23:47 +02:00
test_release_desktop_permissions.py Make publish-desktop-updater.yml manual-dispatch only (#8356) 2026-08-10 20:47:31 -07:00
test_release_desktop_signing.py release-desktop: pin trusted-signing-cli by digest instead of trusting a cache (#8417) 2026-08-11 06:03:28 -07:00
test_release_desktop_signing_simulation.py Revert "Desktop: ship a complete Linux AppImage (#8695)" (#8823) 2026-08-14 06:55:54 -07:00
test_scan_npm_packages.py scan_packages: key baseline on matched-code hash so payloads in baselined files are not auto-suppressed (#6552) 2026-07-01 04:03:59 -07:00
test_scan_packages.py Fix CI on main: stale test doubles, a stale router stub, and two source defects (#8956) 2026-08-16 02:02:47 -07:00