| import fs from "node:fs/promises"; |
| import os from "node:os"; |
| import path from "node:path"; |
| import { beforeEach, describe, expect, it, vi } from "vitest"; |
| import { withEnvAsync } from "../test-utils/env.js"; |
| import { |
| ensureTailscaleEndpoint, |
| resetGmailSetupUtilsCachesForTest, |
| resolvePythonExecutablePath, |
| } from "./gmail-setup-utils.js"; |
|
|
| const itUnix = process.platform === "win32" ? it.skip : it; |
| const runCommandWithTimeoutMock = vi.fn(); |
|
|
| vi.mock("../process/exec.js", () => ({ |
| runCommandWithTimeout: (...args: unknown[]) => runCommandWithTimeoutMock(...args), |
| })); |
|
|
| beforeEach(() => { |
| runCommandWithTimeoutMock.mockClear(); |
| resetGmailSetupUtilsCachesForTest(); |
| }); |
|
|
| describe("resolvePythonExecutablePath", () => { |
| itUnix( |
| "resolves a working python path and caches the result", |
| async () => { |
| const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-python-")); |
| try { |
| const realPython = path.join(tmp, "python-real"); |
| await fs.writeFile(realPython, "#!/bin/sh\nexit 0\n", "utf-8"); |
| await fs.chmod(realPython, 0o755); |
|
|
| const shimDir = path.join(tmp, "shims"); |
| await fs.mkdir(shimDir, { recursive: true }); |
| const shim = path.join(shimDir, "python3"); |
| await fs.writeFile(shim, "#!/bin/sh\nexit 0\n", "utf-8"); |
| await fs.chmod(shim, 0o755); |
|
|
| await withEnvAsync({ PATH: `${shimDir}${path.delimiter}/usr/bin` }, async () => { |
| runCommandWithTimeoutMock.mockResolvedValue({ |
| stdout: `${realPython}\n`, |
| stderr: "", |
| code: 0, |
| signal: null, |
| killed: false, |
| }); |
|
|
| const resolved = await resolvePythonExecutablePath(); |
| expect(resolved).toBe(realPython); |
|
|
| await withEnvAsync({ PATH: "/bin" }, async () => { |
| const cached = await resolvePythonExecutablePath(); |
| expect(cached).toBe(realPython); |
| }); |
| expect(runCommandWithTimeoutMock).toHaveBeenCalledTimes(1); |
| }); |
| } finally { |
| await fs.rm(tmp, { recursive: true, force: true }); |
| } |
| }, |
| 60_000, |
| ); |
| }); |
|
|
| describe("ensureTailscaleEndpoint", () => { |
| it("includes stdout and exit code when tailscale serve fails", async () => { |
| runCommandWithTimeoutMock |
| .mockResolvedValueOnce({ |
| stdout: JSON.stringify({ Self: { DNSName: "host.tailnet.ts.net." } }), |
| stderr: "", |
| code: 0, |
| signal: null, |
| killed: false, |
| }) |
| .mockResolvedValueOnce({ |
| stdout: "tailscale output", |
| stderr: "Warning: client version mismatch", |
| code: 1, |
| signal: null, |
| killed: false, |
| }); |
|
|
| let message = ""; |
| try { |
| await ensureTailscaleEndpoint({ |
| mode: "serve", |
| path: "/gmail-pubsub", |
| port: 8788, |
| }); |
| } catch (err) { |
| message = err instanceof Error ? err.message : String(err); |
| } |
|
|
| expect(message).toContain("code=1"); |
| expect(message).toContain("stderr: Warning: client version mismatch"); |
| expect(message).toContain("stdout: tailscale output"); |
| }); |
|
|
| it("includes JSON parse failure details with stdout", async () => { |
| runCommandWithTimeoutMock.mockResolvedValueOnce({ |
| stdout: "not-json", |
| stderr: "", |
| code: 0, |
| signal: null, |
| killed: false, |
| }); |
|
|
| let message = ""; |
| try { |
| await ensureTailscaleEndpoint({ |
| mode: "funnel", |
| path: "/gmail-pubsub", |
| port: 8788, |
| }); |
| } catch (err) { |
| message = err instanceof Error ? err.message : String(err); |
| } |
|
|
| expect(message).toContain("returned invalid JSON"); |
| expect(message).toContain("stdout: not-json"); |
| expect(message).toContain("code=0"); |
| }); |
| }); |
|
|