synchronizing-with-idle について
Jetpack Compose のテスト作成者向けスキルで、idle と各種 wait API の意味やタイムアウトの違いを説明します。非同期 UI に合わせた待機方法の選び方を扱います。
主な機能
- 01Compose の idle 条件の説明
- 02wait API とタイムアウトの比較
- 03条件別の選択表
- 04IdlingResource と Espresso の橋渡し
- 05状態操作とアサーションの例
活用シーン
- 01ノード不足や件数不一致の断続的な失敗の調査
- 02Snackbar、ナビゲーション、IO 状態の反映待ち
- 03Compose 状態と外部処理に応じた待機方法の選択
- 04外部非同期処理向け IdlingResource の登録
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ skydoves/ android-testing-skills --skill synchronizing-with-idleインストール後、Claude Code で/synchronizing-with-idleを入力して読み込まれたか確認します。
詳細
アイドル状態との同期 — 適切な待機方法を選ぶ
誤ったプリミティブを使って待機する Compose テストは、不安定さの最大の原因です。このスキルでは、ComposeTestRule と ComposeUiTest にあるすべてのアイドル/待機 API を列挙し、「アイドル状態」の実際の意味を明確にして、どの API を選ぶべきかを判断するためのマトリックスを示します。アニメーション固有の待機方法は ../testing-animations-deterministically/SKILL.md に、基盤となるクロックのセマンティクスは ../controlling-the-test-clock/SKILL.md に記載されています。
このスキルを使う場面
- テストがときどき「node not found」や「node count mismatch」で失敗するものの、本番コードは正しい場合。
- 画面、スナックバー、ナビゲーションの遷移、または IO に基づく状態を待つために、開発者が
Thread.sleep(2000)を使おうとしている場合。 ViewModelがコルーチンから状態を投稿し、UI に反映されるまでテストで待つ必要がある場合。- 開発者が
IdlingResource、Espresso ブリッジ、runOnIdleとrunOnUiThreadの違い、またはrunWhenIdleのほうが速いかどうかについて質問している場合。 - 開発者が
waitUntil、waitUntilExactlyOneExists、waitUntilNodeCount、AndroidComposeUiTestTimeoutException、またはIdlingPolicies.setMasterPolicyTimeoutに言及している場合。 - 開発者がテストスレッドから状態(例:
state.firstVisibleItemIndex)を読み取ったり変更したりしていて、ときどき古いデータが返る理由を尋ねている場合。
このスキルを使わない場面
- 条件がクロックを一時停止したアニメーションの場合。
../testing-animations-deterministically/SKILL.mdとmainClock.advanceTimeUntilを使います。 MainTestClock自体の仕組み(フレーム、丸め処理、ディスパッチャー)が不明な場合。まず../controlling-the-test-clock/SKILL.mdを読みます。- テストタグやファインダーの問題が原因でテストが失敗していて、非同期のタイミングが問題ではない場合。
../../debug/printing-the-semantics-tree/SKILL.mdと../../finders/finding-nodes-by-tag-text-content/SKILL.mdを使います。 - 実際の質問が Espresso との相互運用性に関する場合。
../../interop/testing-with-espresso-interop/SKILL.md(兄弟スキル、対象範囲は別)を使います。
前提条件
androidx.compose.ui:ui-testとandroidx.compose.ui:ui-test-junit4(またはrunComposeUiTest)。createComposeRule()から取得したComposeContestTestRule(推奨 v2:androidx.compose.ui.test.junit4.v2.createComposeRule)。- 実験的な
waitUntilNodeCount/waitUntilExactlyOneExists/waitUntilAtLeastOneExists/waitUntilDoesNotExistを使う場合:テストクラスまたはメソッドに@OptIn(ExperimentalTestApi::class)を指定します。
「アイドル状態」の意味
次の 3 つの条件がすべて同時に満たされている状態です(compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/ComposeUiTest.kt:174-186)。
- 保留中の再コンポジションがないこと。 未処理のスナップショット適用通知がなく、リコンポーザーに保留中の作業がなく、テストフレームクロックで
withFrameNanosを待っているものがありません。 - 保留中の描画呼び出しがないこと。 測定とレイアウトのパスが実行済みで、Android では描画パスがフレームワークによって要求されています(Android では Choreographer が描画を制御し、テストクロックは制御しません —
MainTestClock.kt:36-41)。 - 登録済みのすべての
IdlingResourceがisIdleNow == trueを返すこと。 これにより、Compose 以外の非同期処理(OkHttpの呼び出しやRoomのクエリ)が同期に参加します。
Compose 自身の処理は ComposeIdlingResource(compose/ui/ui-test/src/androidMain/kotlin/androidx/compose/ui/test/ComposeIdlingResource.android.kt)を通じて自動的に登録されます。このリソースは、再コンポーザー、スナップショット、フレームクロックの待機処理をループで処理し、呼び出しあたり最大 100 フレームに制限します(109 行目)。安定するまでに 100 フレームを超えて必要とするテストは、1 回の waitForIdle に詰め込みすぎです。
プリミティブな API
以下の各項目は、ComposeUiTest.kt / ComposeTestRule.jvmAndAndroid.kt に記載されています。
| API | 効果 | タイムアウトの基準 |
|---|---|---|
waitForIdle() | 3 つのアイドル条件が満たされるまで、現在のスレッドをブロックします。autoAdvance = true の場合は、保留中の処理を消化するためにクロックも自動で進めます。 | 実時間 — IdlingPolicies.getMasterIdlingPolicy() |
awaitIdle() | waitForIdle の suspend 版です。 | 同上 |
waitUntil(conditionDescription, timeoutMillis = 1_000, condition) | condition() が true を返すまでブロックします。各反復では、(autoAdvance == true の場合)mainClock.advanceTimeByFrame() を呼び出し、さらに実時間で Thread.sleep(10) を実行します(ComposeUiTest.android.kt:899-902)。タイムアウトは System.nanoTime() を基準に測定されるため、テストクロックも進む一方で、実時間が経過するとタイムアウトします。 | 実時間 — 呼び出しごとの timeoutMillis |
waitUntilNodeCount(matcher, count, timeoutMillis = 1_000L) | @ExperimentalTestApi。onAllNodes(matcher).fetchSemanticsNodes(atLeastOneRootRequired = false).size == count を条件にした waitUntil です(ComposeUiTest.kt:275-285)。 | 実時間 |
waitUntilExactlyOneExists(matcher, timeoutMillis = 1_000L) | @ExperimentalTestApi。waitUntilNodeCount(matcher, 1, timeoutMillis) の簡易版です。 | 実時間 |
waitUntilAtLeastOneExists(matcher, timeoutMillis = 1_000L) | @ExperimentalTestApi。(ComposeUiTest.kt:298-305) | 実時間 |
waitUntilDoesNotExist(matcher, timeoutMillis = 1_000L) | @ExperimentalTestApi。waitUntilNodeCount(matcher, 0, timeoutMillis) の簡易版です。 | 実時間 |
runOnIdle { … } | waitForIdle() を呼び出してから runOnUiThread { … } を実行します。状態の変更や読み取りには、通常これを使用します。 | 実時間(内部の waitForIdle) |
runOnUiThread { … } | Instrumentation.runOnMainSync を介して FutureTask をポストします(AndroidSynchronization.android.kt)。すでに UI スレッド上にいる場合は、その場で実行します。アイドル状態になるまで待機しません。 | なし |
runWhenIdle { … } | waitForIdle() を呼び出してから runOnUiThread { … } を実行しますが、ラムダ内のノードクエリによって暗黙に呼び出される waitForIdle を抑制します。アサーションのみのブロックを高速に実行できます。内部で状態を変更してはいけません。 | 実時間 |
awaitAndRunWhenIdle { … } | runWhenIdle の suspend 版です。 | 実時間 |
hasPendingWork(): Boolean | 受動的なスナップショットです。メインクロックの待機処理、スナップショットの変更、またはリコンポーザーの保留中の処理があるかを調べます。信頼できるのは autoAdvance = false の場合のみです(ComposeUiTest.kt:251-256)。 | なし |
IdlingResource の登録 | rule.registerIdlingResource(myResource) / unregisterIdlingResource。 | 実時間 |
タイムアウトの基準 — 混同しないでください
| API | 基準 | デフォルト | 変更方法 |
|---|---|---|---|
waitForIdle / awaitIdle | 実時間 — Espresso の IdlingPolicies マスタータイムアウト | 26 s | @Before で IdlingPolicies.setMasterPolicyTimeout(...) を呼び出します |
waitUntil(…) ファミリー | 実時間 | 呼び出しごとに 1000 ms | 呼び出しごとの timeoutMillis |
MainTestClock.advanceTimeUntil(…) | テストクロック | 呼び出しごとに 1000 ms | 呼び出しごとの timeoutMillis |
runComposeUiTest(testTimeout = …) | 実時間 | 60.seconds | runComposeUiTest(testTimeout = 5.minutes) { … }。タイムアウトすると AndroidComposeUiTestTimeoutException がスローされます |
Skydoves のホットテイク #4: waitUntil のタイムアウトは実時間、advanceTimeUntil はテストクロックに基づきます。待機する条件が Compose の状態から観測できる場合は、常に mainClock.advanceTimeUntil を優先してください。waitUntil は Compose のスナップショットシステムの外部にある条件(Job.isCompleted、LaunchedEffect からインクリメントされるカウンター、外部サービスなど)に限って使用します。
RobolectricIdlingStrategy も同じ Espresso の IdlingPolicies.getMasterIdlingPolicy() を参照するため、グローバルな setMasterPolicyTimeout の延長は、ホスト(Robolectric)テストとデバイステストの両方に適用されます。
Espresso ブリッジ
Espresso には独自の androidx.test.espresso.IdlingResource インターフェースがあり、Compose の androidx.compose.ui.test.IdlingResource とは異なります。ブリッジとなるのは EspressoLink(compose/ui/ui-test/src/androidMain/kotlin/androidx/compose/ui/test/EspressoLink.android.kt)です。
Espresso (instrumentation tests)
│ withStrategy { … }
▼
EspressoLink ──implements──> androidx.test.espresso.IdlingResource
│
│ delegates isIdleNow() to ──> IdlingResourceRegistry
▼
IdlingResourceRegistry — Compose's registry
│
├── ComposeIdlingResource (recomposer + snapshot + frame-clock awaiters)
└── any rule.registerIdlingResource(...)
ホストテストでは、RobolectricIdlingStrategy も同じレジストリを読み取ります。開発者が Compose の IdlingResource を登録すると、フレームワークがブリッジ処理を行います。
interface IdlingResource {
val isIdleNow: Boolean
fun getDiagnosticMessageIfBusy(): String? = null
}
(compose/ui/ui-test/src/jvmAndAndroidMain/kotlin/androidx/compose/ui/test/IdlingResource.kt:37-52 より。)待機がタイムアウトしたときに役立つメッセージを表示するには、getDiagnosticMessageIfBusy() をオーバーライドします。このメッセージはタイムアウト例外のテキストに追加されます。
判断マトリクス
Need to wait for… | Use
------------------------------------------|----------------------------------------------
A node count to stabilize | waitUntilNodeCount / waitUntilExactlyOneExists
A node to appear | waitUntilAtLeastOneExists
A node to disappear | waitUntilDoesNotExist
A Compose state condition | mainClock.advanceTimeUntil { state }
A non-Compose condition (Job, counter) | waitUntil(conditionDescription) { condition }
"Just settle the UI" | waitForIdle() (or runOnIdle for read+act)
External async work (HTTP, DB) | rule.registerIdlingResource(MyIdlingResource)
A paused-clock animation frame | mainClock.advanceTimeBy(...) -- different skill
A particular state.value to read safely | rule.runOnIdle { state.value }
ワークフロー
1. 状態とのやり取りには、まず runOnIdle を使います
テストスレッドから Compose の状態を読み取ったり変更したりすると、リコンポーザーとの競合が発生します。リコンポーザーはメインスレッドでスナップショットへの書き込みを適用するため、テストスレッドからは適用途中の状態が見える場合があります。runOnIdle は、アイドル状態になるまで待機してから UI スレッドで処理を実行することで、この問題を解決します。
val firstVisibleIndex = rule.runOnIdle { state.firstVisibleItemIndex }
(skydoves の大胆な意見 #5 — 状態の変更は runOnIdle または runOnUiThread 経由で行います。)
2. 読み取り専用のアサーションブロックには runWhenIdle を使います
rule.runWhenIdle {
val node = rule.onNodeWithTag("counter").fetchSemanticsNode()
assertEquals("3", node.config[SemanticsProperties.Text].first().text)
val rect = node.boundsInRoot
// ... more reads ...
}
通常の runOnIdle 内でノードをクエリするたびに、waitForIdle() が再実行されます。runWhenIdle 内では、ブロックの開始時にアイドル状態が確認済みのため、こうした暗黙の待機が抑制されます。これはフレームを手動で進めるテストで重要です。runOnIdle 内でノードをクエリすると waitForIdle() が繰り返し呼び出され、(autoAdvance = true の場合)クロックが自動的に進んで、手動での制御が損なわれます。runWhenIdle 内で状態を変更してはいけません。
3. 「N 個のノードが一致するまで待機する」場合は、実験的な waitUntilExactlyOneExists などを使います
@OptIn(ExperimentalTestApi::class)
@Test fun snackbarAppears() {
rule.setContent { /* … triggers a snackbar after 200 ms … */ }
rule.onNodeWithTag("trigger").performClick()
rule.waitUntilExactlyOneExists(hasTestTag("snackbar"), timeoutMillis = 2_000)
rule.onNodeWithTag("snackbar").assertTextEquals("Saved")
}
デフォルトのタイムアウトである 1000 ms(ComposeUiTest.kt:278、ComposeUiTest.kt:301、ComposeUiTest.kt:321、ComposeUiTest.kt:334)では、スナックバーやボトムシートのアニメーションに対して短すぎることがよくあります。呼び出しごとに、明示的にタイムアウトを延長してください。
4. Compose 以外の条件には waitUntil { … } のみを使います
// SnackbarHostTest.kt:77-88
val job = scope.launch {
hostState.showSnackbar("1")
Truth.assertThat(resultedInvocation).isEqualTo("1")
hostState.showSnackbar("2")
Truth.assertThat(resultedInvocation).isEqualTo("12")
hostState.showSnackbar("3")
Truth.assertThat(resultedInvocation).isEqualTo("123")
}
rule.waitUntil { job.isCompleted }
Job.isCompleted は Compose の状態ではありません。mainClock.advanceTimeUntil { job.isCompleted } は、クロックをどれだけ進めてもそれだけでは Job が完了しないため、ループしてタイムアウトします。
5. 実際に外部の非同期処理がある場合は IdlingResource を登録します
class HttpIdlingResource(private val client: OkHttpClient) : IdlingResource {
override val isIdleNow: Boolean
get() = client.dispatcher.runningCallsCount() == 0
override fun getDiagnosticMessageIfBusy(): String? =
"${client.dispatcher.runningCallsCount()} HTTP calls still in flight"
}
@Before fun setUp() { rule.registerIdlingResource(HttpIdlingResource(client)) }
@After fun tearDown() { rule.unregisterIdlingResource(HttpIdlingResource(client)) }
登録後は、すべての waitForIdle(およびすべてのノードクエリ内の暗黙的な waitForIdle)が、isIdleNow == true になるまでブロックします。isIdleNow は軽量に保つ必要があります。メインスレッドから呼び出されるためです(IdlingResource.kt:38-46)。
パターン
パターン:誤り — ノードを待つために Thread.sleep を使う
// WRONG
@Test fun successScreen() {
rule.setContent { App() }
rule.onNodeWithTag("login").performClick()
Thread.sleep(2_000)
rule.onNodeWithTag("dashboard").assertExists()
}
// WRONG because: Thread.sleep is unrelated to Compose's synchronization. It either over-
// waits (slowing the test) or under-waits (flake when CI is slow). Compose's own
// IdlingResource is already wired; just wait on the node directly.
// RIGHT
@OptIn(ExperimentalTestApi::class)
@Test fun successScreen() {
rule.setContent { App() }
rule.onNodeWithTag("login").performClick()
rule.waitUntilExactlyOneExists(hasTestTag("dashboard"), timeoutMillis = 2_000)
rule.onNodeWithTag("dashboard").assertExists()
}
パターン:誤り — Compose の状態から観測できる条件に waitUntil を使う
// WRONG
rule.waitUntil(timeoutMillis = 5_000) { state.value == Phase.Done }
// WRONG because: timeout is measured in wall clock and each iteration burns ~10 ms
// of real time on Thread.sleep(10) (ComposeUiTest.android.kt:899-902). For a Compose
// state condition, advanceTimeUntil drives the test clock and is deterministic.
// RIGHT
rule.mainClock.advanceTimeUntil(timeoutMillis = 5_000) { state.value == Phase.Done }
(skydoves の大胆な意見 #4。)
パターン:誤り — テストスレッドから状態を読み取る
// WRONG
val i = state.firstVisibleItemIndex
assertEquals(2, i)
// WRONG because: the test thread races the recomposer. The read may observe a half-applied
// snapshot, especially right after a click or scroll. Compose's docs are explicit: state
// reads from outside the UI thread are not synchronized.
// RIGHT
val i = rule.runOnIdle { state.firstVisibleItemIndex }
assertEquals(2, i)
(skydoves の大胆な意見 #5。)
パターン:hasPendingWork() — 受動的な確認。クロックの一時停止中のみ使用
rule.mainClock.autoAdvance = false
rule.setContent { /* … */ }
rule.mainClock.advanceTimeByFrame()
rule.runOnUiThread { trigger = true }
assertTrue(rule.hasPendingWork()) // recomposition queued, no frame yet
rule.mainClock.advanceTimeByFrame()
assertFalse(rule.hasPendingWork()) // frame applied
ComposeUiTest.kt:251-256 より:
autoAdvanceがtrueの場合、テストフレームワークは保留中の処理を継続的に処理します。この状況でこのメソッドを呼び出すと、その瞬間のスナップショットを取得することになり、通常はfalseを返します。処理がキューに入っていてもフレームワークがまだ自動的にクロックを進めていない場合は、一時的にtrueを返すことがありますが、結果はすぐに変わるため、テストロジックを制御するには信頼できません。
autoAdvance == true のとき、hasPendingWork() に基づいてテストロジックを制御してはいけません。
パターン:CI の遅延に備えて、全体に適用されるアイドルタイムアウトを延長する
@Before fun setUp() {
IdlingPolicies.setMasterPolicyTimeout(60, TimeUnit.SECONDS)
}
ローカルでは成功するのに、CI で waitForIdle(waitUntil ではありません)がタイムアウトする場合に適用してください。EspressoLink と RobolectricIdlingStrategy の両方が、このポリシーに従います。
必須ルール
- 必須: 待機条件が Compose の状態読み取りの場合は、
rule.waitUntil { state }よりもmainClock.advanceTimeUntil { state }を優先してください(skydoves hot take #4)。 - 必須: テストスレッドから状態を読み取る場合は、
runOnIdle { … }またはrunOnUiThread { … }で囲んでください。テストスレッドから直接state.valueを読み取ると、recomposer との競合が発生します(skydoves hot take #5)。 - 必須: クロックが一時停止している状態(
autoAdvance = false)で状態を変更する場合は、runOnIdleではなくrunOnUiThreadを呼び出してください。このモードでは、runOnIdleによる暗黙のwaitForIdleは適切ではありません。../testing-animations-deterministically/SKILL.mdを参照してください。 - 必須: 多数のノードを読み取るアサーションのみのブロックには、
runWhenIdle { … }を使用してください。これにより、クエリごとに発生する冗長なwaitForIdleを抑制できます。ブロック内で状態を変更してはいけません。 - 必須: 手作業で記述した
waitUntil { onAllNodes(...).fetchSemanticsNodes().isNotEmpty() }よりも、waitUntilExactlyOneExists/waitUntilAtLeastOneExists/waitUntilDoesNotExistを優先してください。こちらの方が明確で、タイムアウトメッセージにconditionDescriptionが表示されます。 - 必須:
autoAdvance == trueのときに、hasPendingWork()を対処可能な状態として扱ってはいけません。これは受動的なスナップショットであり、クロックが一時停止している場合にのみ信頼できます(ComposeUiTest.kt:251-256)。 - 必須: 状態が安定するのを待つために
Thread.sleepを使用してはいけません。waitUntil*、mainClock.advanceTimeUntil、またはIdlingResourceに置き換えてください(skydoves hot take #7)。正当な用途のThread.sleepは、リップル/スクリーンショットのテストで RenderThread 上で使う場合のみです。 - 推奨: HTTP、DB、外部カウンターなど、実際に存在する外部非同期ソースごとに
IdlingResourceを用意し、@Beforeで登録して@Afterで登録解除してください。タイムアウトメッセージが役立つように、getDiagnosticMessageIfBusy()をオーバーライドしてください。 - 推奨: エージェントの低速化による CI の不安定さに対処するため、
@BeforeでIdlingPolicies.setMasterPolicyTimeout(...)を設定してください。これにより、Espresso と Robolectric の両方のタイムアウトが一律に引き上げられます。
検証
- ドキュメントに記載された RenderThread/スクリーンショットの例外を除き、どのテストメソッドにも
Thread.sleepが存在しないこと。 - テストスレッドから読み取るすべての状態が
runOnIdle { … }で囲まれていること(検索:grep -nP 'state\.\w+' src/androidTest)。 - Compose の状態条件に対する
waitUntil { state.value … }がすべてmainClock.advanceTimeUntil { state.value … }に移行されていること。 - ノード待機の処理ではすべて、
fetchSemanticsNodes()を使った手作業のwaitUntilではなく、waitUntilExactlyOneExists/waitUntilAtLeastOneExists/waitUntilDoesNotExistを使用していること。 - すべての外部非同期ソースに
IdlingResourceが登録され、@Beforeで登録して@Afterで登録解除されていること。 -
autoAdvance == trueのときにhasPendingWork()に依存する呼び出し箇所が存在しないこと。 - CI の実行が、
AndroidComposeUiTestTimeoutExceptionまたはComposeTimeoutExceptionによる不安定化なしに、50 回の反復を完了すること。
参考資料
- Android Developers — Compose のテスト:https://developer.android.com/develop/ui/compose/testing
- Android Developers — Compose テストのチートシート:https://developer.android.com/develop/ui/compose/testing-cheatsheet
- Espresso
IdlingPoliciesのリファレンス:https://developer.android.com/reference/androidx/test/espresso/IdlingPolicies compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/ComposeUiTest.kt:174-260—waitForIdle、awaitIdle、waitUntil、runOnIdle、runOnUiThread、runWhenIdle、awaitAndRunWhenIdle、hasPendingWork。compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/ComposeUiTest.kt:275-335— 実験的なwaitUntilNodeCount/waitUntilExactlyOneExists/waitUntilAtLeastOneExists/waitUntilDoesNotExist拡張関数。compose/ui/ui-test/src/jvmAndAndroidMain/kotlin/androidx/compose/ui/test/IdlingResource.kt— 公開されているIdlingResourceインターフェース。compose/ui/ui-test/src/jvmAndAndroidMain/kotlin/androidx/compose/ui/test/IdlingResourceRegistry.jvmAndAndroid.kt— ユーザー登録リソースと Compose 独自のリソースを集約するレジストリ。compose/ui/ui-test/src/androidMain/kotlin/androidx/compose/ui/test/ComposeIdlingResource.android.kt— Compose によって自動登録されるリソース。109 行目に 100 フレームの上限があります。compose/ui/ui-test/src/androidMain/kotlin/androidx/compose/ui/test/EspressoLink.android.kt—androidx.test.espresso.IdlingResourceブリッジ。compose/ui/ui-test/src/androidMain/kotlin/androidx/compose/ui/test/ComposeUiTest.android.kt:885-908—waitUntilの 10 ms 間隔の実時間ポーリングループ。compose/ui/ui-test/src/androidMain/kotlin/androidx/compose/ui/test/AndroidSynchronization.android.kt—Instrumentation.runOnMainSyncを介してFutureTaskをポストするrunOnUiThread。compose/material3/material3/src/androidDeviceTest/kotlin/androidx/compose/material3/SnackbarHostTest.kt:77-88— 標準的なrule.waitUntil { job.isCompleted }。- 関連スキル:
../controlling-the-test-clock/SKILL.md—MainTestClock、フレームモデル、advanceTimeUntil。 - 関連スキル:
../testing-animations-deterministically/SKILL.md— クロックを一時停止したアニメーションのテスト。 - 関連スキル:
../../patterns/structuring-a-compose-test/SKILL.md— アイドル待機を取り巻く JUnit ライフサイクル。 - 関連スキル:
../../debug/printing-the-semantics-tree/SKILL.md— 「ノードが見つからない」原因が非同期処理ではなくファインダーの問題だと判明した場合。