@@ -16,6 +16,10 @@ const mocks = vi.hoisted(() => ({
1616 buildPluginCompatibilityWarnings : vi . fn ( ) ,
1717 listTaskFlowRecords : vi . fn < ( ) => unknown [ ] > ( ( ) => [ ] ) ,
1818 listTasksForFlowId : vi . fn < ( flowId : string ) => unknown [ ] > ( ( _flowId : string ) => [ ] ) ,
19+ loadInstalledPluginIndexInstallRecords : vi . fn < ( ) => Promise < Record < string , unknown > > > ( ( ) =>
20+ Promise . resolve ( { } ) ,
21+ ) ,
22+ VERSION : "2026.6.1" ,
1923} ) ) ;
2024
2125vi . mock ( "../agents/agent-scope.js" , ( ) => ( {
@@ -34,6 +38,15 @@ vi.mock("../plugins/status.js", () => ({
3438 mocks . buildPluginCompatibilityWarnings ( ...args ) ,
3539} ) ) ;
3640
41+ vi . mock ( "../plugins/installed-plugin-index-records.js" , ( ) => ( {
42+ loadInstalledPluginIndexInstallRecords : ( ...args : unknown [ ] ) =>
43+ mocks . loadInstalledPluginIndexInstallRecords ( ...args ) ,
44+ } ) ) ;
45+
46+ vi . mock ( "../version.js" , ( ) => ( {
47+ VERSION : mocks . VERSION ,
48+ } ) ) ;
49+
3750vi . mock ( "../tasks/task-flow-runtime-internal.js" , ( ) => ( {
3851 listTaskFlowRecords : ( ) => mocks . listTaskFlowRecords ( ) ,
3952} ) ) ;
@@ -48,6 +61,7 @@ async function runNoteWorkspaceStatusForTest(
4861 opts ?: {
4962 flows ?: unknown [ ] ;
5063 tasksByFlowId ?: ( flowId : string ) => unknown [ ] ;
64+ env ?: Record < string , string > ;
5165 } ,
5266) {
5367 mocks . resolveDefaultAgentId . mockReturnValue ( "default" ) ;
@@ -66,7 +80,7 @@ async function runNoteWorkspaceStatusForTest(
6680 ) ;
6781
6882 const noteSpy = vi . spyOn ( noteModule , "note" ) . mockImplementation ( ( ) => { } ) ;
69- await noteWorkspaceStatus ( { } ) ;
83+ await noteWorkspaceStatus ( { } , opts ?. env ? { env : opts . env as NodeJS . ProcessEnv } : undefined ) ;
7084 return noteSpy ;
7185}
7286
@@ -236,4 +250,42 @@ describe("noteWorkspaceStatus", () => {
236250 noteSpy . mockRestore ( ) ;
237251 }
238252 } ) ;
253+
254+ it ( "reports plugin version drift when an official plugin version mismatches the gateway" , async ( ) => {
255+ mocks . loadInstalledPluginIndexInstallRecords . mockResolvedValue ( {
256+ codex : {
257+ source : "npm" ,
258+ spec : "@openclaw/codex@latest" ,
259+ resolvedName : "@openclaw/codex" ,
260+ resolvedVersion : "2026.5.30-beta.1" ,
261+ } ,
262+ } ) ;
263+
264+ const noteSpy = await runNoteWorkspaceStatusForTest (
265+ createPluginLoadResult ( {
266+ plugins : [
267+ createPluginRecord ( {
268+ id : "codex" ,
269+ name : "Codex" ,
270+ status : "loaded" ,
271+ format : "bundle" ,
272+ bundleCapabilities : [ "provider:codex" ] ,
273+ } ) ,
274+ ] ,
275+ } ) ,
276+ [ ] ,
277+ { env : { OPENCLAW_HOME : "/tmp/test-openclaw" } } ,
278+ ) ;
279+ try {
280+ const driftCalls = noteSpy . mock . calls . filter ( ( [ , title ] ) => title === "Plugin version drift" ) ;
281+ expect ( driftCalls ) . toHaveLength ( 1 ) ;
282+ const [ [ body ] ] = driftCalls ;
283+ expect ( body ) . toContain ( "not on gateway 2026.6.1" ) ;
284+ expect ( body ) . toContain ( "codex: 2026.5.30-beta.1 -> expected 2026.6.1" ) ;
285+ expect ( body ) . toContain ( "openclaw plugins update <plugin-id>" ) ;
286+ } finally {
287+ noteSpy . mockRestore ( ) ;
288+ mocks . loadInstalledPluginIndexInstallRecords . mockResolvedValue ( { } ) ;
289+ }
290+ } ) ;
239291} ) ;
0 commit comments