Skip to content

Commit ef22cf8

Browse files
authored
Merge pull request #654 from objectstack-ai/copilot/analyze-test-issue
2 parents 065693d + 36b9eae commit ef22cf8

5 files changed

Lines changed: 384 additions & 12 deletions

File tree

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
---
2+
"@objectstack/spec": major
3+
---
4+
5+
**Breaking Change: Strict Validation Enabled by Default**
6+
7+
`defineStack()` now validates configurations by default to enforce naming conventions and catch errors early.
8+
9+
**What Changed:**
10+
- `defineStack()` now defaults to `strict: true` (was `strict: false`)
11+
- Field names are now validated to ensure snake_case format
12+
- Object names, field types, and all schema definitions are validated
13+
14+
**Migration Guide:**
15+
16+
If you have existing code that violates naming conventions:
17+
18+
```typescript
19+
// Before (would silently accept invalid names):
20+
defineStack({
21+
manifest: {...},
22+
objects: [{
23+
name: 'my_object',
24+
fields: {
25+
firstName: { type: 'text' } // ❌ Invalid: camelCase
26+
}
27+
}]
28+
});
29+
30+
// After (will throw validation error):
31+
// Error: Field names must be lowercase snake_case
32+
33+
// Fix: Use snake_case
34+
defineStack({
35+
manifest: {...},
36+
objects: [{
37+
name: 'my_object',
38+
fields: {
39+
first_name: { type: 'text' } // ✅ Valid: snake_case
40+
}
41+
}]
42+
});
43+
```
44+
45+
**Temporary Workaround:**
46+
47+
If you need to temporarily disable validation while fixing your code:
48+
49+
```typescript
50+
defineStack(config, { strict: false }); // Bypass validation
51+
```
52+
53+
**Why This Change:**
54+
55+
1. **Catches Errors Early**: Invalid field names caught during development, not runtime
56+
2. **Enforces Conventions**: Ensures consistent snake_case naming across all projects
57+
3. **Prevents AI Hallucinations**: AI-generated objects must follow proper conventions
58+
4. **Database Compatibility**: snake_case prevents case-sensitivity issues in queries
59+
60+
**Impact:**
61+
62+
- Projects with properly named fields (snake_case): ✅ No changes needed
63+
- Projects with camelCase/PascalCase fields: ⚠️ Must update field names or use `strict: false`

