inspector/scripts/verify-bundle-externals.test.mjs
cliffhall e31a20eec7 test(proxy): isolate proxy env per file; word the guard failure by candidate source
Addresses review round 7 on #2099 (three suppressed comments).

- Proxy variables are now cleared for the WHOLE of `transport.test.ts` and
  `cli.test.ts`, not just around the proxy tests. Measured: undici's
  `EnvHttpProxyAgent` captures its proxy URLs when it is CONSTRUCTED and re-reads
  only NO_PROXY per request, so with the process-wide memo, any earlier test that
  connects on a machine with an ambient proxy would pin the agent to it and a
  later test setting HTTP_PROXY would be talking to an agent that is not
  listening. Clearing up front means nothing can build the singleton before the
  proxy tests do, and restoring only in the file-level afterAll means the memo
  cannot be left bound to a closed test proxy.

  Both suites verified green under a hostile ambient env
  (`HTTP_PROXY`/`http_proxy`/`HTTPS_PROXY` pointed at a dead port). In fairness
  this is hardening rather than a fixed failure: the concrete cross-test break
  did not reproduce with the current test order — but it depends on test order
  and on the developer's environment, which is exactly what this removes.

- The guard's failure message said a package "is declared external but was
  inlined" even for a candidate that came only from the root dependencies —
  i.e. #2067's own shape, where the missing `external` entry IS the bug, so the
  message contradicted the config it was asking the reader to check. It now
  words the failure by candidate source, and says to add the entry.

The capture-at-construction behavior is documented on `createProxyFetch`, since
it is the reason the suites are structured this way.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JTHVxSu8AUgHRLvo1ntZ8H
Signed-off-by: cliffhall <cliff@futurescale.com>
2026-08-23 23:00:28 -04:00

333 lines
11 KiB
JavaScript

