Skip to content

Commit 2aa958f

Browse files
committed
chore: add verification sample for PR #164
Verification sample: examples/verification/pr-164-validate-first-yield.ts Verifies that orchestrators yielding non-Task values on their first yield now fail fast with a clear error instead of hanging indefinitely. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 64d5ef1 commit 2aa958f

File tree

1 file changed

+140
-0
lines changed

1 file changed

+140
-0
lines changed
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
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

Comments
 (0)