Skip to content

Commit 8fa873e

Browse files
committed
Add tests to check for update task v2 endpoint usage and fallback
1 parent 5dfccb7 commit 8fa873e

2 files changed

Lines changed: 197 additions & 0 deletions

File tree

src/integration-tests/WorkerRegistration.test.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
MetadataClient,
55
NonRetryableException,
66
TaskHandler,
7+
TaskRunner,
78
WorkflowExecutor,
89
clearWorkerRegistry,
910
getRegisteredWorkers,
@@ -668,4 +669,56 @@ describe("SDK Worker Registration", () => {
668669

669670
expect(getRegisteredWorkers().length).toBe(0);
670671
});
672+
673+
describeForOrkesV5("update-v2 endpoint verification", () => {
674+
test("SDK detects and uses /api/tasks/update-v2 on v5 server", async () => {
675+
const client = await clientPromise;
676+
const taskName = `sdk_test_update_v2_verify_${Date.now()}`;
677+
const workflowName = `sdk_test_update_v2_verify_wf_${Date.now()}`;
678+
679+
// Reset the static probe so this test measures a live response from the server,
680+
// regardless of what earlier tests in this shard may have set.
681+
(TaskRunner as unknown as { updateV2Available: boolean | null }).updateV2Available = null;
682+
683+
const taskRunner = new TaskRunner({
684+
client,
685+
worker: {
686+
taskDefName: taskName,
687+
execute: async () => ({ outputData: {}, status: "COMPLETED" }),
688+
},
689+
options: { pollInterval: 100, concurrency: 1, workerID: "" },
690+
});
691+
taskRunner.startPolling();
692+
693+
await executor.registerWorkflow(true, {
694+
name: workflowName,
695+
version: 1,
696+
ownerEmail: "developers@orkes.io",
697+
tasks: [simpleTask(taskName, taskName, {})],
698+
inputParameters: [],
699+
outputParameters: {},
700+
timeoutSeconds: 0,
701+
});
702+
workflowsToCleanup.push({ name: workflowName, version: 1 });
703+
704+
const executionId = await executor.startWorkflow({
705+
name: workflowName,
706+
input: {},
707+
version: 1,
708+
});
709+
710+
if (!executionId) throw new Error("Execution ID is undefined");
711+
712+
await waitForWorkflowStatus(executor, executionId, "COMPLETED");
713+
taskRunner.stopPolling();
714+
715+
// The workflow only reaches COMPLETED if a task update was accepted.
716+
// This assertion verifies the SDK used /api/tasks/update-v2 (not the v4
717+
// legacy /api/tasks fallback) — if update-v2 broke silently and the SDK
718+
// fell back, this would be false.
719+
expect(
720+
(TaskRunner as unknown as { updateV2Available: boolean | null }).updateV2Available
721+
).toBe(true);
722+
}, 60000);
723+
});
671724
});

src/sdk/clients/worker/__tests__/TaskRunner.test.ts

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,11 @@ jest.mock("@open-api/generated", () => ({
2727
error: undefined,
2828
response: { status: 200, ok: true },
2929
}),
30+
updateTask: jest.fn().mockResolvedValue({
31+
data: null,
32+
error: undefined,
33+
response: { status: 200, ok: true },
34+
}),
3035
},
3136
}));
3237

