Skip to content

Commit 7cb7d71

Browse files
Copilothotlong
andcommitted
feat: extend kernel services with ai/i18n/ui/workflow, add dispatcher protocol, extend API routes
Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
1 parent 0c34bd4 commit 7cb7d71

6 files changed

Lines changed: 389 additions & 8 deletions

File tree

packages/spec/src/api/discovery.zod.ts

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ export const ApiCapabilitiesSchema = z.object({
1111
files: z.boolean().default(true),
1212
analytics: z.boolean().default(false).describe('Is the Analytics/BI engine enabled?'),
1313
hub: z.boolean().default(false).describe('Is Hub management enabled?'),
14+
ai: z.boolean().default(false).describe('Is the AI engine enabled?'),
15+
workflow: z.boolean().default(false).describe('Is the Workflow engine enabled?'),
16+
notifications: z.boolean().default(false).describe('Is the Notification service enabled?'),
17+
i18n: z.boolean().default(false).describe('Is the i18n service enabled?'),
1418
});
1519

1620
/**
@@ -20,31 +24,49 @@ export const ApiCapabilitiesSchema = z.object({
2024
*/
2125
export const ApiRoutesSchema = z.object({
2226
/** Base URL for Object CRUD (Data Protocol) */
23-
data: z.string().describe('e.g. /api/data'),
27+
data: z.string().describe('e.g. /api/v1/data'),
2428

2529
/** Base URL for Schema Definitions (Metadata Protocol) */
26-
metadata: z.string().describe('e.g. /api/meta'),
30+
metadata: z.string().describe('e.g. /api/v1/meta'),
2731

2832
/** Base URL for UI Configurations (Views, Menus) */
29-
ui: z.string().optional().describe('e.g. /api/ui'),
33+
ui: z.string().optional().describe('e.g. /api/v1/ui'),
3034

3135
/** Base URL for Authentication */
32-
auth: z.string().describe('e.g. /api/auth'),
36+
auth: z.string().describe('e.g. /api/v1/auth'),
3337

3438
/** Base URL for Automation (Flows/Scripts) */
35-
automation: z.string().optional().describe('e.g. /api/automation'),
39+
automation: z.string().optional().describe('e.g. /api/v1/automation'),
3640

3741
/** Base URL for File/Storage operations */
38-
storage: z.string().optional().describe('e.g. /api/storage'),
42+
storage: z.string().optional().describe('e.g. /api/v1/storage'),
3943

4044
/** Base URL for Analytics/BI operations */
41-
analytics: z.string().optional().describe('e.g. /api/analytics'),
45+
analytics: z.string().optional().describe('e.g. /api/v1/analytics'),
4246

4347
/** Base URL for Hub Management (Multi-tenant/Marketplace) */
44-
hub: z.string().optional().describe('e.g. /api/hub'),
48+
hub: z.string().optional().describe('e.g. /api/v1/hub'),
4549

4650
/** GraphQL Endpoint (if enabled) */
4751
graphql: z.string().optional().describe('e.g. /graphql'),
52+
53+
/** Base URL for Package Management */
54+
packages: z.string().optional().describe('e.g. /api/v1/packages'),
55+
56+
/** Base URL for Workflow Engine */
57+
workflow: z.string().optional().describe('e.g. /api/v1/workflow'),
58+
59+
/** Base URL for Realtime (WebSocket/SSE) */
60+
realtime: z.string().optional().describe('e.g. /api/v1/realtime'),
61+
62+
/** Base URL for Notification Service */
63+
notifications: z.string().optional().describe('e.g. /api/v1/notifications'),
64+
65+
/** Base URL for AI Engine (NLQ, Chat, Suggest) */
66+
ai: z.string().optional().describe('e.g. /api/v1/ai'),
67+
68+
/** Base URL for Internationalization */
69+
i18n: z.string().optional().describe('e.g. /api/v1/i18n'),
4870
});
4971

