Skip to content

Commit fb0cf56

Browse files
authored
Merge pull request #609 from objectstack-ai/copilot/optimize-protocol-report-another-one
2 parents b9ac384 + 8d88103 commit fb0cf56

29 files changed

Lines changed: 2028 additions & 105 deletions

PROTOCOL_OPTIMIZATION_REPORT.md

Lines changed: 86 additions & 76 deletions
Large diffs are not rendered by default.

packages/spec/src/data/external-lookup.test.ts

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -662,3 +662,156 @@ describe('ExternalLookupSchema', () => {
662662
expect(() => ExternalLookupSchema.parse(salesforceLookup)).not.toThrow();
663663
});
664664
});
665+
666+
describe('External Lookup Retry Configuration', () => {
667+
const baseLookup = {
668+
fieldName: 'external_data',
669+
dataSource: {
670+
id: 'test-api',
671+
name: 'Test API',
672+
type: 'rest-api' as const,
673+
endpoint: 'https://api.example.com',
674+
authentication: { type: 'api-key' as const, config: { key: 'test' } },
675+
},
676+
query: { endpoint: '/data' },
677+
fieldMappings: [{ source: 'name', target: 'name' }],
678+
};
679+
680+
it('should accept lookup with retry configuration', () => {
681+
const lookup = {
682+
...baseLookup,
683+
retry: {
684+
maxRetries: 3,
685+
initialDelayMs: 1000,
686+
maxDelayMs: 30000,
687+
backoffMultiplier: 2,
688+
retryableStatusCodes: [429, 500, 502, 503, 504],
689+
},
690+
};
691+
692+
expect(() => ExternalLookupSchema.parse(lookup)).not.toThrow();
693+
});
694+
695+
it('should apply retry defaults', () => {
696+
const result = ExternalLookupSchema.parse({
697+
...baseLookup,
698+
retry: {},
699+
});
700+
701+
expect(result.retry?.maxRetries).toBe(3);
702+
expect(result.retry?.initialDelayMs).toBe(1000);
703+
expect(result.retry?.maxDelayMs).toBe(30000);
704+
expect(result.retry?.backoffMultiplier).toBe(2);
705+
expect(result.retry?.retryableStatusCodes).toEqual([429, 500, 502, 503, 504]);
706+
});
707+
708+
it('should accept custom retryable status codes', () => {
709+
const result = ExternalLookupSchema.parse({
710+
...baseLookup,
711+
retry: { retryableStatusCodes: [408, 429, 503] },
712+
});
713+
714+
expect(result.retry?.retryableStatusCodes).toEqual([408, 429, 503]);
715+
});
716+
717+
it('should reject negative maxRetries', () => {
718+
expect(() => ExternalLookupSchema.parse({
719+
...baseLookup,
720+
retry: { maxRetries: -1 },
721+
})).toThrow();
722+
});
723+
});
724+
725+
describe('External Lookup Transform Configuration', () => {
726+
const baseLookup = {
727+
fieldName: 'external_data',
728+
dataSource: {
729+
id: 'test-api',
730+
name: 'Test API',
731+
type: 'rest-api' as const,
732+
endpoint: 'https://api.example.com',
733+
authentication: { type: 'api-key' as const, config: { key: 'test' } },
734+
},
735+
query: { endpoint: '/data' },
736+
fieldMappings: [{ source: 'name', target: 'name' }],
737+
};
738+
739+
it('should accept lookup with transform pipeline', () => {
740+
const lookup = {
741+
...baseLookup,
742+
transform: {
743+
request: {
744+
headers: { 'X-Custom-Header': 'value' },
745+
queryParams: { format: 'json' },
746+
},
747+
response: {
748+
dataPath: '$.data.results',
749+
totalPath: '$.meta.total',
750+
},
751+
},
752+
};
753+
754+
expect(() => ExternalLookupSchema.parse(lookup)).not.toThrow();
755+
});
756+
757+
it('should accept partial transform config', () => {
758+
const result = ExternalLookupSchema.parse({
759+
...baseLookup,
760+
transform: {
761+
response: { dataPath: '$.items' },
762+
},
763+
});
764+
765+
expect(result.transform?.response?.dataPath).toBe('$.items');
766+
expect(result.transform?.request).toBeUndefined();
767+
});
768+
});
769+
770+
describe('External Lookup Pagination Configuration', () => {
771+
const baseLookup = {
772+
fieldName: 'external_data',
773+
dataSource: {
774+
id: 'test-api',
775+
name: 'Test API',
776+
type: 'rest-api' as const,
777+
endpoint: 'https://api.example.com',
778+
authentication: { type: 'api-key' as const, config: { key: 'test' } },
779+
},
780+
query: { endpoint: '/data' },
781+
fieldMappings: [{ source: 'name', target: 'name' }],
782+
};
783+
784+
it('should accept lookup with pagination', () => {
785+
const result = ExternalLookupSchema.parse({
786+
...baseLookup,
787+
pagination: {
788+
type: 'cursor',
789+
pageSize: 50,
790+
maxPages: 10,
791+
},
792+
});
793+
794+
expect(result.pagination?.type).toBe('cursor');
795+
expect(result.pagination?.pageSize).toBe(50);
796+
});
797+
798+
it('should apply pagination defaults', () => {
799+
const result = ExternalLookupSchema.parse({
800+
...baseLookup,
801+
pagination: {},
802+
});
803+
804+
expect(result.pagination?.type).toBe('offset');
805+
expect(result.pagination?.pageSize).toBe(100);
806+
});
807+
808+
it('should accept all pagination types', () => {
809+
const types = ['offset', 'cursor', 'page'] as const;
810+
types.forEach(type => {
811+
expect(() => ExternalLookupSchema.parse({
812+
...baseLookup,
813+
pagination: { type },
814+
})).not.toThrow();
815+
});
816+
});
817+
});

