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
|
* @param patches - optional overlay patches applied over the included tree
|
||||||
* (see {@link loadPersonalPatches}); an empty list mounts none.
|
* (see {@link loadPersonalPatches}); an empty list mounts none.
|
||||||
* @param prepare - optional host setup run after Loader installation and before any config-tree entry mounts.
|
* @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(
|
export async function boot(
|
||||||
binName: string,
|
binName: string,
|
||||||
@@ -255,6 +256,13 @@ export async function boot(
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
await ctx.loader.await()
|
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)
|
assertEntriesLoaded(ctx, binName)
|
||||||
assertEntriesActive(ctx, binName)
|
assertEntriesActive(ctx, binName)
|
||||||
return ctx
|
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 () => {
|
it('rejects (never exits 0 half-empty) when a config names a plugin that cannot be imported', async () => {
|
||||||
const dir = tmp()
|
const dir = tmp()
|
||||||
writeFileSync(join(dir, 'cordis.yml'), '- id: ghost\n name: ./missing.mjs\n')
|
writeFileSync(join(dir, 'cordis.yml'), '- id: ghost\n name: ./missing.mjs\n')
|
||||||
|
|||||||
Reference in New Issue
Block a user