The test suite uses only the standard library (no mocking frameworks, no network calls).
python3 -m pytest tests/Or without pytest:
python3 -m unittest discover tests/On macOS, keep bytecode caches out of the working tree — Spotlight indexes stray .pyc
files, and they outrank the Glean Code.app launcher in Cmd+Space:
export PYTHONPYCACHEPREFIX="$HOME/.cache/python"1,151 tests covering the client and every mock response, commands and dispatch, config, UI, auth, completion, help docs, the mock corpus, indexing-walk, scaffold, the installer, the MCP server, the flow mapper, the Pages site builder, the knowledge graph, and Glean Personal (text extraction, the index, the content graph, ranking explanations, local mode, and the local MCP tools).
Notes on the test suite added during development of glean-code-cli.
All 1,151 tests pass. Here's what was added across the development passes:
tests/test_commands_extended.py (155 new tests) — covers all previously untested commands:
/status,/clear,/help/datasources.list(all flag combinations),/datasources.status,/indexing.rotate-token/autocomplete,/recommendations,/feedback,/entities.list/pins.delete,/collections.delete/shortcuts.list,/shortcuts.get,/shortcuts.create,/shortcuts.update,/shortcuts.delete(full flag coverage)/answers.list,/answers.get,/answers.create,/answers.update,/answers.delete(int ID validation, arg passing)/summarize(by URL, by ID, query flag)/verification.list,/verification.verify,/verification.remind(flag passthrough, defaults)/messages.get,/activity.report,/insights(CSV export, all flags)/scaffoldwith--outputflag, cancellation paths_fmt_ts,_render_insights,_export_insights_csv,_print_datasource_status,Session.refresh_client
tests/test_client_extended.py (31 new tests) — covers all new client methods and mock responses:
- Mock responses for all 19 new endpoints (
/unpin,/listshortcuts,/listanswers,/summarize,/insights, etc.) GleanClientmethod bodies: correct paths, partial updates, optional fields, body construction
tests/test_indexing_walk.py (25 new tests) — covers the --path indexing helpers and command flow:
path_to_id,filename_to_title,mime_for_path— slug, title, and extension detectionwalk_files— default include/exclude behaviour,node_modules/.git/.DS_Storefiltering,--max-bytesskip, single-file root, missing-path errors, custom--includeoverridesfile_to_document— Markdown/HTML body shape, view-URL prefix override, unsupported-extension rejection/index.document --path— synthesizes aDocumentDefinition, dry-run skips the API, directory-passed errors, missing-permissions errors, mutex with--from-file, mutex of--publicand--acl-from-file/index.bulk-documents --path— folder walk produces a pagedBulkIndexDocumentsRequest,--includefiltering, dry-run, backward compat with--from-file
tests/test_commands_extended.py and tests/test_client_extended.py (33 new tests) — covers the Custom Metadata API surface:
GleanClientmethodsset_metadata_schema,get_metadata_schema,delete_metadata_schema,attach_metadata,detach_metadata— correct HTTP methods (PUT/GET/DELETE), correct paths under/rest/api/index, body shape, indexing-token requirement_mock_indexing_responsefor/custom-metadata/schema/{group}(schema-shaped GET, ack-style PUT/DELETE) and/document/{docId}/custom-metadata/{group}(ack-style PUT/DELETE)/metadata.set-schema— required--group, mutual exclusion of--from-fileand--keys, inline-key parsing (name:TYPE[:skip]), invalid-type rejection,--dry-runskip, no-token error, file-list-form parsing/metadata.get-schema,/metadata.delete-schema— required--group, no-token error, correct client-method invocation/metadata.attach— required--doc-idand--group, mutual exclusion of--from-fileand--values, inline-value parsing, malformed-value rejection,--dry-runskip, no-token error/metadata.detach— required flags, no-token error, correct client-method invocation
tests/test_mock_corpus.py (35 new tests) — covers the fake corpus behind mock mode:
- Ranking — relevant document first, distinct top hits for distinct queries, match-all ordered by freshness,
--datasourcefiltering, page padding when nothing matches, page-size cap - Snippets and metadata — snippet drawn from the sentence matching the query, author and relative freshness on every result
- Placeholders —
{Q}/{Q+1}/{FY}expansion, next quarter differs from current, no raw placeholders leak into results - Cross-endpoint coherence — a
/searchresult URL resolves through/getdocumentsand/summarizeto the same document,/chatcitations track the question,/getdocumentpermissionsowner is the document author,/peoplereads the roster - Custom corpus files —
mock_corpus_pathandGLEAN_MOCK_CORPUSoverrides (config wins), bare-array form, andCorpusErroron a missing file, invalid JSON, a document with no title, or an empty document list
tests/test_install.py (10 new tests) — covers app-bundle ownership in the installer:
bundle_identifier/owns_bundle— readsCFBundleIdentifierout ofInfo.plist; a missing bundle counts as ours (nothing to clobber), a foreign identifier and an unreadable or binary plist do not- Refusal —
install_macos_appexits rather than writing into a bundle it did not create, leaving that bundle'sInfo.plistbyte-identical and creating noContents/Resources - Legacy cleanup — an old
Glean.appis removed on install when we own it, and left alone when it belongs to another app - Uninstall — removes our own bundle, and never deletes a foreign one (the regression that would have deleted a user's Glean Desktop install)
- The default
APP_DIRis asserted not to beGlean.app
tests/test_mcp.py (9 new tests) — covers mock mode on the MCP server:
_build_client— forces live mode whatevermodethe config file carries (includingauto), and switches to mock only whenGLEAN_MOCKis set; truthy spellings (1,true,yes,on) accepted, everything else ignored- Labelling — all four tools (
search,chat,list_agents,run_agent) prefix their response with the[MOCK MODE]banner when serving fake data, including empty-result responses, and never in live mode - Tool descriptions — every tool docstring names
GLEAN_MOCK, so the warning reaches the agent before it calls anything
tests/test_mcp_control.py (36 new tests) — covers /mcp server control:
- Package diagnostics — reports the installed
mcpversion and whether it can actually run the server (v1 providesmcp.server.fastmcp; v2 does not) - State file — roundtrip, unreadable-file tolerance, and clearing a stale entry when the recorded pid is dead or has been reused by another process
startrefusals — stdio (which needs a client on the other end), unknown transports, missing package, an incompatible v2 install named by version, and a second start while one is runningstop— signals and clears state; a no-op when nothing is running- Client config — the stdio command form and the URL form, both JSON-serialisable
- Defaults — loopback-only bind, non-stdio default transport, and tool names checked against
glean_mcp.pyitself - Command dispatch — bare
/mcpshows status, unknown subcommands and clients error,--urlwithout a server errors, a non-numeric--porterrors
Every test redirects the state and log paths at a temp directory, so ~/.gleancode/ is never touched.
tests/test_flow.py (58 new tests) — covers the flow mapper:
- Capture gating — the
flow_capturekey defaults tomock, so live tenant content is never recorded by accident;onrecords both modes andoffrecords nothing - Capture — chat turns, citations, and search snippets land in SQLite; a shared
chatIdis one session and a new one starts another; every row is tagged with instance and mode; a capture failure cannot break the API call - Enrichment — document text is fetched, and comes from
/getdocumentsnow that the mock returns a body - Linking — identifier links are exact and score 1.0; two sessions with no shared wording connect through a document that mentions the other's subject in passing; link evidence is asserted to be specific rather than generic; links never cross an instance or mode partition
- Title anchoring — the scorer prefers a word from the other document's title over a rarer word that appears nowhere meaningful, which is what stops "across" and "percent" being offered as evidence
- Queries and purge — summary, collapsed, and single-session views are JSON-serialisable; purge is scoped to a partition or clears everything
- Rendering — the timeline is self-contained (no external references), well-formed HTML, badges mock data, renders connections with their evidence, and handles an empty database
- Command dispatch — bare
/flowshows status, unknown subcommands error, and bad--min-score/--limit/--docsvalues are rejected - Schema migration — a database written before
session_linkscarriedto_docgains the column on open, keeps its existing rows, and can be reopened repeatedly without the migration running twice /flow showrendering — sessions hang off a vertical rail, documents are labelled with their datasource, and a connection branches off with both document titles, the shared evidence, and the session number it reaches- Colour is decoration — with colour disabled the output contains no escape sequences and every structural glyph (
●─,│,├──◆,↓,▪) is still present, so the shape survives being piped - Width — no line exceeds the terminal width at 40, 58, 84, or 120 columns, with colour on and off. Measured on visible width, since ANSI escapes have zero display width and
len()would pass a broken layout - Ordering — informative links (
linked-document) come before trivial ones (shared-citation) regardless of score, verified against a database built by running the same investigation twice; documents a thread returned to lead the list, and singly-cited ones keep citation order rather than being interleaved by rank - Overflow —
--linkscaps connections per session and counts the remainder, and both--docsand--linksreject non-integers - Datasource colours — known sources each get a distinct colour, lookup ignores case and padding, and an unknown source falls back to grey rather than borrowing a familiar source's colour
The module patches out the mock client's simulated 0.25s network latency; without that these 58 tests take 31 seconds instead of 2.
tests/test_extract.py (26 tests) — covers local text extraction:
- Registry —
INCLUDE_PATTERNSis derived fromSUPPORTED_EXTS, so the walker's globs cannot drift from what the extractor actually handles; an unsupported extension raises with the supported list in the message - Normalisation — spaces and tabs collapse but paragraph breaks survive, because chunking splits on them; line endings normalise; an oversized document is truncated with a visible marker
- HTML — tags are stripped,
<script>and<style>bodies are dropped rather than indexed, and<title>becomes the document title - JSON — flattened to
key.path: valuelines so both halves are searchable; invalid JSON is indexed verbatim rather than discarded, since JSONL and truncated exports are still worth finding - Office — every fixture is built with
zipfilerather than committed as a binary, which keeps the repo text-only and documents exactly which parts of each format the extractor depends on. Word runs join within a paragraph and paragraphs stay separate; Excel reads shared strings, inline strings and sheet names; PowerPoint ordersslide2beforeslide10, which lexicographic sorting would get wrong - Failure modes — a file that is not a zip, malformed XML inside a valid zip, a missing
word/document.xml, an out-of-range shared-string index, and an archive declaring more uncompressed content than the limit allows. Each raisesExtractErrorwith a reason rather than crashing an index run - Part ordering — sheet names and slide order resolve through each part's relationships, not filenames. A reordered workbook must not pair a real sheet name with another sheet's content, a moved slide must not be cited under its old number, and a workbook with no usable relationships must fall back to generic
Sheet Nlabels rather than a confidently wrong name
tests/test_personal.py (163 tests) — covers Glean Personal:
- Schema — tables,
metaversioning,0600permissions, and idempotent reopening - FTS5 fallback — a forced-broken FTS5 schema exercises the plain-table path end to end: indexing, search and fetch all still work, and an existing database keeps the store it was built with even once FTS5 is available again, because switching would orphan every chunk
- Chunking — headings split sections, a heading with no body is still indexed, paragraphs pack up to the limit, and only an oversized paragraph falls through to sentence windowing with overlap. A single sentence longer than the limit is still cut
- Query building — bare terms become quoted prefix matches and a
"quoted span"stays a phrase, so a strayNEAR,OR,*or-x:yin user text is data rather than FTS5 syntax - Incremental indexing — a second run changes nothing; an edited file is re-read and its neighbours are not; a file touched but not changed is skipped on its content hash;
--reindexforces a re-read; a deleted file leaves the index; filters persist across runs so a later bare re-index repeats them - Skip reasons — oversized files, empty files, and a corrupt
.docxare reported rather than crashing the run, and unsupported extensions are never matched in the first place - Doc ids — two paths that slug to the same string are disambiguated
- Search — one row per document rather than per chunk, source filtering, limits, snippets forced to a single line (structured formats carry no sentence enders and would otherwise emit a whole multi-line chunk into an aligned layout)
- Content graph — related documents link and unrelated ones do not, evidence is stored and returned, a higher threshold yields fewer links, and re-linking replaces rather than accumulates
- Glean shapes —
/search,/chat,/autocompleteand/getdocumentsresponses match the Client API's shapes, which is what lets every existing renderer draw them unchanged - Local mode — routing, an explicit error for endpoints a folder of files cannot honestly answer, the Indexing API explaining itself before asking for a token it will never need, and an assertion that
urlopenis never reached - Commands — every
/personalsubcommand, bad flag values, the purge confirmation prompt in both directions, and/mode localon a populated and an empty index - Graph scale — near-duplicate documents must link at full strength, and phrase pruning must be symmetric across documents. Both are regressions with a measured origin: a per-document rank cut left two real 20 KB decks that shared 1,495 phrases sharing none, scoring the strongest link in the corpus at 0.0. The symmetry test was verified to fail against the broken implementation before being kept
--explain— matched and missed terms (including the porter-stemmed plural/singular case the re-derivation has to cover), passage counts as a real fraction of the document, tie grouping, score-ratio ordering, bar clamping at every ratio, sparse payloads, empty result sets, and that the default result carries noexplainkey so the Client API shape stays byte-compatible- Response shapes —
/autocompleteand/getdocumentsare asserted against the mock's shape rather than a hand-written expectation, since the whole point of the local adapters is byte-compatibility with the Client API. Both originally diverged, and the original tests locked the divergence in - Unicode — non-English content is searchable, and terms survive tokenisation intact rather than being mangled to ASCII fragments
- Fallback recall — a rare term stays reachable on a 391-chunk plain-store index even alongside a term that 130 other documents share. This needs the scan window, a prefilter covering every term, and rarity weighting all working together; each was broken independently
- Mode plumbing — indexing commands explain local mode rather than asking for a token that would not help,
--dry-runstill works with no credentials in any mode, and/askfalls back to the local pattern-matcher instead of advising/loginfrom inside local mode
tests/test_mcp.py (56 tests, +17 for Glean Personal) — covers the four local MCP tools:
- Each tool's output carries the
[LOCAL INDEX]banner, and an empty index names the command that fixes it - The local tools never call the Glean client and are unaffected by
GLEAN_MOCK, which governs the Glean-backed tools only — the local index has no fictional alternative to serve - A corrupt database returns an error string rather than a traceback
The linking fixture deliberately uses five documents rather than two. With two, every shared phrase appears in every document, so IDF cannot distinguish "topically shared" from "common vocabulary" and nothing can score above the threshold — a property of IDF at that scale, not a bug worth distorting the scoring to hide.
tests/test_spec_conformance.py reads glean_code/client.py with ast and asserts
that every REST path and HTTP method it calls exists in Glean's published OpenAPI
spec. It compares against tests/spec_manifest.json rather than the live spec, so
it stays offline like the rest of the suite.
Regenerate the manifest when Glean ships a spec update:
python3 -m pip install pyyaml # tooling-only dependency
python3 tools/refresh_spec_manifest.py
python3 -m unittest tests.test_spec_conformance -vThe check exists because twelve Client API calls once drifted from the spec
undetected: _mock_response is keyed on the same path strings the live client
posts to, so a wrong path was wrong in both places and every test passed. It
catches wrong paths, wrong methods, and invented endpoints.
Two deliberate limits:
- It does not require the client to call every documented endpoint. Plenty are intentionally unimplemented.
- It does not check request body fields. Those are covered by
TestSpecConformantBodiesintests/test_client_extended.py.
Endpoints the spec declares but leaves undocumented (currently
/indexemployeelist) are tracked in the manifest's undocumented section and
allowed, but the test fails if one becomes documented again — the signal to drop
the exception.