Migrate all repos into monorepo context folders
Bahn: aisupport, Analyse-O2C-C2S, awesome-bahn-mcp-servers, beam-mcp,
Confluence_Bot, db-planet-mcp-server, O2C-Harness, project-audit,
Projekt-KIQ-HP, teamlandkarte-mcp
Dhive: Jury-Voting
Privat: CV, NoteGraph (NOTE: NoteGraph needs complete redo after consolidation)
Shared: AI-Orchestrator, OrgMyLife, power_skills_and_more
Shared/references: symphony (read-only)
Bahn repos remain available as independent remotes - this monorepo
pulls them in via subtree, the originals are untouched.
This commit is contained in:
@@ -0,0 +1,661 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import {
|
||||
validateEntity,
|
||||
validatePerson,
|
||||
validateExperience,
|
||||
validateSkill,
|
||||
validateOrganization,
|
||||
validateProject,
|
||||
validateCertification,
|
||||
validateTandem,
|
||||
isKebabCase,
|
||||
isValidDate,
|
||||
isTemporallyConsistent,
|
||||
ValidationResult,
|
||||
} from './validate';
|
||||
|
||||
// --- Helper: minimal valid entity headers ---
|
||||
const baseHeader = (type: string) => ({
|
||||
id: 'test-entity',
|
||||
type,
|
||||
created: '2025-01-15',
|
||||
modified: '2025-01-15',
|
||||
});
|
||||
|
||||
describe('isKebabCase', () => {
|
||||
it('accepts valid kebab-case strings', () => {
|
||||
expect(isKebabCase('hello')).toBe(true);
|
||||
expect(isKebabCase('hello-world')).toBe(true);
|
||||
expect(isKebabCase('a-b-c')).toBe(true);
|
||||
expect(isKebabCase('test123')).toBe(true);
|
||||
expect(isKebabCase('my-skill-2')).toBe(true);
|
||||
});
|
||||
|
||||
it('rejects invalid kebab-case strings', () => {
|
||||
expect(isKebabCase('')).toBe(false);
|
||||
expect(isKebabCase('-leading')).toBe(false);
|
||||
expect(isKebabCase('trailing-')).toBe(false);
|
||||
expect(isKebabCase('UPPER')).toBe(false);
|
||||
expect(isKebabCase('camelCase')).toBe(false);
|
||||
expect(isKebabCase('has space')).toBe(false);
|
||||
expect(isKebabCase('double--hyphen')).toBe(false);
|
||||
expect(isKebabCase('under_score')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isValidDate', () => {
|
||||
it('accepts valid ISO dates', () => {
|
||||
expect(isValidDate('2025-01-15')).toBe(true);
|
||||
expect(isValidDate('2025-01')).toBe(true);
|
||||
expect(isValidDate('2020-12-31')).toBe(true);
|
||||
expect(isValidDate('1999-06')).toBe(true);
|
||||
});
|
||||
|
||||
it('rejects invalid dates', () => {
|
||||
expect(isValidDate('')).toBe(false);
|
||||
expect(isValidDate('2025')).toBe(false);
|
||||
expect(isValidDate('2025-13')).toBe(false);
|
||||
expect(isValidDate('2025-00')).toBe(false);
|
||||
expect(isValidDate('not-a-date')).toBe(false);
|
||||
expect(isValidDate('2025/01/15')).toBe(false);
|
||||
expect(isValidDate('01-2025')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isTemporallyConsistent', () => {
|
||||
it('returns true when end is null or undefined', () => {
|
||||
expect(isTemporallyConsistent('2025-01', null)).toBe(true);
|
||||
expect(isTemporallyConsistent('2025-01', undefined)).toBe(true);
|
||||
});
|
||||
|
||||
it('returns true when start <= end', () => {
|
||||
expect(isTemporallyConsistent('2020-01', '2025-01')).toBe(true);
|
||||
expect(isTemporallyConsistent('2025-01', '2025-01')).toBe(true);
|
||||
expect(isTemporallyConsistent('2025-01-01', '2025-01-02')).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false when start > end', () => {
|
||||
expect(isTemporallyConsistent('2025-06', '2025-01')).toBe(false);
|
||||
expect(isTemporallyConsistent('2025-01-15', '2025-01-01')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateEntity', () => {
|
||||
it('rejects null/undefined', () => {
|
||||
expect(validateEntity(null).valid).toBe(false);
|
||||
expect(validateEntity(undefined).valid).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects non-objects', () => {
|
||||
expect(validateEntity('string').valid).toBe(false);
|
||||
expect(validateEntity(42).valid).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects missing type', () => {
|
||||
const result = validateEntity({ id: 'test' });
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.errors).toContain('Missing required field: type');
|
||||
});
|
||||
|
||||
it('rejects invalid type', () => {
|
||||
const result = validateEntity({ ...baseHeader('invalid-type') });
|
||||
expect(result.valid).toBe(false);
|
||||
});
|
||||
|
||||
it('dispatches to correct validator', () => {
|
||||
const person = {
|
||||
...baseHeader('person'),
|
||||
name: { first: 'Andre', last: 'Knie', display: 'Andre Knie' },
|
||||
};
|
||||
expect(validateEntity(person).valid).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('validatePerson', () => {
|
||||
it('accepts a valid person', () => {
|
||||
const person = {
|
||||
...baseHeader('person'),
|
||||
name: { first: 'Andre', last: 'Knie', display: 'Andre Knie' },
|
||||
};
|
||||
expect(validatePerson(person).valid).toBe(true);
|
||||
});
|
||||
|
||||
it('rejects missing name', () => {
|
||||
const result = validatePerson(baseHeader('person'));
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.errors).toContain('Missing required field: name');
|
||||
});
|
||||
|
||||
it('rejects missing name subfields', () => {
|
||||
const person = { ...baseHeader('person'), name: { first: 'Andre' } };
|
||||
const result = validatePerson(person);
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.errors).toContain('Missing required field: name.last');
|
||||
expect(result.errors).toContain('Missing required field: name.display');
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateExperience', () => {
|
||||
it('accepts a valid experience', () => {
|
||||
const exp = {
|
||||
...baseHeader('experience'),
|
||||
title: 'Lead Developer',
|
||||
organization: 'db-cargo',
|
||||
start: '2023-04',
|
||||
end: null,
|
||||
};
|
||||
expect(validateExperience(exp).valid).toBe(true);
|
||||
});
|
||||
|
||||
it('rejects missing required fields', () => {
|
||||
const result = validateExperience(baseHeader('experience'));
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.errors).toContain('Missing required field: title');
|
||||
expect(result.errors).toContain('Missing required field: organization');
|
||||
expect(result.errors).toContain('Missing required field: start');
|
||||
});
|
||||
|
||||
it('detects temporal inconsistency', () => {
|
||||
const exp = {
|
||||
...baseHeader('experience'),
|
||||
title: 'Dev',
|
||||
organization: 'org',
|
||||
start: '2025-06',
|
||||
end: '2025-01',
|
||||
};
|
||||
const result = validateExperience(exp);
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.errors.some(e => e.includes('Temporal inconsistency'))).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateSkill', () => {
|
||||
it('accepts a valid skill', () => {
|
||||
const skill = {
|
||||
...baseHeader('skill'),
|
||||
name: 'Python',
|
||||
category: 'programming-language',
|
||||
level: 'expert',
|
||||
};
|
||||
expect(validateSkill(skill).valid).toBe(true);
|
||||
});
|
||||
|
||||
it('rejects invalid category', () => {
|
||||
const skill = {
|
||||
...baseHeader('skill'),
|
||||
name: 'Python',
|
||||
category: 'invalid-cat',
|
||||
level: 'expert',
|
||||
};
|
||||
const result = validateSkill(skill);
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.errors.some(e => e.includes('category'))).toBe(true);
|
||||
});
|
||||
|
||||
it('rejects invalid level', () => {
|
||||
const skill = {
|
||||
...baseHeader('skill'),
|
||||
name: 'Python',
|
||||
category: 'framework',
|
||||
level: 'master',
|
||||
};
|
||||
const result = validateSkill(skill);
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.errors.some(e => e.includes('level'))).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateOrganization', () => {
|
||||
it('accepts a valid organization', () => {
|
||||
const org = {
|
||||
...baseHeader('organization'),
|
||||
name: 'DB Cargo AG',
|
||||
industry: 'logistics',
|
||||
};
|
||||
expect(validateOrganization(org).valid).toBe(true);
|
||||
});
|
||||
|
||||
it('rejects missing fields', () => {
|
||||
const result = validateOrganization(baseHeader('organization'));
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.errors).toContain('Missing required field: name');
|
||||
expect(result.errors).toContain('Missing required field: industry');
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateProject', () => {
|
||||
it('accepts a valid project', () => {
|
||||
const project = {
|
||||
...baseHeader('project'),
|
||||
name: 'AI Platform',
|
||||
organization: 'db-cargo',
|
||||
start: '2024-01',
|
||||
};
|
||||
expect(validateProject(project).valid).toBe(true);
|
||||
});
|
||||
|
||||
it('detects temporal inconsistency', () => {
|
||||
const project = {
|
||||
...baseHeader('project'),
|
||||
name: 'AI Platform',
|
||||
organization: 'db-cargo',
|
||||
start: '2025-06',
|
||||
end: '2024-01',
|
||||
};
|
||||
const result = validateProject(project);
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.errors.some(e => e.includes('Temporal inconsistency'))).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateCertification', () => {
|
||||
it('accepts a valid certification', () => {
|
||||
const cert = {
|
||||
...baseHeader('certification'),
|
||||
name: 'AWS Solutions Architect',
|
||||
issuer: 'Amazon Web Services',
|
||||
date: '2024-03',
|
||||
person: 'andre-knie',
|
||||
};
|
||||
expect(validateCertification(cert).valid).toBe(true);
|
||||
});
|
||||
|
||||
it('rejects missing fields', () => {
|
||||
const result = validateCertification(baseHeader('certification'));
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.errors).toContain('Missing required field: name');
|
||||
expect(result.errors).toContain('Missing required field: issuer');
|
||||
expect(result.errors).toContain('Missing required field: date');
|
||||
expect(result.errors).toContain('Missing required field: person');
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateTandem', () => {
|
||||
it('accepts a valid tandem', () => {
|
||||
const tandem = {
|
||||
...baseHeader('tandem'),
|
||||
partners: ['andre-knie', 'claudia-froldi'],
|
||||
sharedVision: 'Shared vision text',
|
||||
complementarySkills: 'Complementary skills text',
|
||||
collaborationModel: 'Collaboration model text',
|
||||
};
|
||||
expect(validateTandem(tandem).valid).toBe(true);
|
||||
});
|
||||
|
||||
it('rejects partners with wrong count', () => {
|
||||
const tandem = {
|
||||
...baseHeader('tandem'),
|
||||
partners: ['only-one'],
|
||||
sharedVision: 'Vision',
|
||||
complementarySkills: 'Skills',
|
||||
collaborationModel: 'Model',
|
||||
};
|
||||
const result = validateTandem(tandem);
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.errors.some(e => e.includes('exactly 2'))).toBe(true);
|
||||
});
|
||||
|
||||
it('rejects non-array partners', () => {
|
||||
const tandem = {
|
||||
...baseHeader('tandem'),
|
||||
partners: 'not-an-array',
|
||||
sharedVision: 'Vision',
|
||||
complementarySkills: 'Skills',
|
||||
collaborationModel: 'Model',
|
||||
};
|
||||
const result = validateTandem(tandem);
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.errors.some(e => e.includes('must be an array'))).toBe(true);
|
||||
});
|
||||
|
||||
it('rejects missing tandem-specific fields', () => {
|
||||
const tandem = {
|
||||
...baseHeader('tandem'),
|
||||
partners: ['a', 'b'],
|
||||
};
|
||||
const result = validateTandem(tandem);
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.errors).toContain('Missing required field: sharedVision');
|
||||
expect(result.errors).toContain('Missing required field: complementarySkills');
|
||||
expect(result.errors).toContain('Missing required field: collaborationModel');
|
||||
});
|
||||
|
||||
it('rejects invalid id format in common fields', () => {
|
||||
const tandem = {
|
||||
id: 'Invalid-ID',
|
||||
type: 'tandem',
|
||||
created: '2025-01-15',
|
||||
modified: '2025-01-15',
|
||||
partners: ['a', 'b'],
|
||||
sharedVision: 'Vision',
|
||||
complementarySkills: 'Skills',
|
||||
collaborationModel: 'Model',
|
||||
};
|
||||
const result = validateTandem(tandem);
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.errors.some(e => e.includes('kebab-case'))).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
// --- Property-Based Tests (fast-check) ---
|
||||
|
||||
import fc from 'fast-check';
|
||||
|
||||
// --- Arbitraries ---
|
||||
|
||||
/** Generates a valid kebab-case ID */
|
||||
const kebabCaseId = fc
|
||||
.array(
|
||||
fc.stringMatching(/^[a-z0-9]{1,8}$/),
|
||||
{ minLength: 1, maxLength: 4 }
|
||||
)
|
||||
.map(parts => parts.join('-'));
|
||||
|
||||
/** Generates a valid ISO date (YYYY-MM or YYYY-MM-DD) */
|
||||
const isoDate = fc.oneof(
|
||||
fc.record({
|
||||
year: fc.integer({ min: 1990, max: 2030 }),
|
||||
month: fc.integer({ min: 1, max: 12 }),
|
||||
}).map(({ year, month }) => `${year}-${String(month).padStart(2, '0')}`),
|
||||
fc.record({
|
||||
year: fc.integer({ min: 1990, max: 2030 }),
|
||||
month: fc.integer({ min: 1, max: 12 }),
|
||||
day: fc.integer({ min: 1, max: 28 }),
|
||||
}).map(({ year, month, day }) => `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`)
|
||||
);
|
||||
|
||||
/** Generates a valid entity header */
|
||||
const entityHeader = (type: string) =>
|
||||
fc.record({
|
||||
id: kebabCaseId,
|
||||
type: fc.constant(type),
|
||||
created: isoDate,
|
||||
modified: isoDate,
|
||||
});
|
||||
|
||||
/** Generates a valid Person entity */
|
||||
const personArb = entityHeader('person').chain(header =>
|
||||
fc.record({
|
||||
first: fc.string({ minLength: 1, maxLength: 20 }).filter(s => s.trim().length > 0),
|
||||
last: fc.string({ minLength: 1, maxLength: 20 }).filter(s => s.trim().length > 0),
|
||||
display: fc.string({ minLength: 1, maxLength: 40 }).filter(s => s.trim().length > 0),
|
||||
}).map(name => ({
|
||||
...header,
|
||||
name: { first: name.first, last: name.last, display: name.display },
|
||||
}))
|
||||
);
|
||||
|
||||
/** Generates a valid Skill entity */
|
||||
const skillArb = entityHeader('skill').chain(header =>
|
||||
fc.record({
|
||||
name: fc.string({ minLength: 1, maxLength: 30 }).filter(s => s.trim().length > 0),
|
||||
category: fc.constantFrom('programming-language', 'framework', 'methodology', 'soft-skill', 'domain'),
|
||||
level: fc.constantFrom('beginner', 'intermediate', 'advanced', 'expert'),
|
||||
}).map(fields => ({ ...header, ...fields }))
|
||||
);
|
||||
|
||||
/** Generates a valid Organization entity */
|
||||
const orgArb = entityHeader('organization').chain(header =>
|
||||
fc.record({
|
||||
name: fc.string({ minLength: 1, maxLength: 30 }).filter(s => s.trim().length > 0),
|
||||
industry: fc.string({ minLength: 1, maxLength: 20 }).filter(s => s.trim().length > 0),
|
||||
}).map(fields => ({ ...header, ...fields }))
|
||||
);
|
||||
|
||||
/** Generates a valid Experience entity */
|
||||
const experienceArb = entityHeader('experience').chain(header =>
|
||||
fc.record({
|
||||
title: fc.string({ minLength: 1, maxLength: 30 }).filter(s => s.trim().length > 0),
|
||||
organization: kebabCaseId,
|
||||
start: isoDate,
|
||||
}).map(fields => ({ ...header, ...fields, end: null }))
|
||||
);
|
||||
|
||||
/** Generates a valid Project entity */
|
||||
const projectArb = entityHeader('project').chain(header =>
|
||||
fc.record({
|
||||
name: fc.string({ minLength: 1, maxLength: 30 }).filter(s => s.trim().length > 0),
|
||||
organization: kebabCaseId,
|
||||
start: isoDate,
|
||||
}).map(fields => ({ ...header, ...fields }))
|
||||
);
|
||||
|
||||
/** Generates a valid Certification entity */
|
||||
const certArb = entityHeader('certification').chain(header =>
|
||||
fc.record({
|
||||
name: fc.string({ minLength: 1, maxLength: 30 }).filter(s => s.trim().length > 0),
|
||||
issuer: fc.string({ minLength: 1, maxLength: 30 }).filter(s => s.trim().length > 0),
|
||||
date: isoDate,
|
||||
person: kebabCaseId,
|
||||
}).map(fields => ({ ...header, ...fields }))
|
||||
);
|
||||
|
||||
/** Generates a valid Tandem entity */
|
||||
const tandemArb = entityHeader('tandem').chain(header =>
|
||||
fc.record({
|
||||
partner1: kebabCaseId,
|
||||
partner2: kebabCaseId,
|
||||
sharedVision: fc.string({ minLength: 1, maxLength: 50 }).filter(s => s.trim().length > 0),
|
||||
complementarySkills: fc.string({ minLength: 1, maxLength: 50 }).filter(s => s.trim().length > 0),
|
||||
collaborationModel: fc.string({ minLength: 1, maxLength: 50 }).filter(s => s.trim().length > 0),
|
||||
}).map(fields => ({
|
||||
...header,
|
||||
partners: [fields.partner1, fields.partner2] as [string, string],
|
||||
sharedVision: fields.sharedVision,
|
||||
complementarySkills: fields.complementarySkills,
|
||||
collaborationModel: fields.collaborationModel,
|
||||
}))
|
||||
);
|
||||
|
||||
/** Generates any valid entity */
|
||||
const anyValidEntity = fc.oneof(
|
||||
personArb,
|
||||
skillArb,
|
||||
orgArb,
|
||||
experienceArb,
|
||||
projectArb,
|
||||
certArb,
|
||||
tandemArb,
|
||||
);
|
||||
|
||||
// --- Property 1: Entity serialization round-trip ---
|
||||
|
||||
describe('Property 1: Entity serialization round-trip', () => {
|
||||
it('JSON.parse(JSON.stringify(entity)) produces an equivalent entity that passes validation', () => {
|
||||
fc.assert(
|
||||
fc.property(anyValidEntity, (entity) => {
|
||||
const roundTripped = JSON.parse(JSON.stringify(entity));
|
||||
// All attributes preserved
|
||||
expect(roundTripped).toEqual(entity);
|
||||
// Still passes validation
|
||||
const result = validateEntity(roundTripped);
|
||||
expect(result.valid).toBe(true);
|
||||
}),
|
||||
{ numRuns: 100 }
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
* Validates: Requirements 2.1, 8.2
|
||||
*/
|
||||
});
|
||||
|
||||
// --- Property 2: Entity schema validation ---
|
||||
|
||||
describe('Property 2: Entity schema validation', () => {
|
||||
it('accepts any valid entity with all required attributes present and correctly typed', () => {
|
||||
fc.assert(
|
||||
fc.property(anyValidEntity, (entity) => {
|
||||
const result = validateEntity(entity);
|
||||
expect(result.valid).toBe(true);
|
||||
expect(result.errors).toHaveLength(0);
|
||||
}),
|
||||
{ numRuns: 100 }
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects entities missing required fields', () => {
|
||||
const entityTypes = ['person', 'experience', 'skill', 'organization', 'project', 'certification', 'tandem'] as const;
|
||||
fc.assert(
|
||||
fc.property(
|
||||
fc.constantFrom(...entityTypes),
|
||||
kebabCaseId,
|
||||
isoDate,
|
||||
isoDate,
|
||||
(type, id, created, modified) => {
|
||||
// Entity with only header fields — missing type-specific required fields
|
||||
const entity = { id, type, created, modified };
|
||||
const result = validateEntity(entity);
|
||||
// Person needs name, experience needs title/org/start, etc.
|
||||
// All types have additional required fields beyond the header
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.errors.length).toBeGreaterThan(0);
|
||||
}
|
||||
),
|
||||
{ numRuns: 100 }
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
* Validates: Requirements 1.1, 1.3
|
||||
*/
|
||||
});
|
||||
|
||||
// --- Property 3: Temporal consistency ---
|
||||
|
||||
describe('Property 3: Temporal consistency', () => {
|
||||
it('validation passes when start <= end for time-bound entities', () => {
|
||||
fc.assert(
|
||||
fc.property(
|
||||
kebabCaseId,
|
||||
isoDate,
|
||||
isoDate,
|
||||
fc.constantFrom('experience', 'project'),
|
||||
(id, date1, date2, type) => {
|
||||
const [start, end] = date1 <= date2 ? [date1, date2] : [date2, date1];
|
||||
const entity: Record<string, unknown> = {
|
||||
id,
|
||||
type,
|
||||
created: start,
|
||||
modified: start,
|
||||
start,
|
||||
end,
|
||||
};
|
||||
if (type === 'experience') {
|
||||
entity.title = 'Test Role';
|
||||
entity.organization = 'test-org';
|
||||
} else {
|
||||
entity.name = 'Test Project';
|
||||
entity.organization = 'test-org';
|
||||
}
|
||||
const result = validateEntity(entity);
|
||||
// Should not have temporal inconsistency errors
|
||||
const temporalErrors = result.errors.filter(e => e.includes('Temporal inconsistency'));
|
||||
expect(temporalErrors).toHaveLength(0);
|
||||
}
|
||||
),
|
||||
{ numRuns: 100 }
|
||||
);
|
||||
});
|
||||
|
||||
it('validation fails when start > end for time-bound entities', () => {
|
||||
fc.assert(
|
||||
fc.property(
|
||||
kebabCaseId,
|
||||
fc.record({
|
||||
year: fc.integer({ min: 1990, max: 2025 }),
|
||||
month: fc.integer({ min: 1, max: 12 }),
|
||||
}),
|
||||
fc.integer({ min: 1, max: 60 }),
|
||||
fc.constantFrom('experience', 'project'),
|
||||
(id, startParts, monthsLater, type) => {
|
||||
const startStr = `${startParts.year}-${String(startParts.month).padStart(2, '0')}`;
|
||||
// Compute an end date that is strictly before start
|
||||
const endYear = startParts.year - Math.ceil(monthsLater / 12);
|
||||
const endMonth = ((startParts.month - (monthsLater % 12) - 1 + 12) % 12) + 1;
|
||||
const endStr = `${endYear}-${String(endMonth).padStart(2, '0')}`;
|
||||
|
||||
// Only test when end is actually before start (string comparison)
|
||||
fc.pre(endStr < startStr);
|
||||
|
||||
const entity: Record<string, unknown> = {
|
||||
id,
|
||||
type,
|
||||
created: startStr,
|
||||
modified: startStr,
|
||||
start: startStr,
|
||||
end: endStr,
|
||||
};
|
||||
if (type === 'experience') {
|
||||
entity.title = 'Test Role';
|
||||
entity.organization = 'test-org';
|
||||
} else {
|
||||
entity.name = 'Test Project';
|
||||
entity.organization = 'test-org';
|
||||
}
|
||||
const result = validateEntity(entity);
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.errors.some(e => e.includes('Temporal inconsistency'))).toBe(true);
|
||||
}
|
||||
),
|
||||
{ numRuns: 100 }
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
* Validates: Requirements 1.4
|
||||
*/
|
||||
});
|
||||
|
||||
// --- Property 15: File naming convention ---
|
||||
|
||||
describe('Property 15: File naming convention', () => {
|
||||
it('any valid entity ID produces a file name matching <entity-id>.yaml with kebab-case', () => {
|
||||
fc.assert(
|
||||
fc.property(kebabCaseId, (id) => {
|
||||
const fileName = `${id}.yaml`;
|
||||
// File name matches pattern: lowercase letters, numbers, hyphens, ending in .yaml
|
||||
expect(fileName).toMatch(/^[a-z0-9]+(-[a-z0-9]+)*\.yaml$/);
|
||||
// The ID itself is valid kebab-case
|
||||
expect(isKebabCase(id)).toBe(true);
|
||||
}),
|
||||
{ numRuns: 100 }
|
||||
);
|
||||
});
|
||||
|
||||
it('non-kebab-case IDs are rejected by validation', () => {
|
||||
const invalidIds = fc.oneof(
|
||||
fc.constant(''),
|
||||
fc.constant('-leading'),
|
||||
fc.constant('trailing-'),
|
||||
fc.constant('UPPERCASE'),
|
||||
fc.constant('camelCase'),
|
||||
fc.constant('has space'),
|
||||
fc.constant('double--hyphen'),
|
||||
fc.constant('under_score'),
|
||||
fc.stringMatching(/^[A-Z]{1,10}$/),
|
||||
);
|
||||
|
||||
fc.assert(
|
||||
fc.property(invalidIds, isoDate, isoDate, (id, created, modified) => {
|
||||
const entity = {
|
||||
id,
|
||||
type: 'skill',
|
||||
created,
|
||||
modified,
|
||||
name: 'Test',
|
||||
category: 'framework',
|
||||
level: 'expert',
|
||||
};
|
||||
const result = validateEntity(entity);
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.errors.some(e => e.includes('kebab-case') || e.includes('id'))).toBe(true);
|
||||
}),
|
||||
{ numRuns: 100 }
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
* Validates: Requirements 9.2
|
||||
*/
|
||||
});
|
||||
Reference in New Issue
Block a user