Skip to content

Commit cd87c3c

Browse files
alex-strukclaude
andcommitted
test(experiments): port per-workflow tests to develop GraphWorkflow API
The extraction-experiment per-workflow tests (experiment-01..05) were written against the old inline-graph GraphWorkflow contract. develop since changed it: - GraphWorkflowInput.graph (inline) -> workflowVersionId (graph loaded via the getWorkflowGraphConfig activity by id) - GraphWorkflowResult no longer returns `ctx`; terminal ctx is read via the getStatus query Port each suite to the current contract (mirrors graph-workflow.test.ts): - makeWorkflowInput returns workflowVersionId + a test-only __testGraph, stripped before execution - withGraphConfigLoader mocks getWorkflowGraphConfig to serve the test graph, and mocks document.getStatus for develop's new post-execution hook - read terminal ctx via the getStatus query inside worker.runUntil Also guard redactCtxForQuery against ctx values that are `undefined`: JSON.stringify(undefined) returns undefined, so `.length` crashed the getStatus query whenever any ctx key held undefined (surfaced by these tests on a real Temporal cluster). Defensive one-liner; strictly more permissive. All 5 suites pass (119 tests) against the dev-stack Temporal; tsc --noEmit clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 807becc commit cd87c3c

6 files changed

Lines changed: 407 additions & 199 deletions

apps/temporal/src/experiment-01-neural-doc-intelligence.test.ts

