From b58796d7a693f99dfb24bacfb908b7552f7e43a5 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Tue, 3 Nov 2026 21:46:22 -0800 Subject: [PATCH 1/5] chore(quest): claim quest/m0/ci/bun-test-paths.md Quest-Claim: 65f35907ff9c7715 From 2ea9a07390db70a01453f865322cc622fb5e3e10 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Tue, 3 Nov 2026 21:51:33 -0800 Subject: [PATCH 2/5] fix(ci): pass exact Bun test file paths Co-Authored-By: GPT-6.1 Sol --- .github/justfile | 6 ++--- quest/m0/ci/README.md | 1 - quest/m0/ci/bun-test-paths.md | 31 -------------------------- sh/gh/test-inventory.py | 41 ++++++++++++++++++++++++++++++----- sh/gh/test-inventory.test.py | 41 +++++++++++++++++++++++++++++++++++ test/json-properties.sh | 2 +- test/justfile | 6 ++--- test/resume-model.sh | 2 +- test/smoke/justfile | 2 +- 9 files changed, 86 insertions(+), 46 deletions(-) delete mode 100644 quest/m0/ci/bun-test-paths.md diff --git a/.github/justfile b/.github/justfile index 9601229430d..c7c07caac1c 100644 --- a/.github/justfile +++ b/.github/justfile @@ -27,7 +27,7 @@ check: actionlint zizmor --offline --quiet .github sh/gh/alert.sh check-coverage - bun test sh/gh/alert.test.ts + bun test ./sh/gh/alert.test.ts bun sh/gh/check-runs.ts python3 sh/gh/test-inventory.py python3 bench/relay.test.py @@ -52,12 +52,12 @@ test: python3 sh/dispatch-query.test.py python3 sh/dispatch-query.test.py --bench python3 sh/gh/test-inventory.test.py - bun test sh/gh/setup.test.ts + bun test ./sh/gh/setup.test.ts bun sh/gh/queue.test.ts python3 sh/gh/install-formula.test.py sh/gh/release.test.sh python3 sh/gh/ffi.test.py - bun test sh/gh/ffi.test.ts sh/gh/cpp.test.ts sh/gh/publish.test.ts + bun test ./sh/gh/ffi.test.ts ./sh/gh/cpp.test.ts ./sh/gh/publish.test.ts python3 sh/rs/package-rename.test.py python3 sh/dart/build.test.py python3 sh/kt/gradle.test.py diff --git a/quest/m0/ci/README.md b/quest/m0/ci/README.md index 061a768aab2..57fe6cd064b 100644 --- a/quest/m0/ci/README.md +++ b/quest/m0/ci/README.md @@ -167,4 +167,3 @@ The historical samples locate candidates; they are not controlled benchmarks. - [A release merge opens no stale release PR](/quest/m0/ci/release-pr-after-release.md) - `just rs release` publishes before it proposes, so a merged release isn't re-proposed - [Dependabot leaves the workspace-hack to hakari](/quest/m0/ci/dependabot-workspace-hack.md) - cargo group PRs stop hand-editing `rs/workspace-hack/Cargo.toml` - [mbx's free-space GC never removes a live target](/quest/m0/ci/mbx-gc.md) - the cargo shim stops deleting targets other sessions use when the host disk runs low -- [Bun runs only the test files we name](/quest/m0/ci/bun-test-paths.md) - scripted `bun test` calls pass exact paths, so generated copies never run diff --git a/quest/m0/ci/bun-test-paths.md b/quest/m0/ci/bun-test-paths.md deleted file mode 100644 index 60afffd0baf..00000000000 --- a/quest/m0/ci/bun-test-paths.md +++ /dev/null @@ -1,31 +0,0 @@ -# [XS] Bun runs only the test files we name - -## Goal - -Every scripted `bun test ` passes an exact path. Bun then never runs a -same-named copy elsewhere in the checkout, such as a generated release tree -under `target/` or a scratch directory. A check keeps new bare filters -from coming back. - -## Plan - -Decided 2026-11-03 (maintainer, /quest-plan of the follow-ups from -[moq-dev/moq#7680](https://github.com/moq-dev/moq/pull/7680), first seen in -[moq-dev/moq#7669](https://github.com/moq-dev/moq/pull/7669)): milestone m0, -in the CI epic. - -- Cause: bun treats a bare argument as a substring filter over discovered - files. Only arguments that start with `./` or `/` are exact. The API - checker's generated release tree under the build cache held copies of - `sh/gh/*.test.ts`, and those old tests ran against main's workflows. -- Bare filters on main: `.github/justfile:30,54,59` - (`sh/gh/alert|setup|ffi|cpp|publish.test.ts`), - `test/json-properties.sh:12`, `test/resume-model.sh:12`, - `test/justfile:52,54,277` (check what the harness lists expand to), and - `test/smoke/justfile:122`. Prefix each with `./`. -- Guard: have the test inventory (`sh/gh/test-inventory.py`) or a small - lint refuse a scripted `bun test` argument that ends in `.test.ts` and - lacks `./` or `/`. If no root `bunfig.toml` exists, also consider one - that limits discovery to the source roots. - -Public API: none. Wire: none. diff --git a/sh/gh/test-inventory.py b/sh/gh/test-inventory.py index c7edff90c56..341ef14f006 100644 --- a/sh/gh/test-inventory.py +++ b/sh/gh/test-inventory.py @@ -1,4 +1,4 @@ -"""Fail when an opt-in test target has no CI caller. +"""Fail when an opt-in test target has no CI caller or a Bun file filter is ambiguous. nextest, `bun test`, and pytest discover ordinary tests by themselves; these targets run only when something names them: @@ -20,6 +20,9 @@ callers' first argument selects. A local-only target needs an entry in test-inventory.allow naming the quest that owns it. +Scripted Bun test files need `./` or `/`: bare paths are substring filters +that also run same-named tests in generated trees. + `--root DIR` checks another tree, for the tests. """ @@ -1033,6 +1036,34 @@ def discovered(root: Path, graph: Graph) -> set[Path]: return found +def bun_test_paths(root: Path, recipes: list[Recipe]) -> list[str]: + """Reject bare Bun test-file filters in recipes, shell scripts, workflows, and npm scripts.""" + texts = [(recipe.text, recipe_line(root, recipe)) for recipe in recipes] + texts += [(text, where) for text, where, _ in workflow_steps(root)] + texts += [(path.read_text(), str(path.relative_to(root))) for path in tracked(root, "*.sh")] + for manifest in tracked(root, "package.json", "*/package.json"): + texts += [ + (script, f"{manifest.relative_to(root)} scripts.{name}") + for name, script in json.loads(manifest.read_text()).get("scripts", {}).items() + ] + errors = set() + for text, where in texts: + for words in commands(text): + for index in range(len(words) - 1): + if words[index : index + 2] != ["bun", "test"]: + continue + value = False + for word in words[index + 2 :]: + if value: + value = False + continue + if word in ("-t", "--test-name-pattern", "--preload", "--reporter-outfile", "--coverage-dir"): + value = True + elif word.endswith(JS) and literal(word) and not word.startswith(("./", "/", "-")): + errors.add(f"{where}: bun test {word} is a substring filter; prefix the file path with ./") + return sorted(errors) + + def inventory(root: Path) -> tuple[list[tuple[str, str]], list[str]]: """Every orphaned target as (key, where), and errors in the inputs.""" justfiles = load_recipes(root) @@ -1044,7 +1075,8 @@ def inventory(root: Path) -> tuple[list[tuple[str, str]], list[str]]: manifests[package.directory.resolve()] = package.name orphans: list[tuple[str, str]] = [] - errors: list[str] = [] + recipes = {recipe.path: recipe for module in justfiles.values() for recipe in module.recipes.values()} + errors = bun_test_paths(root, list(recipes.values())) selections = [] for run in graph.runs: if any( @@ -1102,7 +1134,6 @@ def inventory(root: Path) -> tuple[list[tuple[str, str]], list[str]]: benches = bench_entries(root, graph) # Recipes that run tests. - recipes = {recipe.path: recipe for module in justfiles.values() for recipe in module.recipes.values()} for _, recipe in sorted(recipes.items()): if recipe.path not in graph.reached and runs_tests(graph, recipe, found, benches): orphans.append((f"recipe {recipe.path}", recipe_line(root, recipe))) @@ -1175,8 +1206,8 @@ def main(argv: list[str]) -> int: errors.append(f"{where}: {key} is not an orphan; drop the entry") if errors: print( - "Every opt-in test target needs a CI caller, or an entry in sh/gh/test-inventory.allow" - " naming the quest that owns it:", + "Test inventory failed (opt-in targets need a CI caller or a quest in sh/gh/test-inventory.allow;" + " Bun test files need exact paths):", file=sys.stderr, ) for error in errors: diff --git a/sh/gh/test-inventory.test.py b/sh/gh/test-inventory.test.py index dce5a05e7ec..18c915a52a0 100644 --- a/sh/gh/test-inventory.test.py +++ b/sh/gh/test-inventory.test.py @@ -350,6 +350,47 @@ def test_a_bun_workspace_covers_its_tests_once_its_runner_is_reached(self): self.write("js/net/package.json", '{"scripts": {"manual-test": "bun test src"}}') self.assertIn("script js/net/src/codec.test.ts", self.orphans()) + def test_bun_file_paths_are_exact_in_every_scripted_caller(self): + self.write( + "justfile", + JUSTFILE + + """ + files := ''' + test/compat.test.ts + ./test/exact.test.ts + ''' + + local: + cd test && bash ../sh/local.sh bun test {{ replace(files, "\\n", " ") }} +""", + ) + self.write("sh/local.sh", '"$@"\n') + self.write( + "sh/run.sh", + 'bun test \\\n "test/codec.test.ts" ./test/exact.test.ts # bun test commented.test.ts\n', + ) + self.write("package.json", '{"scripts": {"test": "bun test test/codec.spec.ts"}}') + workflow = self.root / ".github/workflows/ci.yml" + self.write(str(workflow.relative_to(self.root)), WORKFLOW + " - run: bun test test/frame.test.ts\n") + _, errors = module.inventory(self.root) + self.assertEqual( + errors, + [ + ".github/workflows/ci.yml (fuzz): bun test test/frame.test.ts is a substring filter;" + " prefix the file path with ./", + "justfile:16: bun test test/compat.test.ts is a substring filter; prefix the file path with ./", + "package.json scripts.test: bun test test/codec.spec.ts is a substring filter;" + " prefix the file path with ./", + "sh/run.sh: bun test test/codec.test.ts is a substring filter; prefix the file path with ./", + ], + ) + self.write( + "sh/run.sh", + 'bun test ./test/codec.test.ts /source/test/exact.test.ts src -t "codec.test.ts" --preload setup.ts\n' + "# bun test commented.test.ts\n", + ) + self.assertFalse(any("sh/run.sh:" in error for error in module.inventory(self.root)[1])) + def test_a_js_bench_entry_point_needs_a_caller(self): # Nightly's shape: a loop's literal names reach a recipe parameter through a helper's "$@". self.write( diff --git a/test/json-properties.sh b/test/json-properties.sh index 7e340fa1149..ca33fbeda72 100644 --- a/test/json-properties.sh +++ b/test/json-properties.sh @@ -9,6 +9,6 @@ export MOQ_JSON_SEED MOQ_JSON_CASES echo "JSON codec properties: MOQ_JSON_SEED=$MOQ_JSON_SEED MOQ_JSON_CASES=$MOQ_JSON_CASES" bun install --frozen-lockfile just rs test -p moq-json --lib -E 'test(snapshot_codec_property) | test(window_codec_property)' -bun test js/json/src/snapshot/properties.test.ts js/json/src/window/properties.test.ts +bun test ./js/json/src/snapshot/properties.test.ts ./js/json/src/window/properties.test.ts bunx tsc --noEmit --project js/json/bench/tsconfig.json bun js/json/bench/codec.ts diff --git a/test/justfile b/test/justfile index 7b702810714..02b319e803d 100644 --- a/test/justfile +++ b/test/justfile @@ -32,8 +32,8 @@ harness_browser_tests := ''' ''' harness_compat_tests := ''' - test/interop/compat/claims.test.ts - test/interop/compat/resolve.test.ts + ./test/interop/compat/claims.test.ts + ./test/interop/compat/resolve.test.ts ''' # List the cross-language suites. @@ -277,7 +277,7 @@ resume-model: qmux: cargo build --locked --quiet --manifest-path ../rs/qmux/Cargo.toml --example interop-server --example interop-client cd .. && bun install --frozen-lockfile - cd .. && QMUX_INTEROP_BIN={{ quote(qmux_examples) }} bun test test/qmux/interop.test.ts + cd .. && QMUX_INTEROP_BIN={{ quote(qmux_examples) }} bun test ./test/qmux/interop.test.ts # Complete MoQ SETUP and transfer a frame over a real browser data-channel pair. p2p: diff --git a/test/resume-model.sh b/test/resume-model.sh index 40678bc9af4..1cdb99d055b 100755 --- a/test/resume-model.sh +++ b/test/resume-model.sh @@ -9,4 +9,4 @@ export MOQ_RESUME_SEED MOQ_RESUME_CASES echo "Resume model: MOQ_RESUME_SEED=$MOQ_RESUME_SEED MOQ_RESUME_CASES=$MOQ_RESUME_CASES" just rs test -p moq-net --lib -E 'test(=model::resume_model::readers_match_the_logical_track)' bun install --frozen-lockfile -bun test js/net/src/resume.model.test.ts +bun test ./js/net/src/resume.model.test.ts diff --git a/test/smoke/justfile b/test/smoke/justfile index b335039c451..5acd8a831ee 100644 --- a/test/smoke/justfile +++ b/test/smoke/justfile @@ -119,7 +119,7 @@ harness-check: # Install the browser clients and Chromium's system libraries. playwright-deps: cd clients/js && bun install - cd clients/js && bun test diagnostics.test.ts + cd clients/js && bun test ./diagnostics.test.ts cd clients/js && bunx playwright install-deps chromium # Compile-check and unit-test the local integrity client against the current From ba00b1b197d6ff0b5decc6ae11220d60fc29d700 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Tue, 3 Nov 2026 22:00:37 -0800 Subject: [PATCH 3/5] fix(ci): read Bun caller files from the working tree Co-Authored-By: GPT-6.1 Sol --- sh/gh/test-inventory.py | 4 ++-- sh/gh/test-inventory.test.py | 13 +++++++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/sh/gh/test-inventory.py b/sh/gh/test-inventory.py index 341ef14f006..9d2f23da0cf 100644 --- a/sh/gh/test-inventory.py +++ b/sh/gh/test-inventory.py @@ -1040,8 +1040,8 @@ def bun_test_paths(root: Path, recipes: list[Recipe]) -> list[str]: """Reject bare Bun test-file filters in recipes, shell scripts, workflows, and npm scripts.""" texts = [(recipe.text, recipe_line(root, recipe)) for recipe in recipes] texts += [(text, where) for text, where, _ in workflow_steps(root)] - texts += [(path.read_text(), str(path.relative_to(root))) for path in tracked(root, "*.sh")] - for manifest in tracked(root, "package.json", "*/package.json"): + texts += [(path.read_text(), str(path.relative_to(root))) for path in tracked(root, "*.sh") if path.is_file()] + for manifest in (path for path in tracked(root, "package.json", "*/package.json") if path.is_file()): texts += [ (script, f"{manifest.relative_to(root)} scripts.{name}") for name, script in json.loads(manifest.read_text()).get("scripts", {}).items() diff --git a/sh/gh/test-inventory.test.py b/sh/gh/test-inventory.test.py index 18c915a52a0..3a406e9b8cc 100644 --- a/sh/gh/test-inventory.test.py +++ b/sh/gh/test-inventory.test.py @@ -391,6 +391,19 @@ def test_bun_file_paths_are_exact_in_every_scripted_caller(self): ) self.assertFalse(any("sh/run.sh:" in error for error in module.inventory(self.root)[1])) + def test_bun_guard_reads_the_working_tree_after_unstaged_deletions(self): + files = {"sh/run.sh": "bun test ./codec.test.ts\n", "web/package.json": '{"scripts": {"test": "bun test"}}'} + for name, text in files.items(): + self.write(name, text) + subprocess.run(["git", "add", *files], cwd=self.root, check=True) + for name, text in files.items(): + with self.subTest(name=name): + (self.root / name).unlink() + try: + self.orphans() + finally: + self.write(name, text) + def test_a_js_bench_entry_point_needs_a_caller(self): # Nightly's shape: a loop's literal names reach a recipe parameter through a helper's "$@". self.write( From d2a720aa4e733baa63ef264fd3facb76703acda4 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Tue, 3 Nov 2026 22:38:30 -0800 Subject: [PATCH 4/5] fix(ci): guard every Bun test-file extension The bare-path guard matched only .ts, .mts, .js, and .mjs, but Bun discovers *.test.{js,jsx,ts,tsx,mjs,cjs,mts,cts}. Use a dedicated tuple so JS keeps driving import and bench discovery unchanged. Co-Authored-By: Claude Opus 5.5 --- sh/gh/test-inventory.py | 4 +++- sh/gh/test-inventory.test.py | 12 +++++++++--- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/sh/gh/test-inventory.py b/sh/gh/test-inventory.py index 9d2f23da0cf..82ee10aa55f 100644 --- a/sh/gh/test-inventory.py +++ b/sh/gh/test-inventory.py @@ -43,6 +43,8 @@ SCRIPT = re.compile(r"[\w.$/{}-]*[\w-]\.(?:sh|py|ts|mts|js|mjs)\b") JS = (".ts", ".mts", ".js", ".mjs") +# Every extension Bun discovers as a test file (`*.test.{js,jsx,ts,tsx,mjs,cjs,mts,cts}`). +BUN_TEST = (".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs", ".mts", ".cts") # A relative module specifier in a static or dynamic import, or a re-export. IMPORT = re.compile(r"""\b(?:from|import)\s*\(?\s*["'](\.\.?/[^"']+)["']""") # Commands that run a test suite. Python sources spell them as argument lists, @@ -1059,7 +1061,7 @@ def bun_test_paths(root: Path, recipes: list[Recipe]) -> list[str]: continue if word in ("-t", "--test-name-pattern", "--preload", "--reporter-outfile", "--coverage-dir"): value = True - elif word.endswith(JS) and literal(word) and not word.startswith(("./", "/", "-")): + elif word.endswith(BUN_TEST) and literal(word) and not word.startswith(("./", "/", "-")): errors.add(f"{where}: bun test {word} is a substring filter; prefix the file path with ./") return sorted(errors) diff --git a/sh/gh/test-inventory.test.py b/sh/gh/test-inventory.test.py index 3a406e9b8cc..b56c0457183 100644 --- a/sh/gh/test-inventory.test.py +++ b/sh/gh/test-inventory.test.py @@ -357,6 +357,7 @@ def test_bun_file_paths_are_exact_in_every_scripted_caller(self): + """ files := ''' test/compat.test.ts + test/view.test.tsx ./test/exact.test.ts ''' @@ -367,9 +368,9 @@ def test_bun_file_paths_are_exact_in_every_scripted_caller(self): self.write("sh/local.sh", '"$@"\n') self.write( "sh/run.sh", - 'bun test \\\n "test/codec.test.ts" ./test/exact.test.ts # bun test commented.test.ts\n', + 'bun test \\\n "test/codec.test.ts" test/a.test.jsx test/b.test.cjs ./test/exact.test.ts # bun test commented.test.ts\n', ) - self.write("package.json", '{"scripts": {"test": "bun test test/codec.spec.ts"}}') + self.write("package.json", '{"scripts": {"test": "bun test test/codec.spec.ts test/codec.test.cts"}}') workflow = self.root / ".github/workflows/ci.yml" self.write(str(workflow.relative_to(self.root)), WORKFLOW + " - run: bun test test/frame.test.ts\n") _, errors = module.inventory(self.root) @@ -378,9 +379,14 @@ def test_bun_file_paths_are_exact_in_every_scripted_caller(self): [ ".github/workflows/ci.yml (fuzz): bun test test/frame.test.ts is a substring filter;" " prefix the file path with ./", - "justfile:16: bun test test/compat.test.ts is a substring filter; prefix the file path with ./", + "justfile:17: bun test test/compat.test.ts is a substring filter; prefix the file path with ./", + "justfile:17: bun test test/view.test.tsx is a substring filter; prefix the file path with ./", "package.json scripts.test: bun test test/codec.spec.ts is a substring filter;" " prefix the file path with ./", + "package.json scripts.test: bun test test/codec.test.cts is a substring filter;" + " prefix the file path with ./", + "sh/run.sh: bun test test/a.test.jsx is a substring filter; prefix the file path with ./", + "sh/run.sh: bun test test/b.test.cjs is a substring filter; prefix the file path with ./", "sh/run.sh: bun test test/codec.test.ts is a substring filter; prefix the file path with ./", ], ) From b46c9d78dd56d5ac06e1a7f025b265404bfe7df5 Mon Sep 17 00:00:00 2001 From: quest fixer Date: Wed, 4 Nov 2026 00:12:34 -0800 Subject: [PATCH 5/5] style: wrap inventory test fixture string --- sh/gh/test-inventory.test.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sh/gh/test-inventory.test.py b/sh/gh/test-inventory.test.py index b56c0457183..ae890ad8924 100644 --- a/sh/gh/test-inventory.test.py +++ b/sh/gh/test-inventory.test.py @@ -368,7 +368,8 @@ def test_bun_file_paths_are_exact_in_every_scripted_caller(self): self.write("sh/local.sh", '"$@"\n') self.write( "sh/run.sh", - 'bun test \\\n "test/codec.test.ts" test/a.test.jsx test/b.test.cjs ./test/exact.test.ts # bun test commented.test.ts\n', + 'bun test \\\n "test/codec.test.ts" test/a.test.jsx test/b.test.cjs ' + "./test/exact.test.ts # bun test commented.test.ts\n", ) self.write("package.json", '{"scripts": {"test": "bun test test/codec.spec.ts test/codec.test.cts"}}') workflow = self.root / ".github/workflows/ci.yml"