-
-
Notifications
You must be signed in to change notification settings - Fork 85
Develop #474
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Develop #474
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -22,21 +22,133 @@ namespace Resgrid.Services | |||||||||||||||||||||
| /// </summary> | ||||||||||||||||||||||
| public static class DispatchVoicePromptBuilder | ||||||||||||||||||||||
| { | ||||||||||||||||||||||
| // Ordinal wording for re-dispatches ("Second alarm"). Index = dispatch count. | ||||||||||||||||||||||
| private static readonly string[] AlarmOrdinals = | ||||||||||||||||||||||
| { | ||||||||||||||||||||||
| null, null, "Second", "Third", "Fourth", "Fifth", "Sixth", "Seventh", "Eighth", "Ninth" | ||||||||||||||||||||||
| }; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // Trailing address segments that carry no value over the phone. Country names | ||||||||||||||||||||||
| // and state/zip segments are stripped from the END only, so freeform locations | ||||||||||||||||||||||
| // ("Building 5, Floor 3, Room 10") are never touched. | ||||||||||||||||||||||
| private static readonly HashSet<string> CountryNames = new(StringComparer.OrdinalIgnoreCase) | ||||||||||||||||||||||
| { | ||||||||||||||||||||||
| "USA", "U.S.A.", "US", "U.S.", "United States", "United States of America", "Canada" | ||||||||||||||||||||||
| }; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| private static readonly HashSet<string> StateNames = new(StringComparer.OrdinalIgnoreCase) | ||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Address trimming gap in Core/Resgrid.Services/DispatchVoicePromptBuilder.cs: private static readonly HashSet<string> StateNames = new(StringComparer.OrdinalIgnoreCase)
{
// U.S. states/territories...
"Puerto Rico", "Guam",
// Canadian provinces/territories
"Alberta", "British Columbia", "Manitoba", "New Brunswick", "Newfoundland and Labrador",
"Nova Scotia", "Ontario", "Prince Edward Island", "Quebec", "Saskatchewan",
"Northwest Territories", "Nunavut", "Yukon"
};
private static readonly HashSet<string> StateAbbreviations = new(StringComparer.Ordinal)
{
// U.S. states/territories...
"DC", "PR", "GU",
// Canadian provinces/territories
"AB", "BC", "MB", "NB", "NL", "NS", "ON", "PE", "QC", "SK", "NT", "NU", "YT"
};Prompt for LLMTalk to Kody by mentioning @kody Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction. |
||||||||||||||||||||||
| { | ||||||||||||||||||||||
| "Alabama", "Alaska", "Arizona", "Arkansas", "California", "Colorado", "Connecticut", "Delaware", | ||||||||||||||||||||||
| "Florida", "Georgia", "Hawaii", "Idaho", "Illinois", "Indiana", "Iowa", "Kansas", "Kentucky", | ||||||||||||||||||||||
| "Louisiana", "Maine", "Maryland", "Massachusetts", "Michigan", "Minnesota", "Mississippi", | ||||||||||||||||||||||
| "Missouri", "Montana", "Nebraska", "Nevada", "New Hampshire", "New Jersey", "New Mexico", | ||||||||||||||||||||||
| "New York", "North Carolina", "North Dakota", "Ohio", "Oklahoma", "Oregon", "Pennsylvania", | ||||||||||||||||||||||
| "Rhode Island", "South Carolina", "South Dakota", "Tennessee", "Texas", "Utah", "Vermont", | ||||||||||||||||||||||
| "Virginia", "Washington", "West Virginia", "Wisconsin", "Wyoming", "District of Columbia", | ||||||||||||||||||||||
| "Puerto Rico", "Guam", | ||||||||||||||||||||||
| // Canadian provinces/territories | ||||||||||||||||||||||
| "Alberta", "British Columbia", "Manitoba", "New Brunswick", "Newfoundland and Labrador", | ||||||||||||||||||||||
| "Nova Scotia", "Ontario", "Prince Edward Island", "Quebec", "Saskatchewan", | ||||||||||||||||||||||
| "Northwest Territories", "Nunavut", "Yukon" | ||||||||||||||||||||||
| }; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // Two-letter state codes match case-sensitively: CAD feeds emit them upper-case, | ||||||||||||||||||||||
| // and a case-insensitive match would eat street segments like "La" or "In". | ||||||||||||||||||||||
| private static readonly HashSet<string> StateAbbreviations = new(StringComparer.Ordinal) | ||||||||||||||||||||||
| { | ||||||||||||||||||||||
| "AL", "AK", "AZ", "AR", "CA", "CO", "CT", "DE", "FL", "GA", "HI", "ID", "IL", "IN", "IA", | ||||||||||||||||||||||
| "KS", "KY", "LA", "ME", "MD", "MA", "MI", "MN", "MS", "MO", "MT", "NE", "NV", "NH", "NJ", | ||||||||||||||||||||||
| "NM", "NY", "NC", "ND", "OH", "OK", "OR", "PA", "RI", "SC", "SD", "TN", "TX", "UT", "VT", | ||||||||||||||||||||||
| "VA", "WA", "WV", "WI", "WY", "DC", "PR", "GU", | ||||||||||||||||||||||
| // Canadian provinces/territories | ||||||||||||||||||||||
| "AB", "BC", "MB", "NB", "NL", "NS", "ON", "PE", "QC", "SK", "NT", "NU", "YT" | ||||||||||||||||||||||
| }; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| private static readonly Regex PostalCodeRegex = new(@"^(\d{5}(-\d{4})?|[A-Za-z]\d[A-Za-z]\s?\d[A-Za-z]\d)$", RegexOptions.Compiled | RegexOptions.CultureInvariant); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| public static string BuildDispatchPrompt(Call call, string address) | ||||||||||||||||||||||
| { | ||||||||||||||||||||||
| // Periods between the segments give the TTS engine sentence boundaries | ||||||||||||||||||||||
| // (Piper inserts 0.35s of silence per sentence), which keeps the priority, | ||||||||||||||||||||||
| // address and nature audibly separated instead of running together. | ||||||||||||||||||||||
| // (Piper inserts 0.35s of silence per sentence), which keeps the alarm | ||||||||||||||||||||||
| // intro, nature, address and priority audibly separated. | ||||||||||||||||||||||
| var nature = StringHelpers.StripHtmlTagsCharArray(call.NatureOfCall); | ||||||||||||||||||||||
| var prompt = !String.IsNullOrWhiteSpace(address) | ||||||||||||||||||||||
| ? string.Format("{0}, Priority {1}. Address {2}. Nature {3}", call.Name, call.GetPriorityText(), address, nature) | ||||||||||||||||||||||
| : string.Format("{0}, Priority {1}. Nature {2}", call.Name, call.GetPriorityText(), nature); | ||||||||||||||||||||||
| var intro = BuildAlarmIntro(call.DispatchCount); | ||||||||||||||||||||||
| var spokenAddress = TrimAddressForSpeech(address); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| string prompt; | ||||||||||||||||||||||
| if (!String.IsNullOrWhiteSpace(spokenAddress)) | ||||||||||||||||||||||
| { | ||||||||||||||||||||||
| // "Address" for street-style values (leading house number), "Location" for | ||||||||||||||||||||||
| // freeform values ("Building 5, Floor 3", "Bottom of Bucks Canyon"). | ||||||||||||||||||||||
| var placeLabel = char.IsDigit(spokenAddress.TrimStart()[0]) ? "Address" : "Location"; | ||||||||||||||||||||||
| prompt = $"{intro}, {call.Name}. Nature, {nature}. {placeLabel}, {spokenAddress}. Priority, {call.GetPriorityText()}"; | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| else | ||||||||||||||||||||||
| { | ||||||||||||||||||||||
| prompt = $"{intro}, {call.Name}. Nature, {nature}. Priority, {call.GetPriorityText()}"; | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| return prompt.EndsWith(".", StringComparison.Ordinal) || prompt.EndsWith("!", StringComparison.Ordinal) || prompt.EndsWith("?", StringComparison.Ordinal) | ||||||||||||||||||||||
| ? prompt | ||||||||||||||||||||||
| : $"{prompt}."; | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| /// <summary> | ||||||||||||||||||||||
| /// "New call" for a first dispatch, "Second alarm" / "Third alarm" / ... for | ||||||||||||||||||||||
| /// re-dispatches. DispatchCount is 0 for departments whose dispatch path never | ||||||||||||||||||||||
| /// increments it, and 1 after the first send — both mean a first dispatch. | ||||||||||||||||||||||
| /// </summary> | ||||||||||||||||||||||
| private static string BuildAlarmIntro(int dispatchCount) | ||||||||||||||||||||||
| { | ||||||||||||||||||||||
| if (dispatchCount <= 1) | ||||||||||||||||||||||
| return "New call"; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| return dispatchCount < AlarmOrdinals.Length | ||||||||||||||||||||||
| ? $"{AlarmOrdinals[dispatchCount]} alarm" | ||||||||||||||||||||||
| : $"Alarm {dispatchCount}"; | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| /// <summary> | ||||||||||||||||||||||
| /// Drops trailing state, postal code and country segments from a comma-separated | ||||||||||||||||||||||
| /// postal address so the spoken prompt stays short ("123 Main St, Springfield, WA | ||||||||||||||||||||||
| /// 98111, USA" becomes "123 Main St, Springfield"). Trimming walks from the end | ||||||||||||||||||||||
| /// and stops at the first segment that isn't a state/zip/country, so freeform | ||||||||||||||||||||||
| /// locations pass through untouched. At least one segment is always kept. | ||||||||||||||||||||||
| /// </summary> | ||||||||||||||||||||||
| public static string TrimAddressForSpeech(string address) | ||||||||||||||||||||||
| { | ||||||||||||||||||||||
| if (String.IsNullOrWhiteSpace(address)) | ||||||||||||||||||||||
| return address; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| var segments = address.Split(',') | ||||||||||||||||||||||
| .Select(segment => segment.Trim()) | ||||||||||||||||||||||
| .Where(segment => segment.Length > 0) | ||||||||||||||||||||||
| .ToList(); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| while (segments.Count > 1 && IsDroppableTrailingSegment(segments[^1])) | ||||||||||||||||||||||
| { | ||||||||||||||||||||||
| segments.RemoveAt(segments.Count - 1); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
Comment on lines
+128
to
+131
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Keep the city when its name matches a state name.
Raise the floor to two segments. The documented example still trims correctly: 🐛 Proposed fix- while (segments.Count > 1 && IsDroppableTrailingSegment(segments[^1]))
+ // Keep at least a street and one more segment: a city named after a state
+ // ("New York", "Washington") classifies as droppable and would otherwise be lost.
+ while (segments.Count > 2 && IsDroppableTrailingSegment(segments[^1]))
{
segments.RemoveAt(segments.Count - 1);
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||
|
|
||||||||||||||||||||||
| return string.Join(", ", segments); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| private static bool IsDroppableTrailingSegment(string segment) | ||||||||||||||||||||||
| { | ||||||||||||||||||||||
| if (CountryNames.Contains(segment) || PostalCodeRegex.IsMatch(segment)) | ||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Denial-of-service risk in regex processing: Prompt for LLMTalk to Kody by mentioning @kody Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction. |
||||||||||||||||||||||
| return true; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // "WA", "Washington", "WA 98111" or "Washington 98111". | ||||||||||||||||||||||
| var lastSpaceIndex = segment.LastIndexOf(' '); | ||||||||||||||||||||||
| var head = lastSpaceIndex > 0 ? segment[..lastSpaceIndex].Trim() : segment; | ||||||||||||||||||||||
| var tail = lastSpaceIndex > 0 ? segment[(lastSpaceIndex + 1)..] : null; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| if (tail != null && !PostalCodeRegex.IsMatch(tail)) | ||||||||||||||||||||||
| return StateAbbreviations.Contains(segment) || StateNames.Contains(segment); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| return StateAbbreviations.Contains(head) || StateNames.Contains(head); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| public static async Task<string> ResolveDispatchAddressAsync(Call call, IGeoLocationProvider geoLocationProvider, CancellationToken cancellationToken = default) | ||||||||||||||||||||||
| { | ||||||||||||||||||||||
| var address = call.Address; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,88 @@ | ||
| using System; | ||
| using FluentAssertions; | ||
| using Newtonsoft.Json; | ||
| using NUnit.Framework; | ||
| using Resgrid.Web.Services.Helpers; | ||
|
|
||
| namespace Resgrid.Tests.Web | ||
| { | ||
| /// <summary> | ||
| /// TEMPORARY compatibility shim (RG-T132): deployed app builds parse CallResult.LoggedOnUtc as | ||
| /// device-local time and offset "now" to compensate, so the wire value must stay zone-less until | ||
| /// the fixed apps are rolled out. These tests pin that contract: writes carry no "Z", reads accept | ||
| /// both the zone-less and the "Z" form as the same UTC instant. Delete alongside the converter. | ||
| /// </summary> | ||
| [TestFixture] | ||
| public class LegacyZonelessUtcDateTimeConverterTests | ||
| { | ||
| private static readonly DateTime ExpectedUtc = new DateTime(2026, 8, 12, 13, 5, 22, 123, DateTimeKind.Utc); | ||
|
|
||
| [TestCase(DateTimeKind.Unspecified, TestName = "An Unspecified instant serialises zone-less")] | ||
| [TestCase(DateTimeKind.Utc, TestName = "A UTC instant serialises zone-less")] | ||
| public void Serialize_WritesTheInstantWithoutAZoneMarker(DateTimeKind kind) | ||
| { | ||
| // Arrange | ||
| var original = new LegacyTimestampPayload | ||
| { | ||
| Timestamp = DateTime.SpecifyKind(new DateTime(2026, 8, 12, 13, 5, 22, 123), kind) | ||
| }; | ||
|
|
||
| // Act | ||
| var serialized = JsonConvert.SerializeObject(original); | ||
|
|
||
| // Assert | ||
| serialized.Should().Contain("2026-08-12T13:05:22.123"); | ||
| serialized.Should().NotContain("2026-08-12T13:05:22.123Z"); | ||
| } | ||
|
|
||
| [Test] | ||
| public void Serialize_WithALocalInstant_WritesTheUtcWallTimeZoneless() | ||
| { | ||
| // Arrange -- a Local value means the process timezone leaked in somewhere upstream. | ||
| var original = new LegacyTimestampPayload { Timestamp = ExpectedUtc.ToLocalTime() }; | ||
|
|
||
| // Act | ||
| var serialized = JsonConvert.SerializeObject(original); | ||
|
|
||
| // Assert | ||
| serialized.Should().Contain("2026-08-12T13:05:22.123"); | ||
| serialized.Should().NotContain("2026-08-12T13:05:22.123Z"); | ||
| } | ||
|
|
||
| [TestCase("2026-08-12T13:05:22.123", TestName = "Zone-less string reads back as the same UTC instant")] | ||
| [TestCase("2026-08-12T13:05:22.123Z", TestName = "String with Z reads back as the same UTC instant")] | ||
| [TestCase("2026-08-12T06:05:22.123-07:00", TestName = "String with an offset is converted to UTC")] | ||
| public void ReadJson_AcceptsBothFormats(string serialized) | ||
| { | ||
| // Arrange -- DateParseHandling.None forces the reader to hand over a JsonToken.String. | ||
| var settings = new JsonSerializerSettings { DateParseHandling = DateParseHandling.None }; | ||
|
|
||
| // Act | ||
| var payload = JsonConvert.DeserializeObject<LegacyTimestampPayload>($"{{\"Timestamp\":\"{serialized}\"}}", settings); | ||
|
|
||
| // Assert | ||
| payload.Timestamp.Kind.Should().Be(DateTimeKind.Utc); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Null dereference risk in Kody rule violation: Add null checks before accessing properties payload?.Timestamp.Kind.Should().Be(DateTimeKind.Utc);Prompt for LLMTalk to Kody by mentioning @kody Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction. |
||
| payload.Timestamp.Should().Be(ExpectedUtc); | ||
| } | ||
|
|
||
| [Test] | ||
| public void SerializeThenDeserialize_PreservesTheInstant() | ||
| { | ||
| // Arrange | ||
| var original = new LegacyTimestampPayload { Timestamp = ExpectedUtc }; | ||
|
|
||
| // Act | ||
| var round = JsonConvert.DeserializeObject<LegacyTimestampPayload>(JsonConvert.SerializeObject(original)); | ||
|
|
||
| // Assert | ||
| round.Timestamp.Kind.Should().Be(DateTimeKind.Utc); | ||
| round.Timestamp.Should().Be(ExpectedUtc); | ||
| } | ||
|
|
||
| private class LegacyTimestampPayload | ||
| { | ||
| [JsonConverter(typeof(LegacyZonelessUtcDateTimeConverter))] | ||
| public DateTime Timestamp { get; set; } | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,107 @@ | ||
| using FluentAssertions; | ||
| using NUnit.Framework; | ||
| using Resgrid.Model; | ||
| using Resgrid.Services; | ||
|
|
||
| namespace Resgrid.Tests.Web.Services | ||
| { | ||
| [TestFixture] | ||
| public class DispatchVoicePromptBuilderTests | ||
| { | ||
| private static Call CreateCall(int dispatchCount = 0) | ||
| { | ||
| return new Call | ||
| { | ||
| Name = "Call 42", | ||
| Priority = (int)CallPriority.High, | ||
| NatureOfCall = "Structure fire", | ||
| DispatchCount = dispatchCount | ||
| }; | ||
| } | ||
|
|
||
| [Test] | ||
| public void should_announce_new_call_with_nature_then_address_then_priority() | ||
| { | ||
| DispatchVoicePromptBuilder.BuildDispatchPrompt(CreateCall(), "123 Main St") | ||
| .Should().Be("New call, Call 42. Nature, Structure fire. Address, 123 Main St. Priority, High."); | ||
| } | ||
|
|
||
| [Test] | ||
| public void should_omit_the_place_sentence_when_no_address_is_available() | ||
| { | ||
| DispatchVoicePromptBuilder.BuildDispatchPrompt(CreateCall(), null) | ||
| .Should().Be("New call, Call 42. Nature, Structure fire. Priority, High."); | ||
| } | ||
|
|
||
| [TestCase(0, "New call")] | ||
| [TestCase(1, "New call")] | ||
| [TestCase(2, "Second alarm")] | ||
| [TestCase(3, "Third alarm")] | ||
| [TestCase(4, "Fourth alarm")] | ||
| [TestCase(9, "Ninth alarm")] | ||
| [TestCase(12, "Alarm 12")] | ||
| public void should_announce_the_alarm_level_from_the_dispatch_count(int dispatchCount, string expectedIntro) | ||
| { | ||
| DispatchVoicePromptBuilder.BuildDispatchPrompt(CreateCall(dispatchCount), "123 Main St") | ||
| .Should().StartWith($"{expectedIntro}, Call 42."); | ||
| } | ||
|
|
||
| [Test] | ||
| public void should_speak_location_instead_of_address_for_freeform_places() | ||
| { | ||
| DispatchVoicePromptBuilder.BuildDispatchPrompt(CreateCall(), "At the bottom of Bucks Canyon") | ||
| .Should().Contain("Location, At the bottom of Bucks Canyon."); | ||
| } | ||
|
|
||
| [Test] | ||
| public void should_strip_html_from_the_nature_of_call() | ||
| { | ||
| var call = CreateCall(); | ||
| call.NatureOfCall = "<div>Structure fire</div>"; | ||
|
|
||
| DispatchVoicePromptBuilder.BuildDispatchPrompt(call, null) | ||
| .Should().Contain("Nature, Structure fire."); | ||
| } | ||
|
|
||
| [TestCase("123 Main St, Springfield, WA 98111, USA", "123 Main St, Springfield")] | ||
| [TestCase("123 Main St, Springfield, WA, USA", "123 Main St, Springfield")] | ||
| [TestCase("123 Main St, Springfield, Washington 98111", "123 Main St, Springfield")] | ||
| [TestCase("123 Main St, Springfield, WA", "123 Main St, Springfield")] | ||
| [TestCase("450 Elk Run Rd, Victor, Idaho, United States", "450 Elk Run Rd, Victor")] | ||
| [TestCase("1 Front St, Toronto, M5J 2X5, Canada", "1 Front St, Toronto")] | ||
| [TestCase("1 Front St, Toronto, ON, Canada", "1 Front St, Toronto")] | ||
| [TestCase("100 Main St, Whitehorse, Yukon", "100 Main St, Whitehorse")] | ||
| [TestCase("20 Water St, Charlottetown, Prince Edward Island", "20 Water St, Charlottetown")] | ||
| [TestCase("300 Portage Ave, Winnipeg, MB, Canada", "300 Portage Ave, Winnipeg")] | ||
| public void trim_address_should_drop_trailing_state_zip_and_country(string input, string expected) | ||
| { | ||
| DispatchVoicePromptBuilder.TrimAddressForSpeech(input).Should().Be(expected); | ||
| } | ||
|
|
||
| [TestCase("Building 5, Floor 3, Room 10")] | ||
| [TestCase("At the bottom of Bucks Canyon")] | ||
| [TestCase("123 Main St, Springfield")] | ||
| [TestCase("Main St and 5th Ave")] | ||
| public void trim_address_should_leave_freeform_locations_untouched(string input) | ||
| { | ||
| DispatchVoicePromptBuilder.TrimAddressForSpeech(input).Should().Be(input); | ||
| } | ||
|
|
||
| [Test] | ||
| public void trim_address_should_always_keep_at_least_one_segment() | ||
| { | ||
| DispatchVoicePromptBuilder.TrimAddressForSpeech("WA 98111").Should().Be("WA 98111"); | ||
| } | ||
|
|
||
| // A rural/unincorporated address carries no city segment, so trimming has to | ||
| // run down to the street alone — guarding the loop at two segments instead of | ||
| // one would leave the zip and state being read aloud. | ||
| [TestCase("123 Main St, WA 98111, USA", "123 Main St")] | ||
| [TestCase("450 Elk Run Rd, Idaho", "450 Elk Run Rd")] | ||
| [TestCase("1 Front St, Canada", "1 Front St")] | ||
| public void trim_address_should_trim_down_to_the_street_when_no_city_segment_exists(string input, string expected) | ||
| { | ||
| DispatchVoicePromptBuilder.TrimAddressForSpeech(input).Should().Be(expected); | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Mutability ambiguity in
Core/Resgrid.Config/TtsConfig.cs:public static bool PiperPersistentProcessEnabled = true;and the similar field atCore/Resgrid.Config/TtsConfig.cs:60-60allow reassignment at runtime despite constant semantics. Mark the fieldreadonlyorconstwhere applicable to communicate intent and prevent accidental mutation.Kody rule violation: Use `readonly` or `const` for Immutable Data
Prompt for LLM
Talk to Kody by mentioning @kody
Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.