Lines changed: 82 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ import { NativeConnection, Worker } from "@temporalio/worker";
3737
import { computeConfigHash } from "./config-hash";
3838
import { computeTopologicalOrder } from "./graph-engine/graph-algorithms";
3939
import { validateGraphConfigForExecution } from "./graph-schema-validator";
40-
import { graphWorkflow } from "./graph-workflow";
40+
import { getStatus, graphWorkflow } from "./graph-workflow";
4141
import type {
4242
ActivityNode,
4343
GraphEdge,
@@ -260,18 +260,47 @@ function buildMockActivities(opts: {
260260
};
261261
}
262262

263+
type TestGraphWorkflowInput = GraphWorkflowInput & {
264+
__testGraph: GraphWorkflowConfig;
265+
};
266+
263267
function makeWorkflowInput(
264268
graph: GraphWorkflowConfig,
265269
initialCtx: Record<string, unknown>,
266-
): GraphWorkflowInput {
270+
): TestGraphWorkflowInput {
267271
return {
268-
graph,
272+
workflowVersionId: "test-workflow-version-id",
269273
initialCtx,
270274
configHash: computeConfigHash(graph),
271275
runnerVersion: "1.0.0",
276+
__testGraph: graph,
272277
};
273278
}
274279

280+
/**
281+
* Mock getWorkflowGraphConfig so the workflow loads the test graph by id
282+
* (develop moved from inline-graph input to DB-loaded-by-workflowVersionId).
283+
*/
284+
function withGraphConfigLoader(
285+
activities: Record<string, ActivityImpl>,
286+
graph: GraphWorkflowConfig,
287+
): Record<string, ActivityImpl> {
288+
activities.getWorkflowGraphConfig = (async () => ({
289+
graph,
290+
workflowVersionId: "test-workflow-version-id",
291+
configHash: computeConfigHash(graph),
292+
})) as unknown as ActivityImpl;
293+
// develop's workflow runs a post-execution hook that reads document.getStatus;
294+
// mock it so the workflow closes cleanly (else the trailing failed activity
295+
// adds noise and can race the terminal getStatus query).
296+
if (!activities["document.getStatus"]) {
297+
activities["document.getStatus"] = (async () => ({
298+
status: "extracted",
299+
})) as unknown as ActivityImpl;
300+
}
301+
return activities;
302+
}
303+
275304
// ---------------------------------------------------------------------------
276305
// Static + structural tests
277306
// ---------------------------------------------------------------------------
@@ -504,12 +533,16 @@ describeRuntime(
504533
const fixture = loadFixture();
505534
const ocrResult = buildOcrResultFromFixture(fixture);
506535
const calls: ActivityCall[] = [];
507-
const activities = buildMockActivities({
508-
ocrResult,
509-
averageConfidence: 0.99,
510-
requiresReview: false,
511-
callsRef: calls,
512-
});
536+
const graph = loadTemplateForRuntime();
537+
const activities = withGraphConfigLoader(
538+
buildMockActivities({
539+
ocrResult,
540+
averageConfidence: 0.99,
541+
requiresReview: false,
542+
callsRef: calls,
543+
}),
544+
graph,
545+
);
513546

514547
const worker = await Worker.create({
515548
connection: nativeConnection,
@@ -519,26 +552,31 @@ describeRuntime(
519552
activities,
520553
});
521554

522-
const graph = loadTemplateForRuntime();
523-
const input = makeWorkflowInput(graph, {
555+
const { __testGraph: _graph, ...input } = makeWorkflowInput(graph, {
524556
documentId: "test-doc-high-confidence",
525557
blobKey: "blobs/1-81.jpg",
526558
fileName: "1 81.jpg",
527559
fileType: "image",
528560
contentType: "image/jpeg",
529561
});
530562

531-
const result = await worker.runUntil(
532-
client.workflow.execute(graphWorkflow, {
533-
workflowId: `e01-test-high-${Date.now()}`,
563+
const workflowId = `e01-test-high-${Date.now()}`;
564+
const activeClient = client;
565+
const { result, ctx } = await worker.runUntil(async () => {
566+
const result = await activeClient.workflow.execute(graphWorkflow, {
567+
workflowId,
534568
taskQueue,
535569
args: [input],
536-
}),
537-
);
570+
});
571+
const status = await activeClient.workflow
572+
.getHandle(workflowId)
573+
.query(getStatus);
574+
return { result, ctx: status.ctx };
575+
});
538576

539577
expect(result.status).toBe("completed");
540-
expect(result.ctx.requiresReview).toBe(false);
541-
expect(result.ctx.averageConfidence).toBe(0.99);
578+
expect(ctx.requiresReview).toBe(false);
579+
expect(ctx.averageConfidence).toBe(0.99);
542580

543581
// Note: pre-execution document.updateStatus runs first (graph engine
544582
// overhead), so we filter to ctx-driven activities for ordering.
@@ -592,12 +630,16 @@ describeRuntime(
592630
const fixture = loadFixture();
593631
const ocrResult = buildOcrResultFromFixture(fixture);
594632
const calls: ActivityCall[] = [];
595-
const activities = buildMockActivities({
596-
ocrResult,
597-
averageConfidence: 0.42,
598-
requiresReview: true,
599-
callsRef: calls,
600-
});
633+
const graph = loadTemplateForRuntime();
634+
const activities = withGraphConfigLoader(
635+
buildMockActivities({
636+
ocrResult,
637+
averageConfidence: 0.42,
638+
requiresReview: true,
639+
callsRef: calls,
640+
}),
641+
graph,
642+
);
601643

602644
const worker = await Worker.create({
603645
connection: nativeConnection,
@@ -607,8 +649,7 @@ describeRuntime(
607649
activities,
608650
});
609651

610-
const graph = loadTemplateForRuntime();
611-
const input = makeWorkflowInput(graph, {
652+
const { __testGraph: _graph, ...input } = makeWorkflowInput(graph, {
612653
documentId: "test-doc-low-confidence",
613654
blobKey: "blobs/1-81.jpg",
614655
fileName: "1 81.jpg",
@@ -622,23 +663,24 @@ describeRuntime(
622663
args: [input],
623664
});
624665

625-
const resultPromise = handle.result();
626-
const runPromise = worker.runUntil(resultPromise);
627-
628-
// Temporal queues the signal; humanGate consumes it once the workflow
629-
// reaches that node.
630-
await handle.signal("humanApproval", {
631-
approved: true,
632-
reviewer: "test-reviewer",
633-
comments: "ok",
634-
rejectionReason: "",
635-
annotations: "",
666+
const { result, ctx } = await worker.runUntil(async () => {
667+
// Temporal queues the signal; humanGate consumes it once the workflow
668+
// reaches that node.
669+
await handle.signal("humanApproval", {
670+
approved: true,
671+
reviewer: "test-reviewer",
672+
comments: "ok",
673+
rejectionReason: "",
674+
annotations: "",
675+
});
676+
const result = await handle.result();
677+
const status = await handle.query(getStatus);
678+
return { result, ctx: status.ctx };
636679
});
637680

638-
const result = await runPromise;
639681
expect(result.status).toBe("completed");
640-
expect(result.ctx.requiresReview).toBe(true);
641-
expect(result.ctx.averageConfidence).toBe(0.42);
682+
expect(ctx.requiresReview).toBe(true);
683+
expect(ctx.averageConfidence).toBe(0.42);
642684

643685
// storeResults must still run, but only after the humanReview gate.
644686
expect(calls.map((c) => c.type)).toContain("ocr.storeResults");

apps/temporal/src/experiment-02-mistral-doc-ai-azure.test.ts

Lines changed: 80 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ import { NativeConnection, Worker } from "@temporalio/worker";
3535
import { computeConfigHash } from "./config-hash";
3636
import { computeTopologicalOrder } from "./graph-engine/graph-algorithms";
3737
import { validateGraphConfigForExecution } from "./graph-schema-validator";
38-
import { graphWorkflow } from "./graph-workflow";
38+
import { getStatus, graphWorkflow } from "./graph-workflow";
3939
import type {
4040
ActivityNode,
4141
GraphEdge,
@@ -172,18 +172,47 @@ function buildMockActivities(opts: {
172172
};
173173
}
174174

175+
type TestGraphWorkflowInput = GraphWorkflowInput & {
176+
__testGraph: GraphWorkflowConfig;
177+
};
178+
175179
function makeWorkflowInput(
176180
graph: GraphWorkflowConfig,
177181
initialCtx: Record<string, unknown>,
178-
): GraphWorkflowInput {
182+
): TestGraphWorkflowInput {
179183
return {
180-
graph,
184+
workflowVersionId: "test-workflow-version-id",
181185
initialCtx,
182186
configHash: computeConfigHash(graph),
183187
runnerVersion: "1.0.0",
188+
__testGraph: graph,
184189
};
185190
}
186191

192+
/**
193+
* Mock getWorkflowGraphConfig so the workflow loads the test graph by id
194+
* (develop moved from inline-graph input to DB-loaded-by-workflowVersionId).
195+
*/
196+
function withGraphConfigLoader(
197+
activities: Record<string, ActivityImpl>,
198+
graph: GraphWorkflowConfig,
199+
): Record<string, ActivityImpl> {
200+
activities.getWorkflowGraphConfig = (async () => ({
201+
graph,
202+
workflowVersionId: "test-workflow-version-id",
203+
configHash: computeConfigHash(graph),
204+
})) as unknown as ActivityImpl;
205+
// develop's workflow runs a post-execution hook that reads document.getStatus;
206+
// mock it so the workflow closes cleanly (else the trailing failed activity
207+
// makes the terminal getStatus query unreliable on a real Temporal cluster).
208+
if (!activities["document.getStatus"]) {
209+
activities["document.getStatus"] = (async () => ({
210+
status: "extracted",
211+
})) as unknown as ActivityImpl;
212+
}
213+
return activities;
214+
}
215+
187216
// ---------------------------------------------------------------------------
188217
// Static + structural tests
189218
// ---------------------------------------------------------------------------
@@ -481,12 +510,16 @@ describeRuntime(
481510
const fixture = loadFixture();
482511
const ocrResult = buildOcrResultFromFixture(fixture);
483512
const calls: ActivityCall[] = [];
484-
const activities = buildMockActivities({
485-
ocrResult,
486-
averageConfidence: 0.99,
487-
requiresReview: false,
488-
callsRef: calls,
489-
});
513+
const graph = loadTemplate();
514+
const activities = withGraphConfigLoader(
515+
buildMockActivities({
516+
ocrResult,
517+
averageConfidence: 0.99,
518+
requiresReview: false,
519+
callsRef: calls,
520+
}),
521+
graph,
522+
);
490523

491524
const worker = await Worker.create({
492525
connection: nativeConnection,
@@ -496,26 +529,31 @@ describeRuntime(
496529
activities,
497530
});
498531

499-
const graph = loadTemplate();
500-
const input = makeWorkflowInput(graph, {
532+
const { __testGraph: _graph, ...input } = makeWorkflowInput(graph, {
501533
documentId: "test-doc-high-confidence",
502534
blobKey: "blobs/1-81.jpg",
503535
fileName: "1 81.jpg",
504536
fileType: "image",
505537
contentType: "image/jpeg",
506538
});
507539

508-
const result = await worker.runUntil(
509-
client.workflow.execute(graphWorkflow, {
510-
workflowId: `e02-test-high-${Date.now()}`,
540+
const workflowId = `e02-test-high-${Date.now()}`;
541+
const activeClient = client;
542+
const { result, ctx } = await worker.runUntil(async () => {
543+
const result = await activeClient.workflow.execute(graphWorkflow, {
544+
workflowId,
511545
taskQueue,
512546
args: [input],
513-
}),
514-
);
547+
});
548+
const status = await activeClient.workflow
549+
.getHandle(workflowId)
550+
.query(getStatus);
551+
return { result, ctx: status.ctx };
552+
});
515553

516554
expect(result.status).toBe("completed");
517-
expect(result.ctx.requiresReview).toBe(false);
518-
expect(result.ctx.averageConfidence).toBe(0.99);
555+
expect(ctx.requiresReview).toBe(false);
556+
expect(ctx.averageConfidence).toBe(0.99);
519557

520558
const ctxOrder = calls.map((c) => c.type);
521559
expect(ctxOrder).toContain("file.prepare");
@@ -556,12 +594,16 @@ describeRuntime(
556594
const fixture = loadFixture();
557595
const ocrResult = buildOcrResultFromFixture(fixture);
558596
const calls: ActivityCall[] = [];
559-
const activities = buildMockActivities({
560-
ocrResult,
561-
averageConfidence: 0.42,
562-
requiresReview: true,
563-
callsRef: calls,
564-
});
597+
const graph = loadTemplate();
598+
const activities = withGraphConfigLoader(
599+
buildMockActivities({
600+
ocrResult,
601+
averageConfidence: 0.42,
602+
requiresReview: true,
603+
callsRef: calls,
604+
}),
605+
graph,
606+
);
565607

566608
const worker = await Worker.create({
567609
connection: nativeConnection,
@@ -571,8 +613,7 @@ describeRuntime(
571613
activities,
572614
});
573615

574-
const graph = loadTemplate();
575-
const input = makeWorkflowInput(graph, {
616+
const { __testGraph: _graph, ...input } = makeWorkflowInput(graph, {
576617
documentId: "test-doc-low-confidence",
577618
blobKey: "blobs/1-81.jpg",
578619
fileName: "1 81.jpg",
@@ -586,21 +627,21 @@ describeRuntime(
586627
args: [input],
587628
});
588629

589-
const resultPromise = handle.result();
590-
const runPromise = worker.runUntil(resultPromise);
591-
592-
await handle.signal("humanApproval", {
593-
approved: true,
594-
reviewer: "test-reviewer",
595-
comments: "ok",
596-
rejectionReason: "",
597-
annotations: "",
630+
const { result, ctx } = await worker.runUntil(async () => {
631+
await handle.signal("humanApproval", {
632+
approved: true,
633+
reviewer: "test-reviewer",
634+
comments: "ok",
635+
rejectionReason: "",
636+
annotations: "",
637+
});
638+
const result = await handle.result();
639+
const status = await handle.query(getStatus);
640+
return { result, ctx: status.ctx };
598641
});
599-
600-
const result = await runPromise;
601642
expect(result.status).toBe("completed");
602-
expect(result.ctx.requiresReview).toBe(true);
603-
expect(result.ctx.averageConfidence).toBe(0.42);
643+
expect(ctx.requiresReview).toBe(true);
644+
expect(ctx.averageConfidence).toBe(0.42);
604645

605646
expect(calls.map((c) => c.type)).toContain("ocr.storeResults");
606647

0 commit comments

Comments
 (0)