fix(web-app): defer to a user-configured client-hmr row

The whole-tree name scan (entries() recurses into subtrees) already
skips creation when any patch layer carries the row, including a
disabled one; make that contract explicit in the comment, pin it with a
test, and record it in the Agent Note.
This commit is contained in:
Turtle
2026-08-11 14:41:27 +08:00
parent c598989d08
commit fb301ace65
5 changed files with 31 additions and 6 deletions

View File

@@ -160,8 +160,11 @@ export function apply(ctx: Context, config: Config): void {
} else {
void loader.await().then(async () => {
// The tree can be disposed while settlement was in flight (early
// SIGTERM); re-check before mutating it. A reload of this fiber must
// not duplicate the row a previous generation created.
// SIGTERM); re-check before mutating it. The name scan spans every
// tree (entries() recurses into subtrees), so a row the user
// configured in a patch layer — enabled, reconfigured, or
// deliberately disabled — wins over this default, and a reload of
// this fiber never duplicates the row a previous generation created.
if (ctx.get('loader') === undefined) return
const mounted = [...ctx.loader.entries()].some(entry => entry.options.name === HMR_ROW_NAME)
if (!mounted) await ctx.loader.create({ name: HMR_ROW_NAME })

View File

@@ -187,6 +187,28 @@ describe('web-app runtime glue', () => {
await ctx.fiber.dispose()
})
it('defers to a user-configured client-hmr row anywhere in the tree', async () => {
stageDist()
const ctx = new Context()
ctx.provide('httpServer', fakeHttpServer().server)
const created: string[] = []
// The user's own row — possibly patched into an include subtree and even
// disabled there — already carries the name; the runtime must not create
// a second one beside it.
ctx.provide('loader', {
entries: () => [{ options: { id: 'my-hmr', name: '@deepseek-ai/dsh-client-hmr', disabled: true } }][Symbol.iterator](),
create: (options: { name: string }) => {
created.push(options.name)
return Promise.resolve(options.name)
},
await: () => Promise.resolve(),
} as never)
apply(ctx, new Config({ mode: 'development', printUrl: false, surfaceContext: false, trustedHosts: [] }))
await new Promise(resolve => setTimeout(resolve, 0))
expect(created).toEqual([])
await ctx.fiber.dispose()
})
it('skips the dev row when the tree is disposed during settlement and logs a creation failure', async () => {
stageDist()
const raced = new Context()