1+ #!/usr/bin/env node
2+ /**
3+ * Simple integration test for conditional meta file deletion
4+ * This can be run directly with node to test the basic functionality
5+ */
6+
7+ import { expect } from 'chai' ;
8+ import fs from 'fs-extra' ;
9+ import path from 'node:path' ;
10+ import * as url from 'node:url' ;
11+ import { fileURLToPath } from 'node:url' ;
12+
13+ const __filename = fileURLToPath ( import . meta. url ) ;
14+ const __dirname = path . dirname ( __filename ) ;
15+
16+ // Mock objects database for testing
17+ class MockObjectsDB {
18+ constructor ( ) {
19+ this . objects = new Map ( ) ;
20+ }
21+
22+ async setObject ( id , obj ) {
23+ this . objects . set ( id , { ...obj , _id : id } ) ;
24+ }
25+
26+ async getObject ( id ) {
27+ return this . objects . get ( id ) || null ;
28+ }
29+
30+ async getObjectAsync ( id ) {
31+ return this . getObject ( id ) ;
32+ }
33+
34+ async delObject ( id ) {
35+ this . objects . delete ( id ) ;
36+ }
37+
38+ async delObjectAsync ( id ) {
39+ return this . delObject ( id ) ;
40+ }
41+
42+ async getObjectViewAsync ( design , view , params ) {
43+ const { startkey, endkey } = params ;
44+ const results = Array . from ( this . objects . entries ( ) )
45+ . filter ( ( [ id ] ) => id >= startkey && id < endkey )
46+ . filter ( ( [ , obj ] ) => obj . type === 'meta' )
47+ . map ( ( [ id , obj ] ) => ( { value : obj } ) ) ;
48+
49+ return { rows : results } ;
50+ }
51+ }
52+
53+ // Mock implementation of the conditional deletion logic
54+ class MockConditionalDeletion {
55+ constructor ( objects , testDir ) {
56+ this . objects = objects ;
57+ this . testDir = testDir ;
58+ }
59+
60+ async _hasInstanceMetaFiles ( adapter , instance ) {
61+ const adapterPrefix = `${ adapter } .${ instance } .` ;
62+ const doc = await this . objects . getObjectViewAsync ( 'system' , 'meta' , {
63+ startkey : `${ adapterPrefix } ` ,
64+ endkey : `${ adapterPrefix } \u9999` ,
65+ } ) ;
66+
67+ return doc . rows . some ( ( row ) =>
68+ row . value . _id &&
69+ row . value . _id . startsWith ( adapterPrefix ) &&
70+ row . value . _id !== `${ adapter } .${ instance } `
71+ ) ;
72+ }
73+
74+ async _isMetaFileDeletionAllowed ( adapter ) {
75+ try {
76+ const ioPackagePath = path . join ( this . testDir , 'adapters' , adapter , 'io-package.json' ) ;
77+ if ( await fs . pathExists ( ioPackagePath ) ) {
78+ const ioPackage = await fs . readJSON ( ioPackagePath ) ;
79+ return ioPackage . common ?. allowDeletionOfFilesInMetaObject === true ;
80+ }
81+ return false ;
82+ } catch {
83+ return false ;
84+ }
85+ }
86+
87+ async deleteInstance ( adapter , instance , withMeta ) {
88+ // Delete instance object
89+ await this . objects . delObjectAsync ( `system.adapter.${ adapter } .${ instance } ` ) ;
90+
91+ const hasMetaFiles = await this . _hasInstanceMetaFiles ( adapter , instance ) ;
92+
93+ if ( ! hasMetaFiles ) {
94+ return { metaDeleted : false , reason : 'no-meta-files' } ;
95+ }
96+
97+ const allowedByAdapter = await this . _isMetaFileDeletionAllowed ( adapter ) ;
98+
99+ if ( allowedByAdapter ) {
100+ await this . _deleteInstanceFiles ( adapter , instance ) ;
101+ return { metaDeleted : true , reason : 'adapter-allows' } ;
102+ }
103+
104+ if ( withMeta ) {
105+ await this . _deleteInstanceFiles ( adapter , instance ) ;
106+ return { metaDeleted : true , reason : 'with-meta-flag' } ;
107+ }
108+
109+ // In a real implementation, this would show an interactive prompt
110+ // For testing, we preserve the files
111+ return { metaDeleted : false , reason : 'user-not-confirmed' } ;
112+ }
113+
114+ async _deleteInstanceFiles ( adapter , instance ) {
115+ const adapterPrefix = `${ adapter } .${ instance } ` ;
116+ const doc = await this . objects . getObjectViewAsync ( 'system' , 'meta' , {
117+ startkey : `${ adapterPrefix } ` ,
118+ endkey : `${ adapterPrefix } \u9999` ,
119+ } ) ;
120+
121+ // Delete instance folder and all meta files
122+ await this . objects . delObjectAsync ( `${ adapter } .${ instance } ` ) ;
123+ for ( const row of doc . rows ) {
124+ if ( row . value . _id && row . value . _id . startsWith ( adapterPrefix ) ) {
125+ await this . objects . delObjectAsync ( row . value . _id ) ;
126+ }
127+ }
128+ }
129+ }
130+
131+ // Test runner
132+ async function runTests ( ) {
133+ console . log ( '🧪 Running Conditional Meta File Deletion Tests...\n' ) ;
134+
135+ const testDir = path . join ( __dirname , '../../tmp/test-meta-deletion' ) ;
136+ await fs . ensureDir ( testDir ) ;
137+
138+ try {
139+ const objects = new MockObjectsDB ( ) ;
140+ const deletion = new MockConditionalDeletion ( objects , testDir ) ;
141+
142+ // Test 1: Instance with meta files, adapter disallows deletion
143+ console . log ( 'Test 1: Preserve meta files when adapter disallows deletion' ) ;
144+ await setupTest1 ( objects , testDir ) ;
145+ const result1 = await deletion . deleteInstance ( 'testadapter' , 0 ) ;
146+ expect ( result1 . metaDeleted ) . to . be . false ;
147+ expect ( result1 . reason ) . to . equal ( 'user-not-confirmed' ) ;
148+ console . log ( '✅ PASSED: Meta files preserved\n' ) ;
149+
150+ // Test 2: Instance with meta files, adapter allows deletion
151+ console . log ( 'Test 2: Delete meta files when adapter allows deletion' ) ;
152+ await setupTest2 ( objects , testDir ) ;
153+ const result2 = await deletion . deleteInstance ( 'testadapter2' , 0 ) ;
154+ expect ( result2 . metaDeleted ) . to . be . true ;
155+ expect ( result2 . reason ) . to . equal ( 'adapter-allows' ) ;
156+ console . log ( '✅ PASSED: Meta files deleted due to adapter config\n' ) ;
157+
158+ // Test 3: Instance with meta files, withMeta flag
159+ console . log ( 'Test 3: Delete meta files when --with-meta flag is used' ) ;
160+ await setupTest1 ( objects , testDir ) ; // Reuse setup but different instance
161+ const result3 = await deletion . deleteInstance ( 'testadapter' , 1 , true ) ;
162+ expect ( result3 . metaDeleted ) . to . be . true ;
163+ expect ( result3 . reason ) . to . equal ( 'with-meta-flag' ) ;
164+ console . log ( '✅ PASSED: Meta files deleted due to --with-meta flag\n' ) ;
165+
166+ // Test 4: Instance without meta files
167+ console . log ( 'Test 4: Normal behavior when no meta files exist' ) ;
168+ await setupTest4 ( objects , testDir ) ;
169+ const result4 = await deletion . deleteInstance ( 'testadapter3' , 0 ) ;
170+ expect ( result4 . metaDeleted ) . to . be . false ;
171+ expect ( result4 . reason ) . to . equal ( 'no-meta-files' ) ;
172+ console . log ( '✅ PASSED: Normal deletion when no meta files\n' ) ;
173+
174+ // Test 5: Meta file enumeration logic
175+ console . log ( 'Test 5: Verify meta file detection logic' ) ;
176+ await setupTest5 ( objects , testDir ) ;
177+ const hasMetaFiles = await deletion . _hasInstanceMetaFiles ( 'testadapter4' , 0 ) ;
178+ const hasNoMetaFiles = await deletion . _hasInstanceMetaFiles ( 'testadapter4' , 1 ) ;
179+ expect ( hasMetaFiles ) . to . be . true ;
180+ expect ( hasNoMetaFiles ) . to . be . false ;
181+ console . log ( '✅ PASSED: Meta file detection works correctly\n' ) ;
182+
183+ console . log ( '🎉 All tests passed! The conditional meta file deletion feature works correctly.' ) ;
184+
185+ } finally {
186+ // Cleanup
187+ await fs . remove ( testDir ) ;
188+ }
189+ }
190+
191+ // Test setup functions
192+ async function setupTest1 ( objects , testDir ) {
193+ // Create instance
194+ await objects . setObject ( 'system.adapter.testadapter.0' , {
195+ type : 'instance' ,
196+ common : { name : 'testadapter' }
197+ } ) ;
198+
199+ // Create meta objects
200+ await objects . setObject ( 'testadapter.0' , {
201+ type : 'meta' ,
202+ common : { type : 'meta.folder' }
203+ } ) ;
204+ await objects . setObject ( 'testadapter.0.project1' , {
205+ type : 'meta' ,
206+ common : { type : 'meta.user' }
207+ } ) ;
208+
209+ // Create io-package.json that DOES NOT allow deletion
210+ await fs . ensureDir ( path . join ( testDir , 'adapters/testadapter' ) ) ;
211+ await fs . writeJSON ( path . join ( testDir , 'adapters/testadapter/io-package.json' ) , {
212+ common : {
213+ name : 'testadapter' ,
214+ allowDeletionOfFilesInMetaObject : false
215+ }
216+ } ) ;
217+ }
218+
219+ async function setupTest2 ( objects , testDir ) {
220+ // Create instance
221+ await objects . setObject ( 'system.adapter.testadapter2.0' , {
222+ type : 'instance' ,
223+ common : { name : 'testadapter2' }
224+ } ) ;
225+
226+ // Create meta objects
227+ await objects . setObject ( 'testadapter2.0' , {
228+ type : 'meta' ,
229+ common : { type : 'meta.folder' }
230+ } ) ;
231+ await objects . setObject ( 'testadapter2.0.project1' , {
232+ type : 'meta' ,
233+ common : { type : 'meta.user' }
234+ } ) ;
235+
236+ // Create io-package.json that ALLOWS deletion
237+ await fs . ensureDir ( path . join ( testDir , 'adapters/testadapter2' ) ) ;
238+ await fs . writeJSON ( path . join ( testDir , 'adapters/testadapter2/io-package.json' ) , {
239+ common : {
240+ name : 'testadapter2' ,
241+ allowDeletionOfFilesInMetaObject : true
242+ }
243+ } ) ;
244+ }
245+
246+ async function setupTest4 ( objects , testDir ) {
247+ // Create instance without meta files
248+ await objects . setObject ( 'system.adapter.testadapter3.0' , {
249+ type : 'instance' ,
250+ common : { name : 'testadapter3' }
251+ } ) ;
252+
253+ // No meta objects created for this test
254+ }
255+
256+ async function setupTest5 ( objects , testDir ) {
257+ // Create instance with meta files
258+ await objects . setObject ( 'testadapter4.0.project1' , {
259+ type : 'meta' ,
260+ common : { type : 'meta.user' }
261+ } ) ;
262+
263+ // Instance 1 has no meta files
264+ // (no objects created for instance 1)
265+ }
266+
267+ // Run the tests
268+ if ( import . meta. url === url . pathToFileURL ( process . argv [ 1 ] ) . href ) {
269+ runTests ( ) . catch ( console . error ) ;
270+ }
271+
272+ export { runTests } ;
0 commit comments