Skip to content

Put mypy on the whole tree, and stop the suite phoning a real daemon - #415

Merged
dovvnloading merged 2 commits into
mainfrom
types/tail-to-zero
Sep 4, 2026
Merged

Put mypy on the whole tree, and stop the suite phoning a real daemon#415
dovvnloading merged 2 commits into
mainfrom
types/tail-to-zero

Conversation

@dovvnloading

Copy link
Copy Markdown
Owner

Problem

Two things, both found while finishing the type sweep.

The test suite made real network calls. PR #413 fixed one test that opened a
TCP connection to 127.0.0.1:11434 and noted nine other files that set
OLLAMA_MODELS the same way but had not been checked. Instrumenting
socket.connect across a full run answered it — the suite made 102 outbound
connects to that port:

46x  _get_ollama_capabilities   <- ollama_supports_tools
46x  _get_ollama_capabilities   <- ollama_supports_embedding
10x  _get_ollama_context_window <- _ollama_effective_context_window

#413 had closed only the third, and only inside one file.

179 mypy errors remained, across 47 files.

Change

The network

Each connect costs ~2s on a machine with nothing listening, and
_get_ollama_capabilities deliberately does not cache a probe failure — correct
in production, where a daemon may be restarting, but it means every check pays
again. It was also a quiet correctness problem: a developer running Ollama
locally got different answers out of these three functions than CI did, from the
same code.

backend/tests/conftest.py now makes ollama.show raise, beside the existing
_never_touch_the_real_user_data_dir fixture and for the same reason.
test_no_network_in_tests.py asserts it, phrased as "no socket was opened" so it
keeps holding if the probing moves. A full run now makes zero outbound
connects
— the only remaining socket.connect calls are asyncio's Windows
self-pipe. Verified the gate is not vacuous: with the fixture removed, all three
socket assertions fail.

The suite also got ~19% faster (103.2s → 84.0s), which was not the point.

The types

mypy now covers 339 source files, up from 4 when this audit started.

api_provider.py held 32 errors and deserved the most care. Four
base_url: str = None signatures. Three module-level caches with no declared
shape — one of which I first annotated dict[str, Any] and mypy rejected,
because llama_cpp_runtime keys it by a five-tuple. LLAMA_CPP_SETTINGS
inferring as dict[str, object] from its mixed literal, which made every .get()
unusable at the far end and accounted for most of provider_runtime's errors
too. And three "Missing return statement" reports that were one thing:
_translate_chat_exception's docstring says "Always raises … never returns
normally"
— which is what NoReturn is for.

A real bug, fixed

backend/harness/transcript.py did from graphlink_version import __version__.
There is no __version__graphlink_version.py holds one line, APP_VERSION
and the name appears nowhere else in the repo. The import raised ImportError
on every call, the surrounding except Exception swallowed it, and every
harness transcript ever written recorded "app": ""
instead of the version
that field exists to record. about.py, diagnostic_bundle.py and
workspace_archive.py all import the right name. Verified before and after: the
meta record now says v1.0.8.

The one coercion

AnthropicProvider and GeminiProvider take api_key: str; the snapshot's is
str | None — genuinely, since API_KEY starts as None and only
initialize_api sets it. Eight call sites now pass state.api_key or "". Both
providers store the value verbatim and send it as a header, so None and ""
both end in an auth failure; "" is the one the type system can describe, and it
fails the same way rather than as a TypeError inside the SDK.

Everything else is annotations, renamed locals that had been reused at two
different types, and containers that never said what they held.

On the 28 # type: ignores

Every one carries the invariant it rests on — sqlite's cursor.lastrowid being
Optional only before an INSERT, the optional-dependency x = None fallback,
duck-typed attributes whose declared home is a subclass. I read all 28 and
verified the reasoning, including running reasoning_budget_hint to confirm the
one that claimed "low" and "high" can never return None.

backend/knowledge_embeddings.py is worth singling out for what was not
done: embed() lives on two of the five providers, so widening the shared
Provider protocol would have broken the other three. It gets a local two-line
protocol and a cast instead — guarded, as it already was, by an explicit
if not provider.capabilities.embedding: raise before each call.

Gate integrity

[tool.mypy] gains follow_imports = "silent"; without it, listing a clean
module drags its whole import closure into the gate. Verified this does not
neuter it by injecting a deliberate type error into five different modules —
api_provider, session_save, seed_demo_graph, graphlink_desktop,
provider_runtime/reasoningcaught every time.

Result

audit start now
mypy errors 1,059 0
Files under the CI type gate 4 339
Outbound network connects per run 102 0
Python suite 3,138 3,285

Test plan

  • Full suite: 3,285 passed, 20 skipped.
  • ruff check . clean. mypy clean across 339 source files.
  • Both new gates verified non-vacuous by removing what they guard.

🤖 Generated with Claude Code

