test(install): port pnpm/test/update.ts (13/22)#438
Conversation
Lands [test/pnpm_update.bats](test/pnpm_update.bats) covering the
basic-update, --no-save, --latest --prod, and recursive update fanout
cases (single + multi-project). Updates [test/PNPM_TEST_IMPORT.md] with
the per-test status, the divergences uncovered while porting, and the
fixtures still needed (`@pnpm.e2e/qar`, `@pnpm.e2e/has-prerelease`)
for the remaining tests.
Divergences documented (not fixed in this PR):
- `aube update <pkg>@<spec>` is not parsed; ports use `--latest <pkg>`.
- `aube update --depth N` is a no-op and plain `aube update` preserves
indirect-dep versions from the lockfile snapshot, so misc.ts:599
('deep update') doesn't translate.
- `aube update <indirect-pkg>` errors when the package isn't in
package.json's dep map (update.rs:139-146), so misc.ts:690 doesn't
translate either.
- `aube update -r` always writes per-project lockfiles, regardless of
`sharedWorkspaceLockfile`. The shared-lockfile assertions on
pnpm/test/update.ts:471-475 and :531-535 are dropped from the ports;
per-project lockfile assertions stand in.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit e707eb6. Configure here.
Greptile SummaryPorts 13 of 22 Confidence Score: 5/5Safe to merge — only P2 test-completeness gaps, no logic errors in the production code. All findings are P2 (missing lockfile assertions and a slightly misleading test name). The Rust changes are well-reasoned, internally consistent, and covered by the 13 passing bats tests plus the existing update.bats/add.bats regression suite. test/pnpm_update.bats — two minor test-completeness gaps noted inline. Important Files Changed
Reviews (4): Last reviewed commit: "fix(cli): aube update -r honors --prod/-..." | Re-trigger Greptile |
Benchmark changesVersions:
Public ratios: warm installs vs Bun 4x -> 4x; warm installs vs pnpm 5x -> 8x.
5806988 vs 2ad5c54 | aube/bun/pnpm | 3 scenarios | 3 runs | 500mbit/50ms | generated by Codex. |
Synthesizes the @pnpm.e2e/has-prerelease fixture (versions 1.0.0,
2.0.0, 3.0.0-rc.0) at test/registry/storage/@pnpm.e2e/has-prerelease/
— the package doesn't exist on registry.npmjs.org, so the tarballs
are produced via `npm pack` on a minimal manifest and the packument
is hand-built. Lands one new port that this fixture unblocks:
- pnpm/test/update.ts:659 ('update with tag @latest will downgrade
prerelease') → `aube update --latest <pkg>` translation.
Three more has-prerelease tests (615, 728, 807) didn't translate
cleanly: aube's `update --latest` downgrades a manifest pin from a
newer prerelease (e.g. 3.0.0-rc.0) to the registry's `latest`
dist-tag (2.0.0) instead of preserving the prerelease as pnpm does.
Documented as a fourth divergence in PNPM_TEST_IMPORT.md.
8/22 ported.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Replace `refute grep` with `run grep; assert_failure` (cursor[bot]): `refute` accepts any non-zero exit, so `grep` failing because the lockfile is missing (exit 2) silently passed instead of failing the test. The `run; assert_failure` form keeps `$status`/`$output` inspectable and matches the pattern used elsewhere in the file. - Add the missing `project/package.json` manifest assertion to the `--no-shared-workspace-lockfile` test (greptile[bot]): `--latest` is supposed to rewrite specifiers, so the test now verifies both the per-project lockfile location AND the manifest rewrite to `^100.1.0`, matching the assertion shape used in the other `--latest` tests. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mirrors `@pnpm.e2e/qar` (100.0.0, 100.1.0) at
test/registry/storage/@pnpm.e2e/qar/ using the same hand-built packument
+ npm pack approach as @pnpm.e2e/has-prerelease, and ports the five
pnpm/test/update.ts tests this fixture unblocks. Three small aube-side
fixes landed alongside the ports to make them green:
1. update --latest now rewrites `npm:` alias specifiers in the manifest
(previously: lockfile bumped, manifest stuck). Aliased direct deps
land in the lockfile graph with `pkg.name == "<alias>"` and
`pkg.alias_of == Some("<real>")`; the version-lookup match in
update.rs accepted only `real_name`, so aliased entries fell through
to `continue` and the manifest was never rewritten. Added a
`lookup_pkg` helper that matches either name, and extended the
filtered-existing snapshot to drop entries whose `pkg.name` matches
the manifest key (alias) too — without this the resolver kept the
locked alias version under --latest.
2. New `--save-exact` / `-E` flag on `aube update`, mirroring
`pnpm update --save-exact`. Pairs with --latest to drop the
caret/tilde prefix on the rewritten specifier (manifest carries
`"1.2.3"` instead of `"^1.2.3"`).
3. Recursive update silently skips projects that don't declare any of
the named args. Without this, `aube update -r --latest foo alias`
hard-errors on the first project that doesn't have `alias` in its
package.json — pnpm's recursive update just filters the arg list
per project. Fixes the fanout for the four
`recursive update --latest <name>` tests in update.ts.
Ports landed:
- pnpm/test/update.ts:143 ('update --latest')
- pnpm/test/update.ts:170 ('update --latest --save-exact')
- pnpm/test/update.ts:197 ('update --latest specific dependency')
- pnpm/test/update.ts:369 ('recursive update --latest specific no-shared')
- pnpm/test/update.ts:543 ('recursive update --latest specific shared',
asserted per-project — see PNPM_TEST_IMPORT.md for the
shared-lockfile divergence)
The `kevva/is-negative` GitHub-shorthand assertion was dropped from
143/170/197 — the `is-negative` portion is the only `is-negative`-related
expectation in those tests, and adding `user/repo` shorthand resolution
is its own feature (parser + git tarball fetcher + storage). Tracked as
a remaining gap in PNPM_TEST_IMPORT.md.
13/22 ported (was 8/22).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ct args run_filtered's per-project "declared" set was a union of every dep bucket — but `run` itself filters by `--prod`/`--dev`/`--no-optional` before looking up args. So a named arg that exists only as a devDep in some project would survive the per-project filter (because it's in the project's full dep map) and then hard-error inside `run` with 'package X is not a dependency' (the inner all_specifiers had already excluded devDeps under --prod). Mirror the include_prod / include_dev / include_optional logic from `run` when building `declared`, so each project's declared set matches the bucket the inner update will actually look at. Adds a regression test exercising the failure mode greptile flagged (`aube update -r --latest --prod foo` against a workspace where one project has foo only as a devDep — must skip, not error). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ps (#446) ## Summary Two small changes in [crates/aube/src/commands/update.rs](crates/aube/src/commands/update.rs) that finish the `pnpm/test/update.ts` cluster everywhere except the documented prerelease-preservation and shared-lockfile divergences. Brings the file to **15/22 ports** (was 13/22). ## Changes ### 1. `<pkg>@<spec>` arg syntax pnpm phrases the manifest-rewrite-past-range case as `pnpm update foo@latest`; aube was rejecting that with *package foo@latest is not a dependency* because it looked up the literal arg in the deps map. Now `<name>@<spec>` is split (scope-aware via a `split_pkg_arg` helper), the bare `name` drives the dep lookup, and `@latest` (the only spec form pnpm tests rely on) acts as a per-key `--latest` — the manifest rewrite triggers only for that one entry instead of every direct dep. This drops the translation note from the misc.ts:14 port — the new test in this PR uses the original `update <pkg>@latest` syntax verbatim instead of rewriting to `update --latest <pkg>`. ### 2. `aube update <indirect-pkg>` no longer errors Previously the args loop hard-failed on any name that wasn't in `package.json`'s deps map. Indirect deps now flow through: - Validated against the lockfile graph (rejected if not in any direct/transitive snapshot entry). - Filtered out of the locked snapshot so the resolver re-resolves. - Their parents' locked dep edges get rewritten to `latest` when the user passed `<indirect>@latest`. - Manifest is left alone (the indirect has no entry to rewrite). - The direct dep that pulls in the indirect stays at its locked version — only the named arg bumps. The parent-edge rewrite is the non-obvious bit. The resolver's lockfile-reuse path ([aube_resolver::resolve.rs:1164](crates/aube-resolver/src/resolve.rs#L1164)) iterates each parent's locked `dependencies` map and enqueues transitive tasks using the locked **version** as the range. Just dropping the indirect from the `packages` map isn't enough — the parent's pinned edge (`pkg-with-1-dep@100.0.0`'s `dependencies: { dep-of: 100.0.0 }`) still re-resolves to the same version. Forwarding `latest` on the edge makes the transitive task a dist-tag spec, so the resolver fetches the packument and picks the new latest. ## Ports | pnpm test (line) | aube `@test` | |---|---| | `update <dep>` (14) | `aube update <pkg>@latest: parses arg syntax, rewrites manifest past range` | | `update indirect dependency should not update package.json` (690) | `aube update <indirect-pkg>@latest: refreshes a transitive dep, leaves manifest alone` | The earlier `update --latest <pkg>` translation of misc.ts:14 stays as its own test — keeps coverage of both invocation forms. ## Remaining gaps in update.ts After this PR, 7 of 22 tests stay unported, all because of distinct divergences (each tracked in [PNPM_TEST_IMPORT.md](test/PNPM_TEST_IMPORT.md)): - 51, 95: `aube update` (no `--latest`) doesn't rewrite the manifest spec when the existing range allows a newer version (pnpm does). - 599: `aube update --depth N` is parsed-but-no-op. - 615, 728, 807: aube's `update --latest` downgrades a manifest pin past a registry's older `latest` dist-tag (pnpm preserves the pin when it's numerically newer). - 249, 302: subsumed by 369/543 — aube's `update -r` always writes per-project lockfiles regardless of `sharedWorkspaceLockfile`. ## Test plan - [x] `AUBE_TEST_REGISTRY=http://localhost:4873 ./test/bats/bin/bats test/pnpm_update.bats` — 16/16 pass. - [x] `AUBE_TEST_REGISTRY=http://localhost:4873 ./test/bats/bin/bats test/update.bats test/add.bats test/install.bats` — 96/96 pass (no regression to the existing aube-native test surface). - [x] `cargo fmt --check` - [x] `cargo clippy --all-targets -- -D warnings` - [x] `cargo test --workspace` - [x] `mise run render` — no docs/usage drift (the `--save-exact` flag landed in [#438](#438)). 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Changes `aube update` dependency selection and lockfile filtering/rewriting behavior, including transitive edge rewrites, which could affect resolution results across workspaces. Scope is contained to the update command and covered by new bats regression tests. > > **Overview** > `aube update` now parses positional args of the form `<pkg>@<spec>` (scope-aware) and treats `@latest` as a per-package equivalent of `--latest`, while hard-erroring on any other spec to avoid silently ignoring user intent. > > The update flow now accepts *indirect (transitive) deps* named on the CLI: it validates them against the lockfile graph (with a workspace-root lockfile fallback), drops their snapshots from the reused lockfile, and for `<indirect>@latest` rewrites parent locked dependency edges to `latest` so the resolver actually re-resolves the transitive version; `package.json` is left unchanged for indirect updates. > > Recursive `update -r` was adjusted to forward indirect args into per-project updates by consulting each project’s lockfile (with shared-lockfile fallback) and to avoid misclassifying flag-excluded direct deps (e.g. `--prod <devDep>`) as indirect. New bats tests cover the new arg syntax, indirect updates, spec rejection, and the `--prod` regression. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 7f95613. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Summary
Lands test/pnpm_update.bats — the first batch of pnpm/test/update.ts ports. 13/22 ported. Plus two synthesized fixtures and three small aube-side fixes that fell out of porting.
Ports
@testupdate <dep>(14)aube update --latest <pkg>: bumps a single dep past its declared rangeupdate --no-save(34)aube update --no-save: refreshes the lockfile, leaves package.json range aloneupdate --latest(143)aube update --latest: bumps prod deps, npm: aliases, and rangesupdate --latest --save-exact(170)aube update --latest -E: rewrites manifest specs as exact pinsupdate --latest specific dep(197)aube update --latest <name>: bumps named deps, leaves others pinnedupdate --latest --prod(225)aube update --latest --prod: bumps prod deps, leaves devDeps pinnedrecursive update --no-save(72)aube update -r --no-save: refreshes a workspace lockfile, leaves manifests alonerecursive update --no-shared-workspace-lockfile(118)aube update -r --no-shared-workspace-lockfile: writes a per-project lockfilerecursive update --latest specific no-shared(369)aube update -r --latest <name>: bumps named deps across workspacerecursive update --latest on shared lockfile(426)aube update -r --latest: bumps every workspace project's manifestrecursive update --latest --prod on shared lockfile(478)aube update -r --latest --prod: skips devDeps in workspace fanoutrecursive update --latest specific shared(543)aube update -r --latest <name>: same shape as no-shared (per-project)update with tag @latest will downgrade prerelease(659)aube update --latest <pkg>: downgrades prerelease to the latest dist-tagPhase 0 fixtures mirrored
Both fixtures are pnpm-internal (don't exist on registry.npmjs.org), so tarballs are produced via
npm packon a minimalpackage.jsonand the packuments are hand-built.@pnpm.e2e/has-prereleaseat 1.0.0, 2.0.0, 3.0.0-rc.0 (latest=2.0.0, so the prerelease is "newer than latest").@pnpm.e2e/qarat 100.0.0, 100.1.0 — used as thenpm:alias target by the misc.ts:143/170/197/369/543 ports.Aube-side fixes
Three small fixes landed alongside the ports (each under ~15 lines):
update --latestrewritesnpm:alias specifiers (update.rs). Previously the lockfile bumped but the manifest stayed at the original version. Aliased direct deps land in the lockfile graph withpkg.name == "<alias>"andpkg.alias_of == Some("<real>"); the version-lookup match accepted onlyreal_name, so aliased entries fell through tocontinueand the manifest was never rewritten. Added alookup_pkghelper that matches either name, and extended the filtered-existing snapshot to drop entries whosepkg.namematches the manifest key (alias) so the resolver doesn't keep the locked alias version under--latest.--save-exact/-Eflag onaube updatemirroringpnpm update --save-exact. Pairs with--latestto drop the caret/tilde prefix on the rewritten specifier (manifest carries"1.2.3"instead of"^1.2.3").aube update -r --latest foo aliashard-errors on the first project that doesn't havealiasin itspackage.json— pnpm's recursive update just filters the arg list per project. Fixes the fanout for therecursive update --latest <name>tests.Divergences uncovered (documented in PNPM_TEST_IMPORT.md, not fixed here)
aube update <pkg>@<spec>not parsed.update.rslooks up the literal arg in package.json's deps map, soaube update foo@latesterrors with not a dependency. Ports translate toaube update --latest <pkg>.aube update --depth Nis a no-op + plainaube updatepreserves indirect-dep versions from the lockfile. pnpm/test/update.ts:599 ('deep update') doesn't have an equivalent.aube update <indirect-pkg>errors even though pnpm allows it (pnpm/test/update.ts:690).aube update -ralways writes per-project lockfiles, regardless ofsharedWorkspaceLockfile. Per-project lockfile assertions stand in for shared-lockfile assertions.--latest. pnpm keeps a manifest pin like3.0.0-rc.0when the registry'slatestis older (e.g. 2.0.0); aube downgrades. Affects 615, 728, 807.Skipped
kevva/is-negative) is its own feature (parser + git tarball fetcher + storage). The singleis-negativeassertion was dropped from the 143/170/197 ports — every other assertion in those tests stands. Tracked as a remaining gap in PNPM_TEST_IMPORT.md.recursive update --latest [--prod] on no-shared-lockfile projects) are subsumed by the shared-lockfile ports above — aube'supdate -rproduces per-project lockfiles in both modes, so the two pnpm variants collapse.Test plan
AUBE_TEST_REGISTRY=http://localhost:4873 ./test/bats/bin/bats test/pnpm_update.bats— 13/13 pass.AUBE_TEST_REGISTRY=http://localhost:4873 ./test/bats/bin/bats test/update.bats test/add.bats— 35/35 still pass (no regression).cargo test --workspace— all green, includingcli_ordering_tests::test_cli_ordering.cargo fmt --checkcargo clippy --all-targets -- -D warningsmise run render—aube.usage.kdlanddocs/cli/update.mdregenerated for the new-E/--save-exactflag.🤖 Generated with Claude Code
Note
Medium Risk
Modifies
aube updateresolution and manifest-rewrite behavior (including alias handling and workspace fanout), which can affect dependency upgrades and lockfile contents across projects. Changes are well-covered by new end-to-end Bats tests but touch core package-update logic.Overview
Ports a first batch of pnpm’s
updateintegration tests into a newtest/pnpm_update.bats, along with new local-registry fixtures for@pnpm.e2e/has-prereleaseand@pnpm.e2e/qar.Extends
aube updateto better match pnpm semantics: adds-E/--exact(--save-exact) to force exact pins when rewriting manifests under--latest, fixes--latesthandling fornpm:alias dependencies by matching/updating via the manifest key as well as the real package name, and makes recursive/workspace updates skip projects that don’t declare any of the requested package args instead of erroring. CLI usage/docs are regenerated to include the new flag.Reviewed by Cursor Bugbot for commit 5806988. Bugbot is set up for automated code reviews on this repo. Configure here.