fix(gazelle): find a ts_test's vitest config at the package root - #156
Open
mikn wants to merge 1 commit into
Open
fix(gazelle): find a ts_test's vitest config at the package root#156mikn wants to merge 1 commit into
mikn wants to merge 1 commit into
Conversation
vitestConfigIn read vitest.config.* from the test's own directory, while plain `vitest` reads it from the directory it runs in, the package root beside package.json. A package keeping its config there with its tests under test/ got a ts_test with no config, so the generated config's user layer was empty: a worker's defineWorkersConfig pool ran as no pool, and a test.server.deps.inline entry never applied, so an ESM dependency with extensionless relative imports failed at import time under Node. With no config beside the tests the generator now walks up to the nearest directory holding a package.json, or the repository root, and writes the config it finds there as config = "//<pkg>:vitest_config", a public filegroup over the file that the owning package emits. The filegroup is a named rule, so the merger matches it across runs and the generator withdraws it with the file; config is not among the ts_test attributes Gazelle owns, so the tests below keep the label until edited. A bare specifier the config imports is a dep of the test as before; a relative import in a config above the tests is read against the test's package, not the config's, so it gets neither a dep nor the data it needs. A config beside the tests still wins, a package.json in the test's own directory ends the walk there, and a directory between the two with no package.json is passed over, as `vitest` run from the package root passes it over. No label is written into a directory under a ts_ignore, one a boundary directive between the two leaves disagreeing, one whose target is already named vitest_config, or, in tsconfig mode, one below the root without a tsconfig.json. Red at 2bcc23f with the seven Go cases and the roundtrip fixture in the tree; four of the seven fail, and the three that pass pin what already held: a config beside the tests by name, and no attr with a package.json in the test's directory or with the config off the package root (rt-evidence/logs/fix8r/M/01-red-unit.log, 02-red-roundtrip.log): $ bazelisk --output_base=/var/tmp/fix8-M-ob test //gazelle:typescript_test --nocache_test_results --local_test_jobs=2 --test_output=all --test_filter=TestGenerate_VitestConfig|TestGenerate_APackageJsonInTheTestDirEndsTheWalk|TestGenerate_WithdrawsTheVitestConfigFilegroupWithTheFile --test_arg=-test.v [... 20 lines ...] generate_test.go:524: ts_test config = "", want "//pkg:vitest_config" generate_test.go:527: the config's own imports never reached the test target generate_test.go:533: no filegroup named vitest_config in pkg; got map[pkg:ts_compile] [... 1 lines ...] generate_test.go:524: ts_test config = "", want "//pkg:vitest_config" generate_test.go:527: the config's own imports never reached the test target generate_test.go:533: no filegroup named vitest_config in pkg; got map[] [... 4 lines ...] generate_test.go:559: ts_test config = "", want "//:vitest_config" generate_test.go:563: no filegroup named vitest_config at the root; got map[root:ts_compile] [... 8 lines ...] generate_test.go:642: the filegroup outlived its file; Empty = [] [... 12 lines ...] Executed 1 out of 1 test: 1 fails locally. # exit=3 $ bazelisk --output_base=/var/tmp/fix8-M-ob test //tests/integration:gazelle_roundtrip_test --nocache_test_results --local_test_jobs=2 --test_output=all [... 32 lines ...] FAIL: Gazelle did not generate configured/BUILD.bazel [... 10 lines ...] Executed 1 out of 1 test: 1 fails locally. # exit=3 Green at aabfc8b, this commit before its message was amended, the same tree eccc78f (rt-evidence/logs/fix9r/M/05-green-targeted.log; bazel-m9.sh beside the log execs bazelisk --output_base=/var/tmp/fix9-M-ob): $ bazel-m9.sh test //gazelle:typescript_test //tests/integration:gazelle_roundtrip_test //tools/changelog:changelog_test --nocache_test_results --local_test_jobs=2 --test_output=errors --test_summary=short [... 17 lines ...] //gazelle:typescript_test PASSED in 24.8s //tests/integration:gazelle_roundtrip_test PASSED in 60.4s //tools/changelog:changelog_test PASSED in 0.0s [... 1 lines ...] Executed 3 out of 3 tests: 3 tests pass. [... 1 lines ...] # exit=0
This was referenced Sep 6, 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.
vitestConfigIn(args.Dir)(gazelle/generate.go) readvitest.config.*from thets_test's own directory, and plainvitestreads it from the directory it runs in, the package root besidepackage.json. On the trial monorepo the workers keep their config at the package root with the tests undertest/orsrc/: of the 67 BUILD files holding ats_testunderweb,workers,packages,npm-packagesandscripts, 44 have theirvitest.config.*only in the nearestpackage.jsondirectory above, 1 (web) beside the tests, and 22 have none (logs/inv7b/runtime-cloud-proxy/vitest-config-placement.log;grep -cofROOT_ONLY,OWNandNONEover the rows below its header). Each of the 44 gets ats_testwith noconfig, so its generated vitest config's user layer isconst user = {};(generated-configs.log). For//workers/cloud-proxy/test:test_testthe setting that never applied istest.server.deps.inline: [/@flarelabs-net\/workers-observability-utils/](workers/cloud-proxy/vitest.config.ts): the package is ESM with an extensionlessimport "./types"indist/metrics.js, Node v22.23.1 rejects it, and the suite fails at import time after 263 ms withCannot find module '.../@flarelabs-net/workers-observability-utils/dist/types' imported from .../dist/metrics.js(bazel-test.log), while the same test with that config carried in the generated config's user layer passes 5 of 5 (skeptic/bazel-layout-fixed-shape.log, RUN F and RUN G).With no config beside the tests the generator now walks up to the directory plain
vitestruns from, the nearest one holding apackage.jsonor the repository root (vitestRootAbove, the walknearestHandWrittenTsConfigmakes for the tsconfig), and takes thevitest.config.*it holds. The test'sconfigis//<pkg>:vitest_config, and that package's generation emits a publicfilegroupnamedvitest_configover the file (ownVitestConfigRule, besideownTsConfigRule): a named rule, which Gazelle's merger matches by name on the next run, where a namelessexports_filescannot be matched and the extension writes none. The existingfilegroupKindsentry (MatchAttrsname, mergeablesrcsandvisibility) covers it. A directory holding onlypackage.jsonand the config returned before writing anything (the.mtsspelling is not a compile source), and that is the directory the label points into, so the early return now also waits on the filegroup as it does on ats_config; the filegroup is withdrawn with the file, andemptyResultnames it. A bare specifier the config imports becomes a dep of the test as before (importsIn(cfgDir, ...)reads the file where it sits); a relative import in the config is among the limits below. A config beside the tests still wins; apackage.jsonin the test's own directory makes it the vitest root, and with no config there the test gets none; a config in a directory between the two with nopackage.jsonis passed over, asvitestrun from the package root passes it over. Both sides go throughexportedVitestConfig, and the child side adds the refusalstsConfigReachFrommakes for the tsconfig label: an owner under# gazelle:ts_ignore, ats_package_boundarydirective between the two, an owner whose package target is already namedvitest_config, and, in tsconfig mode, an owner below the root with notsconfig.json, which is no boundary and gets no BUILD file. Each refusal leavesconfigunset, as before, and none is logged.ts_test.configtakes the label as it is,allow_single_fileover a one-file filegroup (ts/private/ts_test.bzl), staged by the sameexpand_template; the launcher is unchanged.Three limits, none of them this PR's fix. Vite's root is the test's package (
root: process.env.TS_TEST_PACKAGE_DIR, the directory of the generated config), so a relative path inside a config found above the tests resolves against the test's directory, not the config's. Gazelle writes nodatafor a config's relative imports, whichts_testrequires (docs/rules/ts-test.md: "Anything the config imports relatively must be indata"), and for a config found above the tests it writes no dep for one either:importsInrebases a relative specifier only within the config's own directory (rebaseRelativereturns it unchanged fordirRel.),resolveRelativejoins it onto the test's package,from.Pkg, andlabelForUnindexedanswers a module in the importing package that no rule claims with nothing; nothing is logged. Measured withgazelle_typescriptbuilt at a8fe3f6 over fresh BUILD files (logs/fix8r/M-skeptic/15-gazelle-ownership.log, steps D and E):import "./helper"inconfigured/test/vitest.config.tswithhelper.tsbeside it gives the testdeps = [":test", "@npm//:vitest"]; the same import inconfigured/vitest.config.mtswithconfigured/helper.tsbeside it givesdeps = ["@npm//:vitest"]. cloud-proxy's config imports onlyvitest/config. Andconfigis not among thets_testattributes Gazelle owns (gazelle/language.goMergeableAttrs, unchanged; thets_testrow of the directives table), so the merger sets it when absent and never rewrites or removes it: the filegroup goes with its file, and the tests below keepconfig = "//<pkg>:vitest_config", a label naming nothing, until edited (14-gazelle-two-runs.log: afterrm configured/vitest.config.mtsand a fix run,cat configured/BUILD.bazelprints nothing andconfigured/test/BUILD.bazelstill readsconfig = "//configured:vitest_config"). Over an existing BUILD file a config written beside the tests does not displace the label either (same log: withconfigured/test/vitest.config.tsadded, the attr stays//configured:vitest_config), and a same-dirconfig = "vitest.config.ts"outlives its file the same way (15-gazelle-ownership.log, step B). New with this PR is the shape: a config moved from the package root intotest/leaves the test failing on a missing target, where before it had noconfig. A follow-up may makeconfigowned when its value is the label Gazelle writes.Docs:
docs/gazelle/overview.md§ Package Boundary Heuristic gains the lookup rule, the filegroup and label shape, the bare-versus-relative import sentence and the kept-label sentence from the limits above, the refusals and the root note;docs/gazelle/directives.md§ Attributes Gazelle Owns gains thefilegroup(name = "vitest_config")row;docs/guides/testing.md§ An Existing vitest Config gains the paragraph on where Gazelle finds the file. ThefilegroupKinds comment ingazelle/language.go, which said the kind is written beside atsconfig.jsonfor itstypesfiles and that onlysrcsis managed, is rewritten.changelog.d/gazelle-vitest-config-walk-up.mdrecords the fix.Fixture
gazelle/generate_test.gogainswriteTreeandgenerateAt, a harness over a repository-relative tree for a package whose generation reads directories above it, and seven cases besideTestGenerate_VitestConfigBesideTestsReachesTheTestTarget:TestGenerate_VitestConfigAtThePackageRootReachesATestBelow(forvitest.config.tsand.mts: the test atpkg/testgetsconfig = "//pkg:vitest_config"andvitest/configamong its imports, andpkg's generation emits the filegroup over the file with public visibility),TestGenerate_VitestConfigAtTheRepoRootIsTheRootLabel(//:vitest_config),TestGenerate_VitestConfigBesideTheTestsBeatsThePackageRoots(the nearer file by name, and the root config's@cloudflare/vitest-pool-workers/configimport not among the test's),TestGenerate_APackageJsonInTheTestDirEndsTheWalk(no attr),TestGenerate_VitestConfigOffThePackageRootIsNotExported(a config inpkg/srcwith thepackage.jsonatpkg: no attr onpkg/src/unit's test, no filegroup inpkg/src), andTestGenerate_WithdrawsTheVitestConfigFilegroupWithTheFile(a BUILD holding the filegroup and a directory holding onlypackage.json: the filegroup is inEmpty).tests/integration/gazelle_roundtrip/configured/is the trial's shape:package.jsonand avitest.config.mtsat the root, the test intest/. The config installs a plugin answeringvirtual:answerwithexport default 42;and imports nothing; the test importsvirtual:answerand expects 42, withtest/virtual.d.tsdeclaring the module astests/vitest/config_mergedoes. The runner addsconfiguredandconfigured/testto the directories whose BUILD files it requires after pass 1 and compares across the delete-and-regenerate, runs the test underbazel test //..., and inpackageRootVitestConfigReachesTheTestBelowasserts the filegroup text inconfigured/BUILD.bazelandconfig = "//configured:vitest_config"inconfigured/test/BUILD.bazel, then removes that line, runsbazel test //configured/test:test_test, requires it to fail withvirtual:answerin the output, and restores the file.Red on 2bcc23f with the seven cases, the fixture and the runner change in the tree and the generator untouched (
logs/fix8r/M/01-red-unit.log,02-red-roundtrip.log; each header'stree=lists the three working-tree entries):The other three cases pass on 2bcc23f: they pin what already holds (a config beside the tests by name, no attr with a
package.jsonin the test directory or with the config off the package root), so the change cannot loosen it. The filter also runs the existingTestGenerate_VitestConfigBesideTestsReachesTheTestTarget, so over the log^--- FAILmatches 3 lines (functions; 5 with the two subtests) and^--- PASS4 (that function and the three cases).Green at aabfc8b, 8fd2fad before its message was amended, from the same tree eccc78f (
logs/fix9r/M/05-green-targeted.log;08-commit.logholds bothrev-parses;bazel-m9.shbeside the logs execsbazelisk --output_base=/var/tmp/fix9-M-ob). 8fd2fad, this PR's head, is d15fbda, a8fe3f6 rebased onto 9156811, plus the prose corrections indocs/gazelle/overview.mdand the changelog fragment (git diff --stat d15fbda 8fd2fad: 2 files, 19 insertions, 14 deletions, in08-commit.log); its Go, fixture and runner files are d15fbda's, and d15fbda's own patch differs from a8fe3f6's by the one merged line below:At aabfc8b the same filter under
--test_arg=-test.vprints--- PASSfor all seven cases and for the existingTestGenerate_VitestConfigBesideTestsReachesTheTestTarget, 7 lines matching^--- PASS, 12 with subtests, 0--- FAIL, andExecuted 1 out of 1 test: 1 test passes.(06-green-unit.log); the roundtrip runner's own lines under--test_output=allread (07-green-roundtrip.log, lines 351-354; 131PASS:and 0FAIL:over the log):bazel run //gazelle -- -mode=diffat a8fe3f6 prints 595 lines, byte-identical to the run on 2bcc23f's tree (logs/fix8r/M/08-modediff.log,08-modediff.stdoutagainstlogs/restack-3/s08-skeptic-modediff.stdout,diffexit 0, 0 lines): the fixture sits undertests/integration's# gazelle:exclude gazelle_roundtrip, andeslint-plugin/, the ruleset's one directory with apackage.jsonbeside avitest.config.tsand a test below it (src/rules/__tests__/), carries# gazelle:ts_exclude vitest.config.ts, which the export honours, so it gains no filegroup.buildifier --mode=check -r .,gofmt -l .andgo vetover CI's package list exit 0 at 8fd2fad (logs/fix9r/M/09-lint-buildifier.log,10-lint-gofmt.log,11-lint-govet.log);uvx --from mkdocs-material mkdocs build --strictbuilds the site (12-lint-mkdocs.log, exit 0; the two cross-references the pages add are../gazelle/overview.md#package-boundary-heuristicanddirectives.md#attributes-gazelle-owns, anchors of existing headings);tools/ci/check_test_sources.sh(131 test source files, all claimed by a test target; 5 manual-only, allowlisted) andtools/ci/check_integration_shards.sh(15 tests in//tests/integrationover 2 legs, each on exactly one: npm 5, core 10) exit 0 (13-check-test-sources.log,14-check-integration-shards.log). No.bzlchanges; the full suite ran at the restacked stack's tip, d3c9520, and is in #160's body (logs/fix8r/restack/12-suite.log, 380 of 380).The rebase onto 9156811 conflicted on one line, the roundtrip runner's
dirsslice: #155 addsjsxandjsx/viewto it and this PRconfiguredandconfigured/test. The resolution keeps both, #155's first;git range-diff 2bcc23f..a8fe3f6 9156811..d15fbdashows that hunk and nothing else (logs/fix8r/restack/03-conflict-M-main.go.logholds the conflict hunk,04-resolve-M.logthe resolution,05-rangediff-M.logthe range-diff).//tests/integration:gazelle_roundtrip_testran the merged runner in the tip's suite; its test.log carries thePASSlines of both fixtures, 131PASS:and 0FAIL:(21-roundtrip-testlog.log).On the trial
Measured at cae299e with the override at 2bcc23f (
logs/inv7b/runtime-cloud-proxy/):workers/cloud-proxy/test/BUILD.bazelhas noconfig, the generated config's user layer isconst user = {};, and//workers/cloud-proxy/test:test_testfails at import time (bazel-test.log). Expected once the override moves and the converge re-runs:workers/cloud-proxy/BUILD.bazelgainsfilegroup(name = "vitest_config", srcs = ["vitest.config.ts"], visibility = ["//visibility:public"])beside thets_compilethat already carries the file as a source;workers/cloud-proxy/test/BUILD.bazelandworkers/cloud-proxy/src/BUILD.bazelgainconfig = "//workers/cloud-proxy:vitest_config"; the user layer carriesdeps.inline; and//workers/cloud-proxy/test:test_testruns the 5 tests the skeptic's fixed-shape run passed (skeptic/bazel-layout-fixed-shape.log, RUN F and RUN G). The other 43ROOT_ONLYpackages take the same shape, each gaining the label and its root the filegroup; the 22 with no config, andweb, whose config sits beside its tests under itsts_package_boundary tsconfig, are unchanged. Whether each of the 43 then runs is the trial's to measure:workers/entri-webhook/vitest.config.mts'scloudflareTest({ wrangler: { configPath: "./wrangler.test.jsonc" } })now applies to//workers/entri-webhook/test:test_test, which fails today (logs/trial-7/measure/red-targets-test.log:1667), and a config with relative imports still gets nodata, nor a dep for one found above the tests.Base branch:
mikn/gazelle-jsx-runtime-dep(bb61fce); head fcef9f4, the polished commit 8fd2fad rebased onto it with its patch unchanged (logs/fix9r/restack/restack-mechanical.log).