test: add shared node and usage helper coverage

This commit is contained in:
Peter Steinberger 2026-03-13 20:26:22 +00:00
parent 2192bb7eb5
commit d291148e93
5 changed files with 255 additions and 0 deletions

View File

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

View File

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

View File

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

View File

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

View File

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