test: add frontmatter and node match coverage

This commit is contained in:
Peter Steinberger
2026-03-13 20:23:26 +00:00
parent 341d3e3493
commit 8dd454530d
3 changed files with 195 additions and 110 deletions

View File

@@ -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" }]);
});
});

View File

@@ -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/);
});
});

View File

@@ -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:/);
});
});