mirror of https://github.com/openclaw/openclaw.git
test: add shared node and usage helper coverage
This commit is contained in:
parent
2192bb7eb5
commit
d291148e93
|
|
@ -0,0 +1,33 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import { resolveEmojiAndHomepage } from "./entry-metadata.js";
|
||||
|
||||
describe("shared/entry-metadata", () => {
|
||||
it("prefers metadata emoji and homepage when present", () => {
|
||||
expect(
|
||||
resolveEmojiAndHomepage({
|
||||
metadata: { emoji: "🦀", homepage: " https://openclaw.ai " },
|
||||
frontmatter: { emoji: "🙂", homepage: "https://example.com" },
|
||||
}),
|
||||
).toEqual({
|
||||
emoji: "🦀",
|
||||
homepage: "https://openclaw.ai",
|
||||
});
|
||||
});
|
||||
|
||||
it("falls back through frontmatter homepage aliases and drops blanks", () => {
|
||||
expect(
|
||||
resolveEmojiAndHomepage({
|
||||
frontmatter: { emoji: "🙂", website: " https://docs.openclaw.ai " },
|
||||
}),
|
||||
).toEqual({
|
||||
emoji: "🙂",
|
||||
homepage: "https://docs.openclaw.ai",
|
||||
});
|
||||
expect(
|
||||
resolveEmojiAndHomepage({
|
||||
metadata: { homepage: " " },
|
||||
frontmatter: { url: " " },
|
||||
}),
|
||||
).toEqual({});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import { inferParamBFromIdOrName } from "./model-param-b.js";
|
||||
|
||||
describe("shared/model-param-b", () => {
|
||||
it("extracts the largest valid b-sized parameter token", () => {
|
||||
expect(inferParamBFromIdOrName("llama-8b mixtral-22b")).toBe(22);
|
||||
expect(inferParamBFromIdOrName("Qwen 0.5B Instruct")).toBe(0.5);
|
||||
});
|
||||
|
||||
it("ignores malformed, zero, and non-delimited matches", () => {
|
||||
expect(inferParamBFromIdOrName("abc70beta 0b x70b2")).toBeNull();
|
||||
expect(inferParamBFromIdOrName("model 0b")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import { resolveNodeFromNodeList, resolveNodeIdFromNodeList } from "./node-resolve.js";
|
||||
|
||||
describe("shared/node-resolve", () => {
|
||||
const nodes = [
|
||||
{ nodeId: "mac-123", displayName: "Mac Studio", connected: true },
|
||||
{ nodeId: "pi-456", displayName: "Raspberry Pi", connected: false },
|
||||
];
|
||||
|
||||
it("resolves node ids through candidate matching", () => {
|
||||
expect(resolveNodeIdFromNodeList(nodes, "Mac Studio")).toBe("mac-123");
|
||||
});
|
||||
|
||||
it("supports optional default-node selection when query is blank", () => {
|
||||
expect(
|
||||
resolveNodeIdFromNodeList(nodes, " ", {
|
||||
allowDefault: true,
|
||||
pickDefaultNode: (entries) => entries.find((entry) => entry.connected) ?? null,
|
||||
}),
|
||||
).toBe("mac-123");
|
||||
});
|
||||
|
||||
it("still throws when default selection is disabled or returns null", () => {
|
||||
expect(() => resolveNodeIdFromNodeList(nodes, " ")).toThrow(/node required/);
|
||||
expect(() =>
|
||||
resolveNodeIdFromNodeList(nodes, "", {
|
||||
allowDefault: true,
|
||||
pickDefaultNode: () => null,
|
||||
}),
|
||||
).toThrow(/node required/);
|
||||
});
|
||||
|
||||
it("returns the full node object and falls back to a synthetic entry when needed", () => {
|
||||
expect(resolveNodeFromNodeList(nodes, "pi-456")).toEqual(nodes[1]);
|
||||
expect(
|
||||
resolveNodeFromNodeList([], "", {
|
||||
allowDefault: true,
|
||||
pickDefaultNode: () => ({ nodeId: "synthetic-1" }),
|
||||
}),
|
||||
).toEqual({ nodeId: "synthetic-1" });
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
import { describe, expect, it, vi } from "vitest";
|
||||
import { resolveTailnetHostWithRunner } from "./tailscale-status.js";
|
||||
|
||||
describe("shared/tailscale-status", () => {
|
||||
it("returns null when no runner is provided", async () => {
|
||||
await expect(resolveTailnetHostWithRunner()).resolves.toBeNull();
|
||||
});
|
||||
|
||||
it("prefers DNS names and trims trailing dots from status json", async () => {
|
||||
const run = vi.fn().mockResolvedValue({
|
||||
code: 0,
|
||||
stdout: 'noise\n{"Self":{"DNSName":"mac.tail123.ts.net.","TailscaleIPs":["100.64.0.8"]}}',
|
||||
});
|
||||
|
||||
await expect(resolveTailnetHostWithRunner(run)).resolves.toBe("mac.tail123.ts.net");
|
||||
expect(run).toHaveBeenCalledWith(["tailscale", "status", "--json"], { timeoutMs: 5000 });
|
||||
});
|
||||
|
||||
it("falls back across command candidates and then to the first tailscale ip", async () => {
|
||||
const run = vi.fn().mockRejectedValueOnce(new Error("missing binary")).mockResolvedValueOnce({
|
||||
code: 0,
|
||||
stdout: '{"Self":{"TailscaleIPs":["100.64.0.9","fd7a::1"]}}',
|
||||
});
|
||||
|
||||
await expect(resolveTailnetHostWithRunner(run)).resolves.toBe("100.64.0.9");
|
||||
expect(run).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
["/Applications/Tailscale.app/Contents/MacOS/Tailscale", "status", "--json"],
|
||||
{
|
||||
timeoutMs: 5000,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it("returns null for non-zero exits, blank output, or invalid json", async () => {
|
||||
const run = vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce({ code: 1, stdout: "boom" })
|
||||
.mockResolvedValueOnce({ code: 0, stdout: " " });
|
||||
|
||||
await expect(resolveTailnetHostWithRunner(run)).resolves.toBeNull();
|
||||
|
||||
const invalid = vi.fn().mockResolvedValue({
|
||||
code: 0,
|
||||
stdout: "not-json",
|
||||
});
|
||||
await expect(resolveTailnetHostWithRunner(invalid)).resolves.toBeNull();
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,117 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
buildUsageAggregateTail,
|
||||
mergeUsageDailyLatency,
|
||||
mergeUsageLatency,
|
||||
} from "./usage-aggregates.js";
|
||||
|
||||
describe("shared/usage-aggregates", () => {
|
||||
it("merges latency totals and ignores empty inputs", () => {
|
||||
const totals = {
|
||||
count: 1,
|
||||
sum: 100,
|
||||
min: 100,
|
||||
max: 100,
|
||||
p95Max: 100,
|
||||
};
|
||||
|
||||
mergeUsageLatency(totals, undefined);
|
||||
mergeUsageLatency(totals, {
|
||||
count: 2,
|
||||
avgMs: 50,
|
||||
minMs: 20,
|
||||
maxMs: 90,
|
||||
p95Ms: 80,
|
||||
});
|
||||
|
||||
expect(totals).toEqual({
|
||||
count: 3,
|
||||
sum: 200,
|
||||
min: 20,
|
||||
max: 100,
|
||||
p95Max: 100,
|
||||
});
|
||||
});
|
||||
|
||||
it("merges daily latency by date and computes aggregate tail sorting", () => {
|
||||
const dailyLatencyMap = new Map<
|
||||
string,
|
||||
{
|
||||
date: string;
|
||||
count: number;
|
||||
sum: number;
|
||||
min: number;
|
||||
max: number;
|
||||
p95Max: number;
|
||||
}
|
||||
>();
|
||||
|
||||
mergeUsageDailyLatency(dailyLatencyMap, [
|
||||
{ date: "2026-03-12", count: 2, avgMs: 50, minMs: 20, maxMs: 90, p95Ms: 80 },
|
||||
{ date: "2026-03-12", count: 1, avgMs: 120, minMs: 120, maxMs: 120, p95Ms: 120 },
|
||||
{ date: "2026-03-11", count: 1, avgMs: 30, minMs: 30, maxMs: 30, p95Ms: 30 },
|
||||
]);
|
||||
|
||||
const tail = buildUsageAggregateTail({
|
||||
byChannelMap: new Map([
|
||||
["discord", { totalCost: 4 }],
|
||||
["telegram", { totalCost: 8 }],
|
||||
]),
|
||||
latencyTotals: {
|
||||
count: 3,
|
||||
sum: 200,
|
||||
min: 20,
|
||||
max: 120,
|
||||
p95Max: 120,
|
||||
},
|
||||
dailyLatencyMap,
|
||||
modelDailyMap: new Map([
|
||||
["b", { date: "2026-03-12", cost: 1 }],
|
||||
["a", { date: "2026-03-12", cost: 2 }],
|
||||
["c", { date: "2026-03-11", cost: 9 }],
|
||||
]),
|
||||
dailyMap: new Map([
|
||||
["b", { date: "2026-03-12" }],
|
||||
["a", { date: "2026-03-11" }],
|
||||
]),
|
||||
});
|
||||
|
||||
expect(tail.byChannel.map((entry) => entry.channel)).toEqual(["telegram", "discord"]);
|
||||
expect(tail.latency).toEqual({
|
||||
count: 3,
|
||||
avgMs: 200 / 3,
|
||||
minMs: 20,
|
||||
maxMs: 120,
|
||||
p95Ms: 120,
|
||||
});
|
||||
expect(tail.dailyLatency).toEqual([
|
||||
{ date: "2026-03-11", count: 1, avgMs: 30, minMs: 30, maxMs: 30, p95Ms: 30 },
|
||||
{ date: "2026-03-12", count: 3, avgMs: 220 / 3, minMs: 20, maxMs: 120, p95Ms: 120 },
|
||||
]);
|
||||
expect(tail.modelDaily).toEqual([
|
||||
{ date: "2026-03-11", cost: 9 },
|
||||
{ date: "2026-03-12", cost: 2 },
|
||||
{ date: "2026-03-12", cost: 1 },
|
||||
]);
|
||||
expect(tail.daily).toEqual([{ date: "2026-03-11" }, { date: "2026-03-12" }]);
|
||||
});
|
||||
|
||||
it("omits latency when no requests were counted", () => {
|
||||
const tail = buildUsageAggregateTail({
|
||||
byChannelMap: new Map(),
|
||||
latencyTotals: {
|
||||
count: 0,
|
||||
sum: 0,
|
||||
min: Number.POSITIVE_INFINITY,
|
||||
max: 0,
|
||||
p95Max: 0,
|
||||
},
|
||||
dailyLatencyMap: new Map(),
|
||||
modelDailyMap: new Map(),
|
||||
dailyMap: new Map(),
|
||||
});
|
||||
|
||||
expect(tail.latency).toBeUndefined();
|
||||
expect(tail.dailyLatency).toEqual([]);
|
||||
});
|
||||
});
|
||||
Loading…
Reference in New Issue