@@ -67,6 +67,30 @@ function writePackagePlugin(rootDir: string, options: { configPaths?: readonly s
6767 ) ;
6868}
6969
70+ function writeBundledPlugin ( rootDir : string , pluginId : string , entryPath : string ) {
71+ fs . mkdirSync ( rootDir , { recursive : true } ) ;
72+ fs . writeFileSync ( path . join ( rootDir , entryPath ) , "export default { register() {} };\n" , "utf8" ) ;
73+ fs . writeFileSync (
74+ path . join ( rootDir , "openclaw.plugin.json" ) ,
75+ JSON . stringify ( {
76+ id : pluginId ,
77+ name : pluginId ,
78+ description : pluginId ,
79+ configSchema : { type : "object" } ,
80+ } ) ,
81+ "utf8" ,
82+ ) ;
83+ fs . writeFileSync (
84+ path . join ( rootDir , "package.json" ) ,
85+ JSON . stringify ( {
86+ name : `@openclaw/${ pluginId } ` ,
87+ version : "1.0.0" ,
88+ openclaw : { extensions : [ `./${ entryPath } ` ] } ,
89+ } ) ,
90+ "utf8" ,
91+ ) ;
92+ }
93+
7094function createCandidate ( rootDir : string , pluginId = "demo" ) : PluginCandidate {
7195 fs . mkdirSync ( rootDir , { recursive : true } ) ;
7296 fs . writeFileSync ( path . join ( rootDir , "index.ts" ) , "export default { register() {} };\n" , "utf8" ) ;
@@ -753,6 +777,40 @@ describe("loadPluginRegistrySnapshotWithMetadata", () => {
753777 expectDiagnosticsContainCode ( result . diagnostics , "persisted-registry-stale-source" ) ;
754778 } ) ;
755779
780+ it ( "keeps mixed source-checkout bundled roots from the same checkout" , ( ) => {
781+ const tempRoot = makeTempDir ( ) ;
782+ const packageRoot = path . join ( tempRoot , "openclaw" ) ;
783+ const bundledRoot = path . join ( packageRoot , "dist" , "extensions" ) ;
784+ const sourceRoot = path . join ( packageRoot , "extensions" ) ;
785+ const stateDir = path . join ( tempRoot , "state" ) ;
786+ const env = {
787+ OPENCLAW_BUNDLED_PLUGINS_DIR : bundledRoot ,
788+ OPENCLAW_STATE_DIR : stateDir ,
789+ OPENCLAW_VERSION : "2026.4.26" ,
790+ VITEST : "true" ,
791+ } ;
792+
793+ fs . mkdirSync ( path . join ( packageRoot , "src" ) , { recursive : true } ) ;
794+ fs . writeFileSync ( path . join ( packageRoot , ".git" ) , "gitdir: /tmp/mock\n" , "utf8" ) ;
795+ fs . writeFileSync ( path . join ( packageRoot , "pnpm-workspace.yaml" ) , "packages: []\n" , "utf8" ) ;
796+ writeBundledPlugin ( path . join ( bundledRoot , "codex" ) , "codex" , "index.js" ) ;
797+ writeBundledPlugin ( path . join ( sourceRoot , "whatsapp" ) , "whatsapp" , "index.ts" ) ;
798+
799+ const index = loadInstalledPluginIndex ( { config : { } , env, stateDir } ) ;
800+ expect ( index . plugins . map ( ( plugin ) => plugin . pluginId ) ) . toEqual ( [ "codex" , "whatsapp" ] ) ;
801+ expect ( index . plugins . map ( ( plugin ) => plugin . rootDir ) ) . toEqual ( [
802+ fs . realpathSync ( path . join ( bundledRoot , "codex" ) ) ,
803+ fs . realpathSync ( path . join ( sourceRoot , "whatsapp" ) ) ,
804+ ] ) ;
805+ writePersistedInstalledPluginIndexSync ( index , { stateDir } ) ;
806+
807+ const result = loadPluginRegistrySnapshotWithMetadata ( { config : { } , env, stateDir } ) ;
808+
809+ expect ( result . source ) . toBe ( "persisted" ) ;
810+ expect ( result . diagnostics ) . toStrictEqual ( [ ] ) ;
811+ expect ( result . snapshot . plugins . map ( ( plugin ) => plugin . pluginId ) ) . toEqual ( [ "codex" , "whatsapp" ] ) ;
812+ } ) ;
813+
756814 it ( "treats persisted registry as stale when a plugin diagnostic source path no longer exists" , ( ) => {
757815 const tempRoot = makeTempDir ( ) ;
758816 const stateDir = path . join ( tempRoot , "state" ) ;
0 commit comments