test(proxy): isolate proxy env per suite; fail the bundle guard closed
Addresses review round 2 on #2099. - The /api/fetch proxy test set only uppercase HTTP_PROXY, so an ambient lowercase `http_proxy` (which undici's EnvHttpProxyAgent prefers) or a NO_PROXY could route the request elsewhere and fail the assertion. All six proxy vars are now saved and cleared for the whole /api/fetch suite and restored in afterAll — suite-level rather than per-test because the agent is memoized process-wide once built. Verified the test still passes under a deliberately hostile ambient env (`http_proxy=http://127.0.0.1:9 NO_PROXY='*'`). - `verify:bundle-externals` reported success when `parseExternals` returned nothing, so an ordinary tsup refactor — hoisting the array to a constant, or a line break after `external:` — would silently disable the guard. It now fails closed: every bundled client has externals, so "none parsed" means the parser lost track of the config. Verified against exactly that refactor. The per-client verdict moved into a pure `evaluateClient()` so all three failure modes are unit-tested, and the failure epilogue is now printed only for a real inlining, since "add the package to `external`" is the wrong instruction for the other two. 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>
This commit is contained in:
parent
9a084f49e5
commit
2f83549bc3
2 changed files with 163 additions and 28 deletions
|
|
@ -133,6 +133,40 @@ export function findInlinedExternals({ externals, files }) {
|
|||
return { noBanners: false, violations };
|
||||
}
|
||||
|
||||
/**
|
||||
* The per-client verdict, as pure data so every branch is unit-testable.
|
||||
*
|
||||
* Note it **fails closed on an empty `external` list**. `parseExternals` keys
|
||||
* off the literal text `external: [`, so an ordinary tsup refactor — hoisting
|
||||
* the array to a constant, or a line break after the colon — would make it
|
||||
* return nothing, and a guard that checks zero packages reports success. Every
|
||||
* enrolled bundle has externals today, so "none parsed" means the parser lost
|
||||
* track of the config, not that the invariant holds.
|
||||
*/
|
||||
export function evaluateClient({ name, build, externals, files }) {
|
||||
if (externals.length === 0) {
|
||||
return [
|
||||
`${name}: no \`external\` entries could be parsed from its tsup config. ` +
|
||||
`Every bundled client has some, so this means the config no longer matches ` +
|
||||
`what parseExternals() reads (it keys off the literal text "external: ["). ` +
|
||||
`Update the parser rather than leaving the guard checking nothing.`,
|
||||
];
|
||||
}
|
||||
|
||||
const { noBanners, violations } = findInlinedExternals({ externals, files });
|
||||
if (noBanners) {
|
||||
return [
|
||||
`${name}: ${build} has no esbuild module banners, so this guard cannot see ` +
|
||||
`what was inlined. Has minification been enabled for this bundle?`,
|
||||
];
|
||||
}
|
||||
|
||||
return violations.map(
|
||||
(v) =>
|
||||
`${name}: "${v.pkg}" is declared external but was inlined (${v.reason}).`,
|
||||
);
|
||||
}
|
||||
|
||||
function main() {
|
||||
const failures = [];
|
||||
for (const client of BUNDLED_CLIENTS) {
|
||||
|
|
@ -153,34 +187,31 @@ function main() {
|
|||
name,
|
||||
source: readFileSync(join(buildDir, name), "utf8"),
|
||||
}));
|
||||
const { noBanners, violations } = findInlinedExternals({
|
||||
externals,
|
||||
files,
|
||||
});
|
||||
if (noBanners) {
|
||||
failures.push(
|
||||
`${client.name}: ${client.build} has no esbuild module banners, so this guard ` +
|
||||
`cannot see what was inlined. Has minification been enabled for this bundle?`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
for (const v of violations) {
|
||||
failures.push(
|
||||
`${client.name}: "${v.pkg}" is declared external but was inlined (${v.reason}).`,
|
||||
);
|
||||
}
|
||||
failures.push(
|
||||
...evaluateClient({
|
||||
name: client.name,
|
||||
build: client.build,
|
||||
externals,
|
||||
files,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
if (failures.length > 0) {
|
||||
console.error("verify:bundle-externals FAILED\n");
|
||||
for (const f of failures) console.error(` - ${f}`);
|
||||
console.error(
|
||||
"\nAn externalized package must resolve from the consumer's install. If it was\n" +
|
||||
"inlined, esbuild rewrote its specifier to a relative chunk that no user-side\n" +
|
||||
"install can satisfy — and a CommonJS package inlined into an ESM bundle throws\n" +
|
||||
'`Dynamic require of "..." is not supported` on first use (#2067).\n' +
|
||||
"Add the package to that client's tsup `external` list.",
|
||||
);
|
||||
// Only for a real inlining — the other two failure modes (nothing parsed,
|
||||
// no banners) are about the guard losing sight of the bundle, and "add the
|
||||
// package to `external`" would be the wrong instruction for either.
|
||||
if (failures.some((f) => f.includes("was inlined"))) {
|
||||
console.error(
|
||||
"\nAn externalized package must resolve from the consumer's install. If it was\n" +
|
||||
"inlined, esbuild rewrote its specifier to a relative chunk that no user-side\n" +
|
||||
"install can satisfy — and a CommonJS package inlined into an ESM bundle throws\n" +
|
||||
'`Dynamic require of "..." is not supported` on first use (#2067).\n' +
|
||||
"Add the package to that client's tsup `external` list.",
|
||||
);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import assert from "node:assert/strict";
|
|||
|
||||
import {
|
||||
BUNDLED_CLIENTS,
|
||||
evaluateClient,
|
||||
parseExternals,
|
||||
findInlinedExternals,
|
||||
} from "./verify-bundle-externals.mjs";
|
||||
|
|
@ -152,6 +153,71 @@ describe("findInlinedExternals", () => {
|
|||
});
|
||||
});
|
||||
|
||||
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("passes a clean bundle", () => {
|
||||
assert.deepEqual(
|
||||
evaluateClient({
|
||||
name: "web",
|
||||
build: "clients/web/build",
|
||||
externals: ["undici"],
|
||||
files: clean,
|
||||
}),
|
||||
[],
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("BUNDLED_CLIENTS", () => {
|
||||
test("covers every tsup-bundled client", () => {
|
||||
// `clients/launcher` is plain tsc and emits no bundle, so it is deliberately
|
||||
|
|
|
|||
Loading…
Reference in a new issue