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..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. @@ -287,6 +298,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 + const result = convertEntityMetadata(metadata); + + // Assert - should default to epoch (not current time) for consistency + // with OrchestrationState's createdAt/lastUpdatedAt defaults + expect(result.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 + const result = convertEntityMetadata(metadata); + + // Assert - should use the actual timestamp + expect(result.lastModifiedTime.toISOString()).toBe(expectedDate.toISOString()); + }); + it("should gracefully handle invalid JSON in serialized state", () => { // Arrange - simulate what convertEntityMetadata does const metadata = new pb.EntityMetadata();