diff --git a/scripts/e2e/parallels/npm-update-smoke.ts b/scripts/e2e/parallels/npm-update-smoke.ts index 65e4d1983041..02809f96a874 100755 --- a/scripts/e2e/parallels/npm-update-smoke.ts +++ b/scripts/e2e/parallels/npm-update-smoke.ts @@ -439,6 +439,39 @@ function parseOpenClawPackageSpecVersion(spec: string): string { return resolveOpenClawRegistryVersion(value) || ""; } +function readString(value: unknown): string { + return typeof value === "string" ? value : ""; +} + +function isRecord(value: unknown): value is Record { + return value !== null && typeof value === "object" && !Array.isArray(value); +} + +export function parseRegistryPackageMetadata(raw: string): { + gitHead: string; + tarball: string; + version: string; +} { + const value = raw.trim(); + if (!value) { + return { gitHead: "", tarball: "", version: "" }; + } + try { + const parsed = JSON.parse(value) as unknown; + if (!isRecord(parsed)) { + return { gitHead: "", tarball: "", version: "" }; + } + const dist = isRecord(parsed.dist) ? parsed.dist : {}; + return { + gitHead: readString(parsed.gitHead), + tarball: readString(parsed["dist.tarball"]) || readString(dist.tarball), + version: readString(parsed.version), + }; + } catch { + return { gitHead: "", tarball: "", version: "" }; + } +} + export class NpmUpdateSmoke { private auth: ProviderAuth; private windowsAuth: ProviderAuth; @@ -773,20 +806,7 @@ export class NpmUpdateSmoke { if (!output) { return { gitHead: "", tarball: "", version: "" }; } - try { - const parsed = JSON.parse(output) as { - dist?: { tarball?: string }; - gitHead?: string; - version?: string; - }; - return { - gitHead: parsed.gitHead ?? "", - tarball: parsed.dist?.tarball ?? "", - version: parsed.version ?? "", - }; - } catch { - return { gitHead: "", tarball: "", version: "" }; - } + return parseRegistryPackageMetadata(output); } private async runSameGuestUpdates(): Promise { diff --git a/test/scripts/parallels-npm-update-smoke.test.ts b/test/scripts/parallels-npm-update-smoke.test.ts index 528fdf7d3baa..828ebddf496e 100644 --- a/test/scripts/parallels-npm-update-smoke.test.ts +++ b/test/scripts/parallels-npm-update-smoke.test.ts @@ -13,6 +13,7 @@ import { import { freshLaneTimeoutMs, NpmUpdateSmoke, + parseRegistryPackageMetadata, parseArgs, spawnLoggedCommand, } from "../../scripts/e2e/parallels/npm-update-smoke.ts"; @@ -210,6 +211,35 @@ exit 1 expect(script).toContain("this.updateExpectedNeedle = this.targetTarballVersion"); }); + it("accepts keyed and nested npm metadata for published update targets", () => { + expect( + parseRegistryPackageMetadata( + JSON.stringify({ + version: "2026.5.20-beta.1", + "dist.tarball": "https://registry.example/openclaw-keyed.tgz", + gitHead: "abcdef0123456789", + }), + ), + ).toEqual({ + version: "2026.5.20-beta.1", + tarball: "https://registry.example/openclaw-keyed.tgz", + gitHead: "abcdef0123456789", + }); + + expect( + parseRegistryPackageMetadata( + JSON.stringify({ + version: "2026.5.20-beta.1", + dist: { tarball: "https://registry.example/openclaw-nested.tgz" }, + }), + ), + ).toEqual({ + version: "2026.5.20-beta.1", + tarball: "https://registry.example/openclaw-nested.tgz", + gitHead: "", + }); + }); + it("guards beta validation against cross-version harness checkouts", () => { const script = readFileSync(SCRIPT_PATH, "utf8");