fix(core): restore validation errors compatibility and reject malformed collections

This commit is contained in:
Lum1104
2026-03-26 22:09:38 +08:00
Unverified
parent 835377b968
commit d79e3c862c
2 changed files with 64 additions and 2 deletions
@@ -593,6 +593,7 @@ describe("permissive validation", () => {
const result = validateGraph(validGraph);
expect(result.success).toBe(true);
expect(result.issues).toEqual([]);
expect(result.errors).toBeUndefined();
});
it("auto-corrects and loads graph that would have failed strict validation", () => {
@@ -634,4 +635,30 @@ describe("permissive validation", () => {
expect.objectContaining({ level: "auto-corrected", category: "type-coercion" })
);
});
it("returns fatal when edges is present but not an array", () => {
const graph = structuredClone(validGraph) as any;
graph.edges = { source: "node-1", target: "node-1" };
const result = validateGraph(graph);
expect(result.success).toBe(false);
expect(result.fatal).toContain('"edges" must be an array');
expect(result.errors).toContain('"edges" must be an array when present');
expect(result.issues).toContainEqual(
expect.objectContaining({
level: "fatal",
category: "invalid-collection",
path: "edges",
})
);
});
it("preserves deprecated errors for dropped-item callers", () => {
const graph = structuredClone(validGraph);
graph.edges[0].target = "non-existent-node";
const result = validateGraph(graph);
expect(result.success).toBe(true);
expect(result.errors).toContain('edges[0]: target "non-existent-node" does not exist in nodes — removed');
});
});
@@ -328,10 +328,27 @@ export interface GraphIssue {
export interface ValidationResult {
success: boolean;
data?: z.infer<typeof KnowledgeGraphSchema>;
/** @deprecated Use issues/fatal instead */
errors?: string[];
issues: GraphIssue[];
fatal?: string;
}
function buildInvalidCollectionIssue(name: string): GraphIssue {
return {
level: "fatal",
category: "invalid-collection",
message: `"${name}" must be an array when present`,
path: name,
};
}
function buildErrors(issues: GraphIssue[], fatal?: string): string[] | undefined {
const messages = issues.map((issue) => issue.message);
if (fatal && !messages.includes(fatal)) messages.unshift(fatal);
return messages.length > 0 ? messages : undefined;
}
export function normalizeGraph(data: unknown): unknown {
if (typeof data !== "object" || data === null) return data;
@@ -372,7 +389,8 @@ export function normalizeGraph(data: unknown): unknown {
export function validateGraph(data: unknown): ValidationResult {
// Tier 4: Fatal — not even an object
if (typeof data !== "object" || data === null) {
return { success: false, issues: [], fatal: "Invalid input: not an object" };
const fatal = "Invalid input: not an object";
return { success: false, issues: [], fatal, errors: buildErrors([], fatal) };
}
const raw = data as Record<string, unknown>;
@@ -386,11 +404,27 @@ export function validateGraph(data: unknown): ValidationResult {
// Tier 2: Auto-fix defaults and coercion
const { data: fixed, issues } = autoFixGraph(normalized);
// Tier 4: Fatal — malformed top-level collections
const requiredCollections = ["nodes", "edges", "layers", "tour"] as const;
for (const collection of requiredCollections) {
if (collection in fixed && fixed[collection] !== undefined && !Array.isArray(fixed[collection])) {
const issue = buildInvalidCollectionIssue(collection);
issues.push(issue);
return {
success: false,
errors: buildErrors(issues, issue.message),
issues,
fatal: issue.message,
};
}
}
// Tier 4: Fatal — missing project metadata
const projectResult = ProjectMetaSchema.safeParse(fixed.project);
if (!projectResult.success) {
return {
success: false,
errors: buildErrors(issues, "Missing or invalid project metadata"),
issues,
fatal: "Missing or invalid project metadata",
};
@@ -420,6 +454,7 @@ export function validateGraph(data: unknown): ValidationResult {
if (validNodes.length === 0) {
return {
success: false,
errors: buildErrors(issues, "No valid nodes found in knowledge graph"),
issues,
fatal: "No valid nodes found in knowledge graph",
};
@@ -514,5 +549,5 @@ export function validateGraph(data: unknown): ValidationResult {
tour: validTour,
};
return { success: true, data: graph, issues };
return { success: true, data: graph, issues, errors: buildErrors(issues) };
}