diff --git a/packages/adk/src/base/registry.ts b/packages/adk/src/base/registry.ts index a60e4c3c..e27e4c34 100644 --- a/packages/adk/src/base/registry.ts +++ b/packages/adk/src/base/registry.ts @@ -88,6 +88,23 @@ const adtToKind = new Map(); /** ADK kind to ADT main type mapping (reverse) */ const kindToAdt = new Map(); +/** + * Test-only: clear all registry state. + * + * The registry is module-level singleton state populated by side-effectful + * `registerObjectType()` calls in `src/objects/repository/**`. Tests that + * exercise registration need a clean slate, but production code must never + * call this — it would break every ADK object type resolution. + * + * Name intentionally uses the `__` prefix to signal "not part of the public + * API". Do not import this from runtime code. + */ +export function __resetRegistryForTests(): void { + registry.clear(); + adtToKind.clear(); + kindToAdt.clear(); +} + /** Options for registerObjectType */ export interface RegisterObjectTypeOptions { /** ADT REST endpoint path segment (e.g., 'oo/classes', 'ddic/tabletypes') */ diff --git a/packages/adk/tests/fetch-utils.test.ts b/packages/adk/tests/fetch-utils.test.ts new file mode 100644 index 00000000..eefddfe7 --- /dev/null +++ b/packages/adk/tests/fetch-utils.test.ts @@ -0,0 +1,56 @@ +/** + * ADK Fetch Utils Unit Tests + * + * Tests for fetch-utils.ts functions that handle fetch response normalization. + */ + +import { describe, it, expect } from 'vitest'; +import { toText } from '../src/base/fetch-utils'; + +describe('toText', () => { + it('should return string as-is', async () => { + const result = await toText('hello world'); + expect(result).toBe('hello world'); + }); + + it('should handle Response-like object with text method', async () => { + const mockResponse = { + text: () => Promise.resolve('response text'), + }; + const result = await toText(mockResponse); + expect(result).toBe('response text'); + }); + + it('should convert null to empty string', async () => { + expect(await toText(null)).toBe(''); + }); + + it('should convert undefined to empty string', async () => { + expect(await toText(undefined)).toBe(''); + }); + + it('should convert number to string', async () => { + expect(await toText(123)).toBe('123'); + }); + + it('should JSON-stringify plain objects', async () => { + const result = await toText({ key: 'value' }); + expect(result).toBe('{"key":"value"}'); + }); + + it('should JSON-stringify objects with non-function text property', async () => { + // `text` is not a function, so the Response-like branch is skipped and + // the value falls through to the JSON.stringify path. + const result = await toText({ text: 'not a function' }); + expect(result).toBe('{"text":"not a function"}'); + }); + + it('should fall back to String() when JSON.stringify throws', async () => { + // JSON.stringify throws on circular references — the catch branch in + // toText() must return the default string coercion instead of propagating. + const circular: Record = {}; + circular.self = circular; + const result = await toText(circular); + expect(result).toBe('[object Object]'); + }); +}); diff --git a/packages/adk/tests/registry.test.ts b/packages/adk/tests/registry.test.ts new file mode 100644 index 00000000..f357c19b --- /dev/null +++ b/packages/adk/tests/registry.test.ts @@ -0,0 +1,250 @@ +/** + * ADK Registry Unit Tests + * + * Tests for registry.ts functions that handle ADT type to ADK kind mapping + * and object type registration/resolution. + * + * NOTE: The registry is module-level singleton state (`registry`, `adtToKind`, + * `kindToAdt` Maps in `src/base/registry.ts`). Each test resets it via + * `__resetRegistryForTests()` so ordering and import side effects from other + * modules cannot leak in. Do not remove the `beforeEach` call below. + */ + +import { describe, it, expect, beforeEach } from 'vitest'; +import { + parseAdtType, + getMainType, + registerObjectType, + resolveType, + resolveKind, + getKindForType, + getTypeForKind, + isTypeRegistered, + getRegisteredTypes, + getRegisteredKinds, + getEndpointForType, + __resetRegistryForTests, + type AdkObjectConstructor, +} from '../src/base/registry'; +import * as kinds from '../src/base/kinds'; +import type { AdkKind } from '../src/base/kinds'; + +// Minimal stand-in for an AdkObject constructor; tests only care about +// identity round-tripping through the registry, not the object shape. +class MockAdkObject { + constructor( + public ctx: unknown, + public nameOrData: unknown, + ) {} +} + +// Single typed cast — the mock intentionally does not implement the full +// AdkObject contract, so we bridge through `unknown` once here rather than +// sprinkling `as any` at every call site. +const mockCtor = MockAdkObject as unknown as AdkObjectConstructor; + +beforeEach(() => { + __resetRegistryForTests(); +}); + +describe('parseAdtType', () => { + it('should parse full type with sub type', () => { + expect(parseAdtType('DEVC/K')).toEqual({ + full: 'DEVC/K', + main: 'DEVC', + sub: 'K', + }); + }); + + it('should parse main type without sub type', () => { + expect(parseAdtType('CLAS')).toEqual({ + full: 'CLAS', + main: 'CLAS', + sub: undefined, + }); + }); + + it('should handle lowercase input', () => { + expect(parseAdtType('tabl/ds')).toEqual({ + full: 'tabl/ds', + main: 'TABL', + sub: 'DS', + }); + }); + + it('should handle empty sub type', () => { + expect(parseAdtType('TABL/')).toEqual({ + full: 'TABL/', + main: 'TABL', + sub: '', + }); + }); +}); + +describe('getMainType', () => { + it('should return main type from full type', () => { + expect(getMainType('DEVC/K')).toBe('DEVC'); + }); + + it('should return type as-is for main type', () => { + expect(getMainType('CLAS')).toBe('CLAS'); + }); + + it('should handle lowercase input', () => { + expect(getMainType('prog')).toBe('PROG'); + }); +}); + +describe('registerObjectType', () => { + it('should register a type with endpoint and nameTransform', () => { + registerObjectType('PROG', kinds.Program, mockCtor, { + endpoint: 'abap/programs', + nameTransform: 'preserve', + }); + + const entry = resolveType('PROG'); + expect(entry).toBeDefined(); + expect(entry?.kind).toBe(kinds.Program); + expect(entry?.endpoint).toBe('abap/programs'); + expect(entry?.nameTransform).toBe('preserve'); + }); + + it('should register without optional parameters', () => { + registerObjectType('TEST', 'TestType' as AdkKind, mockCtor); + + const entry = resolveType('TEST'); + expect(entry).toBeDefined(); + expect(entry?.kind).toBe('TestType'); + }); + + it('should handle case-insensitive registration', () => { + registerObjectType('prog', kinds.Program, mockCtor); + + expect(resolveType('PROG')).toBeDefined(); + expect(resolveType('prog')).toBeDefined(); + }); +}); + +describe('resolveType', () => { + it('should resolve exact type match first', () => { + registerObjectType('MYTAB', kinds.Table, mockCtor, { + endpoint: 'ddic/tables', + }); + registerObjectType('MYTAB/DS', kinds.Structure as AdkKind, mockCtor, { + endpoint: 'ddic/structs', + }); + + expect(resolveType('MYTAB/DS')?.endpoint).toBe('ddic/structs'); + }); + + it('should fall back to main type if full type not found', () => { + registerObjectType('ANOTAB', kinds.Table, mockCtor, { + endpoint: 'ddic/tables', + }); + + expect(resolveType('ANOTAB/DS')?.endpoint).toBe('ddic/tables'); + }); + + it('should return undefined for unregistered type', () => { + expect(resolveType('UNREGISTERED')).toBeUndefined(); + }); +}); + +describe('getKindForType', () => { + it('should return kind for registered type', () => { + registerObjectType('CLAS', kinds.Class, mockCtor); + expect(getKindForType('CLAS')).toBe(kinds.Class); + }); + + it('should return kind for full type', () => { + registerObjectType('TABL', kinds.Table, mockCtor); + expect(getKindForType('TABL/DS')).toBe(kinds.Table); + }); + + it('should return undefined for unregistered type', () => { + expect(getKindForType('UNREG')).toBeUndefined(); + }); +}); + +describe('getTypeForKind', () => { + it('should return ADT type for registered kind', () => { + registerObjectType('CLAS', kinds.Class, mockCtor); + expect(getTypeForKind(kinds.Class)).toBe('CLAS'); + }); + + it('should return undefined for unregistered kind', () => { + expect(getTypeForKind('UnknownKind' as AdkKind)).toBeUndefined(); + }); +}); + +describe('isTypeRegistered', () => { + it('should return true for registered main type', () => { + registerObjectType('CLAS', kinds.Class, mockCtor); + expect(isTypeRegistered('CLAS')).toBe(true); + }); + + it('should return false for unregistered type', () => { + expect(isTypeRegistered('UNREG')).toBe(false); + }); + + it('should treat full types as registered when main type is registered', () => { + registerObjectType('TABL', kinds.Table, mockCtor); + expect(isTypeRegistered('TABL/DS')).toBe(true); + }); +}); + +describe('getRegisteredTypes', () => { + it('should return array of registered types', () => { + registerObjectType('TYPE1', 'Type1' as AdkKind, mockCtor); + registerObjectType('TYPE2', 'Type2' as AdkKind, mockCtor); + + const types = getRegisteredTypes(); + expect(types).toContain('TYPE1'); + expect(types).toContain('TYPE2'); + }); + + it('should return empty array when nothing registered', () => { + // Registry was cleared in beforeEach; no registrations have occurred in + // this test yet, so the list must be empty (not just array-shaped). + expect(getRegisteredTypes()).toEqual([]); + }); +}); + +describe('getRegisteredKinds', () => { + it('should return array of registered kinds', () => { + registerObjectType('TYPE1', 'Kind1' as AdkKind, mockCtor); + + const kindsList = getRegisteredKinds(); + expect(kindsList).toContain('Kind1'); + }); +}); + +describe('resolveKind', () => { + it('should resolve registered kind to entry', () => { + registerObjectType('CLAS', kinds.Class, mockCtor, { + endpoint: 'oo/classes', + }); + + const entry = resolveKind(kinds.Class); + expect(entry?.kind).toBe(kinds.Class); + expect(entry?.endpoint).toBe('oo/classes'); + }); + + it('should return undefined for unregistered kind', () => { + expect(resolveKind('UnknownKind' as AdkKind)).toBeUndefined(); + }); +}); + +describe('getEndpointForType', () => { + it('should return endpoint for registered type', () => { + registerObjectType('PROG', kinds.Program, mockCtor, { + endpoint: 'abap/programs', + }); + + expect(getEndpointForType('PROG')).toBe('abap/programs'); + }); + + it('should return undefined for unregistered type', () => { + expect(getEndpointForType('UNREG')).toBeUndefined(); + }); +});