Skip to content

fix(gazelle): find a ts_test's vitest config at the package root - #156

Open
mikn wants to merge 1 commit into
mikn/gazelle-jsx-runtime-depfrom
mikn/gazelle-vitest-config-walk-up
Open

fix(gazelle): find a ts_test's vitest config at the package root#156
mikn wants to merge 1 commit into
mikn/gazelle-jsx-runtime-depfrom
mikn/gazelle-vitest-config-walk-up

Conversation

@mikn

@mikn mikn commented Sep 6, 2026

Copy link
Copy Markdown
Owner

vitestConfigIn(args.Dir) (gazelle/generate.go) read vitest.config.* from the ts_test's own directory, and plain vitest reads it from the directory it runs in, the package root beside package.json. On the trial monorepo the workers keep their config at the package root with the tests under test/ or src/: of the 67 BUILD files holding a ts_test under web, workers, packages, npm-packages and scripts, 44 have their vitest.config.* only in the nearest package.json directory above, 1 (web) beside the tests, and 22 have none (logs/inv7b/runtime-cloud-proxy/vitest-config-placement.log; grep -c of ROOT_ONLY, OWN and NONE over the rows below its header). Each of the 44 gets a ts_test with no config, so its generated vitest config's user layer is const user = {}; (generated-configs.log). For //workers/cloud-proxy/test:test_test the setting that never applied is test.server.deps.inline: [/@flarelabs-net\/workers-observability-utils/] (workers/cloud-proxy/vitest.config.ts): the package is ESM with an extensionless import "./types" in dist/metrics.js, Node v22.23.1 rejects it, and the suite fails at import time after 263 ms with Cannot 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 vitest runs from, the nearest one holding a package.json or the repository root (vitestRootAbove, the walk nearestHandWrittenTsConfig makes for the tsconfig), and takes the vitest.config.* it holds. The test's config is //<pkg>:vitest_config, and that package's generation emits a public filegroup named vitest_config over the file (ownVitestConfigRule, beside ownTsConfigRule): a named rule, which Gazelle's merger matches by name on the next run, where a nameless exports_files cannot be matched and the extension writes none. The existing filegroup Kinds entry (MatchAttrs name, mergeable srcs and visibility) covers it. A directory holding only package.json and the config returned before writing anything (the .mts spelling 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 a ts_config; the filegroup is withdrawn with the file, and emptyResult names 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; a package.json in 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 no package.json is passed over, as vitest run from the package root passes it over. Both sides go through exportedVitestConfig, and the child side adds the refusals tsConfigReachFrom makes for the tsconfig label: an owner under # gazelle:ts_ignore, a ts_package_boundary directive between the two, an owner whose package target is already named vitest_config, and, in tsconfig mode, an owner below the root with no tsconfig.json, which is no boundary and gets no BUILD file. Each refusal leaves config unset, as before, and none is logged. ts_test.config takes the label as it is, allow_single_file over a one-file filegroup (ts/private/ts_test.bzl), staged by the same expand_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 no data for a config's relative imports, which ts_test requires (docs/rules/ts-test.md: "Anything the config imports relatively must be in data"), and for a config found above the tests it writes no dep for one either: importsIn rebases a relative specifier only within the config's own directory (rebaseRelative returns it unchanged for dirRel .), resolveRelative joins it onto the test's package, from.Pkg, and labelForUnindexed answers a module in the importing package that no rule claims with nothing; nothing is logged. Measured with gazelle_typescript built at a8fe3f6 over fresh BUILD files (logs/fix8r/M-skeptic/15-gazelle-ownership.log, steps D and E): import "./helper" in configured/test/vitest.config.ts with helper.ts beside it gives the test deps = [":test", "@npm//:vitest"]; the same import in configured/vitest.config.mts with configured/helper.ts beside it gives deps = ["@npm//:vitest"]. cloud-proxy's config imports only vitest/config. And config is not among the ts_test attributes Gazelle owns (gazelle/language.go MergeableAttrs, unchanged; the ts_test row 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 keep config = "//<pkg>:vitest_config", a label naming nothing, until edited (14-gazelle-two-runs.log: after rm configured/vitest.config.mts and a fix run, cat configured/BUILD.bazel prints nothing and configured/test/BUILD.bazel still reads config = "//configured:vitest_config"). Over an existing BUILD file a config written beside the tests does not displace the label either (same log: with configured/test/vitest.config.ts added, the attr stays //configured:vitest_config), and a same-dir config = "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 into test/ leaves the test failing on a missing target, where before it had no config. A follow-up may make config owned 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 the filegroup(name = "vitest_config") row; docs/guides/testing.md § An Existing vitest Config gains the paragraph on where Gazelle finds the file. The filegroup Kinds comment in gazelle/language.go, which said the kind is written beside a tsconfig.json for its types files and that only srcs is managed, is rewritten. changelog.d/gazelle-vitest-config-walk-up.md records the fix.

Fixture

