@@ -58,6 +58,8 @@ describe("runUpdate", () => {
5858 } ) ;
5959 mkdirSync ( makePath ( "node_modules" , "opendevbrowser" ) , { recursive : true } ) ;
6060 mkdirSync ( makePath ( "node_modules" , "oh-my-opencode" ) , { recursive : true } ) ;
61+ mkdirSync ( makePath ( "packages" , "opendevbrowser@latest" ) , { recursive : true } ) ;
62+ mkdirSync ( makePath ( "packages" , "oh-my-opencode@latest" ) , { recursive : true } ) ;
6163 writeFileSync ( makePath ( "package-lock.json" ) , "{\"lockfileVersion\":3}\n" , "utf8" ) ;
6264
6365 const result = runUpdate ( ) ;
@@ -69,6 +71,8 @@ describe("runUpdate", () => {
6971 } ) ;
7072 expect ( existsSync ( makePath ( "node_modules" , "opendevbrowser" ) ) ) . toBe ( false ) ;
7173 expect ( existsSync ( makePath ( "node_modules" , "oh-my-opencode" ) ) ) . toBe ( true ) ;
74+ expect ( existsSync ( makePath ( "packages" , "opendevbrowser@latest" ) ) ) . toBe ( false ) ;
75+ expect ( existsSync ( makePath ( "packages" , "oh-my-opencode@latest" ) ) ) . toBe ( true ) ;
7276 expect ( existsSync ( makePath ( "package-lock.json" ) ) ) . toBe ( false ) ;
7377 expect ( readManifest ( ) ) . toEqual ( {
7478 dependencies : {
@@ -107,9 +111,25 @@ describe("runUpdate", () => {
107111 expect ( existsSync ( makePath ( "package-lock.json" ) ) ) . toBe ( false ) ;
108112 } ) ;
109113
114+ it ( "repairs OpenCode package alias cache state" , ( ) => {
115+ mkdirSync ( makePath ( "packages" , "opendevbrowser@latest" ) , { recursive : true } ) ;
116+ mkdirSync ( makePath ( "packages" , "yaml-language-server" ) , { recursive : true } ) ;
117+
118+ const result = runUpdate ( ) ;
119+
120+ expect ( result ) . toEqual ( {
121+ success : true ,
122+ message : "Cache repaired. OpenCode will install the latest version on next run." ,
123+ cleared : true
124+ } ) ;
125+ expect ( existsSync ( makePath ( "packages" , "opendevbrowser@latest" ) ) ) . toBe ( false ) ;
126+ expect ( existsSync ( makePath ( "packages" , "yaml-language-server" ) ) ) . toBe ( true ) ;
127+ } ) ;
128+
110129 it ( "refuses to mutate cache state while another update lock is held" , ( ) => {
111130 writeManifest ( { dependencies : { opendevbrowser : "0.0.24" } } ) ;
112131 mkdirSync ( makePath ( "node_modules" , "opendevbrowser" ) , { recursive : true } ) ;
132+ mkdirSync ( makePath ( "packages" , "opendevbrowser@latest" ) , { recursive : true } ) ;
113133 writeFileSync ( makePath ( "package-lock.json" ) , "{\"lockfileVersion\":3}\n" , "utf8" ) ;
114134 writeFileSync (
115135 makePath ( ".opendevbrowser-update.lock" ) ,
@@ -124,6 +144,7 @@ describe("runUpdate", () => {
124144 expect ( result . message ) . toContain ( "another update is already running" ) ;
125145 expect ( readManifest ( ) ) . toEqual ( { dependencies : { opendevbrowser : "0.0.24" } } ) ;
126146 expect ( existsSync ( makePath ( "node_modules" , "opendevbrowser" ) ) ) . toBe ( true ) ;
147+ expect ( existsSync ( makePath ( "packages" , "opendevbrowser@latest" ) ) ) . toBe ( true ) ;
127148 expect ( existsSync ( makePath ( "package-lock.json" ) ) ) . toBe ( true ) ;
128149 } ) ;
129150
@@ -252,13 +273,15 @@ describe("runUpdate", () => {
252273
253274 it ( "does not delete package cache before validating a malformed manifest" , ( ) => {
254275 mkdirSync ( makePath ( "node_modules" , "opendevbrowser" ) , { recursive : true } ) ;
276+ mkdirSync ( makePath ( "packages" , "opendevbrowser@latest" ) , { recursive : true } ) ;
255277 writeFileSync ( makePath ( "package.json" ) , "{bad-json}" , "utf8" ) ;
256278
257279 const result = runUpdate ( ) ;
258280
259281 expect ( result . success ) . toBe ( false ) ;
260282 expect ( result . cleared ) . toBe ( false ) ;
261283 expect ( existsSync ( makePath ( "node_modules" , "opendevbrowser" ) ) ) . toBe ( true ) ;
284+ expect ( existsSync ( makePath ( "packages" , "opendevbrowser@latest" ) ) ) . toBe ( true ) ;
262285 } ) ;
263286
264287 it ( "refuses to rewrite symlinked cache manifests" , ( ) => {
@@ -298,6 +321,33 @@ describe("runUpdate", () => {
298321 expect ( result . message ) . toContain ( "refusing to modify symlinked cache path" ) ;
299322 } ) ;
300323
324+ it ( "refuses to delete through a symlinked packages parent" , ( ) => {
325+ const outsidePackages = join ( cacheDir , ".." , "outside-packages" ) ;
326+ mkdirSync ( outsidePackages , { recursive : true } ) ;
327+ writeFileSync ( join ( outsidePackages , "sentinel.txt" ) , "keep\n" , "utf8" ) ;
328+ symlinkSync ( outsidePackages , makePath ( "packages" ) ) ;
329+
330+ const result = runUpdate ( ) ;
331+
332+ expect ( result . success ) . toBe ( false ) ;
333+ expect ( result . message ) . toContain ( "refusing to modify symlinked cache path" ) ;
334+ expect ( readFileSync ( join ( outsidePackages , "sentinel.txt" ) , "utf8" ) ) . toBe ( "keep\n" ) ;
335+ } ) ;
336+
337+ it ( "refuses to delete a symlinked package alias cache" , ( ) => {
338+ const outsideAlias = join ( cacheDir , ".." , "outside-opendevbrowser-alias" ) ;
339+ mkdirSync ( makePath ( "packages" ) , { recursive : true } ) ;
340+ mkdirSync ( outsideAlias , { recursive : true } ) ;
341+ writeFileSync ( join ( outsideAlias , "sentinel.txt" ) , "keep\n" , "utf8" ) ;
342+ symlinkSync ( outsideAlias , makePath ( "packages" , "opendevbrowser@latest" ) ) ;
343+
344+ const result = runUpdate ( ) ;
345+
346+ expect ( result . success ) . toBe ( false ) ;
347+ expect ( result . message ) . toContain ( "refusing to modify symlinked cache path" ) ;
348+ expect ( readFileSync ( join ( outsideAlias , "sentinel.txt" ) , "utf8" ) ) . toBe ( "keep\n" ) ;
349+ } ) ;
350+
301351 it ( "preflights symlinked cache paths before rewriting stale manifest pins" , ( ) => {
302352 const outsideModules = join ( cacheDir , ".." , "outside-node-modules-with-manifest" ) ;
303353 mkdirSync ( outsideModules , { recursive : true } ) ;
0 commit comments