packages/spec/src/data/external-lookup.zod.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,67 @@ export const ExternalLookupSchema = z.object({
241241
*/
242242
burstSize: z.number().optional().describe('Burst size'),
243243
}).optional().describe('Rate limiting'),
244+
245+
/**
246+
* Retry configuration with exponential backoff
247+
*
248+
* @example
249+
* ```json
250+
* {
251+
* "maxRetries": 3,
252+
* "initialDelayMs": 1000,
253+
* "maxDelayMs": 30000,
254+
* "backoffMultiplier": 2,
255+
* "retryableStatusCodes": [429, 500, 502, 503, 504]
256+
* }
257+
* ```
258+
*/
259+
retry: z.object({
260+
/** Maximum number of retry attempts */
261+
maxRetries: z.number().min(0).default(3).describe('Maximum retry attempts'),
262+
/** Initial delay before first retry (ms) */
263+
initialDelayMs: z.number().default(1000).describe('Initial retry delay in milliseconds'),
264+
/** Maximum delay between retries (ms) */
265+
maxDelayMs: z.number().default(30000).describe('Maximum retry delay in milliseconds'),
266+
/** Backoff multiplier for exponential backoff */
267+
backoffMultiplier: z.number().default(2).describe('Exponential backoff multiplier'),
268+
/** HTTP status codes that trigger a retry */
269+
retryableStatusCodes: z.array(z.number()).default([429, 500, 502, 503, 504])
270+
.describe('HTTP status codes that are retryable'),
271+
}).optional().describe('Retry configuration with exponential backoff'),
272+
273+
/**
274+
* Request/response transformation pipeline
275+
*
276+
* Allows transforming request parameters and response data
277+
* before they are processed by the external lookup system.
278+
*/
279+
transform: z.object({
280+
/** Transform request parameters before sending */
281+
request: z.object({
282+
/** Header transformations (key-value additions) */
283+
headers: z.record(z.string(), z.string()).optional().describe('Additional request headers'),
284+
/** Query parameter transformations */
285+
queryParams: z.record(z.string(), z.string()).optional().describe('Additional query parameters'),
286+
}).optional().describe('Request transformation'),
287+
/** Transform response data after receiving */
288+
response: z.object({
289+
/** JSONPath expression to extract data from response */
290+
dataPath: z.string().optional().describe('JSONPath to extract data (e.g., "$.data.results")'),
291+
/** JSONPath expression to extract total count for pagination */
292+
totalPath: z.string().optional().describe('JSONPath to extract total count (e.g., "$.meta.total")'),
293+
}).optional().describe('Response transformation'),
294+
}).optional().describe('Request/response transformation pipeline'),
295+
296+
/** Pagination support for external data sources */
297+
pagination: z.object({
298+
/** Pagination type */
299+
type: z.enum(['offset', 'cursor', 'page']).default('offset').describe('Pagination type'),
300+
/** Page size */
301+
pageSize: z.number().default(100).describe('Items per page'),
302+
/** Maximum pages to fetch */
303+
maxPages: z.number().optional().describe('Maximum number of pages to fetch'),
304+
}).optional().describe('Pagination configuration for external data'),
244305
});
245306

246307
// Type exports

packages/spec/src/system/cache.test.ts

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ import {
44
CacheTierSchema,
55
CacheInvalidationSchema,
66
CacheConfigSchema,
7+
CacheConsistencySchema,
8+
CacheAvalanchePreventionSchema,
9+
CacheWarmupSchema,
10+
DistributedCacheConfigSchema,
711
} from './cache.zod';
812

