Repository navigation
Description
Summary
clients/web/src/lib/ and clients/web/src/utils/ are both grab-bags whose names are industry-ambiguous. Empirically this codebase does follow a rule — utils/ = pure helpers, lib/ = plumbing — but it's undocumented and applied loosely, so a handful of modules sit on the wrong side of it. This issue sharpens the rule, writes it into AGENTS.md, and moves the misfiled modules.
The rule to codify
src/utils/— pure, side-effect-free functions. Input → output, no DOM/browser/storage I/O, no subsystem ownership. Trivially unit-testable with no mocks. (Anchors:jsonUtils,schemaUtils,toolUtils,maskSecrets,inspectorTabs.)src/lib/— infrastructure / integration / stateful adapters. Modules that instantiate or compose subsystems, wrap@inspector/core, touch the DOM /window/sessionStorage, or otherwise produce side effects. (Anchors:environmentFactorycomposesInspectorClientEnvironment;remoteOAuthStorageis an adapter class overcore/auth.)
One-liner for AGENTS.md: utils = functions that compute; lib = things that instantiate, adapt, or touch the environment. If it does I/O or wraps a subsystem, it's lib; if it's a pure transform, it's utils.
Confirmed misfiled (verified by reading the source)
src/lib/sandbox-csp.ts→src/utils/— pure CSP-string construction from an untrusted_meta.ui.csp; no side effects (only a type import fromext-apps).src/utils/browserTabVisibility.ts→src/lib/— readsdocument.visibilityStateand registersvisibilitychangelisteners (DOM side effects).src/utils/oauthResume.ts→src/lib/— reads/writeswindow.sessionStorage(stateful I/O).
Move each module's co-located *.test.ts with it.
Borderline — audit case-by-case during the move
A coarse grep for sessionStorage / document. / window. / @inspector/core flags these, but several are false positives (type-only imports, guarded typeof document SSR checks, pure URL parsing). Read each and place per the rule:
src/utils/pendingReauth.ts,src/utils/clearServerOAuthState.ts— likelysessionStorageI/O → probablylib.src/utils/deepLink.ts,src/utils/correlateTransportErrors.ts,src/utils/mcpNetworkHeaders.ts,src/utils/oauthUx.ts,src/utils/oauthFlow.ts,src/utils/oauthNetworkPhase.ts— likely pure (type-only core imports / URL parsing) → probably stayutils.src/lib/downloadFile.ts— mixed: the headlinedownloadBlob/downloadJsonFileare DOM side effects (stayslib), but the pure helpers it also exports (buildExportFilename,fileNameFromUri,isHttpUrl) areutils-shaped. Decide whether to split or leave whole (leaving whole inlibis acceptable).
Related straggler (in scope, cheap)
src/test/lib/downloadFile.test.ts breaks the side-by-side test convention — its three src/lib/ siblings (environmentFactory.test.ts, remoteOAuthStorage.test.ts, sandbox-csp.test.ts) are co-located with their source. Move it to src/lib/downloadFile.test.ts (or src/utils/ if downloadFile is reclassified). This is the only web-owned test currently living under src/test/ instead of beside its source.
The test-placement rule to codify (why src/test/ exists at all)
While we're documenting structure, capture the companion rule that the downloadFile.test.ts straggler violates, because it looks contradictory until spelled out: most tests live beside their source, yet there's also a src/test/ tree. These aren't competing conventions — src/test/ is specifically the home for tests whose source lives outside clients/web/src, plus shared test scaffolding. The rule:
- Side-by-side (
<Name>.test.tsxnext to the source) — the default for web's ownsrc/code. Components, hooks,lib/,utils/. This is the overwhelming majority. src/test/— three things that cannot be co-located:- Tests of the repo-root
core/package (src/test/core/…, ~99 files, mirroring thecore/folder layout).core/physically lives at/core, outsideclients/web/, is consumed via the@inspector/corealias, and has nopackage.jsonor test harness of its own. Its browser-facing behavior is tested here rather than incore/, because co-locating would pollute the shared, isomorphic package with web-only test infra (happy-dom,renderWithMantine, the web vitest config). - The
integrationvitest project (src/test/integration/…, ~49 files, node env, 30s timeouts). Placement in that folder is the manifest — the project's glob keys off the path — and it mostly exercisescore/(transports, auth, storage) over real test servers. - Shared test infrastructure —
renderWithMantine.tsx,setup.ts,fixtures/,scrollAreaStoryAssertions.ts. Not tests of a source file, so there is nothing to sit beside.
- Tests of the repo-root
One-liner for AGENTS.md: tests live beside their source, except tests for the repo-root core/ package (which lives outside clients/web/) and shared test scaffolding — both of which live under src/test/, with core/ tests mirroring the core/ layout and integration tests under src/test/integration/. (AGENTS.md already documents the core/-tests-under-src/test/core/ and integration-folder-is-the-manifest pieces; this makes the side-by-side-vs-src/test/ split itself explicit, so downloadFile.test.ts-style stragglers are unambiguous going forward.)
Acceptance
-
src/libvssrc/utilsrule documented inAGENTS.md(React/structure section) and, if useful,clients/web/README.md. - Test-placement rule (side-by-side vs
src/test/, and why) documented inAGENTS.md. - Confirmed-misfiled modules moved (source + co-located test), imports updated.
- Borderline files audited and placed with a one-line rationale in the PR.
-
downloadFile.test.tsrelocated beside its source. -
npm run cigreen (this is a pure move — no behavior change; coverage globs invite.config.tsdon't key offlib/utils, so no config change expected).
Notes
Nothing depends on the boundary — no path alias, no build/coverage config globs off lib vs utils — so this is a mechanical, behavior-preserving refactor. The alternative (collapse both into one dir) was considered and rejected: the pure-vs-plumbing signal is genuinely useful at import time in a codebase this test-heavy (90% per-file gate), and only ~3–4 files are actually misplaced.
Activity
refactor(web): address review — fix stale paths, move customHeaders, …
docs: address review round 2 — stale customHeaders refs, rule wording (…
docs: address review round 3 — scope the "no types/ bucket" claim (#1776
docs: address review round 4 — reconcile src/types/ claim with #1785 (#…
docs: address review round 5 — fix garbled #1785 clause, restructure …
docs+config: address review round 6 — correct the coverage-glob claim (…
refactor(web): sharpen the src/lib vs src/utils rule and move misfile…
cliffhall commentedon Jul 26, 2026
Merged via #1784 (squash into v2/main). Auto-close doesn't fire on the non-default v2/main branch, so closing manually per AGENTS.md.