From f6be3fa0f646f5930c971b7d81dbdda24f4ed25f Mon Sep 17 00:00:00 2001 From: Copilot <223556219+Copilot@users.noreply.github.com> Date: Mon, 4 May 2026 09:04:06 +0000 Subject: [PATCH 1/2] fix: use epoch default for missing entity lastModifiedTime convertEntityMetadata() used new Date() (current time) as the default when the proto lastModifiedTime field was missing. This is inconsistent with _createOrchestrationStateFromProto(), which uses new Date(0) (Unix epoch) for missing timestamps. The current-time default makes it impossible for callers to distinguish between 'entity was just modified' and 'entity has no known modification time'. Changed the default to new Date(0) for consistency and correctness. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- packages/durabletask-js/src/client/client.ts | 2 +- .../durabletask-js/test/entity-client.spec.ts | 33 +++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/packages/durabletask-js/src/client/client.ts b/packages/durabletask-js/src/client/client.ts index d0be02e..0dea458 100644 --- a/packages/durabletask-js/src/client/client.ts +++ b/packages/durabletask-js/src/client/client.ts @@ -1205,7 +1205,7 @@ export class TaskHubGrpcClient { const instanceIdStr = protoMetadata.getInstanceid(); const entityId = EntityInstanceId.fromString(instanceIdStr); - const lastModifiedTime = protoMetadata.getLastmodifiedtime()?.toDate() ?? new Date(); + const lastModifiedTime = protoMetadata.getLastmodifiedtime()?.toDate() ?? new Date(0); const backlogQueueSize = protoMetadata.getBacklogqueuesize(); const lockedBy = protoMetadata.getLockedby()?.getValue(); const serializedState = protoMetadata.getSerializedstate()?.getValue(); diff --git a/packages/durabletask-js/test/entity-client.spec.ts b/packages/durabletask-js/test/entity-client.spec.ts index e77b46a..bb33044 100644 --- a/packages/durabletask-js/test/entity-client.spec.ts +++ b/packages/durabletask-js/test/entity-client.spec.ts @@ -287,6 +287,39 @@ describe("Entity Client Proto Conversion", () => { expect(metadata.getSerializedstate()).toBeUndefined(); }); + it("should default lastModifiedTime to epoch when missing from proto", () => { + // Arrange - proto metadata without lastModifiedTime set + const metadata = new pb.EntityMetadata(); + metadata.setInstanceid("@counter@test"); + metadata.setBacklogqueuesize(0); + // No lastModifiedTime set + + // Act - replicate the same logic as convertEntityMetadata + const lastModifiedTime = metadata.getLastmodifiedtime()?.toDate() ?? new Date(0); + + // Assert - should default to epoch (not current time) for consistency + // with OrchestrationState's createdAt/lastUpdatedAt defaults + expect(lastModifiedTime.getTime()).toBe(0); + }); + + it("should use actual timestamp when lastModifiedTime is present in proto", () => { + // Arrange + const metadata = new pb.EntityMetadata(); + metadata.setInstanceid("@counter@test"); + metadata.setBacklogqueuesize(0); + + const expectedDate = new Date("2026-03-15T10:30:00Z"); + const ts = new Timestamp(); + ts.fromDate(expectedDate); + metadata.setLastmodifiedtime(ts); + + // Act - replicate the same logic as convertEntityMetadata + const lastModifiedTime = metadata.getLastmodifiedtime()?.toDate() ?? new Date(0); + + // Assert - should use the actual timestamp + expect(lastModifiedTime.toISOString()).toBe(expectedDate.toISOString()); + }); + it("should gracefully handle invalid JSON in serialized state", () => { // Arrange - simulate what convertEntityMetadata does const metadata = new pb.EntityMetadata(); From 31f7d3bb17b4f41aa511679bea78e8f4f5e9a075 Mon Sep 17 00:00:00 2001 From: wangbill Date: Fri, 12 Jun 2026 11:18:54 -0700 Subject: [PATCH 2/2] test: exercise entity metadata conversion Update entity metadata timestamp tests to call TaskHubGrpcClient.convertEntityMetadata instead of duplicating the fallback expression in the test. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../durabletask-js/test/entity-client.spec.ts | 23 ++++++++++++++----- 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/packages/durabletask-js/test/entity-client.spec.ts b/packages/durabletask-js/test/entity-client.spec.ts index bb33044..b6dbfa6 100644 --- a/packages/durabletask-js/test/entity-client.spec.ts +++ b/packages/durabletask-js/test/entity-client.spec.ts @@ -3,10 +3,21 @@ import { EntityInstanceId } from "../src/entities/entity-instance-id"; import { EntityQuery } from "../src/entities/entity-query"; +import { EntityMetadata } from "../src/entities/entity-metadata"; +import { TaskHubGrpcClient } from "../src/client/client"; import * as pb from "../src/proto/orchestrator_service_pb"; import { Timestamp } from "google-protobuf/google/protobuf/timestamp_pb"; import { StringValue, Int32Value } from "google-protobuf/google/protobuf/wrappers_pb"; +type EntityMetadataConverter = { + convertEntityMetadata(protoMetadata: pb.EntityMetadata, includeState: boolean): EntityMetadata; +}; + +function convertEntityMetadata(metadata: pb.EntityMetadata, includeState = false): EntityMetadata { + const client = new TaskHubGrpcClient({ hostAddress: "localhost:4001" }); + return (client as unknown as EntityMetadataConverter).convertEntityMetadata(metadata, includeState); +} + // Note: These are unit tests for the entity client methods. // They test the proto request/response conversion logic. // Integration tests with actual gRPC calls are in e2e tests. @@ -294,12 +305,12 @@ describe("Entity Client Proto Conversion", () => { metadata.setBacklogqueuesize(0); // No lastModifiedTime set - // Act - replicate the same logic as convertEntityMetadata - const lastModifiedTime = metadata.getLastmodifiedtime()?.toDate() ?? new Date(0); + // Act + const result = convertEntityMetadata(metadata); // Assert - should default to epoch (not current time) for consistency // with OrchestrationState's createdAt/lastUpdatedAt defaults - expect(lastModifiedTime.getTime()).toBe(0); + expect(result.lastModifiedTime.getTime()).toBe(0); }); it("should use actual timestamp when lastModifiedTime is present in proto", () => { @@ -313,11 +324,11 @@ describe("Entity Client Proto Conversion", () => { ts.fromDate(expectedDate); metadata.setLastmodifiedtime(ts); - // Act - replicate the same logic as convertEntityMetadata - const lastModifiedTime = metadata.getLastmodifiedtime()?.toDate() ?? new Date(0); + // Act + const result = convertEntityMetadata(metadata); // Assert - should use the actual timestamp - expect(lastModifiedTime.toISOString()).toBe(expectedDate.toISOString()); + expect(result.lastModifiedTime.toISOString()).toBe(expectedDate.toISOString()); }); it("should gracefully handle invalid JSON in serialized state", () => {