1111// takes effect or is rejected out loud — never dropped. These tests pin the
1212// rejection at each door it can come in through, and pin that the arms which
1313// DO honour the block still do.
14+ //
15+ // #5931 — the SISTER ARM. `memory` dropped `pool` exactly as silently
16+ // (`buildMemoryConfig` reads `spec.config` only, and `InMemoryDriver` has no
17+ // pool concept whatsoever), but it was left out of the rejected set because
18+ // #5714's ruling was scoped to the two sqlite arms. Measured the same way:
19+ //
20+ // memory + pool{min:3,max:9} driver config {"persistence":false} pool undefined
21+ //
22+ // The maintainer ruling of 2026-08-07 folds it in, with its own explanation
23+ // rather than SQLite's, and sets the default for the next sister arm (#6140).
24+ // The pin that used to hold `memory` OUT of the set is flipped below, not
25+ // deleted — it is the same fact, re-judged.
1426
1527import { describe , it , expect , vi } from 'vitest' ;
1628import {
1729 POOL_UNSUPPORTED_DRIVER_IDS ,
1830 driverReadsDeclaredPool ,
1931 unsupportedPoolIssue ,
32+ unsupportedPoolMessage ,
2033 assertDatasourcePoolSupported ,
2134} from '../datasource-pool-support.js' ;
2235import { createDefaultDatasourceDriverFactory } from '../default-datasource-driver-factory.js' ;
@@ -29,8 +42,8 @@ import { DatasourceAdminService, type StoredDatasource } from '../datasource-adm
2942import type { IDatasourceDriverFactory } from '../contracts/datasource-driver-factory.js' ;
3043
3144describe ( '#5714 — which driver arms read a declared `pool`' , ( ) => {
32- it ( 'names exactly the two sqlite arms as unable to honour it' , ( ) => {
33- expect ( [ ...POOL_UNSUPPORTED_DRIVER_IDS ] ) . toEqual ( [ 'sqlite' , 'sqlite-wasm' ] ) ;
45+ it ( 'names the two sqlite arms and `memory` as unable to honour it (#5931) ' , ( ) => {
46+ expect ( [ ...POOL_UNSUPPORTED_DRIVER_IDS ] ) . toEqual ( [ 'memory' , ' sqlite', 'sqlite-wasm' ] ) ;
3447 } ) ;
3548
3649 it ( 'rejects every spelling of the sqlite arms, case-insensitively' , ( ) => {
@@ -53,17 +66,24 @@ describe('#5714 — which driver arms read a declared `pool`', () => {
5366 expect ( unsupportedPoolIssue ( { driver : 'com.vendor.snowflake' , pool : { min : 3 , max : 9 } } ) ) . toBeUndefined ( ) ;
5467 } ) ;
5568
56- // Deliberate, and filed rather than silently widened: `memory` reads no pool
57- // either, but the #5714 ruling authorised this tightening for the sqlite arms
58- // only. #5931 carries the decision.
59- it ( 'leaves `memory` out of the rejected set (#5931), deliberately' , ( ) => {
60- expect ( driverReadsDeclaredPool ( 'memory' ) ) . toBe ( true ) ;
69+ // THE FLIPPED PIN (#5931). This case used to assert the opposite — `memory`
70+ // deliberately OUT of the rejected set, pending a ruling — and #5954 left it
71+ // green on purpose so the hole would read as drawn rather than overlooked.
72+ // The ruling of 2026-08-07 folded the arm in, so the same fact now has the
73+ // opposite verdict. Every spelling, because `mingo` and `in-memory` build the
74+ // very same driver.
75+ it ( 'rejects `memory` and every spelling of it (#5931 — the flipped pin)' , ( ) => {
76+ for ( const id of [ 'memory' , 'inmemory' , 'in-memory' , 'mingo' , 'Memory' , ' MEMORY ' ] ) {
77+ expect ( driverReadsDeclaredPool ( id ) , id ) . toBe ( false ) ;
78+ }
6179 } ) ;
6280
6381 it ( 'treats an absent or empty block as no declaration' , ( ) => {
64- expect ( unsupportedPoolIssue ( { driver : 'sqlite' } ) ) . toBeUndefined ( ) ;
65- expect ( unsupportedPoolIssue ( { driver : 'sqlite' , pool : { } } ) ) . toBeUndefined ( ) ;
66- expect ( unsupportedPoolIssue ( { driver : 'sqlite' , pool : undefined } ) ) . toBeUndefined ( ) ;
82+ for ( const driver of [ 'sqlite' , 'memory' ] ) {
83+ expect ( unsupportedPoolIssue ( { driver } ) , driver ) . toBeUndefined ( ) ;
84+ expect ( unsupportedPoolIssue ( { driver, pool : { } } ) , driver ) . toBeUndefined ( ) ;
85+ expect ( unsupportedPoolIssue ( { driver, pool : undefined } ) , driver ) . toBeUndefined ( ) ;
86+ }
6787 } ) ;
6888
6989 it ( 'names the datasource and the one edit that fixes it' , ( ) => {
@@ -77,14 +97,73 @@ describe('#5714 — which driver arms read a declared `pool`', () => {
7797 // authoring mistake has a correction; suggesting an env var that boots past
7898 // it (or a different driver) sends the author away from the fix.
7999 it ( 'offers no escape hatch and no "use another driver" advice' , ( ) => {
80- const msg = unsupportedPoolIssue ( { driver : 'sqlite-wasm' , pool : { max : 9 } , name : 'ds' } ) ?? '' ;
81- expect ( msg ) . not . toMatch ( / O S _ A L L O W _ D R I V E R _ C O N N E C T _ F A I L U R E / ) ;
82- expect ( msg ) . not . toMatch ( / O S _ [ A - Z _ ] * = 1 / ) ;
83- expect ( msg ) . not . toMatch ( / s w i t c h | i n s t e a d u s e | c h a n g e t h e d r i v e r / i) ;
100+ for ( const driver of [ 'sqlite-wasm' , 'memory' ] ) {
101+ const msg = unsupportedPoolIssue ( { driver, pool : { max : 9 } , name : 'ds' } ) ?? '' ;
102+ expect ( msg , driver ) . not . toMatch ( / O S _ A L L O W _ D R I V E R _ C O N N E C T _ F A I L U R E / ) ;
103+ expect ( msg , driver ) . not . toMatch ( / O S _ [ A - Z _ ] * = 1 / ) ;
104+ expect ( msg , driver ) . not . toMatch ( / s w i t c h | i n s t e a d u s e | c h a n g e t h e d r i v e r / i) ;
105+ }
106+ } ) ;
107+
108+ // #5931 — the memory arm gets its OWN explanation. The ruling was explicit
109+ // that SQLite's sentence must not be reused, and the reason is not style:
110+ // "the driver owns the connection strategy" would send the author looking for
111+ // a strategy knob, when the truth is that there is no connection to pool.
112+ it ( 'explains `memory` in its own terms, never SQLite\'s (#5931)' , ( ) => {
113+ const msg = unsupportedPoolIssue ( { driver : 'memory' , pool : { min : 3 , max : 9 } , name : 'scratch' } ) ?? '' ;
114+ expect ( msg ) . toContain ( `Datasource 'scratch'` ) ;
115+ expect ( msg ) . toContain ( `the 'memory' driver does not read it` ) ;
116+ expect ( msg ) . toMatch ( / n o p o o l t o s i z e , a n d n o c o n n e c t i o n t o p o o l / ) ;
117+ expect ( msg ) . toMatch ( / p l a i n d a t a s t r u c t u r e i n s i d e t h i s p r o c e s s / ) ;
118+ // …and not one part of SQLite's reasoning, which is about a DIFFERENT
119+ // failure (one datasource split across several stores).
120+ expect ( msg ) . not . toMatch ( / S Q L i t e / ) ;
121+ expect ( msg ) . not . toMatch ( / c o n n e c t i o n s t r a t e g y i s o w n e d b y t h e d r i v e r / ) ;
122+ expect ( msg ) . not . toMatch ( / S E P A R A T E , e m p t y d a t a b a s e / ) ;
123+ expect ( msg ) . not . toMatch ( / s p l i t o n e d a t a s o u r c e ' s d a t a / ) ;
124+ // The shared frame survives: the one edit that fixes it, and where the key
125+ // stays meaningful.
126+ expect ( msg ) . toMatch ( / R e m o v e ` p o o l ` f r o m t h i s d a t a s o u r c e d e c l a r a t i o n / ) ;
127+ expect ( msg ) . toMatch ( / p o s t g r e s \/ m y s q l \/ m o n g o / ) ;
128+ } ) ;
129+
130+ it ( 'quotes the spelling the author actually wrote, with the memory reason behind it' , ( ) => {
131+ const msg = unsupportedPoolIssue ( { driver : 'mingo' , pool : { max : 2 } , name : 'scratch' } ) ?? '' ;
132+ expect ( msg ) . toContain ( `the 'mingo' driver does not read it` ) ;
133+ expect ( msg ) . toMatch ( / n o p o o l t o s i z e , a n d n o c o n n e c t i o n t o p o o l / ) ;
134+ } ) ;
135+
136+ // The two sqlite arms' text is UNCHANGED by #5931 — pinned whole, against the
137+ // literal as it stood on `origin/main` before this change, because "we only
138+ // added an arm" is a claim about bytes.
139+ it ( 'leaves the sqlite arms\' message byte-for-byte as #5714 wrote it' , ( ) => {
140+ const expected =
141+ "Datasource 'crm_primary' declares a `pool` block, but the 'sqlite' driver does not read " +
142+ 'it: a SQLite connection strategy is owned by the driver, not by the datasource — one ' +
143+ 'connection per database, because a second connection to `:memory:` opens a SEPARATE, ' +
144+ "empty database. Sizing it here would therefore split one datasource's data across " +
145+ 'several stores, so the block is rejected instead of dropped. Remove `pool` from this ' +
146+ 'datasource declaration; it stays meaningful on the pooled drivers ' +
147+ '(postgres / mysql / mongo).' ;
148+ expect ( unsupportedPoolMessage ( 'sqlite' , 'crm_primary' ) ) . toBe ( expected ) ;
149+ expect ( unsupportedPoolMessage ( 'sqlite-wasm' , 'crm_primary' ) )
150+ . toBe ( expected . replace ( "the 'sqlite' driver" , "the 'sqlite-wasm' driver" ) ) ;
151+ } ) ;
152+
153+ // Unreachable through `unsupportedPoolIssue` (nothing produces a message for a
154+ // driver that reads the block), but the helper is exported and takes a plain
155+ // string. The honest answer there is the part true of every rejected arm —
156+ // never another arm's specific reasoning.
157+ it ( 'borrows no arm\'s reasoning for a driver that is not in the set' , ( ) => {
158+ const msg = unsupportedPoolMessage ( 'postgres' ) ;
159+ expect ( msg ) . toMatch ( / n o t h i n g i n t h e b l o c k r e a c h e s a c o n n e c t i o n / ) ;
160+ expect ( msg ) . not . toMatch ( / S Q L i t e / ) ;
161+ expect ( msg ) . not . toMatch ( / p l a i n d a t a s t r u c t u r e i n s i d e t h i s p r o c e s s / ) ;
84162 } ) ;
85163
86164 it ( 'assert throws exactly when the issue is reported' , ( ) => {
87165 expect ( ( ) => assertDatasourcePoolSupported ( { driver : 'sqlite' , pool : { max : 5 } } ) ) . toThrow ( / d o e s n o t r e a d i t / ) ;
166+ expect ( ( ) => assertDatasourcePoolSupported ( { driver : 'memory' , pool : { max : 5 } } ) ) . toThrow ( / d o e s n o t r e a d i t / ) ;
88167 expect ( ( ) => assertDatasourcePoolSupported ( { driver : 'postgres' , pool : { max : 5 } } ) ) . not . toThrow ( ) ;
89168 } ) ;
90169} ) ;
@@ -119,6 +198,43 @@ describe('#5714 — the driver factory rejects a pool it cannot honour', () => {
119198 ) . rejects . toThrow ( / d o e s n o t r e a d i t / ) ;
120199 } ) ;
121200
201+ // #5931 — the arm this file used to pin as deliberately unguarded. Before the
202+ // ruling this call RESOLVED, handing back an `InMemoryDriver` built from
203+ // `buildMemoryConfig(spec)` alone, with `pool` nowhere in it.
204+ it ( 'memory + pool is rejected instead of built with the block dropped (#5931)' , async ( ) => {
205+ await expect (
206+ factory ( ) . create ( {
207+ name : 'scratch' ,
208+ driver : 'memory' ,
209+ config : { } ,
210+ pool : { min : 3 , max : 9 } ,
211+ } ) ,
212+ ) . rejects . toThrow ( / D a t a s o u r c e ' s c r a t c h ' d e c l a r e s a ` p o o l ` b l o c k / ) ;
213+ } ) ;
214+
215+ it ( 'rejects the `inmemory` alias too, and says why in memory terms' , async ( ) => {
216+ const err = await Promise . resolve (
217+ factory ( ) . create ( { name : 'scratch' , driver : 'inmemory' , config : { } , pool : { max : 9 } } ) ,
218+ ) . then ( ( ) => undefined , ( e : Error ) => e ) ;
219+ expect ( err ?. message ) . toMatch ( / n o p o o l t o s i z e , a n d n o c o n n e c t i o n t o p o o l / ) ;
220+ expect ( err ?. message ) . not . toMatch ( / S Q L i t e / ) ;
221+ } ) ;
222+
223+ it ( 'memory WITHOUT a pool still builds exactly as before' , async ( ) => {
224+ const handle : any = await factory ( ) . create ( { name : 'scratch' , driver : 'memory' , config : { } } ) ;
225+ const driver = handle . driver ?? handle ;
226+ expect ( driver ?. constructor ?. name ) . toMatch ( / I n M e m o r y D r i v e r $ / ) ;
227+ // The #4083 shape is untouched: ephemeral unless the author opts in.
228+ expect ( driver . config ?. persistence ) . toBe ( false ) ;
229+ try { await handle . disconnect ?.( ) ; } catch { /* never connected */ }
230+ } ) ;
231+
232+ it ( 'memory with an EMPTY pool block still builds — nothing was declared' , async ( ) => {
233+ const handle : any = await factory ( ) . create ( { name : 'scratch' , driver : 'memory' , config : { } , pool : { } } ) ;
234+ expect ( ( handle . driver ?? handle ) ?. constructor ?. name ) . toMatch ( / I n M e m o r y D r i v e r $ / ) ;
235+ try { await handle . disconnect ?.( ) ; } catch { /* never connected */ }
236+ } ) ;
237+
122238 it ( 'sqlite WITHOUT a pool still builds exactly as before' , async ( ) => {
123239 const handle : any = await factory ( ) . create ( {
124240 name : 'crm_primary' ,
@@ -240,6 +356,46 @@ describe('#5714 — boot refuses a declared pool the driver cannot honour', () =
240356 ) . resolves . toEqual ( [ ] ) ;
241357 } ) ;
242358
359+ // #5931 — the same door, the sister arm. A memory datasource carrying a pool
360+ // used to boot silently and run on a store no pool setting ever touched.
361+ it ( 'refuses a memory datasource carrying a pool, before anything is connected (#5931)' , async ( ) => {
362+ const { service, factory, engine } = svc ( ) ;
363+ await expect (
364+ service . connectDeclared ( {
365+ datasources : [ { name : 'scratch' , driver : 'memory' , config : { } , pool : { min : 3 , max : 9 } } ] ,
366+ objects : [ ] ,
367+ } ) ,
368+ ) . rejects . toThrow ( / D a t a s o u r c e ' s c r a t c h ' d e c l a r e s a ` p o o l ` b l o c k / ) ;
369+ expect ( ( factory . create as any ) . mock . calls . length ) . toBe ( 0 ) ;
370+ expect ( engine . drivers . size ) . toBe ( 0 ) ;
371+ } ) ;
372+
373+ it ( 'names a memory offender alongside a sqlite one in the same throw' , async ( ) => {
374+ const { service } = svc ( ) ;
375+ const err = await service
376+ . connectDeclared ( {
377+ datasources : [ sqliteWithPool , { name : 'scratch' , driver : 'memory' , pool : { max : 4 } } ] ,
378+ objects : [ ] ,
379+ } )
380+ . then ( ( ) => undefined , ( e : Error ) => e ) ;
381+ expect ( err ?. message ) . toMatch ( / 2 d e c l a r e d d a t a s o u r c e \( s \) / ) ;
382+ expect ( err ?. message ) . toContain ( `Datasource 'crm_primary'` ) ;
383+ expect ( err ?. message ) . toContain ( `Datasource 'scratch'` ) ;
384+ // Each offender keeps its own explanation in the aggregate.
385+ expect ( err ?. message ) . toMatch ( / a S Q L i t e c o n n e c t i o n s t r a t e g y i s o w n e d b y t h e d r i v e r / ) ;
386+ expect ( err ?. message ) . toMatch ( / n o p o o l t o s i z e , a n d n o c o n n e c t i o n t o p o o l / ) ;
387+ } ) ;
388+
389+ it ( 'leaves a memory datasource with no pool block connecting as before' , async ( ) => {
390+ const { service, engine } = svc ( ) ;
391+ const results = await service . connectDeclared ( {
392+ datasources : [ { name : 'scratch' , driver : 'memory' , config : { } , autoConnect : true } ] ,
393+ objects : [ ] ,
394+ } ) ;
395+ expect ( results . map ( ( r ) => r . status ) ) . toEqual ( [ 'connected' ] ) ;
396+ expect ( engine . drivers . has ( 'scratch' ) ) . toBe ( true ) ;
397+ } ) ;
398+
243399 it ( 'leaves a sqlite datasource with no pool block connecting as before' , async ( ) => {
244400 const { service, engine } = svc ( ) ;
245401 const results = await service . connectDeclared ( {
@@ -328,6 +484,39 @@ describe('#5714 — the Setup wizard rejects it before the record is stored', ()
328484 expect ( records [ 0 ] ?. pool ) . toEqual ( { min : 3 , max : 9 } ) ;
329485 } ) ;
330486
487+ // #5931 — the wizard is the door an author is most likely to come through
488+ // with `driver: memory`, since it is the dev/test choice the Setup UI offers.
489+ it ( 'create: a memory draft carrying a pool never reaches the store (#5931)' , async ( ) => {
490+ const { service, records, registered } = adminHarness ( ) ;
491+ await expect (
492+ service . createDatasource ( {
493+ name : 'scratch' ,
494+ driver : 'memory' ,
495+ config : { } ,
496+ pool : { min : 1 , max : 5 } ,
497+ } ) ,
498+ ) . rejects . toThrow ( / n o p o o l t o s i z e , a n d n o c o n n e c t i o n t o p o o l / ) ;
499+ expect ( records ) . toHaveLength ( 0 ) ;
500+ expect ( registered ) . toHaveLength ( 0 ) ;
501+ } ) ;
502+
503+ it ( 'create: a memory draft with no pool is stored as before' , async ( ) => {
504+ const { service, records, registered } = adminHarness ( ) ;
505+ await service . createDatasource ( { name : 'scratch' , driver : 'memory' , config : { } } ) ;
506+ expect ( records [ 0 ] ?. name ) . toBe ( 'scratch' ) ;
507+ expect ( records [ 0 ] ?. pool ) . toBeUndefined ( ) ;
508+ expect ( registered ) . toEqual ( [ 'scratch' ] ) ;
509+ } ) ;
510+
511+ it ( 'update: switching a pooled datasource TO memory is rejected on the merged record (#5931)' , async ( ) => {
512+ const { service } = adminHarness ( [
513+ { name : 'reporting' , driver : 'postgres' , config : { } , pool : { min : 3 , max : 9 } , origin : 'runtime' } ,
514+ ] ) ;
515+ await expect (
516+ service . updateDatasource ( 'reporting' , { driver : 'memory' , config : { } } ) ,
517+ ) . rejects . toThrow ( / d e c l a r e s a ` p o o l ` b l o c k / ) ;
518+ } ) ;
519+
331520 it ( 'update: patching a pool onto a stored sqlite datasource is rejected' , async ( ) => {
332521 const { service } = adminHarness ( [
333522 { name : 'local_cache' , driver : 'sqlite' , config : { filename : ':memory:' } , origin : 'runtime' } ,
0 commit comments