Skip to content

Fix: run the orphaned unit:integrations vitest project from the root test scripts - #236

Open
AmaadMartin wants to merge 6 commits into
mainfrom
fix/run-unit-integrations-vitest-project
Open

Fix: run the orphaned unit:integrations vitest project from the root test scripts#236
AmaadMartin wants to merge 6 commits into
mainfrom
fix/run-unit-integrations-vitest-project

Conversation

@AmaadMartin

@AmaadMartin AmaadMartin commented Jul 29, 2026

Copy link
Copy Markdown
Owner

Please ensure you have read the contribution guide before creating a pull request.

Link to Issue or Description of Change

  1. Link to an existing issue (if applicable):

No existing issue. The gap originates in google#449, the PR that added the integrations package together with the unit:integrations vitest project.

  1. Or, if no issue exists, describe the change:

Problem: vitest.config.ts declares six projects, but the root test scripts selected only five. unit:integrations — the project that owns integrations/test/**/*_test.ts (vitest.config.ts:51) — was selected by nothing: no npm script, no workflow. The string appeared exactly once in the whole repository, in its own declaration:

$ grep -rn "unit:integrations" . --exclude-dir=node_modules
./vitest.config.ts:51:          name: 'unit:integrations',

.github/workflows/validation.yaml:41 runs npm run test:coverage, and .github/workflows/cross-language-integration.yml runs npm run test:cross-language; between them every project reached CI except this one. So integrations/test/ had never executed — not in CI, and not for a contributor following CONTRIBUTING.md's npm run build && npm test.

The blind spot had already rotted the one test living there. integrations/test/version_test.ts asserted expect(version).toBe('1.3.0') while integrations/src/version.ts:8 exports '1.4.0' at this branch's base. The drift is easy to trace: the test was introduced asserting '1.3.0' in google#449, and the later v1.4.0 release bumped integrations/src/version.ts through release-please's extra-files entry in release-please-config.json. release-please does not touch test files, and nothing ran the test, so nobody noticed.

The gap has since widened by another release, which is the clearest evidence that a literal is the wrong fix: main today exports '1.5.0' from integrations/src/version.ts while that file's assertion is still '1.3.0' — two releases stale. The derived assertion shipped here is unaffected by that, and was verified against it directly (durability proof 4 below).

The next re-rot is not hypothetical either. google#586 (Release: v1.6.0, branch release-please--branches--main) is open right now, and its diff moves both integrations/src/version.ts and integrations/package.json from 1.5.0 to 1.6.0 while touching integrations/test/version_test.ts not at all:

$ gh pr diff 586 --repo google/adk-js --name-only | grep integrations
integrations/CHANGELOG.md
integrations/package.json
integrations/src/version.ts        # test file absent, as always

So re-pinning the literal would turn that release PR red the moment it rebases — the identical breakage, except this time it blocks a release. Because the assertion is derived from the manifest google#586 bumps in the same commit, it survives untouched; durability proof 4 below runs exactly that 1.6.0 / 1.6.0 pair.

That is why the script change cannot land on its own — it would turn CI red. Both halves have to ship together.

