@@ -97,6 +97,10 @@ import {
9797 ensurePluginRegistryLoaded ,
9898} from "./runtime/runtime-registry-loader.js" ;
9999import type { PluginSdkResolutionPreference } from "./sdk-alias.js" ;
100+ import {
101+ writeGeneratedRuntimeDepsManifest ,
102+ writeInstalledRuntimeDepPackage ,
103+ } from "./test-helpers/bundled-runtime-deps-fixtures.js" ;
100104let cachedBundledTelegramDir = "" ;
101105let cachedBundledMemoryDir = "" ;
102106
@@ -1594,6 +1598,131 @@ module.exports = {
15941598 expect ( registry . plugins . find ( ( entry ) => entry . id === "alpha" ) ?. status ) . toBe ( "loaded" ) ;
15951599 } ) ;
15961600
1601+ it ( "does not reuse cached bundled runtime deps after an in-place package version upgrade" , ( ) => {
1602+ const packageRoot = makeTempDir ( ) ;
1603+ const stageDir = makeTempDir ( ) ;
1604+ const markerDir = makeTempDir ( ) ;
1605+ const markerPath = path . join ( markerDir , "browser-runtime-marker.json" ) ;
1606+ const bundledDir = path . join ( packageRoot , "dist" , "extensions" ) ;
1607+ const pluginRoot = path . join ( bundledDir , "browser" ) ;
1608+ fs . mkdirSync ( pluginRoot , { recursive : true } ) ;
1609+ fs . writeFileSync (
1610+ path . join ( pluginRoot , "package.json" ) ,
1611+ JSON . stringify (
1612+ {
1613+ name : "@openclaw/browser" ,
1614+ version : "1.0.0" ,
1615+ dependencies : {
1616+ "browser-runtime" : "1.0.0" ,
1617+ } ,
1618+ openclaw : { extensions : [ "./index.cjs" ] } ,
1619+ } ,
1620+ null ,
1621+ 2 ,
1622+ ) ,
1623+ "utf-8" ,
1624+ ) ;
1625+ fs . writeFileSync (
1626+ path . join ( pluginRoot , "openclaw.plugin.json" ) ,
1627+ JSON . stringify (
1628+ {
1629+ id : "browser" ,
1630+ enabledByDefault : true ,
1631+ configSchema : EMPTY_PLUGIN_SCHEMA ,
1632+ } ,
1633+ null ,
1634+ 2 ,
1635+ ) ,
1636+ "utf-8" ,
1637+ ) ;
1638+
1639+ const env = {
1640+ ...process . env ,
1641+ OPENCLAW_BUNDLED_PLUGINS_DIR : bundledDir ,
1642+ OPENCLAW_PLUGIN_STAGE_DIR : stageDir ,
1643+ OPENCLAW_TEST_TRUST_BUNDLED_PLUGINS_DIR : "1" ,
1644+ VITEST : "true" ,
1645+ } ;
1646+ const writePackageVersion = ( version : string ) => {
1647+ fs . writeFileSync (
1648+ path . join ( packageRoot , "package.json" ) ,
1649+ JSON . stringify ( { name : "openclaw" , version, type : "module" } , null , 2 ) ,
1650+ "utf-8" ,
1651+ ) ;
1652+ } ;
1653+ const writeRuntimeEntry = ( marker : string ) => {
1654+ fs . writeFileSync (
1655+ path . join ( pluginRoot , "index.cjs" ) ,
1656+ `
1657+ const fs = require("node:fs");
1658+ const runtimeDep = require("browser-runtime/package.json");
1659+ fs.writeFileSync(
1660+ ${ JSON . stringify ( markerPath ) } ,
1661+ JSON.stringify({ marker: ${ JSON . stringify ( marker ) } , filename: __filename, runtimeDep: runtimeDep.name }) + "\\n",
1662+ "utf-8",
1663+ );
1664+ module.exports = { id: "browser", register() {} };
1665+ ` ,
1666+ "utf-8" ,
1667+ ) ;
1668+ } ;
1669+ const installRoots : string [ ] = [ ] ;
1670+ const loadOptions = {
1671+ env,
1672+ onlyPluginIds : [ "browser" ] ,
1673+ config : {
1674+ plugins : {
1675+ enabled : true ,
1676+ } ,
1677+ } ,
1678+ bundledRuntimeDepsInstaller : ( { installRoot, installSpecs, missingSpecs } ) => {
1679+ installRoots . push ( installRoot ) ;
1680+ writeInstalledRuntimeDepPackage ( installRoot , "browser-runtime" , "1.0.0" ) ;
1681+ writeGeneratedRuntimeDepsManifest ( installRoot , installSpecs ?? missingSpecs ) ;
1682+ } ,
1683+ } satisfies Parameters < typeof loadOpenClawPlugins > [ 0 ] ;
1684+
1685+ writePackageVersion ( "2026.4.26" ) ;
1686+ writeRuntimeEntry ( "v26" ) ;
1687+ const first = withEnv ( env , ( ) => loadOpenClawPlugins ( loadOptions ) ) ;
1688+ const firstInstallRoot = installRoots . at ( - 1 ) ;
1689+ if ( ! firstInstallRoot ) {
1690+ throw new Error ( "expected first runtime-deps install root" ) ;
1691+ }
1692+ const firstPlugin = first . plugins . find ( ( entry ) => entry . id === "browser" ) ;
1693+ expect ( firstPlugin ?. error ) . toBeUndefined ( ) ;
1694+ expect ( firstPlugin ?. status ) . toBe ( "loaded" ) ;
1695+ const firstMarker = JSON . parse ( fs . readFileSync ( markerPath , "utf-8" ) ) as {
1696+ filename : string ;
1697+ marker : string ;
1698+ runtimeDep : string ;
1699+ } ;
1700+
1701+ expect ( firstMarker . marker ) . toBe ( "v26" ) ;
1702+ expect ( firstMarker . runtimeDep ) . toBe ( "browser-runtime" ) ;
1703+ expect ( firstMarker . filename ) . toContain ( path . join ( firstInstallRoot , "dist" , "extensions" ) ) ;
1704+
1705+ writePackageVersion ( "2026.4.27" ) ;
1706+ writeRuntimeEntry ( "v27" ) ;
1707+ const second = withEnv ( env , ( ) => loadOpenClawPlugins ( loadOptions ) ) ;
1708+ const secondInstallRoot = installRoots . at ( - 1 ) ;
1709+ if ( ! secondInstallRoot ) {
1710+ throw new Error ( "expected second runtime-deps install root" ) ;
1711+ }
1712+ const secondMarker = JSON . parse ( fs . readFileSync ( markerPath , "utf-8" ) ) as {
1713+ filename : string ;
1714+ marker : string ;
1715+ runtimeDep : string ;
1716+ } ;
1717+
1718+ expect ( second ) . not . toBe ( first ) ;
1719+ expect ( second . plugins . find ( ( entry ) => entry . id === "browser" ) ?. status ) . toBe ( "loaded" ) ;
1720+ expect ( secondMarker . marker ) . toBe ( "v27" ) ;
1721+ expect ( secondMarker . runtimeDep ) . toBe ( "browser-runtime" ) ;
1722+ expect ( secondMarker . filename ) . toContain ( path . join ( secondInstallRoot , "dist" , "extensions" ) ) ;
1723+ expect ( secondInstallRoot ) . not . toBe ( firstInstallRoot ) ;
1724+ } ) ;
1725+
15971726 it ( "loads bundled plugins from symlinked package roots with an external stage dir" , ( ) => {
15981727 const packageRoot = makeTempDir ( ) ;
15991728 const stageDir = makeTempDir ( ) ;
0 commit comments