Skip to content

Commit 8fb25bf

Browse files
Copilothotlong
andcommitted
Add tests for kernel Zod schemas: context, feature, package-registry, plugin-structure
Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
1 parent bf3a942 commit 8fb25bf

4 files changed

Lines changed: 620 additions & 0 deletions

File tree

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import { describe, it, expect } from 'vitest';
2+
import {
3+
RuntimeMode,
4+
KernelContextSchema,
5+
type KernelContext,
6+
} from './context.zod';
7+
8+
describe('RuntimeMode', () => {
9+
it('should accept valid runtime modes', () => {
10+
expect(() => RuntimeMode.parse('development')).not.toThrow();
11+
expect(() => RuntimeMode.parse('production')).not.toThrow();
12+
expect(() => RuntimeMode.parse('test')).not.toThrow();
13+
expect(() => RuntimeMode.parse('provisioning')).not.toThrow();
14+
});
15+
16+
it('should reject invalid runtime modes', () => {
17+
expect(() => RuntimeMode.parse('staging')).toThrow();
18+
expect(() => RuntimeMode.parse('debug')).toThrow();
19+
expect(() => RuntimeMode.parse('')).toThrow();
20+
});
21+
});
22+
23+
describe('KernelContextSchema', () => {
24+
const validContext: KernelContext = {
25+
instanceId: '550e8400-e29b-41d4-a716-446655440000',
26+
mode: 'production',
27+
version: '1.0.0',
28+
cwd: '/app',
29+
startTime: Date.now(),
30+
features: {},
31+
};
32+
33+
it('should accept valid minimal context', () => {
34+
expect(() => KernelContextSchema.parse(validContext)).not.toThrow();
35+
});
36+
37+
it('should accept context with all optional fields', () => {
38+
const full = {
39+
...validContext,
40+
appName: 'My App',
41+
workspaceRoot: '/workspace',
42+
};
43+
const parsed = KernelContextSchema.parse(full);
44+
expect(parsed.appName).toBe('My App');
45+
expect(parsed.workspaceRoot).toBe('/workspace');
46+
});
47+
48+
it('should apply default mode to production', () => {
49+
const { mode: _, ...withoutMode } = validContext;
50+
const parsed = KernelContextSchema.parse(withoutMode);
51+
expect(parsed.mode).toBe('production');
52+
});
53+
54+
it('should apply default features to empty record', () => {
55+
const { features: _, ...withoutFeatures } = validContext;
56+
const parsed = KernelContextSchema.parse(withoutFeatures);
57+
expect(parsed.features).toEqual({});
58+
});
59+
60+
it('should accept feature flags', () => {
61+
const ctx = {
62+
...validContext,
63+
features: { darkMode: true, beta: false },
64+
};
65+
const parsed = KernelContextSchema.parse(ctx);
66+
expect(parsed.features.darkMode).toBe(true);
67+
expect(parsed.features.beta).toBe(false);
68+
});
69+
70+
it('should reject invalid instanceId (not UUID)', () => {
71+
expect(() => KernelContextSchema.parse({
72+
...validContext,
73+
instanceId: 'not-a-uuid',
74+
})).toThrow();
75+
});
76+
77+
it('should reject missing required fields', () => {
78+
expect(() => KernelContextSchema.parse({})).toThrow();
79+
expect(() => KernelContextSchema.parse({ instanceId: '550e8400-e29b-41d4-a716-446655440000' })).toThrow();
80+
});
81+
82+
it('should reject non-integer startTime', () => {
83+
expect(() => KernelContextSchema.parse({
84+
...validContext,
85+
startTime: 1.5,
86+
})).toThrow();
87+
});
88+
89+
it('should accept all runtime modes in context', () => {
90+
const modes = ['development', 'production', 'test', 'provisioning'] as const;
91+
modes.forEach(mode => {
92+
const parsed = KernelContextSchema.parse({ ...validContext, mode });
93+
expect(parsed.mode).toBe(mode);
94+
});
95+
});
96+
});
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
import { describe, it, expect } from 'vitest';
2+
import {
3+
FeatureStrategy,
4+
FeatureFlagSchema,
5+
FeatureFlag,
6+
type FeatureFlag as FeatureFlagType,
7+
} from './feature.zod';
8+
9+
describe('FeatureStrategy', () => {
10+
it('should accept valid strategies', () => {
11+
expect(() => FeatureStrategy.parse('boolean')).not.toThrow();
12+
expect(() => FeatureStrategy.parse('percentage')).not.toThrow();
13+
expect(() => FeatureStrategy.parse('user_list')).not.toThrow();
14+
expect(() => FeatureStrategy.parse('group')).not.toThrow();
15+
expect(() => FeatureStrategy.parse('custom')).not.toThrow();
16+
});
17+
18+
it('should reject invalid strategies', () => {
19+
expect(() => FeatureStrategy.parse('random')).toThrow();
20+
expect(() => FeatureStrategy.parse('')).toThrow();
21+
});
22+
});
23+
24+
describe('FeatureFlagSchema', () => {
25+
const minimalFlag = {
26+
name: 'dark_mode',
27+
};
28+
29+
it('should accept minimal feature flag', () => {
30+
expect(() => FeatureFlagSchema.parse(minimalFlag)).not.toThrow();
31+
});
32+
33+
it('should apply default values', () => {
34+
const parsed = FeatureFlagSchema.parse(minimalFlag);
35+
expect(parsed.enabled).toBe(false);
36+
expect(parsed.strategy).toBe('boolean');
37+
expect(parsed.environment).toBe('all');
38+
});
39+
40+
it('should accept feature flag with all fields', () => {
41+
const full: FeatureFlagType = {
42+
name: 'new_dashboard',
43+
label: 'New Dashboard',
44+
description: 'Enables the new dashboard UI',
45+
enabled: true,
46+
strategy: 'percentage',
47+
conditions: {
48+
percentage: 50,
49+
users: ['user_1', 'user_2'],
50+
groups: ['beta_testers'],
51+
expression: 'user.plan == "pro"',
52+
},
53+
environment: 'staging',
54+
expiresAt: '2025-12-31T23:59:59Z',
55+
};
56+
57+
const parsed = FeatureFlagSchema.parse(full);
58+
expect(parsed.label).toBe('New Dashboard');
59+
expect(parsed.conditions?.percentage).toBe(50);
60+
expect(parsed.conditions?.users).toHaveLength(2);
61+
expect(parsed.environment).toBe('staging');
62+
});
63+
64+
it('should reject invalid name (not snake_case)', () => {
65+
expect(() => FeatureFlagSchema.parse({ name: 'DarkMode' })).toThrow();
66+
expect(() => FeatureFlagSchema.parse({ name: 'dark-mode' })).toThrow();
67+
expect(() => FeatureFlagSchema.parse({ name: '1invalid' })).toThrow();
68+
});
69+
70+
it('should reject name that is too short', () => {
71+
expect(() => FeatureFlagSchema.parse({ name: 'a' })).toThrow();
72+
});
73+
74+
it('should accept all environment values', () => {
75+
const envs = ['dev', 'staging', 'prod', 'all'] as const;
76+
envs.forEach(environment => {
77+
const parsed = FeatureFlagSchema.parse({ name: 'test_flag', environment });
78+
expect(parsed.environment).toBe(environment);
79+
});
80+
});
81+
82+
it('should reject invalid environment', () => {
83+
expect(() => FeatureFlagSchema.parse({
84+
name: 'test_flag',
85+
environment: 'local',
86+
})).toThrow();
87+
});
88+
89+
it('should reject percentage out of range', () => {
90+
expect(() => FeatureFlagSchema.parse({
91+
name: 'test_flag',
92+
conditions: { percentage: 101 },
93+
})).toThrow();
94+
95+
expect(() => FeatureFlagSchema.parse({
96+
name: 'test_flag',
97+
conditions: { percentage: -1 },
98+
})).toThrow();
99+
});
100+
101+
it('should accept percentage at boundaries', () => {
102+
const zero = FeatureFlagSchema.parse({
103+
name: 'test_flag',
104+
conditions: { percentage: 0 },
105+
});
106+
expect(zero.conditions?.percentage).toBe(0);
107+
108+
const hundred = FeatureFlagSchema.parse({
109+
name: 'test_flag',
110+
conditions: { percentage: 100 },
111+
});
112+
expect(hundred.conditions?.percentage).toBe(100);
113+
});
114+
115+
it('should validate expiresAt as ISO datetime', () => {
116+
expect(() => FeatureFlagSchema.parse({
117+
name: 'test_flag',
118+
expiresAt: 'not-a-date',
119+
})).toThrow();
120+
121+
expect(() => FeatureFlagSchema.parse({
122+
name: 'test_flag',
123+
expiresAt: '2025-06-15T10:00:00Z',
124+
})).not.toThrow();
125+
});
126+
});
127+
128+
describe('FeatureFlag.create', () => {
129+
it('should return the config object as-is', () => {
130+
const config = { name: 'my_feature', enabled: true };
131+
const result = FeatureFlag.create(config);
132+
expect(result).toEqual(config);
133+
});
134+
});

0 commit comments

Comments
 (0)