/** * Smoke tests for the BUILT bundles in dist/. * * Every other suite runs against src/ through ts-jest, where the platform * modules resolve as ordinary relative paths. That masked a bug in which the * published package loaded fine but threw "Cannot find module './node'" as soon * as anything touched the platform layer — rollup flattens the modules, so the * runtime require had nothing to resolve. The client could not be constructed * at all from the npm package. * * These tests exercise the artefact that actually ships. Run `npm run build` * first; they skip themselves if dist/ is absent. */ import * as fs from 'fs'; import * as path from 'path'; import { spawnSync } from 'child_process'; const DIST = path.join(__dirname, '..', '..', 'dist'); const NODE_CJS = path.join(DIST, 'node', 'index.js'); const BROWSER_CJS = path.join(DIST, 'browser', 'index.cjs'); const ESM_ENTRY = path.join(DIST, 'esm', 'index.mjs'); const built = fs.existsSync(NODE_CJS); const describeIfBuilt = built ? describe : describe.skip; if (!built) { // eslint-disable-next-line no-console console.warn('dist/ not found — run `npm run build` to exercise the bundle smoke tests'); } interface NomyoModule { SecureChatCompletion: new (config?: object) => { dispose(): void }; SecureCompletionClient: new (config: object) => { dispose(): void }; getMemoryProtectionInfo: () => { method: string; canLock: boolean }; AttestationPolicy: new (options?: object) => { enforce: boolean }; SecurityError: new (message: string) => Error; } describeIfBuilt('built Node bundle', () => { // eslint-disable-next-line @typescript-eslint/no-var-requires const nomyo = require(NODE_CJS) as NomyoModule; test('exposes the public API', () => { expect(typeof nomyo.SecureChatCompletion).toBe('function'); expect(typeof nomyo.SecureCompletionClient).toBe('function'); expect(typeof nomyo.getMemoryProtectionInfo).toBe('function'); expect(typeof nomyo.AttestationPolicy).toBe('function'); }); test('constructs a client — the platform layer resolves', () => { // This is the exact call that threw "Cannot find module './node'" const client = new nomyo.SecureChatCompletion({}); expect(client).toBeTruthy(); client.dispose(); }); test('constructs the low-level client too', () => { const client = new nomyo.SecureCompletionClient({ routerUrl: 'https://router.test', keyRotationInterval: 0, }); expect(client).toBeTruthy(); client.dispose(); }); test('reaches the real secure-memory implementation', () => { const info = nomyo.getMemoryProtectionInfo(); expect(['mlock', 'zero-only', 'none']).toContain(info.method); expect(typeof info.canLock).toBe('boolean'); }); test('carries no unresolved runtime requires', () => { const source = fs.readFileSync(NODE_CJS, 'utf-8'); // './node' / './browser' only ever resolved pre-bundling expect(source).not.toMatch(/require\(['"]\.\/node['"]\)/); expect(source).not.toMatch(/require\(['"]\.\/browser['"]\)/); }); test('attestation policy survives bundling', () => { const policy = new nomyo.AttestationPolicy({ enforce: true }); expect(policy.enforce).toBe(true); }); }); describeIfBuilt('built browser bundle', () => { // eslint-disable-next-line @typescript-eslint/no-var-requires const nomyo = require(BROWSER_CJS) as NomyoModule; test('exposes the public API', () => { expect(typeof nomyo.SecureChatCompletion).toBe('function'); expect(typeof nomyo.getMemoryProtectionInfo).toBe('function'); }); test('constructs a client without a DOM', () => { const client = new nomyo.SecureChatCompletion({}); expect(client).toBeTruthy(); client.dispose(); }); test('reports browser memory protection', () => { const info = nomyo.getMemoryProtectionInfo(); expect(info.canLock).toBe(false); expect(info.method).toBe('zero-only'); }); test('carries no unresolved runtime requires', () => { const source = fs.readFileSync(BROWSER_CJS, 'utf-8'); expect(source).not.toMatch(/require\(['"]\.\/node['"]\)/); expect(source).not.toMatch(/require\(['"]\.\/browser['"]\)/); }); test('does not drag the Node platform layer into the browser build', () => { const source = fs.readFileSync(BROWSER_CJS, 'utf-8'); // The Node HTTP client and the optional native addon must not appear. // `fs`/`path` legitimately do: KeyManager has Node-only branches guarded // by `typeof window`, shared by both builds. for (const nodeOnly of ['https', 'http', 'url', 'nomyo-native', 'node-gyp-build']) { expect(source).not.toMatch( new RegExp(`require\\(['"]${nodeOnly}['"]\\)`) ); } }); }); describeIfBuilt('built ESM entry point', () => { test('is real ESM with an unambiguous extension', () => { // A .js ES module in a non-"type":"module" package is parsed as // CommonJS by Node and fails on older versions expect(fs.existsSync(ESM_ENTRY)).toBe(true); expect(fs.existsSync(path.join(DIST, 'esm', 'index.js'))).toBe(false); expect(fs.readFileSync(ESM_ENTRY, 'utf-8')).toMatch(/^export |[\n;]export /); }); test('shims require() for its lazily-loaded modules', () => { // The source loads crypto/fs/path/jose/the native addon via require(). // `require` is not defined in ES module scope, so the bundle must // provide it or those paths throw ReferenceError at runtime. const source = fs.readFileSync(ESM_ENTRY, 'utf-8'); if (/[^.\w]require\(/.test(source)) { expect(source).toMatch(/createRequire/); } }); test('is importable and usable by a real Node process', () => { // Not `await import(...)` — ts-jest compiles dynamic import down to // require() for its CommonJS target, which cannot load .mjs and would // test the compiler rather than the bundle. Spawn Node instead. const script = `import(${JSON.stringify(ESM_ENTRY)})` + '.then(m => {' + ' const c = new m.SecureChatCompletion({}); c.dispose();' // Touches the require()-dependent crypto path + ' if (typeof m.getMemoryProtectionInfo().method !== "string") process.exit(2);' + '})' + '.catch(e => { console.error(e.message); process.exit(3); })'; const result = spawnSync(process.execPath, ['--input-type=module', '-e', script], { encoding: 'utf-8', }); expect(result.stderr.trim()).toBe(''); expect(result.status).toBe(0); }); });