Repository: square/workflow-kotlin
Issue: nextRendering() is called in runtime if Workflow's scope is cancelled.

Right now in `renderWorkflowIn` we check if the `CoroutineScope` `isActive` at the top of the loop:

```
  scope.launch(start = ATOMIC) {
    while (isActive) {
      // It might look weird to start by consuming the output before getting the rendering below,
      // but remember the first render pass already occurred above, before this coroutine was even
      // launched.
      val output = runner.nextOutput()

      // After receiving an output, the next render pass must be done before emitting that output,
      // so that the workflow states appear consistent to observers of the outputs and renderings.
      renderingsAndSnapshots.value = runner.nextRendering()
      output?.let { onOutput(it.value) }
    }
  }
```

But since `nextOutput()` is suspending we could resume and have had an `action` `cancel()` the `CoroutineScope` of the root workflow. We should pro-actively check this before calling `nextRendering()`.

Something like:

```
  scope.launch(start = ATOMIC) {
    while (isActive) {
      // It might look weird to start by consuming the output before getting the rendering below,
      // but remember the first render pass already occurred above, before this coroutine was even
      // launched.
      val output = runner.nextOutput()

      if (!isActive) return@launch
      // After receiving an output, the next render pass must be done before emitting that output,
      // so that the workflow states appear consistent to observers of the outputs and renderings.
      renderingsAndSnapshots.value = runner.nextRendering()
      output?.let { onOutput(it.value) }
    }
  }
```

Changed files:
workflow-runtime/src/test/java/com/squareup/workflow1/RenderWorkflowInTest.kt
workflow-runtime/src/main/java/com/squareup/workflow1/RenderWorkflow.kt

diff --git a/workflow-runtime/src/main/java/com/squareup/workflow1/RenderWorkflow.kt b/workflow-runtime/src/main/java/com/squareup/workflow1/RenderWorkflow.kt
index 26fd1145..d7006f58 100644
--- a/workflow-runtime/src/main/java/com/squareup/workflow1/RenderWorkflow.kt
+++ b/workflow-runtime/src/main/java/com/squareup/workflow1/RenderWorkflow.kt
@@ -138,6 +138,10 @@ public fun <PropsT, OutputT, RenderingT> renderWorkflowIn(
       // launched.
       val output = runner.nextOutput()
 
+      // After resuming from runner.nextOutput() our coroutine could now be cancelled, check so we
+      // don't surprise anyone with an unexpected rendering pass. Show's over, go home.
+      if (!isActive) return@launch
+
       // After receiving an output, the next render pass must be done before emitting that output,
       // so that the workflow states appear consistent to observers of the outputs and renderings.
       renderingsAndSnapshots.value = runner.nextRendering()
diff --git a/workflow-runtime/src/test/java/com/squareup/workflow1/RenderWorkflowInTest.kt b/workflow-runtime/src/test/java/com/squareup/workflow1/RenderWorkflowInTest.kt
index 03347581..651025ce 100644
--- a/workflow-runtime/src/test/java/com/squareup/workflow1/RenderWorkflowInTest.kt
+++ b/workflow-runtime/src/test/java/com/squareup/workflow1/RenderWorkflowInTest.kt
@@ -380,6 +380,27 @@ class RenderWorkflowInTest {
     assertNull(cancellationException!!.cause)
   }
 
+  @Test fun `cancelling scope in action cancels runtime and does not render again`() {
+    val trigger = CompletableDeferred<Unit>()
+    var renderCount = 0
+    val workflow = Workflow.stateless<Unit, Nothing, Unit> {
+      renderCount++
+      runningWorker(Worker.from { trigger.await() }) {
+        action {
+          expectedSuccessScope.cancel()
+        }
+      }
+    }
+    renderWorkflowIn(workflow, expectedSuccessScope, MutableStateFlow(Unit)) {}
+    assertTrue(expectedSuccessScope.isActive)
+    assertTrue(renderCount == 1)
+
+    trigger.complete(Unit)
+    expectedSuccessScope.advanceUntilIdle()
+    assertFalse(expectedSuccessScope.isActive)
+    assertEquals(1, renderCount, "Should not render after CoroutineScope is canceled.")
+  }
+
   @Test fun `failing scope cancels runtime`() {
     var cancellationException: Throwable? = null
     val workflow = Workflow.stateless<Unit, Nothing, Unit> {