| import { describe, expect, it } from "vitest"; |
| import { |
| parseFrontmatter, |
| resolveOpenClawMetadata, |
| resolveHookInvocationPolicy, |
| } from "./frontmatter.js"; |
|
|
| describe("parseFrontmatter", () => { |
| it("parses single-line key-value pairs", () => { |
| const content = `--- |
| name: test-hook |
| description: "A test hook" |
| homepage: https://example.com |
| --- |
| |
| # Test Hook |
| `; |
| const result = parseFrontmatter(content); |
| expect(result.name).toBe("test-hook"); |
| expect(result.description).toBe("A test hook"); |
| expect(result.homepage).toBe("https://example.com"); |
| }); |
|
|
| it("handles missing frontmatter", () => { |
| const content = "# Just a markdown file"; |
| const result = parseFrontmatter(content); |
| expect(result).toEqual({}); |
| }); |
|
|
| it("handles unclosed frontmatter", () => { |
| const content = `--- |
| name: broken |
| `; |
| const result = parseFrontmatter(content); |
| expect(result).toEqual({}); |
| }); |
|
|
| it("parses multi-line metadata block with indented JSON", () => { |
| const content = `--- |
| name: session-memory |
| description: "Save session context" |
| metadata: |
| { |
| "openclaw": { |
| "emoji": "💾", |
| "events": ["command:new"] |
| } |
| } |
| --- |
| |
| # Session Memory Hook |
| `; |
| const result = parseFrontmatter(content); |
| expect(result.name).toBe("session-memory"); |
| expect(result.description).toBe("Save session context"); |
| expect(result.metadata).toBeDefined(); |
| expect(typeof result.metadata).toBe("string"); |
|
|
| |
| const parsed = JSON.parse(result.metadata); |
| expect(parsed.openclaw.emoji).toBe("💾"); |
| expect(parsed.openclaw.events).toEqual(["command:new"]); |
| }); |
|
|
| it("parses multi-line metadata with complex nested structure", () => { |
| const content = `--- |
| name: command-logger |
| description: "Log all command events" |
| metadata: |
| { |
| "openclaw": |
| { |
| "emoji": "📝", |
| "events": ["command"], |
| "requires": { "config": ["workspace.dir"] }, |
| "install": [{ "id": "bundled", "kind": "bundled", "label": "Bundled" }] |
| } |
| } |
| --- |
| `; |
| const result = parseFrontmatter(content); |
| expect(result.name).toBe("command-logger"); |
| expect(result.metadata).toBeDefined(); |
|
|
| const parsed = JSON.parse(result.metadata); |
| expect(parsed.openclaw.emoji).toBe("📝"); |
| expect(parsed.openclaw.events).toEqual(["command"]); |
| expect(parsed.openclaw.requires.config).toEqual(["workspace.dir"]); |
| expect(parsed.openclaw.install[0].kind).toBe("bundled"); |
| }); |
|
|
| it("handles single-line metadata (inline JSON)", () => { |
| const content = `--- |
| name: simple-hook |
| metadata: {"openclaw": {"events": ["test"]}} |
| --- |
| `; |
| const result = parseFrontmatter(content); |
| expect(result.name).toBe("simple-hook"); |
| expect(result.metadata).toBe('{"openclaw": {"events": ["test"]}}'); |
| }); |
|
|
| it("handles mixed single-line and multi-line values", () => { |
| const content = `--- |
| name: mixed-hook |
| description: "A hook with mixed values" |
| homepage: https://example.com |
| metadata: |
| { |
| "openclaw": { |
| "events": ["command:new"] |
| } |
| } |
| enabled: true |
| --- |
| `; |
| const result = parseFrontmatter(content); |
| expect(result.name).toBe("mixed-hook"); |
| expect(result.description).toBe("A hook with mixed values"); |
| expect(result.homepage).toBe("https://example.com"); |
| expect(result.metadata).toBeDefined(); |
| expect(result.enabled).toBe("true"); |
| }); |
|
|
| it("strips surrounding quotes from values", () => { |
| const content = `--- |
| name: "quoted-name" |
| description: 'single-quoted' |
| --- |
| `; |
| const result = parseFrontmatter(content); |
| expect(result.name).toBe("quoted-name"); |
| expect(result.description).toBe("single-quoted"); |
| }); |
|
|
| it("handles CRLF line endings", () => { |
| const content = "---\r\nname: test\r\ndescription: crlf\r\n---\r\n"; |
| const result = parseFrontmatter(content); |
| expect(result.name).toBe("test"); |
| expect(result.description).toBe("crlf"); |
| }); |
|
|
| it("handles CR line endings", () => { |
| const content = "---\rname: test\rdescription: cr\r---\r"; |
| const result = parseFrontmatter(content); |
| expect(result.name).toBe("test"); |
| expect(result.description).toBe("cr"); |
| }); |
| }); |
|
|
| describe("resolveOpenClawMetadata", () => { |
| it("extracts openclaw metadata from parsed frontmatter", () => { |
| const frontmatter = { |
| name: "test-hook", |
| metadata: JSON.stringify({ |
| openclaw: { |
| emoji: "🔥", |
| events: ["command:new", "command:reset"], |
| requires: { |
| config: ["workspace.dir"], |
| bins: ["git"], |
| }, |
| }, |
| }), |
| }; |
|
|
| const result = resolveOpenClawMetadata(frontmatter); |
| expect(result).toBeDefined(); |
| expect(result?.emoji).toBe("🔥"); |
| expect(result?.events).toEqual(["command:new", "command:reset"]); |
| expect(result?.requires?.config).toEqual(["workspace.dir"]); |
| expect(result?.requires?.bins).toEqual(["git"]); |
| }); |
|
|
| it("returns undefined when metadata is missing", () => { |
| const frontmatter = { name: "no-metadata" }; |
| const result = resolveOpenClawMetadata(frontmatter); |
| expect(result).toBeUndefined(); |
| }); |
|
|
| it("returns undefined when openclaw key is missing", () => { |
| const frontmatter = { |
| metadata: JSON.stringify({ other: "data" }), |
| }; |
| const result = resolveOpenClawMetadata(frontmatter); |
| expect(result).toBeUndefined(); |
| }); |
|
|
| it("returns undefined for invalid JSON", () => { |
| const frontmatter = { |
| metadata: "not valid json {", |
| }; |
| const result = resolveOpenClawMetadata(frontmatter); |
| expect(result).toBeUndefined(); |
| }); |
|
|
| it("handles install specs", () => { |
| const frontmatter = { |
| metadata: JSON.stringify({ |
| openclaw: { |
| events: ["command"], |
| install: [ |
| { id: "bundled", kind: "bundled", label: "Bundled with OpenClaw" }, |
| { id: "npm", kind: "npm", package: "@openclaw/hook" }, |
| ], |
| }, |
| }), |
| }; |
|
|
| const result = resolveOpenClawMetadata(frontmatter); |
| expect(result?.install).toHaveLength(2); |
| expect(result?.install?.[0].kind).toBe("bundled"); |
| expect(result?.install?.[1].kind).toBe("npm"); |
| expect(result?.install?.[1].package).toBe("@openclaw/hook"); |
| }); |
|
|
| it("handles os restrictions", () => { |
| const frontmatter = { |
| metadata: JSON.stringify({ |
| openclaw: { |
| events: ["command"], |
| os: ["darwin", "linux"], |
| }, |
| }), |
| }; |
|
|
| const result = resolveOpenClawMetadata(frontmatter); |
| expect(result?.os).toEqual(["darwin", "linux"]); |
| }); |
|
|
| it("parses real session-memory HOOK.md format", () => { |
| |
| const content = `--- |
| name: session-memory |
| description: "Save session context to memory when /new or /reset command is issued" |
| homepage: https://docs.openclaw.ai/automation/hooks#session-memory |
| metadata: |
| { |
| "openclaw": |
| { |
| "emoji": "💾", |
| "events": ["command:new", "command:reset"], |
| "requires": { "config": ["workspace.dir"] }, |
| "install": [{ "id": "bundled", "kind": "bundled", "label": "Bundled with OpenClaw" }], |
| }, |
| } |
| --- |
| |
| # Session Memory Hook |
| `; |
|
|
| const frontmatter = parseFrontmatter(content); |
| expect(frontmatter.name).toBe("session-memory"); |
| expect(frontmatter.metadata).toBeDefined(); |
|
|
| const openclaw = resolveOpenClawMetadata(frontmatter); |
| expect(openclaw).toBeDefined(); |
| expect(openclaw?.emoji).toBe("💾"); |
| expect(openclaw?.events).toEqual(["command:new", "command:reset"]); |
| expect(openclaw?.requires?.config).toEqual(["workspace.dir"]); |
| expect(openclaw?.install?.[0].kind).toBe("bundled"); |
| }); |
|
|
| it("parses YAML metadata map", () => { |
| const content = `--- |
| name: yaml-metadata |
| metadata: |
| openclaw: |
| emoji: disk |
| events: |
| - command:new |
| --- |
| `; |
| const frontmatter = parseFrontmatter(content); |
| const openclaw = resolveOpenClawMetadata(frontmatter); |
| expect(openclaw?.emoji).toBe("disk"); |
| expect(openclaw?.events).toEqual(["command:new"]); |
| }); |
| }); |
|
|
| describe("resolveHookInvocationPolicy", () => { |
| it("defaults to enabled when missing", () => { |
| expect(resolveHookInvocationPolicy({}).enabled).toBe(true); |
| }); |
|
|
| it("parses enabled flag", () => { |
| expect(resolveHookInvocationPolicy({ enabled: "no" }).enabled).toBe(false); |
| expect(resolveHookInvocationPolicy({ enabled: "on" }).enabled).toBe(true); |
| }); |
| }); |
|
|