@@ -384,6 +384,31 @@ public void ToWorkflow_ReasonPrefixKeyIgnored()
384384 Assert . Equal ( string . Empty , link . ToWorkflow ( ) . Reason ) ;
385385 }
386386
387+ [ Fact ]
388+ public void ToWorkflow_ReasonKeyLookupIsCaseSensitive ( )
389+ {
390+ // Query params are held in a dictionary with the default ordinal comparer, so "Reason" is a
391+ // different param than "reason" and does not populate the field.
392+ var link = new NexusLink (
393+ new Uri ( "temporal:///namespaces/ns/workflows/wf-id/run-id?Reason=nope" ) ,
394+ Api . Common . V1 . Link . Types . Workflow . Descriptor . FullName ) ;
395+
396+ Assert . Equal ( string . Empty , link . ToWorkflow ( ) . Reason ) ;
397+ }
398+
399+ [ Fact ]
400+ public void ToWorkflow_RepeatedReasonKey_Throws ( )
401+ {
402+ // A repeated key cannot go into the dictionary, so it surfaces as the same ArgumentException
403+ // as any other malformed link. Callers converting links catch that and drop the link with a
404+ // warning rather than failing the operation.
405+ var link = new NexusLink (
406+ new Uri ( "temporal:///namespaces/ns/workflows/wf-id/run-id?reason=a&reason=b" ) ,
407+ Api . Common . V1 . Link . Types . Workflow . Descriptor . FullName ) ;
408+
409+ Assert . Throws < ArgumentException > ( ( ) => link . ToWorkflow ( ) ) ;
410+ }
411+
387412 [ Fact ]
388413 public void ToWorkflow_LiteralPlusInPathIsPreserved ( )
389414 {
@@ -430,6 +455,8 @@ public void ToProtoLink_WorkflowShape_RoundTrips()
430455 [ Fact ]
431456 public void ProtoToNexusLink_WorkflowVariant_Dispatches ( )
432457 {
458+ // The workflow variant is what a link carries when there is no history event to point at,
459+ // e.g. a Query or a rejected update.
433460 var protoLink = new Api . Common . V1 . Link
434461 {
435462 Workflow = new ( ) { Namespace = "ns" , WorkflowId = "wf" , RunId = "run" } ,
@@ -500,11 +527,12 @@ public void ToWorkflowEvent_FormDecodesQueryValues()
500527 [ Fact ]
501528 public void ToWorkflow_AcceptsEmptyRunId ( )
502529 {
503- // Characterization, not a statement of intent. A trailing slash still yields the expected
504- // segment count, so the run ID comes back empty rather than being rejected. The same
505- // leniency exists in the workflow-event, activity, and nexus-operation converters, since
506- // they share this path parsing, so tightening it is a decision about all four rather than
507- // about this converter.
530+ // A trailing slash still yields the expected segment count, so the run ID comes back empty
531+ // rather than being rejected. The workflow-event, activity, and nexus-operation converters
532+ // are equally lenient because all four share this path parsing, so tightening it is a
533+ // decision about all four and belongs with that change rather than this one. This test
534+ // exists so that whichever way it is decided, the behavior changes visibly instead of
535+ // silently.
508536 var link = new NexusLink (
509537 new Uri ( "temporal:///namespaces/ns/workflows/wf-id/" ) ,
510538 Api . Common . V1 . Link . Types . Workflow . Descriptor . FullName ) ;
@@ -515,7 +543,7 @@ public void ToWorkflow_AcceptsEmptyRunId()
515543 [ Fact ]
516544 public void ToWorkflow_AcceptsEmptyNamespace ( )
517545 {
518- // Characterization ; see ToWorkflow_AcceptsEmptyRunId .
546+ // The same shared-path-parsing leniency as ToWorkflow_AcceptsEmptyRunId ; see that test .
519547 var link = new NexusLink (
520548 new Uri ( "temporal:///namespaces//workflows/wf-id/run-id" ) ,
521549 Api . Common . V1 . Link . Types . Workflow . Descriptor . FullName ) ;
0 commit comments