From 396b333171feb1b7a633a5ff2b05c993763b6c6c Mon Sep 17 00:00:00 2001 From: wan9chi Date: Mon, 9 Nov 2026 18:20:07 +0800 Subject: [PATCH 1/2] fix(workspace): error when a package#task names no workspace package `vp run nope#build`, with no package named `nope`, ran nothing and exited 0 without a warning. A `package#task` specifier names one package on purpose, so it now fails with `Package 'nope' not found in the workspace` and exits 1, also when nested in a task's command. A short name that auto-completes to several scoped packages now fails with the existing ambiguous-name error instead of matching nothing. `--filter` keeps its pnpm-compatible warning and exit 0; only the `pkg#task` pattern, now flagged `exactly_one` instead of `unique`, errors. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 1 + .../fixtures/filter_unmatched/snapshots.toml | 14 ++++++ ...nknown_package_in_task_specifier_errors.md | 12 +++++ ...nknown_package_in_task_specifier_errors.md | 12 +++++ .../fixtures/filter_unmatched/vite-task.json | 4 ++ crates/vt_graph/src/query/mod.rs | 4 +- .../duplicate_package_names/snapshots.toml | 7 +++ .../query_ambiguous_short_package_name.snap | 1 + .../fixtures/filter_workspace/snapshots.toml | 6 +++ .../query_short_package_specifier.jsonc | 4 ++ crates/vt_workspace/src/package_filter.rs | 12 +++-- crates/vt_workspace/src/package_graph.rs | 50 ++++++++++++++----- 12 files changed, 107 insertions(+), 20 deletions(-) create mode 100644 crates/vt_bin/tests/e2e_snapshots/fixtures/filter_unmatched/snapshots/nested_unknown_package_in_task_specifier_errors.md create mode 100644 crates/vt_bin/tests/e2e_snapshots/fixtures/filter_unmatched/snapshots/unknown_package_in_task_specifier_errors.md create mode 100644 crates/vt_plan/tests/plan_snapshots/fixtures/duplicate_package_names/snapshots/query_ambiguous_short_package_name.snap create mode 100644 crates/vt_plan/tests/plan_snapshots/fixtures/filter_workspace/snapshots/query_short_package_specifier.jsonc diff --git a/CHANGELOG.md b/CHANGELOG.md index 6fbf58d2d..267b050f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ # Changelog +- **Fixed** A task specifier like `vp run nope#build`, where no workspace package is named `nope`, now fails with `Package 'nope' not found in the workspace` and exits 1, including from a task's command, instead of running nothing and exiting 0. A short name that auto-completes to several scoped packages, like `app` for `@a/app` and `@b/app`, now fails as ambiguous. `--filter nope` still only warns and exits 0, unless `--fail-if-no-match` is set ([#984](https://github.com/voidzero-dev/vite-task/pull/984)). - **Added** `vp run -vv` also lists what each cached run depends on, changed or not, and the output files a hit restores: in the detailed summary, under `Unchanged:` and `Outputs:`, and with `--dry-run --json`, as each step's `baseline.fingerprint` and `baseline.outputs`. It shows, for an unexpected hit, whether an edited file is an input at all. `-v` is now a counted flag, so `-vv` is the same as `--verbose --verbose` ([#969](https://github.com/voidzero-dev/vite-task/pull/969)). - **Added** `vp run --dry-run --json` prints what a run would do as one JSON document for scripts, CI and coding agents: each task the run would run, the tasks it waits for, and each step's program, arguments and directory, with whether it would hit the cache and, if not, every change found. Like `vp run --json`, it's indented in a terminal and on one line when captured, and its `$schema` field points to a JSON Schema that describes each field. A task that runs `vp run` shows that run's own plan as its step. A step that waits on one that would write files first, by running or by restoring outputs, is `unknown`, since its files may change before it starts. `--json` with a task requires `--dry-run` ([#966](https://github.com/voidzero-dev/vite-task/pull/966), [#967](https://github.com/voidzero-dev/vite-task/pull/967), [#968](https://github.com/voidzero-dev/vite-task/pull/968)). - **Changed** A cache miss now lists every change to the command, the task's cache settings and the envs it depends on, instead of the first: when a task's `input` and `output` settings changed together, or a tracked env var and a file changed together, `vp run --verbose` and `vp run --last-details` show each change on its own line. Without `-v`, files are checked only up to the first changed one; with `-v`, every input of a miss is checked, and every changed file is listed in path order. When the command, a setting or a `cache.env` var changed, only those changes are listed, since there's no cached run with the new ones to compare files with. A summary saved by an earlier version can't be shown by `vp run --last-details` ([#961](https://github.com/voidzero-dev/vite-task/pull/961), [#963](https://github.com/voidzero-dev/vite-task/pull/963)). diff --git a/crates/vt_bin/tests/e2e_snapshots/fixtures/filter_unmatched/snapshots.toml b/crates/vt_bin/tests/e2e_snapshots/fixtures/filter_unmatched/snapshots.toml index 5a559345b..7a270f543 100644 --- a/crates/vt_bin/tests/e2e_snapshots/fixtures/filter_unmatched/snapshots.toml +++ b/crates/vt_bin/tests/e2e_snapshots/fixtures/filter_unmatched/snapshots.toml @@ -100,3 +100,17 @@ comment = """ Strict mode also propagates through nesting: a task command that adds `--fail-if-no-match` aborts the outer task when the nested filter selects nothing. The error chain identifies both the nested command and the unmatched filter source. """ steps = [["vt", "run", "filter-nonexistent-strict"]] + +[[e2e]] +name = "unknown_package_in_task_specifier_errors" +comment = """ +A `package#task` specifier names its package explicitly, so a package that doesn't exist is an error rather than an empty `--filter`: the run exits 1, and the error goes to stderr with nothing on stdout. +""" +steps = [["vtt", "pipe-stdout", "--", "vt", "run", "nonexistent#build"]] + +[[e2e]] +name = "nested_unknown_package_in_task_specifier_errors" +comment = """ +A nested `vt run nonexistent#build` fails the outer task's planning too, unlike the nested `--filter nonexistent` above. The error chain names the nested command and the missing package. +""" +steps = [["vt", "run", "run-unknown-package"]] diff --git a/crates/vt_bin/tests/e2e_snapshots/fixtures/filter_unmatched/snapshots/nested_unknown_package_in_task_specifier_errors.md b/crates/vt_bin/tests/e2e_snapshots/fixtures/filter_unmatched/snapshots/nested_unknown_package_in_task_specifier_errors.md new file mode 100644 index 000000000..6960d06da --- /dev/null +++ b/crates/vt_bin/tests/e2e_snapshots/fixtures/filter_unmatched/snapshots/nested_unknown_package_in_task_specifier_errors.md @@ -0,0 +1,12 @@ +# nested_unknown_package_in_task_specifier_errors + +A nested `vt run nonexistent#build` fails the outer task's planning too, unlike the nested `--filter nonexistent` above. The error chain names the nested command and the missing package. + +## `vt run run-unknown-package` + +**Exit code:** 1 + +``` +error: Failed to plan tasks from `vt run nonexistent#build` in task filter-unmatched-test#run-unknown-package +* Package 'nonexistent' not found in the workspace +``` diff --git a/crates/vt_bin/tests/e2e_snapshots/fixtures/filter_unmatched/snapshots/unknown_package_in_task_specifier_errors.md b/crates/vt_bin/tests/e2e_snapshots/fixtures/filter_unmatched/snapshots/unknown_package_in_task_specifier_errors.md new file mode 100644 index 000000000..27a45296d --- /dev/null +++ b/crates/vt_bin/tests/e2e_snapshots/fixtures/filter_unmatched/snapshots/unknown_package_in_task_specifier_errors.md @@ -0,0 +1,12 @@ +# unknown_package_in_task_specifier_errors + +A `package#task` specifier names its package explicitly, so a package that doesn't exist is an error rather than an empty `--filter`: the run exits 1, and the error goes to stderr with nothing on stdout. + +## `vtt pipe-stdout -- vt run nonexistent#build` + +**Exit code:** 1 + +``` +error: Package 'nonexistent' not found in the workspace +--- stdout --- +``` diff --git a/crates/vt_bin/tests/e2e_snapshots/fixtures/filter_unmatched/vite-task.json b/crates/vt_bin/tests/e2e_snapshots/fixtures/filter_unmatched/vite-task.json index c895e5e6c..4bb9d6198 100644 --- a/crates/vt_bin/tests/e2e_snapshots/fixtures/filter_unmatched/vite-task.json +++ b/crates/vt_bin/tests/e2e_snapshots/fixtures/filter_unmatched/vite-task.json @@ -8,6 +8,10 @@ "filter-nonexistent-strict": { "command": "vt run --filter nonexistent --fail-if-no-match build", "cache": false + }, + "run-unknown-package": { + "command": "vt run nonexistent#build", + "cache": false } } } diff --git a/crates/vt_graph/src/query/mod.rs b/crates/vt_graph/src/query/mod.rs index 79332ed60..be4b10641 100644 --- a/crates/vt_graph/src/query/mod.rs +++ b/crates/vt_graph/src/query/mod.rs @@ -133,8 +133,8 @@ impl IndexedTaskGraph<'_> { /// /// # Errors /// - /// Returns [`PackageQueryResolveError::AmbiguousPackageName`] when an exact - /// package name (from a `pkg#task` specifier) matches multiple packages, and + /// Returns [`PackageQueryResolveError`] when an exact package name (from a + /// `pkg#task` specifier) matches no package or several, and /// [`TaskGraphLoadError`] when the tasks of a selected package fail to load. /// /// # Order of operations diff --git a/crates/vt_plan/tests/plan_snapshots/fixtures/duplicate_package_names/snapshots.toml b/crates/vt_plan/tests/plan_snapshots/fixtures/duplicate_package_names/snapshots.toml index d92619e44..50e1da6fc 100644 --- a/crates/vt_plan/tests/plan_snapshots/fixtures/duplicate_package_names/snapshots.toml +++ b/crates/vt_plan/tests/plan_snapshots/fixtures/duplicate_package_names/snapshots.toml @@ -15,3 +15,10 @@ args = ["run", "-t", "@test/duplicate#build"] compact = true name = "filter_matches_both_duplicates" args = ["run", "--filter", "@test/duplicate", "build"] + +# The short name `duplicate` auto-completes to `@test/duplicate`, which two +# packages share. A `pkg#task` specifier errors instead of matching neither. +[[plan]] +compact = true +name = "ambiguous_short_package_name" +args = ["run", "duplicate#build"] diff --git a/crates/vt_plan/tests/plan_snapshots/fixtures/duplicate_package_names/snapshots/query_ambiguous_short_package_name.snap b/crates/vt_plan/tests/plan_snapshots/fixtures/duplicate_package_names/snapshots/query_ambiguous_short_package_name.snap new file mode 100644 index 000000000..52bf576a2 --- /dev/null +++ b/crates/vt_plan/tests/plan_snapshots/fixtures/duplicate_package_names/snapshots/query_ambiguous_short_package_name.snap @@ -0,0 +1 @@ +Package name 'duplicate' is ambiguous; found in multiple locations: /packages/pkg-a, /packages/pkg-b \ No newline at end of file diff --git a/crates/vt_plan/tests/plan_snapshots/fixtures/filter_workspace/snapshots.toml b/crates/vt_plan/tests/plan_snapshots/fixtures/filter_workspace/snapshots.toml index ed7b3449f..fa58d1093 100644 --- a/crates/vt_plan/tests/plan_snapshots/fixtures/filter_workspace/snapshots.toml +++ b/crates/vt_plan/tests/plan_snapshots/fixtures/filter_workspace/snapshots.toml @@ -135,6 +135,12 @@ compact = true name = "transitive_with_package_specifier" args = ["run", "-t", "@test/app#build"] +# package#task with a short name auto-completes to the one scoped package. +[[plan]] +compact = true +name = "short_package_specifier" +args = ["run", "app#build"] + # recursive flag = All [[plan]] compact = true diff --git a/crates/vt_plan/tests/plan_snapshots/fixtures/filter_workspace/snapshots/query_short_package_specifier.jsonc b/crates/vt_plan/tests/plan_snapshots/fixtures/filter_workspace/snapshots/query_short_package_specifier.jsonc new file mode 100644 index 000000000..250125d7b --- /dev/null +++ b/crates/vt_plan/tests/plan_snapshots/fixtures/filter_workspace/snapshots/query_short_package_specifier.jsonc @@ -0,0 +1,4 @@ +// run app#build +{ + "packages/app#build": [] +} diff --git a/crates/vt_workspace/src/package_filter.rs b/crates/vt_workspace/src/package_filter.rs index 8948f3c43..b2858f89e 100644 --- a/crates/vt_workspace/src/package_filter.rs +++ b/crates/vt_workspace/src/package_filter.rs @@ -80,9 +80,11 @@ pub(crate) enum PackageNamePattern { /// but exactly one `@*/bar` package exists, that package is matched. /// pnpm ref: /// - /// When `unique` is true, resolution errors if multiple packages share the - /// name. Set for `pkg#task` CLI specifiers; false for `--filter`. - Exact { name: Str, unique: bool }, + /// When `exactly_one` is true, resolution errors unless the name picks + /// exactly one package: none found, several sharing the name, or several + /// scoped candidates. Set for `pkg#task` CLI specifiers; false for + /// `--filter`, where an unmatched name only warns. + Exact { name: Str, exactly_one: bool }, /// Glob pattern (e.g. `@scope/*`, `*-utils`). Iterates all packages. /// @@ -404,7 +406,7 @@ impl PackageQueryArgs { exclude: false, selector: PackageSelector::Name(PackageNamePattern::Exact { name, - unique: true, + exactly_one: true, }), traversal, source: None, @@ -613,7 +615,7 @@ fn build_name_pattern(name: &str) -> Result>().join(", ") )] AmbiguousPackageName { package_name: Str, package_paths: Box<[Arc]> }, + + #[error("Package '{package_name}' not found in the workspace")] + PackageNotFound { package_name: Str }, } // ──────────────────────────────────────────────────────────────────────────── @@ -206,7 +209,8 @@ impl IndexedPackageGraph { /// # Errors /// /// Returns [`PackageQueryResolveError::AmbiguousPackageName`] when an exact - /// package name (from a `pkg#task` specifier) matches multiple packages. + /// package name (from a `pkg#task` specifier) matches multiple packages, and + /// [`PackageQueryResolveError::PackageNotFound`] when it matches none. pub fn resolve_query( &self, query: &PackageQuery, @@ -343,29 +347,24 @@ impl IndexedPackageGraph { /// For `Exact` names, scoped auto-completion applies /// (pnpm ref: ): /// if `"bar"` has no exact match but exactly one `@*/bar` package exists, - /// that package is used instead. + /// that package is used instead. An `exactly_one` name that still matches + /// no package, or several, is an error. fn match_by_name_pattern( &self, pattern: &PackageNamePattern, out: &mut FxHashSet, ) -> Result<(), PackageQueryResolveError> { match pattern { - PackageNamePattern::Exact { name, unique } => { + PackageNamePattern::Exact { name, exactly_one } => { if let Some(indices) = self.get_package_indices_by_name(name) { - if *unique && indices.len() > 1 { - return Err(PackageQueryResolveError::AmbiguousPackageName { - package_name: name.clone(), - package_paths: indices - .iter() - .map(|i| Arc::clone(&self.graph[*i].absolute_path)) - .collect(), - }); + if *exactly_one && indices.len() > 1 { + return Err(self.ambiguous_package_name(name, indices)); } out.extend(indices.iter().copied()); } else { // Scoped auto-completion: `"bar"` → `"@scope/bar"` if exactly one match. let scoped_suffix = vt_str::format!("/{}", name); - let scoped: Vec<_> = self + let mut scoped: Vec<_> = self .indices_by_name .iter() .filter(|(pkg_name, _)| { @@ -375,8 +374,18 @@ impl IndexedPackageGraph { .collect(); if scoped.len() == 1 { out.insert(scoped[0]); + } else if *exactly_one { + if scoped.is_empty() { + return Err(PackageQueryResolveError::PackageNotFound { + package_name: name.clone(), + }); + } + // Sorted so the listed paths don't depend on hash order. + scoped.sort_unstable(); + return Err(self.ambiguous_package_name(name, &scoped)); } - // 0 matches: nothing to insert. + // For `--filter`: + // 0 matches: nothing to insert; the caller warns about the filter. // >1 matches: ambiguous; skip rather than guessing wrong. } } @@ -393,6 +402,21 @@ impl IndexedPackageGraph { Ok(()) } + /// The error for a `pkg#task` name that matches several packages. + fn ambiguous_package_name( + &self, + name: &Str, + indices: &[PackageNodeIndex], + ) -> PackageQueryResolveError { + PackageQueryResolveError::AmbiguousPackageName { + package_name: name.clone(), + package_paths: indices + .iter() + .map(|i| Arc::clone(&self.graph[*i].absolute_path)) + .collect(), + } + } + /// Match packages by a directory pattern, inserting into `out`. /// /// pnpm ref: From ed120e026e004b8b557b01904c79ba83383c239e Mon Sep 17 00:00:00 2001 From: wan9chi Date: Mon, 9 Nov 2026 19:31:46 +0800 Subject: [PATCH 2/2] test(run): expect an unknown package to fail `--dry-run --json` `run_plan_json::selection` recorded `nope#build` giving an empty plan with exit 0. With the planner now rejecting a `pkg#task` that names no workspace package, the dry run exits 1 with the error on stderr and nothing on stdout, as a real run does. Co-Authored-By: Claude Opus 5.5 --- .../e2e_snapshots/fixtures/run_plan_json/snapshots.toml | 2 +- .../fixtures/run_plan_json/snapshots/selection.md | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/crates/vt_bin/tests/e2e_snapshots/fixtures/run_plan_json/snapshots.toml b/crates/vt_bin/tests/e2e_snapshots/fixtures/run_plan_json/snapshots.toml index 1cac53d2e..a56b58dfa 100644 --- a/crates/vt_bin/tests/e2e_snapshots/fixtures/run_plan_json/snapshots.toml +++ b/crates/vt_bin/tests/e2e_snapshots/fixtures/run_plan_json/snapshots.toml @@ -142,7 +142,7 @@ steps = [ "--dry-run", "--json", "nope#build", - ], comment = "No package is named `nope`, so, as in a real run, the plan is empty.", redact = { '"\$schema":"[^"]*"' = '"$$schema":""' } }, + ], comment = "No package is named `nope`, so, as in a real run, it's an error and nothing goes to stdout." }, { argv = [ "vtt", "pipe-stdout", diff --git a/crates/vt_bin/tests/e2e_snapshots/fixtures/run_plan_json/snapshots/selection.md b/crates/vt_bin/tests/e2e_snapshots/fixtures/run_plan_json/snapshots/selection.md index c586ae831..396161499 100644 --- a/crates/vt_bin/tests/e2e_snapshots/fixtures/run_plan_json/snapshots/selection.md +++ b/crates/vt_bin/tests/e2e_snapshots/fixtures/run_plan_json/snapshots/selection.md @@ -33,11 +33,13 @@ error: Task "typo" not found ## `vtt pipe-stdout -- vt run --dry-run --json nope#build` -No package is named `nope`, so, as in a real run, the plan is empty. +No package is named `nope`, so, as in a real run, it's an error and nothing goes to stdout. + +**Exit code:** 1 ``` +error: Package 'nope' not found in the workspace --- stdout --- -{"$schema":"","version":1,"workspaceRoot":"","plan":{}} ``` ## `vtt pipe-stdout -- vt run --dry-run --json --filter nope build`