2026-02-18 05:04:44 +00:00
import fs from "node:fs/promises" ;
import os from "node:os" ;
import path from "node:path" ;
import { afterEach , beforeEach , describe , expect , it , vi } from "vitest" ;
import {
packNpmSpecToArchive ,
resolveArchiveSourcePath ,
withTempDir ,
} from "./install-source-utils.js" ;
const runCommandWithTimeoutMock = vi . fn ( ) ;
2026-02-21 19:50:23 +00:00
const TEMP_DIR_PREFIX = "openclaw-install-source-utils-" ;
2026-02-18 05:04:44 +00:00
vi . mock ( "../process/exec.js" , ( ) = > ( {
runCommandWithTimeout : ( . . . args : unknown [ ] ) = > runCommandWithTimeoutMock ( . . . args ) ,
} ) ) ;
const tempDirs : string [ ] = [ ] ;
async function createTempDir ( prefix : string ) {
const dir = await fs . mkdtemp ( path . join ( os . tmpdir ( ) , prefix ) ) ;
tempDirs . push ( dir ) ;
return dir ;
}
2026-02-21 19:50:23 +00:00
async function createFixtureDir() {
return await createTempDir ( TEMP_DIR_PREFIX ) ;
}
async function createFixtureFile ( params : {
fileName : string ;
contents : string ;
dir? : string ;
} ) : Promise < { dir : string ; filePath : string } > {
const dir = params . dir ? ? ( await createFixtureDir ( ) ) ;
const filePath = path . join ( dir , params . fileName ) ;
await fs . writeFile ( filePath , params . contents , "utf-8" ) ;
return { dir , filePath } ;
}
function mockPackCommandResult ( params : { stdout : string ; stderr? : string ; code? : number } ) {
runCommandWithTimeoutMock . mockResolvedValue ( {
stdout : params.stdout ,
stderr : params.stderr ? ? "" ,
code : params.code ? ? 0 ,
signal : null ,
killed : false ,
} ) ;
}
async function runPack ( spec : string , cwd : string , timeoutMs = 1000 ) {
return await packNpmSpecToArchive ( {
spec ,
timeoutMs ,
cwd ,
} ) ;
}
2026-03-13 18:08:48 +00:00
async function expectPackFallsBackToDetectedArchive ( params : {
stdout : string ;
expectedMetadata? : Record < string , unknown > ;
} ) {
2026-03-02 19:48:38 +00:00
const cwd = await createTempDir ( "openclaw-install-source-utils-" ) ;
const archivePath = path . join ( cwd , "openclaw-plugin-1.2.3.tgz" ) ;
await fs . writeFile ( archivePath , "" , "utf-8" ) ;
runCommandWithTimeoutMock . mockResolvedValue ( {
stdout : params.stdout ,
stderr : "" ,
code : 0 ,
signal : null ,
killed : false ,
} ) ;
const result = await packNpmSpecToArchive ( {
spec : "openclaw-plugin@1.2.3" ,
timeoutMs : 5000 ,
cwd ,
} ) ;
expect ( result ) . toEqual ( {
ok : true ,
archivePath ,
2026-03-13 18:08:48 +00:00
metadata : params.expectedMetadata ? ? { } ,
2026-03-02 19:48:38 +00:00
} ) ;
}
2026-02-18 05:04:44 +00:00
beforeEach ( ( ) = > {
2026-02-22 08:25:04 +00:00
runCommandWithTimeoutMock . mockClear ( ) ;
2026-02-18 05:04:44 +00:00
} ) ;
afterEach ( async ( ) = > {
while ( tempDirs . length > 0 ) {
const dir = tempDirs . pop ( ) ;
if ( ! dir ) {
break ;
}
await fs . rm ( dir , { recursive : true , force : true } ) ;
}
} ) ;
describe ( "withTempDir" , ( ) = > {
it ( "creates a temp dir and always removes it after callback" , async ( ) = > {
let observedDir = "" ;
const markerFile = "marker.txt" ;
const value = await withTempDir ( "openclaw-install-source-utils-" , async ( tmpDir ) = > {
observedDir = tmpDir ;
await fs . writeFile ( path . join ( tmpDir , markerFile ) , "ok" , "utf-8" ) ;
await expect ( fs . stat ( path . join ( tmpDir , markerFile ) ) ) . resolves . toBeDefined ( ) ;
return "done" ;
} ) ;
expect ( value ) . toBe ( "done" ) ;
await expect ( fs . stat ( observedDir ) ) . rejects . toThrow ( ) ;
} ) ;
} ) ;
describe ( "resolveArchiveSourcePath" , ( ) = > {
it ( "returns not found error for missing archive paths" , async ( ) = > {
const result = await resolveArchiveSourcePath ( "/tmp/does-not-exist-openclaw-archive.tgz" ) ;
expect ( result . ok ) . toBe ( false ) ;
if ( ! result . ok ) {
expect ( result . error ) . toContain ( "archive not found" ) ;
}
} ) ;
it ( "rejects unsupported archive extensions" , async ( ) = > {
2026-02-21 19:50:23 +00:00
const { filePath } = await createFixtureFile ( {
fileName : "plugin.txt" ,
contents : "not-an-archive" ,
} ) ;
2026-02-18 05:04:44 +00:00
const result = await resolveArchiveSourcePath ( filePath ) ;
expect ( result . ok ) . toBe ( false ) ;
if ( ! result . ok ) {
expect ( result . error ) . toContain ( "unsupported archive" ) ;
}
} ) ;
2026-03-13 18:08:48 +00:00
it . each ( [ "plugin.zip" , "plugin.tgz" , "plugin.tar.gz" ] ) (
"accepts supported archive extension %s" ,
async ( fileName ) = > {
const { filePath } = await createFixtureFile ( {
fileName ,
contents : "" ,
} ) ;
const result = await resolveArchiveSourcePath ( filePath ) ;
expect ( result ) . toEqual ( { ok : true , path : filePath } ) ;
} ,
) ;
2026-02-18 05:04:44 +00:00
} ) ;
describe ( "packNpmSpecToArchive" , ( ) = > {
2026-02-19 15:10:57 +01:00
it ( "packs spec and returns archive path using JSON output metadata" , async ( ) = > {
2026-02-21 19:50:23 +00:00
const cwd = await createFixtureDir ( ) ;
2026-02-27 11:00:24 +08:00
const archivePath = path . join ( cwd , "openclaw-plugin-1.2.3.tgz" ) ;
await fs . writeFile ( archivePath , "" , "utf-8" ) ;
2026-02-21 19:50:23 +00:00
mockPackCommandResult ( {
2026-02-19 15:10:57 +01:00
stdout : JSON.stringify ( [
{
id : "openclaw-plugin@1.2.3" ,
name : "openclaw-plugin" ,
version : "1.2.3" ,
filename : "openclaw-plugin-1.2.3.tgz" ,
integrity : "sha512-test-integrity" ,
shasum : "abc123" ,
} ,
] ) ,
2026-02-18 05:04:44 +00:00
} ) ;
2026-02-21 19:50:23 +00:00
const result = await runPack ( "openclaw-plugin@1.2.3" , cwd ) ;
2026-02-18 05:04:44 +00:00
expect ( result ) . toEqual ( {
ok : true ,
2026-02-27 11:00:24 +08:00
archivePath ,
2026-02-19 15:10:57 +01:00
metadata : {
name : "openclaw-plugin" ,
version : "1.2.3" ,
resolvedSpec : "openclaw-plugin@1.2.3" ,
integrity : "sha512-test-integrity" ,
shasum : "abc123" ,
} ,
2026-02-18 05:04:44 +00:00
} ) ;
expect ( runCommandWithTimeoutMock ) . toHaveBeenCalledWith (
2026-02-19 15:10:57 +01:00
[ "npm" , "pack" , "openclaw-plugin@1.2.3" , "--ignore-scripts" , "--json" ] ,
2026-02-18 05:04:44 +00:00
expect . objectContaining ( {
cwd ,
timeoutMs : 300_000 ,
} ) ,
) ;
} ) ;
2026-02-19 15:10:57 +01:00
it ( "falls back to parsing final stdout line when npm json output is unavailable" , async ( ) = > {
2026-02-21 19:50:23 +00:00
const cwd = await createFixtureDir ( ) ;
2026-02-27 11:00:24 +08:00
const expectedArchivePath = path . join ( cwd , "openclaw-plugin-1.2.3.tgz" ) ;
await fs . writeFile ( expectedArchivePath , "" , "utf-8" ) ;
2026-02-21 19:50:23 +00:00
mockPackCommandResult ( {
2026-02-19 15:10:57 +01:00
stdout : "npm notice created package\nopenclaw-plugin-1.2.3.tgz\n" ,
} ) ;
2026-02-21 19:50:23 +00:00
const result = await runPack ( "openclaw-plugin@1.2.3" , cwd ) ;
2026-02-19 15:10:57 +01:00
expect ( result ) . toEqual ( {
ok : true ,
2026-02-27 11:00:24 +08:00
archivePath : expectedArchivePath ,
2026-02-19 15:10:57 +01:00
metadata : { } ,
} ) ;
} ) ;
2026-02-18 05:04:44 +00:00
it ( "returns npm pack error details when command fails" , async ( ) = > {
2026-02-21 19:50:23 +00:00
const cwd = await createFixtureDir ( ) ;
mockPackCommandResult ( {
2026-02-18 05:04:44 +00:00
stdout : "fallback stdout" ,
stderr : "registry timeout" ,
code : 1 ,
} ) ;
2026-02-21 19:50:23 +00:00
const result = await runPack ( "bad-spec" , cwd , 5000 ) ;
2026-02-18 05:04:44 +00:00
expect ( result . ok ) . toBe ( false ) ;
if ( ! result . ok ) {
expect ( result . error ) . toContain ( "npm pack failed" ) ;
expect ( result . error ) . toContain ( "registry timeout" ) ;
}
} ) ;
2026-03-13 18:08:48 +00:00
it . each ( [
{
name : "falls back to archive detected in cwd when npm pack stdout is empty" ,
stdout : " \n\n" ,
} ,
{
name : "falls back to archive detected in cwd when stdout does not contain a tgz" ,
stdout : "npm pack completed successfully\n" ,
} ,
{
name : "falls back to cwd archive when logged JSON metadata omits filename" ,
stdout :
'npm notice using cache\n[{"id":"openclaw-plugin@1.2.3","name":"openclaw-plugin","version":"1.2.3","integrity":"sha512-test-integrity","shasum":"abc123"}]\n' ,
expectedMetadata : {
name : "openclaw-plugin" ,
version : "1.2.3" ,
resolvedSpec : "openclaw-plugin@1.2.3" ,
integrity : "sha512-test-integrity" ,
shasum : "abc123" ,
} ,
} ,
] ) ( "$name" , async ( { stdout , expectedMetadata } ) = > {
await expectPackFallsBackToDetectedArchive ( { stdout , expectedMetadata } ) ;
2026-02-27 11:00:24 +08:00
} ) ;
2026-02-26 21:16:28 -06:00
it ( "returns friendly error for 404 (package not on npm)" , async ( ) = > {
const cwd = await createFixtureDir ( ) ;
mockPackCommandResult ( {
stdout : "" ,
stderr : "npm error code E404\nnpm error 404 '@openclaw/whatsapp@*' is not in this registry." ,
code : 1 ,
} ) ;
const result = await runPack ( "@openclaw/whatsapp" , cwd ) ;
expect ( result . ok ) . toBe ( false ) ;
if ( ! result . ok ) {
expect ( result . error ) . toContain ( "Package not found on npm" ) ;
expect ( result . error ) . toContain ( "@openclaw/whatsapp" ) ;
expect ( result . error ) . toContain ( "docs.openclaw.ai/tools/plugin" ) ;
}
} ) ;
2026-02-18 05:04:44 +00:00
it ( "returns explicit error when npm pack produces no archive name" , async ( ) = > {
2026-02-21 19:50:23 +00:00
const cwd = await createFixtureDir ( ) ;
mockPackCommandResult ( {
2026-02-18 05:04:44 +00:00
stdout : " \n\n" ,
} ) ;
2026-02-21 19:50:23 +00:00
const result = await runPack ( "openclaw-plugin@1.2.3" , cwd , 5000 ) ;
2026-02-18 05:04:44 +00:00
expect ( result ) . toEqual ( {
ok : false ,
error : "npm pack produced no archive" ,
} ) ;
} ) ;
2026-02-21 19:50:23 +00:00
it ( "parses scoped metadata from id-only json output even with npm notice prefix" , async ( ) = > {
const cwd = await createFixtureDir ( ) ;
2026-02-27 11:00:24 +08:00
await fs . writeFile ( path . join ( cwd , "openclaw-plugin-demo-2.0.0.tgz" ) , "" , "utf-8" ) ;
2026-02-21 19:50:23 +00:00
mockPackCommandResult ( {
stdout :
"npm notice creating package\n" +
JSON . stringify ( [
{
id : "@openclaw/plugin-demo@2.0.0" ,
filename : "openclaw-plugin-demo-2.0.0.tgz" ,
} ,
] ) ,
} ) ;
const result = await runPack ( "@openclaw/plugin-demo@2.0.0" , cwd ) ;
expect ( result ) . toEqual ( {
ok : true ,
archivePath : path.join ( cwd , "openclaw-plugin-demo-2.0.0.tgz" ) ,
metadata : {
resolvedSpec : "@openclaw/plugin-demo@2.0.0" ,
} ,
} ) ;
} ) ;
it ( "uses stdout fallback error text when stderr is empty" , async ( ) = > {
const cwd = await createFixtureDir ( ) ;
mockPackCommandResult ( {
stdout : "network timeout" ,
stderr : " " ,
code : 1 ,
} ) ;
const result = await runPack ( "bad-spec" , cwd ) ;
expect ( result ) . toEqual ( {
ok : false ,
error : "npm pack failed: network timeout" ,
} ) ;
} ) ;
2026-02-18 05:04:44 +00:00
} ) ;