Gate derived test contexts on invariant readiness

This commit is contained in:
Hypatia May
2026-07-30 23:54:21 +08:00
parent cc91724157
commit 573a062afd
2 changed files with 72 additions and 7 deletions
+50 -1
View File
@@ -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) => {
+22 -6
View File
@@ -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) {