Files
bun-src/test/cli/install/lockfile-version-2.test.ts
2026-08-27 21:09:14 +00:00

655 lines
24 KiB
TypeScript

import { file, spawn } from "bun";
import { expect, it } from "bun:test";
import { exists } from "fs/promises";
import { bunExe, bunEnv as env, tempDir } from "harness";
import { join } from "path";
// These tests cover the text lockfile bump to version 2 and the parse-time
// checks gated behind it. They run fully offline — using `file:` deps or
// loopback/unreachable endpoints — so no external network or registry is
// required.
it("a freshly written text lockfile defaults to version 2", async () => {
using dir = tempDir("lockfile-v2-default", {
"package.json": JSON.stringify({ name: "root", dependencies: { dep: "file:./dep" } }),
"dep/package.json": JSON.stringify({ name: "dep", version: "1.0.0" }),
});
await using proc = spawn({
cmd: [bunExe(), "install", "--save-text-lockfile"],
cwd: String(dir),
env,
stdout: "pipe",
stderr: "pipe",
});
const [out, err, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
const lockfile = await file(join(String(dir), "bun.lock")).text();
expect(err).not.toContain("error:");
expect(lockfile).toContain(`"lockfileVersion": 2,`);
expect(exitCode).toBe(0);
});
// Re-saving an existing lockfile must never bump its version. A v1 `bun.lock`
// that is rewritten — here because a new dependency is added — keeps
// `lockfileVersion: 1`, even though every entry would satisfy the v2 invariants.
// Only a lockfile with no prior version (fresh install / migration) is written
// at the current version.
it("re-saving a v1 lockfile keeps it at version 1 even after adding a dependency", async () => {
const v1Lockfile =
JSON.stringify(
{
lockfileVersion: 1,
configVersion: 1,
workspaces: { "": { name: "root", dependencies: { a: "file:./a" } } },
packages: { a: ["a@file:a", {}] },
},
null,
2,
) + "\n";
using dir = tempDir("lockfile-v1-no-bump", {
// package.json asks for a second `file:` dep the lockfile doesn't have yet,
// forcing a real re-save (not a no-op skip).
"package.json": JSON.stringify({ name: "root", dependencies: { a: "file:./a", b: "file:./b" } }),
"a/package.json": JSON.stringify({ name: "a", version: "1.0.0" }),
"b/package.json": JSON.stringify({ name: "b", version: "1.0.0" }),
"bun.lock": v1Lockfile,
});
await using proc = spawn({
cmd: [bunExe(), "install"],
cwd: String(dir),
env,
stdout: "pipe",
stderr: "pipe",
});
const [out, err, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
const after = await file(join(String(dir), "bun.lock")).text();
expect(err).not.toContain("error:");
// The lockfile was actually rewritten (the new dep is present)...
expect(after).toContain(`"b": ["b@file:b"`);
// ...but its version was preserved, not bumped to 2.
expect(after).toContain(`"lockfileVersion": 1,`);
expect(after).not.toContain(`"lockfileVersion": 2,`);
expect(exitCode).toBe(0);
});
// A v0 lockfile is the one version that must NOT be preserved verbatim: v0→v1
// was a content-format change (v1 stopped emitting a workspace package's deps
// object), and the writer only ever emits the v1+ single-element
// `["name@workspace:path"]` form. Stamping v0 on that output would make the
// next parse fail with "Missing dependencies object". So a re-saved v0 lockfile
// is floored to v1 — and, critically, must still parse on the next install.
it("re-saving a v0 lockfile floors it to version 1 so it stays parseable", async () => {
const v0Lockfile =
JSON.stringify(
{
lockfileVersion: 0,
workspaces: {
"": { name: "root", dependencies: { pkg1: "workspace:*" } },
"packages/pkg1": { name: "pkg1" },
},
// v0 workspace entry shape (with a trailing object).
packages: { pkg1: ["pkg1@workspace:packages/pkg1", {}] },
},
null,
2,
) + "\n";
using dir = tempDir("lockfile-v0-floor", {
"package.json": JSON.stringify({
name: "root",
workspaces: ["packages/*"],
dependencies: { pkg1: "workspace:*" },
}),
"packages/pkg1/package.json": JSON.stringify({ name: "pkg1" }),
"bun.lock": v0Lockfile,
});
// First install re-saves the lockfile.
await using first = spawn({
cmd: [bunExe(), "install"],
cwd: String(dir),
env,
stdout: "pipe",
stderr: "pipe",
});
const [, firstErr, firstExit] = await Promise.all([first.stdout.text(), first.stderr.text(), first.exited]);
const after = await file(join(String(dir), "bun.lock")).text();
expect(firstErr).not.toContain("error:");
// Floored to v1 — never left at v0 (the writer can't emit v0 content), and
// not bumped past the existing version to v2 either.
expect(after).toContain(`"lockfileVersion": 1,`);
expect(after).not.toContain(`"lockfileVersion": 0,`);
expect(after).not.toContain(`"lockfileVersion": 2,`);
expect(firstExit).toBe(0);
// The re-saved lockfile must still parse. With the version left at v0 this
// would fail with "failed to parse lockfile" / "Missing dependencies object".
await using second = spawn({
cmd: [bunExe(), "install", "--frozen-lockfile"],
cwd: String(dir),
env,
stdout: "pipe",
stderr: "pipe",
});
const [, secondErr, secondExit] = await Promise.all([second.stdout.text(), second.stderr.text(), second.exited]);
expect(secondErr).not.toContain("failed to parse lockfile");
expect(secondErr).not.toContain("Ignoring lockfile");
expect(secondErr).not.toContain("lockfile had changes, but lockfile is frozen");
expect(secondExit).toBe(0);
});
it("an existing v1 lockfile still loads (backward compatible)", async () => {
const v1Lockfile =
JSON.stringify(
{
lockfileVersion: 1,
configVersion: 1,
workspaces: { "": { name: "root", dependencies: { dep: "file:./dep" } } },
packages: { dep: ["dep@file:dep", {}] },
},
null,
2,
) + "\n";
using dir = tempDir("lockfile-v1-load", {
"package.json": JSON.stringify({ name: "root", dependencies: { dep: "file:./dep" } }),
"dep/package.json": JSON.stringify({ name: "dep", version: "1.0.0" }),
"bun.lock": v1Lockfile,
});
await using proc = spawn({
cmd: [bunExe(), "install", "--frozen-lockfile"],
cwd: String(dir),
env,
stdout: "pipe",
stderr: "pipe",
});
const [out, err, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(err).not.toContain("error:");
expect(await exists(join(String(dir), "node_modules", "dep", "package.json"))).toBe(true);
expect(exitCode).toBe(0);
});
// An off-registry npm tarball URL with no integrity hash is a breaking change
// introduced after the Rust rewrite: rejecting it breaks lockfiles written
// before the check existed, so it is only enforced at version 2.
it("off-registry npm tarball integrity is enforced only at version 2", async () => {
// A loopback host that is never the configured registry. For v2 parsing fails
// before any fetch, so it is not contacted; for v1 parsing succeeds and the
// install proceeds to request the tarball, hitting this 404 handler.
await using offRegistry = Bun.serve({
port: 0,
hostname: "127.0.0.1",
fetch() {
return new Response("not found", { status: 404 });
},
});
const offRegistryTarball = `http://127.0.0.1:${offRegistry.port}/no-deps/-/no-deps-1.0.0.tgz`;
const lockfile = (lockfileVersion: number) =>
JSON.stringify({
lockfileVersion,
configVersion: 1,
workspaces: { "": { name: "root", dependencies: { "no-deps": "1.0.0" } } },
packages: { "no-deps": ["[email protected]", offRegistryTarball, {}, ""] },
});
// version 2 fails closed while parsing.
{
using dir = tempDir("lockfile-v2-integrity", {
"package.json": JSON.stringify({ name: "root", dependencies: { "no-deps": "1.0.0" } }),
"bun.lock": lockfile(2),
});
await using proc = spawn({
cmd: [bunExe(), "install", "--frozen-lockfile"],
cwd: String(dir),
env,
stdout: "pipe",
stderr: "pipe",
});
const [out, err, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(err).toContain(
"Missing integrity hash for npm package resolved to a tarball URL outside the configured registry",
);
expect(await exists(join(String(dir), "node_modules", "no-deps"))).toBe(false);
expect(exitCode).not.toBe(0);
}
// version 1 predates the check, so parsing accepts it (the install then fails
// to download the tarball from the 404 handler, but not with the integrity error).
{
using dir = tempDir("lockfile-v1-integrity", {
"package.json": JSON.stringify({ name: "root", dependencies: { "no-deps": "1.0.0" } }),
"bun.lock": lockfile(1),
});
await using proc = spawn({
cmd: [bunExe(), "install", "--frozen-lockfile"],
cwd: String(dir),
env,
stdout: "pipe",
stderr: "pipe",
});
const [out, err, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(err).not.toContain("Missing integrity hash");
}
});
// An unsafe `git` `.bun-tag` is a post-rewrite breaking change that is gated to
// v2. `Repository::checkout` re-validates the tag before running git, so a v1
// lockfile carrying an unsafe git tag still never executes anything unsafe.
// (The `github` tarball path has no such re-validation — see the next test.)
it("unsafe git .bun-tag is rejected only at version 2", async () => {
// When v1 parsing succeeds, install proceeds to `git clone` (an https
// attempt, then ssh). `GIT_ALLOW_PROTOCOL=file` makes git reject both
// transports immediately, so the clone fails fast with no network at all —
// connecting to an "unreachable" loopback port can hang on some CI hosts.
const gitEnv = { ...env, GIT_ALLOW_PROTOCOL: "file" };
const gitUrl = "git+ssh://[email protected]:1/example/repo.git#main";
const lockfile = (lockfileVersion: number) =>
JSON.stringify({
lockfileVersion,
configVersion: 1,
workspaces: {
"": { name: "root", dependencies: { dep: gitUrl } },
},
packages: {
// `.bun-tag` (last element) contains a path separator.
dep: [`dep@${gitUrl}`, {}, "../escape"],
},
});
// version 2 rejects the unsafe tag while parsing, before any git work.
{
using dir = tempDir("lockfile-v2-gittag", {
"package.json": JSON.stringify({ name: "root", dependencies: { dep: gitUrl } }),
"bun.lock": lockfile(2),
});
await using proc = spawn({
cmd: [bunExe(), "install", "--frozen-lockfile"],
cwd: String(dir),
env: gitEnv,
stdout: "pipe",
stderr: "pipe",
});
const [out, err, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(err).toContain("Invalid git dependency tag");
expect(exitCode).not.toBe(0);
}
// version 1 predates the check, so parsing no longer rejects it (the install
// then fails cloning the repo, but not with the parse-time error).
{
using dir = tempDir("lockfile-v1-gittag", {
"package.json": JSON.stringify({ name: "root", dependencies: { dep: gitUrl } }),
"bun.lock": lockfile(1),
});
await using proc = spawn({
cmd: [bunExe(), "install", "--frozen-lockfile", "--lockfile-only"],
cwd: String(dir),
env: gitEnv,
stdout: "pipe",
stderr: "pipe",
});
const [out, err, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(err).not.toContain("Invalid git dependency tag");
// v1 parses cleanly and `--lockfile-only` skips the install, so it exits 0.
expect(exitCode).toBe(0);
}
});
// A `github` dependency resolves via the tarball-download path, not
// `Repository::checkout`, so its `.bun-tag` is fed into the cache folder name
// with no use-site re-validation. The parse-time safety check therefore stays
// unconditional for github — an unsafe tag is rejected at every version.
it("unsafe github .bun-tag is rejected at every version", async () => {
const ghUrl = "github:example/repo#main";
const lockfile = (lockfileVersion: number) =>
JSON.stringify({
lockfileVersion,
configVersion: 1,
workspaces: {
"": { name: "root", dependencies: { dep: ghUrl } },
},
packages: {
// `.bun-tag` (last element) contains a path separator.
dep: [`dep@${ghUrl}`, {}, "../escape"],
},
});
for (const version of [1, 2]) {
using dir = tempDir(`lockfile-v${version}-githubtag`, {
"package.json": JSON.stringify({ name: "root", dependencies: { dep: ghUrl } }),
"bun.lock": lockfile(version),
});
await using proc = spawn({
cmd: [bunExe(), "install", "--frozen-lockfile"],
cwd: String(dir),
env,
stdout: "pipe",
stderr: "pipe",
});
const [out, err, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(err).toContain("Invalid git dependency tag");
expect(exitCode).not.toBe(0);
}
});
it("a github resolution whose committish contains a slash reads back with owner, repo and tag intact", async () => {
const ghUrl = "github:example/repo#feature/branch";
const lockfile = JSON.stringify({
lockfileVersion: 1,
configVersion: 1,
workspaces: {
"": { name: "root", dependencies: { dep: ghUrl } },
},
packages: {
dep: [`dep@${ghUrl}`, {}, "example-repo-abc1234"],
},
});
using dir = tempDir("lockfile-github-committish-slash", {
"package.json": JSON.stringify({ name: "root", dependencies: { dep: ghUrl } }),
"bun.lock": lockfile,
});
await using proc = spawn({
cmd: [bunExe(), "pm", "hash-string"],
cwd: String(dir),
env,
stdout: "pipe",
stderr: "pipe",
});
const [out, err, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(out).toContain("dep@github:example/repo#abc1234\n");
expect(exitCode).toBe(0);
});
// The writer must not silently upgrade a lockfile to v2 if doing so would make
// it fail the v2 parse checks on the next install. A v1 lockfile with an
// off-registry tarball and no integrity hash must round-trip as v1.
it("re-saving a v1 off-registry lockfile keeps it at version 1", async () => {
await using offRegistry = Bun.serve({
port: 0,
hostname: "127.0.0.1",
fetch() {
return new Response("not found", { status: 404 });
},
});
const v1Lockfile = JSON.stringify({
lockfileVersion: 1,
configVersion: 1,
workspaces: { "": { name: "root", dependencies: { "no-deps": "1.0.0" } } },
packages: {
"no-deps": ["[email protected]", `http://127.0.0.1:${offRegistry.port}/no-deps/-/no-deps-1.0.0.tgz`, {}, ""],
},
});
using dir = tempDir("lockfile-v1-roundtrip", {
"package.json": JSON.stringify({ name: "root", dependencies: { "no-deps": "1.0.0" } }),
"bun.lock": v1Lockfile,
});
// `--lockfile-only` re-serializes the lockfile without performing the install
// (so the unreachable off-registry tarball is never fetched). This is the
// write path that would wrongly stamp v2 without the version-selection guard.
await using proc = spawn({
cmd: [bunExe(), "install", "--lockfile-only"],
cwd: String(dir),
env,
stdout: "pipe",
stderr: "pipe",
});
const [out, err, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
const after = await file(join(String(dir), "bun.lock")).text();
expect(err).toContain("Saved lockfile");
// Still v1 — the off-registry no-integrity entry can't be made v2-valid, so
// stamping v2 would make the next parse reject it.
expect(after).toContain(`"lockfileVersion": 1,`);
expect(exitCode).toBe(0);
});
// A nested override rule alone must not push a walk-held v1 file to v3 (v3 implies v2 strictness).
it("re-saving a v1 off-registry lockfile that gains a nested override rule stays at version 1 and re-reads frozen in the same directory", async () => {
await using offRegistry = Bun.serve({
port: 0,
hostname: "127.0.0.1",
fetch() {
return new Response("not found", { status: 404 });
},
});
const v1Lockfile = JSON.stringify({
lockfileVersion: 1,
configVersion: 1,
workspaces: { "": { name: "root", dependencies: { "no-deps": "1.0.0" } } },
packages: {
"no-deps": ["[email protected]", `http://127.0.0.1:${offRegistry.port}/no-deps/-/no-deps-1.0.0.tgz`, {}, ""],
},
});
using dir = tempDir("lockfile-v1-nested-override-roundtrip", {
"package.json": JSON.stringify({
name: "root",
dependencies: { "no-deps": "1.0.0" },
overrides: { "no-deps": { bar: "1.0.0" } },
}),
"bun.lock": v1Lockfile,
});
await using proc = spawn({
cmd: [bunExe(), "install", "--lockfile-only"],
cwd: String(dir),
env,
stdout: "pipe",
stderr: "pipe",
});
const [, err, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
const after = await file(join(String(dir), "bun.lock")).text();
expect(err).toContain("Saved lockfile");
expect(after).toContain(`"lockfileVersion": 1,`);
expect(after).not.toContain(`"lockfileVersion": 3,`);
expect(after).toContain(`"no-deps": {`);
expect(after).toContain(`"bar": "1.0.0"`);
expect(after).toContain(`no-deps-1.0.0.tgz", {}, ""]`);
expect(exitCode).toBe(0);
await using frozenProc = spawn({
cmd: [bunExe(), "install", "--frozen-lockfile", "--lockfile-only"],
cwd: String(dir),
env,
stdout: "pipe",
stderr: "pipe",
});
const [, frozenErr, frozenExit] = await Promise.all([
frozenProc.stdout.text(),
frozenProc.stderr.text(),
frozenProc.exited,
]);
expect(frozenErr).not.toContain(
"Missing integrity hash for npm package resolved to a tarball URL outside the configured registry",
);
expect(frozenErr).not.toContain("Ignoring lockfile");
expect(frozenErr).not.toContain("lockfile had changes, but lockfile is frozen");
expect(await file(join(String(dir), "bun.lock")).text()).toBe(after);
expect(frozenExit).toBe(0);
});
// The version stamp must not depend on the writer's registry config. A lockfile
// is committed and shared, so whether the *reader* accepts it cannot hinge on
// the *writer*'s `~/.npmrc` / scoped registries. A v1 lockfile with a tarball
// under a writer-only scoped registry (and no integrity hash) used to be stamped
// v2 — matching the writer's own scope — which then failed to parse for a
// teammate or CI that lacks that scope. It must round-trip as v1 so it keeps
// loading regardless of the reader's config.
it("re-saving keeps v1 for a tarball under a writer-only scoped registry", async () => {
// A scoped registry the writer knows about but a reader won't. `--lockfile-only`
// never fetches the tarball, so the host need not be reachable.
await using scopedRegistry = Bun.serve({
port: 0,
hostname: "127.0.0.1",
fetch() {
return new Response("not found", { status: 404 });
},
});
const scopedRegistryUrl = `http://127.0.0.1:${scopedRegistry.port}/`;
const scopedTarball = `${scopedRegistryUrl}@myorg/foo/-/foo-1.0.0.tgz`;
const v1Lockfile = JSON.stringify({
lockfileVersion: 1,
configVersion: 1,
workspaces: { "": { name: "root", dependencies: { "@myorg/foo": "1.0.0" } } },
packages: {
// Off-registry tarball (under the scoped registry, not the default) with
// an empty integrity hash.
"@myorg/foo": ["@myorg/[email protected]", scopedTarball, {}, ""],
},
});
// Writer: has the `@myorg` scope configured, so `scope_for_package_name`
// resolves the tarball URL to its registry. This is the config that used to
// make the writer consider the entry "v2-clean" and stamp v2.
using writerDir = tempDir("lockfile-scoped-writer", {
"package.json": JSON.stringify({ name: "root", dependencies: { "@myorg/foo": "1.0.0" } }),
"bunfig.toml": Bun.TOML.stringify({ install: { scopes: { myorg: { url: scopedRegistryUrl } } } }),
"bun.lock": v1Lockfile,
});
await using writerProc = spawn({
cmd: [bunExe(), "install", "--lockfile-only"],
cwd: String(writerDir),
env,
stdout: "pipe",
stderr: "pipe",
});
const [, writerErr, writerExit] = await Promise.all([
writerProc.stdout.text(),
writerProc.stderr.text(),
writerProc.exited,
]);
const rewritten = await file(join(String(writerDir), "bun.lock")).text();
expect(writerErr).toContain("Saved lockfile");
// Must stay v1: the tarball is not under the *default* registry, which is the
// only normalization the writer applies, so v2 can't be guaranteed to parse
// for a reader without the `@myorg` scope.
expect(rewritten).toContain(`"lockfileVersion": 1,`);
expect(writerExit).toBe(0);
// Reader: no `@myorg` scope. The re-saved lockfile must still load — if the
// writer had stamped v2, this would fail with the integrity error.
using readerDir = tempDir("lockfile-scoped-reader", {
"package.json": JSON.stringify({ name: "root", dependencies: { "@myorg/foo": "1.0.0" } }),
"bun.lock": rewritten,
});
await using readerProc = spawn({
cmd: [bunExe(), "install", "--frozen-lockfile", "--lockfile-only"],
cwd: String(readerDir),
env,
stdout: "pipe",
stderr: "pipe",
});
const [, readerErr, readerExit] = await Promise.all([
readerProc.stdout.text(),
readerProc.stderr.text(),
readerProc.exited,
]);
expect(readerErr).not.toContain(
"Missing integrity hash for npm package resolved to a tarball URL outside the configured registry",
);
// v1 round-trips with no fetch, so the reader exits cleanly.
expect(readerExit).toBe(0);
});
it("re-saving keeps v1 for a writer-only scoped tarball even when a nested override rule is added", async () => {
await using scopedRegistry = Bun.serve({
port: 0,
hostname: "127.0.0.1",
fetch() {
return new Response("not found", { status: 404 });
},
});
const scopedRegistryUrl = `http://127.0.0.1:${scopedRegistry.port}/`;
const scopedTarball = `${scopedRegistryUrl}@myorg/foo/-/foo-1.0.0.tgz`;
const v1Lockfile = JSON.stringify({
lockfileVersion: 1,
configVersion: 1,
workspaces: { "": { name: "root", dependencies: { "@myorg/foo": "1.0.0" } } },
packages: {
"@myorg/foo": ["@myorg/[email protected]", scopedTarball, {}, ""],
},
});
// `bar` is not in the tree, so the new rule re-resolves nothing and the row survives verbatim.
const packageJson = JSON.stringify({
name: "root",
dependencies: { "@myorg/foo": "1.0.0" },
overrides: { "@myorg/foo": { bar: "1.0.0" } },
});
using writerDir = tempDir("lockfile-scoped-nested-writer", {
"package.json": packageJson,
"bunfig.toml": Bun.TOML.stringify({ install: { scopes: { myorg: { url: scopedRegistryUrl } } } }),
"bun.lock": v1Lockfile,
});
await using writerProc = spawn({
cmd: [bunExe(), "install", "--lockfile-only"],
cwd: String(writerDir),
env,
stdout: "pipe",
stderr: "pipe",
});
const [, writerErr, writerExit] = await Promise.all([
writerProc.stdout.text(),
writerProc.stderr.text(),
writerProc.exited,
]);
const rewritten = await file(join(String(writerDir), "bun.lock")).text();
expect(writerErr).toContain("Saved lockfile");
expect(rewritten).toContain(`"lockfileVersion": 1,`);
expect(rewritten).not.toContain(`"lockfileVersion": 3,`);
expect(rewritten).toContain(`"@myorg/foo": {`);
expect(rewritten).toContain(`"bar": "1.0.0"`);
expect(rewritten).toContain(scopedTarball);
expect(writerExit).toBe(0);
using readerDir = tempDir("lockfile-scoped-nested-reader", {
"package.json": packageJson,
"bun.lock": rewritten,
});
await using readerProc = spawn({
cmd: [bunExe(), "install", "--frozen-lockfile", "--lockfile-only"],
cwd: String(readerDir),
env,
stdout: "pipe",
stderr: "pipe",
});
const [, readerErr, readerExit] = await Promise.all([
readerProc.stdout.text(),
readerProc.stderr.text(),
readerProc.exited,
]);
expect(readerErr).not.toContain(
"Missing integrity hash for npm package resolved to a tarball URL outside the configured registry",
);
expect(readerErr).not.toContain("Ignoring lockfile");
expect(readerErr).not.toContain("lockfile had changes, but lockfile is frozen");
expect(await file(join(String(readerDir), "bun.lock")).text()).toBe(rewritten);
expect(readerExit).toBe(0);
});