@@ -1459,3 +1464,142 @@ describe("V2 task chaining", () => {
14591464
expect(mockUpdateTask).toHaveBeenCalledTimes(2);
14601465
});
14611466
});
1467+
1468+
describe("v5/v4 backend detection", () => {
1469+
const makeTask = (taskId: string, workflowInstanceId = "wf-1"): Task => ({
1470+
taskId,
1471+
workflowInstanceId,
1472+
status: "IN_PROGRESS",
1473+
inputData: {},
1474+
});
1475+
1476+
const makeArgs = (mockClient: Client): RunnerArgs => ({
1477+
worker: {
1478+
taskDefName: "test",
1479+
execute: async () => ({ outputData: {}, status: "COMPLETED" }),
1480+
},
1481+
options: { pollInterval: 10, domain: "", concurrency: 1, workerID: "worker-id" },
1482+
logger: mockLogger,
1483+
client: mockClient,
1484+
});
1485+
1486+
test("uses updateTaskV2 when server returns 200 (v5)", async () => {
1487+
const mockClient = createMockClient();
1488+
1489+
(TaskResource.batchPoll as jest.MockedFunction<typeof TaskResource.batchPoll>)
1490+
.mockResolvedValueOnce({ data: [makeTask("task-1")] } as Awaited<ReturnType<typeof TaskResource.batchPoll>>)
1491+
.mockResolvedValue({ data: [] } as Awaited<ReturnType<typeof TaskResource.batchPoll>>);
1492+
1493+
// Default mock already returns 200 — no override needed
1494+
1495+
const runner = new TaskRunner(makeArgs(mockClient));
1496+
activeRunners.push(runner);
1497+
runner.startPolling();
1498+
1499+
await new Promise((r) => setTimeout(() => r(true), 200));
1500+
runner.stopPolling();
1501+
1502+
expect(TaskResource.updateTaskV2).toHaveBeenCalledTimes(1);
1503+
expect(TaskResource.updateTask).not.toHaveBeenCalled();
1504+
expect(
1505+
(TaskRunner as unknown as { updateV2Available: boolean | null }).updateV2Available
1506+
).toBe(true);
1507+
});
1508+
1509+
test("falls back to updateTask when server returns 404 (v4 — endpoint absent)", async () => {
1510+
const mockClient = createMockClient();
1511+
1512+
(TaskResource.batchPoll as jest.MockedFunction<typeof TaskResource.batchPoll>)
1513+
.mockResolvedValueOnce({ data: [makeTask("task-1")] } as Awaited<ReturnType<typeof TaskResource.batchPoll>>)
1514+
.mockResolvedValue({ data: [] } as Awaited<ReturnType<typeof TaskResource.batchPoll>>);
1515+
1516+
(TaskResource.updateTaskV2 as jest.MockedFunction<typeof TaskResource.updateTaskV2>)
1517+
.mockResolvedValue({ data: null, error: undefined, response: { status: 404, ok: false } } as Awaited<ReturnType<typeof TaskResource.updateTaskV2>>);
1518+
1519+
const runner = new TaskRunner(makeArgs(mockClient));
1520+
activeRunners.push(runner);
1521+
runner.startPolling();
1522+
1523+
await new Promise((r) => setTimeout(() => r(true), 200));
1524+
runner.stopPolling();
1525+
1526+
// Probed once, then fell back
1527+
expect(TaskResource.updateTaskV2).toHaveBeenCalledTimes(1);
1528+
expect(TaskResource.updateTask).toHaveBeenCalledTimes(1);
1529+
expect(
1530+
(TaskRunner as unknown as { updateV2Available: boolean | null }).updateV2Available
1531+
).toBe(false);
1532+
});
1533+
1534+
test("falls back to updateTask when server returns 405 (v4 — wrong method)", async () => {
1535+
const mockClient = createMockClient();
1536+
1537+
(TaskResource.batchPoll as jest.MockedFunction<typeof TaskResource.batchPoll>)
1538+
.mockResolvedValueOnce({ data: [makeTask("task-1")] } as Awaited<ReturnType<typeof TaskResource.batchPoll>>)
1539+
.mockResolvedValue({ data: [] } as Awaited<ReturnType<typeof TaskResource.batchPoll>>);
1540+
1541+
(TaskResource.updateTaskV2 as jest.MockedFunction<typeof TaskResource.updateTaskV2>)
1542+
.mockResolvedValue({ data: null, error: undefined, response: { status: 405, ok: false } } as Awaited<ReturnType<typeof TaskResource.updateTaskV2>>);
1543+
1544+
const runner = new TaskRunner(makeArgs(mockClient));
1545+
activeRunners.push(runner);
1546+
runner.startPolling();
1547+
1548+
await new Promise((r) => setTimeout(() => r(true), 200));
1549+
runner.stopPolling();
1550+
1551+
expect(TaskResource.updateTaskV2).toHaveBeenCalledTimes(1);
1552+
expect(TaskResource.updateTask).toHaveBeenCalledTimes(1);
1553+
expect(
1554+
(TaskRunner as unknown as { updateV2Available: boolean | null }).updateV2Available
1555+
).toBe(false);
1556+
});
1557+
1558+
test("skips probe on subsequent tasks after v5 detection — uses updateTaskV2 directly", async () => {
1559+
const mockClient = createMockClient();
1560+
1561+
(TaskResource.batchPoll as jest.MockedFunction<typeof TaskResource.batchPoll>)
1562+
.mockResolvedValueOnce({ data: [makeTask("task-1")] } as Awaited<ReturnType<typeof TaskResource.batchPoll>>)
1563+
.mockResolvedValueOnce({ data: [makeTask("task-2")] } as Awaited<ReturnType<typeof TaskResource.batchPoll>>)
1564+
.mockResolvedValue({ data: [] } as Awaited<ReturnType<typeof TaskResource.batchPoll>>);
1565+
1566+
// Explicitly reset to 200: jest.clearAllMocks() preserves mockResolvedValue overrides
1567+
// from previous tests (e.g. the 405-fallback test), so we cannot rely on the module-level
1568+
// default here.
1569+
(TaskResource.updateTaskV2 as jest.MockedFunction<typeof TaskResource.updateTaskV2>)
1570+
.mockResolvedValue({ data: null, error: undefined, response: { status: 200, ok: true } } as Awaited<ReturnType<typeof TaskResource.updateTaskV2>>);
1571+
1572+
const runner = new TaskRunner(makeArgs(mockClient));
1573+
activeRunners.push(runner);
1574+
runner.startPolling();
1575+
1576+
await new Promise((r) => setTimeout(() => r(true), 300));
1577+
runner.stopPolling();
1578+
1579+
expect(TaskResource.updateTaskV2).toHaveBeenCalledTimes(2);
1580+
expect(TaskResource.updateTask).not.toHaveBeenCalled();
1581+
});
1582+
1583+
test("skips probe on subsequent tasks after v4 detection — uses updateTask directly", async () => {
1584+
const mockClient = createMockClient();
1585+
1586+
(TaskResource.batchPoll as jest.MockedFunction<typeof TaskResource.batchPoll>)
1587+
.mockResolvedValueOnce({ data: [makeTask("task-1")] } as Awaited<ReturnType<typeof TaskResource.batchPoll>>)
1588+
.mockResolvedValueOnce({ data: [makeTask("task-2")] } as Awaited<ReturnType<typeof TaskResource.batchPoll>>)
1589+
.mockResolvedValue({ data: [] } as Awaited<ReturnType<typeof TaskResource.batchPoll>>);
1590+
1591+
(TaskResource.updateTaskV2 as jest.MockedFunction<typeof TaskResource.updateTaskV2>)
1592+
.mockResolvedValue({ data: null, error: undefined, response: { status: 405, ok: false } } as Awaited<ReturnType<typeof TaskResource.updateTaskV2>>);
1593+
1594+
const runner = new TaskRunner(makeArgs(mockClient));
1595+
activeRunners.push(runner);
1596+
runner.startPolling();
1597+
1598+
await new Promise((r) => setTimeout(() => r(true), 300));
1599+
runner.stopPolling();
1600+
1601+
// Probe fires once for task-1, then task-2 goes directly to updateTask
1602+
expect(TaskResource.updateTaskV2).toHaveBeenCalledTimes(1);
1603+
expect(TaskResource.updateTask).toHaveBeenCalledTimes(2);
1604+
});
1605+
});

0 commit comments

Comments
 (0)