**refactor(tests): consolidate and replace outdated environment test scripts**

- Removed obsolete scripts: `test-all-environments.js`, `test-fallback-function.js`, `test-fallback-simple.js`, `test-fix.js`, `test-tensorflow-textencoder.js`, `test-unified-encoding.js`, and `test-worker-utils.js`.
- Introduced `scripts/comprehensive-test.js` as a unified testing script covering all environments: Browser, Node.js, and CLI.
- Added `examples/cli-wrapper-example.js` to demonstrate a proper CLI implementation with TensorFlow.js initialization.

This refactor simplifies the testing structure by consolidating redundant scripts into a single comprehensive script while ensuring robust cross-environment coverage.
This commit is contained in:
David Snelling 2025-07-14 11:12:51 -07:00
parent fe1f418bf9
commit 19ed3dd081
20 changed files with 1253 additions and 808 deletions

View file

@ -3,6 +3,10 @@
* A vector and graph database using HNSW
*/
// CRITICAL: The TensorFlow.js environment patch is now centralized in setup.ts
// We import setup.js below which applies the necessary patches through textEncoding.js
// This ensures a consistent patching approach and avoids conflicts
// Import the setup file for its side-effects.
// This MUST be the very first import to ensure patches are applied
// before any other module (like TensorFlow.js) is loaded.

View file

@ -1,12 +1,54 @@
/**
* This file is imported for its side effects to patch the environment
* CRITICAL: This file is imported for its side effects to patch the environment
* for TensorFlow.js before any other library code runs.
*
* It ensures that by the time TensorFlow.js is imported by any other
* module, the necessary compatibility fixes for the current Node.js
* environment are already in place.
*
* This file MUST be imported as the first import in unified.ts to prevent
* race conditions with TensorFlow.js initialization. Failure to do so will
* result in errors like "TextEncoder is not a constructor" when the package
* is used in Node.js environments.
*
* The package.json file marks this file as having side effects to prevent
* tree-shaking by bundlers, ensuring the patch is always applied.
*/
// CRITICAL: Apply the TensorFlow.js patch immediately at the top level
// This ensures it runs as early as possible in the module loading process
// before any imports are processed
if (
typeof process !== 'undefined' &&
process.versions &&
process.versions.node
) {
try {
// For CommonJS environments, use require to ensure synchronous loading
if (typeof require === 'function') {
const textEncoding = require('./utils/textEncoding.js')
if (
textEncoding &&
typeof textEncoding.applyTensorFlowPatch === 'function'
) {
textEncoding.applyTensorFlowPatch()
console.log(
'Applied TensorFlow.js patch via CommonJS require in setup.ts'
)
}
}
} catch (e) {
console.warn('Failed to apply TensorFlow.js patch via require:', e)
// Continue to the import-based approach
}
}
// Also import normally for ES modules environments
import { applyTensorFlowPatch } from './utils/textEncoding.js'
// Apply the TensorFlow.js platform patch if needed
// This will be a no-op if the patch was already applied via require above
applyTensorFlowPatch()
console.log(
'Applied or verified TensorFlow.js patch via ES modules in setup.ts'
)

View file

