11import { afterEach , describe , expect , it } from "bun:test" ;
2- import { mkdtempSync , rmSync } from "node:fs" ;
2+ import { existsSync , mkdirSync , mkdtempSync , rmSync , writeFileSync } from "node:fs" ;
33import { tmpdir } from "node:os" ;
4- import { join } from "node:path" ;
4+ import { dirname , join } from "node:path" ;
55import {
66 initializeDatabase ,
77 runMigrations ,
88} from "@magic-context/core/features/magic-context/storage" ;
99import { computeLegacyRustDirIdentity } from "@magic-context/core/features/magic-context/v22-deferred-backfill" ;
1010import { Database } from "@magic-context/core/shared/sqlite" ;
1111import { parse as parseJsonc , stringify as stringifyJsonc } from "comment-json" ;
12+ import {
13+ OPENCODE_PLUGIN_ENTRY_WITH_VERSION ,
14+ OPENCODE_PLUGIN_NAME ,
15+ } from "../lib/opencode-plugin-cache" ;
1216import { runV22BackfillCommands } from "../lib/v22-backfill-commands" ;
1317import { migrateLegacyAgentEnabledConfigForDoctor } from "./doctor-opencode" ;
18+ import { clearPluginCache } from "./doctor-opencode-cache" ;
1419
1520function migrate ( input : Record < string , unknown > ) {
1621 const logs : Array < { level : "success" | "warn" ; message : string } > = [ ] ;
@@ -71,6 +76,7 @@ describe("doctor OpenCode legacy agent enabled migration", () => {
7176
7277const tempDirs : string [ ] = [ ] ;
7378const dbs : Database [ ] = [ ] ;
79+ let originalXdgCacheHome : string | undefined ;
7480
7581function makeTempDir ( prefix = "mc-v22-doctor-" ) : string {
7682 const dir = mkdtempSync ( join ( tmpdir ( ) , prefix ) ) ;
@@ -121,6 +127,12 @@ function makeHarness(database: Database, messages: string[]) {
121127}
122128
123129afterEach ( ( ) => {
130+ if ( originalXdgCacheHome === undefined ) {
131+ delete process . env . XDG_CACHE_HOME ;
132+ } else {
133+ process . env . XDG_CACHE_HOME = originalXdgCacheHome ;
134+ }
135+ originalXdgCacheHome = undefined ;
124136 for ( const db of dbs . splice ( 0 ) ) {
125137 db . close ( ) ;
126138 }
@@ -129,6 +141,120 @@ afterEach(() => {
129141 }
130142} ) ;
131143
144+ function createCachedOpenCodePlugin (
145+ root : string ,
146+ version : string ,
147+ entry = OPENCODE_PLUGIN_ENTRY_WITH_VERSION ,
148+ ) : string {
149+ const pluginCachePath = join ( root , "opencode" , "packages" , entry ) ;
150+ const installedPackagePath = join (
151+ pluginCachePath ,
152+ "node_modules" ,
153+ "@cortexkit" ,
154+ "opencode-magic-context" ,
155+ "package.json" ,
156+ ) ;
157+ mkdirSync ( dirname ( installedPackagePath ) , { recursive : true } ) ;
158+ writeFileSync ( installedPackagePath , `${ JSON . stringify ( { version } ) } \n` ) ;
159+ return pluginCachePath ;
160+ }
161+
162+ describe ( "doctor OpenCode plugin cache" , ( ) => {
163+ it ( "clears stale @latest cache when cached plugin is older than npm latest" , async ( ) => {
164+ const cacheRoot = makeTempDir ( "mc-opencode-cache-" ) ;
165+ originalXdgCacheHome = process . env . XDG_CACHE_HOME ;
166+ process . env . XDG_CACHE_HOME = cacheRoot ;
167+ const pluginCachePath = createCachedOpenCodePlugin ( cacheRoot , "0.26.0" ) ;
168+
169+ const result = await clearPluginCache ( { latestVersion : "0.29.1" } ) ;
170+
171+ expect ( result ) . toMatchObject ( {
172+ action : "cleared" ,
173+ cached : "0.26.0" ,
174+ latest : "0.29.1" ,
175+ path : pluginCachePath ,
176+ } ) ;
177+ expect ( existsSync ( pluginCachePath ) ) . toBe ( false ) ;
178+ } ) ;
179+
180+ it ( "keeps @latest cache when cached plugin matches npm latest" , async ( ) => {
181+ const cacheRoot = makeTempDir ( "mc-opencode-cache-" ) ;
182+ originalXdgCacheHome = process . env . XDG_CACHE_HOME ;
183+ process . env . XDG_CACHE_HOME = cacheRoot ;
184+ const pluginCachePath = createCachedOpenCodePlugin ( cacheRoot , "0.29.1" ) ;
185+
186+ const result = await clearPluginCache ( { latestVersion : "0.29.1" } ) ;
187+
188+ expect ( result ) . toMatchObject ( {
189+ action : "up_to_date" ,
190+ cached : "0.29.1" ,
191+ latest : "0.29.1" ,
192+ path : pluginCachePath ,
193+ } ) ;
194+ expect ( existsSync ( pluginCachePath ) ) . toBe ( true ) ;
195+ } ) ;
196+
197+ it ( "clears stale versionless cache even when @latest cache is current" , async ( ) => {
198+ const cacheRoot = makeTempDir ( "mc-opencode-cache-" ) ;
199+ originalXdgCacheHome = process . env . XDG_CACHE_HOME ;
200+ process . env . XDG_CACHE_HOME = cacheRoot ;
201+ const latestCachePath = createCachedOpenCodePlugin ( cacheRoot , "0.29.1" ) ;
202+ const versionlessCachePath = createCachedOpenCodePlugin (
203+ cacheRoot ,
204+ "0.26.0" ,
205+ OPENCODE_PLUGIN_NAME ,
206+ ) ;
207+
208+ const result = await clearPluginCache ( { latestVersion : "0.29.1" } ) ;
209+
210+ expect ( result ) . toMatchObject ( {
211+ action : "cleared" ,
212+ cached : "0.26.0" ,
213+ latest : "0.29.1" ,
214+ path : versionlessCachePath ,
215+ paths : [ versionlessCachePath ] ,
216+ } ) ;
217+ expect ( existsSync ( latestCachePath ) ) . toBe ( true ) ;
218+ expect ( existsSync ( versionlessCachePath ) ) . toBe ( false ) ;
219+ } ) ;
220+
221+ it ( "preserves existing cache when plugin npm latest is unavailable" , async ( ) => {
222+ const cacheRoot = makeTempDir ( "mc-opencode-cache-" ) ;
223+ originalXdgCacheHome = process . env . XDG_CACHE_HOME ;
224+ process . env . XDG_CACHE_HOME = cacheRoot ;
225+ const pluginCachePath = createCachedOpenCodePlugin ( cacheRoot , "0.29.1" ) ;
226+
227+ const result = await clearPluginCache ( { latestVersion : null } ) ;
228+
229+ expect ( result ) . toMatchObject ( {
230+ action : "check_unavailable" ,
231+ cached : "0.29.1" ,
232+ path : pluginCachePath ,
233+ paths : [ pluginCachePath ] ,
234+ } ) ;
235+ expect ( result . latest ) . toBeUndefined ( ) ;
236+ expect ( existsSync ( pluginCachePath ) ) . toBe ( true ) ;
237+ } ) ;
238+
239+ it ( "force-clears existing cache even when plugin npm latest is unavailable" , async ( ) => {
240+ const cacheRoot = makeTempDir ( "mc-opencode-cache-" ) ;
241+ originalXdgCacheHome = process . env . XDG_CACHE_HOME ;
242+ process . env . XDG_CACHE_HOME = cacheRoot ;
243+ const pluginCachePath = createCachedOpenCodePlugin ( cacheRoot , "0.29.1" ) ;
244+
245+ const result = await clearPluginCache ( { force : true , latestVersion : null } ) ;
246+
247+ expect ( result ) . toMatchObject ( {
248+ action : "cleared" ,
249+ cached : "0.29.1" ,
250+ path : pluginCachePath ,
251+ paths : [ pluginCachePath ] ,
252+ } ) ;
253+ expect ( result . latest ) . toBeUndefined ( ) ;
254+ expect ( existsSync ( pluginCachePath ) ) . toBe ( false ) ;
255+ } ) ;
256+ } ) ;
257+
132258describe ( "doctor v22 backfill commands" , ( ) => {
133259 it ( "--check-v22-backfill reports status" , async ( ) => {
134260 const database = makeDb ( ) ;
0 commit comments