Merge schema/graph-schemas: node/edge attribute schemas, SerializedGraph generic factory
# Conflicts: # test/schema.test.ts
This commit is contained in:
@@ -9,6 +9,13 @@ import {
|
||||
TaskStatusEnum,
|
||||
Nullable,
|
||||
} from '../src/schema/enums.js';
|
||||
import {
|
||||
TaskGraphNodeAttributes,
|
||||
TaskGraphNodeAttributesUpdate,
|
||||
TaskGraphEdgeAttributes,
|
||||
TaskGraphSerialized,
|
||||
SerializedGraph,
|
||||
} from '../src/schema/graph.js';
|
||||
|
||||
describe('Enum schemas', () => {
|
||||
// --- TaskScopeEnum ---
|
||||
@@ -545,8 +552,6 @@ describe('EvConfig schema', () => {
|
||||
});
|
||||
|
||||
it('rejects unknown properties (additionalProperties)', () => {
|
||||
// TypeBox Object by default allows additional properties through Check
|
||||
// This test documents behavior — strict additionalProperties would need Type.Object({...}, { additionalProperties: false })
|
||||
expect(Value.Check(EvConfigSchema, { unknownField: 42 })).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -668,4 +673,203 @@ describe('Result type alias correctness (compile-time)', () => {
|
||||
};
|
||||
expect(result.unspecified).toEqual([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Graph schema tests
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('TaskGraphNodeAttributes', () => {
|
||||
it('accepts a valid node with only required fields', () => {
|
||||
const node = { name: 'my-task' };
|
||||
expect(Value.Check(TaskGraphNodeAttributes, node)).toBe(true);
|
||||
});
|
||||
|
||||
it('accepts a node with all optional categorical enum fields', () => {
|
||||
const node = {
|
||||
name: 'my-task',
|
||||
scope: 'narrow',
|
||||
risk: 'high',
|
||||
impact: 'component',
|
||||
level: 'implementation',
|
||||
priority: 'critical',
|
||||
status: 'in-progress',
|
||||
};
|
||||
expect(Value.Check(TaskGraphNodeAttributes, node)).toBe(true);
|
||||
});
|
||||
|
||||
it('rejects a node missing the required name field', () => {
|
||||
const node = { scope: 'narrow' };
|
||||
expect(Value.Check(TaskGraphNodeAttributes, node)).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects invalid categorical enum values', () => {
|
||||
expect(Value.Check(TaskGraphNodeAttributes, { name: 't', scope: 'invalid' })).toBe(false);
|
||||
expect(Value.Check(TaskGraphNodeAttributes, { name: 't', risk: 'unknown' })).toBe(false);
|
||||
expect(Value.Check(TaskGraphNodeAttributes, { name: 't', status: 'done' })).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects non-object values', () => {
|
||||
expect(Value.Check(TaskGraphNodeAttributes, null)).toBe(false);
|
||||
expect(Value.Check(TaskGraphNodeAttributes, 'string')).toBe(false);
|
||||
expect(Value.Check(TaskGraphNodeAttributes, 42)).toBe(false);
|
||||
});
|
||||
|
||||
it('does NOT carry tags/assignee/due fields (analysis-only)', () => {
|
||||
const schema = TaskGraphNodeAttributes as any;
|
||||
const props = schema?.properties;
|
||||
expect(props).toBeDefined();
|
||||
expect(props.tags).toBeUndefined();
|
||||
expect(props.assignee).toBeUndefined();
|
||||
expect(props.due).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('TaskGraphNodeAttributesUpdate', () => {
|
||||
it('accepts an empty object (all fields optional)', () => {
|
||||
expect(Value.Check(TaskGraphNodeAttributesUpdate, {})).toBe(true);
|
||||
});
|
||||
|
||||
it('accepts a partial object with only name', () => {
|
||||
expect(Value.Check(TaskGraphNodeAttributesUpdate, { name: 't' })).toBe(true);
|
||||
});
|
||||
|
||||
it('accepts a partial object with only optional categorical fields', () => {
|
||||
expect(Value.Check(TaskGraphNodeAttributesUpdate, { risk: 'low', status: 'pending' })).toBe(true);
|
||||
});
|
||||
|
||||
it('rejects invalid categorical enum values in partial', () => {
|
||||
expect(Value.Check(TaskGraphNodeAttributesUpdate, { risk: 'invalid' })).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('TaskGraphEdgeAttributes', () => {
|
||||
it('accepts an empty object (qualityRetention is optional)', () => {
|
||||
expect(Value.Check(TaskGraphEdgeAttributes, {})).toBe(true);
|
||||
});
|
||||
|
||||
it('accepts a valid qualityRetention number', () => {
|
||||
expect(Value.Check(TaskGraphEdgeAttributes, { qualityRetention: 0.9 })).toBe(true);
|
||||
expect(Value.Check(TaskGraphEdgeAttributes, { qualityRetention: 0.0 })).toBe(true);
|
||||
expect(Value.Check(TaskGraphEdgeAttributes, { qualityRetention: 1.0 })).toBe(true);
|
||||
});
|
||||
|
||||
it('rejects non-number qualityRetention', () => {
|
||||
expect(Value.Check(TaskGraphEdgeAttributes, { qualityRetention: 'high' })).toBe(false);
|
||||
expect(Value.Check(TaskGraphEdgeAttributes, { qualityRetention: null })).toBe(false);
|
||||
expect(Value.Check(TaskGraphEdgeAttributes, { qualityRetention: true })).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('SerializedGraph generic factory', () => {
|
||||
it('produces a schema that validates graphology JSON format', () => {
|
||||
const graph = {
|
||||
attributes: {},
|
||||
options: { type: 'directed' as const, multi: false, allowSelfLoops: false },
|
||||
nodes: [
|
||||
{ key: 'task-a', attributes: { name: 'Task A' } },
|
||||
{ key: 'task-b', attributes: { name: 'Task B', scope: 'narrow' as const } },
|
||||
],
|
||||
edges: [
|
||||
{ key: 'edge-1', source: 'task-a', target: 'task-b', attributes: { qualityRetention: 0.9 } },
|
||||
],
|
||||
};
|
||||
expect(Value.Check(TaskGraphSerialized, graph)).toBe(true);
|
||||
});
|
||||
|
||||
it('accepts empty nodes and edges arrays', () => {
|
||||
const graph = {
|
||||
attributes: {},
|
||||
options: { type: 'directed' as const, multi: false, allowSelfLoops: false },
|
||||
nodes: [],
|
||||
edges: [],
|
||||
};
|
||||
expect(Value.Check(TaskGraphSerialized, graph)).toBe(true);
|
||||
});
|
||||
|
||||
it('rejects wrong options.type', () => {
|
||||
const graph = {
|
||||
attributes: {},
|
||||
options: { type: 'undirected', multi: false, allowSelfLoops: false },
|
||||
nodes: [],
|
||||
edges: [],
|
||||
};
|
||||
expect(Value.Check(TaskGraphSerialized, graph)).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects wrong options.multi', () => {
|
||||
const graph = {
|
||||
attributes: {},
|
||||
options: { type: 'directed' as const, multi: true, allowSelfLoops: false },
|
||||
nodes: [],
|
||||
edges: [],
|
||||
};
|
||||
expect(Value.Check(TaskGraphSerialized, graph)).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects wrong options.allowSelfLoops', () => {
|
||||
const graph = {
|
||||
attributes: {},
|
||||
options: { type: 'directed' as const, multi: false, allowSelfLoops: true },
|
||||
nodes: [],
|
||||
edges: [],
|
||||
};
|
||||
expect(Value.Check(TaskGraphSerialized, graph)).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects node with invalid attributes', () => {
|
||||
const graph = {
|
||||
attributes: {},
|
||||
options: { type: 'directed' as const, multi: false, allowSelfLoops: false },
|
||||
nodes: [{ key: 'bad', attributes: { scope: 'invalid' } }],
|
||||
edges: [],
|
||||
};
|
||||
expect(Value.Check(TaskGraphSerialized, graph)).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects edge with invalid source/target', () => {
|
||||
const graph = {
|
||||
attributes: {},
|
||||
options: { type: 'directed' as const, multi: false, allowSelfLoops: false },
|
||||
nodes: [],
|
||||
edges: [{ key: 'e1', source: 42, target: 'b', attributes: {} }],
|
||||
};
|
||||
expect(Value.Check(TaskGraphSerialized, graph)).toBe(false);
|
||||
});
|
||||
|
||||
it('has no schema version field', () => {
|
||||
const schema = TaskGraphSerialized as any;
|
||||
const props = schema?.properties;
|
||||
expect(props).toBeDefined();
|
||||
expect(props.version).toBeUndefined();
|
||||
expect(props.schemaVersion).toBeUndefined();
|
||||
});
|
||||
|
||||
it('can be composed with different attribute schemas (is generic)', () => {
|
||||
const CustomGraph = SerializedGraph(
|
||||
TaskGraphNodeAttributes,
|
||||
TaskGraphEdgeAttributes,
|
||||
{ type: 'object', properties: { label: { type: 'string' } } } as any,
|
||||
);
|
||||
expect(CustomGraph).toBeDefined();
|
||||
expect((CustomGraph as any).properties).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('TaskGraphSerialized type alias (compile-time)', () => {
|
||||
it('type alias resolves correctly for a valid graph', () => {
|
||||
const graph: TaskGraphSerialized = {
|
||||
attributes: {},
|
||||
options: { type: 'directed', multi: false, allowSelfLoops: false },
|
||||
nodes: [{ key: 'a', attributes: { name: 'A' } }],
|
||||
edges: [],
|
||||
};
|
||||
expect(graph.options.type).toBe('directed');
|
||||
});
|
||||
});
|
||||
|
||||
// Import graph type aliases for compile-time verification
|
||||
type TaskGraphNodeAttributes = import('../src/schema/graph.js').TaskGraphNodeAttributes;
|
||||
type TaskGraphNodeAttributesUpdate = import('../src/schema/graph.js').TaskGraphNodeAttributesUpdate;
|
||||
type TaskGraphEdgeAttributes = import('../src/schema/graph.js').TaskGraphEdgeAttributes;
|
||||
type TaskGraphSerialized = import('../src/schema/graph.js').TaskGraphSerialized;
|
||||
|
||||
Reference in New Issue
Block a user