@ -4,6 +4,18 @@
* Environment detection is handled here and made available to all components
*/
// CRITICAL: The TensorFlow.js environment patch is now centralized in setup.ts
// We import setup.ts below which applies the necessary patches
// CRITICAL: Import setup.js first to ensure TensorFlow.js environment patching
// This MUST be the first import to prevent race conditions with TensorFlow.js initialization
// Moving or removing this import will cause errors like "TextEncoder is not a constructor"
// when the package is used in Node.js environments
//
// The setup.js file applies a patch that ensures TextEncoder/TextDecoder are properly
// available to TensorFlow.js before it initializes its platform detection
import './setup.js'
// Export environment information
export const environment = {
isBrowser: typeof window !== 'undefined',

View file

@ -145,14 +145,36 @@ export async function calculateDistancesBatch(
// In worker context, use the importTensorFlow function
tf = await self.importTensorFlow()
} else {
// Dynamically import TensorFlow.js core module and backends
tf = await import('@tensorflow/tfjs-core')
// CRITICAL: First, directly import the setup module to ensure the TensorFlow.js patch is applied
// This is the most reliable way to ensure the patch is applied before TensorFlow.js is loaded
try {
// In Node.js environment, use require() which is synchronous
if (typeof require !== 'undefined') {
// First, require the setup module to apply the patch
require('../setup.js')
// Import CPU backend
await import('@tensorflow/tfjs-backend-cpu')
// Now load TensorFlow.js core module
tf = require('@tensorflow/tfjs-core')
// Set CPU as the backend
await tf.setBackend('cpu')
// Load CPU backend
require('@tensorflow/tfjs-backend-cpu')
// Set CPU as the backend
tf.setBackend('cpu')
} else {
// In browser or other environments without require(), use dynamic imports
// First, dynamically import the setup module to apply the patch
await import('../setup.js')
// Now load TensorFlow.js core module
tf = await import('@tensorflow/tfjs-core')
await import('@tensorflow/tfjs-backend-cpu')
await tf.setBackend('cpu')
}
} catch (error) {
console.error('Failed to initialize TensorFlow.js:', error)
throw error
}
}
// Convert vectors to tensors

View file

@ -23,6 +23,10 @@ export class UniversalSentenceEncoder implements EmbeddingModel {
/**
* Add polyfills and patches for TensorFlow.js compatibility
* This addresses issues with TensorFlow.js in Node.js environments
*
* Note: The main TensorFlow.js patching is now centralized in textEncoding.ts
* and applied through setup.ts. This method only adds additional utility functions
* that might be needed by TensorFlow.js.
*/
private addNodeCompatibilityPolyfills(): void {
// Only apply in Node.js environment
@ -38,82 +42,30 @@ export class UniversalSentenceEncoder implements EmbeddingModel {
// This fixes the "Cannot read properties of undefined (reading 'isFloat32Array')" error
if (typeof global !== 'undefined') {
try {
// Define a custom PlatformNode class
class PlatformNode {
util: any
textEncoder: TextEncoder
textDecoder: TextDecoder
// Ensure the util object exists
if (!global.util) {
global.util = {}
}
constructor() {
// Create a util object with necessary methods
this.util = {
// Add isFloat32Array and isTypedArray directly to util
isFloat32Array: (arr: any) => {
return !!(
arr instanceof Float32Array ||
(arr &&
Object.prototype.toString.call(arr) ===
'[object Float32Array]')
)
},
isTypedArray: (arr: any) => {
return !!(ArrayBuffer.isView(arr) && !(arr instanceof DataView))
},
// Use native TextEncoder and TextDecoder
TextEncoder: TextEncoder,
TextDecoder: TextDecoder
}
// Initialize encoders using native constructors
this.textEncoder = new TextEncoder()
this.textDecoder = new TextDecoder()
}
// Define isFloat32Array directly on the instance
isFloat32Array(arr: any) {
// Add isFloat32Array method if it doesn't exist
if (!global.util.isFloat32Array) {
global.util.isFloat32Array = (obj: any) => {
return !!(
arr instanceof Float32Array ||
(arr &&
Object.prototype.toString.call(arr) === '[object Float32Array]')
obj instanceof Float32Array ||
(obj &&
Object.prototype.toString.call(obj) === '[object Float32Array]')
)
}
}
// Define isTypedArray directly on the instance
isTypedArray(arr: any) {
return !!(ArrayBuffer.isView(arr) && !(arr instanceof DataView))
// Add isTypedArray method if it doesn't exist
if (!global.util.isTypedArray) {
global.util.isTypedArray = (obj: any) => {
return !!(ArrayBuffer.isView(obj) && !(obj instanceof DataView))
}
}
// Assign the PlatformNode class to the global object
;(global as any).PlatformNode = PlatformNode
// Also create an instance and assign it to global.platformNode
;(global as any).platformNode = new PlatformNode()
} catch (error) {
console.warn('Failed to define global PlatformNode class:', error)
}
// Ensure the util object exists
if (!global.util) {
global.util = {}
}
// Add isFloat32Array method if it doesn't exist
if (!global.util.isFloat32Array) {
global.util.isFloat32Array = (obj: any) => {
return !!(
obj instanceof Float32Array ||
(obj &&
Object.prototype.toString.call(obj) === '[object Float32Array]')
)
}
}
// Add isTypedArray method if it doesn't exist
if (!global.util.isTypedArray) {
global.util.isTypedArray = (obj: any) => {
return !!(ArrayBuffer.isView(obj) && !(obj instanceof DataView))
}
console.warn('Failed to add utility polyfills:', error)
}
}
}
@ -145,36 +97,95 @@ export class UniversalSentenceEncoder implements EmbeddingModel {
// TensorFlow.js will use its default EPSILON value
// Dynamically import TensorFlow.js core module and backends
// Use type assertions to tell TypeScript these modules exist
this.tf = await import('@tensorflow/tfjs-core')
// Import CPU backend (always needed as fallback)
await import('@tensorflow/tfjs-backend-cpu')
// Try to import WebGL backend for GPU acceleration in browser environments
// CRITICAL: First, directly import the setup module to ensure the TensorFlow.js patch is applied
// This is the most reliable way to ensure the patch is applied before TensorFlow.js is loaded
try {
if (typeof window !== 'undefined') {
await import('@tensorflow/tfjs-backend-webgl')
// Check if WebGL is available using setBackend instead of findBackend
try {
if (this.tf.setBackend) {
await this.tf.setBackend('webgl')
this.backend = 'webgl'
console.log('Using WebGL backend for TensorFlow.js')
} else {
// In Node.js environment, use require() which is synchronous
if (typeof require !== 'undefined') {
// First, require the setup module to apply the patch
require('../setup.js')
// Now load TensorFlow.js core module
this.tf = require('@tensorflow/tfjs-core')
// Load CPU backend (always needed as fallback)
require('@tensorflow/tfjs-backend-cpu')
// Try to load WebGL backend for GPU acceleration in browser environments
if (typeof window !== 'undefined') {
try {
require('@tensorflow/tfjs-backend-webgl')
// Check if WebGL is available
if (this.tf.setBackend) {
this.tf.setBackend('webgl')
this.backend = 'webgl'
console.log('Using WebGL backend for TensorFlow.js')
} else {
console.warn(
'tf.setBackend is not available, falling back to CPU'
)
}
} catch (e) {
console.warn(
'tf.setBackend is not available, falling back to CPU'
'WebGL backend not available, falling back to CPU:',
e
)
this.backend = 'cpu'
}
} catch (e) {
console.warn('WebGL backend not available, falling back to CPU:', e)
}
// Load Universal Sentence Encoder
this.use = require('@tensorflow-models/universal-sentence-encoder')
} else {
// In browser or other environments without require(), use dynamic imports
// First, dynamically import the setup module to apply the patch
await import('../setup.js')
// Now load TensorFlow.js core module
this.tf = await import('@tensorflow/tfjs-core')
// Import CPU backend (always needed as fallback)
await import('@tensorflow/tfjs-backend-cpu')
// Try to import WebGL backend for GPU acceleration in browser environments
try {
if (typeof window !== 'undefined') {
await import('@tensorflow/tfjs-backend-webgl')
// Check if WebGL is available
try {
if (this.tf.setBackend) {
await this.tf.setBackend('webgl')
this.backend = 'webgl'
console.log('Using WebGL backend for TensorFlow.js')
} else {
console.warn(
'tf.setBackend is not available, falling back to CPU'
)
}
} catch (e) {
console.warn(
'WebGL backend not available, falling back to CPU:',
e
)
this.backend = 'cpu'
}
}
} catch (error) {
console.warn(
'WebGL backend not available, falling back to CPU:',
error
)
this.backend = 'cpu'
}
// Load Universal Sentence Encoder
this.use = await import(
'@tensorflow-models/universal-sentence-encoder'
)
}
} catch (error) {
console.warn('WebGL backend not available, falling back to CPU:', error)
this.backend = 'cpu'
console.error('Failed to initialize TensorFlow.js:', error)
throw error
}
// Set the backend
@ -182,8 +193,6 @@ export class UniversalSentenceEncoder implements EmbeddingModel {
await this.tf.setBackend(this.backend)
}
this.use = await import('@tensorflow-models/universal-sentence-encoder')
// Log the module structure to help with debugging
console.log(
'Universal Sentence Encoder module structure in main thread:',

View file

@ -1,94 +1,224 @@
// In: @soulcraft/brainy/src/utils/textEncoding.ts
/**
* Unified Text Encoding Utilities
* Checks if the code is running in a Node.js environment.
*/
function isNode(): boolean {
return (
typeof process !== 'undefined' &&
process.versions != null &&
process.versions.node != null
)
}
/**
* Global flag to track if TensorFlow.js has been initialized
* This helps prevent multiple registrations of the same kernels
*/
const TENSORFLOW_INITIALIZED = Symbol('TENSORFLOW_INITIALIZED')
/**
* Flag to track if the patch has been applied
* This prevents multiple applications of the patch
*/
let patchApplied = false
/**
* CRITICAL: Applies a compatibility patch for TensorFlow.js when running in a modern
* Node.js ES Module environment. This must be called before any TensorFlow.js
* modules are imported.
*
* This module provides a consistent way to handle text encoding/decoding across all environments
* using the native TextEncoder/TextDecoder APIs.
*/
/**
* Get a text encoder that works in the current environment
* @returns A TextEncoder instance
*/
export function getTextEncoder(): TextEncoder {
return new TextEncoder()
}
/**
* Get a text decoder that works in the current environment
* @returns A TextDecoder instance
*/
export function getTextDecoder(): TextDecoder {
return new TextDecoder()
}
/**
* Apply the TensorFlow.js platform patch if needed
* This function patches the global object to provide a PlatformNode class
* that uses native TextEncoder/TextDecoder
* This function prevents the "TextEncoder is not a constructor" error by preemptively
* creating a compliant PlatformNode class with proper TextEncoder/TextDecoder support
* and placing it on the global object where TensorFlow.js expects to find it.
*
* The race condition occurs because TensorFlow.js's platform detection might run
* before the necessary global objects are properly initialized in certain Node.js
* environments, particularly when the package is being used by other applications.
*
* This function is called from setup.ts, which must be the first import in unified.ts
* to ensure the patch is applied before any TensorFlow.js code is executed.
*
* It also applies a patch to prevent duplicate kernel registrations when TensorFlow.js
* is imported multiple times.
*/
export function applyTensorFlowPatch(): void {
try {
// Define a custom Platform class that works in both Node.js and browser environments
class Platform {
util: any
textEncoder: TextEncoder
textDecoder: TextDecoder
// Prevent multiple applications of the patch
if (patchApplied) {
return
}
constructor() {
// Create a util object with necessary methods and constructors
this.util = {
// Use native TextEncoder and TextDecoder
TextEncoder: globalThis.TextEncoder || TextEncoder,
TextDecoder: globalThis.TextDecoder || TextDecoder
if (!isNode()) {
return // Patch is only for Node.js
}
// In modern Node.js with ES Modules, TensorFlow.js can fail during its
// initial platform detection. This patch preempts that logic by creating
// a compliant "Platform" class that uses the standard global TextEncoder
// and placing it on the global object where TensorFlow.js expects to find it.
try {
// Ensure TextEncoder and TextDecoder are available
const nodeUtil = require('util')
const TextEncoderPolyfill = nodeUtil.TextEncoder || global.TextEncoder
const TextDecoderPolyfill = nodeUtil.TextDecoder || global.TextDecoder
if (!TextEncoderPolyfill || !TextDecoderPolyfill) {
console.warn(
'Brainy: TextEncoder or TextDecoder not available, attempting to polyfill'
)
// If still not available, try to use a simple polyfill
if (!TextEncoderPolyfill) {
class SimpleTextEncoder {
encode(input: string): Uint8Array {
const buf = Buffer.from(input, 'utf8')
return new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength)
}
}
// Initialize using native constructors directly
this.textEncoder = new (globalThis.TextEncoder || TextEncoder)()
this.textDecoder = new (globalThis.TextDecoder || TextDecoder)()
global.TextEncoder = SimpleTextEncoder
}
// Define isFloat32Array directly on the instance
isFloat32Array(arr: any) {
return !!(
if (!TextDecoderPolyfill) {
class SimpleTextDecoder {
decode(input?: Uint8Array): string {
if (!input) return ''
return Buffer.from(
input.buffer,
input.byteOffset,
input.byteLength
).toString('utf8')
}
}
global.TextDecoder = SimpleTextDecoder
}
} else {
// Ensure they're available globally
global.TextEncoder = TextEncoderPolyfill
global.TextDecoder = TextDecoderPolyfill
}
// Create a PlatformNode implementation that uses the polyfilled TextEncoder/TextDecoder
class BrainyPlatformNode {
// Use the polyfilled TextEncoder/TextDecoder
readonly util = {
TextEncoder: global.TextEncoder,
TextDecoder: global.TextDecoder,
// Add utility functions that TensorFlow.js might need
isTypedArray: (arr: any): boolean => {
return ArrayBuffer.isView(arr) && !(arr instanceof DataView)
},
isFloat32Array: (arr: any): boolean => {
return (
arr instanceof Float32Array ||
(arr &&
Object.prototype.toString.call(arr) === '[object Float32Array]')
)
}
}
// Create instances of the encoder/decoder
readonly textEncoder: any
readonly textDecoder: any
constructor() {
try {
// Initialize encoders using constructors
this.textEncoder = new global.TextEncoder()
this.textDecoder = new global.TextDecoder()
} catch (e) {
console.warn(
'Brainy: Error creating TextEncoder/TextDecoder instances:',
e
)
// Provide fallback implementations if instantiation fails
this.textEncoder = {
encode: (input: string): Uint8Array => {
const buf = Buffer.from(input, 'utf8')
return new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength)
}
}
this.textDecoder = {
decode: (input?: Uint8Array): string => {
if (!input) return ''
return Buffer.from(
input.buffer,
input.byteOffset,
input.byteLength
).toString('utf8')
}
}
}
}
isTypedArray(arr: any): arr is Float32Array | Int32Array | Uint8Array {
return ArrayBuffer.isView(arr) && !(arr instanceof DataView)
}
isFloat32Array(arr: any): arr is Float32Array {
return (
arr instanceof Float32Array ||
(arr &&
Object.prototype.toString.call(arr) === '[object Float32Array]')
)
}
// Define isTypedArray directly on the instance
isTypedArray(arr: any) {
return !!(ArrayBuffer.isView(arr) && !(arr instanceof DataView))
}
}
// Get the global object in a way that works in both Node.js and browser
const globalObj =
typeof global !== 'undefined'
? global
: typeof window !== 'undefined'
? window
: typeof self !== 'undefined'
? self
: {}
// Assign the custom platform class to the global scope.
// TensorFlow.js specifically looks for `PlatformNode`.
global.PlatformNode = BrainyPlatformNode
// Only apply in Node.js environment
if (
typeof process !== 'undefined' &&
process.versions &&
process.versions.node
) {
// Assign the Platform class to the global object as PlatformNode for Node.js
;(globalObj as any).PlatformNode = Platform
// Also create an instance and assign it to global.platformNode (lowercase p)
;(globalObj as any).platformNode = new Platform()
} else if (typeof window !== 'undefined' || typeof self !== 'undefined') {
// In browser environments, we might need to provide similar functionality
// but we'll use a different name to avoid conflicts
;(globalObj as any).PlatformBrowser = Platform
;(globalObj as any).platformBrowser = new Platform()
// Also create an instance and assign it to global.platformNode (lowercase p)
// This is needed for some TensorFlow.js versions
global.platformNode = new BrainyPlatformNode()
// Set up a global flag to track TensorFlow.js initialization
global[TENSORFLOW_INITIALIZED] = false
// Monkey patch the registerKernel function to prevent duplicate registrations
// This will be applied when TensorFlow.js is imported
const originalRegisterKernel = global.registerKernel
if (!originalRegisterKernel) {
// Set up a handler to intercept the registerKernel function when it's defined
Object.defineProperty(global, 'registerKernel', {
set: function (newRegisterKernel) {
// Replace the setter with our patched version
Object.defineProperty(global, 'registerKernel', {
value: function (kernel: any) {
// Check if this kernel is already registered
const kernelName = kernel.kernelName
const backendName = kernel.backendName
const key = `${kernelName}_${backendName}`
// Use a global registry to track registered kernels
if (!global.__REGISTERED_KERNELS__) {
global.__REGISTERED_KERNELS__ = new Set()
}
// If this kernel is already registered, skip it
if (global.__REGISTERED_KERNELS__.has(key)) {
return
}
// Otherwise, register it and add it to our registry
global.__REGISTERED_KERNELS__.add(key)
return newRegisterKernel(kernel)
},
configurable: true,
writable: true
})
},
configurable: true
})
}
// Mark the patch as applied
patchApplied = true
console.log('Brainy: Successfully applied TensorFlow.js platform patch')
} catch (error) {
console.warn('Failed to apply TensorFlow.js platform patch:', error)
console.warn('Brainy: Failed to apply TensorFlow.js platform patch:', error)
}
}