Skip to content

Commit ebb6474

Browse files
bodia-uzclaude
andcommitted
testKit: configurable waitForLoading timeout + report all root unready APIs on timeout
getRootUnreadyAPI walked a single dependency chain from an arbitrary first unready entry point - sibling branches were dropped and only one API was reported even when several independent APIs are missing. Replace the walk with getRootUnreadyAPIs (plural) on repluggableAppDebug.utils: dependencies of unready entry points that are neither ready nor declared by another unready entry point. The singular util becomes its first element. The testKit timeout message now lists all of them, and the hardcoded 3s timeout becomes an optional parameter (default unchanged) so heavy hosts can raise it above their boot time and let this error surface before the test runner's own opaque timeout. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 27738f7 commit ebb6474

5 files changed

Lines changed: 108 additions & 67 deletions

File tree

packages/repluggable-core/src/repluggableAppDebug/debug.d.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ export interface DependencyTree {
2626
export interface RepluggableDebugUtils {
2727
apis(): APIDebugInfo[]
2828
unReadyEntryPoints(): EntryPoint[]
29+
getRootUnreadyAPIs(): AnySlotKey[]
30+
/**
31+
* @deprecated Use `getRootUnreadyAPIs` instead
32+
*/
2933
getRootUnreadyAPI(): SlotKey<any>
3034
whyEntryPointUnready(name: string): void
3135
findAPI(name: string): APIDebugInfo[]

packages/repluggable-core/src/repluggableAppDebug/repluggableAppDebug.ts

Lines changed: 21 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -36,57 +36,6 @@ function mapApiToEntryPoint(allPackages: EntryPoint[]) {
3636
return apiToEntryPoint
3737
}
3838

39-
/**
40-
* a function that returns all the entry points in the system with their declared APIs and dependencies
41-
*/
42-
const getAllEntryPoints = () => {
43-
return [
44-
...globalThis.repluggableAppDebug.utils.unReadyEntryPoints(),
45-
...[...globalThis.repluggableAppDebug.addedShells].map(([_, shell]) => shell.entryPoint)
46-
]
47-
}
48-
49-
/**
50-
* this function is used to get the root unready API in case there are too many to understand.
51-
* for example if you have 200 unready entry points, running this function will give you the first unready API that
52-
* will unblock the rest of the entry point (note that there might be more than one)
53-
*
54-
* this function basically takes the first unready entry point, get its dependencies and iterates over them to find an API that is not ready
55-
* at this point it follows the same process recursively until it reaced the target API.
56-
*/
57-
const getRootUnreadyAPI = (host: AppHost) => {
58-
return () => {
59-
// get all unready entry points
60-
const allEntryPoints = getAllEntryPoints()
61-
const unReadyAPIsArray = []
62-
// get the depdenencies of the first unready entry point
63-
let dependenciesOfUnreadyEntryPoint = allEntryPoints?.[0]?.getDependencyAPIs?.()
64-
65-
while (dependenciesOfUnreadyEntryPoint?.length) {
66-
const currentAPI = dependenciesOfUnreadyEntryPoint.pop()
67-
68-
if (!currentAPI) {
69-
continue
70-
}
71-
// try to get the API from this host, we are looking for an API that is not ready
72-
try {
73-
const api = host.getAPI(currentAPI as SlotKey<any>)
74-
if (api) {
75-
continue
76-
}
77-
} catch (e) {
78-
unReadyAPIsArray.push(currentAPI)
79-
// we found an API that is unready, lets find which entry point declares it
80-
const declarer = allEntryPoints.find(entryPointData =>
81-
entryPointData.declareAPIs?.().some(api => currentAPI?.name === api.name)
82-
)
83-
dependenciesOfUnreadyEntryPoint = declarer?.getDependencyAPIs?.()
84-
}
85-
}
86-
return unReadyAPIsArray.reverse()[0]
87-
}
88-
}
89-
9039
export type DependencyTree = {
9140
entryPoint: string
9241
deps: Array<{ api: string; subtree: DependencyTree | null }>
@@ -198,6 +147,17 @@ export function setupDebugInfo({
198147
shellInstallers,
199148
performance: { options, trace, memoizedArr }
200149
}: SetupDebugInfoParams) {
150+
const getRootUnreadyAPIs = (): AnySlotKey[] => {
151+
const unreadyEntryPoints = getUnreadyEntryPoints()
152+
const unreadyEntryPointsDeclares = new Set(unreadyEntryPoints.flatMap(ep => (ep.declareAPIs?.() || []).map(key => key.name)))
153+
154+
const rootUnreadyAPIs = unreadyEntryPoints
155+
.flatMap(ep => ep.getDependencyAPIs?.() || [])
156+
.filter(key => !readyAPIs.has(getOwnSlotKey(key)) && !unreadyEntryPointsDeclares.has(key.name))
157+
158+
return _.uniqBy(rootUnreadyAPIs, 'name')
159+
}
160+
201161
const utils = {
202162
apis: () => {
203163
return Array.from(readyAPIs).map((apiKey: AnySlotKey) => {
@@ -207,7 +167,16 @@ export function setupDebugInfo({
207167
}
208168
})
209169
},
210-
getRootUnreadyAPI: getRootUnreadyAPI(host),
170+
/**
171+
* dependencies of unready entry points that are neither ready nor declared by another
172+
* unready entry point - the APIs actually blocking the host from loading
173+
* (missing entry point or a contribution that never completed)
174+
*/
175+
getRootUnreadyAPIs,
176+
/**
177+
* @deprecated Use `getRootUnreadyAPIs` instead
178+
*/
179+
getRootUnreadyAPI: () => getRootUnreadyAPIs()[0],
211180
unReadyEntryPoints: (): EntryPoint[] => getUnreadyEntryPoints(),
212181
whyEntryPointUnready: (name: string) => {
213182
const unreadyEntryPoint = _.find(

packages/repluggable-core/test/repluggableAppDebug.spec.ts

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,17 @@ const unreadAPI = { name: 'unreadyAPI' }
44

55
const getUnreadyAPIs = async () => {
66
await new Promise(resolve => setTimeout(resolve, 0))
7-
return globalThis.repluggableAppDebug.utils.getRootUnreadyAPI()
7+
return globalThis.repluggableAppDebug.utils.getRootUnreadyAPIs()
88
}
99

1010
describe('RepluggableAppDebug', () => {
11-
describe('getRootUnreadyAPI', () => {
11+
describe('getRootUnreadyAPIs', () => {
1212
it('should not report any issues in case there are no entry points', async () => {
1313
createAppHost([])
1414

1515
const unreadyAPIs = await getUnreadyAPIs()
1616

17-
expect(unreadyAPIs).toBeUndefined()
17+
expect(unreadyAPIs).toEqual([])
1818
})
1919

2020
it('should not report any issues in case all entry points are loaded', async () => {
@@ -27,7 +27,7 @@ describe('RepluggableAppDebug', () => {
2727

2828
const unreadyAPIs = await getUnreadyAPIs()
2929

30-
expect(unreadyAPIs).toBeUndefined()
30+
expect(unreadyAPIs).toEqual([])
3131
})
3232

3333
it('should return an API if its not ready', async () => {
@@ -40,7 +40,7 @@ describe('RepluggableAppDebug', () => {
4040

4141
const unreadyAPIs = await getUnreadyAPIs()
4242

43-
expect(unreadyAPIs).toEqual({ name: 'unreadyAPI' })
43+
expect(unreadyAPIs).toEqual([unreadAPI])
4444
})
4545

4646
it('should return the root unready API when there are multiple entry points that depend on the same API', async () => {
@@ -61,7 +61,7 @@ describe('RepluggableAppDebug', () => {
6161

6262
const unreadyAPIs = await getUnreadyAPIs()
6363

64-
expect(unreadyAPIs).toEqual({ name: 'unreadyAPI' })
64+
expect(unreadyAPIs).toEqual([unreadAPI])
6565
})
6666

6767
it('should return the root unready API when there is a graph of dependencies', async () => {
@@ -85,7 +85,7 @@ describe('RepluggableAppDebug', () => {
8585

8686
const unreadyAPIs = await getUnreadyAPIs()
8787

88-
expect(unreadyAPIs).toEqual({ name: 'unreadyAPI' })
88+
expect(unreadyAPIs).toEqual([unreadAPI])
8989
})
9090

9191
it('should return all traversal paths from an entry point to a transitive API', () => {
@@ -240,9 +240,29 @@ describe('RepluggableAppDebug', () => {
240240
)
241241

242242
const unreadyAPIs = await getUnreadyAPIs()
243-
// because we take the first unready entry point, and in this case its the root,
244-
// we don't get the rest of the unready APIs
245-
expect(unreadyAPIs).toEqual({ name: 'unreadyAPI' })
243+
244+
expect(unreadyAPIs).toEqual([unreadAPI])
245+
})
246+
247+
it('should return all root unready APIs, excluding transitively blocked ones', async () => {
248+
createAppHost([
249+
{
250+
name: 'entryPoint A',
251+
getDependencyAPIs: () => [{ name: 'API B' }]
252+
},
253+
{
254+
name: 'entryPoint B',
255+
declareAPIs: () => [{ name: 'API B' }],
256+
getDependencyAPIs: () => [{ name: 'missing B' }]
257+
},
258+
{
259+
name: 'entryPoint C',
260+
getDependencyAPIs: () => [{ name: 'missing C' }]
261+
}
262+
])
263+
await new Promise(resolve => setTimeout(resolve, 0))
264+
265+
expect(globalThis.repluggableAppDebug.utils.getRootUnreadyAPIs()).toEqual([{ name: 'missing B' }, { name: 'missing C' }])
246266
})
247267
})
248268
})

packages/repluggable-core/test/testKit.spec.tsx

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,5 +149,47 @@ describe('App Host TestKit', () => {
149149
jest.runAllTimers()
150150
await expect(hostPromise).rejects.toThrow(new RegExp(MockPublicAPI.name))
151151
})
152+
153+
it('should report all root unready APIs of independent unready entry points', async () => {
154+
const hostPromise = createAppHostAndWaitForLoading(
155+
[
156+
{
157+
name: 'entryPoint A',
158+
declareAPIs: () => [{ name: 'API A' }],
159+
getDependencyAPIs: () => [{ name: 'missing A' }]
160+
},
161+
{
162+
name: 'entryPoint B',
163+
declareAPIs: () => [{ name: 'API B' }],
164+
getDependencyAPIs: () => [{ name: 'missing B' }]
165+
}
166+
],
167+
[]
168+
)
169+
jest.runAllTimers()
170+
const error: Error = await hostPromise.then(
171+
() => {
172+
throw new Error('expected hostPromise to reject')
173+
},
174+
e => e
175+
)
176+
177+
expect(error.message).toContain('"missing A"')
178+
expect(error.message).toContain('"missing B"')
179+
})
180+
181+
it('should respect a custom loading timeout', async () => {
182+
let settled = false
183+
const hostPromise = createAppHostAndWaitForLoading([dependsOnMockPackageEntryPoint], [], 10000)
184+
hostPromise.catch(() => (settled = true))
185+
186+
jest.advanceTimersByTime(9999)
187+
await Promise.resolve()
188+
await Promise.resolve()
189+
expect(settled).toBe(false)
190+
191+
jest.advanceTimersByTime(1)
192+
await expect(hostPromise).rejects.toThrow('timed out after 10000ms')
193+
})
152194
})
153195
})

packages/repluggable-core/testKit/index.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,11 @@ export function createAppHostWithPacts(packages: EntryPointOrPackage[], pacts: P
8181
})
8282
}
8383

84-
export async function createAppHostAndWaitForLoading(packages: EntryPointOrPackage[], pacts: PactAPIBase[]): Promise<AppHost> {
84+
export async function createAppHostAndWaitForLoading(
85+
packages: EntryPointOrPackage[],
86+
pacts: PactAPIBase[],
87+
timeout: number = 3000
88+
): Promise<AppHost> {
8589
const appHost = createAppHostWithPacts(packages, pacts)
8690
const declaredAPIs = _(packages)
8791
.flatten()
@@ -92,17 +96,19 @@ export async function createAppHostAndWaitForLoading(packages: EntryPointOrPacka
9296
setTimeout(() => {
9397
const readyAPIs = Array.from(globalThis.repluggableAppDebug.readyAPIs)
9498
const unreadyAPIs = declaredAPIs.filter(api => !readyAPIs.some(readyAPI => readyAPI.name === api.name))
95-
const rootUnreadyAPI = globalThis.repluggableAppDebug.utils.getRootUnreadyAPI()
99+
const rootUnreadyAPIs = globalThis.repluggableAppDebug.utils.getRootUnreadyAPIs()
96100

97101
reject(
98102
new Error(
99-
`createAppHostAndWaitForLoading - waiting for loading timed out.
100-
there's a high chance this missing API is the main reason for it: ${JSON.stringify(rootUnreadyAPI)}
103+
`createAppHostAndWaitForLoading - waiting for loading timed out after ${timeout}ms.
104+
these root unready APIs are most likely the reason - unready entry points require them but nothing declares them (missing entry point or pact?): ${JSON.stringify(
105+
rootUnreadyAPIs
106+
)}
101107
102108
in addition here's the full list of declared APIs that have not been contributed: ${JSON.stringify(unreadyAPIs)}`
103109
)
104110
)
105-
}, 3000)
111+
}, timeout)
106112
})
107113

108114
const loadingPromise = new Promise<void>(async resolve => {

0 commit comments

Comments
 (0)