@@ -2,10 +2,13 @@ const fs = require('fs');
22const os = require ( 'os' ) ;
33const path = require ( 'path' ) ;
44const childProcess = require ( 'child_process' ) ;
5+ const undici = require ( 'undici' ) ;
56
67const originalExec = childProcess . exec ;
8+ const originalFetch = undici . fetch ;
79let tempDir = null ;
810let success = false ;
11+ let categoriesBackup = null ;
912
1013( async ( ) => {
1114 let execInvocations = 0 ;
@@ -30,7 +33,54 @@ let success = false;
3033 } ) ;
3134 } ;
3235
36+ const repoRoot = path . resolve ( __dirname , '..' ) ;
37+ const categoriesCachePath = path . join ( repoRoot , 'categories-cache.json' ) ;
38+ const categoriesMetaPath = path . join ( repoRoot , 'categories-cache.meta.json' ) ;
39+
40+ function snapshotCategoriesFiles ( ) {
41+ return {
42+ cache : fs . existsSync ( categoriesCachePath ) ? fs . readFileSync ( categoriesCachePath ) : null ,
43+ meta : fs . existsSync ( categoriesMetaPath ) ? fs . readFileSync ( categoriesMetaPath ) : null
44+ } ;
45+ }
46+
47+ function restoreCategoriesFiles ( snapshot ) {
48+ if ( ! snapshot ) return ;
49+ if ( snapshot . cache ) fs . writeFileSync ( categoriesCachePath , snapshot . cache ) ;
50+ else if ( fs . existsSync ( categoriesCachePath ) ) fs . rmSync ( categoriesCachePath ) ;
51+ if ( snapshot . meta ) fs . writeFileSync ( categoriesMetaPath , snapshot . meta ) ;
52+ else if ( fs . existsSync ( categoriesMetaPath ) ) fs . rmSync ( categoriesMetaPath ) ;
53+ }
54+
55+ function createHeadersProxy ( headers = { } ) {
56+ const normalized = Object . fromEntries (
57+ Object . entries ( headers ) . map ( ( [ k , v ] ) => [ String ( k ) . toLowerCase ( ) , v ] )
58+ ) ;
59+ return {
60+ get ( name ) {
61+ return normalized [ String ( name ) . toLowerCase ( ) ] || null ;
62+ }
63+ } ;
64+ }
65+
66+ function createResponse ( { status = 200 , ok = true , jsonData = null , textData = '' , headers = { } } ) {
67+ return {
68+ status,
69+ ok,
70+ async json ( ) {
71+ if ( jsonData === null ) throw new Error ( 'JSON payload missing' ) ;
72+ return jsonData ;
73+ } ,
74+ async text ( ) {
75+ return textData ;
76+ } ,
77+ headers : createHeadersProxy ( headers )
78+ } ;
79+ }
80+
3381 try {
82+ categoriesBackup = snapshotCategoriesFiles ( ) ;
83+
3484 const { detectPackageManager, invalidatePackageManagerCache } = require ( '../src/main/packageManager' ) ;
3585 const { createIconCacheManager } = require ( '../src/main/iconCache' ) ;
3686
@@ -82,6 +132,71 @@ let success = false;
82132 if ( ! purgeResult || purgeResult . removed < 1 ) throw new Error ( 'Purge cache did not report removed files' ) ;
83133 if ( fs . existsSync ( fakeIconPath ) ) throw new Error ( 'Purge cache did not delete dummy icon' ) ;
84134
135+ const fileEtags = new Map ( ) ;
136+ const markdownByFile = {
137+ 'games.md' : `| App | Desc |\n| --- | --- |\n| ***alpha*** | great app |` ,
138+ 'tools.md' : `| App | Desc |\n| --- | --- |\n| ***beta*** | tool desc |`
139+ } ;
140+
141+ undici . fetch = async ( url , options = { } ) => {
142+ const headers = options . headers || { } ;
143+ if ( url . endsWith ( '/contents' ) ) {
144+ return createResponse ( {
145+ jsonData : [
146+ { name : 'games.md' } ,
147+ { name : 'tools.md' } ,
148+ { name : 'README.md' }
149+ ]
150+ } ) ;
151+ }
152+ const fileName = path . basename ( url ) ;
153+ if ( ! markdownByFile [ fileName ] ) {
154+ return createResponse ( { status : 404 , ok : false , textData : 'missing' } ) ;
155+ }
156+ const previousEtag = fileEtags . get ( fileName ) ;
157+ if ( headers [ 'If-None-Match' ] && previousEtag && headers [ 'If-None-Match' ] === previousEtag ) {
158+ return createResponse ( { status : 304 , ok : false } ) ;
159+ }
160+ const nextEtag = `W/"etag-${ fileName } -${ Date . now ( ) } "` ;
161+ fileEtags . set ( fileName , nextEtag ) ;
162+ return createResponse ( {
163+ textData : markdownByFile [ fileName ] ,
164+ headers : { etag : nextEtag , 'last-modified' : new Date ( ) . toUTCString ( ) }
165+ } ) ;
166+ } ;
167+
168+ const { registerCategoryHandlers } = require ( '../src/main/categories' ) ;
169+ const ipcHandlers = new Map ( ) ;
170+ const ipcMain = {
171+ handle ( channel , handler ) {
172+ ipcHandlers . set ( channel , handler ) ;
173+ }
174+ } ;
175+
176+ registerCategoryHandlers ( ipcMain ) ;
177+ if ( ! ipcHandlers . has ( 'fetch-all-categories' ) ) throw new Error ( 'fetch-all-categories handler missing' ) ;
178+
179+ const firstFetch = await ipcHandlers . get ( 'fetch-all-categories' ) ( ) ;
180+ if ( ! firstFetch . ok ) throw new Error ( `fetch-all-categories failed: ${ firstFetch . error } ` ) ;
181+ if ( ! Array . isArray ( firstFetch . categories ) || firstFetch . categories . length !== 2 ) {
182+ throw new Error ( 'Unexpected categories payload on first fetch' ) ;
183+ }
184+ if ( ! firstFetch . categories [ 0 ] . apps . includes ( 'alpha' ) ) throw new Error ( 'Category parsing failed' ) ;
185+
186+ const cached = await ipcHandlers . get ( 'get-categories-cache' ) ( ) ;
187+ if ( ! cached . ok || cached . categories . length !== 2 ) throw new Error ( 'get-categories-cache returned invalid data' ) ;
188+
189+ const secondFetch = await ipcHandlers . get ( 'fetch-all-categories' ) ( ) ;
190+ if ( ! secondFetch . ok ) throw new Error ( 'Second fetch should still be ok (even with 304)' ) ;
191+ if ( secondFetch . categories . length !== firstFetch . categories . length ) {
192+ throw new Error ( 'Second fetch should reuse cached categories when not modified' ) ;
193+ }
194+
195+ const deleteResult = await ipcHandlers . get ( 'delete-categories-cache' ) ( ) ;
196+ if ( ! deleteResult . ok ) throw new Error ( 'delete-categories-cache failed' ) ;
197+ if ( fs . existsSync ( categoriesCachePath ) ) throw new Error ( 'Cache file still present after deletion' ) ;
198+ if ( fs . existsSync ( categoriesMetaPath ) ) throw new Error ( 'Cache meta file still present after deletion' ) ;
199+
85200 success = true ;
86201 console . log ( '✔ Main-process modules verified successfully.' ) ;
87202 } catch ( err ) {
@@ -96,6 +211,8 @@ let success = false;
96211 console . warn ( 'Warning: failed to clean temporary directory:' , cleanupErr . message ) ;
97212 }
98213 childProcess . exec = originalExec ;
214+ undici . fetch = originalFetch ;
215+ restoreCategoriesFiles ( categoriesBackup ) ;
99216 if ( success ) {
100217 process . exitCode = 0 ;
101218 }
0 commit comments