|
| 1 | +// Copyright (c) Microsoft Corporation. All rights reserved. |
| 2 | +// Licensed under the MIT License. |
| 3 | + |
| 4 | +// Verification sample for PR #164: Validate first yielded value in orchestrator run() |
| 5 | +// |
| 6 | +// Customer scenario: A developer writes a data-enrichment orchestrator that accidentally |
| 7 | +// yields a raw Promise (from a helper function) instead of a Task from ctx.callActivity(). |
| 8 | +// Before this fix, the orchestration would hang indefinitely with no error — the developer |
| 9 | +// would have no indication of what went wrong. After the fix, the SDK immediately fails |
| 10 | +// with a clear error: "The orchestrator generator yielded a non-Task object". |
| 11 | +// |
| 12 | +// This sample verifies two things: |
| 13 | +// 1. A correct orchestrator (yielding Tasks) still works normally. |
| 14 | +// 2. A buggy orchestrator (yielding a non-Task value) now fails fast with a clear error |
| 15 | +// instead of hanging forever. |
| 16 | + |
| 17 | +import { |
| 18 | + OrchestrationContext, |
| 19 | + ActivityContext, |
| 20 | + TOrchestrator, |
| 21 | + OrchestrationStatus, |
| 22 | +} from "@microsoft/durabletask-js"; |
| 23 | +import { |
| 24 | + DurableTaskAzureManagedClientBuilder, |
| 25 | + DurableTaskAzureManagedWorkerBuilder, |
| 26 | +} from "@microsoft/durabletask-js-azuremanaged"; |
| 27 | + |
| 28 | +const endpoint = process.env.ENDPOINT || "localhost:8080"; |
| 29 | +const taskHub = process.env.TASKHUB || "default"; |
| 30 | + |
| 31 | +// --- Activities --- |
| 32 | + |
| 33 | +// Simulates fetching enrichment data from an external service |
| 34 | +const fetchEnrichmentData = async (_ctx: ActivityContext, recordId: string): Promise<string> => { |
| 35 | + return `enriched-data-for-${recordId}`; |
| 36 | +}; |
| 37 | + |
| 38 | +// --- Orchestrators --- |
| 39 | + |
| 40 | +// CORRECT orchestrator: yields a Task from ctx.callActivity() |
| 41 | +const correctEnrichmentOrchestrator: TOrchestrator = async function* (ctx: OrchestrationContext): any { |
| 42 | + const result: string = yield ctx.callActivity(fetchEnrichmentData, "record-001"); |
| 43 | + return { enriched: true, data: result }; |
| 44 | +}; |
| 45 | + |
| 46 | +// BUGGY orchestrator: accidentally yields a raw Promise instead of a Task. |
| 47 | +// This simulates a common developer mistake — calling a helper function that returns |
| 48 | +// a Promise directly, rather than using ctx.callActivity(). |
| 49 | +const buggyEnrichmentOrchestrator: TOrchestrator = async function* (_ctx: OrchestrationContext): any { |
| 50 | + // Bug: yielding a raw Promise instead of ctx.callActivity(...) |
| 51 | + // Before PR #164, this would cause the orchestration to hang indefinitely. |
| 52 | + // After PR #164, this throws: "The orchestrator generator yielded a non-Task object" |
| 53 | + yield Promise.resolve("this-is-not-a-task"); |
| 54 | +}; |
| 55 | + |
| 56 | +async function main() { |
| 57 | + console.log(`Connecting to DTS emulator at ${endpoint}, taskHub: ${taskHub}`); |
| 58 | + |
| 59 | + const client = new DurableTaskAzureManagedClientBuilder() |
| 60 | + .endpoint(endpoint, taskHub, null) |
| 61 | + .build(); |
| 62 | + |
| 63 | + const worker = new DurableTaskAzureManagedWorkerBuilder() |
| 64 | + .endpoint(endpoint, taskHub, null) |
| 65 | + .build(); |
| 66 | + |
| 67 | + worker.addNamedOrchestrator("CorrectEnrichment", correctEnrichmentOrchestrator); |
| 68 | + worker.addNamedOrchestrator("BuggyEnrichment", buggyEnrichmentOrchestrator); |
| 69 | + worker.addNamedActivity("fetchEnrichmentData", fetchEnrichmentData); |
| 70 | + |
| 71 | + await worker.start(); |
| 72 | + console.log("Worker started."); |
| 73 | + |
| 74 | + let allPassed = true; |
| 75 | + |
| 76 | + // --- Test 1: Correct orchestrator should complete successfully --- |
| 77 | + console.log("\n--- Test 1: Correct orchestrator (yields Task) ---"); |
| 78 | + const correctId = await client.scheduleNewOrchestration("CorrectEnrichment"); |
| 79 | + console.log(`Scheduled correct orchestration: ${correctId}`); |
| 80 | + |
| 81 | + const correctState = await client.waitForOrchestrationCompletion(correctId, undefined, 30); |
| 82 | + const correctPassed = correctState?.runtimeStatus === OrchestrationStatus.COMPLETED; |
| 83 | + console.log(`Status: ${correctState?.runtimeStatus}`); |
| 84 | + console.log(`Output: ${correctState?.serializedOutput}`); |
| 85 | + console.log(`Result: ${correctPassed ? "PASS" : "FAIL"}`); |
| 86 | + if (!correctPassed) allPassed = false; |
| 87 | + |
| 88 | + // --- Test 2: Buggy orchestrator should FAIL (not hang) with clear error --- |
| 89 | + console.log("\n--- Test 2: Buggy orchestrator (yields non-Task Promise) ---"); |
| 90 | + const buggyId = await client.scheduleNewOrchestration("BuggyEnrichment"); |
| 91 | + console.log(`Scheduled buggy orchestration: ${buggyId}`); |
| 92 | + |
| 93 | + const buggyState = await client.waitForOrchestrationCompletion(buggyId, undefined, 30); |
| 94 | + const buggyFailed = buggyState?.runtimeStatus === OrchestrationStatus.FAILED; |
| 95 | + const hasNonTaskError = buggyState?.failureDetails?.message?.includes("non-Task") ?? false; |
| 96 | + const buggyPassed = buggyFailed && hasNonTaskError; |
| 97 | + console.log(`Status: ${buggyState?.runtimeStatus}`); |
| 98 | + console.log(`Failure message: ${buggyState?.failureDetails?.message ?? "none"}`); |
| 99 | + console.log(`Result: ${buggyPassed ? "PASS" : "FAIL"}`); |
| 100 | + if (!buggyPassed) allPassed = false; |
| 101 | + |
| 102 | + // --- Verification summary --- |
| 103 | + console.log("\n=== VERIFICATION RESULT ==="); |
| 104 | + console.log( |
| 105 | + JSON.stringify( |
| 106 | + { |
| 107 | + pr: 164, |
| 108 | + scenario: "validate-first-yield-non-task", |
| 109 | + checks: [ |
| 110 | + { |
| 111 | + name: "Correct orchestrator completes successfully", |
| 112 | + expected: "COMPLETED", |
| 113 | + actual: OrchestrationStatus[correctState?.runtimeStatus ?? -1] ?? String(correctState?.runtimeStatus), |
| 114 | + passed: correctPassed, |
| 115 | + }, |
| 116 | + { |
| 117 | + name: "Buggy orchestrator fails fast with non-Task error", |
| 118 | + expected: "FAILED with non-Task message", |
| 119 | + actual: `${OrchestrationStatus[buggyState?.runtimeStatus ?? -1] ?? buggyState?.runtimeStatus} - ${buggyState?.failureDetails?.message ?? "no error"}`, |
| 120 | + passed: buggyPassed, |
| 121 | + }, |
| 122 | + ], |
| 123 | + allPassed, |
| 124 | + timestamp: new Date().toISOString(), |
| 125 | + }, |
| 126 | + null, |
| 127 | + 2, |
| 128 | + ), |
| 129 | + ); |
| 130 | + |
| 131 | + await worker.stop(); |
| 132 | + await client.stop(); |
| 133 | + |
| 134 | + process.exit(allPassed ? 0 : 1); |
| 135 | +} |
| 136 | + |
| 137 | +main().catch((err) => { |
| 138 | + console.error("Verification failed with error:", err); |
| 139 | + process.exit(1); |
| 140 | +}); |
0 commit comments