fix(frontmatter): normalize depends_on to dependsOn for Rust CLI compatibility
YAML frontmatter from the Rust CLI uses depends_on (snake_case) but the TaskInput schema expects dependsOn (camelCase). Without normalization, Value.Clean() strips the unknown key and dependencies are silently lost. Add step 3.5 in parseFrontmatter: if depends_on is present and dependsOn is not, remap the key before schema validation. If both are present, dependsOn wins (camelCase is canonical).
This commit is contained in:
@@ -115,6 +115,17 @@ export function parseFrontmatter(markdown: string): TaskInputType {
|
|||||||
throw new InvalidInputError('', 'YAML frontmatter must be a mapping (object), not a scalar or array');
|
throw new InvalidInputError('', 'YAML frontmatter must be a mapping (object), not a scalar or array');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Step 3.5: Normalize known snake_case aliases to camelCase.
|
||||||
|
// The YAML format uses snake_case (e.g., `depends_on`) but our schema
|
||||||
|
// expects camelCase (e.g., `dependsOn`). Without this, `Value.Clean()`
|
||||||
|
// would strip the unknown snake_case key and the field would be silently
|
||||||
|
// lost — causing empty dependency lists and broken graphs.
|
||||||
|
const record = parsed as Record<string, unknown>;
|
||||||
|
if ('depends_on' in record && !('dependsOn' in record)) {
|
||||||
|
record.dependsOn = record.depends_on;
|
||||||
|
delete record.depends_on;
|
||||||
|
}
|
||||||
|
|
||||||
// Step 4: Clean — strip unknown properties from untrusted input
|
// Step 4: Clean — strip unknown properties from untrusted input
|
||||||
const cleaned = Value.Clean(TaskInput, parsed);
|
const cleaned = Value.Clean(TaskInput, parsed);
|
||||||
|
|
||||||
|
|||||||
@@ -484,4 +484,44 @@ risk: invalid-value
|
|||||||
const result = parseFrontmatter(input);
|
const result = parseFrontmatter(input);
|
||||||
expect(result.id).toBe('bom-task');
|
expect(result.id).toBe('bom-task');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ─── Snake_case compatibility ──────────────────────────────────────────
|
||||||
|
|
||||||
|
it('normalizes depends_on to dependsOn', () => {
|
||||||
|
const input = `---
|
||||||
|
id: snake-task
|
||||||
|
name: Snake Task
|
||||||
|
depends_on:
|
||||||
|
- task-a
|
||||||
|
- task-b
|
||||||
|
---
|
||||||
|
Body`;
|
||||||
|
const result = parseFrontmatter(input);
|
||||||
|
expect(result.dependsOn).toEqual(['task-a', 'task-b']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('prefers dependsOn when both depends_on and dependsOn are present', () => {
|
||||||
|
const input = `---
|
||||||
|
id: both-task
|
||||||
|
name: Both Task
|
||||||
|
dependsOn:
|
||||||
|
- from-camel
|
||||||
|
depends_on:
|
||||||
|
- from-snake
|
||||||
|
---
|
||||||
|
Body`;
|
||||||
|
const result = parseFrontmatter(input);
|
||||||
|
expect(result.dependsOn).toEqual(['from-camel']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('normalizes depends_on with empty array', () => {
|
||||||
|
const input = `---
|
||||||
|
id: empty-snake
|
||||||
|
name: Empty Snake
|
||||||
|
depends_on: []
|
||||||
|
---
|
||||||
|
Body`;
|
||||||
|
const result = parseFrontmatter(input);
|
||||||
|
expect(result.dependsOn).toEqual([]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
Reference in New Issue
Block a user