fix(host,client): roll back partial flow registrations; cover the widened double
- The browse client half mirrors -native's rollback: a pair construction that throws halfway (declared-but-occupied second hole) disposes the earlier deferral before rethrowing, so no orphaned slot subscription outlives the failed fiber (ds-review-bot). - The TestWorkspaces browse methods gain the coverage CI's per-file gate requires (defaults + stub overrides, recorded like their siblings).
This commit is contained in:
@@ -322,6 +322,26 @@ describe('workspaces', () => {
|
|||||||
expect(stub).toHaveBeenCalledOnce()
|
expect(stub).toHaveBeenCalledOnce()
|
||||||
await runtime.dispose()
|
await runtime.dispose()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('records the browse calls: listDirectory serves an empty home, createDirectory joins, stubs override', async () => {
|
||||||
|
const runtime = await runtimeWithFrame()
|
||||||
|
// Defaults: an empty home level and parent/name joining.
|
||||||
|
await expect(runtime.workspaces.listDirectory()).resolves.toMatchObject({ path: '/home/test', entries: [] })
|
||||||
|
await expect(runtime.workspaces.listDirectory('/home/test')).resolves.toMatchObject({ path: '/home/test' })
|
||||||
|
await expect(runtime.workspaces.createDirectory('/home/test', 'fresh')).resolves.toBe('/home/test/fresh')
|
||||||
|
expect(runtime.workspaces.calls).toEqual([
|
||||||
|
{ method: 'listDirectory', args: [undefined] },
|
||||||
|
{ method: 'listDirectory', args: ['/home/test'] },
|
||||||
|
{ method: 'createDirectory', args: ['/home/test', 'fresh'] },
|
||||||
|
])
|
||||||
|
// Stubs replace the defaults like every sibling method.
|
||||||
|
const listing = { path: '/x', home: '/x', crumbs: [], entries: [] }
|
||||||
|
runtime.workspaces.stub('listDirectory', vi.fn(() => Promise.resolve(listing as never)))
|
||||||
|
runtime.workspaces.stub('createDirectory', vi.fn(() => Promise.resolve('/x/made' as never)))
|
||||||
|
await expect(runtime.workspaces.listDirectory('/x')).resolves.toBe(listing)
|
||||||
|
await expect(runtime.workspaces.createDirectory('/x', 'made')).resolves.toBe('/x/made')
|
||||||
|
await runtime.dispose()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('feature mount and disposal', () => {
|
describe('feature mount and disposal', () => {
|
||||||
|
|||||||
@@ -98,12 +98,19 @@ export function apply(ctx: ClientContext): void {
|
|||||||
t: ctx.locale.bind(LOCALE_NS),
|
t: ctx.locale.bind(LOCALE_NS),
|
||||||
})
|
})
|
||||||
ctx.effect(() => {
|
ctx.effect(() => {
|
||||||
const deferred = [
|
// Constructing the pair can throw halfway (a declared hole already
|
||||||
deferRegistration(ctx.slots, 'conversation.hero.workspace.directoryFlow', BrowseDirectoryFlow, () =>
|
// occupied registers synchronously): roll the earlier deferral back so
|
||||||
ctx.slots.register({ name: 'conversation.hero.workspace.directoryFlow', inject: injected }, BrowseDirectoryFlow)),
|
// no live subscription outlives the failed fiber.
|
||||||
deferRegistration(ctx.slots, 'sidebar.workspaces.directoryFlow', BrowseDirectoryFlow, () =>
|
const deferred: ReturnType<typeof deferRegistration>[] = []
|
||||||
ctx.slots.register({ name: 'sidebar.workspaces.directoryFlow', inject: injected }, BrowseDirectoryFlow)),
|
try {
|
||||||
]
|
deferred.push(deferRegistration(ctx.slots, 'conversation.hero.workspace.directoryFlow', BrowseDirectoryFlow, () =>
|
||||||
|
ctx.slots.register({ name: 'conversation.hero.workspace.directoryFlow', inject: injected }, BrowseDirectoryFlow)))
|
||||||
|
deferred.push(deferRegistration(ctx.slots, 'sidebar.workspaces.directoryFlow', BrowseDirectoryFlow, () =>
|
||||||
|
ctx.slots.register({ name: 'sidebar.workspaces.directoryFlow', inject: injected }, BrowseDirectoryFlow)))
|
||||||
|
} catch (error) {
|
||||||
|
for (const entry of deferred) entry.dispose()
|
||||||
|
throw error
|
||||||
|
}
|
||||||
return () => { for (const entry of deferred) entry.dispose() }
|
return () => { for (const entry of deferred) entry.dispose() }
|
||||||
}, 'directory-picker-browse: flow registrations')
|
}, 'directory-picker-browse: flow registrations')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,6 +66,31 @@ describe('directory-picker-browse client half', () => {
|
|||||||
for (const hole of HOLES) expect(after.slots.entries(hole)).toHaveLength(1)
|
for (const hole of HOLES) expect(after.slots.entries(hole)).toHaveLength(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('rolls back the first deferral when the second hole is already occupied', async () => {
|
||||||
|
const b = await bench()
|
||||||
|
b.declare()
|
||||||
|
// Foreign occupant in the SECOND registered hole: the pair construction
|
||||||
|
// throws after the first deferral installed its subscription.
|
||||||
|
b.slots.register({ name: HOLES[1] } as never, () => null)
|
||||||
|
const rejections: unknown[] = []
|
||||||
|
const onUnhandled = (reason: unknown): void => { rejections.push(reason) }
|
||||||
|
process.on('unhandledRejection', onUnhandled)
|
||||||
|
try {
|
||||||
|
const fiber = b.ctx.plugin({ inject: [...inject], apply })
|
||||||
|
await expect(fiber.await()).rejects.toThrow(/already has a registration/)
|
||||||
|
// A leaked first deferral would now race this probe registration and
|
||||||
|
// throw from its orphaned subscription against the HERO hole; the
|
||||||
|
// rollback leaves only the activation failure itself (cordis re-raises
|
||||||
|
// the apply throw as a late rejection — installFailLoud's contract).
|
||||||
|
const disposeProbe = b.slots.register({ name: HOLES[0] } as never, () => null)
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 20))
|
||||||
|
expect(rejections.map(String).filter(text => text.includes(HOLES[0]))).toEqual([])
|
||||||
|
disposeProbe()
|
||||||
|
} finally {
|
||||||
|
process.off('unhandledRejection', onUnhandled)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
it('registers the dialog dictionaries and binds this package namespace', async () => {
|
it('registers the dialog dictionaries and binds this package namespace', async () => {
|
||||||
const b = await bench()
|
const b = await bench()
|
||||||
b.declare()
|
b.declare()
|
||||||
|
|||||||
Reference in New Issue
Block a user