FIELD_VALIDATION_FIX.md

Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
# Field Name Validation Fix - Technical Summary
2+
3+
## Issue Report (Chinese)
4+
按规则,字段名必须是 snake_case,这里的代码是怎么 test 通过的?为什么启动没有报错?
5+
6+
**Translation:**
7+
According to the rules, field names must be snake_case. How did this code pass the tests? Why didn't it report an error at startup?
8+
9+
## Root Cause Analysis
10+
11+
### The Problem
12+
The ObjectStack Protocol specification mandates snake_case naming for field names, but this validation was not being enforced in practice.
13+
14+
### Investigation Findings
15+
16+
1. **Field Schema Validation EXISTS** (but wasn't running):
17+
- Location: `packages/spec/src/data/field.zod.ts:346`
18+
- Regex: `/^[a-z_][a-z0-9_]*$/`
19+
- Field `name` property is optional and validated when provided
20+
21+
2. **Object Schema Validation EXISTS** (but wasn't running):
22+
- Location: `packages/spec/src/data/object.zod.ts:263-265`
23+
- Field **keys** in `fields` record validated with same regex
24+
- Custom error message: "Field names must be lowercase snake_case"
25+
26+
3. **defineStack() Bypassed Validation**:
27+
- Location: `packages/spec/src/stack.zod.ts:335-337`
28+
- Default was `strict: false`
29+
- When `strict: false`, returned config as-is **without any validation**
30+
- Code: `if (!options?.strict) { return config as ObjectStackDefinition; }`
31+
32+
### Why Tests Passed
33+
34+
The test suite was passing because:
35+
- Tests that checked validation explicitly used `ObjectSchema.parse()`
36+
- Tests that used `defineStack()` relied on default `strict: false` behavior
37+
- The validation logic itself was correct, just never executed
38+
- Examples used `as any` casts to bypass TypeScript checking
39+
40+
### Why No Runtime Errors
41+
42+
Runtime didn't fail because:
43+
- `defineStack()` with default options skipped validation entirely
44+
- Invalid field names were accepted and passed through
45+
- Only when strict=true was explicitly set would validation run
46+
47+
## Solution Implemented
48+
49+
### Core Fix
50+
Changed `defineStack()` default from `strict: false` to `strict: true`:
51+
52+
```typescript
53+
// Before (packages/spec/src/stack.zod.ts)
54+
/**
55+
* @default false // ❌ BAD: No validation by default
56+
*/
57+
strict?: boolean;
58+
59+
if (!options?.strict) {
60+
return config as ObjectStackDefinition; // ❌ Skip validation
61+
}
62+
63+
// After
64+
/**
65+
* @default true // ✅ GOOD: Validate by default
66+
*/
67+
strict?: boolean;
68+
69+
const strict = options?.strict !== false; // ✅ Default to true
70+
71+
if (!strict) {
72+
return config as ObjectStackDefinition; // Only skip if explicitly requested
73+
}
74+
```
75+
76+
### Additional Changes
77+
78+
1. **Updated Tests** (`packages/spec/src/stack.test.ts`):
79+
- Fixed test expecting old behavior
80+
- Added 4 new field name validation tests
81+
- Verified camelCase/PascalCase rejection
82+
- Verified snake_case acceptance
83+
- Verified strict=false bypass
84+
85+
2. **Added Validation Tests** (`packages/spec/src/data/field-name-validation.test.ts`):
86+
- Direct FieldSchema validation tests
87+
- Confirms regex works correctly
88+
89+
3. **Created Changeset** (`.changeset/strict-validation-by-default.md`):
90+
- Documents breaking change
91+
- Migration guide for users
92+
- Explains benefits and impact
93+
94+
## Validation Rules Reference
95+
96+
### Valid Field Names (snake_case)
97+
```typescript
98+
first_name
99+
last_name
100+
email
101+
company_name
102+
annual_revenue
103+
_system_id
104+
```
105+
106+
### Invalid Field Names
107+
```typescript
108+
firstName // camelCase
109+
FirstName // PascalCase
110+
first-name // kebab-case
111+
first name // spaces
112+
❌ 123field // starts with number
113+
first.name // dots
114+
```
115+
116+
## Validation Flow
117+
118+
### With strict=true (NEW DEFAULT)
119+
```
120+
defineStack(config)
121+
122+
ObjectStackDefinitionSchema.safeParse(config)
123+
124+
ObjectSchema validation
125+
126+
fields: z.record(regex validation on keys)
127+
128+
Reject if any field key is not snake_case
129+
130+
Cross-reference validation
131+
132+
Return validated data
133+
```
134+
135+
### With strict=false (OPT-OUT)
136+
```
137+
defineStack(config, { strict: false })
138+
139+
Return config as-is (NO VALIDATION)
140+
```
141+
142+
## Impact Assessment
143+
144+
### Breaking Change
145+
This is a **major version breaking change** because:
146+
- Existing code with invalid field names will now fail
147+
- Default behavior changed from permissive to strict
148+
- Migration required for affected projects
149+
150+
### Benefits
151+
1. **Early Error Detection**: Catches naming violations during development
152+
2. **Convention Enforcement**: Ensures consistent snake_case naming
153+
3. **Database Compatibility**: Prevents case-sensitivity issues in SQL queries
154+
4. **AI Safety**: Prevents AI-generated objects from violating conventions
155+
5. **Better DX**: Clear error messages guide developers to fix issues
156+
157+
### Migration Path
158+
159+
**Option 1: Fix Field Names (Recommended)**
160+
```typescript
161+
// Change field definitions to use snake_case
162+
fields: {
163+
first_name: Field.text(), // ✅ Fixed
164+
last_name: Field.text(), // ✅ Fixed
165+
}
166+
```
167+
168+
**Option 2: Temporary Bypass (Not Recommended)**
169+
```typescript
170+
// Use strict: false to temporarily disable validation
171+
defineStack(config, { strict: false });
172+
```
173+
174+
## Test Results
175+
176+
**All tests passing**: 195 test files, 5251 tests
177+
**Examples verified**: All use correct snake_case naming
178+
**Validation confirmed**: Correctly rejects camelCase/PascalCase
179+
**Validation confirmed**: Correctly accepts snake_case
180+
**Backward compatibility**: strict=false still works for edge cases
181+
182+
## Files Modified
183+
184+
1. `packages/spec/src/stack.zod.ts` - Changed default to strict=true
185+
2. `packages/spec/src/stack.test.ts` - Updated tests for new behavior
186+
3. `packages/spec/src/data/field-name-validation.test.ts` - New validation tests
187+
4. `.changeset/strict-validation-by-default.md` - Breaking change documentation
188+
189+
## Conclusion
190+
191+
The issue was that field name validation **existed but was disabled by default**. By changing `defineStack()` to validate by default, we now enforce the snake_case naming convention that was always part of the specification but wasn't being checked in practice.
192+
193+
This fix ensures that the rule "字段名必须是 snake_case" (field names must be snake_case) is now actually enforced, preventing issues like the one reported by the user.
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { describe, it, expect } from 'vitest';
2+
import { FieldSchema } from './field.zod';
3+
4+
describe('FieldSchema name validation', () => {
5+
it('should reject camelCase name when provided', () => {
6+
const fieldWithCamelCase = {
7+
name: 'camelCaseName',
8+
type: 'text',
9+
label: 'Test Field'
10+
};
11+
12+
expect(() => FieldSchema.parse(fieldWithCamelCase)).toThrow();
13+
});
14+
15+
it('should reject PascalCase name when provided', () => {
16+
const fieldWithPascalCase = {
17+
name: 'PascalCaseName',
18+
type: 'text',
19+
label: 'Test Field'
20+
};
21+
22+
expect(() => FieldSchema.parse(fieldWithPascalCase)).toThrow();
23+
});
24+
25+
it('should accept snake_case name when provided', () => {
26+
const fieldWithSnakeCase = {
27+
name: 'snake_case_name',
28+
type: 'text',
29+
label: 'Test Field'
30+
};
31+
32+
expect(() => FieldSchema.parse(fieldWithSnakeCase)).not.toThrow();
33+
});
34+
35+
it('should accept field without name (optional)', () => {
36+
const fieldWithoutName = {
37+
type: 'text',
38+
label: 'Test Field'
39+
};
40+
41+
expect(() => FieldSchema.parse(fieldWithoutName)).not.toThrow();
42+
});
43+
});

packages/spec/src/stack.test.ts

Lines changed: 72 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -424,16 +424,19 @@ describe('defineStack', () => {
424424
type: 'app' as const,
425425
};
426426

427-
it('should return config as-is in default mode (backward compatible)', () => {
427+
it('should validate config in default mode (strict by default)', () => {
428428
const config = { manifest: baseManifest, objects: [] };
429429
const result = defineStack(config);
430-
expect(result).toBe(config);
430+
// Default is now strict=true, so result is validated and is a different object reference
431+
expect(result).not.toBe(config); // Validation creates new object
432+
expect(result).toEqual(config); // But content is the same
433+
expect(result.manifest).toBeDefined();
431434
});
432435

433436
it('should return config as-is when strict is false', () => {
434437
const config = { manifest: baseManifest };
435438
const result = defineStack(config, { strict: false });
436-
expect(result).toBe(config);
439+
expect(result).toBe(config); // When strict=false, should return same reference
437440
});
438441

439442
it('should parse and validate in strict mode', () => {
@@ -523,3 +526,69 @@ describe('defineStack', () => {
523526
expect(() => defineStack(config, { strict: true })).not.toThrow();
524527
});
525528
});
529+
530+
describe('defineStack - Field Name Validation', () => {
531+
const baseManifest = {
532+
id: 'com.example.test',
533+
name: 'test-field-validation',
534+
version: '1.0.0',
535+
type: 'app' as const,
536+
};
537+
538+
it('should reject camelCase field names in strict mode (default)', () => {
539+
const config = {
540+
manifest: baseManifest,
541+
objects: [{
542+
name: 'test_object',
543+
fields: {
544+
firstName: { type: 'text' as const } // Invalid: camelCase
545+
}
546+
}]
547+
};
548+
549+
expect(() => defineStack(config)).toThrow(/Invalid key in record|Field names must be lowercase snake_case/);
550+
});
551+
552+
it('should reject PascalCase field names in strict mode (default)', () => {
553+
const config = {
554+
manifest: baseManifest,
555+
objects: [{
556+
name: 'test_object',
557+
fields: {
558+
FirstName: { type: 'text' as const } // Invalid: PascalCase
559+
}
560+
}]
561+
};
562+
563+
expect(() => defineStack(config)).toThrow(/Invalid key in record|Field names must be lowercase snake_case/);
564+
});
565+
566+
it('should accept snake_case field names', () => {
567+
const config = {
568+
manifest: baseManifest,
569+
objects: [{
570+
name: 'test_object',
571+
fields: {
572+
first_name: { type: 'text' as const }, // Valid
573+
last_name: { type: 'text' as const }, // Valid
574+
}
575+
}]
576+
};
577+
578+
expect(() => defineStack(config)).not.toThrow();
579+
});
580+
581+
it('should bypass validation when strict is false', () => {
582+
const config = {
583+
manifest: baseManifest,
584+
objects: [{
585+
name: 'test_object',
586+
fields: {
587+
firstName: { type: 'text' as const } // Invalid, but allowed in non-strict mode
588+
}
589+
}]
590+
};
591+
592+
expect(() => defineStack(config, { strict: false })).not.toThrow();
593+
});
594+
});

0 commit comments

Comments
 (0)