diff --git a/src/infra/openclaw-root.test.ts b/src/infra/openclaw-root.test.ts index 85d24512468..e12b2d77f64 100644 --- a/src/infra/openclaw-root.test.ts +++ b/src/infra/openclaw-root.test.ts @@ -141,6 +141,19 @@ describe("resolveOpenClawPackageRoot", () => { expect(resolveOpenClawPackageRootSync({ moduleUrl })).toBe(pkgRoot); }); + it("falls through from a non-openclaw moduleUrl candidate to cwd", async () => { + const wrongPkgRoot = fx("moduleurl-fallthrough", "wrong"); + const cwdPkgRoot = fx("moduleurl-fallthrough", "cwd"); + setFile(path.join(wrongPkgRoot, "package.json"), JSON.stringify({ name: "not-openclaw" })); + setFile(path.join(cwdPkgRoot, "package.json"), JSON.stringify({ name: "openclaw" })); + const moduleUrl = pathToFileURL(path.join(wrongPkgRoot, "dist", "index.js")).toString(); + + expect(resolveOpenClawPackageRootSync({ moduleUrl, cwd: cwdPkgRoot })).toBe(cwdPkgRoot); + await expect(resolveOpenClawPackageRoot({ moduleUrl, cwd: cwdPkgRoot })).resolves.toBe( + cwdPkgRoot, + ); + }); + it("ignores invalid moduleUrl values and falls back to cwd", async () => { const pkgRoot = fx("invalid-moduleurl"); setFile(path.join(pkgRoot, "package.json"), JSON.stringify({ name: "openclaw" })); @@ -160,6 +173,16 @@ describe("resolveOpenClawPackageRoot", () => { expect(resolveOpenClawPackageRootSync({ cwd: pkgRoot })).toBeNull(); }); + it("falls back from a symlinked argv1 to the node_modules package root", () => { + const project = fx("symlink-node-modules-fallback"); + const argv1 = path.join(project, "node_modules", ".bin", "openclaw"); + state.realpaths.set(abs(argv1), abs(path.join(project, "versions", "current", "openclaw.mjs"))); + const pkgRoot = path.join(project, "node_modules", "openclaw"); + setFile(path.join(pkgRoot, "package.json"), JSON.stringify({ name: "openclaw" })); + + expect(resolveOpenClawPackageRootSync({ argv1 })).toBe(pkgRoot); + }); + it("async resolver matches sync behavior", async () => { const pkgRoot = fx("async"); setFile(path.join(pkgRoot, "package.json"), JSON.stringify({ name: "openclaw" })); diff --git a/src/infra/provider-usage.fetch.shared.test.ts b/src/infra/provider-usage.fetch.shared.test.ts index 213a5a3eb2d..dea3097fa4a 100644 --- a/src/infra/provider-usage.fetch.shared.test.ts +++ b/src/infra/provider-usage.fetch.shared.test.ts @@ -1,11 +1,17 @@ -import { describe, expect, it } from "vitest"; +import { afterEach, describe, expect, it, vi } from "vitest"; import { buildUsageErrorSnapshot, buildUsageHttpErrorSnapshot, + fetchJson, parseFiniteNumber, } from "./provider-usage.fetch.shared.js"; describe("provider usage fetch shared helpers", () => { + afterEach(() => { + vi.useRealTimers(); + vi.restoreAllMocks(); + }); + it("builds a provider error snapshot", () => { expect(buildUsageErrorSnapshot("zai", "API error")).toEqual({ provider: "zai", @@ -23,6 +29,56 @@ describe("provider usage fetch shared helpers", () => { expect(parseFiniteNumber(value)).toBe(expected); }); + it("forwards request init and clears the timeout on success", async () => { + vi.useFakeTimers(); + const clearTimeoutSpy = vi.spyOn(globalThis, "clearTimeout"); + const fetchFn = vi.fn( + async (_url: string, init?: RequestInit) => + new Response(JSON.stringify({ aborted: init?.signal?.aborted ?? false }), { status: 200 }), + ); + + const response = await fetchJson( + "https://example.com/usage", + { + method: "POST", + headers: { authorization: "Bearer test" }, + }, + 1_000, + fetchFn, + ); + + expect(fetchFn).toHaveBeenCalledWith( + "https://example.com/usage", + expect.objectContaining({ + method: "POST", + headers: { authorization: "Bearer test" }, + signal: expect.any(AbortSignal), + }), + ); + await expect(response.json()).resolves.toEqual({ aborted: false }); + expect(clearTimeoutSpy).toHaveBeenCalledTimes(1); + }); + + it("aborts timed out requests and clears the timer on rejection", async () => { + vi.useFakeTimers(); + const clearTimeoutSpy = vi.spyOn(globalThis, "clearTimeout"); + const fetchFn = vi.fn( + (_url: string, init?: RequestInit) => + new Promise((_, reject) => { + init?.signal?.addEventListener("abort", () => reject(new Error("aborted by timeout")), { + once: true, + }); + }), + ); + + const request = fetchJson("https://example.com/usage", {}, 50, fetchFn); + const rejection = expect(request).rejects.toThrow("aborted by timeout"); + await vi.advanceTimersByTimeAsync(50); + + await rejection; + expect(clearTimeoutSpy).toHaveBeenCalledTimes(1); + }); + it("maps configured status codes to token expired", () => { const snapshot = buildUsageHttpErrorSnapshot({ provider: "openai-codex",