Spaces:
Paused
Paused
| 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"); | |
| // Verify the metadata is valid JSON | |
| 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", () => { | |
| // This is the actual format used in the bundled hooks | |
| const content = `--- | |
| name: session-memory | |
| description: "Save session context to memory when /new command is issued" | |
| homepage: https://docs.openclaw.ai/hooks#session-memory | |
| metadata: | |
| { | |
| "openclaw": | |
| { | |
| "emoji": "๐พ", | |
| "events": ["command:new"], | |
| "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"]); | |
| 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); | |
| }); | |
| }); | |