fix(app-boot): survive a surface that exits before the tree settles
boot() asserts over ctx.loader after awaiting the Loader, but the TUI renders as soon as its own fiber starts: an /exit typed before the last entry settles runs disposeRootAndExit, which takes the Loader service with the tree. The assertions then read undefined and crashed the process with a TypeError over an app that exited exactly as asked. The keyless personal-overlay PTY smoke lost this race in roughly two of three runs.
This commit is contained in:
@@ -234,7 +234,8 @@ export function assertEntriesActive(ctx: Context, binName: string): void {
|
||||
* @param patches - optional overlay patches applied over the included tree
|
||||
* (see {@link loadPersonalPatches}); an empty list mounts none.
|
||||
* @param prepare - optional host setup run after Loader installation and before any config-tree entry mounts.
|
||||
* @returns the root context once every entry has started.
|
||||
* @returns the root context once every entry has started, or as soon as a
|
||||
* surface disposed the tree while startup was still in flight.
|
||||
*/
|
||||
export async function boot(
|
||||
binName: string,
|
||||
@@ -255,6 +256,13 @@ export async function boot(
|
||||
},
|
||||
})
|
||||
await ctx.loader.await()
|
||||
// A surface can finish and dispose the whole tree while that await is still
|
||||
// pending: the TUI renders as soon as its own fiber starts, so an `/exit`
|
||||
// typed before the last entry settles tears the context down under us. The
|
||||
// Loader service goes with it, and both assertions below describe a live
|
||||
// tree — reading `ctx.loader` here would throw a TypeError over an app that
|
||||
// exited exactly as asked.
|
||||
if (ctx.get('loader') === undefined) return ctx
|
||||
assertEntriesLoaded(ctx, binName)
|
||||
assertEntriesActive(ctx, binName)
|
||||
return ctx
|
||||
|
||||
@@ -207,6 +207,25 @@ describe('boot', () => {
|
||||
}
|
||||
})
|
||||
|
||||
it('returns instead of asserting over a tree a surface disposed mid-startup', async () => {
|
||||
// What a TUI `/exit` does (ui-tui's disposeRootAndExit): dispose the root
|
||||
// fiber, which lands while boot() is still awaiting the Loader whenever the
|
||||
// surface renders before the last entry settles. The Loader service goes
|
||||
// with the tree, so reading it for the post-boot assertions would crash an
|
||||
// app that exited exactly as the user asked.
|
||||
const dir = tmp()
|
||||
writeFileSync(join(dir, 'exiting.mjs'), [
|
||||
'export const name = "exiting"',
|
||||
'export function apply(ctx) {',
|
||||
' void ctx.root.fiber.dispose()',
|
||||
'}',
|
||||
'',
|
||||
].join('\n'))
|
||||
writeFileSync(join(dir, 'cordis.yml'), '- id: exiting\n name: ./exiting.mjs\n')
|
||||
const ctx = await boot(NAME, join(dir, 'cordis.yml'))
|
||||
expect(ctx.get('loader')).toBeUndefined()
|
||||
})
|
||||
|
||||
it('rejects (never exits 0 half-empty) when a config names a plugin that cannot be imported', async () => {
|
||||
const dir = tmp()
|
||||
writeFileSync(join(dir, 'cordis.yml'), '- id: ghost\n name: ./missing.mjs\n')
|
||||
|
||||
Reference in New Issue
Block a user