Merge schema/input-schemas: TaskInput and DependencyEdge schemas with Nullable helper

This commit is contained in:
2026-04-27 11:04:07 +00:00
3 changed files with 265 additions and 9 deletions

View File

@@ -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;