mirror of https://github.com/openclaw/openclaw.git
fix: harden zai and ssh helper coverage
This commit is contained in:
parent
da2f85ae2b
commit
54728c60d5
|
|
@ -25,6 +25,20 @@ describe("fetchZaiUsage", () => {
|
||||||
expect(result.windows).toHaveLength(0);
|
expect(result.windows).toHaveLength(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("falls back to a generic API error for blank unsuccessful messages", async () => {
|
||||||
|
const mockFetch = createProviderUsageFetch(async () =>
|
||||||
|
makeResponse(200, {
|
||||||
|
success: false,
|
||||||
|
code: 500,
|
||||||
|
msg: " ",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await fetchZaiUsage("key", 5000, mockFetch);
|
||||||
|
expect(result.error).toBe("API error");
|
||||||
|
expect(result.windows).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
it("parses token and monthly windows with reset times", async () => {
|
it("parses token and monthly windows with reset times", async () => {
|
||||||
const tokenReset = "2026-01-08T00:00:00Z";
|
const tokenReset = "2026-01-08T00:00:00Z";
|
||||||
const minuteReset = "2026-01-08T00:30:00Z";
|
const minuteReset = "2026-01-08T00:30:00Z";
|
||||||
|
|
@ -83,4 +97,47 @@ describe("fetchZaiUsage", () => {
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("clamps invalid percentages and falls back to alternate plan fields", async () => {
|
||||||
|
const mockFetch = createProviderUsageFetch(async () =>
|
||||||
|
makeResponse(200, {
|
||||||
|
success: true,
|
||||||
|
code: 200,
|
||||||
|
data: {
|
||||||
|
plan: "Pro",
|
||||||
|
limits: [
|
||||||
|
{
|
||||||
|
type: "TOKENS_LIMIT",
|
||||||
|
percentage: -5,
|
||||||
|
unit: 99,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "TIME_LIMIT",
|
||||||
|
percentage: 140,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "OTHER_LIMIT",
|
||||||
|
percentage: 50,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await fetchZaiUsage("key", 5000, mockFetch);
|
||||||
|
|
||||||
|
expect(result.plan).toBe("Pro");
|
||||||
|
expect(result.windows).toEqual([
|
||||||
|
{
|
||||||
|
label: "Tokens (Limit)",
|
||||||
|
usedPercent: 0,
|
||||||
|
resetAt: undefined,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Monthly",
|
||||||
|
usedPercent: 100,
|
||||||
|
resetAt: undefined,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -46,11 +46,12 @@ export async function fetchZaiUsage(
|
||||||
|
|
||||||
const data = (await res.json()) as ZaiUsageResponse;
|
const data = (await res.json()) as ZaiUsageResponse;
|
||||||
if (!data.success || data.code !== 200) {
|
if (!data.success || data.code !== 200) {
|
||||||
|
const errorMessage = typeof data.msg === "string" ? data.msg.trim() : "";
|
||||||
return {
|
return {
|
||||||
provider: "zai",
|
provider: "zai",
|
||||||
displayName: PROVIDER_LABELS.zai,
|
displayName: PROVIDER_LABELS.zai,
|
||||||
windows: [],
|
windows: [],
|
||||||
error: data.msg || "API error",
|
error: errorMessage || "API error",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,17 @@ describe("ssh-config", () => {
|
||||||
expect(parsed.identityFiles).toEqual(["/tmp/id"]);
|
expect(parsed.identityFiles).toEqual(["/tmp/id"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("ignores invalid ports and blank lines in ssh -G output", () => {
|
||||||
|
const parsed = parseSshConfigOutput(
|
||||||
|
"user bob\nhostname example.com\nport not-a-number\nidentityfile none\nidentityfile \n",
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(parsed.user).toBe("bob");
|
||||||
|
expect(parsed.host).toBe("example.com");
|
||||||
|
expect(parsed.port).toBeUndefined();
|
||||||
|
expect(parsed.identityFiles).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
it("resolves ssh config via ssh -G", async () => {
|
it("resolves ssh config via ssh -G", async () => {
|
||||||
const config = await resolveSshConfig({ user: "me", host: "alias", port: 22 });
|
const config = await resolveSshConfig({ user: "me", host: "alias", port: 22 });
|
||||||
expect(config?.user).toBe("steipete");
|
expect(config?.user).toBe("steipete");
|
||||||
|
|
@ -68,6 +79,16 @@ describe("ssh-config", () => {
|
||||||
expect(args?.slice(-2)).toEqual(["--", "me@alias"]);
|
expect(args?.slice(-2)).toEqual(["--", "me@alias"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("adds non-default port and trimmed identity arguments", async () => {
|
||||||
|
await resolveSshConfig(
|
||||||
|
{ user: "me", host: "alias", port: 2022 },
|
||||||
|
{ identity: " /tmp/custom_id " },
|
||||||
|
);
|
||||||
|
|
||||||
|
const args = spawnMock.mock.calls.at(-1)?.[1] as string[] | undefined;
|
||||||
|
expect(args).toEqual(["-G", "-p", "2022", "-i", "/tmp/custom_id", "--", "me@alias"]);
|
||||||
|
});
|
||||||
|
|
||||||
it("returns null when ssh -G fails", async () => {
|
it("returns null when ssh -G fails", async () => {
|
||||||
spawnMock.mockImplementationOnce(
|
spawnMock.mockImplementationOnce(
|
||||||
(_command: string, _args: readonly string[], _options: SpawnOptions): ChildProcess => {
|
(_command: string, _args: readonly string[], _options: SpawnOptions): ChildProcess => {
|
||||||
|
|
@ -82,4 +103,18 @@ describe("ssh-config", () => {
|
||||||
const config = await resolveSshConfig({ user: "me", host: "bad-host", port: 22 });
|
const config = await resolveSshConfig({ user: "me", host: "bad-host", port: 22 });
|
||||||
expect(config).toBeNull();
|
expect(config).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("returns null when the ssh process emits an error", async () => {
|
||||||
|
spawnMock.mockImplementationOnce(
|
||||||
|
(_command: string, _args: readonly string[], _options: SpawnOptions): ChildProcess => {
|
||||||
|
const { child } = createMockSpawnChild();
|
||||||
|
process.nextTick(() => {
|
||||||
|
child.emit("error", new Error("spawn boom"));
|
||||||
|
});
|
||||||
|
return child as unknown as ChildProcess;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
await expect(resolveSshConfig({ user: "me", host: "bad-host", port: 22 })).resolves.toBeNull();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue