Skip to content

Commit cee1e14

Browse files
committed
test(sea): decouple loader tests from the runner's Node version
The unit-test matrix spans Node 14–20, but SeaNativeLoader's version gate read the live `process.version` before the injected `load` seam ran. On the 14 and 16 runners every "successful load" / "load-failure hints" / "shape check" test hit `requires Node >=18` instead of the path it asserted (7 failures per job). Make Node-major detection a second injectable ctor seam (default = live process.version), inject a supported major in the load-path tests, and inject the version-under-test in the gate's own tests (no more mutating process.version). Tests now pass identically on every matrix Node. Co-authored-by: Isaac Signed-off-by: Madhavendra Rathore <madhavendra.rathore@databricks.com>
1 parent d6101aa commit cee1e14

2 files changed

Lines changed: 35 additions & 32 deletions

File tree

lib/sea/SeaNativeLoader.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -131,10 +131,20 @@ export class SeaNativeLoader {
131131

132132
private cachedError: Error | undefined;
133133

134-
constructor(private readonly load: () => SeaNativeBinding = defaultRequire) {}
134+
/**
135+
* @param load injectable module-require seam (stub a binding in tests)
136+
* @param nodeMajor injectable Node-major detector. Defaults to reading the
137+
* live `process.version`; injected in unit tests so the
138+
* load/shape branches are exercised independently of the
139+
* runner's actual Node version (the matrix spans 14–20).
140+
*/
141+
constructor(
142+
private readonly load: () => SeaNativeBinding = defaultRequire,
143+
private readonly nodeMajor: () => number = detectNodeMajor,
144+
) {}
135145

136146
private tryLoad(): SeaNativeBinding | undefined {
137-
const nodeMajor = detectNodeMajor();
147+
const nodeMajor = this.nodeMajor();
138148
// Fail closed: if we cannot determine the Node major (NaN) or it is
139149
// below the floor, refuse the load and fall back to Thrift.
140150
if (!Number.isFinite(nodeMajor) || nodeMajor < MIN_NODE_MAJOR) {

tests/unit/sea/loader.test.ts

Lines changed: 23 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,13 @@ import { SeaNativeLoader, SeaNativeBinding } from '../../../lib/sea/SeaNativeLoa
1717

1818
// Pure-logic tests for SeaNativeLoader. These exercise the load-failure
1919
// hint branches, the Node-version gate, the shape check, and caching via
20-
// the injectable `load` seam — so they run everywhere regardless of
21-
// whether a real `.node` is installed on the test machine.
20+
// the injectable `load` and `nodeMajor` seams — so they run everywhere
21+
// regardless of whether a real `.node` is installed on the test machine
22+
// OR which Node version the runner happens to be (the CI matrix spans
23+
// 14–20, below and above the >=18 floor). Tests that exercise the load
24+
// path inject a supported Node major so the version gate never short-
25+
// circuits them; the gate's own tests inject the version under test.
26+
const SUPPORTED_NODE_MAJOR = () => 18;
2227

2328
function stubBinding(overrides: Partial<Record<keyof SeaNativeBinding, unknown>> = {}): SeaNativeBinding {
2429
return {
@@ -52,7 +57,7 @@ describe('SeaNativeLoader', () => {
5257
describe('successful load', () => {
5358
it('get() returns the binding from the injected loader', () => {
5459
const binding = stubBinding();
55-
const loader = new SeaNativeLoader(() => binding);
60+
const loader = new SeaNativeLoader(() => binding, SUPPORTED_NODE_MAJOR);
5661
expect(loader.get()).to.equal(binding);
5762
expect(loader.tryGet()).to.equal(binding);
5863
});
@@ -63,7 +68,7 @@ describe('SeaNativeLoader', () => {
6368
const loader = new SeaNativeLoader(() => {
6469
calls += 1;
6570
return binding;
66-
});
71+
}, SUPPORTED_NODE_MAJOR);
6772
loader.get();
6873
loader.tryGet();
6974
loader.get();
@@ -75,7 +80,7 @@ describe('SeaNativeLoader', () => {
7580
it('MODULE_NOT_FOUND → "not installed" hint pointing at the README', () => {
7681
const loader = new SeaNativeLoader(() => {
7782
throw errWithCode('MODULE_NOT_FOUND', "Cannot find module '../../native/sea'");
78-
});
83+
}, SUPPORTED_NODE_MAJOR);
7984
expect(loader.tryGet()).to.equal(undefined);
8085
const msg = thrownMessage(() => loader.get());
8186
expect(msg).to.match(/not installed/);
@@ -85,7 +90,7 @@ describe('SeaNativeLoader', () => {
8590
it('ERR_DLOPEN_FAILED → includes the underlying dlerror string and remediation', () => {
8691
const loader = new SeaNativeLoader(() => {
8792
throw errWithCode('ERR_DLOPEN_FAILED', 'GLIBC_2.32 not found');
88-
});
93+
}, SUPPORTED_NODE_MAJOR);
8994
const msg = thrownMessage(() => loader.get());
9095
expect(msg).to.match(/GLIBC_2\.32 not found/);
9196
expect(msg).to.match(/musl/);
@@ -95,22 +100,22 @@ describe('SeaNativeLoader', () => {
95100
it('a generic Error (no code) preserves its message', () => {
96101
const loader = new SeaNativeLoader(() => {
97102
throw new Error('totally unexpected');
98-
});
103+
}, SUPPORTED_NODE_MAJOR);
99104
expect(() => loader.get()).to.throw(/totally unexpected/);
100105
});
101106

102107
it('a non-Error throw is wrapped', () => {
103108
const loader = new SeaNativeLoader(() => {
104109
// eslint-disable-next-line no-throw-literal
105110
throw 'a string';
106-
});
111+
}, SUPPORTED_NODE_MAJOR);
107112
expect(() => loader.get()).to.throw(/non-standard error/);
108113
});
109114
});
110115

111116
describe('shape check', () => {
112117
it('rejects a binding missing an expected export', () => {
113-
const loader = new SeaNativeLoader(() => stubBinding({ openSession: undefined }));
118+
const loader = new SeaNativeLoader(() => stubBinding({ openSession: undefined }), SUPPORTED_NODE_MAJOR);
114119
expect(loader.tryGet()).to.equal(undefined);
115120
const msg = thrownMessage(() => loader.get());
116121
expect(msg).to.match(/missing expected export/);
@@ -120,30 +125,18 @@ describe('SeaNativeLoader', () => {
120125

121126
describe('Node-version gate', () => {
122127
it('fails closed on a Node version below the floor', () => {
123-
const original = process.version;
124-
try {
125-
Object.defineProperty(process, 'version', { value: 'v16.20.0', configurable: true });
126-
let loadCalled = false;
127-
const loader = new SeaNativeLoader(() => {
128-
loadCalled = true;
129-
return stubBinding();
130-
});
131-
expect(() => loader.get()).to.throw(/requires Node >=18/);
132-
expect(loadCalled, 'load() must not be attempted on an unsupported Node').to.equal(false);
133-
} finally {
134-
Object.defineProperty(process, 'version', { value: original, configurable: true });
135-
}
128+
let loadCalled = false;
129+
const loader = new SeaNativeLoader(() => {
130+
loadCalled = true;
131+
return stubBinding();
132+
}, () => 16);
133+
expect(() => loader.get()).to.throw(/requires Node >=18/);
134+
expect(loadCalled, 'load() must not be attempted on an unsupported Node').to.equal(false);
136135
});
137136

138137
it('fails closed when the Node version is unparseable (NaN)', () => {
139-
const original = process.version;
140-
try {
141-
Object.defineProperty(process, 'version', { value: 'vNOT-A-VERSION', configurable: true });
142-
const loader = new SeaNativeLoader(() => stubBinding());
143-
expect(() => loader.get()).to.throw(/requires Node >=18/);
144-
} finally {
145-
Object.defineProperty(process, 'version', { value: original, configurable: true });
146-
}
138+
const loader = new SeaNativeLoader(() => stubBinding(), () => NaN);
139+
expect(() => loader.get()).to.throw(/requires Node >=18/);
147140
});
148141
});
149142
});

0 commit comments

Comments
 (0)