diff --git a/.changeset/lemon-falcons-hunt.md b/.changeset/lemon-falcons-hunt.md new file mode 100644 index 00000000..3473dabf --- /dev/null +++ b/.changeset/lemon-falcons-hunt.md @@ -0,0 +1,5 @@ +--- +"@openai/agents-core": patch +--- + +fix(randomUUID): add fallback when crypto.randomUUID is unavailable diff --git a/packages/agents-core/src/shims/shims-browser.ts b/packages/agents-core/src/shims/shims-browser.ts index 231e8c8e..55e5397f 100644 --- a/packages/agents-core/src/shims/shims-browser.ts +++ b/packages/agents-core/src/shims/shims-browser.ts @@ -82,7 +82,20 @@ export class BrowserEventEmitter< export { BrowserEventEmitter as RuntimeEventEmitter }; -export const randomUUID = crypto.randomUUID.bind(crypto); +export const randomUUID: () => `${string}-${string}-${string}-${string}-${string}` = + () => { + if (typeof crypto !== 'undefined' && crypto.randomUUID) { + return crypto.randomUUID(); + } + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace( + /[xy]/g, + function (c) { + const r = (Math.random() * 16) | 0; + const v = c === 'x' ? r : (r & 0x3) | 0x8; + return v.toString(16); + }, + ) as `${string}-${string}-${string}-${string}-${string}`; + }; export const Readable = class Readable { constructor() {} pipeTo( diff --git a/packages/agents-core/test/shims/browser-event-emitter.test.ts b/packages/agents-core/test/shims/browser-event-emitter.test.ts deleted file mode 100644 index a596366a..00000000 --- a/packages/agents-core/test/shims/browser-event-emitter.test.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { describe, expect, test } from 'vitest'; - -import { BrowserEventEmitter } from '../../src/shims/shims-browser'; - -describe('BrowserEventEmitter', () => { - test('off removes previously registered listener', () => { - const emitter = new BrowserEventEmitter<{ foo: [string] }>(); - const calls: string[] = []; - - const handler = (value: string) => { - calls.push(value); - }; - - emitter.on('foo', handler); - emitter.emit('foo', 'first'); - emitter.off('foo', handler); - emitter.emit('foo', 'second'); - - expect(calls).toEqual(['first']); - }); - - test('once triggers listener only once', () => { - const emitter = new BrowserEventEmitter<{ foo: [string] }>(); - let callCount = 0; - - emitter.once('foo', () => { - callCount += 1; - }); - - emitter.emit('foo', 'first'); - emitter.emit('foo', 'second'); - - expect(callCount).toBe(1); - }); - - test('multiple identical listeners fire for each registration and are removed by off', () => { - const emitter = new BrowserEventEmitter<{ foo: [string] }>(); - const calls: string[] = []; - - const handler = (value: string) => { - calls.push(value); - }; - - emitter.on('foo', handler); - emitter.on('foo', handler); - - emitter.emit('foo', 'first'); - expect(calls).toEqual(['first', 'first']); - - emitter.off('foo', handler); - emitter.emit('foo', 'second'); - - expect(calls).toEqual(['first', 'first']); - }); -}); diff --git a/packages/agents-core/test/shims/browser-shims.test.ts b/packages/agents-core/test/shims/browser-shims.test.ts new file mode 100644 index 00000000..6fdb6979 --- /dev/null +++ b/packages/agents-core/test/shims/browser-shims.test.ts @@ -0,0 +1,120 @@ +import { describe, expect, test, vi } from 'vitest'; + +import { BrowserEventEmitter, randomUUID } from '../../src/shims/shims-browser'; + +describe('BrowserEventEmitter', () => { + test('off removes previously registered listener', () => { + const emitter = new BrowserEventEmitter<{ foo: [string] }>(); + const calls: string[] = []; + + const handler = (value: string) => { + calls.push(value); + }; + + emitter.on('foo', handler); + emitter.emit('foo', 'first'); + emitter.off('foo', handler); + emitter.emit('foo', 'second'); + + expect(calls).toEqual(['first']); + }); + + test('once triggers listener only once', () => { + const emitter = new BrowserEventEmitter<{ foo: [string] }>(); + let callCount = 0; + + emitter.once('foo', () => { + callCount += 1; + }); + + emitter.emit('foo', 'first'); + emitter.emit('foo', 'second'); + + expect(callCount).toBe(1); + }); + + test('multiple identical listeners fire for each registration and are removed by off', () => { + const emitter = new BrowserEventEmitter<{ foo: [string] }>(); + const calls: string[] = []; + + const handler = (value: string) => { + calls.push(value); + }; + + emitter.on('foo', handler); + emitter.on('foo', handler); + + emitter.emit('foo', 'first'); + expect(calls).toEqual(['first', 'first']); + + emitter.off('foo', handler); + emitter.emit('foo', 'second'); + + expect(calls).toEqual(['first', 'first']); + }); +}); + +describe('randomUUID', () => { + test('uses native crypto.randomUUID when available', () => { + const mockUUID = '12345678-1234-1234-1234-123456789abc'; + const originalCrypto = global.crypto; + + Object.defineProperty(global, 'crypto', { + value: { randomUUID: vi.fn(() => mockUUID) }, + configurable: true, + }); + + const result = randomUUID(); + expect(result).toBe(mockUUID); + expect(global.crypto.randomUUID).toHaveBeenCalled(); + + Object.defineProperty(global, 'crypto', { + value: originalCrypto, + configurable: true, + }); + }); + + test('uses fallback when crypto.randomUUID is unavailable', () => { + const originalCrypto = global.crypto; + + Object.defineProperty(global, 'crypto', { + value: undefined, + configurable: true, + }); + + const result = randomUUID(); + expect(result).toMatch( + /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/, + ); + + Object.defineProperty(global, 'crypto', { + value: originalCrypto, + configurable: true, + }); + }); + + test('fallback generates valid UUID v4 format', () => { + const originalCrypto = global.crypto; + + Object.defineProperty(global, 'crypto', { + value: undefined, + configurable: true, + }); + + const uuids = Array.from({ length: 10 }, () => randomUUID()); + + uuids.forEach((uuid) => { + expect(uuid).toMatch( + /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/, + ); + }); + + const uniqueUUIDs = new Set(uuids); + expect(uniqueUUIDs.size).toBe(uuids.length); + + Object.defineProperty(global, 'crypto', { + value: originalCrypto, + configurable: true, + }); + }); +});