11// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
22//
3- // [#21922 / #21944] The host's code-datasource set: one set, filled from code
4- // in Phase 1, complete before ANY `start()` runs — which is when the
3+ // [#21922 / #21944] The host's code-datasource set: one set, contributed to
4+ // from code in Phase 1, complete before ANY `start()` runs — which is when the
55// datasource-admin plugin's boot restore reads it.
66//
77// The boot case composes its reader FIRST, ahead of every producer. Start
88// order follows insertion where no plugin declares an edge, and none of
99// AppPlugin, DefaultDatasourcePlugin and DatasourceAdminServicePlugin declares
10- // one to another — so a set filled in `start()` would be empty here. Filled in
11- // `init()`, it is whole.
10+ // one to another — so a set contributed to in `start()` would be short here.
11+ // Contributed to in `init()`, it is whole. (AppPlugin's contribution is a
12+ // function the set resolves at that first read — `CodeDatasourceNames` says
13+ // why; `plugin-dev`'s #15292 falsifier pins that `init()` itself never reads
14+ // the artifact's `packages[]` for it.)
1215
1316import { describe , it , expect } from 'vitest' ;
1417import type { Plugin , PluginContext } from '@objectstack/core' ;
1518import { Runtime } from './runtime.js' ;
1619import { DefaultDatasourcePlugin } from './default-datasource-plugin.js' ;
1720import { AppPlugin } from './app-plugin.js' ;
18- import { CODE_DATASOURCE_NAMES_SERVICE , contributeCodeDatasourceNames } from './code-datasource-names.js' ;
21+ import { CODE_DATASOURCE_NAMES_SERVICE , CodeDatasourceNames , contributeCodeDatasourceNames } from './code-datasource-names.js' ;
1922
2023// [#10126] Pay the first transform of these dist-resolved workspace deps at MODULE
2124// LOAD, not inside a clocked `it()` body (`scripts/check-test-source-alias.mjs`).
@@ -53,13 +56,38 @@ describe('contributeCodeDatasourceNames — one set per kernel', () => {
5356 contributeCodeDatasourceNames ( ctx , [ 'crm_wh' , 'erp_wh' ] ) ;
5457
5558 expect ( registrations ( ) ) . toBe ( 1 ) ;
56- const set = services . get ( CODE_DATASOURCE_NAMES_SERVICE ) as Set < string > ;
57- expect ( [ ...set ] . sort ( ) ) . toEqual ( [ 'crm_wh' , 'default' , 'erp_wh' ] ) ;
59+ const set = services . get ( CODE_DATASOURCE_NAMES_SERVICE ) as CodeDatasourceNames ;
60+ expect ( set . list ( ) ) . toEqual ( [ 'crm_wh' , 'default' , 'erp_wh' ] ) ;
61+ expect ( set . has ( 'crm_wh' ) ) . toBe ( true ) ;
62+ expect ( set . has ( 'runtime_wh' ) ) . toBe ( false ) ;
5863 } ) ;
5964
60- it ( 'a name the kernel holds as something other than a set is a composition fault, refused loudly ' , ( ) => {
65+ it ( 'a contribution made as a function is resolved at the first read, not when it is made ' , ( ) => {
6166 const { ctx, services } = registryCtx ( ) ;
62- services . set ( CODE_DATASOURCE_NAMES_SERVICE , { has : ( ) => false } ) ;
67+ let calls = 0 ;
68+
69+ contributeCodeDatasourceNames ( ctx , ( ) => { calls += 1 ; return [ 'app_wh' ] ; } ) ;
70+ expect ( calls ) . toBe ( 0 ) ;
71+
72+ const set = services . get ( CODE_DATASOURCE_NAMES_SERVICE ) as CodeDatasourceNames ;
73+ expect ( set . has ( 'app_wh' ) ) . toBe ( true ) ;
74+ expect ( set . has ( 'app_wh' ) ) . toBe ( true ) ;
75+ expect ( calls ) . toBe ( 1 ) ;
76+ } ) ;
77+
78+ it ( 'a contribution that throws stays pending and reaches every reader — the set never answers without it' , ( ) => {
79+ const { ctx, services } = registryCtx ( ) ;
80+ contributeCodeDatasourceNames ( ctx , [ 'default' ] ) ;
81+ contributeCodeDatasourceNames ( ctx , ( ) => { throw new Error ( 'the artifact cannot be read' ) ; } ) ;
82+
83+ const set = services . get ( CODE_DATASOURCE_NAMES_SERVICE ) as CodeDatasourceNames ;
84+ expect ( ( ) => set . has ( 'default' ) ) . toThrow ( / t h e a r t i f a c t c a n n o t b e r e a d / ) ;
85+ expect ( ( ) => set . has ( 'default' ) ) . toThrow ( / t h e a r t i f a c t c a n n o t b e r e a d / ) ;
86+ } ) ;
87+
88+ it ( 'a name the kernel holds as something other than this set is a composition fault, refused loudly' , ( ) => {
89+ const { ctx, services } = registryCtx ( ) ;
90+ services . set ( CODE_DATASOURCE_NAMES_SERVICE , new Set ( [ 'default' ] ) ) ;
6391
6492 expect ( ( ) => contributeCodeDatasourceNames ( ctx , [ 'default' ] ) ) . toThrow ( / a l r e a d y r e g i s t e r e d / ) ;
6593 } ) ;
@@ -77,8 +105,7 @@ describe('the host\'s code-datasource set on a booted kernel', () => {
77105 version : '1.0.0' ,
78106 init : async ( ) => { } ,
79107 start : async ( ctx : PluginContext ) => {
80- const set = ctx . getService < Set < string > > ( CODE_DATASOURCE_NAMES_SERVICE ) ;
81- seenAtFirstStart = [ ...set ] . sort ( ) ;
108+ seenAtFirstStart = ctx . getService < CodeDatasourceNames > ( CODE_DATASOURCE_NAMES_SERVICE ) . list ( ) ;
82109 } ,
83110 } ;
84111
@@ -94,7 +121,7 @@ describe('the host\'s code-datasource set on a booted kernel', () => {
94121 await kernel . bootstrap ( ) ;
95122 expect ( seenAtFirstStart ) . toEqual ( [ 'app_wh' , 'default' ] ) ;
96123 // One set: the kernel service the two readers resolve by name.
97- expect ( [ ... kernel . getService < Set < string > > ( CODE_DATASOURCE_NAMES_SERVICE ) ] . sort ( ) ) . toEqual ( [ 'app_wh' , 'default' ] ) ;
124+ expect ( kernel . getService < CodeDatasourceNames > ( CODE_DATASOURCE_NAMES_SERVICE ) . list ( ) ) . toEqual ( [ 'app_wh' , 'default' ] ) ;
98125 } finally {
99126 await kernel . shutdown ( ) ;
100127 }
@@ -109,7 +136,7 @@ describe('the host\'s code-datasource set on a booted kernel', () => {
109136 await kernel . use ( new AppPlugin ( { manifest : { id : 'com.test.no-ds' , name : 'No DS' , version : '1.0.0' } } ) ) ;
110137 try {
111138 await kernel . bootstrap ( ) ;
112- expect ( [ ... kernel . getService < Set < string > > ( CODE_DATASOURCE_NAMES_SERVICE ) ] ) . toEqual ( [ 'default' ] ) ;
139+ expect ( kernel . getService < CodeDatasourceNames > ( CODE_DATASOURCE_NAMES_SERVICE ) . list ( ) ) . toEqual ( [ 'default' ] ) ;
113140 } finally {
114141 await kernel . shutdown ( ) ;
115142 }
0 commit comments