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