From 8dd454530d231e323a0917fc9baf49dbdaadf47e Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 13 Mar 2026 20:23:26 +0000 Subject: [PATCH] test: add frontmatter and node match coverage --- src/shared/frontmatter.test.ts | 135 +++++++++++++++++++++++++++++++++ src/shared/node-match.test.ts | 59 ++++++++++++++ src/shared/shared-misc.test.ts | 111 +-------------------------- 3 files changed, 195 insertions(+), 110 deletions(-) create mode 100644 src/shared/frontmatter.test.ts create mode 100644 src/shared/node-match.test.ts diff --git a/src/shared/frontmatter.test.ts b/src/shared/frontmatter.test.ts new file mode 100644 index 00000000000..606114b9f56 --- /dev/null +++ b/src/shared/frontmatter.test.ts @@ -0,0 +1,135 @@ +import { describe, expect, it, test } from "vitest"; +import { + applyOpenClawManifestInstallCommonFields, + getFrontmatterString, + normalizeStringList, + parseFrontmatterBool, + parseOpenClawManifestInstallBase, + resolveOpenClawManifestBlock, + resolveOpenClawManifestInstall, + resolveOpenClawManifestOs, + resolveOpenClawManifestRequires, +} from "./frontmatter.js"; + +describe("shared/frontmatter", () => { + test("normalizeStringList handles strings, arrays, and non-list values", () => { + expect(normalizeStringList("a, b,,c")).toEqual(["a", "b", "c"]); + expect(normalizeStringList([" a ", "", "b", 42])).toEqual(["a", "b", "42"]); + expect(normalizeStringList(null)).toEqual([]); + }); + + test("getFrontmatterString extracts strings only", () => { + expect(getFrontmatterString({ a: "b" }, "a")).toBe("b"); + expect(getFrontmatterString({ a: 1 }, "a")).toBeUndefined(); + }); + + test("parseFrontmatterBool respects explicit values and fallback", () => { + expect(parseFrontmatterBool("true", false)).toBe(true); + expect(parseFrontmatterBool("false", true)).toBe(false); + expect(parseFrontmatterBool(undefined, true)).toBe(true); + }); + + test("resolveOpenClawManifestBlock reads current manifest keys and custom metadata fields", () => { + expect( + resolveOpenClawManifestBlock({ + frontmatter: { + metadata: "{ openclaw: { foo: 1, bar: 'baz' } }", + }, + }), + ).toEqual({ foo: 1, bar: "baz" }); + + expect( + resolveOpenClawManifestBlock({ + frontmatter: { + pluginMeta: "{ openclaw: { foo: 2 } }", + }, + key: "pluginMeta", + }), + ).toEqual({ foo: 2 }); + }); + + test("resolveOpenClawManifestBlock returns undefined for invalid input", () => { + expect(resolveOpenClawManifestBlock({ frontmatter: {} })).toBeUndefined(); + expect( + resolveOpenClawManifestBlock({ frontmatter: { metadata: "not-json5" } }), + ).toBeUndefined(); + expect( + resolveOpenClawManifestBlock({ frontmatter: { metadata: "{ nope: { a: 1 } }" } }), + ).toBeUndefined(); + }); + + it("normalizes manifest requirement and os lists", () => { + expect( + resolveOpenClawManifestRequires({ + requires: { + bins: "bun, node", + anyBins: [" ffmpeg ", ""], + env: ["OPENCLAW_TOKEN", " OPENCLAW_URL "], + config: null, + }, + }), + ).toEqual({ + bins: ["bun", "node"], + anyBins: ["ffmpeg"], + env: ["OPENCLAW_TOKEN", "OPENCLAW_URL"], + config: [], + }); + expect(resolveOpenClawManifestRequires({})).toBeUndefined(); + expect(resolveOpenClawManifestOs({ os: [" darwin ", "linux", ""] })).toEqual([ + "darwin", + "linux", + ]); + }); + + it("parses and applies install common fields", () => { + const parsed = parseOpenClawManifestInstallBase( + { + type: " Brew ", + id: "brew.git", + label: "Git", + bins: [" git ", "git"], + }, + ["brew", "npm"], + ); + + expect(parsed).toEqual({ + raw: { + type: " Brew ", + id: "brew.git", + label: "Git", + bins: [" git ", "git"], + }, + kind: "brew", + id: "brew.git", + label: "Git", + bins: ["git", "git"], + }); + expect(parseOpenClawManifestInstallBase({ kind: "bad" }, ["brew"])).toBeUndefined(); + expect(applyOpenClawManifestInstallCommonFields({ extra: true }, parsed!)).toEqual({ + extra: true, + id: "brew.git", + label: "Git", + bins: ["git", "git"], + }); + }); + + it("maps install entries through the parser and filters rejected specs", () => { + expect( + resolveOpenClawManifestInstall( + { + install: [{ id: "keep" }, { id: "drop" }, "bad"], + }, + (entry) => { + if ( + typeof entry === "object" && + entry !== null && + (entry as { id?: string }).id === "keep" + ) { + return { id: "keep" }; + } + return undefined; + }, + ), + ).toEqual([{ id: "keep" }]); + }); +}); diff --git a/src/shared/node-match.test.ts b/src/shared/node-match.test.ts new file mode 100644 index 00000000000..2ddc3663d3f --- /dev/null +++ b/src/shared/node-match.test.ts @@ -0,0 +1,59 @@ +import { describe, expect, it } from "vitest"; +import { normalizeNodeKey, resolveNodeIdFromCandidates, resolveNodeMatches } from "./node-match.js"; + +describe("shared/node-match", () => { + it("normalizes node keys by lowercasing and collapsing separators", () => { + expect(normalizeNodeKey(" Mac Studio! ")).toBe("mac-studio"); + expect(normalizeNodeKey("---PI__Node---")).toBe("pi-node"); + }); + + it("matches candidates by node id, remote ip, normalized name, and long prefix", () => { + const nodes = [ + { nodeId: "mac-abcdef", displayName: "Mac Studio", remoteIp: "100.0.0.1" }, + { nodeId: "pi-456789", displayName: "Raspberry Pi", remoteIp: "100.0.0.2" }, + ]; + + expect(resolveNodeMatches(nodes, "mac-abcdef")).toEqual([nodes[0]]); + expect(resolveNodeMatches(nodes, "100.0.0.2")).toEqual([nodes[1]]); + expect(resolveNodeMatches(nodes, "mac studio")).toEqual([nodes[0]]); + expect(resolveNodeMatches(nodes, "pi-456")).toEqual([nodes[1]]); + expect(resolveNodeMatches(nodes, "pi")).toEqual([]); + expect(resolveNodeMatches(nodes, " ")).toEqual([]); + }); + + it("resolves unique matches and prefers a unique connected node", () => { + expect( + resolveNodeIdFromCandidates( + [ + { nodeId: "ios-old", displayName: "iPhone", connected: false }, + { nodeId: "ios-live", displayName: "iPhone", connected: true }, + ], + "iphone", + ), + ).toBe("ios-live"); + }); + + it("throws clear unknown and ambiguous node errors", () => { + expect(() => + resolveNodeIdFromCandidates( + [ + { nodeId: "mac-123", displayName: "Mac Studio", remoteIp: "100.0.0.1" }, + { nodeId: "pi-456" }, + ], + "nope", + ), + ).toThrow(/unknown node: nope.*known: Mac Studio, pi-456/); + + expect(() => + resolveNodeIdFromCandidates( + [ + { nodeId: "ios-a", displayName: "iPhone", connected: true }, + { nodeId: "ios-b", displayName: "iPhone", connected: true }, + ], + "iphone", + ), + ).toThrow(/ambiguous node: iphone.*matches: iPhone, iPhone/); + + expect(() => resolveNodeIdFromCandidates([], "")).toThrow(/node required/); + }); +}); diff --git a/src/shared/shared-misc.test.ts b/src/shared/shared-misc.test.ts index 8a729109513..aab46b776d7 100644 --- a/src/shared/shared-misc.test.ts +++ b/src/shared/shared-misc.test.ts @@ -1,12 +1,5 @@ -import { describe, expect, it, test } from "vitest"; +import { describe, expect, it } from "vitest"; import { extractTextFromChatContent } from "./chat-content.js"; -import { - getFrontmatterString, - normalizeStringList, - parseFrontmatterBool, - resolveOpenClawManifestBlock, -} from "./frontmatter.js"; -import { resolveNodeIdFromCandidates } from "./node-match.js"; describe("extractTextFromChatContent", () => { it("normalizes string content", () => { @@ -47,105 +40,3 @@ describe("extractTextFromChatContent", () => { ).toBe("hello\nworld"); }); }); - -describe("shared/frontmatter", () => { - test("normalizeStringList handles strings and arrays", () => { - expect(normalizeStringList("a, b,,c")).toEqual(["a", "b", "c"]); - expect(normalizeStringList([" a ", "", "b"])).toEqual(["a", "b"]); - expect(normalizeStringList(null)).toEqual([]); - }); - - test("getFrontmatterString extracts strings only", () => { - expect(getFrontmatterString({ a: "b" }, "a")).toBe("b"); - expect(getFrontmatterString({ a: 1 }, "a")).toBeUndefined(); - }); - - test("parseFrontmatterBool respects fallback", () => { - expect(parseFrontmatterBool("true", false)).toBe(true); - expect(parseFrontmatterBool("false", true)).toBe(false); - expect(parseFrontmatterBool(undefined, true)).toBe(true); - }); - - test("resolveOpenClawManifestBlock parses JSON5 metadata and picks openclaw block", () => { - const frontmatter = { - metadata: "{ openclaw: { foo: 1, bar: 'baz' } }", - }; - expect(resolveOpenClawManifestBlock({ frontmatter })).toEqual({ foo: 1, bar: "baz" }); - }); - - test("resolveOpenClawManifestBlock returns undefined for invalid input", () => { - expect(resolveOpenClawManifestBlock({ frontmatter: {} })).toBeUndefined(); - expect( - resolveOpenClawManifestBlock({ frontmatter: { metadata: "not-json5" } }), - ).toBeUndefined(); - expect( - resolveOpenClawManifestBlock({ frontmatter: { metadata: "{ nope: { a: 1 } }" } }), - ).toBeUndefined(); - }); -}); - -describe("resolveNodeIdFromCandidates", () => { - it("matches nodeId", () => { - expect( - resolveNodeIdFromCandidates( - [ - { nodeId: "mac-123", displayName: "Mac Studio", remoteIp: "100.0.0.1" }, - { nodeId: "pi-456", displayName: "Raspberry Pi", remoteIp: "100.0.0.2" }, - ], - "pi-456", - ), - ).toBe("pi-456"); - }); - - it("matches displayName using normalization", () => { - expect( - resolveNodeIdFromCandidates([{ nodeId: "mac-123", displayName: "Mac Studio" }], "mac studio"), - ).toBe("mac-123"); - }); - - it("matches nodeId prefix (>=6 chars)", () => { - expect(resolveNodeIdFromCandidates([{ nodeId: "mac-abcdef" }], "mac-ab")).toBe("mac-abcdef"); - }); - - it("throws unknown node with known list", () => { - expect(() => - resolveNodeIdFromCandidates( - [ - { nodeId: "mac-123", displayName: "Mac Studio", remoteIp: "100.0.0.1" }, - { nodeId: "pi-456" }, - ], - "nope", - ), - ).toThrow(/unknown node: nope.*known: /); - }); - - it("throws ambiguous node with matches list", () => { - expect(() => - resolveNodeIdFromCandidates([{ nodeId: "mac-abcdef" }, { nodeId: "mac-abc999" }], "mac-abc"), - ).toThrow(/ambiguous node: mac-abc.*matches:/); - }); - - it("prefers a unique connected node when names are duplicated", () => { - expect( - resolveNodeIdFromCandidates( - [ - { nodeId: "ios-old", displayName: "iPhone", connected: false }, - { nodeId: "ios-live", displayName: "iPhone", connected: true }, - ], - "iphone", - ), - ).toBe("ios-live"); - }); - - it("stays ambiguous when multiple connected nodes match", () => { - expect(() => - resolveNodeIdFromCandidates( - [ - { nodeId: "ios-a", displayName: "iPhone", connected: true }, - { nodeId: "ios-b", displayName: "iPhone", connected: true }, - ], - "iphone", - ), - ).toThrow(/ambiguous node: iphone.*matches:/); - }); -});