



























@@ -0,0 +1,111 @@
1+import fs from "node:fs/promises";
2+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
3+import { maybeOfferUpdateBeforeDoctor } from "./doctor-update.js";
4+5+const originalStdinIsTtyDescriptor = Object.getOwnPropertyDescriptor(process.stdin, "isTTY");
6+7+const mocks = vi.hoisted(() => ({
8+note: vi.fn(),
9+runCommandWithTimeout: vi.fn(),
10+runGatewayUpdate: vi.fn(),
11+}));
12+13+vi.mock("../process/exec.js", () => ({
14+runCommandWithTimeout: mocks.runCommandWithTimeout,
15+}));
16+17+vi.mock("../infra/update-runner.js", () => ({
18+runGatewayUpdate: mocks.runGatewayUpdate,
19+}));
20+21+vi.mock("../terminal/note.js", () => ({
22+note: mocks.note,
23+}));
24+25+async function runOffer(params?: {
26+root?: string;
27+confirm?: (p: { message: string; initialValue: boolean }) => Promise<boolean>;
28+}): Promise<Awaited<ReturnType<typeof maybeOfferUpdateBeforeDoctor>>> {
29+const confirm = params?.confirm ?? vi.fn().mockResolvedValue(false);
30+return await maybeOfferUpdateBeforeDoctor({
31+runtime: {} as never,
32+options: {},
33+root: params?.root ?? "/repo/link",
34+ confirm,
35+outro: vi.fn(),
36+});
37+}
38+39+beforeEach(async () => {
40+mocks.note.mockReset();
41+mocks.runCommandWithTimeout.mockReset();
42+mocks.runGatewayUpdate.mockReset();
43+Object.defineProperty(process.stdin, "isTTY", {
44+configurable: true,
45+value: true,
46+});
47+});
48+49+afterEach(() => {
50+vi.restoreAllMocks();
51+if (originalStdinIsTtyDescriptor) {
52+Object.defineProperty(process.stdin, "isTTY", originalStdinIsTtyDescriptor);
53+} else {
54+delete (process.stdin as Partial<typeof process.stdin>).isTTY;
55+}
56+});
57+58+describe("maybeOfferUpdateBeforeDoctor", () => {
59+it("treats a linked package root as a git checkout when realpaths match", async () => {
60+const confirm = vi.fn().mockResolvedValue(false);
61+vi.spyOn(fs, "realpath").mockImplementation(async (candidate) => {
62+const value = String(candidate);
63+if (value === "/repo/link" || value === "/repo/real") {
64+return "/repo/real";
65+}
66+return value;
67+});
68+mocks.runCommandWithTimeout.mockResolvedValue({
69+stdout: "/repo/real\n",
70+stderr: "",
71+code: 0,
72+killed: false,
73+signal: null,
74+termination: "exit",
75+noOutputTimedOut: false,
76+});
77+78+await expect(runOffer({ root: "/repo/link", confirm })).resolves.toEqual({ updated: false });
79+80+expect(confirm).toHaveBeenCalledWith({
81+message: "Update OpenClaw from git before running doctor?",
82+initialValue: true,
83+});
84+expect(mocks.note).not.toHaveBeenCalledWith(
85+expect.stringContaining("This install is not a git checkout."),
86+"Update",
87+);
88+});
89+90+it("keeps package-manager guidance when git reports a different checkout", async () => {
91+const confirm = vi.fn();
92+vi.spyOn(fs, "realpath").mockImplementation(async (candidate) => String(candidate));
93+mocks.runCommandWithTimeout.mockResolvedValue({
94+stdout: "/repo/other\n",
95+stderr: "",
96+code: 0,
97+killed: false,
98+signal: null,
99+termination: "exit",
100+noOutputTimedOut: false,
101+});
102+103+await expect(runOffer({ root: "/repo/link", confirm })).resolves.toEqual({ updated: false });
104+105+expect(confirm).not.toHaveBeenCalled();
106+expect(mocks.note).toHaveBeenCalledWith(
107+expect.stringContaining("This install is not a git checkout."),
108+"Update",
109+);
110+});
111+});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。