Skip to content

Commit 74cfb7e

Browse files
authored
fix: use getValue() instead of toString() on protobuf StringValue in activity execution (#280)
1 parent 592d441 commit 74cfb7e

2 files changed

Lines changed: 105 additions & 2 deletions

File tree

packages/durabletask-js/src/worker/task-hub-grpc-worker.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -915,11 +915,11 @@ export class TaskHubGrpcWorker {
915915
instanceId,
916916
req.getName(),
917917
req.getTaskid(),
918-
req.getInput()?.toString() ?? "",
918+
req.getInput()?.getValue() ?? "",
919919
);
920920

921921
const s = new StringValue();
922-
s.setValue(result?.toString() ?? "");
922+
s.setValue(result ?? "");
923923

924924
res = new pb.ActivityResponse();
925925
res.setInstanceid(instanceId);

packages/durabletask-js/test/worker-activity-response.spec.ts

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,109 @@ describe("Worker Activity Response", () => {
9797
expect(mockStub.capturedResponse!.getFailuredetails()).toBeUndefined();
9898
});
9999

100+
it("should correctly extract and pass input from protobuf StringValue to activity", async () => {
101+
// Arrange
102+
const worker = new TaskHubGrpcWorker({
103+
logger: new NoOpLogger(),
104+
});
105+
106+
let receivedInput: unknown;
107+
const inputCapturingActivity = (_ctx: ActivityContext, input: unknown) => {
108+
receivedInput = input;
109+
return "done";
110+
};
111+
112+
worker.addActivity(inputCapturingActivity);
113+
114+
const mockStub = createMockStub();
115+
const testInput = { key: "value", nested: { arr: [1, 2, 3] } };
116+
const req = createActivityRequest("inputCapturingActivity", JSON.stringify(testInput));
117+
118+
// Act
119+
await (worker as any)._executeActivityInternal(req, COMPLETION_TOKEN, mockStub.stub);
120+
121+
// Assert — the activity must receive the deserialized input object
122+
expect(receivedInput).toEqual(testInput);
123+
expect(mockStub.capturedResponse).not.toBeNull();
124+
expect(mockStub.capturedResponse!.getResult()?.getValue()).toBe(JSON.stringify("done"));
125+
});
126+
127+
it("should pass empty string as input when protobuf StringValue is not set", async () => {
128+
// Arrange
129+
const worker = new TaskHubGrpcWorker({
130+
logger: new NoOpLogger(),
131+
});
132+
133+
let receivedInput: unknown = "sentinel";
134+
const inputCapturingActivity = (_ctx: ActivityContext, input: unknown) => {
135+
receivedInput = input;
136+
return "done";
137+
};
138+
139+
worker.addActivity(inputCapturingActivity);
140+
141+
const mockStub = createMockStub();
142+
// Create request WITHOUT setting input — simulates no input from sidecar
143+
const req = createActivityRequest("inputCapturingActivity");
144+
145+
// Act
146+
await (worker as any)._executeActivityInternal(req, COMPLETION_TOKEN, mockStub.stub);
147+
148+
// Assert — no input means the activity receives undefined (empty string is parsed as falsy by executor)
149+
expect(receivedInput).toBeUndefined();
150+
});
151+
152+
it("should correctly set result on ActivityResponse using getValue()", async () => {
153+
// Arrange
154+
const worker = new TaskHubGrpcWorker({
155+
logger: new NoOpLogger(),
156+
});
157+
158+
const outputObject = { result: "test", count: 42 };
159+
const objectReturningActivity = (_ctx: ActivityContext) => {
160+
return outputObject;
161+
};
162+
163+
worker.addActivity(objectReturningActivity);
164+
165+
const mockStub = createMockStub();
166+
const req = createActivityRequest("objectReturningActivity", JSON.stringify("input"));
167+
168+
// Act
169+
await (worker as any)._executeActivityInternal(req, COMPLETION_TOKEN, mockStub.stub);
170+
171+
// Assert — the result StringValue should contain the JSON-serialized output
172+
expect(mockStub.capturedResponse).not.toBeNull();
173+
const resultValue = mockStub.capturedResponse!.getResult();
174+
expect(resultValue).toBeDefined();
175+
expect(resultValue!.getValue()).toBe(JSON.stringify(outputObject));
176+
});
177+
178+
it("should set empty result on ActivityResponse when activity returns undefined", async () => {
179+
// Arrange
180+
const worker = new TaskHubGrpcWorker({
181+
logger: new NoOpLogger(),
182+
});
183+
184+
const voidActivity = (_ctx: ActivityContext) => {
185+
// returns undefined implicitly
186+
};
187+
188+
worker.addActivity(voidActivity);
189+
190+
const mockStub = createMockStub();
191+
const req = createActivityRequest("voidActivity");
192+
193+
// Act
194+
await (worker as any)._executeActivityInternal(req, COMPLETION_TOKEN, mockStub.stub);
195+
196+
// Assert — result should be an empty string for undefined return
197+
expect(mockStub.capturedResponse).not.toBeNull();
198+
const resultValue = mockStub.capturedResponse!.getResult();
199+
expect(resultValue).toBeDefined();
200+
expect(resultValue!.getValue()).toBe("");
201+
});
202+
100203
it("should set instanceId on ActivityResponse when activity fails", async () => {
101204
// Arrange
102205
const worker = new TaskHubGrpcWorker({

0 commit comments

Comments
 (0)