gazelle/generate_test.go gains writeTree and generateAt, a harness over a repository-relative tree for a package whose generation reads directories above it, and seven cases beside TestGenerate_VitestConfigBesideTestsReachesTheTestTarget: TestGenerate_VitestConfigAtThePackageRootReachesATestBelow (for vitest.config.ts and .mts: the test at pkg/test gets config = "//pkg:vitest_config" and vitest/config among its imports, and pkg'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/config import not among the test's), TestGenerate_APackageJsonInTheTestDirEndsTheWalk (no attr), TestGenerate_VitestConfigOffThePackageRootIsNotExported (a config in pkg/src with the package.json at pkg: no attr on pkg/src/unit's test, no filegroup in pkg/src), and TestGenerate_WithdrawsTheVitestConfigFilegroupWithTheFile (a BUILD holding the filegroup and a directory holding only package.json: the filegroup is in Empty).

tests/integration/gazelle_roundtrip/configured/ is the trial's shape: package.json and a vitest.config.mts at the root, the test in test/. The config installs a plugin answering virtual:answer with export default 42; and imports nothing; the test imports virtual:answer and expects 42, with test/virtual.d.ts declaring the module as tests/vitest/config_merge does. The runner adds configured and configured/test to the directories whose BUILD files it requires after pass 1 and compares across the delete-and-regenerate, runs the test under bazel test //..., and in packageRootVitestConfigReachesTheTestBelow asserts the filegroup text in configured/BUILD.bazel and config = "//configured:vitest_config" in configured/test/BUILD.bazel, then removes that line, runs bazel test //configured/test:test_test, requires it to fail with virtual:answer in 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's tree= lists the three working-tree entries):

$ 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

The other three cases pass on 2bcc23f: they pin what already holds (a config beside the tests by name, no attr with a package.json in the test directory or with the config off the package root), so the change cannot loosen it. The filter also runs the existing TestGenerate_VitestConfigBesideTestsReachesTheTestTarget, so over the log ^--- FAIL matches 3 lines (functions; 5 with the two subtests) and ^--- PASS 4 (that function and the three cases).

$ 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, 8fd2fad before its message was amended, from the same tree eccc78f (logs/fix9r/M/05-green-targeted.log; 08-commit.log holds both rev-parses; bazel-m9.sh beside the logs execs bazelisk --output_base=/var/tmp/fix9-M-ob). 8fd2fad, this PR's head, is d15fbda, a8fe3f6 rebased onto 9156811, plus the prose corrections in docs/gazelle/overview.md and the changelog fragment (git diff --stat d15fbda 8fd2fad: 2 files, 19 insertions, 14 deletions, in 08-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:

$ 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

At aabfc8b the same filter under --test_arg=-test.v prints --- PASS for all seven cases and for the existing TestGenerate_VitestConfigBesideTestsReachesTheTestTarget, 7 lines matching ^--- PASS, 12 with subtests, 0 --- FAIL, and Executed 1 out of 1 test: 1 test passes. (06-green-unit.log); the roundtrip runner's own lines under --test_output=all read (07-green-roundtrip.log, lines 351-354; 131 PASS: and 0 FAIL: over the log):

PASS: configured/BUILD.bazel carries the vitest_config filegroup
PASS: //configured/test:test_test names the package root's config by label
[... 1 lines ...]
PASS: without the config //configured/test:test_test fails on `virtual:answer`: the setting reached the test through the label

bazel run //gazelle -- -mode=diff at a8fe3f6 prints 595 lines, byte-identical to the run on 2bcc23f's tree (logs/fix8r/M/08-modediff.log, 08-modediff.stdout against logs/restack-3/s08-skeptic-modediff.stdout, diff exit 0, 0 lines): the fixture sits under tests/integration's # gazelle:exclude gazelle_roundtrip, and eslint-plugin/, the ruleset's one directory with a package.json beside a vitest.config.ts and 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 . and go vet over 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 --strict builds the site (12-lint-mkdocs.log, exit 0; the two cross-references the pages add are ../gazelle/overview.md#package-boundary-heuristic and directives.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) and tools/ci/check_integration_shards.sh (15 tests in //tests/integration over 2 legs, each on exactly one: npm 5, core 10) exit 0 (13-check-test-sources.log, 14-check-integration-shards.log). No .bzl changes; 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 dirs slice: #155 adds jsx and jsx/view to it and this PR configured and configured/test. The resolution keeps both, #155's first; git range-diff 2bcc23f..a8fe3f6 9156811..d15fbda shows that hunk and nothing else (logs/fix8r/restack/03-conflict-M-main.go.log holds the conflict hunk, 04-resolve-M.log the resolution, 05-rangediff-M.log the range-diff). //tests/integration:gazelle_roundtrip_test ran the merged runner in the tip's suite; its test.log carries the PASS lines of both fixtures, 131 PASS: and 0 FAIL: (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.bazel has no config, the generated config's user layer is const user = {};, and //workers/cloud-proxy/test:test_test fails at import time (bazel-test.log). Expected once the override moves and the converge re-runs: workers/cloud-proxy/BUILD.bazel gains filegroup(name = "vitest_config", srcs = ["vitest.config.ts"], visibility = ["//visibility:public"]) beside the ts_compile that already carries the file as a source; workers/cloud-proxy/test/BUILD.bazel and workers/cloud-proxy/src/BUILD.bazel gain config = "//workers/cloud-proxy:vitest_config"; the user layer carries deps.inline; and //workers/cloud-proxy/test:test_test runs the 5 tests the skeptic's fixed-shape run passed (skeptic/bazel-layout-fixed-shape.log, RUN F and RUN G). The other 43 ROOT_ONLY packages take the same shape, each gaining the label and its root the filegroup; the 22 with no config, and web, whose config sits beside its tests under its ts_package_boundary tsconfig, are unchanged. Whether each of the 43 then runs is the trial's to measure: workers/entri-webhook/vitest.config.mts's cloudflareTest({ 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 no data, 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).

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
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