77//
88// This boots the host-config shape (instantiated plugins, no MetadataPlugin —
99// the same shape `examples/app-showcase` runs under `os dev`) with the REAL
10- // driver factory (`createDefaultDatasourceDriverFactory`) building an in-memory
11- // driver, so the full AppPlugin → `datasource-connection` → engine path runs
12- // without any native driver dependency.
10+ // driver factory (`createDefaultDatasourceDriverFactory`) building a real
11+ // sqlite `:memory:` pool, so the full AppPlugin → `datasource-connection` →
12+ // engine path runs against the same engine production uses.
13+ //
14+ // Backend note (#5704 batch 0): both the host default driver and the declared
15+ // datasources ran on `@objectstack/driver-memory` until this file was migrated
16+ // to `@objectstack/driver-sql` + better-sqlite3 `:memory:` — the repo's
17+ // canonical ephemeral store (`examples/app-crm`, `cli db clean`). driver-memory
18+ // is the project's legacy test-convenience backend and its in-project test
19+ // surface is being retired (#5499). `:memory:` keeps every acceptance below
20+ // hermetic: the database lives and dies inside the process, so nothing reaches
21+ // the host filesystem and each boot starts empty.
1322
1423import { describe , it , expect , beforeAll , afterAll , beforeEach , afterEach } from 'vitest' ;
1524import { existsSync , rmSync } from 'node:fs' ;
@@ -21,6 +30,32 @@ import type { DatasourceConnectPolicy } from '@objectstack/service-datasource';
2130
2231const BOOT_TIMEOUT = 60_000 ;
2332
33+ /** The host's default driver — sqlite `:memory:`, constructed the canonical way. */
34+ async function makeDefaultDriver ( ) {
35+ const { SqlDriver } = await import ( '@objectstack/driver-sql' ) ;
36+ return new SqlDriver ( {
37+ client : 'better-sqlite3' ,
38+ connection : { filename : ':memory:' } ,
39+ useNullAsDefault : true ,
40+ } ) ;
41+ }
42+
43+ /**
44+ * `schemaMode: 'external'` forbids DDL through the driver (ADR-0015 — ObjectStack
45+ * is a guest in that database), so the federated table has to already exist,
46+ * exactly as the real remote database an external datasource points at would
47+ * hold it. Raw `execute()` is the out-of-band channel that stands in for
48+ * "somebody else's migration already ran".
49+ *
50+ * With driver-memory this step did not exist: a mingo store materialises a
51+ * collection on first write, so an undeclared table was indistinguishable from a
52+ * declared one. Making it explicit is the point of the migration, not a
53+ * workaround for it.
54+ */
55+ async function ensureRemoteExtNoteTable ( driver : any ) : Promise < void > {
56+ await driver . execute ( 'CREATE TABLE IF NOT EXISTS ext_note (id text primary key, title text)' ) ;
57+ }
58+
2459// One external datasource (auto-connect target) + one managed, unrouted
2560// datasource (must stay metadata-only). NO `onEnable` anywhere.
2661function artifact ( ) {
@@ -41,11 +76,11 @@ function artifact() {
4176 datasources : [
4277 {
4378 name : 'autoconn_ext' ,
44- label : 'External (in- memory)' ,
45- driver : 'memory ' ,
79+ label : 'External (sqlite : memory: )' ,
80+ driver : 'sqlite ' ,
4681 schemaMode : 'external' ,
4782 origin : 'code' ,
48- config : { } ,
83+ config : { filename : ':memory:' } ,
4984 external : { allowWrites : false , validation : { onMismatch : 'warn' , checkOnBoot : false } } ,
5085 active : true ,
5186 } ,
@@ -54,10 +89,10 @@ function artifact() {
5489 {
5590 name : 'decorative' ,
5691 label : 'Decorative (unrouted)' ,
57- driver : 'memory ' ,
92+ driver : 'sqlite ' ,
5893 schemaMode : 'managed' ,
5994 origin : 'code' ,
60- config : { } ,
95+ config : { filename : ':memory:' } ,
6196 active : true ,
6297 } ,
6398 ] ,
@@ -66,17 +101,13 @@ function artifact() {
66101
67102async function boot ( opts : { connectPolicy ?: DatasourceConnectPolicy } = { } ) {
68103 const { ObjectQLPlugin } = await import ( '@objectstack/objectql' ) ;
69- const { InMemoryDriver } = await import ( '@objectstack/driver-memory' ) ;
70104 const { DatasourceAdminServicePlugin, createDefaultDatasourceDriverFactory } = await import (
71105 '@objectstack/service-datasource'
72106 ) ;
73107
74108 const runtime = new Runtime ( { cluster : false } ) ;
75109 const kernel = runtime . getKernel ( ) ;
76- // `persistence: false` keeps the acceptance hermetic. The driver's own default
77- // is `'auto'` — in Node a file adapter at `.objectstack/data/…` under the CWD,
78- // which both reloads and rewrites ambient state between runs (#4083).
79- await kernel . use ( new DriverPlugin ( new InMemoryDriver ( { persistence : false } ) ) ) ; // default driver
110+ await kernel . use ( new DriverPlugin ( await makeDefaultDriver ( ) ) ) ; // default driver
80111 await kernel . use ( new ObjectQLPlugin ( ) ) ;
81112 await kernel . use ( new AppPlugin ( artifact ( ) ) ) ;
82113 await kernel . use (
@@ -127,6 +158,7 @@ describe('ADR-0062 declared-datasource auto-connect', () => {
127158 // Seed the live external driver directly (bypassing the read-only write gate,
128159 // exactly as a real remote DB would already hold the rows).
129160 const driver = engine . getDriverByName ( 'autoconn_ext' ) ;
161+ await ensureRemoteExtNoteTable ( driver ) ;
130162 await driver . bulkCreate ( 'ext_note' , [
131163 { id : 'n1' , title : 'first' } ,
132164 { id : 'n2' , title : 'second' } ,
@@ -136,20 +168,36 @@ describe('ADR-0062 declared-datasource auto-connect', () => {
136168 } ) ;
137169} ) ;
138170
139- // #4083 — the acceptance above passed on a clean checkout and failed on every
140- // subsequent run, reading 2×N rows on the Nth: the auto-connected `memory`
171+ // ADR-0062 D1 acceptance — an auto-connected in-memory federated pool leaves
172+ // nothing behind and does not outlive its kernel.
173+ //
174+ // ## What this pinned before, and why it still says the same thing
175+ //
176+ // #4083: the acceptance above passed on a clean checkout and failed on every
177+ // subsequent run, reading 2×N rows on the Nth. The auto-connected `memory`
141178// datasource inherited `InMemoryDriver`'s then-default `persistence: 'auto'`
142- // (#4065 has since made that default `false`), so it
143- // flushed `ext_note` into `.objectstack/data/memory-driver.json` under the CWD
144- // and the next boot's connect() loaded those rows back before this file seeded
145- // its own. CI never caught it because CI always runs #1 on a fresh checkout.
179+ // (#4065 has since made that default `false`), so it flushed `ext_note` into
180+ // `.objectstack/data/memory-driver.json` under the CWD and the next boot's
181+ // connect() loaded those rows back before this file seeded its own. CI never
182+ // caught it because CI always runs #1 on a fresh checkout. The intermittency
183+ // ("passes once in four") came from WHEN the flush landed — the file adapter
184+ // wrote on a 2s unref'd autosave timer — so the original block called
185+ // `driver.flush?.()` to force the timer's work and remove the race.
186+ //
187+ // Those three things — `memory-driver.json`, `flush()`, the autosave timer —
188+ // are driver-memory FILE-ADAPTER specifics, and this file no longer runs on
189+ // driver-memory (#5704 batch 0). The acceptance target was never the file
190+ // adapter: ADR-0062 D1 asks for a runtime property — a federated in-memory pool
191+ // leaves nothing on the host and a restart starts empty — so the block is
192+ // rewritten against sqlite `:memory:` rather than deleted (deleting it would
193+ // drop D1's acceptance) and rather than moved into driver-memory's own suite
194+ // (the factory-level #4083 pin already lives in service-datasource's tests).
146195//
147- // The intermittency ("passes once in four") came from WHEN the flush lands: the
148- // file adapter writes on a 2s unref'd autosave timer, so a run short enough to
149- // finish first left nothing behind. `flush()` below stands in for that timer, so
150- // this pins the property that was actually broken — a federated in-memory pool
151- // leaves nothing behind and does not outlive its kernel — without a timing race
152- // and without depending on run-to-run state.
196+ // Under `:memory:` the property is asserted more directly than before: a fresh
197+ // pool has no `ext_note` TABLE at all, not merely no rows — the one thing a
198+ // reloaded snapshot could never look like. The filesystem half is unchanged and
199+ // still load-bearing: `filename: ':memory:'` is one config typo away from a
200+ // relative file path, and that typo is exactly what #4083 was.
153201describe ( 'ADR-0062 D1 — the auto-connected in-memory pool leaves nothing behind (#4083)' , ( ) => {
154202 const STATE_DIR = join ( process . cwd ( ) , '.objectstack' ) ;
155203 const clearState = ( ) => { try { rmSync ( STATE_DIR , { recursive : true , force : true } ) ; } catch { /* noop */ } } ;
@@ -159,25 +207,49 @@ describe('ADR-0062 D1 — the auto-connected in-memory pool leaves nothing behin
159207 beforeAll ( clearState ) ;
160208 afterAll ( clearState ) ;
161209
210+ function externalDriver ( kernel : Awaited < ReturnType < typeof boot > > ) {
211+ const engine = kernel . getService < { getDriverByName ( n : string ) : any } > ( 'data' ) ;
212+ return engine . getDriverByName ( 'autoconn_ext' ) ;
213+ }
214+
215+ /** Rows knex returns for a raw SELECT, whichever envelope the dialect uses. */
216+ function rowsOf ( result : any ) : any [ ] {
217+ if ( Array . isArray ( result ) ) return result ;
218+ if ( Array . isArray ( result ?. rows ) ) return result . rows ;
219+ return result == null ? [ ] : [ result ] ;
220+ }
221+
222+ /**
223+ * Does this pool's database already hold the federated table? A pool that
224+ * reloaded a previous boot's state would; a genuinely fresh `:memory:` one
225+ * cannot, because nothing in this composition is allowed to run DDL on an
226+ * `external` datasource.
227+ */
228+ async function hasExtNoteTable ( kernel : Awaited < ReturnType < typeof boot > > ) {
229+ const result = await externalDriver ( kernel ) . execute (
230+ "SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'ext_note'" ,
231+ ) ;
232+ return rowsOf ( result ) . length > 0 ;
233+ }
234+
162235 async function seedAndRead ( kernel : Awaited < ReturnType < typeof boot > > ) {
163236 const engine = kernel . getService < {
164237 getDriverByName ( n : string ) : any ;
165238 find ( object : string , query ?: any ) : Promise < any [ ] > ;
166239 } > ( 'data' ) ;
167240 const driver = engine . getDriverByName ( 'autoconn_ext' ) ;
241+ await ensureRemoteExtNoteTable ( driver ) ;
168242 await driver . bulkCreate ( 'ext_note' , [
169243 { id : 'n1' , title : 'first' } ,
170244 { id : 'n2' , title : 'second' } ,
171245 ] ) ;
172- const titles = ( await engine . find ( 'ext_note' ) ) . map ( ( r ) => r . title ) . sort ( ) ;
173- // Whatever the autosave timer would have written, written now.
174- await driver . flush ?.( ) ;
175- return titles ;
246+ return ( await engine . find ( 'ext_note' ) ) . map ( ( r ) => r . title ) . sort ( ) ;
176247 }
177248
178249 it ( 'writes no state file, and a second boot in the same process starts empty' , async ( ) => {
179250 const first = await boot ( ) ;
180251 try {
252+ expect ( await hasExtNoteTable ( first ) ) . toBe ( false ) ;
181253 expect ( await seedAndRead ( first ) ) . toEqual ( [ 'first' , 'second' ] ) ;
182254 // The seeded rows must not have reached the host filesystem at all.
183255 expect ( existsSync ( STATE_DIR ) ) . toBe ( false ) ;
@@ -187,8 +259,11 @@ describe('ADR-0062 D1 — the auto-connected in-memory pool leaves nothing behin
187259
188260 const second = await boot ( ) ;
189261 try {
190- // Was ['first','first','second','second'] — the first boot's rows, reloaded.
262+ // The whole point: not "no rows carried over" but "no database carried
263+ // over". Was ['first','first','second','second'] under the #4083 defect.
264+ expect ( await hasExtNoteTable ( second ) ) . toBe ( false ) ;
191265 expect ( await seedAndRead ( second ) ) . toEqual ( [ 'first' , 'second' ] ) ;
266+ expect ( existsSync ( STATE_DIR ) ) . toBe ( false ) ;
192267 } finally {
193268 try { await ( second as any ) ?. stop ?.( ) ; } catch { /* noop */ }
194269 }
@@ -206,10 +281,10 @@ describe('ADR-0062 credentials fail-closed (D3)', () => {
206281 datasources : [
207282 {
208283 name : 'needs_secret' ,
209- driver : 'memory ' ,
284+ driver : 'sqlite ' ,
210285 schemaMode : 'external' ,
211286 origin : 'code' ,
212- config : { } ,
287+ config : { filename : ':memory:' } ,
213288 external : {
214289 allowWrites : false ,
215290 credentialsRef : 'sys_secret:does-not-exist' ,
@@ -223,13 +298,12 @@ describe('ADR-0062 credentials fail-closed (D3)', () => {
223298
224299 it ( 'bricks boot with a clear message when a required credential cannot be resolved' , async ( ) => {
225300 const { ObjectQLPlugin } = await import ( '@objectstack/objectql' ) ;
226- const { InMemoryDriver } = await import ( '@objectstack/driver-memory' ) ;
227301 const { DatasourceAdminServicePlugin, createDefaultDatasourceDriverFactory } = await import (
228302 '@objectstack/service-datasource'
229303 ) ;
230304 const runtime = new Runtime ( { cluster : false } ) ;
231305 const kernel = runtime . getKernel ( ) ;
232- await kernel . use ( new DriverPlugin ( new InMemoryDriver ( ) ) ) ;
306+ await kernel . use ( new DriverPlugin ( await makeDefaultDriver ( ) ) ) ;
233307 await kernel . use ( new ObjectQLPlugin ( ) ) ;
234308 await kernel . use ( new AppPlugin ( credArtifact ( ) ) ) ;
235309 await kernel . use (
@@ -275,13 +349,12 @@ describe('ADR-0062 D5 — an explicitly-bound datasource that cannot connect bri
275349
276350 async function bootBound ( ) {
277351 const { ObjectQLPlugin } = await import ( '@objectstack/objectql' ) ;
278- const { InMemoryDriver } = await import ( '@objectstack/driver-memory' ) ;
279352 const { DatasourceAdminServicePlugin, createDefaultDatasourceDriverFactory } = await import (
280353 '@objectstack/service-datasource'
281354 ) ;
282355 const runtime = new Runtime ( { cluster : false } ) ;
283356 const kernel = runtime . getKernel ( ) ;
284- await kernel . use ( new DriverPlugin ( new InMemoryDriver ( ) ) ) ;
357+ await kernel . use ( new DriverPlugin ( await makeDefaultDriver ( ) ) ) ;
285358 await kernel . use ( new ObjectQLPlugin ( ) ) ;
286359 await kernel . use ( new AppPlugin ( boundArtifact ( ) ) ) ;
287360 await kernel . use (
0 commit comments