























@@ -1,4 +1,4 @@
1-import { afterEach, describe, expect, it, vi } from "vitest";
1+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
22import { GatewayTransportError } from "../gateway/call.js";
33import { runRegisteredCli } from "../test-utils/command-runner.js";
44import { formatLogTimestamp, registerLogsCli } from "./logs-cli.js";
@@ -38,6 +38,8 @@ const { MockGatewayTransportError } = vi.hoisted(() => ({
38383939const callGatewayFromCli = vi.fn();
4040const readConfiguredLogTail = vi.fn();
41+const readSystemdServiceRuntime = vi.fn();
42+const execFileUtf8Tail = vi.fn();
4143const buildGatewayConnectionDetails = vi.fn(
4244(_options?: {
4345configPath?: string;
@@ -65,6 +67,21 @@ vi.mock("../logging/log-tail.js", () => ({
6567) => readConfiguredLogTail(...args),
6668}));
676970+vi.mock("./logs-cli.runtime.js", () => ({
71+buildGatewayConnectionDetails: (
72+ ...args: Parameters<typeof import("../gateway/call.js").buildGatewayConnectionDetails>
73+) => buildGatewayConnectionDetails(...args),
74+readSystemdServiceRuntime: (
75+ ...args: Parameters<typeof import("../daemon/systemd.js").readSystemdServiceRuntime>
76+) => readSystemdServiceRuntime(...args),
77+execFileUtf8Tail: (
78+ ...args: Parameters<typeof import("./logs-cli.runtime.js").execFileUtf8Tail>
79+) => execFileUtf8Tail(...args),
80+resolveGatewaySystemdServiceName: (
81+ ...args: Parameters<typeof import("../daemon/constants.js").resolveGatewaySystemdServiceName>
82+) => "openclaw-gateway",
83+}));
84+6885vi.mock("../infra/backoff.js", () => ({
6986computeBackoff: vi.fn().mockReturnValue(0),
7087}));
@@ -104,10 +121,17 @@ function captureStderrWrites() {
104121}
105122106123describe("logs cli", () => {
124+beforeEach(() => {
125+readSystemdServiceRuntime.mockResolvedValue({ status: "stopped" });
126+execFileUtf8Tail.mockResolvedValue({ stdout: "", stderr: "", code: 1, truncated: false });
127+});
128+107129afterEach(() => {
108130callGatewayFromCli.mockClear();
109131readConfiguredLogTail.mockClear();
110132buildGatewayConnectionDetails.mockClear();
133+readSystemdServiceRuntime.mockClear();
134+execFileUtf8Tail.mockClear();
111135vi.restoreAllMocks();
112136});
113137@@ -132,6 +156,47 @@ describe("logs cli", () => {
132156expect(stderrWrites.join("")).toContain("Log cursor reset");
133157});
134158159+it("uses the passive local Gateway client for implicit loopback log reads", async () => {
160+callGatewayFromCli.mockResolvedValueOnce({
161+file: "/tmp/openclaw.log",
162+lines: ["raw line"],
163+});
164+165+captureStdoutWrites();
166+167+await runLogsCli(["logs"]);
168+169+expect(callGatewayFromCli).toHaveBeenCalledWith(
170+"logs.tail",
171+expect.any(Object),
172+{ cursor: undefined, limit: 200, maxBytes: 250_000 },
173+{
174+progress: true,
175+clientName: "gateway-client",
176+mode: "backend",
177+deviceIdentity: null,
178+},
179+);
180+});
181+182+it("keeps explicit Gateway URLs on the normal CLI client identity", async () => {
183+callGatewayFromCli.mockResolvedValueOnce({
184+file: "/tmp/openclaw.log",
185+lines: ["raw line"],
186+});
187+188+captureStdoutWrites();
189+190+await runLogsCli(["logs", "--url", "ws://127.0.0.1:18789"]);
191+192+expect(callGatewayFromCli).toHaveBeenCalledWith(
193+"logs.tail",
194+expect.any(Object),
195+{ cursor: undefined, limit: 200, maxBytes: 250_000 },
196+{ progress: true },
197+);
198+});
199+135200it("wires --local-time through CLI parsing and emits local timestamps", async () => {
136201callGatewayFromCli.mockResolvedValueOnce({
137202file: "/tmp/openclaw.log",
@@ -278,39 +343,101 @@ describe("logs cli", () => {
278343});
279344280345describe("--follow retry behavior", () => {
281-it("uses local fallback (not retry warning) for loopback close errors in --follow mode", async () => {
282-// Loopback close errors are absorbed by shouldUseLocalLogsFallback inside fetchLogs —
283-// they never reach the retry path, so no "gateway disconnected" warning is emitted.
284-callGatewayFromCli.mockRejectedValueOnce(
285-new GatewayTransportError({
286-kind: "closed",
287-code: 1006,
288-reason: "abnormal closure",
289-connectionDetails: {
290-url: "ws://127.0.0.1:18789",
291-urlSource: "local loopback",
292-message: "",
293-},
294-message: "gateway closed (1006 abnormal closure): abnormal closure",
295-}),
346+it("uses the active systemd journal for implicit local follow failures", async () => {
347+vi.spyOn(process, "platform", "get").mockReturnValue("linux");
348+const closeError = new GatewayTransportError({
349+kind: "closed",
350+code: 1006,
351+reason: "abnormal closure",
352+connectionDetails: {
353+url: "ws://127.0.0.1:18789",
354+urlSource: "local loopback",
355+message: "",
356+},
357+message: "gateway closed (1006 abnormal closure): abnormal closure",
358+});
359+callGatewayFromCli.mockRejectedValueOnce(closeError).mockRejectedValueOnce(closeError);
360+readSystemdServiceRuntime.mockResolvedValue({ status: "running", pid: 2557 });
361+execFileUtf8Tail
362+.mockResolvedValueOnce({
363+stdout: [
364+"Authorization: Bearer sk-abcdefghijklmnopqrstuvwxyz",
365+"-- cursor: s=abc",
366+].join("\n"),
367+stderr: "",
368+code: 0,
369+truncated: false,
370+})
371+.mockResolvedValueOnce({
372+stdout: ["second journal line", "-- cursor: s=def"].join("\n"),
373+stderr: "",
374+code: 0,
375+truncated: false,
376+});
377+378+const stderrWrites = captureStderrWrites();
379+const stdoutWrites = captureStdoutWrites();
380+const exitSpy = vi.spyOn(process, "exit").mockImplementation(() => undefined as never);
381+382+await runLogsCli(["logs", "--follow", "--interval", "1"]);
383+384+expect(readConfiguredLogTail).not.toHaveBeenCalled();
385+expect(execFileUtf8Tail).toHaveBeenCalledWith(
386+"journalctl",
387+expect.arrayContaining([
388+"--user",
389+"--boot",
390+"--user-unit=openclaw-gateway.service",
391+"_PID=2557",
392+"--output=cat",
393+"--show-cursor",
394+]),
395+expect.any(Object),
396+);
397+expect(execFileUtf8Tail).toHaveBeenNthCalledWith(
398+2,
399+"journalctl",
400+expect.arrayContaining(["--after-cursor=s=abc"]),
401+expect.any(Object),
296402);
297-readConfiguredLogTail.mockResolvedValueOnce({
298-file: "/tmp/openclaw.log",
299-cursor: 5,
300-lines: ["local fallback line"],
301-truncated: false,
302-reset: false,
403+expect(stderrWrites.join("")).toContain("reading active systemd gateway journal");
404+expect(stdoutWrites.join("")).toContain(
405+"Log source: journalctl --user --boot --user-unit=openclaw-gateway.service _PID=2557",
406+);
407+expect(stdoutWrites.join("")).toContain("Service PID: 2557");
408+expect(stdoutWrites.join("")).toContain("Service Unit: openclaw-gateway.service");
409+expect(stdoutWrites.join("")).not.toContain("sk-abcdefghijklmnopqrstuvwxyz");
410+expect(stdoutWrites.join("")).toContain("Authorization: Bearer");
411+expect(stdoutWrites.join("")).toContain("second journal line");
412+expect(exitSpy).toHaveBeenCalledWith(1);
413+});
414+415+it("retries loopback close errors in --follow mode instead of tailing fallback files", async () => {
416+const closeError = new GatewayTransportError({
417+kind: "closed",
418+code: 1006,
419+reason: "abnormal closure",
420+connectionDetails: {
421+url: "ws://127.0.0.1:18789",
422+urlSource: "local loopback",
423+message: "",
424+},
425+message: "gateway closed (1006 abnormal closure): abnormal closure",
303426});
427+for (let i = 0; i <= 8; i += 1) {
428+callGatewayFromCli.mockRejectedValueOnce(closeError);
429+}
304430305431const stderrWrites = captureStderrWrites();
306432const stdoutWrites = captureStdoutWrites();
307433const exitSpy = vi.spyOn(process, "exit").mockImplementation(() => undefined as never);
308434309435await runLogsCli(["logs", "--follow", "--interval", "1"]);
310436311-expect(stderrWrites.join("")).toContain("Local Gateway RPC unavailable");
312-expect(stderrWrites.join("")).not.toContain("gateway disconnected");
313-expect(stdoutWrites.join("")).toContain("local fallback line");
437+expect(readConfiguredLogTail).not.toHaveBeenCalled();
438+expect((stderrWrites.join("").match(/gateway disconnected/g) ?? []).length).toBe(8);
439+expect(stderrWrites.join("")).toContain("Gateway not reachable");
440+expect(stdoutWrites.join("")).not.toContain("local fallback line");
314441expect(exitSpy).toHaveBeenCalledWith(1);
315442});
316443此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。