/**
* Table-driven tests for the pure parsers of `verify-bundle-externals.mjs`.
*
* One case per rule the guard encodes. The guard itself reads real build output,
* which no unit test can produce cheaply — so what is pinned here is the
* *decision* logic: which entries count as externals, and which emitted shapes
* count as "inlined anyway".
*/
import { test, describe } from "node:test";
import assert from "node:assert/strict";
import { existsSync, readdirSync } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
const repoRoot = path.resolve(
path.dirname(fileURLToPath(import.meta.url)),
"..",
);
import {
BUNDLED_CLIENTS,
evaluateClient,
parseExternals,
findInlinedExternals,
} from "./verify-bundle-externals.mjs";
describe("parseExternals", () => {
test("collects bare string entries", () => {
assert.deepEqual(
parseExternals(`export default { external: ["undici", "pino"] }`),
["undici", "pino"],
);
});
test("ignores package names that appear only inside comments", () => {
// The real configs document each entry at length, and #2067's entry names
// `undici` in its own prose — a comment-blind scan would double-count it and,
// worse, invent entries for packages merely discussed.
const src = `export default { external: [
// "ghost" is explained here but is NOT an entry.
/* neither is "phantom" */
"undici",
] }`;
assert.deepEqual(parseExternals(src), ["undici"]);
});
test("handles single quotes and nested arrays", () => {
assert.deepEqual(
parseExternals(
`export default { external: ['a', 'b'], noExternal: ["c"] }`,
),
["a", "b"],
);
});
test("returns an empty list when there is no external array", () => {
assert.deepEqual(parseExternals(`export default { entry: ["x.ts"] }`), []);
});
});
describe("findInlinedExternals", () => {
const banner = (p) => `// ${p}\nvar x = 1;\n`;
test("flags a package inlined as its own chunk", () => {
// The exact shape #2067 shipped: a dynamically import()ed CommonJS package
// emitted as `undici-HXPKCIY3.js` beside index.js.
const { violations } = findInlinedExternals({
externals: ["undici"],
files: [
{
name: "index.js",
source: banner("../../core/mcp/node/transport.ts"),
},
{
name: "undici-HXPKCIY3.js",
source: banner("../../node_modules/undici/lib/core/symbols.js"),
},
],
});
assert.equal(violations.length, 1);
assert.equal(violations[0].pkg, "undici");
assert.match(violations[0].reason, /undici-HXPKCIY3\.js/);
});
test("flags a package inlined straight into the entry, with no chunk", () => {
// A *statically* imported package is folded into index.js and emits no
// chunk at all, so a chunk-name check would miss it entirely. The banner is
// what gives it away.
const { violations } = findInlinedExternals({
externals: ["pino"],
files: [
{
name: "index.js",
source:
banner("../../core/mcp/index.ts") +
banner("../../node_modules/pino/pino.js"),
},
],
});
assert.equal(violations.length, 1);
assert.equal(violations[0].pkg, "pino");
assert.match(violations[0].reason, /index\.js/);
});
test("passes a genuinely external package", () => {
const { noBanners, violations } = findInlinedExternals({
externals: ["undici", "pino"],
files: [
{
name: "index.js",
source:
banner("../../core/mcp/node/proxyFetch.ts") +
'await import("undici");\nimport "pino";\n',
},
],
});
assert.equal(noBanners, false);
assert.deepEqual(violations, []);
});
test("does not mistake a same-prefix package for the external", () => {
// `undici-types` is a @types transitive and is legitimately inlined; the
// trailing slash in the needle is what keeps it from reading as `undici`.
const { violations } = findInlinedExternals({
externals: ["undici"],
files: [
{
name: "index.js",
source: banner("../../node_modules/undici-types/index.js"),
},
],
});
assert.deepEqual(violations, []);
});
test("matches a scoped package on its full name", () => {
const { violations } = findInlinedExternals({
externals: ["@napi-rs/keyring"],
files: [
{
name: "index.js",
source: banner("../../node_modules/@napi-rs/keyring/index.js"),
},
],
});
assert.equal(violations.length, 1);
assert.equal(violations[0].pkg, "@napi-rs/keyring");
});
test("reports noBanners rather than a clean pass when banners are absent", () => {
// The guard reads esbuild's `// <path>` module banners, so minified output
// would make every bundle look clean. That must fail loudly instead —
// otherwise enabling `minify` silently retires the check.
const { noBanners, violations } = findInlinedExternals({
externals: ["undici"],
files: [{ name: "index.js", source: "var a=1;var b=2;" }],
});
assert.equal(noBanners, true);
assert.deepEqual(violations, []);
});
});
describe("evaluateClient", () => {
const banner = (p) => `// ${p}\nvar x = 1;\n`;
const clean = [
{ name: "index.js", source: banner("../../core/mcp/index.ts") },
];
test("fails closed when no externals could be parsed", () => {
// A tsup refactor — `external: EXTERNALS`, or a line break after the colon —
// makes parseExternals return nothing. Checking zero packages and reporting
// success would silently disable the guard on a change that looks harmless.
const failures = evaluateClient({
name: "web",
build: "clients/web/build",
externals: [],
files: clean,
});
assert.equal(failures.length, 1);
assert.match(failures[0], /no `external` entries could be parsed/);
});
test("fails when the bundle carries no module banners", () => {
const failures = evaluateClient({
name: "cli",
build: "clients/cli/build",
externals: ["undici"],
files: [{ name: "index.js", source: "var a=1;" }],
});
assert.equal(failures.length, 1);
assert.match(failures[0], /no esbuild module banners/);
});
test("reports one failure per inlined package", () => {
const failures = evaluateClient({
name: "tui",
build: "clients/tui/build",
externals: ["undici", "pino"],
files: [
{
name: "index.js",
source:
banner("../../node_modules/undici/index.js") +
banner("../../node_modules/pino/pino.js"),
},
],
});
assert.equal(failures.length, 2);
assert.match(
failures[0],
/tui: "undici" is declared external but was inlined/,
);
});
test("flags a root dependency even when the external list omits it", () => {
// THE #2067 SHAPE, and the reason the candidate set is not just `externals`.
// `undici` was missing from the web and TUI external lists — that omission
// WAS the bug — so a guard driven only by those lists would have inspected
// the inlined 1.05MB bundle and reported success. Removing a package from an
// `external` list must not remove it from scrutiny.
const failures = evaluateClient({
name: "web",
build: "clients/web/build",
externals: ["pino"],
rootDependencies: ["undici", "pino"],
files: [
{
name: "undici-HXPKCIY3.js",
source: banner("../../node_modules/undici/lib/core/symbols.js"),
},
],
});
assert.equal(failures.length, 1);
// Worded for where the candidate came from: `undici` is NOT in this
// client's `external` list here — that absence IS the bug — so a message
// saying it "is declared external" would send the reader looking for a
// declaration that is precisely what is missing.
assert.match(
failures[0],
/"undici" is a root runtime dependency, so it must stay external, but it was inlined/,
);
assert.doesNotMatch(failures[0], /is declared external but was inlined/);
});
test("uses the declared-external wording when the package IS in the list", () => {
const failures = evaluateClient({
name: "web",
build: "clients/web/build",
externals: ["undici"],
rootDependencies: ["undici"],
files: [
{
name: "index.js",
source: banner("../../node_modules/undici/index.js"),
},
],
});
assert.equal(failures.length, 1);
assert.match(failures[0], /"undici" is declared external but was inlined/);
});
test("does not flag a deliberately inlined package that is not a root dependency", () => {
// The TUI inlines `ink-form` / `ink-scroll-view` on purpose (#1952). They are
// declared in the TUI's own manifest, never at the root, which is exactly
// what keeps them out of the candidate set.
assert.deepEqual(
evaluateClient({
name: "tui",
build: "clients/tui/build",
externals: ["react", "ink"],
rootDependencies: ["react"],
files: [
{
name: "index.js",
source: banner("../../node_modules/ink-form/lib/Form.js"),
},
],
}),
[],
);
});
test("passes a clean bundle", () => {
assert.deepEqual(
evaluateClient({
name: "web",
build: "clients/web/build",
externals: ["undici"],
rootDependencies: ["undici"],
files: clean,
}),
[],
);
});
});
describe("BUNDLED_CLIENTS", () => {
test("is enrolled from what is actually on disk, not from itself", () => {
// Asserting the hard-coded list equals a hard-coded copy of itself proves
// nothing — adding `clients/foo` with a tsup build would leave it green
// while the new bundle went unchecked. So the expected set is DISCOVERED:
// a client is tsup-bundled iff it ships a tsup config. `clients/launcher`
// is plain tsc, emits no bundle, and is correctly absent.
const clientsDir = path.join(repoRoot, "clients");
const discovered = readdirSync(clientsDir)
.filter((name) =>
readdirSync(path.join(clientsDir, name)).some((f) =>
/^tsup(\..+)?\.config\.ts$/.test(f),
),
)
.sort();
assert.ok(discovered.length > 0, "no tsup-bundled clients discovered");
assert.deepEqual(
BUNDLED_CLIENTS.map((c) => c.name).sort(),
discovered,
"BUNDLED_CLIENTS is out of sync with the clients that ship a tsup config",
);
});
test("points each client at a config and build dir that exist", () => {
for (const client of BUNDLED_CLIENTS) {
assert.ok(
existsSync(path.join(repoRoot, client.config)),
`${client.name}: missing ${client.config}`,
);
// The build dir is produced by `npm run build`; only assert the path is
// under the client, since the guard itself reports a missing build.
assert.match(client.build, new RegExp(`^clients/${client.name}/`));
}
});
});