dovvnloading and others added 2 commits September 4, 2026 14:59
PR #413 fixed one test that opened a TCP connection to 127.0.0.1:11434 and
noted nine other files that set OLLAMA_MODELS the same way but had not been
checked. Instrumenting socket.connect across a full run answered it: the
suite made 102 outbound connects to that port.

  46x  _get_ollama_capabilities  <- ollama_supports_tools
  46x  _get_ollama_capabilities  <- ollama_supports_embedding
  10x  _get_ollama_context_window <- _ollama_effective_context_window

#413 only closed the third one, and only inside test_providers.py.

Two things made the other 92 expensive rather than merely wrong. Each
attempt costs a ~2s TCP connect on a machine with nothing listening. And
_get_ollama_capabilities deliberately does not cache a probe failure -
correct in production, where a daemon may simply have been restarting, but
it means every check pays that cost again rather than once.

It was also a quiet correctness problem: a developer running Ollama locally
got different answers out of these three functions than CI did, from the
same code, and nothing in the suite would have shown it.

backend/tests/conftest.py now makes ollama.show raise, next to the existing
_never_touch_the_real_user_data_dir fixture and for the same reason. That
is the same "daemon unavailable" outcome the failed connect produced, minus
the socket. Patched on the real module rather than replacing
api_provider.ollama wholesale, because other code reaches through that
module object for ollama.chat; tests that want their own show()
(test_context_budget.py) override it and still win.

backend/tests/test_no_network_in_tests.py asserts the fixture is working,
phrased as "no socket was opened" rather than "show raised" so it keeps
holding if the probing moves to another client or another function. It also
pins that the stub still answers "unavailable" rather than fabricating a
capability set, and that a test can still supply its own show().

Verified: with the fixture removed, all three socket assertions fail. With
it, a full run makes ZERO outbound connects - the only remaining
socket.connect calls in the suite are asyncio's own Windows self-pipe.

The suite also got faster, which was not the point but is worth recording:
103.2s to 84.0s, about 19%.

Test plan: full suite, 3,285 passed / 20 skipped. ruff clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The last of the type sweep. `mypy backend` went from 179 errors to zero,
and [tool.mypy].files - four files when this audit started - now lists
every Python root in the repo: 339 source files, checked in CI.

WHAT WAS ACTUALLY WRONG, because "179 type errors" is not a description:

api_provider.py held 32 of them and was the module worth the most care.
Four `base_url: str = None` signatures that PEP 484 has prohibited since
2018. Three module-level caches with no declared shape - one of which,
_LLAMA_CPP_CLIENT_CACHE, I first annotated dict[str, Any] and mypy
rejected, because llama_cpp_runtime keys it by a five-tuple. LLAMA_CPP_
SETTINGS inferred as dict[str, object] from its mixed literal, which made
every .get() on it unusable at the far end and accounted for most of
provider_runtime's errors too. And three "Missing return statement" reports
that were all one thing: _translate_chat_exception's docstring says
"Always raises ... never returns normally", which is exactly what NoReturn
is for.

A REAL BUG, found by the types and fixed here. backend/harness/
transcript.py did `from graphlink_version import __version__`. There is no
__version__ - graphlink_version.py holds one line, APP_VERSION - and the
name appears nowhere else in the repo. The import raised ImportError on
every call, the surrounding `except Exception` swallowed it, and every
harness transcript ever written recorded "app": "" instead of the version
that field exists to record. about.py, diagnostic_bundle.py and
workspace_archive.py all import the right name. Verified before and after:
the meta record now says v1.0.8.

THE ONE COERCION, called out because it is the only line here that changes
what runs: AnthropicProvider and GeminiProvider take api_key: str, and the
snapshot's api_key is str | None - genuinely, since API_KEY starts as None
and only initialize_api sets it. Eight call sites now pass
`state.api_key or ""`. Both providers store the value verbatim and send it
as a header, so None and "" both end in an auth failure; "" is the one the
type system can describe, and it fails the same way rather than as a
TypeError inside the SDK.

Everything else is annotations, renamed locals that had been reused at two
different types, and containers that never said what they held. 28
`# type: ignore` comments were added and every one carries the invariant it
rests on - sqlite's cursor.lastrowid being Optional only before an INSERT,
the optional-dependency `x = None` fallback, duck-typed attributes whose
declared home is a subclass. I checked each; the reasoning holds in all 28.

backend/knowledge_embeddings.py is worth singling out for what was NOT
done: embed() lives on two of the five providers, so widening the shared
Provider protocol would have broken the other three. It gets a local
two-line protocol and a cast instead, guarded - as it already was - by an
explicit `if not provider.capabilities.embedding: raise` before each call.

[tool.mypy] also gains follow_imports = "silent". Without it, listing a
clean module drags its whole import closure into the gate; with everything
listed it is belt and braces for whatever is added next. Verified the gate
still bites by injecting a deliberate type error into five different
modules - api_provider, session_save, seed_demo_graph, graphlink_desktop,
provider_runtime/reasoning - and it was caught every time.

Test plan: full suite, 3,285 passed / 20 skipped. ruff clean. mypy clean
across 339 source files.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@dovvnloading
dovvnloading merged commit c7fc38a into main Sep 4, 2026
5 checks passed
@dovvnloading
dovvnloading deleted the types/tail-to-zero branch September 4, 2026 22:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant