@@ -43,7 +43,7 @@ let _tmpDir = null
4343export const getApp = ( ) => _app
4444export const getDb = ( ) => _db
4545
46- // Schema — mirrors db.js exactly, DROP+CREATE gives a clean slate every time
46+ // Schema -- mirrors db.js exactly, DROP+CREATE gives a clean slate every time
4747const SCHEMA = `
4848 DROP TABLE IF EXISTS items;
4949 DROP TABLE IF EXISTS channels;
@@ -83,13 +83,39 @@ function dbRun(db, sql, params = []) {
8383 } )
8484}
8585
86+ function dbGet ( db , sql , params = [ ] ) {
87+ return new Promise ( ( resolve , reject ) => {
88+ db . get ( sql , params , ( err , row ) => err ? reject ( err ) : resolve ( row ) )
89+ } )
90+ }
91+
8692async function seedDefaultChannels ( db ) {
8793 const defaults = [ 'general' , 'projects' , 'assets' , 'temp' ]
8894 for ( const name of defaults ) {
8995 await dbRun ( db , 'INSERT INTO channels (name) VALUES (?)' , [ name ] )
9096 }
9197}
9298
99+ /**
100+ * Wait until db.js has finished its own async initialisation.
101+ * db.js uses db.serialize() to queue CREATE TABLE + INSERT channel statements.
102+ * We poll until those statements have landed rather than using a fixed timeout,
103+ * which makes this reliable across machines of different speeds (Windows, Linux CI).
104+ */
105+ async function waitForDbReady ( db , maxWaitMs = 5000 ) {
106+ const start = Date . now ( )
107+ while ( Date . now ( ) - start < maxWaitMs ) {
108+ try {
109+ const row = await dbGet ( db , "SELECT COUNT(*) as count FROM channels" )
110+ if ( row && row . count >= 4 ) return // default channels are seeded, ready
111+ } catch ( e ) {
112+ // table may not exist yet -- keep waiting
113+ }
114+ await new Promise ( resolve => setTimeout ( resolve , 20 ) )
115+ }
116+ throw new Error ( 'DB did not become ready within ' + maxWaitMs + 'ms' )
117+ }
118+
93119/**
94120 * Wipe all tables, recreate schema, re-seed default channels.
95121 * Call in beforeEach to give every test a perfectly clean slate.
@@ -100,7 +126,7 @@ export async function resetDb() {
100126}
101127
102128/**
103- * Insert a text item directly into the DB — bypasses HTTP layer.
129+ * Insert a text item directly into the DB -- bypasses HTTP layer.
104130 * Useful for setting up preconditions quickly.
105131 */
106132export function insertItem ( fields ) {
@@ -141,7 +167,7 @@ export function insertChannel(name, pinned = 0) {
141167export function setup ( ) {
142168 beforeAll ( async ( ) => {
143169
144- // Step 1: isolated temp directory — nothing touches real instbyte-data/
170+ // Step 1: isolated temp directory -- nothing touches real instbyte-data/
145171 _tmpDir = fs . mkdtempSync ( path . join ( os . tmpdir ( ) , 'instbyte-test-' ) )
146172 const uploadsDir = path . join ( _tmpDir , 'uploads' )
147173 fs . mkdirSync ( uploadsDir )
@@ -168,17 +194,16 @@ export function setup() {
168194 _app = mod . app
169195 _db = require ( '../../server/db.js' )
170196
171- // Step 6: wait for db.js to finish its own async init.
197+ // Step 6: wait for db.js to finish its own async init by polling .
172198 // db.js uses db.serialize() which queues CREATE TABLE + INSERT channel
173- // statements asynchronously. We wait for those to land before our first
174- // resetDb() call wipes them — otherwise resetDb() races with the seeding
175- // and causes a UNIQUE constraint error.
176- await new Promise ( resolve => setTimeout ( resolve , 300 ) )
199+ // statements asynchronously. We poll until the channels exist rather than
200+ // using a fixed timeout -- this is reliable across Windows and Linux CI.
201+ await waitForDbReady ( _db )
177202 } )
178203
179204 afterAll ( async ( ) => {
180205 // Close the SQLite connection before deleting the temp folder.
181- // On Windows the .sqlite file stays locked until explicitly closed —
206+ // On Windows the .sqlite file stays locked until explicitly closed --
182207 // fs.rmSync throws EBUSY without this step.
183208 if ( _db ) {
184209 await new Promise ( resolve => _db . close ( ( ) => resolve ( ) ) )
0 commit comments