diff --git a/.github/workflows/openclaw-cross-os-release-checks-reusable.yml b/.github/workflows/openclaw-cross-os-release-checks-reusable.yml index 4cf175db7e3b..392d70ab4812 100644 --- a/.github/workflows/openclaw-cross-os-release-checks-reusable.yml +++ b/.github/workflows/openclaw-cross-os-release-checks-reusable.yml @@ -641,10 +641,11 @@ jobs: env: BASELINE_PACK_JSON: ${{ runner.temp }}/openclaw-cross-os-release-checks/prepare/baseline/pack.json run: | - node <<'NODE' >>"$GITHUB_OUTPUT" - const crypto = require("node:crypto"); - const fs = require("node:fs"); - const path = require("node:path"); + node --input-type=module <<'NODE' >>"$GITHUB_OUTPUT" + import crypto from "node:crypto"; + import fs from "node:fs"; + import path from "node:path"; + import { resolveNpmJsonEntries } from "./scripts/lib/npm-json-output.mjs"; function resolveTarballFileName(value, label) { const fileName = typeof value === "string" ? value.trim() : ""; if ( @@ -658,7 +659,7 @@ jobs: return fileName; } const payload = JSON.parse(fs.readFileSync(process.env.BASELINE_PACK_JSON, "utf8")); - const entry = Array.isArray(payload) ? payload.at(-1) : null; + const entry = resolveNpmJsonEntries(payload).at(-1); const fileName = resolveTarballFileName(entry?.filename, "Baseline npm pack filename"); const sha256 = crypto .createHash("sha256") diff --git a/.github/workflows/plugin-npm-release.yml b/.github/workflows/plugin-npm-release.yml index 7fa92d412124..bb46d8db0edc 100644 --- a/.github/workflows/plugin-npm-release.yml +++ b/.github/workflows/plugin-npm-release.yml @@ -11,6 +11,7 @@ on: - "package.json" - "scripts/generate-npm-package-lock.mjs" - "scripts/lib/npm-publish-plan.mjs" + - "scripts/lib/npm-json-output.mjs" - "scripts/lib/release-version.mjs" - "scripts/lib/plugin-npm-package-manifest.mjs" - "scripts/lib/plugin-npm-release.ts" @@ -338,6 +339,7 @@ jobs: fetch-depth: 1 sparse-checkout: | scripts/generate-npm-package-lock.mjs + scripts/lib/npm-json-output.mjs scripts/lib/plugin-npm-package-manifest.mjs sparse-checkout-cone-mode: false @@ -347,6 +349,9 @@ jobs: cp \ .release-tooling/scripts/generate-npm-package-lock.mjs \ scripts/generate-npm-package-lock.mjs + cp \ + .release-tooling/scripts/lib/npm-json-output.mjs \ + scripts/lib/npm-json-output.mjs cp \ .release-tooling/scripts/lib/plugin-npm-package-manifest.mjs \ scripts/lib/plugin-npm-package-manifest.mjs @@ -398,26 +403,27 @@ jobs: OPENCLAW_PLUGIN_NPM_PACK_OUTPUT_DIR="${artifact_dir}" \ bash scripts/plugin-npm-publish.sh --pack "${PACKAGE_DIR}" > "${pack_output}" - node - "${pack_output}" "${pack_json}" <<'NODE' - const fs = require("node:fs"); + node --input-type=module - "${pack_output}" "${pack_json}" <<'NODE' + import fs from "node:fs"; + import { resolveNpmJsonEntries } from "./scripts/lib/npm-json-output.mjs"; + const raw = fs.readFileSync(process.argv[2], "utf8").trim(); let pack; - for (let index = raw.lastIndexOf("["); index >= 0; index = raw.lastIndexOf("[", index - 1)) { + for (let index = raw.length - 1; index >= 0; index -= 1) { + if (raw[index] !== "[" && raw[index] !== "{") continue; try { const candidate = JSON.parse(raw.slice(index)); - if (Array.isArray(candidate)) { - pack = candidate; + const entries = resolveNpmJsonEntries(candidate); + if (entries.length > 0) { + pack = entries; break; } } catch { // npm can print bundled-dependency summaries before its JSON result. } - if (index === 0) { - break; - } } if (!pack) { - throw new Error("npm pack did not emit a JSON array."); + throw new Error("npm pack did not emit package JSON."); } fs.writeFileSync(process.argv[3], `${JSON.stringify(pack, null, 2)}\n`); NODE @@ -1238,6 +1244,9 @@ jobs: cp \ scripts/generate-npm-package-lock.mjs \ .publication-target/scripts/generate-npm-package-lock.mjs + cp \ + scripts/lib/npm-json-output.mjs \ + .publication-target/scripts/lib/npm-json-output.mjs cp \ scripts/lib/plugin-npm-package-manifest.mjs \ .publication-target/scripts/lib/plugin-npm-package-manifest.mjs diff --git a/scripts/e2e/parallels/package-artifact.ts b/scripts/e2e/parallels/package-artifact.ts index 45bd2be29bb3..6637d91b1e51 100644 --- a/scripts/e2e/parallels/package-artifact.ts +++ b/scripts/e2e/parallels/package-artifact.ts @@ -3,6 +3,7 @@ import { randomUUID } from "node:crypto"; import { mkdir, readFile, rm, stat, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import path from "node:path"; +import { resolveNpmJsonEntries } from "../../lib/npm-json-output.mjs"; import { sleep as delay } from "../../lib/sleep.mjs"; import { readPositiveIntEnv } from "./env-limits.ts"; import { exists, readJson } from "./filesystem.ts"; @@ -28,12 +29,7 @@ export async function packageBuildCommitFromTgz(tgzPath: string): Promise Boolean(entry) && typeof entry === "object" && !Array.isArray(entry), + ); + if (entries.length > 0) { + return entries; + } + } + } + return [value]; +} diff --git a/scripts/lib/npm-pack-budget.d.mts b/scripts/lib/npm-pack-budget.d.mts index 8a3b3a9916e6..318bf5fcb01d 100644 --- a/scripts/lib/npm-pack-budget.d.mts +++ b/scripts/lib/npm-pack-budget.d.mts @@ -3,8 +3,13 @@ export type NpmPackBudgetResult = { unpackedSize?: number; }; +export type NpmPackBudgetResults = + | Iterable + | NpmPackBudgetResult + | Record; + export declare function collectPackUnpackedSizeErrors( - results: Iterable, + results: NpmPackBudgetResults, options?: { budgetBytes?: number; missingDataMessage?: string; diff --git a/scripts/lib/npm-pack-budget.mjs b/scripts/lib/npm-pack-budget.mjs index 7798ce0e55df..c0202ca2dc70 100644 --- a/scripts/lib/npm-pack-budget.mjs +++ b/scripts/lib/npm-pack-budget.mjs @@ -1,3 +1,5 @@ +import { resolveNpmJsonEntries } from "./npm-json-output.mjs"; + // 2026.3.12 ballooned to ~213.6 MiB unpacked and correlated with low-memory // startup/doctor OOM reports. 2026.4.12 intentionally stages Matrix runtime // dependencies, including crypto wasm, so packaged installs do not miss Docker @@ -22,12 +24,15 @@ function formatPackUnpackedSizeBudgetError(params) { } export function collectPackUnpackedSizeErrors(results, options = {}) { - const entries = Array.from(results); + const entries = resolveNpmJsonEntries(results); const errors = []; const budgetBytes = options.budgetBytes ?? NPM_PACK_UNPACKED_SIZE_BUDGET_BYTES; let checkedCount = 0; for (const [index, entry] of entries.entries()) { + if (!entry || typeof entry !== "object" || Array.isArray(entry)) { + continue; + } if (typeof entry.unpackedSize !== "number" || !Number.isFinite(entry.unpackedSize)) { continue; } diff --git a/scripts/openclaw-npm-release-check.ts b/scripts/openclaw-npm-release-check.ts index 9dd14821b183..f51437492711 100644 --- a/scripts/openclaw-npm-release-check.ts +++ b/scripts/openclaw-npm-release-check.ts @@ -5,6 +5,7 @@ import { execFileSync } from "node:child_process"; import { readFileSync } from "node:fs"; import { join } from "node:path"; import { pathToFileURL } from "node:url"; +import { resolveNpmJsonEntries } from "./lib/npm-json-output.mjs"; import { resolveNpmDistTagMirrorAuth as resolveNpmDistTagMirrorAuthBase } from "./lib/npm-publish-plan.mjs"; import { readPositiveEnvInt } from "./lib/numeric-options.mjs"; import { @@ -575,16 +576,20 @@ export function parseNpmPackJsonOutput(stdout: string): NpmPackResult[] | null { } const candidates = [trimmed]; - const trailingArrayStart = trimmed.lastIndexOf("\n["); - if (trailingArrayStart !== -1) { - candidates.push(trimmed.slice(trailingArrayStart + 1).trim()); + const trailingJsonStart = Math.max(trimmed.lastIndexOf("\n["), trimmed.lastIndexOf("\n{")); + if (trailingJsonStart !== -1) { + candidates.push(trimmed.slice(trailingJsonStart + 1).trim()); } for (const candidate of candidates) { try { const parsed = JSON.parse(candidate) as unknown; - if (Array.isArray(parsed)) { - return parsed as NpmPackResult[]; + const entries = resolveNpmJsonEntries(parsed).filter( + (entry): entry is NpmPackResult => + Boolean(entry) && typeof entry === "object" && !Array.isArray(entry), + ); + if (entries.length > 0) { + return entries; } } catch { // Try the next candidate. npm lifecycle output can prepend non-JSON logs. diff --git a/scripts/package-openclaw-for-docker.mjs b/scripts/package-openclaw-for-docker.mjs index f1641a9560d4..39743ac8f51f 100644 --- a/scripts/package-openclaw-for-docker.mjs +++ b/scripts/package-openclaw-for-docker.mjs @@ -9,6 +9,7 @@ import path from "node:path"; import { fileURLToPath, pathToFileURL } from "node:url"; import { DOCKER_SELECTED_PLUGIN_BUILD_IDS_ENV } from "./lib/bundled-plugin-build-entries.mjs"; import { terminateManagedChild } from "./lib/managed-child-process.mjs"; +import { resolveNpmJsonEntries } from "./lib/npm-json-output.mjs"; import { resolveNpmRunner } from "./npm-runner.mjs"; import { preparePackageChangelog, restorePackageChangelog } from "./package-changelog.mjs"; import { resolvePnpmRunner } from "./pnpm-runner.mjs"; @@ -416,15 +417,13 @@ async function newestOpenClawTarball(outputDir, packOutput) { let fromOutput = ""; try { const parsed = JSON.parse(packOutput); - if (Array.isArray(parsed)) { - for (const entry of parsed) { - if (typeof entry?.filename !== "string") { - continue; - } - const filename = resolvePackedOpenClawFileName(entry.filename); - if (filename) { - fromOutput = filename; - } + for (const entry of resolveNpmJsonEntries(parsed)) { + if (typeof entry?.filename !== "string") { + continue; + } + const filename = resolvePackedOpenClawFileName(entry.filename); + if (filename) { + fromOutput = filename; } } } catch {} @@ -465,18 +464,22 @@ async function writePackJson(packOutput, tarball, packJsonPath, sourceDir) { } catch (error) { throw new Error("npm pack --json output was not valid JSON", { cause: error }); } - if (!Array.isArray(parsed)) { - throw new Error("npm pack --json output must be an array"); + const entries = resolveNpmJsonEntries(parsed); + if ( + entries.length === 0 || + entries.some((entry) => !entry || typeof entry !== "object" || Array.isArray(entry)) + ) { + throw new Error("npm pack --json output did not contain package results"); } const filename = path.basename(tarball); - for (const entry of parsed) { + for (const entry of entries) { if (entry && typeof entry === "object" && typeof entry.filename === "string") { entry.filename = filename; } } const target = path.resolve(sourceDir, packJsonPath); await fs.mkdir(path.dirname(target), { recursive: true }); - await fs.writeFile(target, `${JSON.stringify(parsed, null, 2)}\n`); + await fs.writeFile(target, `${JSON.stringify(entries, null, 2)}\n`); } async function cleanPackedOpenClawTarballs(outputDir) { diff --git a/scripts/release-check.ts b/scripts/release-check.ts index 0a43e5d8feef..b04a7c9235ca 100755 --- a/scripts/release-check.ts +++ b/scripts/release-check.ts @@ -31,6 +31,7 @@ import { collectRootPackageExcludedExtensionDirs, listBundledPluginPackArtifacts, } from "./lib/bundled-plugin-build-entries.mjs"; +import { resolveNpmJsonEntries } from "./lib/npm-json-output.mjs"; import { collectPackUnpackedSizeErrors as collectNpmPackUnpackedSizeErrors } from "./lib/npm-pack-budget.mjs"; import { readPositiveEnvInt } from "./lib/numeric-options.mjs"; import { @@ -397,7 +398,7 @@ function runPackDry(): PackResult[] { stdio: ["ignore", "pipe", "pipe"], maxBuffer: 1024 * 1024 * 100, }); - return JSON.parse(raw) as PackResult[]; + return resolveNpmJsonEntries(JSON.parse(raw)) as PackResult[]; } function runPack(packDestination: string, cwd?: string): PackResult[] { @@ -410,8 +411,7 @@ function runPack(packDestination: string, cwd?: string): PackResult[] { maxBuffer: 1024 * 1024 * 100, }, ); - const parsed = JSON.parse(raw) as PackResult | PackResult[]; - return Array.isArray(parsed) ? parsed : [parsed]; + return resolveNpmJsonEntries(JSON.parse(raw)) as PackResult[]; } export function resolvePackedTarballPath(packDestination: string, results: PackResult[]): string { diff --git a/scripts/resolve-openclaw-package-candidate.mjs b/scripts/resolve-openclaw-package-candidate.mjs index 7cca13fb0683..1d71325814f7 100644 --- a/scripts/resolve-openclaw-package-candidate.mjs +++ b/scripts/resolve-openclaw-package-candidate.mjs @@ -13,6 +13,7 @@ import os from "node:os"; import path from "node:path"; import { pipeline } from "node:stream/promises"; import { fileURLToPath } from "node:url"; +import { resolveNpmJsonEntries } from "./lib/npm-json-output.mjs"; import { resolveWindowsTaskkillPath } from "./lib/windows-taskkill.mjs"; import { resolveNpmRunner } from "./npm-runner.mjs"; @@ -660,9 +661,10 @@ async function moveNewestPackedTarball(outputDir, packOutput, outputName) { try { parsed = JSON.parse(packOutput); } catch {} - if (Array.isArray(parsed)) { + if (parsed !== undefined) { const packedFilename = - parsed.find((entry) => typeof entry?.filename === "string")?.filename ?? ""; + resolveNpmJsonEntries(parsed).find((entry) => typeof entry?.filename === "string") + ?.filename ?? ""; if (packedFilename) { filename = resolvePackedOpenClawTarballFilename(packedFilename); } diff --git a/scripts/test-install-sh-docker.sh b/scripts/test-install-sh-docker.sh index 7b3703ba3a03..e8c948daf4f0 100755 --- a/scripts/test-install-sh-docker.sh +++ b/scripts/test-install-sh-docker.sh @@ -11,6 +11,25 @@ source "$HARNESS_ROOT/scripts/lib/docker-e2e-package.sh" DOCKER_COMMAND_TIMEOUT="${DOCKER_COMMAND_TIMEOUT:-${OPENCLAW_INSTALL_SMOKE_DOCKER_COMMAND_TIMEOUT:-600s}}" INSTALL_SMOKE_DOCKER_RUN_TIMEOUT="${OPENCLAW_INSTALL_SMOKE_DOCKER_RUN_TIMEOUT:-2700s}" +normalize_npm_pack_json_file() { + local pack_json_file="$1" + ( + cd "$HARNESS_ROOT" + node --input-type=module - "$pack_json_file" <<'NODE' +import fs from "node:fs"; +import { resolveNpmJsonEntries } from "./scripts/lib/npm-json-output.mjs"; + +const packJsonFile = process.argv[2]; +const parsed = JSON.parse(fs.readFileSync(packJsonFile, "utf8")); +const entries = resolveNpmJsonEntries(parsed); +if (entries.length === 0) { + throw new Error("npm pack output did not contain a package result"); +} +fs.writeFileSync(packJsonFile, `${JSON.stringify(entries, null, 2)}\n`, "utf8"); +NODE + ) +} + run_install_smoke_container() { DOCKER_COMMAND_TIMEOUT="$INSTALL_SMOKE_DOCKER_RUN_TIMEOUT" docker_e2e_docker_run_cmd run "$@" } @@ -311,6 +330,7 @@ prepare_update_tarball() { if [[ -n "$UPDATE_PACKAGE_SPEC" ]]; then echo "==> Pack update tgz from spec: $UPDATE_PACKAGE_SPEC" quiet_npm pack "$UPDATE_PACKAGE_SPEC" --json --pack-destination "$UPDATE_DIR" >"$pack_json_file" + normalize_npm_pack_json_file "$pack_json_file" else echo "==> Build local release artifacts for update smoke" if [[ -n "$UPDATE_DIST_IMAGE" ]]; then @@ -366,6 +386,7 @@ process.stdout.write(last.version); echo "==> Pack baseline tgz: ${PACKAGE_NAME}@${UPDATE_BASELINE_VERSION}" quiet_npm pack "${PACKAGE_NAME}@${UPDATE_BASELINE_VERSION}" --json --pack-destination "$UPDATE_DIR" >"$baseline_pack_json_file" + normalize_npm_pack_json_file "$baseline_pack_json_file" BASELINE_TGZ_FILE="$(read_pack_tarball_filename "$baseline_pack_json_file")" UPDATE_BASELINE_VERSION="$( node -e ' diff --git a/src/plugins/npm-install-security-scan.release.test.ts b/src/plugins/npm-install-security-scan.release.test.ts index 15507ba4bac8..0b8f80fcf73f 100644 --- a/src/plugins/npm-install-security-scan.release.test.ts +++ b/src/plugins/npm-install-security-scan.release.test.ts @@ -5,6 +5,7 @@ import { tmpdir } from "node:os"; import { dirname, join, relative, resolve } from "node:path"; import { promisify } from "node:util"; import { beforeAll, describe, expect, it, test } from "vitest"; +import { resolveNpmJsonEntries } from "../infra/npm-registry-spec.js"; import { isScannable, scanDirectoryWithSummary } from "../skills/security/scanner.js"; import { expectNoReaddirSyncDuring } from "../test-utils/fs-scan-assertions.js"; import { listGitTrackedFiles, toRepoPath, toRepoRelativePath } from "../test-utils/repo-files.js"; @@ -55,11 +56,12 @@ const OPTIONAL_REVIEWED_PUBLISHABLE_DIST_CRITICAL_FINDING_COUNTS = new Map { "scripts/lib/bundled-plugin-build-entries.mjs", "scripts/lib/bundled-plugin-paths.mjs", "scripts/lib/managed-child-process.mjs", + "scripts/lib/npm-json-output.mjs", "scripts/lib/optional-bundled-clusters.mjs", "scripts/lib/windows-taskkill.mjs", ]; @@ -794,7 +795,7 @@ describe("package-openclaw-for-docker", () => { } }); - it("writes npm pack metadata for renamed package artifacts", async () => { + it("normalizes npm 12 pack metadata for renamed package artifacts", async () => { const outputDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-docker-pack-json-")); const packJsonPath = path.join(outputDir, "pack.json"); @@ -823,15 +824,15 @@ describe("package-openclaw-for-docker", () => { ]); expect(options.deferForwardedSignalExit).toBe(true); fs.writeFileSync(path.join(outputDir, "openclaw-2026.5.28.tgz"), "package"); - return JSON.stringify([ - { + return JSON.stringify({ + openclaw: { entryCount: 1, filename: "openclaw-2026.5.28.tgz", size: 7, unpackedSize: 7, version: "2026.5.28", }, - ]); + }); }, }); diff --git a/test/openclaw-npm-release-check.test.ts b/test/openclaw-npm-release-check.test.ts index 45e16a3d0ba8..fbd927027842 100644 --- a/test/openclaw-npm-release-check.test.ts +++ b/test/openclaw-npm-release-check.test.ts @@ -544,6 +544,28 @@ describe("parseNpmPackJsonOutput", () => { ]); }); + it("parses npm 12 name-keyed pack output", () => { + expect( + parseNpmPackJsonOutput( + '{"openclaw":{"filename":"openclaw.tgz","files":[{"path":"dist/control-ui/index.html"}]}}', + ), + ).toEqual([ + { + filename: "openclaw.tgz", + files: [{ path: "dist/control-ui/index.html" }], + }, + ]); + }); + + it("parses trailing npm 12 output after lifecycle logs", () => { + const stdout = [ + "> openclaw@2026.7.2 prepack", + '{"openclaw":{"filename":"openclaw.tgz","files":[]}}', + ].join("\n"); + + expect(parseNpmPackJsonOutput(stdout)).toEqual([{ filename: "openclaw.tgz", files: [] }]); + }); + it("parses the trailing JSON payload after npm lifecycle logs", () => { const stdout = [ 'npm warn Unknown project config "node-linker".', diff --git a/test/release-check.test.ts b/test/release-check.test.ts index 81b1bc87295f..91703ff5f7c1 100644 --- a/test/release-check.test.ts +++ b/test/release-check.test.ts @@ -5,6 +5,7 @@ import { dirname, join, resolve as resolvePath, win32 } from "node:path"; import { bundledDistPluginFile, bundledPluginFile } from "openclaw/plugin-sdk/test-fixtures"; import { describe, expect, it } from "vitest"; import { listBundledPluginPackArtifacts } from "../scripts/lib/bundled-plugin-build-entries.mjs"; +import { resolveNpmJsonEntries } from "../scripts/lib/npm-json-output.mjs"; import { LOCAL_BUILD_METADATA_DIST_PATHS, PACKAGE_DIST_INVENTORY_RELATIVE_PATH, @@ -48,6 +49,7 @@ import { } from "../scripts/release-check.ts"; import { listStaticExtensionAssetOutputs } from "../scripts/runtime-postbuild.mjs"; import { COMPLETION_SKIP_PLUGIN_COMMANDS_ENV } from "../src/cli/completion-runtime.ts"; +import { resolveNpmJsonEntries as resolveRuntimeNpmJsonEntries } from "../src/infra/npm-registry-spec.js"; import { withEnv } from "../src/test-utils/env.js"; function makeItem(shortVersion: string, sparkleVersion: string, channel?: string): string { @@ -911,6 +913,14 @@ describe("collectPackUnpackedSizeErrors", () => { ).toStrictEqual([]); }); + it("accepts npm 12 name-keyed pack results", () => { + expect( + collectPackUnpackedSizeErrors({ + openclaw: makePackResult("openclaw-2026.3.14.tgz", 120_354_302), + }), + ).toStrictEqual([]); + }); + it("flags oversized pack results that risk low-memory startup failures", () => { expect( collectPackUnpackedSizeErrors([makePackResult("openclaw-2026.3.12.tgz", 224_002_564)]), @@ -931,6 +941,20 @@ describe("collectPackUnpackedSizeErrors", () => { }); }); +describe("resolveNpmJsonEntries", () => { + it("normalizes npm <=11 arrays and npm 12 name-keyed objects", () => { + const entry = makePackResult("openclaw-2026.7.2.tgz", 120_354_302); + + expect(resolveNpmJsonEntries([entry])).toEqual([entry]); + expect(resolveNpmJsonEntries(entry)).toEqual([entry]); + expect(resolveNpmJsonEntries({ openclaw: entry })).toEqual([entry]); + expect(resolveNpmJsonEntries({ "@openclaw/demo": entry })).toEqual([entry]); + expect(resolveNpmJsonEntries({ openclaw: entry })).toEqual( + resolveRuntimeNpmJsonEntries({ openclaw: entry }), + ); + }); +}); + describe("resolvePackedTarballPath", () => { it("resolves one local npm pack tarball filename inside the pack destination", () => { expect( diff --git a/test/scripts/openclaw-cross-os-release-workflow.test.ts b/test/scripts/openclaw-cross-os-release-workflow.test.ts index 4aaab75ca1c2..6dfb81710fc6 100644 --- a/test/scripts/openclaw-cross-os-release-workflow.test.ts +++ b/test/scripts/openclaw-cross-os-release-workflow.test.ts @@ -65,6 +65,10 @@ describe("cross-OS release checks workflow", () => { const workflow = readFileSync(WORKFLOW_PATH, "utf8"); expect(workflow).toContain("timeout --preserve-status 300s npm pack --ignore-scripts"); + expect(workflow).toContain( + 'import { resolveNpmJsonEntries } from "./scripts/lib/npm-json-output.mjs";', + ); + expect(workflow).toContain("const entry = resolveNpmJsonEntries(payload).at(-1);"); }); it("keeps release artifact tarball filenames local before upload paths use them", () => { diff --git a/test/scripts/plugin-npm-extended-stable-workflow.test.ts b/test/scripts/plugin-npm-extended-stable-workflow.test.ts index 923638f3365a..23e9d4694a27 100644 --- a/test/scripts/plugin-npm-extended-stable-workflow.test.ts +++ b/test/scripts/plugin-npm-extended-stable-workflow.test.ts @@ -105,7 +105,7 @@ describe("plugin npm extended-stable workflow", () => { } }); - it("overlays the complete trusted packaging helper dependency pair", () => { + it("overlays the complete trusted packaging helper dependency set", () => { const parsed = workflow(); const preflightCheckout = step( parsed.jobs?.preview_plugin_pack, @@ -114,12 +114,16 @@ describe("plugin npm extended-stable workflow", () => { expect(preflightCheckout.with?.["sparse-checkout"]).toContain( "scripts/generate-npm-package-lock.mjs", ); + expect(preflightCheckout.with?.["sparse-checkout"]).toContain( + "scripts/lib/npm-json-output.mjs", + ); expect(preflightCheckout.with?.["sparse-checkout"]).toContain( "scripts/lib/plugin-npm-package-manifest.mjs", ); const expectedCopies = [ "scripts/generate-npm-package-lock.mjs", + "scripts/lib/npm-json-output.mjs", "scripts/lib/plugin-npm-package-manifest.mjs", ]; for (const helperPath of expectedCopies) { @@ -202,9 +206,12 @@ describe("plugin npm extended-stable workflow", () => { ); expect(prepare.if).toBeUndefined(); expect(prepare.run).toContain('bash scripts/plugin-npm-publish.sh --pack "${PACKAGE_DIR}"'); - expect(prepare.run).toContain('raw.lastIndexOf("[")'); + expect(prepare.run).toContain( + 'import { resolveNpmJsonEntries } from "./scripts/lib/npm-json-output.mjs";', + ); + expect(prepare.run).toContain('raw[index] !== "[" && raw[index] !== "{"'); + expect(prepare.run).toContain("const entries = resolveNpmJsonEntries(candidate)"); expect(prepare.run).toContain("npm can print bundled-dependency summaries"); - expect(prepare.run).toContain("if (index === 0)"); expect(prepare.run).toContain( "fs.writeFileSync(process.argv[3], `${JSON.stringify(pack, null, 2)}\\n`)", ); diff --git a/test/scripts/resolve-openclaw-package-candidate.test.ts b/test/scripts/resolve-openclaw-package-candidate.test.ts index 68068cc35a52..cc6a7cc5a52d 100644 --- a/test/scripts/resolve-openclaw-package-candidate.test.ts +++ b/test/scripts/resolve-openclaw-package-candidate.test.ts @@ -390,6 +390,20 @@ describe("resolve-openclaw-package-candidate", () => { await expect(readFile(path.join(dir, "openclaw-current.tgz"), "utf8")).resolves.toBe("package"); }); + it("reads npm 12 name-keyed package candidate filenames", async () => { + const dir = await mkdtemp(path.join(tmpdir(), "openclaw-package-npm-pack-")); + tempDirs.push(dir); + await writeFile(path.join(dir, "openclaw-2026.6.17.tgz"), "package"); + + await expect( + moveNewestPackedTarballForTest( + dir, + JSON.stringify({ openclaw: { filename: "openclaw-2026.6.17.tgz" } }), + "openclaw-current.tgz", + ), + ).resolves.toBe(path.join(dir, "openclaw-current.tgz")); + }); + it("rejects path-like npm pack filenames instead of renaming outside the output directory", async () => { const dir = await mkdtemp(path.join(tmpdir(), "openclaw-package-npm-pack-")); tempDirs.push(dir); diff --git a/test/scripts/test-install-sh-docker.test.ts b/test/scripts/test-install-sh-docker.test.ts index 53da50ad5fb3..d3b3d1f797ba 100644 --- a/test/scripts/test-install-sh-docker.test.ts +++ b/test/scripts/test-install-sh-docker.test.ts @@ -1123,6 +1123,9 @@ printf 'status=%s\\n' "$status" expect(script).toContain("print_pack_delta_audit"); expect(script).toContain("==> Pack audit"); expect(script).toContain("==> Pack audit delta"); + expect(script).toContain("normalize_npm_pack_json_file"); + expect(script).toContain('normalize_npm_pack_json_file "$pack_json_file"'); + expect(script).toContain('normalize_npm_pack_json_file "$baseline_pack_json_file"'); }); it("fails the update smoke when the candidate npm pack exceeds the release budget", () => {