

























@@ -12,6 +12,7 @@ const {
1212 enableTailscaleServe,
1313 disableTailscaleServe,
1414 ensureFunnel,
15+ hasTailscaleFunnelRouteForPort,
1516 tailscaleFunnelStatusCoversPort,
1617} = tailscale;
1718const tailscaleBin = "tailscale";
@@ -89,6 +90,47 @@ describe("tailscale helpers", () => {
8990expect(host).toBe("noisy.tailnet.ts.net");
9091});
919293+it("parses noisy JSON output from tailscale whois", async () => {
94+const exec = vi.fn().mockResolvedValue({
95+stdout:
96+'warning: stale state\n{"UserProfile":{"LoginName":"operator@example.com","DisplayName":"Operator"}}\n',
97+});
98+99+await expect(readTailscaleWhoisIdentity("100.64.0.11", exec)).resolves.toEqual({
100+login: "operator@example.com",
101+name: "Operator",
102+});
103+});
104+105+it("caches malformed tailscale whois output on the short error TTL path", async () => {
106+vi.useFakeTimers();
107+vi.setSystemTime(new Date("2026-01-01T00:00:00.000Z"));
108+const exec = vi
109+.fn()
110+.mockResolvedValueOnce({ stdout: "warning: stale state\n{not json}\n" })
111+.mockResolvedValueOnce({
112+stdout: JSON.stringify({ UserProfile: { LoginName: "after@example.com" } }),
113+});
114+115+await expect(
116+readTailscaleWhoisIdentity("100.64.0.12", exec, { errorTtlMs: 1_000 }),
117+).resolves.toBeNull();
118+await expect(
119+readTailscaleWhoisIdentity("100.64.0.12", exec, { errorTtlMs: 1_000 }),
120+).resolves.toBeNull();
121+expect(exec).toHaveBeenCalledTimes(1);
122+123+vi.advanceTimersByTime(1_001);
124+125+await expect(
126+readTailscaleWhoisIdentity("100.64.0.12", exec, { errorTtlMs: 1_000 }),
127+).resolves.toEqual({
128+login: "after@example.com",
129+});
130+131+expect(exec).toHaveBeenCalledTimes(2);
132+});
133+92134it("does not cache whois results when the cache expiry would exceed Date range", async () => {
93135vi.useFakeTimers();
94136vi.setSystemTime(new Date(8_640_000_000_000_000));
@@ -277,6 +319,40 @@ describe("tailscale helpers", () => {
277319});
278320});
279321322+it("ensureFunnel accepts noisy JSON status output", async () => {
323+const exec = vi
324+.fn()
325+.mockResolvedValueOnce({
326+stdout: 'warning: stale state\n{"BackendState":"Running"}\n',
327+})
328+.mockResolvedValueOnce({ stdout: "" });
329+const runtime = createRuntimeWithExitError();
330+const prompt = vi.fn();
331+332+await ensureFunnel(8080, exec as never, runtime, prompt);
333+334+expect(exec).toHaveBeenCalledTimes(2);
335+expectExecCall(exec, 2, tailscaleBin, ["funnel", "--yes", "--bg", "8080"], {
336+maxBuffer: 200_000,
337+timeoutMs: 15_000,
338+});
339+expect(prompt).not.toHaveBeenCalled();
340+});
341+342+it("ensureFunnel treats malformed status output as a failure", async () => {
343+const exec = vi.fn().mockResolvedValueOnce({ stdout: "warning: stale state\n{not json}\n" });
344+const runtime = createRuntimeWithExitError();
345+const prompt = vi.fn();
346+347+await expect(ensureFunnel(8080, exec as never, runtime, prompt)).rejects.toThrow("exit 1");
348+349+expect(exec).toHaveBeenCalledTimes(1);
350+expect(prompt).not.toHaveBeenCalled();
351+expect(runtime.error).toHaveBeenCalledWith(
352+"Failed to enable Tailscale Funnel. Is it allowed on your tailnet?",
353+);
354+});
355+280356it("enableTailscaleServe skips sudo on non-permission errors", async () => {
281357const exec = vi.fn().mockRejectedValueOnce(new Error("boom"));
282358@@ -298,6 +374,23 @@ describe("tailscale helpers", () => {
298374299375expect(exec).toHaveBeenCalledTimes(2);
300376});
377+378+it("hasTailscaleFunnelRouteForPort accepts noisy JSON status output", async () => {
379+const exec = vi.fn().mockResolvedValue({
380+stdout:
381+'warning: stale state\n{"AllowFunnel":{"device.tailnet.ts.net:443":true},"Web":{"device.tailnet.ts.net:443":{"Handlers":{"/":{"Proxy":"http://127.0.0.1:18789"}}}}}\n',
382+});
383+384+await expect(hasTailscaleFunnelRouteForPort(18789, exec)).resolves.toBe(true);
385+});
386+387+it("hasTailscaleFunnelRouteForPort preserves malformed status parse failures", async () => {
388+const exec = vi.fn().mockResolvedValue({
389+stdout: "warning: stale state\n{not json}\n",
390+});
391+392+await expect(hasTailscaleFunnelRouteForPort(18789, exec)).rejects.toThrow(SyntaxError);
393+});
301394});
302395303396describe("tailscaleFunnelStatusCoversPort", () => {
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。