Fix: import ADK symbols from source in core/test and pin the convention (281 -> 139 type errors) - #514
Open
AmaadMartin wants to merge 4 commits into
Open
Fix: import ADK symbols from source in core/test and pin the convention (281 -> 139 type errors)#514AmaadMartin wants to merge 4 commits into
AmaadMartin wants to merge 4 commits into
Conversation
added 4 commits
August 2, 2026 02:49
…pecifier core/test mixed two ways of importing ADK symbols: 125 statements used the package specifier '@google/adk' and 3 used deep subpaths under it, while other files imported relatively from ../../src. TypeScript resolves the package specifier through the node_modules/@google/adk workspace symlink to the built declarations in core/dist/types, so it saw two structurally identical but nominally distinct copies of every class carrying a private member or a brand symbol. That produced 124 spurious errors of the "separate declarations of a private property" and "Property '[BASE_AGENT_SIGNATURE_SYMBOL]' is missing" families under tsc --noEmit. No such duplication exists at runtime: vitest.config.ts already aliases '@google/adk' to ./core/src for every project, so the suite has always loaded exactly one copy, from source. Only TypeScript disagreed with the runtime. Rewrite the 128 affected import statements to depth-correct relative paths into core/src, so tsc sees the same single copy vitest does, and run prettier so the rewritten specifiers land in their new sort order. The 3 deep subpaths are additionally unresolvable for a real consumer: core/package.json exports declares only ".". tsc --noEmit: 281 -> 157 errors. Test-only change; no src file is touched and no runtime module graph changes.
…s in core/test
runner_test.ts wrote `as unknown as Content['parts']![0]` at eight sites. `!`
is a value-position non-null assertion and is not valid in a type position, so
tsc rejected each one with TS17019 and the eight object literals they annotated
went unchecked as a knock-on effect. esbuild strips type annotations without
parsing them, which is why vitest never noticed.
Rather than repair the cast syntax, delete the casts. Every one of the eight
annotated a `{inlineData: {mimeType, data, displayName}}` literal, and all
three fields are declared on Blob in @google/genai, so each literal is already
a valid Part. The enclosing `const newMessage: Content` contextually types the
array, so removing the cast makes the literals genuinely checked against Part
instead of laundered through `unknown`. A typo in `mimeType` is now a compile
error where before it was silently accepted.
Both `@ts-expect-error` directives that the previous commit made unused are
removed, as TS2578 otherwise fails the build:
- functions_test.ts blamed "toolsDict ... imported from the source and BaseTool
imported from '@google/adk'" -- exactly the module-identity split that no
longer exists.
- runner_test.ts claimed to suppress an omitted `appName`, but appName is
declared optional on RunnerOptions (it can arrive via app.name) and the error
it actually suppressed was the duplicated sessionService type. The test still
exercises the real runtime guard that throws when appName is absent.
tsc --noEmit: 157 -> 139 errors. Zero TS17019 and zero TS2578 remain; both
files now type-check completely clean.
The previous two commits removed every '@google/adk' specifier from core/test, but nothing stops one reappearing -- and one silently reintroduces the whole dist-vs-src duplication for whichever types it pulls in, with no test failure to signal it, because vitest aliases the specifier to core/src and so keeps passing. Restrict '@google/adk' and '@google/adk/**' under core/test/**/*.ts, with a message that states the fix and the reason. Scoped to core/test only: dev/test still imports the package specifier in 8 files and is out of scope here. Cover the guard with a test that runs the real ESLint against the real eslint.config.js via lintText, which resolves config for a virtual path and so writes nothing to disk. It pins all three properties the guard needs: that it fires on the package root, a type-only import, a re-export and a deep subpath; that it stays silent on the relative path the convention mandates and on non-ADK packages; and that it does not leak outside core/test. Each assertion was verified to fail against a mutated config -- deleting the block fails the four rejection cases, widening `files` to '**/*.ts' fails the scoping case, and broadening the group to '**' fails the two negative cases.
core/test/tools/example_tool_test.ts had one test whose subject was the import specifier itself: 'is importable from @google/adk (public export)'. Rewriting its import to '../../src/index.js' left the title asserting something the file no longer does, and moved the test's guarantee from the published package surface down to the source barrel, where nothing was left pinning the former. Retitle it to 'is exported from the package barrel', which is what it now checks, and leave its assertion alone. Restore the lost guarantee as a new test under tests/integration, where the package specifier is still legal and the core/test ESLint guard deliberately does not reach. tsc resolves '@google/adk' to the declarations in core/dist/types, so a symbol dropped from the built package fails there even while the source barrel still exports it -- a check core/test can no longer perform by construction. The new test drives ExampleTool through real framework objects (a real LlmAgent, InvocationContext, Session and Context, no casts and no stubs) and asserts the few-shot block actually lands on the outgoing request, so it is not the tautology the old one had become. Verified against two mutations: dropping the ExampleTool re-export from core/src/common.ts fails it with "ExampleTool is not a constructor", and short-circuiting processLlmRequest fails the systemInstruction assertion. tsc --noEmit stays at 139 errors: the new file contributes none, which is itself the evidence that it type-checks against the published declarations.
This was referenced Aug 2, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Please ensure you have read the contribution guide before creating a pull request.
Link to Issue or Description of Change
Closes: #issue_number
Related: #issue_number
Problem:
npm run ts:check(tsc --noEmit) is meant to be the repo-wide type-check gate, but it fails with 281 errors, so it is not wired into.github/workflows/validation.yaml. The test tree is therefore only ever run through Vitest, which transpiles with esbuild and does no type checking at all. Real bugs hide behind that gap.About 124 of the 281 are not real type errors — they are an artefact of
core/testmixing two import conventions. 125 statements imported ADK symbols from the package specifier'@google/adk', which TypeScript resolves through thenode_modules/@google/adkworkspace symlink to the built declarations incore/dist/types, while other statements imported the source at'../../src/…'. TypeScript then saw two structurally identical but nominally distinct copies of every class carrying a private member or a brand symbol, producingTypes have separate declarations of a private property 'updateSessionState'andProperty '[BASE_AGENT_SIGNATURE_SYMBOL]' is missing…. The repo already documented the confusion in a suppression comment atcore/test/agents/functions_test.ts:712.This duplication does not exist at runtime:
vitest.config.tsaliases'@google/adk'to./core/srcfor every project, so the suite has always loaded exactly one copy, from source. Only the type checker disagreed with the runtime.Two clusters behind the remaining errors were outright bugs the transpile-only pipeline hid:
core/test/runner/runner_test.tswroteas unknown as Content['parts']![0]at 8 sites.!is a value-position non-null assertion and is invalid in a type position (TS17019×8). esbuild strips annotations without parsing them, so this never surfaced.core/test/sessions/vertex_ai_session_service_test.tsimported 3 deep subpaths (@google/adk/sessions/session.js, …).core/package.jsonexportsdeclares only".", so these are unresolvable for a real consumer (TS2307×3); Vitest resolved them only via its directory alias.Solution: Settle the convention:
core/testimports ADK symbols from the source tree via relative paths, which is exactly what Vitest already loads, so the type checker and the runtime finally agree.This is step 1 of a series. It deliberately does not try to reach zero — it clears the module-identity class plus the two hidden-bug clusters, and leaves the 136 genuine
core/testtype errors for follow-ups so each PR stays reviewable.from '@google/adk'→ a depth-correctfrom '../../src/index.js'; the 3 deep subpaths →../../src/<subpath>. The rewrite was anchored to specifiers that terminate animport/export … fromstatement, so the two prose occurrences of the text@google/adk(a test description attools/example_tool_test.ts:152and the suppression comment) were not corrupted. Non-ADK specifiers (@google/genai,@google-cloud/vertexai,vitest,zod) are untouched.prettierwithprettier-plugin-organize-importsreorders each rewritten specifier into its new sort slot and merges duplicated import blocks. This is why most files show more than a one-line change.TS17019fixed by deleting the casts, not re-typing them — see Deviations below.@ts-expect-errordirectives removed (they becameTS2578once the above landed).Why relative imports rather than a
tsconfigpathsmapping: a root-levelpathsmapping reaches a similar residual, but it was measured to introduce 7 newTS2322errors indev/src/conformance/conformance_integrations.ts— real errors, invisible today only because--emitDeclarationOnlystrips the types ofprivatemembers and erasesFunctionTool's contravariance inTParameters. Worse,pathsis inherited bydev/tsconfig.jsonandintegrations/tsconfig.json(bothextends: "../tsconfig.json"), so it would change what those packages emit into their published.d.ts. The relative convention gets the identical benefit with none of that blast radius.Collision check (required disclosure). I checked all 412 open PRs on the fork before writing any code (
gh pr list --repo AmaadMartin/adk-js --state open --limit 1000) and read the diffs of every plausibly adjacent one. This area is crowded: at least 11 open PRs targetnpm run ts:check— #178, #204, #248, #293, #294, #326, #370, #408, #414, #421, #487.The closest is #487 ("type-check core/test against core/src and fix the 154 errors it surfaces"). It reaches a similar end state by a different mechanism: a
core/test/tsconfig.jsonwhosepathsmapping mirrors the Vitest alias, keeping the@google/adkspecifier in test files. It also fixes the same 8TS17019sites and removes the same 2 stale directives, and goes further by fixing the genuine type errors this PR defers.These two PRs are alternatives, not complements, and only one should land. They are mutually exclusive by construction: this PR's ESLint guard forbids exactly the specifier #487's
pathsmapping exists to bless. I am raising this explicitly so a maintainer can choose the mechanism rather than discover the conflict at merge time. The trade-off:core/teststops depending oncore/dist/typesentirely — and the convention is machine-enforced. Cost: a 124-file mechanical diff that will conflict textually with other in-flightcore/testPRs.@google/adkspecifier. Cost: a second tsconfig plus a rootexclude, the type-check path stays distinct from the ordinarytsc --noEmit, and nothing prevents a deep-subpath import from reappearing.Related but not overlapping: #435/#325 already restrict deep
@google/adk/*subpath imports repo-wide. This guard iscore/test-scoped and also covers the package root, so it is a superset there; the two rules would coexist without conflict.I did not stack on #487 because a stack would require rewriting the imports its
pathsmapping was added to support, i.e. reverting its central design decision, and because a stacked base gets no CI on this fork.Deviations from the approved plan (disclosed, both make the change stricter — no scope was reduced):
as unknown as NonNullable<Content['parts']>[0]. I deleted them instead. Every one of the 8 annotated a{inlineData: {mimeType, data, displayName}}literal, and all three fields are declared onBlobin@google/genai(node_modules/@google/genai/dist/genai.d.ts:1135-1143), so each literal is already a validPart. The enclosingconst newMessage: Contentcontextually types the array. Keeping anyas unknown as …form would have preserved an unchecked cast and left the literals unverified; deleting it makes them genuinely checked, so a typo inmimeTypeis now a compile error. This is a smaller diff and removes 8 casts rather than repairing them.tests/integration/repo_config/eslint_core_test_import_convention_test.ts, 7 tests). A spot-check leaves nothing behind, and the guard is the only genuinely new behaviour here. It runs the real ESLint against the realeslint.config.jsviaESLint#lintText, which resolves config for a virtual path and writes nothing to disk — no temp files, no cleanup path.["@google/adk", "@google/adk/**"], not["@google/adk", "@google/adk/*"]. Both were measured to behave identically against a deep subpath on ESLint 9.37;\*\*is used for explicitness. Both the baseno-restricted-importsand@typescript-eslint/no-restricted-importswere also measured and catch all four forms includingimport type; the base rule is used, as specified.Review revision -- the one test whose subject was the import specifier.
core/test/tools/example_tool_test.tscontainedit('is importable from @google/adk (public export)'). Repointing its import at../../src/index.jsmade that title false and silently demoted the guarantee from the published package surface to the source barrel, leaving the former covered by nothing. Fixed in two parts, without deleting anything:'is exported from the package barrel'-- what it now actually checks. Its assertion is unchanged.tests/integration/tools/example_tool_test.ts, where the package specifier remains legal and the new guard deliberately does not reach.tscresolves@google/adkthere tocore/dist/types, so a symbol dropped from the built package fails it even while the source barrel still exports it -- a checkcore/testcan no longer perform by construction. It drivesExampleToolthrough real framework objects (a realLlmAgent,InvocationContext,SessionandContext; no stubs, no casts) and asserts the few-shot block lands on the outgoing request, so it is not the tautology the old one had become.core/testnow contains zero occurrences of the string@google/adk, in code or prose.Not shipped as a stack, despite the file count: 124 of the 126 files carry a uniform 1-2 line mechanical import rewrite, splitting it would leave intermediate commits in which the module-identity errors are only partly cleared (no coherent checkpoint), and a stacked base gets no CI on this fork. The three commits provide the review checkpoints instead.
Testing Plan
Please describe the tests that you ran to verify your changes. This is required for all PRs that are not small documentation or typo fixes.
Unit Tests:
[x] I have added or updated unit tests for my change.
[x] All unit tests pass locally.
Baseline re-measured first. The plan pinned
mainat 281 errors; this branch's base is 44 commits later, so the precondition was re-checked before starting:npx tsc --noEmit→ 281 errors (278core/test, 3tests/integration;TS2345127,TS232253,TS274135,TS233925,TS170198,TS27396,TS23074) — an exact match, so the plan's numbers still applied.Type-check progression (
npx tsc --noEmit, afternpm run build):fork/main,b390217e)Final 139 = 136
core/test+ 3tests/integration, matching the planned postcondition. Verified at the pushed commit:TS17019: 0TS2578: 0TS2307incore/test: 0separate declarations of a private property,[*_SIGNATURE_SYMBOL]' is missing) incore/test: 0@google/adkimport specifiers undercore/test: 0The 2 remaining
separate declarationserrors and the 1 remainingTS2307are both intests/integrationand are pre-existing, out-of-scope items (a nested@google/genaicopy bundled by@google-cloud/vertexai, andresponseProcessornot being re-exported).Residual
core/testerrors for the follow-ups:tools37,models37,utils16,code_executors10,auth9,sessions5,agents5,integrations4,events4,context3,apps3,plugins2,memory1.No runtime behaviour changed — measured, not assumed.
npx vitest run --project unit:core:fork/mainbaseline: 168 files / 2351 tests passed — identical.Full CI suite (
npm run test:coverage) run on both branches and the failure sets diffed. The only difference istests/integration/app_loader/app_loader_test.ts, which is a pre-existing flake, not a regression: run 3× in isolation it gave fail/pass/fail on this branch and pass/fail/pass onfork/main. It fails withError: Hook timed out in 40000msin an install-heavybeforeAll, and this PR touches nothing it loads. Every other failing file is identical on both branches (tests/e2e/**needs live Gemini credentials;agent_dirname,skills/script_js,cli_createare install/sandbox-dependent).Coverage thresholds could not be evaluated because those pre-existing failures abort the run identically on both branches. They are unaffected by construction:
vitest.config.tscomputes coverage over**/src/**only, and this PR modifies nosrcfile.Proof the new tests can fail. All 7 guard tests were run against three mutated configs:
eslint.config.jsAssertionError: expected [] to have a length of 1 but got +0filesfromcore/test/**/*.tsto**/*.tsAssertionError: expected [ Array(1) ] to deeply equal []groupto["**"]AssertionError: expected [ …(2) ] to deeply equal []The restored package-specifier test was mutation-checked the same way: dropping the
ExampleToolre-export fromcore/src/common.tsfails it withTypeError: ExampleTool is not a constructor, and short-circuitingprocessLlmRequestfails thesystemInstructionassertion.core/srcwas restored and re-verified clean after each.Every assertion in the file is pinned by at least one mutation; the guard was restored and re-verified at 7/7 passing after each.
The guard was additionally verified to fire on the package root, a type-only import, a re-export and a deep subpath; to stay silent on
@google/genai,zodand the relative path; and to remain scoped — the 8dev/testfiles that still import@google/adklint clean.Manual End-to-End (E2E) Tests:
Please provide instructions on how to manually test your changes, including any necessary setup or configuration.
All of the above were run locally on the pushed commit:
npm run buildexit 0,npm run lintexit 0,npm run format:checkexit 0,npx secretlint "**/*"exit 0.CI note (windows-latest). The three test jobs are green.
windows-latestfailed twice before passing on a third re-run of the same commit, always on the same unrelated case:core/test/code_executors/unsafe_local_code_executor_test.ts > 'should execute shell code and return stdout', timing out at ~5008ms. That test spawns a real Windows PowerShell and was measured at 3114ms on a windows-latest run that passed — vitest flags it slow — against vitest's 5000ms defaulttestTimeout, so it is inherently marginal and tips over with runner variance. This PR's only change to that file is the import specifier; the runtime module graph is identical, since Vitest already aliased@google/adktocore/src.windows-latestis also already red on the base commitb390217e(on a different test,tests/integration/adk_web/webui_test.ts). I deliberately did not bundle a timeout fix for it into this PR — it is unrelated churn — and queued it as separate follow-up work instead.Checklist
[x] I have read the CONTRIBUTING.md document.
[x] I have performed a self-review of my own code.
[x] I have commented my code, particularly in hard-to-understand areas.
[x] I have added tests that prove my fix is effective or that my feature works.
[x] New and existing unit tests pass locally with my changes.