|
3 | 3 | using System.Text.Json.Nodes; |
4 | 4 | using System.Text.RegularExpressions; |
5 | 5 | using SIL.LCModel; |
| 6 | +using SIL.LCModel.Core.Text; |
6 | 7 | using SIL.LCModel.Infrastructure; |
7 | 8 | using SIL.Motif.Commands; |
8 | 9 | using SIL.Motif.Commands.Baselines; |
@@ -457,6 +458,11 @@ public async Task DetailedMemberIdsAreAcceptedAsExpectedMembersByNaturalClassEdi |
457 | 458 | Guid phonemeGuid = default; |
458 | 459 | Guid otherPhonemeGuid = default; |
459 | 460 | Guid naturalClassGuid = default; |
| 461 | + Guid duplicateClassGuid = default; |
| 462 | + Guid environmentGuid = default; |
| 463 | + Guid duplicateEnvironmentGuid = default; |
| 464 | + var suggestionGuids = new List<Guid>(); |
| 465 | + var baselineEnvironmentCount = 0; |
460 | 466 | using var project = new WalkthroughProject(pristine, configure: (cache, _, _) => |
461 | 467 | { |
462 | 468 | NonUndoableUnitOfWorkHelper.Do(cache.ActionHandlerAccessor, () => |
@@ -488,9 +494,38 @@ public async Task DetailedMemberIdsAreAcceptedAsExpectedMembersByNaturalClassEdi |
488 | 494 | naturalClass.Abbreviation.set_String(cache.DefaultAnalWs, "N"); |
489 | 495 | naturalClass.SegmentsRC.Add(phoneme); |
490 | 496 | naturalClass.SegmentsRC.Add(otherPhoneme); |
| 497 | + var duplicateClass = services.GetInstance<IPhNCSegmentsFactory>().Create(); |
| 498 | + phonology.NaturalClassesOS.Add(duplicateClass); |
| 499 | + duplicateClass.Name.set_String(cache.DefaultAnalWs, "nasals"); |
| 500 | + duplicateClass.Abbreviation.set_String(cache.DefaultAnalWs, "N2"); |
| 501 | + duplicateClass.SegmentsRC.Add(phoneme); |
| 502 | + var environment = services.GetInstance<IPhEnvironmentFactory>().Create(); |
| 503 | + phonology.EnvironmentsOS.Add(environment); |
| 504 | + environment.Name.set_String(cache.DefaultAnalWs, "after nasal"); |
| 505 | + environment.StringRepresentation = TsStringUtils.MakeString("/ [N] _", cache.DefaultVernWs); |
| 506 | + var duplicateEnvironment = services.GetInstance<IPhEnvironmentFactory>().Create(); |
| 507 | + phonology.EnvironmentsOS.Add(duplicateEnvironment); |
| 508 | + duplicateEnvironment.Name.set_String(cache.DefaultAnalWs, "after nasal"); |
| 509 | + duplicateEnvironment.StringRepresentation = TsStringUtils.MakeString("/ [N] _", cache.DefaultVernWs); |
| 510 | + var namedEnvironment = services.GetInstance<IPhEnvironmentFactory>().Create(); |
| 511 | + phonology.EnvironmentsOS.Add(namedEnvironment); |
| 512 | + namedEnvironment.Name.set_String(cache.DefaultAnalWs, "after a nasal"); |
| 513 | + namedEnvironment.StringRepresentation = TsStringUtils.MakeString("/ [N] _", cache.DefaultVernWs); |
| 514 | + for (var index = 0; index < 13; index++) |
| 515 | + { |
| 516 | + var suggestion = services.GetInstance<IPhEnvironmentFactory>().Create(); |
| 517 | + phonology.EnvironmentsOS.Add(suggestion); |
| 518 | + suggestion.Name.set_String(cache.DefaultAnalWs, "zzz candidate"); |
| 519 | + suggestion.StringRepresentation = TsStringUtils.MakeString("/ [N] _", cache.DefaultVernWs); |
| 520 | + suggestionGuids.Add(suggestion.Guid); |
| 521 | + } |
| 522 | + baselineEnvironmentCount = phonology.EnvironmentsOS.Count; |
491 | 523 | phonemeGuid = phoneme.Guid; |
492 | 524 | otherPhonemeGuid = otherPhoneme.Guid; |
493 | 525 | naturalClassGuid = naturalClass.Guid; |
| 526 | + duplicateClassGuid = duplicateClass.Guid; |
| 527 | + environmentGuid = environment.Guid; |
| 528 | + duplicateEnvironmentGuid = duplicateEnvironment.Guid; |
494 | 529 | }); |
495 | 530 | }); |
496 | 531 | var managedRoot = RunnerOptions.ResolveRoot(); |
@@ -571,6 +606,70 @@ public async Task DetailedMemberIdsAreAcceptedAsExpectedMembersByNaturalClassEdi |
571 | 606 | ("expectedMembers", memberIds), ("members", new[] { memberId })), CancellationToken.None); |
572 | 607 | Assert.False(namedEdit.IsError ?? false, namedEdit.StructuredContent?.GetRawText()); |
573 | 608 | Assert.Equal(1, namedEdit.StructuredContent!.Value.GetProperty("result").GetProperty("operationCount").GetInt32()); |
| 609 | + |
| 610 | + var start = await MotifMcpServer.CallAsync(editContext, experimentalTools, "motif_start_proposal", |
| 611 | + Arguments(("project", project.FwDataPath), ("draft", "named-references"), ("label", "Use named references"), |
| 612 | + ("rationale", "Check explicit sound references.")), CancellationToken.None); |
| 613 | + Assert.False(start.IsError ?? false, start.StructuredContent?.GetRawText()); |
| 614 | + async Task<ModelContextProtocol.Protocol.CallToolResult> AddEnvironment(string reference, string name) => |
| 615 | + await MotifMcpServer.CallAsync(context, grammarTools, "motif_add_environment", |
| 616 | + Arguments(("project", project.FwDataPath), ("draft", "named-references"), ("name", name), |
| 617 | + ("left", new[] { new { naturalClass = reference } }), ("right", Array.Empty<object>())), CancellationToken.None); |
| 618 | + var missingClass = await AddEnvironment("absent", "missing class"); |
| 619 | + Assert.True(missingClass.IsError); |
| 620 | + Assert.Equal("draft.invalid", missingClass.StructuredContent!.Value.GetProperty("code").GetString()); |
| 621 | + Assert.Contains(CanonicalId.FromGuid(naturalClassGuid).Value, missingClass.StructuredContent.Value.GetProperty("message").GetString()); |
| 622 | + var ambiguousClass = await AddEnvironment("nasals", "ambiguous class"); |
| 623 | + Assert.True(ambiguousClass.IsError); |
| 624 | + var classMessage = ambiguousClass.StructuredContent!.Value.GetProperty("message").GetString()!; |
| 625 | + Assert.Contains("ambiguous", classMessage); |
| 626 | + Assert.Contains(CanonicalId.FromGuid(naturalClassGuid).Value, classMessage); |
| 627 | + Assert.Contains(CanonicalId.FromGuid(duplicateClassGuid).Value, classMessage); |
| 628 | + Assert.False((await AddEnvironment("N", "named class")).IsError ?? false); |
| 629 | + Assert.False((await AddEnvironment(CanonicalId.FromGuid(naturalClassGuid).Value, "identified class")).IsError ?? false); |
| 630 | + |
| 631 | + var replacement = await MotifMcpServer.CallAsync(context, grammarTools, "motif_add_natural_class", |
| 632 | + Arguments(("project", project.FwDataPath), ("draft", "named-references"), ("name", "replacement nasals"), |
| 633 | + ("abbreviation", "RN"), ("members", new[] { memberId })), CancellationToken.None); |
| 634 | + Assert.False(replacement.IsError ?? false, replacement.StructuredContent?.GetRawText()); |
| 635 | + var creation = replacement.StructuredContent!.Value.GetProperty("result").GetProperty("operations") |
| 636 | + .EnumerateArray().Single(operation => operation.GetProperty("kind").GetString()!.EndsWith("/createNaturalClasses")); |
| 637 | + var namedClass = await AddEnvironment("replacement nasals", "after replacement"); |
| 638 | + Assert.False(namedClass.IsError ?? false, namedClass.StructuredContent?.GetRawText()); |
| 639 | + async Task<ModelContextProtocol.Protocol.CallToolResult> Relink(string target) => |
| 640 | + await MotifMcpServer.CallAsync(editContext, experimentalTools, "motif_relink_natural_class", |
| 641 | + Arguments(("project", project.FwDataPath), ("draft", "named-references"), ("source", "N"), |
| 642 | + ("replacement", "replacement nasals"), ("replacementCreationOperation", creation.GetProperty("operationId").GetString()), |
| 643 | + ("environments", new[] { new { target, expectedLeft = new[] { new { naturalClass = "N" } }, expectedRight = Array.Empty<object>() } }), |
| 644 | + ("ruleContexts", Array.Empty<string>())), CancellationToken.None); |
| 645 | + var missingEnvironment = await Relink("absent"); |
| 646 | + Assert.True(missingEnvironment.IsError); |
| 647 | + Assert.Equal("draft.invalid", missingEnvironment.StructuredContent!.Value.GetProperty("code").GetString()); |
| 648 | + var missingMessage = missingEnvironment.StructuredContent.Value.GetProperty("message").GetString()!; |
| 649 | + Assert.Contains(CanonicalId.FromGuid(environmentGuid).Value, missingMessage); |
| 650 | + Assert.Equal(12, Regex.Matches(missingMessage, @"\[[A-Za-z0-9_-]{22}\]").Count); |
| 651 | + Assert.Contains($"Total: {baselineEnvironmentCount + 3}", missingMessage); |
| 652 | + Assert.Contains("motif_grammar(kind=environments)", missingMessage); |
| 653 | + var suggestions = await Relink("zzz"); |
| 654 | + Assert.True(suggestions.IsError); |
| 655 | + var suggestionMessage = suggestions.StructuredContent!.Value.GetProperty("message").GetString()!; |
| 656 | + Assert.All(suggestionGuids, guid => Assert.Contains(CanonicalId.FromGuid(guid).Value, suggestionMessage)); |
| 657 | + Assert.DoesNotContain("Total:", suggestionMessage); |
| 658 | + var manyAmbiguous = await Relink("zzz candidate"); |
| 659 | + Assert.True(manyAmbiguous.IsError); |
| 660 | + var manyMessage = manyAmbiguous.StructuredContent!.Value.GetProperty("message").GetString()!; |
| 661 | + Assert.Contains("ambiguous", manyMessage); |
| 662 | + Assert.All(suggestionGuids, guid => Assert.Contains(CanonicalId.FromGuid(guid).Value, manyMessage)); |
| 663 | + var ambiguousEnvironment = await Relink("after nasal"); |
| 664 | + Assert.True(ambiguousEnvironment.IsError); |
| 665 | + var environmentMessage = ambiguousEnvironment.StructuredContent!.Value.GetProperty("message").GetString()!; |
| 666 | + Assert.Contains("ambiguous", environmentMessage); |
| 667 | + Assert.Contains(CanonicalId.FromGuid(environmentGuid).Value, environmentMessage); |
| 668 | + Assert.Contains(CanonicalId.FromGuid(duplicateEnvironmentGuid).Value, environmentMessage); |
| 669 | + var namedRelink = await Relink("AFTER A NASAL"); |
| 670 | + Assert.False(namedRelink.IsError ?? false, namedRelink.StructuredContent?.GetRawText()); |
| 671 | + var identifiedRelink = await Relink(CanonicalId.FromGuid(environmentGuid).Value); |
| 672 | + Assert.False(identifiedRelink.IsError ?? false, identifiedRelink.StructuredContent?.GetRawText()); |
574 | 673 | } |
575 | 674 |
|
576 | 675 | private static Dictionary<string, JsonElement> Arguments(params (string Name, object? Value)[] values) => |
|
0 commit comments