Solution: three changes, in three files.

  1. Make the assertion self-maintaining (integrations/test/version_test.ts). Bumping the literal to '1.4.0' would re-rot at the very next release, and the breakage would then surface inside an automated release PR, where it is maximally disruptive. Instead the test asserts that the exported constant equals the version declared in integrations/package.json. The linked-versions plugin bumps both sides in the same release commit, so the assertion cannot go stale, and it pins the invariant actually worth pinning: the exported version must never drift from the published package version.

    Note this is a real drift guard, not a tautology: the two files are updated by two independent release-please paths — integrations/src/version.ts by the generic extra-files updater keyed on the // x-release-please-version marker, and integrations/package.json by the node release type. Nothing but this test ties them together.

  2. Wire the project in (package.json). --project unit:integrations is added to test, test:unit and test:coverage, positioned after unit:dev so the flag order matches the declaration order in vitest.config.ts.

  3. Cover the browser entry point (integrations/test/index_web_test.ts, new). Wiring the project in takes index.ts and version.ts from 0% to 100%, but leaves index_web.ts — the one integrations source file no test reaches — still at 0%. That gap is not cosmetic: integrations/build.js compiles src/index.ts and src/index_web.ts as independent entry points (dist/esm + dist/cjs from the first; the dist/web bundle that integrations/package.json's browser field publishes from the second), so an export added to one and forgotten in the other ships a browser bundle silently missing the symbol, with nothing to catch it. The new test asserts the two entry points expose the same export names and the same version binding.

    Both entry points are imported by relative ../src/*.js path rather than through @google/adk-integrations. That is forced rather than a style choice: integrations/package.json declares only the "." subpath in its exports map, so index_web is unreachable by package specifier. It matches the ~101 existing tests that import ../../src/<mod>.js, and importing both sides the same way keeps the comparison symmetric with the two inputs build.js actually consumes.

    The key-set assertion carries an explicit expect(nodeExports).not.toHaveLength(0) guard. Without it the test would pass vacuously if both entry points ever resolved to an empty namespace — an equality assertion between two empty key lists is always true, which is the shape of a test that stays green forever while guarding nothing.

vitest.config.ts is not touched at all. An earlier revision of this branch also added a two-line comment there naming the difference between the similarly-spelled unit:integrations and integration projects. That comment has been removed: the approved scope for this fix is the three script strings plus the version test, and the config file was in scope only for a coverage-threshold re-baseline that measured out as a no-op (see the coverage section). A complexity review had already flagged the comment as duplication once, cutting it from seven lines to two; rather than keep shrinking an edit the fix does not need, it is gone. The diff is exactly the three files above, and git diff on vitest.config.ts is empty.

**The alternative that was rejected: dropping integrations/src/**/\*.tsfromcoverage.include.** There are only two ways to make the coverage report honest — run the project, or stop counting the package — and this PR takes the first. Three reasons:

  1. The unit:integrations project already exists in vitest.config.ts. feat(integrations): create new top-level integrations package google/adk-js#449 wired the package into the root workspaces array and into the vitest project list and only missed the script flags, so the omission is an oversight, not a policy decision. Fixing the flags restores the intent already expressed in the config.
  2. @google/adk-integrations is a published package that will grow (Feat: port FirestoreSessionService from adk-python to @google/adk-integrations #466 is already queuing a FirestoreSessionService into it, with two test files of its own). Excluding it from the gate now guarantees that everything landing there in future is untested and unmeasured, and the exclusion would be invisible at the moment it starts to matter.
  3. Dropping it from coverage.include would leave a dead vitest project and a never-run, permanently-failing test file behind — strictly worse than today, because it converts visible rot into invisible rot.

Two scope notes, both deliberate:

  • test:unit is updated alongside test and test:coverage, although the defect report names only the latter two. unit:integrations is a unit project; leaving it out of the unit alias would reproduce the identical bug one script over, and it is a one-flag addition on an adjacent line.
  • cross-language stays out of test and test:coverage, on purpose. It is the other project the root scripts do not select, but its exclusion is legitimate rather than accidental: it needs a Go toolchain and has its own workflow, .github/workflows/cross-language-integration.yml, which runs npm run test:cross-language on macos-latest after go mod tidy. unit:integrations had no such second home — that is exactly what made it orphaned rather than merely deselected.

Deliberately not done:

Implementation note. integrations/package.json is read with a native JSON module import (import packageJson from '../package.json' with {type: 'json'}) rather than readFileSync + fileURLToPath. It is one line instead of six, and it removes manual path handling entirely, so there is nothing to get wrong on the windows-latest leg. The import attribute is required, not decorative: the root tsconfig.json sets module/moduleResolution: nodenext, under which ESM demands the attribute. Verified to typecheck — see the static-gate results below.

This is the established idiom in this repository rather than a new pattern: grep -rn "with {type: 'json'}" --include=*.ts finds 23 existing import sites (under tests/integration/skills/, tests/integration/streaming/, tests/integration/a2a/, tests/integration/agents/ and others). It also needs no compiler-option change — tsc --showConfig reports resolveJsonModule: true already resolved from the root config, so tsconfig.json is untouched. And it cannot perturb the published build: integrations/tsconfig.json sets "include": ["src/**/*"], so the workspace's tsc --emitDeclarationOnly never sees this test file and no rootDir conflict is introduced.

The readFileSync + fileURLToPath alternative was considered and rejected on evidence. The case against the JSON import is that the root tsconfig.json does not spell out resolveJsonModule, so the import would supposedly force a repo-wide widening of the shared config. That premise is false and was measured rather than assumed:

$ npx tsc --version
Version 5.9.3
$ npx tsc --showConfig | grep resolveJsonModule
        "resolveJsonModule": true,          # implied by moduleResolution: nodenext

$ npx tsc --noEmit --listFilesOnly | grep 'integrations/test'
integrations/test/version_test.ts           # the file IS in the root program
$ npx tsc --noEmit 2>&1 | grep '^integrations/'
                                            # ...and emits zero errors

TypeScript turns resolveJsonModule on by default under moduleResolution: nodenext, so the widening never happens. With that premise gone, the JSON import is one line against six, and — the part that actually matters under the "no weak types" rule — it needs no cast: packageJson.version is typed string from the file itself, whereas JSON.parse returns any and has to be narrowed with as {version: string}, an unchecked assertion that would silently survive {"version": 42}. Fewer lines and a stronger type, so the import wins on both counts.

Collision check (required before implementation; recorded here either way):

gh pr list --repo AmaadMartin/adk-js --state open --limit 1000 --json number,title,headRefName

Re-run in full on every revision of this PR (--limit 1000, not the default page, so older PRs are not silently truncated away), then gh pr diff --name-only on every plausibly adjacent hit. No open PR adds --project unit:integrations to the root scripts, so there is no competing implementation.

The check did surface an overlap in the other direction, and it is worth recording how it was resolved. Two additional test artifacts belonging to this fix — the semver assertion on version_test.ts and the new index_web_test.ts — had already been implemented in open PRs #479 and #513, both based on this very branch. An earlier revision of this PR therefore left both out to avoid duplicating live work. A correctness review disagreed on the second one, on the grounds that index_web.ts sitting at 0% coverage leaves the entry-point parity invariant unguarded no matter which PR is nominally responsible, so index_web_test.ts is now in this PR. That makes #513 a no-op against its own base rather than a rival implementation; the semver assertion stays in #479, which is unaffected. See the stacked-PR bullet below.

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.

1. The bug, reproduced before the fix. With the original test restored, the project that CI never ran is red:

$ npx vitest run --project unit:integrations
 × version > should return the correct version 11ms
   AssertionError: expected '1.4.0' to be '1.3.0' // Object.is equality
   Tests  1 failed (1)

2. After the fix:

$ npx vitest run --project unit:integrations --reporter=verbose
 ✓ |unit:integrations| integrations/test/version_test.ts > version > should match the version declared in package.json 2ms
 ✓ |unit:integrations| integrations/test/index_web_test.ts > index_web > exposes the same public surface as the node entry point 4ms
 ✓ |unit:integrations| integrations/test/index_web_test.ts > index_web > re-exports the same version binding as the node entry point 1ms
   Test Files  2 passed (2)
        Tests  3 passed (3)

3. Mutation proof — the new test can still fail. Coverage is not proof, so the assertion was mutated in both directions, one file at a time:

Mutation Result
integrations/src/version.tsexport const version = '9.9.9'; FAILS: AssertionError: expected '9.9.9' to be '1.4.0'
integrations/package.json version0.0.1 (source left at 1.4.0) FAILS: AssertionError: expected '1.4.0' to be '0.0.1'

Mutating each side one file at a time is the point: the second row is what proves the expected value is genuinely read from integrations/package.json and is not accidentally derived from the same module as the actual value — a same-source comparison would pass both mutations. Both were reverted (git checkout) and the working tree confirmed clean with git status --porcelain.

3b. Mutation proof for index_web_test.ts, with each mutation chosen so that exactly one of its two assertions fires. That separation is the evidence that the second it() is not redundant with the first — two assertions can both be "covered" while their failure modes overlap completely, and this shows they do not:

Mutation Result
export const dummy = 1; appended to src/index.ts only (export added to one entry point) surface test FAILS: AssertionError: expected [ 'version' ] to deeply equal [ 'dummy', 'version' ]; other 2 pass
src/index_web.ts re-export replaced with export const version = '0.0.0-divergent'; surface test PASSES (key sets still match), version-binding test FAILS: expected '0.0.0-divergent' to be '1.4.0'

The second row is the interesting one: the two entry points still agree on export names and disagree on the value behind them, so only a test that compares bindings rather than key sets catches it. Both mutations were reverted with git checkout and git status --porcelain confirmed empty.

4. Durability proof — the reason for deriving instead of hardcoding. Simulating a release-please bump by setting both integrations/package.json and integrations/src/version.ts to the same next version, exactly the pair of edits a release commit makes:

 ✓ |unit:integrations| integrations/test/version_test.ts (1 test) 4ms
   Tests  1 passed (1)

It still passes with no edit to the test — exactly what a hardcoded literal could not do, and the reason this project going stale a second time is now structurally prevented. Both files were reverted afterwards.

This was re-run against the version pair main actually carries today (1.5.0 in both integrations/src/version.ts and integrations/package.json), not just a hypothetical one, since this branch is based on the 1.4.0 state and merges forward into 1.5.0: with both files at 1.5.0 the test passes 1 passed (1), and half-bumping only integrations/package.json to 1.6.0 still FAILS with expected '1.5.0' to be '1.6.0'. So the assertion survives the merge without an edit and keeps its teeth after it. (git merge-tree --write-tree fork/main HEAD also exits 0 — the merge itself is clean; see the note on stacked PRs below for why the branch is not rebased.)

5. Project selection, with a negative control. All three unit selectors resolve (vitest errors on an unknown project name):

$ npx vitest run --project unit:core --project unit:dev --project unit:integrations integrations/test/version_test.ts
 ✓ |unit:integrations| integrations/test/version_test.ts (1 test) 4ms

The negative control is what actually pins the wiring, so it was run explicitly rather than inferred from file counts. With the '9.9.9' mutation from proof 3 still in place, the old two-project selector was re-run:

$ npx vitest run --project unit:core --project unit:dev integrations/test/version_test.ts
No test files found, exiting with code 1
  filter:  integrations/test/version_test.ts     # the old selector cannot reach the file at all

$ npx vitest run --project unit:core --project unit:dev --project unit:integrations integrations/test/version_test.ts
 × version > should match the version declared in package.json
   AssertionError: expected '9.9.9' to be '1.4.0'

The same injected bug is invisible under the old script and fatal under the new one. That difference is the entire content of this PR: the test file is not merely unasserted-against, it is never even collected.

grep -n "unit:integrations" package.json now returns the three script lines (27, 28, 32) instead of nothing.

All proofs above were re-run after vitest.config.ts was restored to its unmodified state, since a config file is exactly where a "no longer touched" claim is worth checking rather than asserting: git diff on that file is empty, the project still resolves and the suite still passes (Test Files 2 passed (2) / Tests 3 passed (3)), and every mutation still fails with the messages in the tables above.

6. Static gates.

$ npm run lint           # exit 0
$ npm run format:check   # exit 0 — All matched files use Prettier code style!
$ npx secretlint ...     # exit 0
$ npm run ts:check       # 308 errors, byte-identical to the same command on main

ts:check fails identically on unmodified main (308 pre-existing errors in the core/dev/tests trees, the subject of separate PRs) and is not part of validation.yaml. The error sets on main and on this branch are byte-identical — verified by diff-ing the two logs, not by comparing counts — so this change introduces zero new type errors, which also confirms the JSON import attribute typechecks under nodenext. Nothing under integrations/ is flagged.

Both integrations test files are genuinely in that typecheck program rather than silently excluded from it, which is the part worth checking before quoting a zero:

$ npx tsc --noEmit --listFilesOnly | grep 'integrations/test'
integrations/test/index_web_test.ts
integrations/test/version_test.ts

npx prettier --check and npx eslint were also run against integrations/test/index_web_test.ts specifically; both exit 0. The diff adds no any, no as cast, and no @ts-expect-error, @ts-ignore, eslint-disable or coverage-tool suppression of any kind.

One local-only caveat worth recording, since it is easy to misattribute to this PR: running npm run test:unit in a shell that exports GOOGLE_CLOUD_PROJECT / GOOGLE_CLOUD_LOCATION fails dev/test/cli/cli_create_test.ts > should handle Vertex AI selection with gcloud defaults. That is ambient-environment leakage in an untouched unit:dev test, not a regression here — env -u GOOGLE_CLOUD_PROJECT -u GOOGLE_CLOUD_LOCATION npx vitest run --project unit:dev dev/test/cli/cli_create_test.ts passes 10/10, CI (which has no such vars) is green, and PRs #203 and #259 already target that hermeticity gap.

7. Coverage. npm run test:coverage — the exact command validation.yaml:41 runs — cannot complete in this sandbox: 22 tests fail with API key must be provided via constructor or GOOGLE_GENAI_API_KEY or GEMINI_API_KEY environment variable and 4 more with Command failed: npx @google/adk-devtools --version (no npm registry egress). Per the rule for failures outside integrations/test, this was verified against unmodified main rather than worked around: the same command on main fails too, and the set of failing test files on this branch is a strict subset of main's (main additionally fails the known app_loader_test.ts flake). No failure is unique to this branch and none is in integrations/. The run also confirms the project is now picked up: 235 test files collected here versus 234 on main.

Because the local run aborts, v8 never prints the summary table there — so the real numbers below are taken from CI, read out of the All files row of the three OS legs of the run on this PR's current head commit:

metric ubuntu macos windows worst-leg floor current threshold
statements 90.69 90.69 90.71 90 86
branches 89.62 89.47 89.67 89 87
functions 91.63 91.62 91.63 91 88
lines 90.69 90.69 90.71 90 86

Every metric moved up relative to the revision without Output 3 (ubuntu: 90.68 / 89.61 / 91.55 / 90.6890.69 / 89.62 / 91.63 / 90.69), which is the aggregate footprint of taking index_web.ts from 0% to 100%. Small, because the integrations package is ~20 lines against the whole of core/src + dev/src — and monotonically upward, which is the property that matters: no threshold can regress.

integrations/src is now at 100% on every metric. All three files were at 0% before this PR, because nothing executed them; measured directly against this branch's head:

$ npx vitest run --project unit:integrations --coverage \
    --coverage.include='integrations/src/**/*.ts' --coverage.thresholds.lines=0 ...
File          | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
All files     |     100 |      100 |     100 |     100 |
 index.ts     |     100 |      100 |     100 |     100 |
 index_web.ts |     100 |      100 |     100 |     100 |
 version.ts   |     100 |      100 |     100 |     100 |

An earlier revision of this PR left index_web.ts at 0 / 0 / 0 / 0 (lines 1-7 uncovered) — that is the gap Output 3 closes.

The coverage thresholds in vitest.config.ts are left where they are, including their dated comment, even though every measured floor now exceeds them. (This PR makes no edit to that file at all — no threshold, glob, comment or project definition moves.) Raising them here would be unrelated churn: the gap is ~6 months of drift since the 2026-02-06 baseline, not an effect of this PR, which contributes roughly two statements against the whole of core/src + dev/src. It would also be risky to do casually — the three legs disagree (branches span 89.45–89.65 because they skip different suites), so a threshold set from the best leg red-lines every open PR on the weakest one. The ratchet is worth doing on its own, with its own verification, and has been filed as a separate task rather than smuggled in here. Nothing in this change lowers coverage, so leaving the gate as-is regresses nothing.

Manual End-to-End (E2E) Tests:

No credentials or configuration are required.

npm install && npm run build

# 1. Before this change, on main: the project is orphaned and its test is red.
grep -rn "unit:integrations" package.json        # -> no matches
npx vitest run --project unit:integrations       # -> FAILS: expected '1.4.0' to be '1.3.0'

# 2. On this branch: the scripts select it and all of it passes.
grep -rn "unit:integrations" package.json        # -> lines 27, 28, 32
npx vitest run --project unit:integrations --reporter=verbose
#    -> Test Files 2 passed (2) / Tests 3 passed (3)
#    ✓ integrations/test/version_test.ts > version > should match the version declared in package.json
#    ✓ integrations/test/index_web_test.ts > index_web > exposes the same public surface as the node entry point
#    ✓ integrations/test/index_web_test.ts > index_web > re-exports the same version binding as the node entry point

# 3. Confirm the fix is durable against a future release bump: set BOTH
#    integrations/package.json's "version" and integrations/src/version.ts to 1.5.0.
npx vitest run --project unit:integrations       # -> still passes, no test edit. Revert both.

# 4. Confirm it still catches real drift: set ONLY integrations/package.json to 1.5.0.
npx vitest run --project unit:integrations       # -> FAILS: expected '1.4.0' to be '1.5.0'. Revert.

# 5. Confirm the browser entry point is really guarded: add an export to
#    integrations/src/index.ts only.
echo 'export const dummy = 1;' >> integrations/src/index.ts
npx vitest run --project unit:integrations       # -> FAILS: expected [ 'version' ] to
                                                 #    deeply equal [ 'dummy', 'version' ]. Revert.

Step 2 is the observable behaviour change: those test names appear in no CI log anywhere before this PR.

CI result. Green on all legs — run-tests (ubuntu-latest), run-tests (macos-latest), run-tests (windows-latest), plus check-license. These are the real build/test jobs, not just the trivial checks.

Green CI only proves nothing broke; it does not by itself prove the newly wired project ran. That was read back out of the workflow log on all three legs rather than inferred from the summary:

$ gh run view <run-id> --log --job <job-id> | grep -E 'unit:integrations|All files'

# ubuntu-latest
 ✓  unit:integrations  integrations/test/index_web_test.ts (2 tests) 7ms
 ✓  unit:integrations  integrations/test/version_test.ts  (1 test)  4ms
# macos-latest
 ✓  unit:integrations  integrations/test/index_web_test.ts (2 tests) 6ms
 ✓  unit:integrations  integrations/test/version_test.ts  (1 test)  2ms
# windows-latest
 ✓  unit:integrations  integrations/test/index_web_test.ts (2 tests) 9ms
 ✓  unit:integrations  integrations/test/version_test.ts  (1 test)  6ms

Those log lines are the deliverable: it is the first time either test has appeared in a CI log at all. The Windows leg in particular confirms both the JSON module import and the relative ../src/*.js specifiers need no path handling to be portable.

Three infrastructure flakes were hit along the way and are recorded rather than hidden, since all are in suites this diff does not touch:

  • Windows, on the commit that added Output 3 (940babf5b): core/test/code_executors/unsafe_local_code_executor_test.ts > UnsafeLocalCodeExecutor > should execute shell code and return stdoutTest timed out in 5000ms. Not this diff — it is a unit:core test on main that shells out to echo "Hello, Shell!" through a real child process with no explicit it() timeout, so it runs against the 5s default, and process spawn on a loaded Windows runner does not reliably fit in 5s. Attributed by evidence rather than assertion: the unit:integrations project passed on that same red leg (✓ index_web_test.ts (2 tests) 9ms, ✓ version_test.ts (1 test) 6ms), the failure is in a file this branch does not modify (confirmed with git diff main --name-only), and the reported location :161 matches the file on the base the PR merges into, not this branch. Green on rerun of the identical commit, which is what makes it a flake rather than a diagnosis. Worth a separate timeout fix, but not one to smuggle into this PR.

  • macOS, twice: tests/integration/app_loader/app_loader_test.ts > should discover apps vs agents across directories and standalone filesTest timed out in 40000ms (1 failed / 224 passed). Classified by evidence, not assumption: the branch fix/app-test-dummy-agent-abstract-members, whose diff is unrelated and which is based on a newer main than this one, failed the macOS leg on the identical test and timeout three hours earlier. The timeout is the test's own explicit TEST_EXECUTION_TIMEOUT = 40000 third argument to it(), so it is not affected by any project-level testTimeout. Green on rerun.

  • Windows, earlier: Error: CLI exited prematurely with code 1 from tests/integration/a2a/input_required/input_required_test.ts at tests/integration/test_case_utils.ts:341 — a child-process spawn flake (that run reported 2510 tests passed and 0 test-level failures; what failed was the suite's server process, not an assertion). Green on rerun. In the latest run Windows was not a failure at all: the matrix cancelled it when macOS went red.

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.

Amaad Martin added 2 commits July 29, 2026 22:11
integrations/test/version_test.ts hardcoded `expect(version).toBe('1.3.0')`
while integrations/src/version.ts exports '1.4.0', so the test is currently
red. It went stale unnoticed because the vitest project that owns it
(`unit:integrations`) is invoked by no npm script and no workflow, so the
file has never actually run.

Bumping the literal to '1.4.0' would only re-rot on the next release: the
release automation rewrites integrations/src/version.ts (via the
x-release-please-version annotation) but never touches test literals. Assert
against the version declared in integrations/package.json instead. Both
sides are updated in the same release commit, so the assertion is
self-maintaining, and it guards the invariant actually worth guarding: the
exported constant must not drift from the published package version.
vitest.config.ts declares a `unit:integrations` project owning
integrations/test/**/*_test.ts, but the name appeared nowhere else in the
repository: no npm script and no workflow invoked it, so those tests never
ran. validation.yaml runs `npm run test:coverage` and
cross-language-integration.yml runs `npm run test:cross-language`, which
between them reached every project except this one.

Add `--project unit:integrations` to `test`, `test:unit` and
`test:coverage`, positioned after `unit:dev` to match the declaration order
in vitest.config.ts.

No `test:integrations` script is added on purpose: it would sit one
character from the existing `test:integration` (which runs the unrelated
`integration` project over tests/integration/) and invite mistakes.
`unit:core` and `unit:dev` have no individual scripts either.

The coverage thresholds are deliberately left untouched. coverage.include
already lists integrations/src/**/*.ts and coverage.all defaults to true,
so those files were already in the denominator scored at 0%; running the
project only adds to the numerator. Measured over `unit:core + unit:dev`
(v8), All files goes 88.94/88.11/89.58/88.94 to 88.95/88.14/89.73/88.95
statements/branches/functions/lines - every metric up.
… projects

The two project names differ by one character and by scope: unit:integrations
owns the integrations/ workspace package, integration owns the cross-component
suite in tests/integration/. That similarity is part of why the former was
overlooked by the root test scripts for three releases. Comments only; no
project definition, glob, or threshold changes.
@AmaadMartin

Copy link
Copy Markdown
Owner Author

Independent verification from a duplicate task that was elaborated against the same three defects and is being closed in favour of this PR. Recording the measurements here so they are not lost — all run locally against this branch's exact two-file state (npm install + npm run build first, since tests/global_setup.ts imports @google/adk through core/dist).

Postconditions

check result
npx vitest run --project unit:integrations Test Files 1 passed (1) / Tests 1 passed (1)
npm run test:unit -- --run selects it ✓ |unit:integrations| integrations/test/version_test.ts (1 test)

The only failure in the test:unit run was dev/test/cli/cli_create_test.ts > "Vertex AI selection with gcloud defaults", a pre-existing sandbox failure (no gcloud config), unrelated to this change.

Mutation proof, both drift directions — the assertion fails when it should:

  • forward, integrations/src/version.ts'9.9.9':
    AssertionError: expected '9.9.9' to be '1.5.0' // Object.is equality at integrations/test/version_test.ts:13:21
  • reverse, integrations/package.json1.6.0, version.ts left at 1.5.0:
    AssertionError: expected '1.5.0' to be '1.6.0' // Object.is equality

Anti-rot — bumping both integrations/package.json and integrations/src/version.ts to 1.6.0 keeps it green with no test edit. That is the property the change exists to provide, confirmed.

Two things a reviewer might otherwise ask for, which the evidence says not to:

  1. An explicit expect(packageJson.version).toMatch(/^\d+\.\d+\.\d+/) vacuity guard. Unreachable here. I deleted the version field from integrations/package.json entirely and the test still failed loudly: AssertionError: expected '1.5.0' to be undefined. The imported version is a string literal that can never itself be undefined, so the vacuous undefined-vs-undefined comparison such a guard defends against cannot occur. The guard only makes sense against a readFileSync + JSON.parse form typed {version?: string}; the import attribute used here removes the failure mode rather than asserting against it. Adding it would be dead code.
  2. Whether import ... with {type: 'json'} breaks npm run ts:check. It does not. integrations/test/version_test.ts is in the tsc program (confirmed via tsc --noEmit --listFiles) and npx tsc --noEmit reports zero errors for it — identical to baseline, with no tsconfig.json edit required.

Amaad Martin added 2 commits August 1, 2026 13:27
The disambiguation between unit:integrations and integration was written
twice, once from each side, and each copy restated the include glob two
lines below it. Keep the comment on unit:integrations -- the project this
change wires in, and the less obvious of the two -- and drop the mirror.
The approved spec scopes this change to the three root test-script strings
in package.json and the integrations version test, touching vitest.config.ts
"at most" for the coverage thresholds block -- whose expected outcome is no
edit at all. The project-naming comment added earlier sits outside that
ceiling, so it is removed and the diff is now exactly the two files the fix
requires. No behaviour change: the comment never affected project resolution.
integrations/build.js compiles src/index.ts and src/index_web.ts into
separate published artifacts -- dist/esm and dist/cjs from the first, the
dist/web bundle the package's browser field points at from the second --
so an export added to one entry point and forgotten in the other ships a
browser bundle silently missing the symbol. Nothing guarded that, and
index_web.ts was the one integrations source file no test reached, sitting
at 0% coverage even after the project was wired into the root scripts.

Assert the two entry points expose the same export names and the same
version binding. The key-set assertion is guarded against passing
vacuously if both entry points ever resolve to nothing.
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