4343 * takes). The served label alone cannot tell these apart under the source
4444 * entry: it runs `NODE_ENV=development`, where the dev metadata door over
4545 * the supervisor's answer serves ALPHA even with the config loaded beside it.
46+ * - the honest ready banner on the bare `os start` path (no flag, the config's
47+ * own dist/ resolved, dist/ holding a different stack): a host config boots
48+ * its own module and the `serve` child's ready banner says `Config:`; a
49+ * non-host config serves its dist/ bundle and the banner says `Artifact:`
50+ * that file. No ready-banner row names a file the boot did not load.
51+ * - `os dev` under a local `OS_ARTIFACT_PATH` at a named path compiles a host
52+ * config INTO that path; the file there is the config's own compiled output,
53+ * so the config still composes its plugins (read through the roster).
4654 * - flag over env — `dev -a` under an `OS_ARTIFACT_URL` naming the other one.
4755 * - the documented first-project path — `start --artifact
48- * ./dist/objectstack.json` beside its config — is the one boot a config
49- * still joins (that file is the config's OWN compiled output), and it serves
56+ * ./dist/objectstack.json` beside its config — is a boot the config still
57+ * joins (that file is the config's OWN compiled output), and it serves
5058 * that file even under an exported `OS_ARTIFACT_PATH` naming another: the
5159 * config boot is handed the supervisor's answer instead of re-deriving it.
5260 *
53- * Each case also checks the supervisor's `Artifact:` row named the file that
54- * was served: the card's rule is never to print one artifact and serve another.
61+ * The named-artifact cases also check the supervisor's `Artifact:` row named the
62+ * file that was served: the card's rule is never to print one artifact and
63+ * serve another.
5564 *
5665 * ## Spawn shape
5766 *
@@ -83,8 +92,8 @@ import { defineStackSourceFromLiteral, linkSpec, writeDefineStackConfig } from '
8392/** The banner's tail — every row above it has printed. */
8493const READY = / P r e s s C t r l \+ C t o s t o p / ;
8594const BOOT_TIMEOUT_MS = 180_000 ;
86- /** Eight boots, one after another, each well under its own budget when healthy. */
87- const ALL_BOOTS_TIMEOUT_MS = 8 * ( BOOT_TIMEOUT_MS + 30_000 ) ;
95+ /** Ten boots, one after another, each well under its own budget when healthy. */
96+ const ALL_BOOTS_TIMEOUT_MS = 10 * ( BOOT_TIMEOUT_MS + 30_000 ) ;
8897/** The host config's plugin — on the boot's plugin roster iff the config was loaded. */
8998const HOST_MARKER = 'com.example.fx.host-marker' ;
9099
@@ -218,6 +227,19 @@ function artifactRow(output: string): string | undefined {
218227 return / ^ .* \b A r t i f a c t : [ ^ \n ] * $ / m. exec ( output ) ?. [ 0 ] ;
219228}
220229
230+ /**
231+ * The `serve` child's READY banner row — `Config:` or `Artifact:` — read only
232+ * after `Server is ready`, so neither the supervisor's pre-boot rows nor the
233+ * child's boot diagnostics can answer for it. `undefined` when the banner
234+ * carries neither (an artifact-fallback boot omits the row by design).
235+ */
236+ function readyRow ( output : string ) : { label : 'Config' | 'Artifact' ; value : string } | undefined {
237+ const at = output . search ( / S e r v e r i s r e a d y / ) ;
238+ if ( at < 0 ) return undefined ;
239+ const m = / ^ [ \t ] + ( C o n f i g | A r t i f a c t ) : [ \t ] + ( [ ^ \n ] * ) $ / m. exec ( output . slice ( at ) ) ;
240+ return m ? { label : m [ 1 ] as 'Config' | 'Artifact' , value : m [ 2 ] . trim ( ) } : undefined ;
241+ }
242+
221243interface Reading {
222244 served ?: string ;
223245 status ?: number ;
@@ -314,10 +336,10 @@ beforeAll(async () => {
314336 readings . leg2NoDist = await measure ( [ ...startArgs ( join ( root , 'h-leg2-nodist' ) ) , '--artifact' , alpha ] , configOnly ) ;
315337 readings . leg2Control = await measure ( [ ...startArgs ( join ( root , 'h-leg2-ctl' ) ) , '--artifact' , alpha ] , noConfig ) ;
316338 readings . hostConfig = await measure ( [ ...startArgs ( join ( root , 'h-host' ) ) , '--artifact' , alpha ] , hostConfig ) ;
317- readings . hostConfigOwn = await measure (
318- [ ... startArgs ( join ( root , 'h-host-own' ) ) , '--artifact' , './dist/objectstack.json' ] ,
319- hostConfig ,
320- ) ;
339+ // The bare `os start` path beside a config: no flag, the config's own dist/
340+ // resolved. dist/ holds BRAVO, a DIFFERENT stack from either config.
341+ readings . hostConfigOwn = await measure ( [ ... startArgs ( join ( root , 'h-host-own' ) ) ] , hostConfig ) ;
342+ readings . bareNonHost = await measure ( [ ... startArgs ( join ( root , 'h-bare-nonhost' ) ) ] , withConfig ) ;
321343 readings . flagOverEnv = await measure ( [ ...devArgs , '-a' , alpha ] , bare , {
322344 OS_ARTIFACT_URL : pathToFileURL ( bravo ) . href ,
323345 } ) ;
@@ -326,6 +348,10 @@ beforeAll(async () => {
326348 withConfig ,
327349 { OS_ARTIFACT_PATH : alpha } ,
328350 ) ;
351+ // `os dev` under a local OS_ARTIFACT_PATH at a NON-default path compiles the
352+ // host config INTO it (it does not exist yet), so that file is the config's
353+ // own compiled output and the config must still compose its plugins.
354+ readings . devNamedPathHost = await measure ( [ ...devArgs ] , hostConfig , { OS_ARTIFACT_PATH : 'build/named.json' } ) ;
329355} , ALL_BOOTS_TIMEOUT_MS ) ;
330356
331357afterAll ( async ( ) => {
@@ -374,10 +400,30 @@ describe('#21501 — the named artifact is the served stack, beside a config or
374400 expect ( r . output ) . not . toContain ( HOST_MARKER ) ;
375401 } ) ;
376402
377- it ( 'a HOST config still boots ITSELF when the artifact is its own compiled output (the marker\'s positive control)' , ( ) => {
403+ it ( 'a bare `os start` beside a HOST config with a differing dist/ boots the config itself — and its ready banner says `Config:`' , ( ) => {
404+ // Also the roster marker's positive control: the config's own compiled
405+ // output is the one artifact a config still joins.
378406 const r = reading ( 'hostConfigOwn' ) ;
379407 expect ( r . status ) . toBe ( 200 ) ;
380408 expect ( r . output ) . toContain ( HOST_MARKER ) ;
409+ // The served label is not asserted here: under the source entry
410+ // (`NODE_ENV=development`) the dev metadata door over dist/ composes beside
411+ // the host module — see the header.
412+ expect ( readyRow ( r . output ?? '' ) ) . toEqual ( { label : 'Config' , value : 'objectstack.config.ts' } ) ;
413+ } ) ;
414+
415+ it ( 'a bare `os start` beside a NON-host config serves its dist/ bundle — and its ready banner says `Artifact:` that file' , ( ) => {
416+ const r = reading ( 'bareNonHost' ) ;
417+ expect ( r . status ) . toBe ( 200 ) ;
418+ expect ( r . served ) . toBe ( 'Widget BRAVO' ) ;
419+ expect ( readyRow ( r . output ?? '' ) ) . toEqual ( { label : 'Artifact' , value : 'dist/objectstack.json' } ) ;
420+ } ) ;
421+
422+ it ( '`os dev` under OS_ARTIFACT_PATH at a named path compiles a HOST config there, and the config still composes its plugins' , ( ) => {
423+ const r = reading ( 'devNamedPathHost' ) ;
424+ expect ( r . status ) . toBe ( 200 ) ;
425+ expect ( r . artifactRow ) . toContain ( 'build/named.json' ) ;
426+ expect ( r . output ) . toContain ( HOST_MARKER ) ;
381427 } ) ;
382428
383429 it ( 'flag over env: `os dev -a ALPHA` under OS_ARTIFACT_URL naming BRAVO serves ALPHA' , ( ) => {
0 commit comments