


























@@ -409,6 +409,48 @@ describe("update-cli", () => {
409409const packageInstallCommandCall = () =>
410410commandCalls().find(([argv]) => argv[0] === "npm" && argv[1] === "i" && argv[2] === "-g");
411411412+const packagePackCommandCall = () =>
413+commandCalls().find(([argv]) => argv[0] === "npm" && argv[1] === "pack");
414+415+const stripOpenClawPackageAlias = (spec: string) => {
416+const trimmed = spec.trim();
417+return trimmed.toLowerCase().startsWith("openclaw@")
418+ ? trimmed.slice("openclaw@".length)
419+ : trimmed;
420+};
421+422+const isNpmGitPackageSpec = (spec: string) => {
423+const target = stripOpenClawPackageAlias(spec);
424+const [repo] = target.split("#", 1);
425+const isGitHubShorthand =
426+!!repo &&
427+!repo.startsWith(".") &&
428+!repo.startsWith("/") &&
429+!repo.startsWith("@") &&
430+repo.split("/").length === 2 &&
431+repo.split("/").every((part) => /^[^\s/:@]+$/u.test(part));
432+let isHttpGitUrl = false;
433+try {
434+const url = new URL(target);
435+const pathname = url.pathname.replace(/\/+$/u, "");
436+const pathParts = pathname.split("/").filter(Boolean);
437+isHttpGitUrl =
438+(url.protocol === "https:" || url.protocol === "http:") &&
439+(pathname.endsWith(".git") ||
440+(url.hostname.toLowerCase() === "github.com" && pathParts.length === 2));
441+} catch {
442+isHttpGitUrl = false;
443+}
444+return (
445+/^github:/i.test(target) ||
446+/^git(?:\+|:)/i.test(target) ||
447+/^ssh:\/\//i.test(target) ||
448+/^[^@\s]+@[^:\s]+:[^#\s]+(?:#.*)?$/u.test(target) ||
449+isHttpGitUrl ||
450+isGitHubShorthand
451+);
452+};
453+412454const doctorCommandCall = () =>
413455commandCalls().find(
414456([argv]) => argv[2] === "doctor" && argv[3] === "--non-interactive" && argv[4] === "--fix",
@@ -490,12 +532,32 @@ describe("update-cli", () => {
490532491533const expectPackageInstallSpec = (spec: string) => {
492534expect(runGatewayUpdate).not.toHaveBeenCalled();
535+let installSpec = spec;
536+if (isNpmGitPackageSpec(spec)) {
537+const packCall = packagePackCommandCall();
538+expect(packCall?.[0]).toEqual([
539+"npm",
540+"pack",
541+spec,
542+"--pack-destination",
543+expect.any(String),
544+"--json",
545+"--loglevel=error",
546+]);
547+const packDir = packCall?.[0][4];
548+if (!packDir) {
549+throw new Error("Expected package pack directory");
550+}
551+installSpec = path.join(packDir, "openclaw-9999.0.0.tgz");
552+} else {
553+expect(packagePackCommandCall()).toBeUndefined();
554+}
493555const call = packageInstallCommandCall();
494556expect(call?.[0]).toEqual([
495557"npm",
496558"i",
497559"-g",
498-spec,
560+installSpec,
499561"--no-fund",
500562"--no-audit",
501563"--loglevel=error",
@@ -655,13 +717,21 @@ describe("update-cli", () => {
655717latestVersion: "1.2.3",
656718},
657719});
658-vi.mocked(runCommandWithTimeout).mockResolvedValue({
659-stdout: "",
660-stderr: "",
661-code: 0,
662-signal: null,
663-killed: false,
664-termination: "exit",
720+vi.mocked(runCommandWithTimeout).mockImplementation(async (argv) => {
721+if (argv[0] === "npm" && argv[1] === "pack") {
722+const destination = argv[argv.indexOf("--pack-destination") + 1];
723+if (destination) {
724+await fs.writeFile(path.join(destination, "openclaw-9999.0.0.tgz"), "packed\n", "utf8");
725+}
726+}
727+return {
728+stdout: "",
729+stderr: "",
730+code: 0,
731+signal: null,
732+killed: false,
733+termination: "exit",
734+};
665735});
666736vi.spyOn(updateCliShared, "readPackageName").mockImplementation(readPackageName);
667737vi.spyOn(updateCliShared, "readPackageVersion").mockImplementation(readPackageVersion);
@@ -2108,6 +2178,73 @@ describe("update-cli", () => {
21082178},
21092179expectedSpec: "openclaw@next",
21102180},
2181+{
2182+name: "main shorthand",
2183+run: async () => {
2184+mockPackageInstallStatus(createCaseDir("openclaw-update"));
2185+await updateCommand({ yes: true, tag: "main" });
2186+},
2187+expectedSpec: "github:openclaw/openclaw#main",
2188+},
2189+{
2190+name: "explicit git package spec",
2191+run: async () => {
2192+mockPackageInstallStatus(createCaseDir("openclaw-update"));
2193+await updateCommand({ yes: true, tag: "github:openclaw/openclaw#main" });
2194+},
2195+expectedSpec: "github:openclaw/openclaw#main",
2196+},
2197+{
2198+name: "aliased git package spec",
2199+run: async () => {
2200+mockPackageInstallStatus(createCaseDir("openclaw-update"));
2201+await updateCommand({ yes: true, tag: "OpenClaw@github:openclaw/openclaw#main" });
2202+},
2203+expectedSpec: "OpenClaw@github:openclaw/openclaw#main",
2204+},
2205+{
2206+name: "full git URL package spec",
2207+run: async () => {
2208+mockPackageInstallStatus(createCaseDir("openclaw-update"));
2209+await updateCommand({ yes: true, tag: "https://github.com/openclaw/openclaw.git#main" });
2210+},
2211+expectedSpec: "https://github.com/openclaw/openclaw.git#main",
2212+},
2213+{
2214+name: "hosted GitHub URL package spec without git suffix",
2215+run: async () => {
2216+mockPackageInstallStatus(createCaseDir("openclaw-update"));
2217+await updateCommand({ yes: true, tag: "https://github.com/openclaw/openclaw#main" });
2218+},
2219+expectedSpec: "https://github.com/openclaw/openclaw#main",
2220+},
2221+{
2222+name: "aliased hosted GitHub URL package spec without git suffix",
2223+run: async () => {
2224+mockPackageInstallStatus(createCaseDir("openclaw-update"));
2225+await updateCommand({
2226+yes: true,
2227+tag: "openclaw@https://github.com/openclaw/openclaw#main",
2228+});
2229+},
2230+expectedSpec: "https://github.com/openclaw/openclaw#main",
2231+},
2232+{
2233+name: "GitHub shorthand package spec",
2234+run: async () => {
2235+mockPackageInstallStatus(createCaseDir("openclaw-update"));
2236+await updateCommand({ yes: true, tag: "openclaw/openclaw#main" });
2237+},
2238+expectedSpec: "openclaw/openclaw#main",
2239+},
2240+{
2241+name: "SCP-style SSH package spec",
2242+run: async () => {
2243+mockPackageInstallStatus(createCaseDir("openclaw-update"));
2244+await updateCommand({ yes: true, tag: "git@github.com:openclaw/openclaw.git#main" });
2245+},
2246+expectedSpec: "git@github.com:openclaw/openclaw.git#main",
2247+},
21112248{
21122249name: "OPENCLAW_UPDATE_PACKAGE_SPEC override",
21132250run: async () => {
@@ -2134,22 +2271,6 @@ describe("update-cli", () => {
21342271},
21352272);
213622732137-it.each(["main", "github:openclaw/openclaw#main", "openclaw@github:openclaw/openclaw#main"])(
2138-"rejects OpenClaw GitHub source package updates: %s",
2139-async (tag) => {
2140-mockPackageInstallStatus(createCaseDir("openclaw-update"));
2141-2142-await updateCommand({ yes: true, tag });
2143-2144-expect(packageInstallCommandCall()).toBeUndefined();
2145-expect(defaultRuntime.exit).toHaveBeenCalledWith(1);
2146-const errors = vi.mocked(defaultRuntime.error).mock.calls.map((call) => String(call[0]));
2147-expect(errors.join("\n")).toContain("Unsupported package update target");
2148-expect(errors.join("\n")).toContain("openclaw/openclaw");
2149-expect(errors.join("\n")).toContain("openclaw update --channel dev");
2150-},
2151-);
2152-21532274it("fails package updates when the installed correction version does not match the requested target", async () => {
21542275const tempDir = createCaseDir("openclaw-update");
21552276const nodeModules = path.join(tempDir, "node_modules");
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。