Gate derived test contexts on invariant readiness
This commit is contained in:
@@ -120,18 +120,20 @@ describe('global test invariant host', () => {
|
||||
const releaseDelayed = deferred()
|
||||
const order: string[] = []
|
||||
let delayedCompanion: TestInvariantCompanion | undefined
|
||||
const companionNestedApply = vi.fn(function companionNestedApply() {})
|
||||
|
||||
await withFakeCompanions(
|
||||
(path, index) => async () => {
|
||||
const companion: TestInvariantCompanion = {
|
||||
name: `test-invariant-${index}`,
|
||||
inject: ['invariants'],
|
||||
async apply() {
|
||||
async apply(companionCtx) {
|
||||
order.push(`companion-start:${path}`)
|
||||
if (index === 0) {
|
||||
delayedStarted.resolve()
|
||||
await releaseDelayed.promise
|
||||
}
|
||||
if (index === 1) await companionCtx.plugin(companionNestedApply)
|
||||
order.push(`companion-active:${path}`)
|
||||
return () => {}
|
||||
},
|
||||
@@ -173,6 +175,7 @@ describe('global test invariant host', () => {
|
||||
expect(targetFiber.state).toBe(FiberState.ACTIVE)
|
||||
expect(targetApply).toHaveBeenCalledOnce()
|
||||
expect(nestedApply).toHaveBeenCalledOnce()
|
||||
expect(companionNestedApply).toHaveBeenCalledOnce()
|
||||
const targetIndex = order.indexOf('target')
|
||||
expect(targetIndex).toBeGreaterThan(-1)
|
||||
expect(order.slice(0, targetIndex)).toHaveLength(Object.keys(testInvariantCompanions).length * 2)
|
||||
@@ -187,6 +190,52 @@ describe('global test invariant host', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('holds plugins registered on a root-derived context until companion readiness', async () => {
|
||||
const delayedStarted = deferred()
|
||||
const releaseDelayed = deferred()
|
||||
|
||||
await withFakeCompanions(
|
||||
(_path, index) => async () => ({
|
||||
name: `test-invariant-${index}`,
|
||||
inject: ['invariants'],
|
||||
async apply() {
|
||||
if (index === 0) {
|
||||
delayedStarted.resolve()
|
||||
await releaseDelayed.promise
|
||||
}
|
||||
return () => {}
|
||||
},
|
||||
}),
|
||||
async () => {
|
||||
const ctx = new Context()
|
||||
const rootApply = vi.fn(function rootApply() {})
|
||||
const derivedApply = vi.fn(function derivedApply() {})
|
||||
const derived = ctx.extend()
|
||||
.isolate('testInvariantDerived')
|
||||
.intercept('testInvariantDerived', {})
|
||||
|
||||
const rootFiber = ctx.plugin(rootApply)
|
||||
const derivedFiber = derived.plugin(derivedApply)
|
||||
|
||||
await delayedStarted.promise
|
||||
await Promise.resolve()
|
||||
await Promise.resolve()
|
||||
expect(rootApply).not.toHaveBeenCalled()
|
||||
expect(derivedApply).not.toHaveBeenCalled()
|
||||
expect(derivedFiber.inject).toEqual({
|
||||
[TEST_INVARIANT_READY_SERVICE]: null,
|
||||
})
|
||||
|
||||
releaseDelayed.resolve()
|
||||
await Promise.all([rootFiber, derivedFiber])
|
||||
expect(rootFiber.state).toBe(FiberState.ACTIVE)
|
||||
expect(derivedFiber.state).toBe(FiberState.ACTIVE)
|
||||
expect(rootApply).toHaveBeenCalledOnce()
|
||||
expect(derivedApply).toHaveBeenCalledOnce()
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
it.each(['load', 'startup'] as const)(
|
||||
'rejects a target when a lazy companion fails during %s without starting the target',
|
||||
async (phase) => {
|
||||
|
||||
@@ -46,6 +46,7 @@ const MANUAL_INVARIANT_TEST_EXCEPTIONS = [
|
||||
|
||||
interface InvariantHost {
|
||||
readonly byCallback: ReadonlyMap<unknown, PluginFiber>
|
||||
readonly barrierOwners: WeakSet<Context['fiber']>
|
||||
readonly ready: Promise<void>
|
||||
}
|
||||
|
||||
@@ -65,13 +66,15 @@ RegistryService.prototype.plugin = function(plugin: Plugin, config?: unknown, ge
|
||||
const callback = this.resolve(plugin)
|
||||
const existing = callback === undefined ? undefined : host.byCallback.get(callback)
|
||||
if (existing !== undefined) {
|
||||
return this.ctx === root ? joinInvariantStartup(existing, host.ready) : existing
|
||||
return hasBarrierOwner(host, this.ctx) ? existing : joinInvariantStartup(existing, host.ready)
|
||||
}
|
||||
|
||||
// Nested plugins run inside a target that already crossed the root barrier.
|
||||
// Adding the same root-owned dependency there would make child lifecycle
|
||||
// depend on an unrelated isolation scope and can deadlock companion startup.
|
||||
if (this.ctx !== root) return originalPlugin.call(this, plugin, config, getOuterStack)
|
||||
// Causal descendants of a gated target have already crossed the barrier.
|
||||
// Host service and companion descendants also bypass it so their own startup
|
||||
// cannot depend on the readiness they are responsible for providing.
|
||||
if (hasBarrierOwner(host, this.ctx)) {
|
||||
return originalPlugin.call(this, plugin, config, getOuterStack)
|
||||
}
|
||||
if (callback === undefined) return originalPlugin.call(this, plugin, config, getOuterStack)
|
||||
|
||||
const fiber = originalPlugin.call(
|
||||
@@ -80,6 +83,7 @@ RegistryService.prototype.plugin = function(plugin: Plugin, config?: unknown, ge
|
||||
config,
|
||||
getOuterStack,
|
||||
)
|
||||
host.barrierOwners.add(fiber.ctx.fiber)
|
||||
return joinInvariantStartup(fiber, host.ready)
|
||||
}
|
||||
|
||||
@@ -120,11 +124,13 @@ export function testInvariantCompanionPaths(testPath: string): string[] {
|
||||
|
||||
function startInvariantHost(root: Context): InvariantHost {
|
||||
const byCallback = new Map<unknown, PluginFiber>()
|
||||
const barrierOwners = new WeakSet<Context['fiber']>()
|
||||
const mount = (plugin: Plugin, config?: unknown): PluginFiber => {
|
||||
const fiber = originalPlugin.call(root.registry, plugin, config)
|
||||
const callback = root.registry.resolve(plugin)
|
||||
if (callback === undefined) throw new Error('test invariants: companion is not a valid Cordis plugin')
|
||||
byCallback.set(callback, fiber)
|
||||
barrierOwners.add(fiber.ctx.fiber)
|
||||
return fiber
|
||||
}
|
||||
|
||||
@@ -157,11 +163,21 @@ function startInvariantHost(root: Context): InvariantHost {
|
||||
await Promise.all(companionFibers.map(({ fiber, path }) => requireActive(fiber, path)))
|
||||
root.provide(TEST_INVARIANT_READY_SERVICE, true)
|
||||
})
|
||||
const host = { byCallback, ready }
|
||||
const host = { byCallback, barrierOwners, ready }
|
||||
hosts.set(root, host)
|
||||
return host
|
||||
}
|
||||
|
||||
function hasBarrierOwner(host: InvariantHost, ctx: Context): boolean {
|
||||
let fiber = ctx.fiber
|
||||
while (true) {
|
||||
if (host.barrierOwners.has(fiber)) return true
|
||||
const parent = fiber.parent.fiber
|
||||
if (parent === fiber) return false
|
||||
fiber = parent
|
||||
}
|
||||
}
|
||||
|
||||
async function requireActive(fiber: PluginFiber, label: string): Promise<void> {
|
||||
await fiber.await()
|
||||
if (fiber.state !== FiberState.ACTIVE) {
|
||||
|
||||
Reference in New Issue
Block a user