From 3416270a2e78111218dd280ef2d816e08cac5b9f Mon Sep 17 00:00:00 2001 From: holny Date: Fri, 25 Sep 2026 08:32:30 +0800 Subject: [PATCH] fix(core): bypass gray-matter content cache when parsing frontmatter gray-matter populates its module-global content cache before parsing, so a failed YAML parse leaves a poisoned entry and every later parse of the same content returns it without throwing. In a long-lived serve process the first config load could sanitize an unquoted-colon frontmatter correctly, but every later instance init replayed the poisoned entry, never threw, and the skill was silently dropped by the empty-data check upstream. Passing an options object bypasses the cache read and write (gray-matter only caches when called with no options), making every parse deterministic and letting the sanitize fallback recover on each load. Regression test covers the repeat-parse recovery, the repeated throw for genuinely unparseable content, and plain content. Fixes #51218 --- packages/core/src/config/markdown.ts | 8 +++-- packages/core/test/config/markdown.test.ts | 35 ++++++++++++++++++++++ 2 files changed, 41 insertions(+), 2 deletions(-) create mode 100644 packages/core/test/config/markdown.test.ts diff --git a/packages/core/src/config/markdown.ts b/packages/core/src/config/markdown.ts index 4887a502db7b..8093ff7e7354 100644 --- a/packages/core/src/config/markdown.ts +++ b/packages/core/src/config/markdown.ts @@ -2,10 +2,14 @@ export * as ConfigMarkdown from "./markdown.js" import matter from "gray-matter" export function parse(content: string) { + // Passing options bypasses gray-matter's module-global content cache, which + // it populates before parsing: a failed YAML parse poisons the entry and + // every later parse of the same content replays it without throwing, so the + // sanitize fallback below never runs. Upstream: jonschlinkert/gray-matter#166. try { - return matter(content) + return matter(content, {}) } catch { - return matter(sanitize(content)) + return matter(sanitize(content), {}) } } diff --git a/packages/core/test/config/markdown.test.ts b/packages/core/test/config/markdown.test.ts new file mode 100644 index 000000000000..d050c5638695 --- /dev/null +++ b/packages/core/test/config/markdown.test.ts @@ -0,0 +1,35 @@ +import { describe, expect, test } from "bun:test" +import { ConfigMarkdown } from "@opencode/core/config/markdown" + +const invalidYaml = `--- +description: Use when the user needs to crawl pages. Keywords: crawl, scrape +--- +body` + +describe("ConfigMarkdown.parse", () => { + test("recovers unquoted-colon frontmatter via the sanitize fallback", () => { + const parsed = ConfigMarkdown.parse(invalidYaml) + expect(parsed.data.description).toBe("Use when the user needs to crawl pages. Keywords: crawl, scrape") + expect(parsed.content.trim()).toBe("body") + }) + + test("recovers the same content again after a previous failed parse", () => { + // gray-matter caches by content before parsing; a poisoned entry used to + // make every later parse of the same text return silently without data. + expect(() => ConfigMarkdown.parse("---\ndescription: [unclosed\n---\nbody")).toThrow() + const parsed = ConfigMarkdown.parse(invalidYaml) + expect(parsed.data.description).toBe("Use when the user needs to crawl pages. Keywords: crawl, scrape") + }) + + test("keeps throwing for the same unparseable content on every call", () => { + const input = "---\ndescription: [unclosed\n---\nbody" + expect(() => ConfigMarkdown.parse(input)).toThrow() + expect(() => ConfigMarkdown.parse(input)).toThrow() + }) + + test("parses plain content without frontmatter", () => { + const parsed = ConfigMarkdown.parse("just body") + expect(parsed.content).toBe("just body") + expect(parsed.data).toEqual({}) + }) +}) \ No newline at end of file