Merge schema/input-schemas: TaskInput and DependencyEdge schemas with Nullable helper
This commit is contained in:
@@ -176,6 +176,201 @@ describe('Type alias correctness (compile-time)', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// --- TaskInput and DependencyEdge tests ---
|
||||
|
||||
import { TaskInput as TaskInputSchema, DependencyEdge as DependencyEdgeSchema } from '../src/schema/task.js';
|
||||
|
||||
// Type alias imports for compile-time verification
|
||||
type TaskInputType = import('../src/schema/task.js').TaskInput;
|
||||
type DependencyEdgeType = import('../src/schema/task.js').DependencyEdge;
|
||||
|
||||
describe('TaskInput schema', () => {
|
||||
const minimal = { id: 'task-1', name: 'My Task', dependsOn: [] };
|
||||
|
||||
it('accepts minimal valid input (id, name, dependsOn only)', () => {
|
||||
expect(Value.Check(TaskInputSchema, minimal)).toBe(true);
|
||||
});
|
||||
|
||||
it('accepts dependsOn with multiple strings', () => {
|
||||
expect(Value.Check(TaskInputSchema, { ...minimal, dependsOn: ['a', 'b', 'c'] })).toBe(true);
|
||||
});
|
||||
|
||||
// --- Categorical fields: Optional + Nullable ---
|
||||
|
||||
it('accepts categorical field set to a valid enum value', () => {
|
||||
expect(Value.Check(TaskInputSchema, { ...minimal, status: 'pending' })).toBe(true);
|
||||
expect(Value.Check(TaskInputSchema, { ...minimal, scope: 'narrow' })).toBe(true);
|
||||
expect(Value.Check(TaskInputSchema, { ...minimal, risk: 'medium' })).toBe(true);
|
||||
expect(Value.Check(TaskInputSchema, { ...minimal, impact: 'component' })).toBe(true);
|
||||
expect(Value.Check(TaskInputSchema, { ...minimal, level: 'implementation' })).toBe(true);
|
||||
expect(Value.Check(TaskInputSchema, { ...minimal, priority: 'high' })).toBe(true);
|
||||
});
|
||||
|
||||
it('accepts categorical field set to null (explicit null in YAML)', () => {
|
||||
expect(Value.Check(TaskInputSchema, { ...minimal, status: null })).toBe(true);
|
||||
expect(Value.Check(TaskInputSchema, { ...minimal, scope: null })).toBe(true);
|
||||
expect(Value.Check(TaskInputSchema, { ...minimal, risk: null })).toBe(true);
|
||||
expect(Value.Check(TaskInputSchema, { ...minimal, impact: null })).toBe(true);
|
||||
expect(Value.Check(TaskInputSchema, { ...minimal, level: null })).toBe(true);
|
||||
expect(Value.Check(TaskInputSchema, { ...minimal, priority: null })).toBe(true);
|
||||
});
|
||||
|
||||
it('accepts categorical field absent (undefined / missing key)', () => {
|
||||
// minimal has no categorical fields — already tested above
|
||||
expect(Value.Check(TaskInputSchema, minimal)).toBe(true);
|
||||
});
|
||||
|
||||
it('rejects categorical field with invalid enum value', () => {
|
||||
expect(Value.Check(TaskInputSchema, { ...minimal, status: 'unknown' })).toBe(false);
|
||||
expect(Value.Check(TaskInputSchema, { ...minimal, scope: 'invalid' })).toBe(false);
|
||||
expect(Value.Check(TaskInputSchema, { ...minimal, risk: 'bad' })).toBe(false);
|
||||
});
|
||||
|
||||
// --- Metadata fields: Optional + Nullable ---
|
||||
|
||||
it('accepts metadata fields with valid values', () => {
|
||||
expect(Value.Check(TaskInputSchema, { ...minimal, tags: ['a', 'b'] })).toBe(true);
|
||||
expect(Value.Check(TaskInputSchema, { ...minimal, assignee: 'alice' })).toBe(true);
|
||||
expect(Value.Check(TaskInputSchema, { ...minimal, due: '2026-05-01' })).toBe(true);
|
||||
expect(Value.Check(TaskInputSchema, { ...minimal, created: '2026-04-20' })).toBe(true);
|
||||
expect(Value.Check(TaskInputSchema, { ...minimal, modified: '2026-04-25' })).toBe(true);
|
||||
});
|
||||
|
||||
it('accepts metadata fields set to null', () => {
|
||||
expect(Value.Check(TaskInputSchema, { ...minimal, assignee: null })).toBe(true);
|
||||
expect(Value.Check(TaskInputSchema, { ...minimal, due: null })).toBe(true);
|
||||
expect(Value.Check(TaskInputSchema, { ...minimal, created: null })).toBe(true);
|
||||
expect(Value.Check(TaskInputSchema, { ...minimal, modified: null })).toBe(true);
|
||||
});
|
||||
|
||||
it('accepts tags field absent', () => {
|
||||
expect(Value.Check(TaskInputSchema, { ...minimal })).toBe(true);
|
||||
});
|
||||
|
||||
// --- Required fields ---
|
||||
|
||||
it('rejects missing id', () => {
|
||||
expect(Value.Check(TaskInputSchema, { name: 'X', dependsOn: [] })).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects missing name', () => {
|
||||
expect(Value.Check(TaskInputSchema, { id: '1', dependsOn: [] })).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects missing dependsOn', () => {
|
||||
expect(Value.Check(TaskInputSchema, { id: '1', name: 'X' })).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects wrong types for required fields', () => {
|
||||
expect(Value.Check(TaskInputSchema, { id: 1, name: 'X', dependsOn: [] })).toBe(false);
|
||||
expect(Value.Check(TaskInputSchema, { id: '1', name: 2, dependsOn: [] })).toBe(false);
|
||||
expect(Value.Check(TaskInputSchema, { id: '1', name: 'X', dependsOn: 'not-array' })).toBe(false);
|
||||
});
|
||||
|
||||
// --- Full valid input ---
|
||||
|
||||
it('accepts fully populated valid input', () => {
|
||||
const full = {
|
||||
id: 'task-1',
|
||||
name: 'My Task',
|
||||
dependsOn: ['task-0'],
|
||||
status: 'in-progress',
|
||||
scope: 'narrow',
|
||||
risk: 'medium',
|
||||
impact: 'component',
|
||||
level: 'implementation',
|
||||
priority: 'high',
|
||||
tags: ['backend', 'api'],
|
||||
assignee: 'bob',
|
||||
due: '2026-06-01',
|
||||
created: '2026-04-01',
|
||||
modified: '2026-04-27',
|
||||
};
|
||||
expect(Value.Check(TaskInputSchema, full)).toBe(true);
|
||||
});
|
||||
|
||||
it('produces structured errors for invalid input', () => {
|
||||
const errors = [...Value.Errors(TaskInputSchema, { id: 1, name: 2, dependsOn: 'nope' })];
|
||||
expect(errors.length).toBeGreaterThan(0);
|
||||
const paths = errors.map(e => e.path);
|
||||
expect(paths).toContain('/id');
|
||||
expect(paths).toContain('/name');
|
||||
expect(paths).toContain('/dependsOn');
|
||||
});
|
||||
});
|
||||
|
||||
describe('DependencyEdge schema', () => {
|
||||
const minimal = { from: 'task-a', to: 'task-b' };
|
||||
|
||||
it('accepts minimal valid edge (from, to only)', () => {
|
||||
expect(Value.Check(DependencyEdgeSchema, minimal)).toBe(true);
|
||||
});
|
||||
|
||||
it('accepts edge with qualityRetention', () => {
|
||||
expect(Value.Check(DependencyEdgeSchema, { ...minimal, qualityRetention: 0.9 })).toBe(true);
|
||||
});
|
||||
|
||||
it('accepts qualityRetention at boundary values 0.0 and 1.0', () => {
|
||||
expect(Value.Check(DependencyEdgeSchema, { ...minimal, qualityRetention: 0.0 })).toBe(true);
|
||||
expect(Value.Check(DependencyEdgeSchema, { ...minimal, qualityRetention: 1.0 })).toBe(true);
|
||||
});
|
||||
|
||||
it('rejects missing from field', () => {
|
||||
expect(Value.Check(DependencyEdgeSchema, { to: 'task-b' })).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects missing to field', () => {
|
||||
expect(Value.Check(DependencyEdgeSchema, { from: 'task-a' })).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects wrong types for from/to', () => {
|
||||
expect(Value.Check(DependencyEdgeSchema, { from: 1, to: 'task-b' })).toBe(false);
|
||||
expect(Value.Check(DependencyEdgeSchema, { from: 'task-a', to: 2 })).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects wrong type for qualityRetention', () => {
|
||||
expect(Value.Check(DependencyEdgeSchema, { ...minimal, qualityRetention: 'high' })).toBe(false);
|
||||
});
|
||||
|
||||
it('allows qualityRetention absent (optional)', () => {
|
||||
expect(Value.Check(DependencyEdgeSchema, minimal)).toBe(true);
|
||||
});
|
||||
|
||||
it('produces structured errors for invalid input', () => {
|
||||
const errors = [...Value.Errors(DependencyEdgeSchema, { from: 1, to: 2 })];
|
||||
expect(errors.length).toBeGreaterThan(0);
|
||||
const paths = errors.map(e => e.path);
|
||||
expect(paths).toContain('/from');
|
||||
expect(paths).toContain('/to');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Type alias correctness — TaskInput and DependencyEdge (compile-time)', () => {
|
||||
it('TaskInput type accepts a valid object', () => {
|
||||
const input: TaskInputType = { id: 't1', name: 'T', dependsOn: [], risk: null };
|
||||
expect(input.id).toBe('t1');
|
||||
});
|
||||
|
||||
it('DependencyEdge type accepts a valid object', () => {
|
||||
const edge: DependencyEdgeType = { from: 'a', to: 'b', qualityRetention: 0.8 };
|
||||
expect(edge.from).toBe('a');
|
||||
});
|
||||
|
||||
it('DependencyEdge type works without qualityRetention', () => {
|
||||
const edge: DependencyEdgeType = { from: 'a', to: 'b' };
|
||||
expect(edge.qualityRetention).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
// Re-export Nullable from task.ts to verify the re-export works
|
||||
import { Nullable as NullableFromTask } from '../src/schema/task.js';
|
||||
|
||||
describe('Nullable re-export from task.ts', () => {
|
||||
it('is the same function as from enums.ts', () => {
|
||||
expect(NullableFromTask).toBe(Nullable);
|
||||
});
|
||||
});
|
||||
|
||||
// Intentionally import type aliases to verify they exist at compile time
|
||||
type TaskScope = import('../src/schema/enums.js').TaskScope;
|
||||
type TaskRisk = import('../src/schema/enums.js').TaskRisk;
|
||||
|
||||
Reference in New Issue
Block a user