@@ -320,6 +320,167 @@ describe("repairMissingConfiguredPluginInstalls", () => {
320320 }
321321 } ) ;
322322
323+ it ( "maps a missing configured plugin install to a structured finding and dry-run effect" , async ( ) => {
324+ mocks . listChannelPluginCatalogEntries . mockReturnValue ( [
325+ {
326+ id : "matrix" ,
327+ pluginId : "matrix" ,
328+ meta : { label : "Matrix" } ,
329+ install : {
330+ npmSpec :
"@openclaw/[email protected] " , 331+ expectedIntegrity : "sha512-test" ,
332+ } ,
333+ trustedSourceLinkedOfficialInstall : true ,
334+ } ,
335+ ] ) ;
336+
337+ const {
338+ configuredPluginInstallIssueToHealthFinding,
339+ configuredPluginInstallIssueToRepairEffect,
340+ detectConfiguredPluginInstallHealthIssues,
341+ } = await import ( "./missing-configured-plugin-install.js" ) ;
342+ const [ issue ] = await detectConfiguredPluginInstallHealthIssues ( {
343+ cfg : {
344+ channels : {
345+ matrix : { enabled : true , homeserver : "https://matrix.example.org" } ,
346+ } ,
347+ } ,
348+ env : { } ,
349+ } ) ;
350+
351+ expect ( mocks . installPluginFromClawHub ) . not . toHaveBeenCalled ( ) ;
352+ expect ( mocks . installPluginFromNpmSpec ) . not . toHaveBeenCalled ( ) ;
353+ expect ( mocks . writePersistedInstalledPluginIndexInstallRecords ) . not . toHaveBeenCalled ( ) ;
354+ expect ( issue ) . toEqual ( {
355+ kind : "missing-install-record" ,
356+ pluginId : "matrix" ,
357+ installSpec :
"@openclaw/[email protected] " , 358+ } ) ;
359+ expect ( configuredPluginInstallIssueToHealthFinding ( issue ) ) . toMatchObject ( {
360+ checkId : "core/doctor/configured-plugin-installs" ,
361+ severity : "warning" ,
362+ target : "matrix" ,
363+ fixHint :
"Run `openclaw doctor --fix` to install @openclaw/[email protected] ." , 364+ } ) ;
365+ expect ( configuredPluginInstallIssueToRepairEffect ( issue ) ) . toEqual ( {
366+ kind : "package" ,
367+ action : "would-install-configured-plugin" ,
368+ target : "matrix" ,
369+ dryRunSafe : false ,
370+ } ) ;
371+ } ) ;
372+
373+ it ( "maps package-update deferrals to structured findings without installing packages" , async ( ) => {
374+ const missingDiscordPath = path . resolve ( "/missing/discord" ) ;
375+ const records = {
376+ discord : {
377+ source : "npm" ,
378+ spec : "@openclaw/discord" ,
379+ installPath : missingDiscordPath ,
380+ } ,
381+ } ;
382+ mocks . loadInstalledPluginIndexInstallRecords . mockResolvedValue ( records ) ;
383+ mocks . listChannelPluginCatalogEntries . mockReturnValue ( [
384+ {
385+ id : "discord" ,
386+ pluginId : "discord" ,
387+ meta : { label : "Discord" } ,
388+ install : {
389+ npmSpec : "@openclaw/discord" ,
390+ } ,
391+ } ,
392+ ] ) ;
393+
394+ const {
395+ configuredPluginInstallIssueToHealthFinding,
396+ configuredPluginInstallIssueToRepairEffect,
397+ detectConfiguredPluginInstallHealthIssues,
398+ } = await import ( "./missing-configured-plugin-install.js" ) ;
399+ const [ issue ] = await detectConfiguredPluginInstallHealthIssues ( {
400+ cfg : {
401+ plugins : {
402+ entries : {
403+ discord : { enabled : true } ,
404+ } ,
405+ } ,
406+ channels : {
407+ discord : { enabled : true } ,
408+ } ,
409+ } ,
410+ env : {
411+ OPENCLAW_UPDATE_IN_PROGRESS : "1" ,
412+ OPENCLAW_UPDATE_DEFER_CONFIGURED_PLUGIN_INSTALL_REPAIR : "1" ,
413+ } ,
414+ } ) ;
415+
416+ expect ( mocks . installPluginFromClawHub ) . not . toHaveBeenCalled ( ) ;
417+ expect ( mocks . installPluginFromNpmSpec ) . not . toHaveBeenCalled ( ) ;
418+ expect ( issue ) . toEqual ( {
419+ kind : "deferred-package-manager-repair" ,
420+ pluginId : "discord" ,
421+ installPath : missingDiscordPath ,
422+ } ) ;
423+ expect ( configuredPluginInstallIssueToHealthFinding ( issue ) ) . toMatchObject ( {
424+ checkId : "core/doctor/configured-plugin-installs" ,
425+ severity : "warning" ,
426+ path : missingDiscordPath ,
427+ target : "discord" ,
428+ } ) ;
429+ expect ( configuredPluginInstallIssueToRepairEffect ( issue ) ) . toEqual ( {
430+ kind : "package" ,
431+ action : "would-defer-configured-plugin-install-repair" ,
432+ target : "discord" ,
433+ dryRunSafe : true ,
434+ } ) ;
435+ } ) ;
436+
437+ it ( "reports one finding when a configured plugin record points at a missing package" , async ( ) => {
438+ const missingDiscordPath = path . resolve ( "/missing/discord" ) ;
439+ const records = {
440+ discord : {
441+ source : "npm" ,
442+ spec : "@openclaw/discord" ,
443+ installPath : missingDiscordPath ,
444+ } ,
445+ } ;
446+ mocks . loadInstalledPluginIndexInstallRecords . mockResolvedValue ( records ) ;
447+ mocks . listChannelPluginCatalogEntries . mockReturnValue ( [
448+ {
449+ id : "discord" ,
450+ pluginId : "discord" ,
451+ meta : { label : "Discord" } ,
452+ install : {
453+ npmSpec : "@openclaw/discord" ,
454+ } ,
455+ } ,
456+ ] ) ;
457+
458+ const { detectConfiguredPluginInstallHealthIssues } =
459+ await import ( "./missing-configured-plugin-install.js" ) ;
460+ const issues = await detectConfiguredPluginInstallHealthIssues ( {
461+ cfg : {
462+ plugins : {
463+ entries : {
464+ discord : { enabled : true } ,
465+ } ,
466+ } ,
467+ channels : {
468+ discord : { enabled : true } ,
469+ } ,
470+ } ,
471+ env : { } ,
472+ } ) ;
473+
474+ expect ( issues ) . toEqual ( [
475+ {
476+ kind : "missing-installed-payload" ,
477+ pluginId : "discord" ,
478+ installPath : missingDiscordPath ,
479+ installSpec : "@openclaw/discord" ,
480+ } ,
481+ ] ) ;
482+ } ) ;
483+
323484 it ( "installs a missing configured OpenClaw channel plugin from npm by default" , async ( ) => {
324485 mocks . listChannelPluginCatalogEntries . mockReturnValue ( [
325486 {
0 commit comments