Skip to content

Commit cf13436

Browse files
committed
feat: add specific error reasons to parseFrontmatter for missing/unclosed cases
Signed-off-by: leocavalcante <leo@cavalcante.dev>
1 parent e8a93fe commit cf13436

File tree

3 files changed

+30
-8
lines changed

3 files changed

+30
-8
lines changed

src/paths.d.mts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,8 @@ export function retryOnTransientError<T>(
126126
export interface ParseFrontmatterResult {
127127
/** Whether frontmatter was found in the content */
128128
found: boolean
129+
/** Reason for failure when found is false: "missing" if content doesn't start with ---, "unclosed" if closing --- not found */
130+
reason?: "missing" | "unclosed"
129131
/** Parsed key-value pairs from the frontmatter */
130132
fields: Record<string, string>
131133
/** Character index where the frontmatter ends (after closing ---\n) */

src/paths.mjs

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -201,18 +201,18 @@ export async function retryOnTransientError(fn, options = {}) {
201201
* Expects frontmatter to be delimited by --- at the start of the file.
202202
*
203203
* @param {string} content - The file content to parse
204-
* @returns {{ found: boolean, fields: Record<string, string>, endIndex: number }} Parse result
204+
* @returns {{ found: boolean, reason?: "missing" | "unclosed", fields: Record<string, string>, endIndex: number }} Parse result
205205
*/
206206
export function parseFrontmatter(content) {
207207
// Frontmatter must start at the beginning of the file
208208
if (!content.startsWith("---")) {
209-
return { found: false, fields: {}, endIndex: 0 }
209+
return { found: false, reason: "missing", fields: {}, endIndex: 0 }
210210
}
211211

212212
// Find the closing ---
213213
const endMatch = content.indexOf("\n---", 3)
214214
if (endMatch === -1) {
215-
return { found: false, fields: {}, endIndex: 0 }
215+
return { found: false, reason: "unclosed", fields: {}, endIndex: 0 }
216216
}
217217

218218
// Extract frontmatter content (between the --- delimiters)
@@ -271,9 +271,13 @@ export function validateAgentContent(content) {
271271
// Check for YAML frontmatter
272272
const frontmatter = parseFrontmatter(content)
273273
if (!frontmatter.found) {
274+
const errorMessage =
275+
frontmatter.reason === "unclosed"
276+
? "Unclosed YAML frontmatter (missing closing ---)"
277+
: "File missing YAML frontmatter (must start with ---)"
274278
return {
275279
valid: false,
276-
error: "File missing YAML frontmatter (must start with ---)",
280+
error: errorMessage,
277281
}
278282
}
279283

tests/paths.test.ts

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -509,16 +509,18 @@ describe("paths.mjs exports", () => {
509509
})
510510

511511
describe("parseFrontmatter", () => {
512-
it("should return found: false when content does not start with ---", () => {
512+
it("should return found: false with reason 'missing' when content does not start with ---", () => {
513513
const result = parseFrontmatter("# No frontmatter")
514514
expect(result.found).toBe(false)
515+
expect(result.reason).toBe("missing")
515516
expect(result.fields).toEqual({})
516517
expect(result.endIndex).toBe(0)
517518
})
518519

519-
it("should return found: false when closing --- is missing", () => {
520+
it("should return found: false with reason 'unclosed' when closing --- is missing", () => {
520521
const result = parseFrontmatter("---\nversion: 1.0\nno closing delimiter")
521522
expect(result.found).toBe(false)
523+
expect(result.reason).toBe("unclosed")
522524
expect(result.fields).toEqual({})
523525
expect(result.endIndex).toBe(0)
524526
})
@@ -531,6 +533,7 @@ requires: opencode
531533
# Content`
532534
const result = parseFrontmatter(content)
533535
expect(result.found).toBe(true)
536+
expect(result.reason).toBeUndefined()
534537
expect(result.fields.version).toBe("1.0")
535538
expect(result.fields.requires).toBe("opencode")
536539
})
@@ -641,15 +644,28 @@ The agent can process multiple items efficiently.
641644
expect(result.error).toContain(`minimum ${MIN_CONTENT_LENGTH}`)
642645
})
643646

644-
it("should return valid: false when frontmatter is missing", () => {
647+
it("should return valid: false with specific error when frontmatter is missing", () => {
645648
const content =
646649
"# No Frontmatter Agent\n\nThis agent has no frontmatter but is an agent for tasks.".padEnd(
647650
MIN_CONTENT_LENGTH + 10,
648651
" ",
649652
)
650653
const result = validateAgentContent(content)
651654
expect(result.valid).toBe(false)
652-
expect(result.error).toContain("YAML frontmatter")
655+
expect(result.error).toBe("File missing YAML frontmatter (must start with ---)")
656+
})
657+
658+
it("should return valid: false with specific error when frontmatter is unclosed", () => {
659+
const content = `---
660+
version: 1.0
661+
requires: opencode
662+
This file has no closing frontmatter delimiter.
663+
# Test Agent
664+
This is a test agent that handles various tasks.
665+
`.padEnd(MIN_CONTENT_LENGTH + 50, " ")
666+
const result = validateAgentContent(content)
667+
expect(result.valid).toBe(false)
668+
expect(result.error).toBe("Unclosed YAML frontmatter (missing closing ---)")
653669
})
654670

655671
it("should return valid: false when version field is missing", () => {

0 commit comments

Comments
 (0)