

























@@ -1,4 +1,9 @@
1+import { createHash } from "node:crypto";
2+import fs from "node:fs/promises";
3+import os from "node:os";
4+import path from "node:path";
15import { describe, expect, it, vi, beforeEach } from "vitest";
6+import { redactSensitiveUrlLikeString } from "../shared/net/redact-sensitive-url.js";
2738const runCommandWithTimeoutMock = vi.fn();
49const installPluginFromInstalledPackageDirMock = vi.fn();
@@ -20,6 +25,14 @@ vi.resetModules();
20252126const { installPluginFromGitSpec, parseGitPluginSpec } = await import("./git-install.js");
222728+function expectedGitRepoDir(params: { gitDir: string; normalizedSpec: string }): string {
29+const hash = createHash("sha256")
30+.update(redactSensitiveUrlLikeString(params.normalizedSpec))
31+.digest("hex")
32+.slice(0, 16);
33+return path.join(params.gitDir, `git-${hash}`, "repo");
34+}
35+2336describe("parseGitPluginSpec", () => {
2437it("normalizes GitHub shorthand and ref selectors", () => {
2538expect(parseGitPluginSpec("git:github.com/acme/demo@v1.2.3")).toMatchObject({
@@ -55,13 +68,18 @@ describe("installPluginFromGitSpec", () => {
5568.mockResolvedValueOnce({ code: 0, stdout: "", stderr: "" })
5669.mockResolvedValueOnce({ code: 0, stdout: "abc123\n", stderr: "" })
5770.mockResolvedValueOnce({ code: 0, stdout: "", stderr: "" });
58-installPluginFromInstalledPackageDirMock.mockResolvedValue({
59-ok: true,
60-pluginId: "demo",
61-targetDir: "/tmp/git-root/repo",
62-version: "1.2.3",
63-extensions: ["index.js"],
64-});
71+installPluginFromInstalledPackageDirMock.mockImplementation(
72+async (params: { packageDir: string }) => {
73+await fs.mkdir(params.packageDir, { recursive: true });
74+return {
75+ok: true,
76+pluginId: "demo",
77+targetDir: params.packageDir,
78+version: "1.2.3",
79+extensions: ["index.js"],
80+};
81+},
82+);
65836684const result = await installPluginFromGitSpec({
6785spec: "git:github.com/acme/demo@v1.2.3",
@@ -115,13 +133,18 @@ describe("installPluginFromGitSpec", () => {
115133.mockResolvedValueOnce({ code: 0, stdout: "", stderr: "" })
116134.mockResolvedValueOnce({ code: 0, stdout: "abc123\n", stderr: "" })
117135.mockResolvedValueOnce({ code: 0, stdout: "", stderr: "" });
118-installPluginFromInstalledPackageDirMock.mockResolvedValue({
119-ok: true,
120-pluginId: "demo",
121-targetDir: "/tmp/git-root/repo",
122-version: "1.2.3",
123-extensions: ["index.js"],
124-});
136+installPluginFromInstalledPackageDirMock.mockImplementation(
137+async (params: { packageDir: string }) => {
138+await fs.mkdir(params.packageDir, { recursive: true });
139+return {
140+ok: true,
141+pluginId: "demo",
142+targetDir: params.packageDir,
143+version: "1.2.3",
144+extensions: ["index.js"],
145+};
146+},
147+);
125148126149await installPluginFromGitSpec({ spec: "git:github.com/acme/demo" });
127150@@ -134,4 +157,76 @@ describe("installPluginFromGitSpec", () => {
134157expect.stringContaining("/repo"),
135158]);
136159});
160+161+it("uses a credential-free managed repo path for authenticated git URLs", async () => {
162+const gitDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-git-install-path-"));
163+try {
164+runCommandWithTimeoutMock
165+.mockResolvedValueOnce({ code: 0, stdout: "", stderr: "" })
166+.mockResolvedValueOnce({ code: 0, stdout: "abc123\n", stderr: "" })
167+.mockResolvedValueOnce({ code: 0, stdout: "", stderr: "" });
168+installPluginFromInstalledPackageDirMock.mockImplementation(
169+async (params: { packageDir: string }) => {
170+await fs.mkdir(params.packageDir, { recursive: true });
171+return {
172+ok: true,
173+pluginId: "demo",
174+targetDir: params.packageDir,
175+version: "1.2.3",
176+extensions: ["index.js"],
177+};
178+},
179+);
180+181+const result = await installPluginFromGitSpec({
182+spec: "git:https://token@github.com/acme/demo.git",
183+ gitDir,
184+});
185+186+expect(result.ok).toBe(true);
187+if (!result.ok) {
188+throw new Error(result.error);
189+}
190+expect(result.targetDir).toBe(
191+expectedGitRepoDir({
192+ gitDir,
193+normalizedSpec: "git:https://token@github.com/acme/demo.git",
194+}),
195+);
196+expect(result.targetDir).not.toContain("token");
197+expect(result.targetDir).not.toContain("github.com");
198+} finally {
199+await fs.rm(gitDir, { recursive: true, force: true });
200+}
201+});
202+203+it("keeps the existing managed repo when replacement install fails", async () => {
204+const gitDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-git-install-preserve-"));
205+const normalizedSpec = "git:https://github.com/acme/demo.git";
206+const existingRepoDir = expectedGitRepoDir({ gitDir, normalizedSpec });
207+const markerPath = path.join(existingRepoDir, "existing.txt");
208+try {
209+await fs.mkdir(existingRepoDir, { recursive: true });
210+await fs.writeFile(markerPath, "keep");
211+runCommandWithTimeoutMock
212+.mockResolvedValueOnce({ code: 0, stdout: "", stderr: "" })
213+.mockResolvedValueOnce({ code: 0, stdout: "abc123\n", stderr: "" })
214+.mockResolvedValueOnce({ code: 1, stdout: "", stderr: "npm failed" });
215+216+const result = await installPluginFromGitSpec({
217+spec: "git:https://github.com/acme/demo.git",
218+ gitDir,
219+mode: "update",
220+});
221+222+expect(result.ok).toBe(false);
223+if (!result.ok) {
224+expect(result.error).toContain("npm install failed");
225+}
226+await expect(fs.readFile(markerPath, "utf8")).resolves.toBe("keep");
227+expect(installPluginFromInstalledPackageDirMock).not.toHaveBeenCalled();
228+} finally {
229+await fs.rm(gitDir, { recursive: true, force: true });
230+}
231+});
137232});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。