5072
/**
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
import { describe, it, expect } from 'vitest';
2+
import {
3+
DispatcherRouteSchema,
4+
DispatcherConfigSchema,
5+
DEFAULT_DISPATCHER_ROUTES,
6+
type DispatcherRoute,
7+
type DispatcherConfig,
8+
} from './dispatcher.zod';
9+
10+
describe('DispatcherRouteSchema', () => {
11+
it('should accept valid route with all fields', () => {
12+
const route: DispatcherRoute = {
13+
prefix: '/api/v1/data',
14+
service: 'data',
15+
authRequired: true,
16+
criticality: 'required',
17+
};
18+
19+
expect(() => DispatcherRouteSchema.parse(route)).not.toThrow();
20+
});
21+
22+
it('should apply default values', () => {
23+
const route = DispatcherRouteSchema.parse({
24+
prefix: '/api/v1/ai',
25+
service: 'ai',
26+
});
27+
28+
expect(route.authRequired).toBe(true);
29+
expect(route.criticality).toBe('optional');
30+
});
31+
32+
it('should accept public route (no auth)', () => {
33+
const route = DispatcherRouteSchema.parse({
34+
prefix: '/api/v1/discovery',
35+
service: 'metadata',
36+
authRequired: false,
37+
});
38+
39+
expect(route.authRequired).toBe(false);
40+
});
41+
42+
it('should accept route with permissions', () => {
43+
const route = DispatcherRouteSchema.parse({
44+
prefix: '/api/v1/meta',
45+
service: 'metadata',
46+
permissions: ['system.metadata.read'],
47+
});
48+
49+
expect(route.permissions).toEqual(['system.metadata.read']);
50+
});
51+
52+
it('should reject route without leading slash', () => {
53+
expect(() => DispatcherRouteSchema.parse({
54+
prefix: 'api/v1/data',
55+
service: 'data',
56+
})).toThrow();
57+
});
58+
59+
it('should reject invalid service name', () => {
60+
expect(() => DispatcherRouteSchema.parse({
61+
prefix: '/api/v1/invalid',
62+
service: 'not-a-service',
63+
})).toThrow();
64+
});
65+
66+
it('should accept all valid criticality levels', () => {
67+
const levels = ['required', 'core', 'optional'] as const;
68+
levels.forEach(criticality => {
69+
const route = DispatcherRouteSchema.parse({
70+
prefix: '/api/v1/test',
71+
service: 'data',
72+
criticality,
73+
});
74+
expect(route.criticality).toBe(criticality);
75+
});
76+
});
77+
});
78+
79+
describe('DispatcherConfigSchema', () => {
80+
it('should accept valid config with routes', () => {
81+
const config: DispatcherConfig = {
82+
routes: [
83+
{ prefix: '/api/v1/data', service: 'data', authRequired: true, criticality: 'required' },
84+
{ prefix: '/api/v1/meta', service: 'metadata', authRequired: true, criticality: 'required' },
85+
],
86+
fallback: '404',
87+
};
88+
89+
expect(() => DispatcherConfigSchema.parse(config)).not.toThrow();
90+
});
91+
92+
it('should apply default fallback', () => {
93+
const config = DispatcherConfigSchema.parse({
94+
routes: [],
95+
});
96+
97+
expect(config.fallback).toBe('404');
98+
});
99+
100+
it('should accept proxy fallback with target', () => {
101+
const config = DispatcherConfigSchema.parse({
102+
routes: [],
103+
fallback: 'proxy',
104+
proxyTarget: 'https://api.example.com',
105+
});
106+
107+
expect(config.fallback).toBe('proxy');
108+
expect(config.proxyTarget).toBe('https://api.example.com');
109+
});
110+
111+
it('should accept custom fallback', () => {
112+
const config = DispatcherConfigSchema.parse({
113+
routes: [],
114+
fallback: 'custom',
115+
});
116+
117+
expect(config.fallback).toBe('custom');
118+
});
119+
});
120+
121+
describe('DEFAULT_DISPATCHER_ROUTES', () => {
122+
it('should have routes for all protocol namespaces', () => {
123+
expect(DEFAULT_DISPATCHER_ROUTES.length).toBeGreaterThanOrEqual(14);
124+
});
125+
126+
it('should include required services', () => {
127+
const services = DEFAULT_DISPATCHER_ROUTES.map(r => r.service);
128+
expect(services).toContain('metadata');
129+
expect(services).toContain('data');
130+
expect(services).toContain('auth');
131+
});
132+
133+
it('should include optional services', () => {
134+
const services = DEFAULT_DISPATCHER_ROUTES.map(r => r.service);
135+
expect(services).toContain('ai');
136+
expect(services).toContain('i18n');
137+
expect(services).toContain('ui');
138+
expect(services).toContain('workflow');
139+
expect(services).toContain('realtime');
140+
expect(services).toContain('notification');
141+
expect(services).toContain('analytics');
142+
expect(services).toContain('automation');
143+
expect(services).toContain('hub');
144+
});
145+
146+
it('should have discovery as public route', () => {
147+
const discovery = DEFAULT_DISPATCHER_ROUTES.find(r => r.prefix.includes('discovery'));
148+
expect(discovery).toBeDefined();
149+
expect(discovery!.authRequired).toBe(false);
150+
});
151+
152+
it('should mark required services with required criticality', () => {
153+
const required = DEFAULT_DISPATCHER_ROUTES.filter(r => r.criticality === 'required');
154+
const requiredServices = required.map(r => r.service);
155+
expect(requiredServices).toContain('metadata');
156+
expect(requiredServices).toContain('data');
157+
expect(requiredServices).toContain('auth');
158+
});
159+
160+
it('should have all prefixes starting with /', () => {
161+
DEFAULT_DISPATCHER_ROUTES.forEach(route => {
162+
expect(route.prefix).toMatch(/^\//);
163+
});
164+
});
165+
166+
it('should be parseable by DispatcherConfigSchema', () => {
167+
expect(() => DispatcherConfigSchema.parse({
168+
routes: DEFAULT_DISPATCHER_ROUTES,
169+
})).not.toThrow();
170+
});
171+
});

0 commit comments

Comments
 (0)