913
describe('CacheStrategySchema', () => {
@@ -158,3 +162,137 @@ describe('CacheConfigSchema', () => {
158162
expect(() => CacheConfigSchema.parse({ tiers: [] })).toThrow();
159163
});
160164
});
165+
166+
describe('CacheConsistencySchema', () => {
167+
it('should accept all consistency strategies', () => {
168+
const strategies = ['write_through', 'write_behind', 'write_around', 'refresh_ahead'] as const;
169+
strategies.forEach(strategy => {
170+
expect(() => CacheConsistencySchema.parse(strategy)).not.toThrow();
171+
});
172+
});
173+
174+
it('should reject invalid strategy', () => {
175+
expect(() => CacheConsistencySchema.parse('read_through')).toThrow();
176+
});
177+
});
178+
179+
describe('CacheAvalanchePreventionSchema', () => {
180+
it('should accept empty config', () => {
181+
expect(() => CacheAvalanchePreventionSchema.parse({})).not.toThrow();
182+
});
183+
184+
it('should accept jitter TTL config', () => {
185+
const result = CacheAvalanchePreventionSchema.parse({
186+
jitterTtl: { enabled: true, maxJitterSeconds: 30 },
187+
});
188+
expect(result.jitterTtl?.enabled).toBe(true);
189+
expect(result.jitterTtl?.maxJitterSeconds).toBe(30);
190+
});
191+
192+
it('should accept circuit breaker config', () => {
193+
const result = CacheAvalanchePreventionSchema.parse({
194+
circuitBreaker: { enabled: true, failureThreshold: 10, resetTimeout: 60 },
195+
});
196+
expect(result.circuitBreaker?.failureThreshold).toBe(10);
197+
});
198+
199+
it('should accept lockout config', () => {
200+
const result = CacheAvalanchePreventionSchema.parse({
201+
lockout: { enabled: true, lockTimeoutMs: 3000 },
202+
});
203+
expect(result.lockout?.lockTimeoutMs).toBe(3000);
204+
});
205+
206+
it('should accept full prevention config', () => {
207+
expect(() => CacheAvalanchePreventionSchema.parse({
208+
jitterTtl: { enabled: true },
209+
circuitBreaker: { enabled: true },
210+
lockout: { enabled: true },
211+
})).not.toThrow();
212+
});
213+
});
214+
215+
describe('CacheWarmupSchema', () => {
216+
it('should accept minimal warmup config', () => {
217+
const result = CacheWarmupSchema.parse({});
218+
expect(result.enabled).toBe(false);
219+
expect(result.strategy).toBe('lazy');
220+
});
221+
222+
it('should accept eager warmup with patterns', () => {
223+
const result = CacheWarmupSchema.parse({
224+
enabled: true,
225+
strategy: 'eager',
226+
patterns: ['config:*', 'user:*'],
227+
concurrency: 20,
228+
});
229+
expect(result.strategy).toBe('eager');
230+
expect(result.patterns).toHaveLength(2);
231+
expect(result.concurrency).toBe(20);
232+
});
233+
234+
it('should accept scheduled warmup', () => {
235+
const result = CacheWarmupSchema.parse({
236+
enabled: true,
237+
strategy: 'scheduled',
238+
schedule: '0 0 * * *',
239+
});
240+
expect(result.schedule).toBe('0 0 * * *');
241+
});
242+
});
243+
244+
describe('DistributedCacheConfigSchema', () => {
245+
it('should accept basic distributed cache', () => {
246+
const config = DistributedCacheConfigSchema.parse({
247+
enabled: true,
248+
tiers: [
249+
{ name: 'l1', type: 'memory', maxSize: 100 },
250+
{ name: 'l2', type: 'redis', maxSize: 1000 },
251+
],
252+
invalidation: [{ trigger: 'update', scope: 'key' }],
253+
consistency: 'write_through',
254+
});
255+
256+
expect(config.consistency).toBe('write_through');
257+
});
258+
259+
it('should accept full distributed cache config', () => {
260+
const config = DistributedCacheConfigSchema.parse({
261+
enabled: true,
262+
tiers: [
263+
{ name: 'l1', type: 'memory', maxSize: 100, ttl: 60, strategy: 'lru' },
264+
{ name: 'l2', type: 'redis', maxSize: 1000, ttl: 300, strategy: 'lru' },
265+
],
266+
invalidation: [{ trigger: 'update', scope: 'key' }],
267+
consistency: 'write_behind',
268+
avalanchePrevention: {
269+
jitterTtl: { enabled: true, maxJitterSeconds: 30 },
270+
circuitBreaker: { enabled: true, failureThreshold: 5 },
271+
lockout: { enabled: true },
272+
},
273+
warmup: {
274+
enabled: true,
275+
strategy: 'eager',
276+
patterns: ['config:*'],
277+
},
278+
});
279+
280+
expect(config.consistency).toBe('write_behind');
281+
expect(config.avalanchePrevention?.jitterTtl?.enabled).toBe(true);
282+
expect(config.warmup?.strategy).toBe('eager');
283+
});
284+
285+
it('should extend CacheConfigSchema fields', () => {
286+
const config = DistributedCacheConfigSchema.parse({
287+
enabled: true,
288+
tiers: [{ name: 'test', type: 'memory' }],
289+
invalidation: [{ trigger: 'update', scope: 'key' }],
290+
prefetch: true,
291+
compression: true,
292+
encryption: true,
293+
});
294+
295+
expect(config.prefetch).toBe(true);
296+
expect(config.compression).toBe(true);
297+
});
298+
});

0 commit comments

Comments
 (0)